blob: 28b8a3e01865421c7204b2526077f2f725009ed5 [file] [edit]
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#if !defined(_POSIX_C_SOURCE) && !defined(_WIN32) && !defined(__sun) && \
!defined(__OpenBSD__)
// POSIX APIs are needed (sigaction, sigemptyset, SA_RESETHAND).
// NOLINTNEXTLINE(bugprone-reserved-identifier)
# define _POSIX_C_SOURCE 200809L
#endif
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__QNX__)
// For isascii
// NOLINTNEXTLINE(bugprone-reserved-identifier)
# define _XOPEN_SOURCE 700
#endif
#include "cmInstrumentationInterrupt.h"
#include <csignal>
#include <cstdlib>
#ifdef _WIN32
# include <atomic>
# include <windows.h>
#else
# include <cstring>
#endif
namespace {
// Flag shared between the interrupt handler and the build flow that writes the
// `cmakeBuild` snippet. On Windows the console control handler runs on a
// separate thread, so an atomic is required; on POSIX the handler runs in
// signal context, where only `volatile sig_atomic_t` is guaranteed safe.
#ifdef _WIN32
std::atomic<int> buildInterruptSignal{ 0 };
BOOL WINAPI cmInstrumentationConsoleHandler(DWORD type)
{
if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
int expected = 0;
buildInterruptSignal.compare_exchange_strong(expected, SIGINT);
// Return TRUE so the main thread can finish writing the snippet before the
// process exits. The native build tool shares the console and receives
// the event directly, so it still terminates and unblocks our build loop.
return TRUE;
}
return FALSE;
}
#else
sig_atomic_t volatile buildInterruptSignal = 0;
struct sigaction savedSigIntAction;
extern "C" void cmInstrumentationSignalHandler(int sig)
{
buildInterruptSignal = sig;
}
#endif
// Set when the pending interrupt was injected by the test seam below rather
// than delivered by the OS. An injected interrupt must NOT be re-raised (the
// process exits normally after flushing the snippet), so the test stays a
// clean-exit, leak-checkable case on every generator.
bool buildInterruptInjected = false;
// Test-only seam. An undocumented, unsupported environment variable lets the
// instrumentation test suite inject a "build was interrupted" condition
// deterministically, with no real OS signal -- so the cmakeBuild interrupt
// path can be exercised on every generator and platform. The double-
// underscore name marks it internal; it is never set in normal use. Mirrors
// CTest's internal fake-hook convention.
void InjectTestInterrupt()
{
char const* value = std::getenv("__CMAKE_INSTRUMENTATION_TEST_INTERRUPT");
if (!value) {
return;
}
int sig = std::atoi(value);
if (sig <= 0) {
return;
}
#ifdef _WIN32
buildInterruptSignal.store(sig);
#else
buildInterruptSignal = static_cast<sig_atomic_t>(sig);
#endif
buildInterruptInjected = true;
}
// Install the interrupt handler and clear any previously recorded signal.
void InstallInterruptHandler()
{
#ifdef _WIN32
buildInterruptSignal.store(0);
SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, TRUE);
#else
buildInterruptSignal = 0;
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = cmInstrumentationSignalHandler;
sigemptyset(&sa.sa_mask);
// One-shot: after the first interrupt the default disposition is restored,
// so a second Ctrl+C terminates immediately even while we are mid-flush.
sa.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &sa, &savedSigIntAction);
#endif
}
// Restore the disposition that was in effect before InstallInterruptHandler().
void RestoreInterruptHandler()
{
#ifdef _WIN32
SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, FALSE);
#else
sigaction(SIGINT, &savedSigIntAction, nullptr);
#endif
}
}
int cmInstrumentationInterrupt::PendingInterruptSignal()
{
#ifdef _WIN32
return buildInterruptSignal.load();
#else
return static_cast<int>(buildInterruptSignal);
#endif
}
cmInstrumentationInterrupt::InterruptOutcome
cmInstrumentationInterrupt::HandleInterrupt(
bool active, std::function<int()> const& callback)
{
// Only trap interrupts when instrumentation is active, so non-instrumented
// flows keep their default signal behavior.
if (!active) {
return { callback(), false, 0, true };
}
InstallInterruptHandler();
buildInterruptInjected = false;
// Test-only: allow the suite to inject an interrupt deterministically.
InjectTestInterrupt();
int ret = callback();
int sig = PendingInterruptSignal();
RestoreInterruptHandler();
// A real OS interrupt should be re-raised so the exit status reflects it; an
// injected (test) interrupt should not, so the process exits cleanly.
return { ret, sig != 0, sig, !buildInterruptInjected };
}
void cmInstrumentationInterrupt::RaiseInterrupt(int sig)
{
#ifdef _WIN32
// On Windows the process exits normally after flushing; the caller
// propagates the (failed) build result.
static_cast<void>(sig);
#else
// Restore the default disposition (SA_RESETHAND already did so for the first
// delivery) and re-raise so the exit status reflects the interrupt.
signal(sig, SIG_DFL);
raise(sig);
#endif
}