Files
Daksh Mamodiya bda67b82e7 instrumentation: Write cmakeBuild snippet when build is interrupted
The overall `cmakeBuild` snippet is written only after the native build
tool returns, so interrupting `cmake --build` with Ctrl+C terminated CMake
before it was recorded and lost the build's delineation.

When instrumentation is active, install a scoped, async-signal-safe handler
(POSIX SIGINT; Windows CTRL_C/CTRL_BREAK) that just flags the interrupt. The
existing write then runs during unwind, records the interrupting signal in a
new `interruptSignal` field, skips the post-build index hook, and re-raises so
the exit status still reflects the signal.

The handler lives in its own translation unit, keeping the platform-divergent
signal code out of the main instrumentation implementation.

Issue: #27859
2026-06-18 15:38:43 +02:00

170 lines
4.5 KiB
C

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
/* Test helper to exercise instrumentation handling of an interrupted build.
Usage: InterruptBuild <delay-seconds> <command> [args...]
It runs <command> in its own process group, waits <delay-seconds>, then
delivers a user interrupt to that group only -- mimicking a user pressing
Ctrl+C on the build's foreground process group, while leaving the calling
test driver untouched. It then reports a fixed exit code so the test can
assert deterministically:
42 - the build was interrupted and then stopped (expected)
2 - usage error
3 - failed to start the build
99 - the build did not stop after the interrupt
100 + N - the build exited normally with code N (unexpected, POSIX only)
On POSIX the interrupt is SIGINT sent to the child's process group; the
instrumented `cmake` re-raises it, so the child terminates via a signal.
On Windows the interrupt is a CTRL_BREAK_EVENT sent to the child's process
group (created with CREATE_NEW_PROCESS_GROUP); `cmake`'s console handler
treats CTRL_BREAK identically to CTRL_C. Because Windows `cmake` returns
the build's (failed) exit code rather than a signal status, success is
detected as "the child exited promptly after the interrupt". The real
assertion -- that an interrupted snippet was written -- is done by the
accompanying check script. */
#include <stdio.h>
#include <stdlib.h>
#if defined(_WIN32)
# include <windows.h>
# include <string.h>
int main(int argc, char** argv)
{
int delay;
char cmdline[32768];
size_t len = 0;
int i;
size_t n;
STARTUPINFOA si;
PROCESS_INFORMATION pi;
DWORD waitRc;
if (argc < 3) {
fprintf(stderr,
"Usage: InterruptBuild <delay-seconds> <command> [args...]\n");
return 2;
}
delay = atoi(argv[1]);
if (delay <= 0) {
delay = 1;
}
/* Rebuild a command line from argv[2..]. The inputs are controlled by the
test (a cmake path plus simple flags) and never contain embedded quotes,
so wrapping each argument in quotes is sufficient and correct. The buffer
is sized to the CreateProcess command-line limit of 32768 characters,
including the terminating null. */
for (i = 2; i < argc; ++i) {
n = strlen(argv[i]);
if (len + n + 4 >= sizeof(cmdline)) {
return 2;
}
if (i > 2) {
cmdline[len++] = ' ';
}
cmdline[len++] = '"';
memcpy(cmdline + len, argv[i], n);
len += n;
cmdline[len++] = '"';
}
cmdline[len] = '\0';
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
/* New process group so the control event reaches only the build (and its
children), not this helper or the test driver. */
if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
CREATE_NEW_PROCESS_GROUP, NULL, NULL, &si, &pi)) {
return 3;
}
Sleep((DWORD)delay * 1000);
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pi.dwProcessId);
waitRc = WaitForSingleObject(pi.hProcess, 30000);
if (waitRc != WAIT_OBJECT_0) {
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 99;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 42;
}
#else
# include <signal.h>
# include <unistd.h>
# include <sys/types.h>
# include <sys/wait.h>
int main(int argc, char** argv)
{
int delay;
pid_t build;
pid_t killer;
int status = 0;
if (argc < 3) {
fprintf(stderr,
"Usage: InterruptBuild <delay-seconds> <command> [args...]\n");
return 2;
}
delay = atoi(argv[1]);
if (delay <= 0) {
delay = 1;
}
build = fork();
if (build < 0) {
return 3;
}
if (build == 0) {
/* Become the leader of a new process group, then run the build. The
native build tool inherits this group, so a later group signal reaches
it too. */
setpgid(0, 0);
execvp(argv[2], &argv[2]);
_exit(127);
}
/* Best-effort from the parent side to avoid a setpgid race; ignore errors.
*/
setpgid(build, build);
killer = fork();
if (killer == 0) {
sleep((unsigned int)delay);
killpg(build, SIGINT);
_exit(0);
}
waitpid(build, &status, 0);
if (killer > 0) {
kill(killer, SIGKILL);
waitpid(killer, NULL, 0);
}
if (WIFSIGNALED(status)) {
return 42;
}
if (WIFEXITED(status)) {
return 100 + WEXITSTATUS(status);
}
return 99;
}
#endif