/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 | } |