refactor(platform): lanceur de processus commun, enfant lié au parent
Le lancement de processus de Studio host (CoreProcess) devient sequa::platform::ChildProcess dans platform/process, pour être partagé par Studio host et le Core (qui lancera le rendu, ADR 0009). Nouvelle option kill_with_parent : l'enfant meurt avec le parent, même brutalement (job object KILL_ON_JOB_CLOSE sous Windows, fork + PR_SET_PDEATHSIG sous Linux, ignorée sous macOS). Les tests de copie fantôme restent dans studio/host/supervisor (test_shadow.cpp) avec l'enfant de test de platform/process.
This commit is contained in:
1 parent
c6aa05680e
commit
464ff6ce88
14 files changed
+447
-123
No files matched your search
@@ -31,6 +31,7 @@ if(SEQUA_BUILD_TESTS)
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
add_subdirectory(platform/process) # lancement de processus enfants (Studio host, Core)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(3d)
|
||||
add_subdirectory(engines/godot/adapter) # Godot lui-même n'est pas compilé ici (SCons, scripts/build-godot.ps1)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# Brique processus commune : lancement et supervision d'un processus enfant
|
||||
# (ex-supervisor du Studio host, ADR 0009). Utilisée par le Studio host, le Core
|
||||
# (lancement du processus de rendu) et le rendu lui-même.
|
||||
|
||||
add_library(sequa_process
|
||||
src/process.cpp)
|
||||
add_library(sequa::process ALIAS sequa_process)
|
||||
|
||||
target_include_directories(sequa_process
|
||||
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_compile_features(sequa_process PUBLIC cxx_std_20)
|
||||
|
||||
# Aucune bibliothèque système à lier à part Windows.h (kernel32 est implicite) ;
|
||||
# bcrypt et shell32 ne servent qu'à random et aux autres briques du host.
|
||||
|
||||
sequa_target_defaults(sequa_process)
|
||||
|
||||
if(SEQUA_BUILD_TESTS)
|
||||
# Processus enfant de test : écrit, attend, sort avec un code donné.
|
||||
add_executable(sequa_process_test_child tests/test_child.cpp)
|
||||
if(WIN32)
|
||||
target_link_libraries(sequa_process_test_child PRIVATE shell32)
|
||||
endif()
|
||||
sequa_target_defaults(sequa_process_test_child)
|
||||
|
||||
add_executable(sequa_process_tests
|
||||
tests/main.cpp
|
||||
tests/test_process.cpp)
|
||||
target_link_libraries(sequa_process_tests PRIVATE sequa::process doctest::doctest)
|
||||
target_compile_definitions(sequa_process_tests PRIVATE
|
||||
SEQUA_TEST_CHILD="$<TARGET_FILE:sequa_process_test_child>")
|
||||
add_dependencies(sequa_process_tests sequa_process_test_child)
|
||||
sequa_target_defaults(sequa_process_tests)
|
||||
doctest_discover_tests(sequa_process_tests)
|
||||
endif()
|
||||
@@ -0,0 +1,24 @@
|
||||
# Plateforme : processus
|
||||
|
||||
Brique commune de lancement et de supervision d'un processus enfant : le Studio
|
||||
host l'utilise pour le Core ([ADR 0006](../../docs/adr/0006-studio-host-et-cycle-de-vie-du-core.md),
|
||||
[spécification Studio - Core §7](../../docs/specs/protocole-studio-core-0.2.md)),
|
||||
le Core la réutilise pour lancer le processus de rendu ([ADR 0009](../../docs/adr/0009-processus-de-rendu-lance-par-le-core.md)),
|
||||
`sequa-render` en disposera pareil pour ses propres enfants.
|
||||
|
||||
## API (`sequa::platform`)
|
||||
|
||||
| Pièce | Rôle |
|
||||
|---|---|
|
||||
| `current_process_id` | PID du processus courant (nommage des copies fantômes, etc.) |
|
||||
| `is_process_alive` | Un PID est-il encore vivant ? Pré-validation seulement (ADR 0006 : la poignée de main reste l'arbitre) |
|
||||
| `LaunchOptions` | Journal en ajout (stdout et stderr fenêtres), console propre (`--core-console`), `kill_with_parent` (« mourir avec le parent » : job object `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` sous Windows, `fork` + `PR_SET_PDEATHSIG` sous Linux ; ignoré sous macOS pour l'instant) |
|
||||
| `ChildProcess` | Lancement (arguments correctement cités sous Windows, journal en ajout, seul le handle du journal est hérité sous Windows), `running`, `wait_for`, `terminate` ; le destructeur NE tue PAS le processus (le cycle de vie appartient à l'appelant, §7.6) |
|
||||
|
||||
Le code POSIX (`posix_spawn`, `waitpid`, 0600) est écrit mais n'a pas encore été
|
||||
compilé ni testé (vérification locale Windows uniquement, ADR 0004).
|
||||
|
||||
## Licences
|
||||
|
||||
- MPL 2.0.
|
||||
- Aucune dépendance externe (doctest seul, pour les tests : MIT).
|
||||
+29
-17
@@ -8,9 +8,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sequa::studio::host {
|
||||
namespace sequa::platform {
|
||||
|
||||
// PID du processus courant (utilisé pour nommer les copies fantômes, §7.5).
|
||||
// PID du processus courant (utilisé pour nommer les copies fantômes et vérifier
|
||||
// un processus, §7.5 du protocole Studio - Core).
|
||||
[[nodiscard]] std::uint32_t current_process_id();
|
||||
|
||||
// Un PID désigne-t-il un processus encore vivant ? Pré-validation d'un fichier
|
||||
@@ -19,10 +20,10 @@ namespace sequa::studio::host {
|
||||
// un processus d'un autre utilisateur (EPERM) est considéré vivant.
|
||||
[[nodiscard]] bool is_process_alive(std::uint32_t pid);
|
||||
|
||||
// Options de lancement (§7.5).
|
||||
// Options de lancement (§7.5 du protocole Studio - Core).
|
||||
struct LaunchOptions {
|
||||
// Journal où rediriger stdout ET stderr EN AJOUT. Si non vide, une ligne
|
||||
// d'en-tête horodatée est écrite par le superviseur AVANT le lancement.
|
||||
// d'en-tête horodatée est écrite par le lanceur AVANT le lancement.
|
||||
// Le fichier est créé si absent ; son dossier doit exister.
|
||||
std::filesystem::path log_path;
|
||||
|
||||
@@ -31,32 +32,43 @@ struct LaunchOptions {
|
||||
// §7.1). Ignoré sous POSIX (pas de notion de console). false : aucune
|
||||
// fenêtre (CREATE_NO_WINDOW sous Windows).
|
||||
bool own_console = false;
|
||||
|
||||
// true : l'enfant meurt dès que le processus lanceur meurt, même brutalement
|
||||
// (plantage, TerminateProcess) : jamais d'orphelin bloqué (ADR 0009). Sous
|
||||
// Windows : l'enfant est affecté à un job object du parent, créé au premier
|
||||
// lancement (std::call_once) avec JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE et
|
||||
// jamais refermé : quand le parent meurt, la table de handles disparaît, le
|
||||
// handle du job se ferme et le système tue tout le job. Linux : posix_spawn
|
||||
// ne permet pas de PR_SET_PDEATHSIG, donc fork + prctl dans l'enfant.
|
||||
// macOS : ignoré pour l'instant (pas d'équivalent simple).
|
||||
bool kill_with_parent = false;
|
||||
};
|
||||
|
||||
// Processus lancé et supervisé (le Core en pratique) : handle de processus
|
||||
// sous Windows, pid + waitpid sous POSIX (ADR 0006 : identification par handle,
|
||||
// pas par le seul PID).
|
||||
// Processus enfant lancé et supervisé (le Core pour le Studio host, le rendu
|
||||
// pour le Core, ADR 0009) : handle de processus sous Windows, pid + waitpid
|
||||
// sous POSIX (ADR 0006 : identification par handle, pas par le seul PID).
|
||||
//
|
||||
// LE CYCLE DE VIE APPARTIENT À L'APPELANT : le destructeur NE tue PAS le
|
||||
// processus (§7.6 : l'appelant choisit d'arrêter, de détacher ou de laisser
|
||||
// vivre). Non copiable, déplaçable.
|
||||
class CoreProcess {
|
||||
class ChildProcess {
|
||||
public:
|
||||
// Lance exe avec args (chaînes UTF-8 ; sous Windows chaque argument est cité
|
||||
// à la manière compatible CommandLineToArgvW, espaces et guillemets
|
||||
// compris). Échec (exécutable absent, générateur d'arguments invalide,
|
||||
// API système en échec) -> nullopt, jamais d'exception.
|
||||
[[nodiscard]] static std::optional<CoreProcess> launch(
|
||||
// API système en échec, assignation au job object impossible ->
|
||||
// l'enfant est terminé) -> nullopt, jamais d'exception.
|
||||
[[nodiscard]] static std::optional<ChildProcess> launch(
|
||||
std::filesystem::path const& exe,
|
||||
std::vector<std::string> const& args,
|
||||
LaunchOptions options = {});
|
||||
|
||||
// Ne tue PAS le processus ; libère seulement le handle.
|
||||
~CoreProcess();
|
||||
CoreProcess(CoreProcess&& other) noexcept;
|
||||
CoreProcess& operator=(CoreProcess&& other) noexcept;
|
||||
CoreProcess(CoreProcess const&) = delete;
|
||||
CoreProcess& operator=(CoreProcess const&) = delete;
|
||||
~ChildProcess();
|
||||
ChildProcess(ChildProcess&& other) noexcept;
|
||||
ChildProcess& operator=(ChildProcess&& other) noexcept;
|
||||
ChildProcess(ChildProcess const&) = delete;
|
||||
ChildProcess& operator=(ChildProcess const&) = delete;
|
||||
|
||||
[[nodiscard]] std::uint32_t pid() const noexcept;
|
||||
|
||||
@@ -74,7 +86,7 @@ public:
|
||||
void terminate();
|
||||
|
||||
private:
|
||||
CoreProcess() = default; // seulement via launch()
|
||||
ChildProcess() = default; // seulement via launch()
|
||||
|
||||
// Sous POSIX : récolte le code de sortie en sondant pendant au plus
|
||||
// grace_ms ; retourne true si le processus s'est révélé terminé.
|
||||
@@ -85,4 +97,4 @@ private:
|
||||
mutable std::optional<int> code_; // code de sortie connu (POSIX : waitpid)
|
||||
};
|
||||
|
||||
} // namespace sequa::studio::host
|
||||
} // namespace sequa::platform
|
||||
@@ -1,7 +1,5 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/studio/host/process.hpp"
|
||||
|
||||
#include "sequa/studio/host/time.hpp"
|
||||
#include "sequa/platform/process.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -16,24 +14,53 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <format>
|
||||
#include <mutex>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
namespace sequa::studio::host {
|
||||
namespace sequa::platform {
|
||||
|
||||
namespace {
|
||||
|
||||
// Horodatage ISO 8601 UTC pour l'en-tête de journal. La brique processus ne
|
||||
// dépend pas du Studio host : réimplémentation locale, minimale.
|
||||
std::string utc_now_iso8601()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SYSTEMTIME now{};
|
||||
GetSystemTime(&now);
|
||||
return std::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}Z",
|
||||
static_cast<int>(now.wYear), static_cast<int>(now.wMonth),
|
||||
static_cast<int>(now.wDay), static_cast<int>(now.wHour),
|
||||
static_cast<int>(now.wMinute), static_cast<int>(now.wSecond));
|
||||
#else
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
if (gmtime_r(&now, &tm) == nullptr) {
|
||||
return {};
|
||||
}
|
||||
char buffer[32];
|
||||
const std::size_t written = std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||
return std::string(buffer, written);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string header_line()
|
||||
{
|
||||
return "\n===== " + utc_now_iso8601() + " : lancement par sequa-studio =====\n";
|
||||
return "\n===== " + utc_now_iso8601() + " : lancement par SEQUA =====\n";
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -79,6 +106,34 @@ void append_quoted(std::wstring& command_line, const std::wstring& argument)
|
||||
command_line += L'"';
|
||||
}
|
||||
|
||||
// Job object unique du processus courant, créé au premier lancement demandant
|
||||
// « mourir avec le parent » (std::call_once : plusieurs fils peuvent lancer
|
||||
// simultanément). JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE tue tous les processus du
|
||||
// job quand le DERNIER handle sur le job se ferme : comme ce handle n'est
|
||||
// jamais refermé ni hérité par l'enfant, c'est la mort du parent (exécution
|
||||
// normale OU brutale, qui détruit toute la table de handles) qui le referme.
|
||||
// nullptr si la création a échoué : le lancement renverra nullopt.
|
||||
HANDLE job_kill_on_close()
|
||||
{
|
||||
static std::once_flag once;
|
||||
static HANDLE job = nullptr;
|
||||
std::call_once(once, [] {
|
||||
HANDLE created = CreateJobObjectW(nullptr, nullptr);
|
||||
if (created == nullptr) {
|
||||
return;
|
||||
}
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if (SetInformationJobObject(created, JobObjectExtendedLimitInformation, &limits,
|
||||
sizeof(limits))) {
|
||||
job = created;
|
||||
} else {
|
||||
CloseHandle(created);
|
||||
}
|
||||
});
|
||||
return job;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
@@ -105,8 +160,9 @@ bool is_process_alive(std::uint32_t pid)
|
||||
return alive;
|
||||
}
|
||||
|
||||
std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe, const std::vector<std::string>& args,
|
||||
LaunchOptions options)
|
||||
std::optional<ChildProcess> ChildProcess::launch(const std::filesystem::path& exe,
|
||||
const std::vector<std::string>& args,
|
||||
LaunchOptions options)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(exe, ec)) {
|
||||
@@ -133,7 +189,8 @@ std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe,
|
||||
WriteFile(log, header.data(), static_cast<DWORD>(header.size()), &written, nullptr);
|
||||
}
|
||||
|
||||
// Seul le handle du journal est hérité : jamais les autres handles du host.
|
||||
// Seul le handle du journal est hérité : jamais les autres handles du parent
|
||||
// (et notamment pas le handle du job object).
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
std::vector<unsigned char> attribute_storage;
|
||||
@@ -158,6 +215,13 @@ std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe,
|
||||
inherit = TRUE;
|
||||
}
|
||||
|
||||
// « Mourir avec le parent » : l'enfant est créé SUSPENDU, affecté au job
|
||||
// AVANT de tourner, puis seulement relancé (sinon une fenêtre minuscule
|
||||
// laisse l'enfant s'exécuter hors du job et survivre au parent).
|
||||
if (options.kill_with_parent) {
|
||||
flags |= CREATE_SUSPENDED;
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION info{};
|
||||
const BOOL created = CreateProcessW(nullptr, command_line.data(), nullptr, nullptr, inherit, flags, nullptr,
|
||||
nullptr, &startup.StartupInfo, &info);
|
||||
@@ -170,27 +234,41 @@ std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe,
|
||||
if (!created) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Affectation au job (le handle du fil est fermé dans tous les cas, échec ou
|
||||
// succès) puis seulement reprise de l'enfant. Tout échec balaie l'enfant
|
||||
// Suspendu, qui n'a exécuté aucun code : TerminateProcess puis nullopt.
|
||||
if (options.kill_with_parent) {
|
||||
HANDLE job = job_kill_on_close();
|
||||
if (job == nullptr || !AssignProcessToJobObject(job, info.hProcess)
|
||||
|| ResumeThread(info.hThread) == static_cast<DWORD>(-1)) {
|
||||
(void)!TerminateProcess(info.hProcess, 1);
|
||||
CloseHandle(info.hThread);
|
||||
CloseHandle(info.hProcess);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
CloseHandle(info.hThread);
|
||||
|
||||
CoreProcess process;
|
||||
ChildProcess process;
|
||||
process.pid_ = static_cast<std::uint32_t>(info.dwProcessId);
|
||||
process.handle_ = info.hProcess;
|
||||
return process;
|
||||
}
|
||||
|
||||
CoreProcess::~CoreProcess()
|
||||
ChildProcess::~ChildProcess()
|
||||
{
|
||||
if (handle_ != nullptr) {
|
||||
CloseHandle(static_cast<HANDLE>(handle_));
|
||||
}
|
||||
}
|
||||
|
||||
bool CoreProcess::running() const
|
||||
bool ChildProcess::running() const
|
||||
{
|
||||
return handle_ != nullptr && WaitForSingleObject(static_cast<HANDLE>(handle_), 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
std::optional<int> CoreProcess::wait_for(std::chrono::milliseconds timeout) const
|
||||
std::optional<int> ChildProcess::wait_for(std::chrono::milliseconds timeout) const
|
||||
{
|
||||
if (code_) {
|
||||
return code_;
|
||||
@@ -210,7 +288,7 @@ std::optional<int> CoreProcess::wait_for(std::chrono::milliseconds timeout) cons
|
||||
return code_;
|
||||
}
|
||||
|
||||
void CoreProcess::terminate()
|
||||
void ChildProcess::terminate()
|
||||
{
|
||||
if (handle_ != nullptr && running()) {
|
||||
TerminateProcess(static_cast<HANDLE>(handle_), 1);
|
||||
@@ -218,7 +296,7 @@ void CoreProcess::terminate()
|
||||
}
|
||||
}
|
||||
|
||||
bool CoreProcess::reap_deadline_ms(int /*grace_ms*/) const
|
||||
bool ChildProcess::reap_deadline_ms(int /*grace_ms*/) const
|
||||
{
|
||||
return !running();
|
||||
}
|
||||
@@ -238,14 +316,89 @@ bool is_process_alive(std::uint32_t pid)
|
||||
return ::kill(static_cast<pid_t>(pid), 0) == 0 || errno == EPERM;
|
||||
}
|
||||
|
||||
std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe, const std::vector<std::string>& args,
|
||||
LaunchOptions options)
|
||||
std::optional<ChildProcess> ChildProcess::launch(const std::filesystem::path& exe,
|
||||
const std::vector<std::string>& args,
|
||||
LaunchOptions options)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(exe, ec)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Tout (argv, ouverts, chemins) est préparé AVANT fork : entre fork et exec,
|
||||
// seules des fonctions async-signal-safe sont autorisées. La console propre
|
||||
// (« own_console ») est ignorée sous POSIX, comme pour posix_spawn.
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
// Linux : posix_spawn ne permet pas PR_SET_PDEATHSIG. Pour « mourir avec le
|
||||
// parent », on passe par fork + prctl dans l'enfant (ADR 0009).
|
||||
if (options.kill_with_parent) {
|
||||
const pid_t parent = getpid();
|
||||
|
||||
// argv en chaînes câblées, stables pendant l'enfant.
|
||||
std::vector<std::string> storage;
|
||||
storage.reserve(args.size() + 1);
|
||||
storage.push_back(exe.string());
|
||||
storage.insert(storage.end(), args.begin(), args.end());
|
||||
std::vector<char*> argv;
|
||||
for (std::string& argument : storage) {
|
||||
argv.push_back(argument.data());
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
|
||||
// Chemin de journal préconverti (l'open est fait dans l'enfant : les fd
|
||||
// hérités seraient en plus à replier après l'échec de exec).
|
||||
const std::string exe_path = storage.front();
|
||||
const std::string log_path = options.log_path.string();
|
||||
const bool redirect = !log_path.empty();
|
||||
|
||||
const pid_t pid = ::fork();
|
||||
if (pid < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (pid == 0) {
|
||||
// Enfant : async-signal-safe uniquement ci-dessous.
|
||||
// La mort du parent (même SIGKILL, même TerminateProcess côté
|
||||
// système d'exploitation) tue l'enfant avec SIGKILL.
|
||||
::prctl(PR_SET_PDEATHSIG, static_cast<unsigned long>(SIGKILL));
|
||||
// Si le parent est déjà mort entre fork et prctl, on ne reste pas
|
||||
// orphelin : le re-parentage (ppid = 1) est l'indice.
|
||||
if (::getppid() != parent) {
|
||||
::_exit(127);
|
||||
}
|
||||
// Mêmes redirections que le chemin posix_spawn : /dev/null sur 0,
|
||||
// journal en ajout sur 1 puis dupliqué sur 2.
|
||||
const int nothing = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
|
||||
if (nothing < 0) {
|
||||
::_exit(127);
|
||||
}
|
||||
if (::dup2(nothing, 0) < 0) {
|
||||
::_exit(127);
|
||||
}
|
||||
::close(nothing);
|
||||
if (redirect) {
|
||||
const int log = ::open(log_path.c_str(), O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0600);
|
||||
if (log < 0) {
|
||||
::_exit(127);
|
||||
}
|
||||
if (::dup2(log, 1) < 0 || ::dup2(log, 2) < 0) {
|
||||
::_exit(127);
|
||||
}
|
||||
::close(log);
|
||||
}
|
||||
::execv(exe_path.c_str(), argv.data());
|
||||
::_exit(127); // execv ne rend que par l'échec
|
||||
}
|
||||
|
||||
ChildProcess process;
|
||||
process.pid_ = static_cast<std::uint32_t>(pid);
|
||||
return process;
|
||||
}
|
||||
#else
|
||||
(void)options.kill_with_parent; // macOS : ignoré, pas d'équivalent simple pour l'instant
|
||||
#endif
|
||||
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
if (!options.log_path.empty()) {
|
||||
@@ -278,19 +431,19 @@ std::optional<CoreProcess> CoreProcess::launch(const std::filesystem::path& exe,
|
||||
if (result != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
CoreProcess process;
|
||||
ChildProcess process;
|
||||
process.pid_ = static_cast<std::uint32_t>(pid);
|
||||
return process;
|
||||
}
|
||||
|
||||
CoreProcess::~CoreProcess() = default; // pas de handle ; le processus n'est pas tué
|
||||
ChildProcess::~ChildProcess() = default; // pas de handle ; le processus n'est pas tué
|
||||
|
||||
bool CoreProcess::running() const
|
||||
bool ChildProcess::running() const
|
||||
{
|
||||
return pid_ != 0 && !wait_for(std::chrono::milliseconds(0));
|
||||
}
|
||||
|
||||
std::optional<int> CoreProcess::wait_for(std::chrono::milliseconds timeout) const
|
||||
std::optional<int> ChildProcess::wait_for(std::chrono::milliseconds timeout) const
|
||||
{
|
||||
if (code_ || pid_ == 0) {
|
||||
return code_;
|
||||
@@ -313,12 +466,12 @@ std::optional<int> CoreProcess::wait_for(std::chrono::milliseconds timeout) cons
|
||||
}
|
||||
}
|
||||
|
||||
bool CoreProcess::reap_deadline_ms(int grace_ms) const
|
||||
bool ChildProcess::reap_deadline_ms(int grace_ms) const
|
||||
{
|
||||
return wait_for(std::chrono::milliseconds(grace_ms)).has_value();
|
||||
}
|
||||
|
||||
void CoreProcess::terminate()
|
||||
void ChildProcess::terminate()
|
||||
{
|
||||
if (pid_ == 0 || code_) {
|
||||
return;
|
||||
@@ -332,16 +485,16 @@ void CoreProcess::terminate()
|
||||
|
||||
#endif
|
||||
|
||||
CoreProcess::CoreProcess(CoreProcess&& other) noexcept
|
||||
ChildProcess::ChildProcess(ChildProcess&& other) noexcept
|
||||
: pid_(std::exchange(other.pid_, 0)), handle_(std::exchange(other.handle_, nullptr)),
|
||||
code_(std::exchange(other.code_, std::nullopt))
|
||||
{
|
||||
}
|
||||
|
||||
CoreProcess& CoreProcess::operator=(CoreProcess&& other) noexcept
|
||||
ChildProcess& ChildProcess::operator=(ChildProcess&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
this->~CoreProcess();
|
||||
this->~ChildProcess();
|
||||
pid_ = std::exchange(other.pid_, 0);
|
||||
handle_ = std::exchange(other.handle_, nullptr);
|
||||
code_ = std::exchange(other.code_, std::nullopt);
|
||||
@@ -349,9 +502,9 @@ CoreProcess& CoreProcess::operator=(CoreProcess&& other) noexcept
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::uint32_t CoreProcess::pid() const noexcept
|
||||
std::uint32_t ChildProcess::pid() const noexcept
|
||||
{
|
||||
return pid_;
|
||||
}
|
||||
|
||||
} // namespace sequa::studio::host
|
||||
} // namespace sequa::platform
|
||||
@@ -0,0 +1,3 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include <doctest/doctest.h>
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Dossier temporaire propre à un test, supprimé à la fin. Jamais le vrai dossier
|
||||
// d'exécution de SEQUA.
|
||||
#pragma once
|
||||
|
||||
#include "sequa/platform/process.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace sequa::platform::testing {
|
||||
|
||||
class TempDir {
|
||||
public:
|
||||
TempDir()
|
||||
: path_(std::filesystem::temp_directory_path()
|
||||
/ ("sequa-process-" + std::to_string(current_process_id()) + "-"
|
||||
+ std::to_string(++counter_)))
|
||||
{
|
||||
std::filesystem::create_directories(path_);
|
||||
}
|
||||
~TempDir()
|
||||
{
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(path_, ignored);
|
||||
}
|
||||
TempDir(const TempDir&) = delete;
|
||||
TempDir& operator=(const TempDir&) = delete;
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; }
|
||||
|
||||
private:
|
||||
// Compteur de secours : deux dossiers créés dans la même milliseconde par le
|
||||
// même processus ne se chevauchent pas.
|
||||
inline static int counter_ = 0;
|
||||
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
} // namespace sequa::platform::testing
|
||||
@@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// Processus enfant des tests du superviseur. Arguments traités dans l'ordre :
|
||||
// Processus enfant des tests de la brique processus. Arguments traités dans l'ordre :
|
||||
// --out <texte> ligne sur stdout --err <texte> ligne sur stderr
|
||||
// --echo-rest recopie chaque argument suivant entre crochets, un par ligne
|
||||
// --sleep <ms> attend --exit <code> sort avec ce code
|
||||
// Les arguments sont relus en UTF-16 sous Windows et écrits en UTF-8 : on vérifie ce que
|
||||
// le superviseur a réellement transmis, pas la conversion de la page de code locale.
|
||||
// le lanceur a réellement transmis, pas la conversion de la page de code locale.
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
+45
-56
@@ -1,8 +1,5 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/studio/host/files.hpp"
|
||||
#include "sequa/studio/host/paths.hpp"
|
||||
#include "sequa/studio/host/process.hpp"
|
||||
#include "sequa/studio/host/shadow.hpp"
|
||||
#include "sequa/platform/process.hpp"
|
||||
|
||||
#include "temp_dir.hpp"
|
||||
|
||||
@@ -10,16 +7,32 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace sequa::studio::host;
|
||||
using namespace sequa::platform;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
const std::filesystem::path child = SEQUA_TEST_CHILD;
|
||||
|
||||
// Les journaux de test restent petits : lecture entière, lignes séparées
|
||||
// (équivalent local à read_last_lines du supervisor, qui reste du côté Studio).
|
||||
std::vector<std::string> read_lines(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
std::vector<std::string> lines;
|
||||
for (std::string line; std::getline(file, line);) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool contains(const std::vector<std::string>& lines, const std::string& expected)
|
||||
{
|
||||
return std::find(lines.begin(), lines.end(), expected) != lines.end();
|
||||
@@ -30,21 +43,21 @@ bool contains(const std::vector<std::string>& lines, const std::string& expected
|
||||
TEST_CASE("Processus : code de sortie et journal en ajout (stdout et stderr)")
|
||||
{
|
||||
testing::TempDir dir;
|
||||
const auto log = log_file_path(dir.path(), 9100);
|
||||
const auto log = dir.path() / "journal-9100.log";
|
||||
LaunchOptions options;
|
||||
options.log_path = log;
|
||||
|
||||
auto first = CoreProcess::launch(child, {"--out", "premier", "--err", "erreur", "--exit", "7"}, options);
|
||||
auto first = ChildProcess::launch(child, {"--out", "premier", "--err", "erreur", "--exit", "7"}, options);
|
||||
REQUIRE(first.has_value());
|
||||
CHECK(first->pid() != 0);
|
||||
CHECK(first->wait_for(5s) == 7);
|
||||
CHECK_FALSE(first->running());
|
||||
|
||||
auto second = CoreProcess::launch(child, {"--out", "second"}, options);
|
||||
auto second = ChildProcess::launch(child, {"--out", "second"}, options);
|
||||
REQUIRE(second.has_value());
|
||||
CHECK(second->wait_for(5s) == 0);
|
||||
|
||||
const auto lines = read_last_lines(log, 100);
|
||||
const auto lines = read_lines(log);
|
||||
CHECK(contains(lines, "premier"));
|
||||
CHECK(contains(lines, "erreur"));
|
||||
CHECK(contains(lines, "second")); // le premier lancement n'a pas été écrasé
|
||||
@@ -63,10 +76,10 @@ TEST_CASE("Processus : arguments avec espaces, guillemets et antislashs")
|
||||
"", "accentué é"};
|
||||
std::vector<std::string> args = {"--echo-rest"};
|
||||
args.insert(args.end(), tricky.begin(), tricky.end());
|
||||
auto process = CoreProcess::launch(child, args, options);
|
||||
auto process = ChildProcess::launch(child, args, options);
|
||||
REQUIRE(process.has_value());
|
||||
REQUIRE(process->wait_for(5s) == 0);
|
||||
const auto lines = read_last_lines(options.log_path, 100);
|
||||
const auto lines = read_lines(options.log_path);
|
||||
for (const std::string& argument : tricky) {
|
||||
CAPTURE(argument);
|
||||
CHECK(contains(lines, "[" + argument + "]"));
|
||||
@@ -75,7 +88,7 @@ TEST_CASE("Processus : arguments avec espaces, guillemets et antislashs")
|
||||
|
||||
TEST_CASE("Processus : wait_for avec delai, terminate, is_process_alive")
|
||||
{
|
||||
auto process = CoreProcess::launch(child, {"--sleep", "10000"});
|
||||
auto process = ChildProcess::launch(child, {"--sleep", "10000"});
|
||||
REQUIRE(process.has_value());
|
||||
CHECK(process->running());
|
||||
CHECK(is_process_alive(process->pid()));
|
||||
@@ -89,10 +102,10 @@ TEST_CASE("Processus : wait_for avec delai, terminate, is_process_alive")
|
||||
|
||||
TEST_CASE("Processus : executable absent -> nullopt ; deplacement")
|
||||
{
|
||||
CHECK_FALSE(CoreProcess::launch("introuvable-sequa.exe", {}).has_value());
|
||||
auto process = CoreProcess::launch(child, {"--exit", "3"});
|
||||
CHECK_FALSE(ChildProcess::launch("introuvable-sequa.exe", {}).has_value());
|
||||
auto process = ChildProcess::launch(child, {"--exit", "3"});
|
||||
REQUIRE(process.has_value());
|
||||
CoreProcess moved = std::move(*process);
|
||||
ChildProcess moved = std::move(*process);
|
||||
CHECK(moved.wait_for(5s) == 3);
|
||||
}
|
||||
|
||||
@@ -100,52 +113,28 @@ TEST_CASE("Processus : le destructeur ne tue pas le processus")
|
||||
{
|
||||
std::uint32_t pid = 0;
|
||||
{
|
||||
auto process = CoreProcess::launch(child, {"--sleep", "1500"});
|
||||
auto process = ChildProcess::launch(child, {"--sleep", "1500"});
|
||||
REQUIRE(process.has_value());
|
||||
pid = process->pid();
|
||||
}
|
||||
CHECK(is_process_alive(pid));
|
||||
}
|
||||
|
||||
TEST_CASE("Copie fantome : nom, nettoyage, copie en cours d'execution conservee")
|
||||
TEST_CASE("Processus : kill_with_parent (job object sous Windows, fork + PR_SET_PDEATHSIG sous Linux)")
|
||||
{
|
||||
testing::TempDir dir;
|
||||
const auto bin = bin_dir(dir.path());
|
||||
const auto first = make_shadow_copy(child, bin);
|
||||
const auto second = make_shadow_copy(child, bin); // même seconde : suffixe -1
|
||||
REQUIRE(first.has_value());
|
||||
REQUIRE(second.has_value());
|
||||
CHECK(*first != *second);
|
||||
CHECK(is_shadow_copy_name(first->filename().string()));
|
||||
CHECK(is_shadow_copy_name(second->filename().string()));
|
||||
CHECK_FALSE(make_shadow_copy(dir.path() / "absent.exe", bin).has_value());
|
||||
|
||||
// Un fichier étranger dans bin/ n'est jamais touché.
|
||||
const auto foreign = bin / "autre-outil.exe";
|
||||
std::filesystem::copy_file(child, foreign);
|
||||
|
||||
auto running = CoreProcess::launch(*first, {"--sleep", "3000"});
|
||||
REQUIRE(running.has_value());
|
||||
const std::size_t removed = cleanup_shadow_copies(bin);
|
||||
#if defined(_WIN32)
|
||||
CHECK(removed == 1); // la copie en cours d'exécution est verrouillée
|
||||
CHECK(std::filesystem::exists(*first));
|
||||
#else
|
||||
CHECK(removed == 2);
|
||||
#endif
|
||||
CHECK_FALSE(std::filesystem::exists(*second));
|
||||
CHECK(std::filesystem::exists(foreign));
|
||||
running->terminate();
|
||||
CHECK(cleanup_shadow_copies(bin) <= 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Copie fantome : reconnaissance du motif")
|
||||
{
|
||||
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234.exe"));
|
||||
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234-2"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-2026110XT210000Z-1.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z-12a.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("autre-20261106T210000Z-1.exe"));
|
||||
// Dans le même processus de test, on ne peut pas observer la mort du parent
|
||||
// (c'est ce processus qui porte le test). On vérifie donc ici que le
|
||||
// lancement avec l'option réussit, que l'enfant tourne et reste supervisable
|
||||
// normalement, et que terminate() rend la sortie comme sans l'option ; la
|
||||
// mort du parent (job object refermé par la disparition de ses handles,
|
||||
// SIGKILL de PR_SET_PDEATHSIG) est le mécanisme système lui-même, à
|
||||
// confirmer en dehors du test unitaire (ADR 0009).
|
||||
LaunchOptions options;
|
||||
options.kill_with_parent = true;
|
||||
auto process = ChildProcess::launch(child, {"--sleep", "10000"}, options);
|
||||
REQUIRE(process.has_value());
|
||||
CHECK(process->running());
|
||||
process->terminate();
|
||||
CHECK(process->wait_for(2s).has_value());
|
||||
CHECK_FALSE(process->running());
|
||||
}
|
||||
@@ -140,7 +140,7 @@ std::string StudioHost::launch_core()
|
||||
LaunchOptions launch;
|
||||
launch.own_console = config_.core_console;
|
||||
launch.log_path = log_file_path(config_.run_dir, config_.core_port);
|
||||
process_ = CoreProcess::launch(*copy, args, launch);
|
||||
process_ = sequa::platform::ChildProcess::launch(*copy, args, launch);
|
||||
if (!process_) {
|
||||
return "Lancement du Core impossible : " + copy->string();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "sequa/studio/host/client.hpp"
|
||||
#include "sequa/studio/host/process.hpp"
|
||||
#include "sequa/studio/host/run_file.hpp"
|
||||
|
||||
#include "sequa/platform/process.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <chrono>
|
||||
@@ -19,6 +20,12 @@
|
||||
|
||||
namespace sequa::studio::host {
|
||||
|
||||
// Les utilitaires et lancement de processus vivent désormais dans la brique
|
||||
// commune sequa::platform::process (ADR 0009) ; ces alias gardent les usages du
|
||||
// Studio host (app, tests) fonctionnels.
|
||||
using sequa::platform::is_process_alive;
|
||||
using sequa::platform::LaunchOptions;
|
||||
|
||||
struct HostConfig {
|
||||
std::filesystem::path core_exe; // exécutable SOURCE du Core (jamais lancé directement)
|
||||
std::filesystem::path run_dir; // dossier d'exécution (§7.4)
|
||||
@@ -89,7 +96,7 @@ private:
|
||||
|
||||
HostConfig config_;
|
||||
std::unique_ptr<CoreClient> client_;
|
||||
std::optional<CoreProcess> process_; // vide si le Core a été rejoint (lancé par un autre Studio)
|
||||
std::optional<sequa::platform::ChildProcess> process_; // vide si le Core a été rejoint (lancé par un autre Studio)
|
||||
RunFile run_;
|
||||
bool attached_existing_ = false;
|
||||
bool linked_ = true;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# Brique supervisor du Studio host : lancement du Core depuis une copie fantôme, handle de
|
||||
# processus, fichier d'exécution, journaux, surveillance de l'exécutable source
|
||||
# (docs/specs/protocole-studio-core-0.2.md §7.2 à §7.6).
|
||||
# Brique supervisor du Studio host : lancement du Core depuis une copie fantôme,
|
||||
# fichier d'exécution, journaux, surveillance de l'exécutable source. Le
|
||||
# lancement en lui-même est passé dans la brique commune platform/process
|
||||
# (le Core lance aussi des processus : ADR 0009), que cette brique lie.
|
||||
|
||||
add_library(sequa_host_supervisor
|
||||
src/files.cpp
|
||||
src/paths.cpp
|
||||
src/process.cpp
|
||||
src/random.cpp
|
||||
src/run_file.cpp
|
||||
src/shadow.cpp
|
||||
@@ -15,29 +15,25 @@ add_library(sequa_host_supervisor
|
||||
add_library(sequa::host_supervisor ALIAS sequa_host_supervisor)
|
||||
target_include_directories(sequa_host_supervisor PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_compile_features(sequa_host_supervisor PUBLIC cxx_std_20)
|
||||
target_link_libraries(sequa_host_supervisor PRIVATE nlohmann_json::nlohmann_json)
|
||||
target_link_libraries(sequa_host_supervisor
|
||||
PUBLIC sequa::process
|
||||
PRIVATE nlohmann_json::nlohmann_json)
|
||||
if(WIN32)
|
||||
target_link_libraries(sequa_host_supervisor PRIVATE bcrypt)
|
||||
endif()
|
||||
sequa_target_defaults(sequa_host_supervisor)
|
||||
|
||||
if(SEQUA_BUILD_TESTS)
|
||||
# Processus enfant de test : écrit, attend, sort avec un code donné.
|
||||
add_executable(sequa_host_test_child tests/test_child.cpp)
|
||||
if(WIN32)
|
||||
target_link_libraries(sequa_host_test_child PRIVATE shell32)
|
||||
endif()
|
||||
sequa_target_defaults(sequa_host_test_child)
|
||||
|
||||
add_executable(sequa_host_supervisor_tests
|
||||
tests/main.cpp
|
||||
tests/test_files.cpp
|
||||
tests/test_process.cpp
|
||||
tests/test_run_file.cpp)
|
||||
tests/test_run_file.cpp
|
||||
tests/test_shadow.cpp)
|
||||
target_link_libraries(sequa_host_supervisor_tests PRIVATE sequa::host_supervisor doctest::doctest)
|
||||
# Enfant de test fourni par platform/process (ajouté avant studio/host).
|
||||
target_compile_definitions(sequa_host_supervisor_tests PRIVATE
|
||||
SEQUA_TEST_CHILD="$<TARGET_FILE:sequa_host_test_child>")
|
||||
add_dependencies(sequa_host_supervisor_tests sequa_host_test_child)
|
||||
SEQUA_TEST_CHILD="$<TARGET_FILE:sequa_process_test_child>")
|
||||
add_dependencies(sequa_host_supervisor_tests sequa_process_test_child)
|
||||
sequa_target_defaults(sequa_host_supervisor_tests)
|
||||
doctest_discover_tests(sequa_host_supervisor_tests)
|
||||
endif()
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/studio/host/shadow.hpp"
|
||||
|
||||
#include "sequa/studio/host/process.hpp"
|
||||
#include "sequa/platform/process.hpp"
|
||||
#include "sequa/studio/host/time.hpp"
|
||||
|
||||
#include <cctype>
|
||||
@@ -75,7 +75,7 @@ std::optional<std::filesystem::path> make_shadow_copy(const std::filesystem::pat
|
||||
}
|
||||
|
||||
const std::string base =
|
||||
std::string(prefix) + utc_now_compact() + "-" + std::to_string(current_process_id());
|
||||
std::string(prefix) + utc_now_compact() + "-" + std::to_string(sequa::platform::current_process_id());
|
||||
for (int attempt = 0; attempt < 100; ++attempt) {
|
||||
const std::string name =
|
||||
base + (attempt == 0 ? std::string() : "-" + std::to_string(attempt)) + std::string(executable_suffix);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "sequa/platform/process.hpp"
|
||||
#include "sequa/studio/host/paths.hpp"
|
||||
#include "sequa/studio/host/shadow.hpp"
|
||||
|
||||
#include "temp_dir.hpp"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
using namespace sequa::studio::host;
|
||||
|
||||
namespace {
|
||||
|
||||
// Enfant de test de platform/process : écrit, attend, sort avec un code donné.
|
||||
const std::filesystem::path child = SEQUA_TEST_CHILD;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Copie fantome : nom, nettoyage, copie en cours d'execution conservee")
|
||||
{
|
||||
testing::TempDir dir;
|
||||
const auto bin = bin_dir(dir.path());
|
||||
const auto first = make_shadow_copy(child, bin);
|
||||
const auto second = make_shadow_copy(child, bin); // même seconde : suffixe -1
|
||||
REQUIRE(first.has_value());
|
||||
REQUIRE(second.has_value());
|
||||
CHECK(*first != *second);
|
||||
CHECK(is_shadow_copy_name(first->filename().string()));
|
||||
CHECK(is_shadow_copy_name(second->filename().string()));
|
||||
CHECK_FALSE(make_shadow_copy(dir.path() / "absent.exe", bin).has_value());
|
||||
|
||||
// Un fichier étranger dans bin/ n'est jamais touché.
|
||||
const auto foreign = bin / "autre-outil.exe";
|
||||
std::filesystem::copy_file(child, foreign);
|
||||
|
||||
auto running = sequa::platform::ChildProcess::launch(*first, {"--sleep", "3000"});
|
||||
REQUIRE(running.has_value());
|
||||
const std::size_t removed = cleanup_shadow_copies(bin);
|
||||
#if defined(_WIN32)
|
||||
CHECK(removed == 1); // la copie en cours d'exécution est verrouillée
|
||||
CHECK(std::filesystem::exists(*first));
|
||||
#else
|
||||
CHECK(removed == 2);
|
||||
#endif
|
||||
CHECK_FALSE(std::filesystem::exists(*second));
|
||||
CHECK(std::filesystem::exists(foreign));
|
||||
running->terminate();
|
||||
CHECK(cleanup_shadow_copies(bin) <= 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Copie fantome : reconnaissance du motif")
|
||||
{
|
||||
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234.exe"));
|
||||
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234-2"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-2026110XT210000Z-1.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z-12a.exe"));
|
||||
CHECK_FALSE(is_shadow_copy_name("autre-20261106T210000Z-1.exe"));
|
||||
}
|
||||
Reference in new issue
Block a user