Coverage Report

Created: 2026-07-14 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmInstrumentationInterrupt.cxx
Line
Count
Source
1
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2
   file LICENSE.rst or https://cmake.org/licensing for details.  */
3
4
#if !defined(_POSIX_C_SOURCE) && !defined(_WIN32) && !defined(__sun) &&       \
5
  !defined(__OpenBSD__)
6
// POSIX APIs are needed (sigaction, sigemptyset, SA_RESETHAND).
7
// NOLINTNEXTLINE(bugprone-reserved-identifier)
8
#  define _POSIX_C_SOURCE 200809L
9
#endif
10
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__QNX__)
11
// For isascii
12
// NOLINTNEXTLINE(bugprone-reserved-identifier)
13
#  define _XOPEN_SOURCE 700
14
#endif
15
16
#include "cmInstrumentationInterrupt.h"
17
18
#include <csignal>
19
#include <cstdlib>
20
#ifdef _WIN32
21
#  include <atomic>
22
23
#  include <windows.h>
24
#else
25
#  include <cstring>
26
#endif
27
28
namespace {
29
// Flag shared between the interrupt handler and the command flow that writes
30
// the instrumentation envelope snippet.  On Windows the console control
31
// handler runs on a separate thread, so an atomic is required; on POSIX the
32
// handler runs in signal context, where only `volatile sig_atomic_t` is
33
// guaranteed safe.
34
#ifdef _WIN32
35
std::atomic<int> interruptSignal{ 0 };
36
37
BOOL WINAPI cmInstrumentationConsoleHandler(DWORD type)
38
{
39
  if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
40
    int expected = 0;
41
    interruptSignal.compare_exchange_strong(expected, SIGINT);
42
    // Return TRUE so the main thread can finish writing the snippet before the
43
    // process exits.  Child processes (native build tool, install scripts,
44
    // tests) share the console and receive the event directly, so they still
45
    // terminate and unblock our loop.
46
    return TRUE;
47
  }
48
  return FALSE;
49
}
50
#else
51
sig_atomic_t volatile interruptSignal = 0;
52
struct sigaction savedSigIntAction;
53
54
extern "C" void cmInstrumentationSignalHandler(int sig)
55
0
{
56
0
  interruptSignal = sig;
57
0
}
58
#endif
59
60
// Set when the pending interrupt was injected by the test seam below rather
61
// than delivered by the OS.  An injected interrupt must NOT be re-raised (the
62
// process exits normally after flushing the snippet), so the test stays a
63
// clean-exit, leak-checkable case on every generator.
64
bool interruptInjected = false;
65
66
// Test-only seam.  An undocumented, unsupported environment variable lets the
67
// instrumentation test suite inject an "interrupted" condition
68
// deterministically, with no real OS signal -- so the instrumentation
69
// interrupt path can be exercised on every generator and platform.  The
70
// double-underscore name marks it internal; it is never set in normal use.
71
// Mirrors CTest's internal fake-hook convention.
72
void InjectTestInterrupt()
73
0
{
74
0
  char const* value = std::getenv("__CMAKE_INSTRUMENTATION_TEST_INTERRUPT");
75
0
  if (!value) {
76
0
    return;
77
0
  }
78
0
  int sig = std::atoi(value);
79
0
  if (sig <= 0) {
80
0
    return;
81
0
  }
82
#ifdef _WIN32
83
  interruptSignal.store(sig);
84
#else
85
0
  interruptSignal = static_cast<sig_atomic_t>(sig);
86
0
#endif
87
0
  interruptInjected = true;
88
0
}
89
90
// Install the interrupt handler and clear any previously recorded signal.
91
void InstallInterruptHandler()
92
0
{
93
#ifdef _WIN32
94
  interruptSignal.store(0);
95
  SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, TRUE);
96
#else
97
0
  interruptSignal = 0;
98
0
  struct sigaction sa;
99
0
  memset(&sa, 0, sizeof(sa));
100
0
  sa.sa_handler = cmInstrumentationSignalHandler;
101
0
  sigemptyset(&sa.sa_mask);
102
  // One-shot: after the first interrupt the default disposition is restored,
103
  // so a second Ctrl+C terminates immediately even while we are mid-flush.
104
0
  sa.sa_flags = SA_RESETHAND;
105
0
  sigaction(SIGINT, &sa, &savedSigIntAction);
106
0
#endif
107
0
}
108
109
// Restore the disposition that was in effect before InstallInterruptHandler().
110
void RestoreInterruptHandler()
111
0
{
112
#ifdef _WIN32
113
  SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, FALSE);
114
#else
115
0
  sigaction(SIGINT, &savedSigIntAction, nullptr);
116
0
#endif
117
0
}
118
}
119
120
int cmInstrumentationInterrupt::PendingInterruptSignal()
121
0
{
122
#ifdef _WIN32
123
  return interruptSignal.load();
124
#else
125
0
  return static_cast<int>(interruptSignal);
126
0
#endif
127
0
}
128
129
cmInstrumentationInterrupt::InterruptOutcome
130
cmInstrumentationInterrupt::HandleInterrupt(
131
  bool active, std::function<int()> const& callback)
132
0
{
133
  // Only trap interrupts when instrumentation is active, so non-instrumented
134
  // flows keep their default signal behavior.
135
0
  if (!active) {
136
0
    return { callback(), false, 0, true };
137
0
  }
138
0
  InstallInterruptHandler();
139
0
  interruptInjected = false;
140
  // Test-only: allow the suite to inject an interrupt deterministically.
141
0
  InjectTestInterrupt();
142
0
  int ret = callback();
143
0
  int sig = PendingInterruptSignal();
144
0
  RestoreInterruptHandler();
145
  // A real OS interrupt should be re-raised so the exit status reflects it; an
146
  // injected (test) interrupt should not, so the process exits cleanly.
147
0
  return { ret, sig != 0, sig, !interruptInjected };
148
0
}
149
150
void cmInstrumentationInterrupt::RaiseInterrupt(int sig)
151
0
{
152
#ifdef _WIN32
153
  // On Windows the process exits normally after flushing; the caller
154
  // propagates the (failed) build result.
155
  static_cast<void>(sig);
156
#else
157
  // Restore the default disposition (SA_RESETHAND already did so for the first
158
  // delivery) and re-raise so the exit status reflects the interrupt.
159
  signal(sig, SIG_DFL);
160
0
  raise(sig);
161
0
#endif
162
0
}