diff --git a/Help/manual/cmake-instrumentation.7.rst b/Help/manual/cmake-instrumentation.7.rst index 3ff309067b..fb6f244b67 100644 --- a/Help/manual/cmake-instrumentation.7.rst +++ b/Help/manual/cmake-instrumentation.7.rst @@ -454,11 +454,11 @@ Snippet files have a filename with the syntax ``interruptSignal`` .. versionadded:: 4.5 - The integer signal number that interrupted the build before it completed + The integer signal number that interrupted the command 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. + is ``cmakeBuild`` or ``cmakeInstall`` and the command was interrupted. + Consumers can use the presence of this field to distinguish an interrupted + command from one that ran to completion. Only available as of data version ``1.2``. diff --git a/Help/manual/instrumentation/snippet-v1-schema.json b/Help/manual/instrumentation/snippet-v1-schema.json index a59de52293..37731ff1a4 100644 --- a/Help/manual/instrumentation/snippet-v1-schema.json +++ b/Help/manual/instrumentation/snippet-v1-schema.json @@ -237,7 +237,7 @@ }, "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.", + "description": "The signal number that interrupted the command before it completed. Only included when role is cmakeBuild or cmakeInstall and the command was interrupted.", "minimum": 1 }, "role": { diff --git a/Help/release/dev/instrumentation-interrupt.rst b/Help/release/dev/instrumentation-interrupt.rst index 10383a372f..c930f2ced6 100644 --- a/Help/release/dev/instrumentation-interrupt.rst +++ b/Help/release/dev/instrumentation-interrupt.rst @@ -8,3 +8,6 @@ instrumentation-interrupt ``interruptSignal`` field, recording the signal that interrupted the build, so that consumers can distinguish an interrupted build from one that ran to completion. +* Similarly, :manual:`cmake-instrumentation(7)` API now records an overall + ``cmakeInstall`` snippet, marked with the same ``interruptSignal`` field, + when a :option:`cmake --install` invocation is interrupted by the user. diff --git a/Source/cmInstallScriptHandler.cxx b/Source/cmInstallScriptHandler.cxx index d1fb45fa86..9b91a08241 100644 --- a/Source/cmInstallScriptHandler.cxx +++ b/Source/cmInstallScriptHandler.cxx @@ -22,6 +22,7 @@ #include "cmCryptoHash.h" #include "cmGeneratedFileStream.h" #include "cmInstrumentation.h" +#include "cmInstrumentationInterrupt.h" #include "cmJSONState.h" #include "cmProcessOutput.h" #include "cmStringAlgorithms.h" @@ -152,6 +153,14 @@ int cmInstallScriptHandler::Install(unsigned int j, std::function queueScripts; queueScripts = [&runners, &working, &installed, &i, &loop, j, &queueScripts]() { + if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) { + // Interrupted (e.g. Ctrl+C): launch no further scripts. In-flight + // children share the process group, receive the signal, and exit on + // their own, draining the event loop; queueScripts is the single + // re-entry point for launching, so guarding it here stops all remaining + // work without killing anything. + return; + } for (auto queue = std::min(j - working, runners.size() - i); queue > 0; --queue) { ++working; @@ -173,10 +182,18 @@ int cmInstallScriptHandler::Install(unsigned int j, queueScripts(); uv_run(loop, UV_RUN_DEFAULT); + // Aggregate child results. When an interrupt stopped queueScripts, the + // runners beyond the dispatched prefix [0, i) were never started (they have + // a null process handle); those are "not run", not "failed", so exclude + // them. With no interrupt every runner was dispatched and this inspects + // them all, matching the non-interrupt behavior exactly. + std::size_t const inspect = + cmInstrumentationInterrupt::PendingInterruptSignal() != 0 ? i + : runners.size(); int result = 0; - for (auto& runner : runners) { - if (runner.Failed()) { - runner.printFailure(); + for (std::size_t k = 0; k < inspect; ++k) { + if (runners[k].Failed()) { + runners[k].printFailure(); result = 1; } } diff --git a/Source/cmInstrumentation.cxx b/Source/cmInstrumentation.cxx index 6fe151c4c7..a63c94b342 100644 --- a/Source/cmInstrumentation.cxx +++ b/Source/cmInstrumentation.cxx @@ -770,8 +770,8 @@ 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 + // If the command was interrupted (e.g. by Ctrl+C), record the signal number + // that stopped it, so consumers can distinguish an interrupted command 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(); @@ -840,14 +840,15 @@ int cmInstrumentation::InstrumentCommand( } this->configureSnippetData.clear(); } - // 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. + // Write the cmakeBuild/cmakeInstall 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. + bool const atomicEnvelope = + command_type == "cmakeBuild" || command_type == "cmakeInstall"; this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name, - command_type == "cmakeBuild" ? Atomic::Yes - : Atomic::No); + atomicEnvelope ? Atomic::Yes : Atomic::No); } return ret; } diff --git a/Source/cmInstrumentationInterrupt.cxx b/Source/cmInstrumentationInterrupt.cxx index 28b8a3e018..e681a48b48 100644 --- a/Source/cmInstrumentationInterrupt.cxx +++ b/Source/cmInstrumentationInterrupt.cxx @@ -26,32 +26,34 @@ #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. +// Flag shared between the interrupt handler and the command flow that writes +// the instrumentation envelope 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 buildInterruptSignal{ 0 }; +std::atomic interruptSignal{ 0 }; BOOL WINAPI cmInstrumentationConsoleHandler(DWORD type) { if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) { int expected = 0; - buildInterruptSignal.compare_exchange_strong(expected, SIGINT); + interruptSignal.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. + // process exits. Child processes (native build tool, install scripts, + // tests) share the console and receive the event directly, so they still + // terminate and unblock our loop. return TRUE; } return FALSE; } #else -sig_atomic_t volatile buildInterruptSignal = 0; +sig_atomic_t volatile interruptSignal = 0; struct sigaction savedSigIntAction; extern "C" void cmInstrumentationSignalHandler(int sig) { - buildInterruptSignal = sig; + interruptSignal = sig; } #endif @@ -59,14 +61,14 @@ extern "C" void cmInstrumentationSignalHandler(int sig) // 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; +bool interruptInjected = 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. +// instrumentation test suite inject an "interrupted" condition +// deterministically, with no real OS signal -- so the instrumentation +// 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"); @@ -78,21 +80,21 @@ void InjectTestInterrupt() return; } #ifdef _WIN32 - buildInterruptSignal.store(sig); + interruptSignal.store(sig); #else - buildInterruptSignal = static_cast(sig); + interruptSignal = static_cast(sig); #endif - buildInterruptInjected = true; + interruptInjected = true; } // Install the interrupt handler and clear any previously recorded signal. void InstallInterruptHandler() { #ifdef _WIN32 - buildInterruptSignal.store(0); + interruptSignal.store(0); SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, TRUE); #else - buildInterruptSignal = 0; + interruptSignal = 0; struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = cmInstrumentationSignalHandler; @@ -118,9 +120,9 @@ void RestoreInterruptHandler() int cmInstrumentationInterrupt::PendingInterruptSignal() { #ifdef _WIN32 - return buildInterruptSignal.load(); + return interruptSignal.load(); #else - return static_cast(buildInterruptSignal); + return static_cast(interruptSignal); #endif } @@ -134,7 +136,7 @@ cmInstrumentationInterrupt::HandleInterrupt( return { callback(), false, 0, true }; } InstallInterruptHandler(); - buildInterruptInjected = false; + interruptInjected = false; // Test-only: allow the suite to inject an interrupt deterministically. InjectTestInterrupt(); int ret = callback(); @@ -142,7 +144,7 @@ cmInstrumentationInterrupt::HandleInterrupt( 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 }; + return { ret, sig != 0, sig, !interruptInjected }; } void cmInstrumentationInterrupt::RaiseInterrupt(int sig) diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx index 97b1712097..9426c6cdbf 100644 --- a/Source/cmakemain.cxx +++ b/Source/cmakemain.cxx @@ -28,6 +28,7 @@ #include "cmGlobalGenerator.h" #include "cmInstallScriptHandler.h" #include "cmInstrumentation.h" +#include "cmInstrumentationInterrupt.h" #include "cmInstrumentationQuery.h" #include "cmList.h" #include "cmMakefile.h" @@ -958,6 +959,12 @@ int do_install(int ac, char const* const* av) ret_ = handler.Install(jobs, instrumentation); } else { for (auto const& script : handler.GetScripts()) { + if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) { + // Interrupted (e.g. Ctrl+C): launch no further scripts. The script + // currently running executes in-process and finishes on its own; we + // simply stop starting new ones. + break; + } std::vector cmd = script.command; cmake cm(cmState::Role::Script); cmSystemTools::SetMessageCallback( @@ -976,15 +983,41 @@ int do_install(int ac, char const* const* av) } } } + if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) { + // Any caught interrupt makes the install unsuccessful even if the work + // that did run happened to succeed. Windows has no signal to re-raise, + // so this is what forces a non-zero exit status there; on POSIX it also + // keeps the snippet `result` consistent with the re-raised signal. + ret_ = 1; + } return int(ret_ > 0); }; std::vector cmd; cm::append(cmd, av, av + ac); - ret = instrumentation.InstrumentCommand( - "cmakeInstall", cmd, [doInstall]() -> cmInstrumentation::CommandResult { - return { doInstall(), cm::nullopt, cm::nullopt }; - }); + // Run the install under an interrupt handler so that a user interrupt (e.g. + // Ctrl+C) still writes the overall `cmakeInstall` snippet before we exit. + cmInstrumentationInterrupt::InterruptOutcome installOutcome = + cmInstrumentationInterrupt::HandleInterrupt( + instrumentation.HasQuery(), + [&instrumentation, &cmd, &doInstall]() -> int { + return instrumentation.InstrumentCommand( + "cmakeInstall", cmd, + [&doInstall]() -> cmInstrumentation::CommandResult { + return { doInstall(), cm::nullopt, cm::nullopt }; + }); + }); + ret = installOutcome.ExitCode; + if (installOutcome.Interrupted) { + // The install was interrupted and its snippet has been written. Skip the + // post-install 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. + if (installOutcome.ShouldRaise) { + cmInstrumentationInterrupt::RaiseInterrupt(installOutcome.Signal); + } + return ret; + } instrumentation.CollectTimingData( cmInstrumentationQuery::Hook::PostCMakeInstall); return ret; diff --git a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake index 18f800d379..95f3c19b64 100644 --- a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake +++ b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake @@ -12,6 +12,8 @@ function(instrument test) "INTERRUPT_SEAM" "INSTALL" "INSTALL_PARALLEL" + "INSTALL_SEAM" + "INSTALL_INTERRUPT" "TEST" "WORKFLOW" "COPY_QUERIES" @@ -127,6 +129,11 @@ function(instrument test) list(APPEND ARGS_CONFIGURE_ARGS "-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c") endif() + if (ARGS_INSTALL_INTERRUPT) + list(APPEND ARGS_CONFIGURE_ARGS + "-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c" + "-DINSTALL_INTERRUPT=ON") + 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) @@ -200,7 +207,7 @@ function(instrument test) endif() set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX}) set(RunCMake_QUIET_ERROR 1) - run_cmake_command(${test}-build + run_cmake_command(${test}-signal ${helper} 3 ${CMAKE_COMMAND} --build . --config Debug) unset(RunCMake_QUIET_ERROR) @@ -221,7 +228,7 @@ function(instrument test) # and skips the hook. set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2) set(RunCMake_TEST_EXPECT_RESULT 0) - run_cmake_command(${test}-build + run_cmake_command(${test}-seam ${CMAKE_COMMAND} --build . --config Debug) unset(RunCMake_TEST_EXPECT_RESULT) unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT}) @@ -239,6 +246,63 @@ function(instrument test) if (ARGS_INSTALL) run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . --prefix install --config Debug) endif() + if (ARGS_INSTALL_SEAM) + # Drive the cmakeInstall interrupt path deterministically via the test-only + # injection seam, with no OS signal, so it runs on every generator. First + # install normally so the postCMakeInstall hook runs and creates its marker + # file; remove it (and the manifest) so their absence after the injected + # install proves that install's hook was skipped and left nothing complete. + set(RunCMake_QUIET_ERROR 1) + run_cmake_command(${test}-warmup + ${CMAKE_COMMAND} --install . --prefix install --config Debug) + file(REMOVE ${v1}/postCMakeInstall.hook) + file(REMOVE ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt) + + # Inject an interrupt (SIGINT == 2) via the undocumented test seam and + # install again; cmake exits with a non-zero status but writes the + # interrupted cmakeInstall snippet and skips the hook. + set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2) + set(RunCMake_TEST_EXPECT_RESULT 1) + run_cmake_command(${test}-seam + ${CMAKE_COMMAND} --install . --prefix install --config Debug) + unset(RunCMake_TEST_EXPECT_RESULT) + unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT}) + unset(RunCMake_QUIET_ERROR) + endif() + if (ARGS_INSTALL_INTERRUPT) + # Build just the interrupt helper and main so the parallel install has + # something to do. This build runs no postCMakeInstall hook (the query only + # requests that hook), so remove any stale marker; its absence after the + # interrupted install then proves that install's hook was skipped. + run_cmake_command(${test}-helper + ${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild main) + file(REMOVE ${v1}/postCMakeBuild.hook) + file(REMOVE ${v1}/postCMakeInstall.hook) + file(REMOVE ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt) + + # Pin InstallScripts.json newest so the parallel install path is chosen + # deterministically (the staleness heuristic can otherwise fall back to + # serial on coarse-mtime filesystems). + file(TOUCH_NOCREATE ${RunCMake_TEST_BINARY_DIR}/CMakeFiles/InstallScripts.json) + + # Run an instrumented parallel install and interrupt it after a few seconds, + # while a slow install script is still running and others are pending. + 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}) + # Merge stdout/stderr: the parallel aggregation prints one "User interrupt" + # diagnostic per in-flight script to stderr, whose presence and count are + # cosmetic and race with the re-raise that terminates the process. + set(RunCMake_TEST_OUTPUT_MERGE 1) + set(RunCMake_QUIET_ERROR 1) + run_cmake_command(${test}-signal + ${helper} 3 + ${CMAKE_COMMAND} --install . --prefix install --config Debug -j 1) + unset(RunCMake_QUIET_ERROR) + unset(RunCMake_TEST_OUTPUT_MERGE) + endif() if (ARGS_TEST) run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug) endif() @@ -266,9 +330,14 @@ if (INSTRUMENTATION_INTERRUPT_REAL) # 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 + instrument(interrupt-build INTERRUPT CHECK_SCRIPT check-interrupted.cmake ) + # Interrupt a parallel `cmake --install` with a real OS signal, proving the + # cooperative cancellation stops pending install scripts and skips the hook. + instrument(interrupt-install INSTALL_INTERRUPT + CHECK_SCRIPT check-installation-interrupted.cmake + ) endif() return() endif() @@ -476,13 +545,17 @@ 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 +# Test that interrupting `cmake --build` or `cmake --install` still writes the +# overall cmakeBuild/cmakeInstall snippet, recording the interrupting signal, +# and skips the corresponding post-command hook. These cases use the +# deterministic test seam (no OS event); the real OS-event counterparts run in +# the separate RunCMake.InstrumentationInterrupt suite. +instrument(interrupt-build INTERRUPT_SEAM CHECK_SCRIPT check-interrupted.cmake ) +instrument(interrupt-install BUILD INSTALL_SEAM + CHECK_SCRIPT check-installation-interrupted.cmake +) # Test make/ninja hooks if(RunCMake_GENERATOR STREQUAL "FASTBuild") diff --git a/Tests/RunCMake/Instrumentation/check-installation-interrupted.cmake b/Tests/RunCMake/Instrumentation/check-installation-interrupted.cmake new file mode 100644 index 0000000000..ededb9fbac --- /dev/null +++ b/Tests/RunCMake/Instrumentation/check-installation-interrupted.cmake @@ -0,0 +1,12 @@ +include(${CMAKE_CURRENT_LIST_DIR}/json.cmake) + +# After an interrupted `cmake --install`, exactly one cmakeInstall snippet should +# be present, marked with the interrupting signal, and the postCMakeInstall hook +# must have been skipped. +check_interrupted_snippet(cmakeInstall postCMakeInstall) + +# An interrupted install is incomplete, so it must not leave behind an install +# manifest that looks complete. +if (EXISTS ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt) + add_error("install_manifest.txt should be absent after an interrupted install") +endif() diff --git a/Tests/RunCMake/Instrumentation/check-interrupted.cmake b/Tests/RunCMake/Instrumentation/check-interrupted.cmake index 00dceed0e9..d6cd07d80f 100644 --- a/Tests/RunCMake/Instrumentation/check-interrupted.cmake +++ b/Tests/RunCMake/Instrumentation/check-interrupted.cmake @@ -1,35 +1,6 @@ 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() +# present, marked with the interrupting signal, and the postCMakeBuild hook must +# have been skipped. +check_interrupted_snippet(cmakeBuild postCMakeBuild) diff --git a/Tests/RunCMake/Instrumentation/interrupt-build-result.txt b/Tests/RunCMake/Instrumentation/interrupt-build-signal-result.txt similarity index 100% rename from Tests/RunCMake/Instrumentation/interrupt-build-result.txt rename to Tests/RunCMake/Instrumentation/interrupt-build-signal-result.txt diff --git a/Tests/RunCMake/Instrumentation/interrupt-install-signal-result.txt b/Tests/RunCMake/Instrumentation/interrupt-install-signal-result.txt new file mode 100644 index 0000000000..d81cc0710e --- /dev/null +++ b/Tests/RunCMake/Instrumentation/interrupt-install-signal-result.txt @@ -0,0 +1 @@ +42 diff --git a/Tests/RunCMake/Instrumentation/json.cmake b/Tests/RunCMake/Instrumentation/json.cmake index 1c59475490..2271e9971f 100644 --- a/Tests/RunCMake/Instrumentation/json.cmake +++ b/Tests/RunCMake/Instrumentation/json.cmake @@ -71,3 +71,30 @@ function(json_equals expected_file actual_file) endif() return(PROPAGATE RunCMake_TEST_FAILED ERROR_MESSAGE) endfunction() + +# Verify the aftermath of an interrupted top-level command whose overall +# instrumentation snippet has role `role` and whose post-command hook is `hook`. +# Exactly one such snippet must be present and marked with the interrupting +# signal (any snippet from an earlier uninterrupted warm-up run was collated and +# removed by its hook). The hook must have been skipped entirely, so its +# callback (hook.cmake writes a `${hook}.hook` marker whenever it runs, and any +# earlier copy was removed before the interrupted run) must not have run. +function(check_interrupted_snippet role hook) + file(GLOB snippets LIST_DIRECTORIES false ${v1}/data/${role}-*.json) + list(LENGTH snippets num) + if (NOT num EQUAL 1) + add_error("Expected exactly one ${role} snippet, found ${num}: ${snippets}") + else() + read_json("${snippets}" contents) + string(JSON interruptSignal ERROR_VARIABLE noSignal GET "${contents}" interruptSignal) + if (noSignal OR NOT interruptSignal MATCHES "^[1-9][0-9]*$") + add_error("${role} snippet is not marked interrupted:\n${contents}") + endif() + endif() + + if (EXISTS ${v1}/${hook}.hook) + add_error("${hook} hook should be skipped on interrupt, but it ran") + endif() + + return(PROPAGATE RunCMake_TEST_FAILED ERROR_MESSAGE) +endfunction() diff --git a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt index 3fde043daf..17d3c76703 100644 --- a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt +++ b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt @@ -79,3 +79,13 @@ if (INTERRUPT_BUILD_SRC) COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: end" ) endif() + +if (INSTALL_INTERRUPT) + # Enable parallel install and add several slow install subdirectories so that + # a `cmake --install -j 1` runs long enough to be interrupted with install + # scripts still pending. + set_property(GLOBAL PROPERTY INSTALL_PARALLEL ON) + add_subdirectory(installSlow1) + add_subdirectory(installSlow2) + add_subdirectory(installSlow3) +endif() diff --git a/Tests/RunCMake/Instrumentation/project/installSlow1/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/installSlow1/CMakeLists.txt new file mode 100644 index 0000000000..d816dd7c3a --- /dev/null +++ b/Tests/RunCMake/Instrumentation/project/installSlow1/CMakeLists.txt @@ -0,0 +1,4 @@ +# A deliberately slow install step so that a parallel `cmake --install -j 1` has +# scripts still pending when a user interrupt arrives. The interrupt must stop +# the remaining install scripts from being launched. +install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)") diff --git a/Tests/RunCMake/Instrumentation/project/installSlow2/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/installSlow2/CMakeLists.txt new file mode 100644 index 0000000000..d816dd7c3a --- /dev/null +++ b/Tests/RunCMake/Instrumentation/project/installSlow2/CMakeLists.txt @@ -0,0 +1,4 @@ +# A deliberately slow install step so that a parallel `cmake --install -j 1` has +# scripts still pending when a user interrupt arrives. The interrupt must stop +# the remaining install scripts from being launched. +install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)") diff --git a/Tests/RunCMake/Instrumentation/project/installSlow3/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/installSlow3/CMakeLists.txt new file mode 100644 index 0000000000..d816dd7c3a --- /dev/null +++ b/Tests/RunCMake/Instrumentation/project/installSlow3/CMakeLists.txt @@ -0,0 +1,4 @@ +# A deliberately slow install step so that a parallel `cmake --install -j 1` has +# scripts still pending when a user interrupt arrives. The interrupt must stop +# the remaining install scripts from being launched. +install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)") diff --git a/Tests/RunCMake/Instrumentation/query/interrupt.json.in b/Tests/RunCMake/Instrumentation/query/interrupt-build.json.in similarity index 100% rename from Tests/RunCMake/Instrumentation/query/interrupt.json.in rename to Tests/RunCMake/Instrumentation/query/interrupt-build.json.in diff --git a/Tests/RunCMake/Instrumentation/query/interrupt-install.json.in b/Tests/RunCMake/Instrumentation/query/interrupt-install.json.in new file mode 100644 index 0000000000..83c9d0780e --- /dev/null +++ b/Tests/RunCMake/Instrumentation/query/interrupt-install.json.in @@ -0,0 +1,5 @@ +{ + "version": 1, + "hooks": ["postCMakeInstall"], + "callbacks": ["@GET_HOOK@"] +}