mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
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
This commit is contained in:
@@ -134,4 +134,5 @@ list(APPEND CTEST_CUSTOM_COVERAGE_EXCLUDE
|
||||
|
||||
list(APPEND CTEST_CUSTOM_MEMCHECK_IGNORE
|
||||
kwsys.testProcess-10 # See Source/kwsys/CTestCustom.cmake.in
|
||||
RunCMake.InstrumentationInterrupt # interrupts cmake with a real signal
|
||||
)
|
||||
|
||||
@@ -450,6 +450,17 @@ Snippet files have a filename with the syntax
|
||||
The exit code of the command, an integer. This will be ``null`` when
|
||||
``role`` is ``build``.
|
||||
|
||||
``interruptSignal``
|
||||
.. versionadded:: 4.5
|
||||
|
||||
The integer signal number that interrupted the build before it completed
|
||||
(for example ``2`` for ``SIGINT`` from Ctrl+C). Only included when ``role``
|
||||
is ``cmakeBuild`` and the build was interrupted. Consumers can use the
|
||||
presence of this field to distinguish an interrupted build from one that
|
||||
ran to completion.
|
||||
|
||||
Only available as of data version ``1.2``.
|
||||
|
||||
``stdout``
|
||||
.. versionadded:: 4.4
|
||||
|
||||
|
||||
@@ -235,6 +235,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"interruptSignal": {
|
||||
"type": "integer",
|
||||
"description": "The signal number that interrupted the build before it completed. Only included when role is cmakeBuild and the build was interrupted.",
|
||||
"minimum": 1
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The type of command executed.",
|
||||
@@ -617,6 +622,9 @@
|
||||
},
|
||||
"traceFile": {
|
||||
"$ref": "#/definitions/fields/traceFile"
|
||||
},
|
||||
"interruptSignal": {
|
||||
"$ref": "#/definitions/fields/interruptSignal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
instrumentation-interrupt
|
||||
-------------------------
|
||||
|
||||
* The :manual:`cmake-instrumentation(7)` data version has been updated to 1.2.
|
||||
* :manual:`cmake-instrumentation(7)` API now records an overall ``cmakeBuild``
|
||||
snippet even when a :option:`cmake --build` invocation is interrupted by the
|
||||
user (for example with Ctrl+C). The snippet includes a new
|
||||
``interruptSignal`` field, recording the signal that interrupted the build,
|
||||
so that consumers can distinguish an interrupted build from one that ran
|
||||
to completion.
|
||||
@@ -385,6 +385,8 @@ add_library(
|
||||
cmInstallScriptHandler.cxx
|
||||
cmInstrumentation.h
|
||||
cmInstrumentation.cxx
|
||||
cmInstrumentationInterrupt.h
|
||||
cmInstrumentationInterrupt.cxx
|
||||
cmInstrumentationCommand.h
|
||||
cmInstrumentationCommand.cxx
|
||||
cmInstrumentationQuery.h
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
#include "cmCryptoHash.h"
|
||||
#include "cmFileLock.h"
|
||||
#include "cmFileLockResult.h"
|
||||
#include "cmGeneratedFileStream.h"
|
||||
#include "cmGeneratorTarget.h"
|
||||
#include "cmGlobalGenerator.h"
|
||||
#include "cmInstrumentationInterrupt.h"
|
||||
#include "cmInstrumentationQuery.h"
|
||||
#include "cmJSONState.h"
|
||||
#include "cmList.h"
|
||||
@@ -555,7 +557,7 @@ Json::Value cmInstrumentation::ReadJsonSnippet(std::string const& file_name)
|
||||
|
||||
void cmInstrumentation::WriteInstrumentationJson(
|
||||
cmInstrumentationQuery::Version version, Json::Value& root,
|
||||
std::string const& subdir, std::string const& file_name)
|
||||
std::string const& subdir, std::string const& file_name, Atomic atomic)
|
||||
{
|
||||
root["version"] = Json::objectValue;
|
||||
root["version"]["major"] = version.Major;
|
||||
@@ -567,8 +569,28 @@ void cmInstrumentation::WriteInstrumentationJson(
|
||||
std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
|
||||
std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir);
|
||||
cmSystemTools::MakeDirectory(directory);
|
||||
std::string const file_path = cmStrCat(directory, '/', file_name);
|
||||
|
||||
cmsys::ofstream ftmp(cmStrCat(directory, '/', file_name).c_str());
|
||||
if (atomic == Atomic::Yes) {
|
||||
// Write to a temporary file and atomically rename it into place, so that
|
||||
// an interrupt during the write cannot leave a truncated snippet.
|
||||
cmGeneratedFileStream ftmp(file_path);
|
||||
if (!ftmp) {
|
||||
throw std::runtime_error(std::string("Unable to open: ") + file_name);
|
||||
}
|
||||
try {
|
||||
JsonWriter->write(root, &ftmp);
|
||||
ftmp << "\n";
|
||||
// The atomic rename happens when the stream is closed/destroyed.
|
||||
} catch (std::ios_base::failure& fail) {
|
||||
cmSystemTools::Error(cmStrCat("Failed to write JSON: ", fail.what()));
|
||||
} catch (...) {
|
||||
cmSystemTools::Error("Error writing JSON output for instrumentation.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cmsys::ofstream ftmp(file_path.c_str());
|
||||
if (!ftmp.good()) {
|
||||
throw std::runtime_error(std::string("Unable to open: ") + file_name);
|
||||
}
|
||||
@@ -747,6 +769,15 @@ int cmInstrumentation::InstrumentCommand(
|
||||
// See SpawnBuildDaemon(); this data is currently meaningless for build.
|
||||
root["result"] = command_type == "build" ? Json::nullValue : ret;
|
||||
|
||||
// If the build was interrupted (e.g. by Ctrl+C), record the signal number
|
||||
// that stopped it, so consumers can distinguish an interrupted build from
|
||||
// one that ran to completion. Omitted when no interrupt occurred; only a
|
||||
// command wrapped by HandleInterrupt can observe a pending signal here.
|
||||
int sig = cmInstrumentationInterrupt::PendingInterruptSignal();
|
||||
if (sig != 0) {
|
||||
root["interruptSignal"] = sig;
|
||||
}
|
||||
|
||||
// Output Sizes
|
||||
if (root.isMember("outputs")) {
|
||||
root["outputSizes"] = Json::arrayValue;
|
||||
@@ -808,7 +839,14 @@ int cmInstrumentation::InstrumentCommand(
|
||||
}
|
||||
this->configureSnippetData.clear();
|
||||
}
|
||||
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name);
|
||||
// Write the cmakeBuild envelope atomically (temp file + rename). This is
|
||||
// the snippet flushed while unwinding from a user interrupt, where a
|
||||
// second Ctrl+C could otherwise truncate it mid-write; the atomic write
|
||||
// guarantees it is either absent or complete. Per-step snippets are never
|
||||
// flushed under interrupt and are left non-atomic.
|
||||
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name,
|
||||
command_type == "cmakeBuild" ? Atomic::Yes
|
||||
: Atomic::No);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public:
|
||||
cm::optional<std::string> StdOut;
|
||||
cm::optional<std::string> StdErr;
|
||||
};
|
||||
|
||||
int InstrumentCommand(
|
||||
std::string command_type, std::vector<std::string> const& command,
|
||||
std::function<CommandResult()> const& callback,
|
||||
@@ -100,10 +101,16 @@ private:
|
||||
Json::Value ReadJsonSnippet(std::string const& file_name);
|
||||
bool AcquireLock(std::string const& lock_file, cmFileLock& lock,
|
||||
unsigned long timeout);
|
||||
enum class Atomic
|
||||
{
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
void WriteInstrumentationJson(cmInstrumentationQuery::Version version,
|
||||
Json::Value& index,
|
||||
std::string const& directory,
|
||||
std::string const& file_name);
|
||||
std::string const& file_name,
|
||||
Atomic atomic = Atomic::No);
|
||||
void InsertStaticSystemInformation(Json::Value& index);
|
||||
void GetDynamicSystemInformation(double& memory, double& load);
|
||||
void InsertDynamicSystemInformation(Json::Value& index,
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/* 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
|
||||
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||
#pragma once
|
||||
|
||||
#include "cmConfigure.h" // IWYU pragma: keep
|
||||
|
||||
#include <functional>
|
||||
|
||||
// Async-signal-safe handling of a user interrupt (Ctrl+C / SIGINT on POSIX, or
|
||||
// a console Ctrl event on Windows) around an instrumented command, so that the
|
||||
// command's snippet can still be written before the process exits.
|
||||
class cmInstrumentationInterrupt
|
||||
{
|
||||
public:
|
||||
// Outcome of running a callback under an installed interrupt handler.
|
||||
struct InterruptOutcome
|
||||
{
|
||||
int ExitCode;
|
||||
bool Interrupted;
|
||||
int Signal;
|
||||
// Whether the caught interrupt should be re-raised so the process exit
|
||||
// status reflects it. True for a real OS signal; false for a test-
|
||||
// injected interrupt, which exits cleanly after flushing the snippet.
|
||||
// Always set explicitly at every construction site (no default member
|
||||
// initializer, so this stays a C++11 aggregate).
|
||||
bool ShouldRaise;
|
||||
};
|
||||
|
||||
// Run `callback` with an interrupt handler installed for its duration, so a
|
||||
// user interrupt sets a flag instead of terminating the process immediately.
|
||||
// The handler is deliberately minimal (async-signal-safe): it only records
|
||||
// the signal. When `active` is false (e.g. instrumentation is not enabled),
|
||||
// no handler is installed and the callback keeps the default signal
|
||||
// behavior. Returns the callback's exit code, whether an interrupt was
|
||||
// caught, and the signal number (0 if none).
|
||||
static InterruptOutcome HandleInterrupt(
|
||||
bool active, std::function<int()> const& callback);
|
||||
|
||||
// Restore the default disposition and re-raise the given signal so that the
|
||||
// process exits as if by the interrupt. No-op on platforms where re-raising
|
||||
// is not the appropriate exit mechanism.
|
||||
static void RaiseInterrupt(int sig);
|
||||
|
||||
// The pending interrupt signal number, or 0 if no interrupt occurred.
|
||||
static int PendingInterruptSignal();
|
||||
};
|
||||
+25
-4
@@ -88,6 +88,7 @@
|
||||
# include "cmFileAPI.h"
|
||||
# include "cmGraphVizWriter.h"
|
||||
# include "cmInstrumentation.h"
|
||||
# include "cmInstrumentationInterrupt.h"
|
||||
# include "cmInstrumentationQuery.h"
|
||||
# include "cmMakefileProfilingData.h"
|
||||
# include "cmSarifLog.h"
|
||||
@@ -4197,10 +4198,30 @@ int cmake::Build(cmBuildArgs buildArgs, std::vector<std::string> targets,
|
||||
// Block the instrumentation build daemon from spawning during this build.
|
||||
// This lock will be released when the process exits at the end of the build.
|
||||
instrumentation.LockBuildDaemon();
|
||||
int buildresult = instrumentation.InstrumentCommand(
|
||||
"cmakeBuild", args, [doBuild]() -> cmInstrumentation::CommandResult {
|
||||
return { doBuild(), cm::nullopt, cm::nullopt };
|
||||
});
|
||||
// Run the build under an interrupt handler so that a user interrupt (e.g.
|
||||
// Ctrl+C) still writes the overall `cmakeBuild` snippet before we exit.
|
||||
cmInstrumentationInterrupt::InterruptOutcome buildOutcome =
|
||||
cmInstrumentationInterrupt::HandleInterrupt(
|
||||
instrumentation.HasQuery(),
|
||||
[&instrumentation, &args, &doBuild]() -> int {
|
||||
return instrumentation.InstrumentCommand(
|
||||
"cmakeBuild", args,
|
||||
[&doBuild]() -> cmInstrumentation::CommandResult {
|
||||
return { doBuild(), cm::nullopt, cm::nullopt };
|
||||
});
|
||||
});
|
||||
int buildresult = buildOutcome.ExitCode;
|
||||
if (buildOutcome.Interrupted) {
|
||||
// The build was interrupted and its snippet has been written. Skip the
|
||||
// post-build indexing hook (which would run callbacks and delete data).
|
||||
// For a real OS interrupt, re-raise so the exit status reflects it; for a
|
||||
// test-injected interrupt, exit cleanly. The next indexing run will
|
||||
// reclaim the snippet written above.
|
||||
if (buildOutcome.ShouldRaise) {
|
||||
cmInstrumentationInterrupt::RaiseInterrupt(buildOutcome.Signal);
|
||||
}
|
||||
return buildresult;
|
||||
}
|
||||
instrumentation.CollectTimingData(
|
||||
cmInstrumentationQuery::Hook::PostCMakeBuild);
|
||||
#else
|
||||
|
||||
@@ -443,7 +443,16 @@ if(CMAKE_GENERATOR MATCHES "Make|Ninja|FASTBuild")
|
||||
-DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
|
||||
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
|
||||
-DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION}
|
||||
-DCMAKE_EXECUTABLE_SUFFIX=${CMAKE_EXECUTABLE_SUFFIX}
|
||||
-DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA})
|
||||
# The real-signal/console-event interrupt case runs in its own suite so it can
|
||||
# be excluded from MemCheck (the interrupted cmake is killed by a real signal)
|
||||
# without dropping leak coverage for the rest of the instrumentation tests.
|
||||
add_RunCMake_test(InstrumentationInterrupt TEST_DIR Instrumentation
|
||||
-DINSTRUMENTATION_INTERRUPT_REAL=1
|
||||
-DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
|
||||
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
|
||||
-DCMAKE_EXECUTABLE_SUFFIX=${CMAKE_EXECUTABLE_SUFFIX})
|
||||
endif()
|
||||
add_RunCMake_test(ConfigDir)
|
||||
if(CMake_TEST_FindPython2)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* 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
|
||||
@@ -8,6 +8,8 @@ function(instrument test)
|
||||
set(OPTIONS
|
||||
"BUILD"
|
||||
"BUILD_MAKE_PROGRAM"
|
||||
"INTERRUPT"
|
||||
"INTERRUPT_SEAM"
|
||||
"INSTALL"
|
||||
"INSTALL_PARALLEL"
|
||||
"TEST"
|
||||
@@ -121,6 +123,10 @@ function(instrument test)
|
||||
if (ARGS_DISABLE_TEST)
|
||||
list(APPEND ARGS_CONFIGURE_ARGS "-DDISABLE_TEST=ON")
|
||||
endif()
|
||||
if (ARGS_INTERRUPT)
|
||||
list(APPEND ARGS_CONFIGURE_ARGS
|
||||
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c")
|
||||
endif()
|
||||
set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project)
|
||||
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
|
||||
set(maybe_CMAKE_BUILD_TYPE -DCMAKE_BUILD_TYPE=Debug)
|
||||
@@ -176,6 +182,51 @@ function(instrument test)
|
||||
unset(RunCMake_TEST_OUTPUT_MERGE)
|
||||
endif()
|
||||
endif()
|
||||
if (ARGS_INTERRUPT)
|
||||
# Build just the interrupt helper so it exists for the interrupted build.
|
||||
# This uninterrupted build runs the postCMakeBuild hook, so remove the
|
||||
# postCMakeBuild.hook file it produces; its absence after the interrupted
|
||||
# build below then proves that build's hook was skipped.
|
||||
run_cmake_command(${test}-helper
|
||||
${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild)
|
||||
file(REMOVE ${v1}/postCMakeBuild.hook)
|
||||
|
||||
# Run an instrumented build and interrupt it after a few seconds, while the
|
||||
# slow target is still running. Multi-config generators place the helper
|
||||
# under a per-config subdirectory; the build below uses --config Debug.
|
||||
set(helper_dir ${RunCMake_TEST_BINARY_DIR})
|
||||
if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
|
||||
set(helper_dir ${helper_dir}/Debug)
|
||||
endif()
|
||||
set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX})
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
run_cmake_command(${test}-build
|
||||
${helper} 3
|
||||
${CMAKE_COMMAND} --build . --config Debug)
|
||||
unset(RunCMake_QUIET_ERROR)
|
||||
endif()
|
||||
if (ARGS_INTERRUPT_SEAM)
|
||||
# Drive the cmakeBuild interrupt path deterministically via the test-only
|
||||
# injection seam, with no OS signal, so it runs on every generator. First
|
||||
# build normally so the postCMakeBuild hook runs and creates its marker
|
||||
# file; remove it so its absence after the injected build proves that
|
||||
# build's hook was skipped.
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
run_cmake_command(${test}-warmup
|
||||
${CMAKE_COMMAND} --build . --config Debug)
|
||||
file(REMOVE ${v1}/postCMakeBuild.hook)
|
||||
|
||||
# Inject an interrupt (SIGINT == 2) via the undocumented test seam and build
|
||||
# again; cmake exits cleanly but writes the interrupted cmakeBuild snippet
|
||||
# and skips the hook.
|
||||
set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2)
|
||||
set(RunCMake_TEST_EXPECT_RESULT 0)
|
||||
run_cmake_command(${test}-build
|
||||
${CMAKE_COMMAND} --build . --config Debug)
|
||||
unset(RunCMake_TEST_EXPECT_RESULT)
|
||||
unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT})
|
||||
unset(RunCMake_QUIET_ERROR)
|
||||
endif()
|
||||
if (ARGS_BUILD_MAKE_PROGRAM)
|
||||
set(RunCMake_TEST_OUTPUT_MERGE 1)
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
@@ -206,6 +257,22 @@ function(instrument test)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (INSTRUMENTATION_INTERRUPT_REAL)
|
||||
# RunCMake.InstrumentationInterrupt runs ONLY the real-signal/
|
||||
# console-event interrupt case, as it must be excluded from MemCheck.
|
||||
#
|
||||
# POSIX delivers a real SIGINT to a contained process group. On Windows, only
|
||||
# the Ninja generator is exercised: its native tool reliably stops on the
|
||||
# console event and does not re-broadcast it to the runner; the other Windows
|
||||
# make-family generators are covered by the injection seam instead.
|
||||
if (NOT WIN32 OR RunCMake_GENERATOR MATCHES "Ninja")
|
||||
instrument(interrupt INTERRUPT
|
||||
CHECK_SCRIPT check-interrupted.cmake
|
||||
)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Bad Queries
|
||||
instrument(bad-option BAD_QUERY
|
||||
CHECK_SCRIPT check-query-dir.cmake
|
||||
@@ -403,6 +470,14 @@ if (NOT Skip_COMPILE_TRACE_QUERY_Case)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Test that interrupting `cmake --build` still writes the cmakeBuild snippet,
|
||||
# recording the interrupting signal. This case uses the deterministic test
|
||||
# seam (no OS event). The real OS-event counterpart runs in the separate
|
||||
# RunCMake.InstrumentationInterrupt suite.
|
||||
instrument(interrupt INTERRUPT_SEAM
|
||||
CHECK_SCRIPT check-interrupted.cmake
|
||||
)
|
||||
|
||||
# Test make/ninja hooks
|
||||
if(RunCMake_GENERATOR STREQUAL "FASTBuild")
|
||||
# FIXME(#27184): This does not work for FASTBuild.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
|
||||
|
||||
# After an interrupted `cmake --build`, exactly one cmakeBuild snippet should be
|
||||
# present, recording the interrupting signal. Any cmakeBuild snippet from the
|
||||
# earlier (uninterrupted) helper build was collated and removed by its
|
||||
# postCMakeBuild hook.
|
||||
file(GLOB cmakeBuildSnippets LIST_DIRECTORIES false ${v1}/data/cmakeBuild-*.json)
|
||||
list(LENGTH cmakeBuildSnippets numCmakeBuild)
|
||||
if (NOT numCmakeBuild EQUAL 1)
|
||||
add_error("Expected exactly one cmakeBuild snippet, found ${numCmakeBuild}: ${cmakeBuildSnippets}")
|
||||
else()
|
||||
read_json("${cmakeBuildSnippets}" contents)
|
||||
|
||||
string(JSON interruptSignal ERROR_VARIABLE noSignal GET "${contents}" interruptSignal)
|
||||
if (noSignal OR NOT interruptSignal MATCHES "^[1-9][0-9]*$")
|
||||
add_error("cmakeBuild snippet is not marked interrupted:\n${contents}")
|
||||
endif()
|
||||
|
||||
string(JSON version_minor GET "${contents}" version minor)
|
||||
if (NOT version_minor EQUAL 2)
|
||||
add_error("cmakeBuild snippet version minor expected 2, got: ${version_minor}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The postCMakeBuild hook must be skipped entirely on interrupt, so its callback
|
||||
# must not run. The callback (hook.cmake) writes a postCMakeBuild.hook file
|
||||
# whenever it runs; the helper build's copy was removed before the interrupted
|
||||
# build, so its presence here would mean the hook wrongly ran on interrupt.
|
||||
if (EXISTS ${v1}/postCMakeBuild.hook)
|
||||
add_error("postCMakeBuild hook should be skipped on interrupt, but it ran")
|
||||
endif()
|
||||
|
||||
if (DEFINED RunCMake_TEST_FAILED)
|
||||
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
|
||||
endif()
|
||||
@@ -0,0 +1 @@
|
||||
42
|
||||
@@ -70,3 +70,12 @@ if (FAIL)
|
||||
add_test(NAME dummy COMMAND ${CMAKE_COMMAND} -E false)
|
||||
install(CODE "message(FATAL_ERROR \"Failed install.\")")
|
||||
endif()
|
||||
|
||||
if (INTERRUPT_BUILD_SRC)
|
||||
add_executable(InterruptBuild "${INTERRUPT_BUILD_SRC}")
|
||||
add_custom_target(interruptSlow ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: begin"
|
||||
COMMAND ${CMAKE_COMMAND} -E sleep 30
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: end"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": ["postCMakeBuild"],
|
||||
"callbacks": ["@GET_HOOK@"]
|
||||
}
|
||||
@@ -9,6 +9,8 @@ function(snippet_has_fields snippet contents)
|
||||
json_has_key("${snippet}" "${contents}" role)
|
||||
json_has_key("${snippet}" "${contents}" workingDir)
|
||||
json_has_key("${snippet}" "${contents}" result)
|
||||
# Only an interrupted build records this; completed commands must omit it.
|
||||
json_missing_key("${snippet}" "${contents}" interruptSignal)
|
||||
if (NOT filename MATCHES "^build-*")
|
||||
json_has_key("${snippet}" "${contents}" command)
|
||||
else()
|
||||
|
||||
Reference in New Issue
Block a user