Encapsulate race-safe process slot synchronization and termination in OS layer for Unix and Windows

This commit is contained in:
Andrea Mazzoleni 2026-09-14 14:55:24 +02:00
parent d7e06271d9
commit 9f48c87dd4
5 changed files with 756 additions and 172 deletions

View file

@ -461,20 +461,41 @@ int main(int argc, char* argv[])
/* unblock signals in parent */
sigprocmask(SIG_SETMASK, &oldset, NULL);
/*
* First wait for child termination without reaping (WNOWAIT).
* The child enters zombie state, holding its PID reserved in the kernel
* so no newly created process can receive the same PID. We clear
* child_pid before the final waitpid() reap so the signal forwarder
* never sends signals to a recycled PID.
*/
siginfo_t info;
int wret;
do {
ret = waitpid(pid, &status, 0);
} while (ret == -1 && errno == EINTR); /* retry if interrupted by signal */
memset(&info, 0, sizeof(info));
wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT);
} while (wret == -1 && errno == EINTR);
if (ret == -1) {
if (wret == -1) {
/* LCOV_EXCL_START */
perror("Failed to wait for SnapRAID");
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
/* clear child PID */
/* clear child PID while PID is still reserved in zombie state */
child_pid = 0;
do {
ret = waitpid(pid, &status, 0);
} while (ret == -1 && errno == EINTR); /* retry if interrupted by signal */
if (ret == -1) {
/* LCOV_EXCL_START */
perror("Failed to reap SnapRAID");
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
/* restore default signal handlers */
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);

View file

@ -309,7 +309,7 @@ static int windows_ps(const char* ps_command, char* out, size_t out_size)
0
};
pid = os_spawn(argv, &stdout_fd, 0, 0);
pid = os_spawn(argv, &stdout_fd, 0, 0, 0);
if (pid < 0) {
log_error(errno, "Failed to run PowerShell command '%s' (from os_spawn).\n", ps_command);
return -1;
@ -337,7 +337,7 @@ static int windows_ps(const char* ps_command, char* out, size_t out_size)
close(stdout_fd);
status = 0;
pid_t pid_ret = os_wait(pid, &status);
pid_t pid_ret = os_wait(pid, &status, 0);
os_dispose(pid);
if (pid_ret < 0) {
log_error(errno, "Failed to wait for PowerShell command '%s' (from os_wait).\n", ps_command);

View file

@ -65,6 +65,207 @@ static windows_key_t last_error;
static windows_mutex_t tick_lock;
static uint64_t tick_last;
/**
* Mutex for process reference publication, termination, and unpublication.
*/
static windows_mutex_t exec_mutex;
#define PID_NONE ((pid_t)0) /**< No process is active. */
#define PID_SPAWN ((pid_t)-1) /**< Process creation is in progress. */
#define PID_TERM ((pid_t)-2) /**< Process termination was requested while creation is in progress. */
#define PID_KILL ((pid_t)-3) /**< Process kill was requested; rejects future spawns or terminates in-progress/active processes. */
/**
* Acquire the process execution mutex.
*/
static void exec_lock(void)
{
windows_mutex_lock(&exec_mutex);
}
/**
* Release the process execution mutex.
*/
static void exec_unlock(void)
{
windows_mutex_unlock(&exec_mutex);
}
/**
* Mark the start of process creation in a process-reference slot.
*/
static int pid_spawn_begin(pid_t* pid_slot)
{
if (pid_slot == 0)
return 0;
exec_lock();
if (*pid_slot == PID_KILL) {
exec_unlock();
errno = ECANCELED;
return -1;
}
*pid_slot = PID_SPAWN;
exec_unlock();
return 0;
}
/**
* Forcibly terminate the specified raw process reference.
*/
static int pid_kill(pid_t pid)
{
if (pid <= 0) {
errno = EINVAL;
return -1;
}
HANDLE h = (void*)pid;
if (!TerminateProcess(h, 1)) {
windows_errno(GetLastError());
return -1;
}
return 0;
}
/**
* Gracefully terminate a process created by os_spawn() on Windows.
*
* os_spawn() creates console applications with both CREATE_NO_WINDOW and
* CREATE_NEW_PROCESS_GROUP.
*
* CREATE_NO_WINDOW does not mean that the child is created as a detached
* process with no console support. For a console application it provides a
* windowless console, so the process can still receive console control events.
*
* Note that the Microsoft documentation is somewhat ambiguous here: it says
* that with CREATE_NO_WINDOW "the console handle for the application is not
* set", which can be misread as meaning that the process has no console at all.
* In practice, CREATE_NO_WINDOW creates a console session without a visible
* console window, and is distinct from DETACHED_PROCESS, which creates a
* process with no attached console.
*
* This behavior is documented and experimentally verified here:
* https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
* https://github.com/rprichard/win32-console-docs
*
* CREATE_NEW_PROCESS_GROUP creates a new process group whose identifier is
* the process ID of the child created by CreateProcess(). This allows
* CTRL_BREAK_EVENT to be directed specifically to the child's process group:
*
* CREATE_NO_WINDOW
* -> child has a windowless console
*
* CREATE_NEW_PROCESS_GROUP
* -> child PID is also the process-group ID
*
* GenerateConsoleCtrlEvent() can signal only processes sharing the console
* of the caller. Therefore, this function first detaches the calling process
* from its current console, if any, temporarily attaches it to the child's
* console using AttachConsole(child_pid), and then sends:
*
* GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid)
*
* The child process group then receives CTRL_BREAK_EVENT and can perform an
* orderly shutdown instead of being forcibly terminated.
*
* This works particularly well when called by a Windows Service. Services
* normally have no console of their own, so the sequence is simply:
*
* Service without a console
* -> AttachConsole(child_pid)
* -> GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid)
* -> child receives CTRL_BREAK_EVENT
* -> FreeConsole()
* -> Service is console-less again
*
* Note that console attachment is process-wide. If the calling process was
* already attached to a console, the initial FreeConsole() detaches it and
* that previous console is not restored on return. FreeConsole() and
* AttachConsole() also reset the process console control-handler table.
* For this reason, this function is best suited to callers that normally
* have no console, such as Windows Services.
*
* If attaching to the child's console fails, fall back to TerminateProcess().
*/
static int pid_term(pid_t pid)
{
HANDLE h = (void*)pid;
DWORD id = GetProcessId(h);
if (id == 0) {
errno = EINVAL;
return -1;
}
/* detach from current console (if any; background service has no console) */
FreeConsole();
/* attach to the child's invisible console host */
if (!AttachConsole(id)) {
/* fallback: terminate process forcibly if console attachment fails */
if (!TerminateProcess(h, 1)) {
windows_errno(GetLastError());
return -1;
}
return 0;
}
/* disable Ctrl-C for the PARENT so we don't kill ourselves */
SetConsoleCtrlHandler(0, TRUE);
/* this will now reach the child's SetConsoleCtrlHandler */
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id);
/* detach from child console, returning calling service process to its console-less state */
FreeConsole();
return 0;
}
/**
* Publish a newly created process reference, or terminate it immediately if canceled during spawn.
*/
static void pid_publish(pid_t* pid_slot, pid_t pid)
{
if (pid_slot == 0)
return;
exec_lock();
if (pid <= 0) {
if (*pid_slot != PID_KILL)
*pid_slot = PID_NONE;
} else {
pid_t prev = *pid_slot;
if (prev == PID_KILL) {
*pid_slot = PID_KILL;
pid_kill(pid);
} else {
*pid_slot = pid;
if (prev == PID_TERM)
pid_term(pid);
}
}
exec_unlock();
}
/**
* Unpublish a process reference from a slot.
*/
static void pid_unpublish(pid_t* pid_slot, pid_t pid)
{
if (pid_slot == 0)
return;
exec_lock();
if (*pid_slot == pid || (*pid_slot < 0 && *pid_slot != PID_KILL))
*pid_slot = PID_NONE;
exec_unlock();
}
/**
* If we are running in Wine.
*/
@ -2846,7 +3047,7 @@ static int process_startup_init(struct process_startup* startup, HANDLE stdin_ha
return 0;
}
pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const char* run_as_user)
pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const char* run_as_user, pid_t* pid_slot)
{
wchar_t conv[CONV_MAX];
HANDLE stdout_write_handle = INVALID_HANDLE_VALUE;
@ -2996,6 +3197,20 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
*/
const wchar_t* cwd = L"C:\\";
if (pid_spawn_begin(pid_slot) != 0) {
CloseHandle(nul_handle);
if (has_out) {
CloseHandle(stdout_write_handle);
close(out_f);
}
if (has_err) {
CloseHandle(stderr_write_handle);
close(err_f);
}
process_startup_done(&startup);
return -1;
}
/* create the child process */
if (run_as_user == 0 || run_as_user[0] == 0) {
ret = CreateProcessW(
@ -3016,6 +3231,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
/* Validate that the requested user is actually a supported Service Account before attempting logon */
if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) {
os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService");
pid_unpublish(pid_slot, 0);
process_startup_done(&startup);
CloseHandle(nul_handle);
if (has_out) {
@ -3032,6 +3248,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
process_startup_done(&startup);
CloseHandle(nul_handle);
if (has_out) {
@ -3050,6 +3267,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
if (!CreateEnvironmentBlock(&env, h_token, FALSE)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
CloseHandle(h_token);
process_startup_done(&startup);
CloseHandle(nul_handle);
@ -3085,6 +3303,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
if (!ret) {
windows_errno(create_error);
os_syslog(OS_LVL_INFO, "failed to create process '%s' for spawn, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno);
pid_unpublish(pid_slot, 0);
CloseHandle(nul_handle);
if (has_out) {
CloseHandle(stdout_write_handle);
@ -3108,6 +3327,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to assign process to job object, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3127,6 +3347,7 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
if (ResumeThread(pi.hThread) == (DWORD)-1) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to resume thread for spawn, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3159,10 +3380,17 @@ pid_t os_spawn(char** argv, int* stdout_read_int, int* stderr_read_int, const ch
*stderr_read_int = err_f;
}
return (intptr_t)pi.hProcess;
pid_t pid = (intptr_t)pi.hProcess;
pid_publish(pid_slot, pid);
return pid;
}
uint64_t os_display_pid(pid_t pid)
/**
* Get the operating system process ID suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
*/
uint64_t os_pid(pid_t pid)
{
if (pid <= 0)
return 0;
@ -3171,7 +3399,26 @@ uint64_t os_display_pid(pid_t pid)
return (uint64_t)GetProcessId(h);
}
pid_t os_wait(pid_t pid, int* status)
/**
* Get the operating system process ID of a published process slot suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
*/
uint64_t os_slot_pid(const pid_t* pid_slot)
{
if (pid_slot == 0)
return 0;
exec_lock();
uint64_t pid = os_pid(*pid_slot);
exec_unlock();
return pid;
}
/**
* Wait for the child process to terminate.
*/
pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot)
{
HANDLE h = (void*)pid;
DWORD exit_code;
@ -3180,13 +3427,20 @@ pid_t os_wait(pid_t pid, int* status)
if (GetExitCodeProcess(h, &exit_code)) {
*status = exit_code;
if (pid_slot != 0)
pid_unpublish(pid_slot, pid);
return pid;
} else {
windows_errno(GetLastError());
if (pid_slot != 0)
pid_unpublish(pid_slot, pid);
return -1;
}
}
/**
* Release the process reference returned by os_spawn().
*/
void os_dispose(pid_t pid)
{
if (pid > 0)
@ -3194,118 +3448,62 @@ void os_dispose(pid_t pid)
}
/**
* Gracefully terminate a process created by os_spawn() on Windows.
*
* os_spawn() creates console applications with both CREATE_NO_WINDOW and
* CREATE_NEW_PROCESS_GROUP.
*
* CREATE_NO_WINDOW does not mean that the child is created as a detached
* process with no console support. For a console application it provides a
* windowless console, so the process can still receive console control events.
*
* Note that the Microsoft documentation is somewhat ambiguous here: it says
* that with CREATE_NO_WINDOW "the console handle for the application is not
* set", which can be misread as meaning that the process has no console at all.
* In practice, CREATE_NO_WINDOW creates a console session without a visible
* console window, and is distinct from DETACHED_PROCESS, which creates a
* process with no attached console.
*
* This behavior is documented and experimentally verified here:
* https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
* https://github.com/rprichard/win32-console-docs
*
* CREATE_NEW_PROCESS_GROUP creates a new process group whose identifier is
* the process ID of the child created by CreateProcess(). This allows
* CTRL_BREAK_EVENT to be directed specifically to the child's process group:
*
* CREATE_NO_WINDOW
* -> child has a windowless console
*
* CREATE_NEW_PROCESS_GROUP
* -> child PID is also the process-group ID
*
* GenerateConsoleCtrlEvent() can signal only processes sharing the console
* of the caller. Therefore, this function first detaches the calling process
* from its current console, if any, temporarily attaches it to the child's
* console using AttachConsole(child_pid), and then sends:
*
* GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid)
*
* The child process group then receives CTRL_BREAK_EVENT and can perform an
* orderly shutdown instead of being forcibly terminated.
*
* This works particularly well when called by a Windows Service. Services
* normally have no console of their own, so the sequence is simply:
*
* Service without a console
* -> AttachConsole(child_pid)
* -> GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, child_pid)
* -> child receives CTRL_BREAK_EVENT
* -> FreeConsole()
* -> Service is console-less again
*
* Note that console attachment is process-wide. If the calling process was
* already attached to a console, the initial FreeConsole() detaches it and
* that previous console is not restored on return. FreeConsole() and
* AttachConsole() also reset the process console control-handler table.
* For this reason, this function is best suited to callers that normally
* have no console, such as Windows Services.
*
* If attaching to the child's console fails, fall back to TerminateProcess().
* Gracefully terminate a published process.
*/
int os_term(pid_t pid)
int os_term(pid_t* pid_slot)
{
HANDLE h = (void*)pid;
DWORD id = GetProcessId(h);
if (id == 0) {
if (pid_slot == 0) {
errno = EINVAL;
return -1;
}
/* detach from current console (if any; background service has no console) */
FreeConsole();
/* attach to the child's invisible console host */
if (!AttachConsole(id)) {
/* fallback: terminate process forcibly if console attachment fails */
if (!TerminateProcess(h, 1)) {
windows_errno(GetLastError());
return -1;
}
exec_lock();
pid_t pid = *pid_slot;
if (pid == PID_SPAWN) {
*pid_slot = PID_TERM;
exec_unlock();
return 0;
}
/* disable Ctrl-C for the PARENT so we don't kill ourselves */
SetConsoleCtrlHandler(0, TRUE);
/* this will now reach the child's SetConsoleCtrlHandler */
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id);
/* detach from child console, returning calling service process to its console-less state */
FreeConsole();
return 0;
if (pid == PID_NONE || pid == PID_TERM || pid == PID_KILL) {
exec_unlock();
return 0;
}
int ret = pid_term(pid);
exec_unlock();
return ret;
}
int os_kill(pid_t pid)
/**
* Forcibly terminate a published process.
*/
int os_kill(pid_t* pid_slot)
{
if (pid <= 0) {
if (pid_slot == 0) {
errno = EINVAL;
return -1;
}
HANDLE h = (void*)pid;
if (!TerminateProcess(h, 1)) {
windows_errno(GetLastError());
return -1;
exec_lock();
pid_t pid = *pid_slot;
if (pid == PID_NONE || pid == PID_SPAWN || pid == PID_TERM) {
*pid_slot = PID_KILL;
exec_unlock();
return 0;
}
return 0;
if (pid == PID_KILL) {
exec_unlock();
return 0;
}
*pid_slot = PID_KILL;
int ret = pid_kill(pid);
exec_unlock();
return ret;
}
int os_command(const char* command, const char* run_as_user, const char* stdin_text)
/**
* Execute a shell command synchronously and wait for termination.
*/
int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot)
{
wchar_t conv[CONV_MAX];
HANDLE stdin_read_handle;
@ -3371,6 +3569,14 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
*/
const wchar_t* cwd = L"C:\\";
if (pid_spawn_begin(pid_slot) != 0) {
CloseHandle(stdin_read_handle);
CloseHandle(stdin_write_handle);
CloseHandle(nul);
process_startup_done(&startup);
return -1;
}
/*
* No drop of privilege requested
* Run exactly as the parent daemon
@ -3400,6 +3606,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
*/
if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) {
os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService");
pid_unpublish(pid_slot, 0);
process_startup_done(&startup);
CloseHandle(stdin_read_handle);
CloseHandle(stdin_write_handle);
@ -3410,6 +3617,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
process_startup_done(&startup);
CloseHandle(stdin_read_handle);
CloseHandle(stdin_write_handle);
@ -3424,6 +3632,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (!CreateEnvironmentBlock(&env, h_token, FALSE)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
process_startup_done(&startup);
CloseHandle(stdin_read_handle);
CloseHandle(stdin_write_handle);
@ -3453,6 +3662,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (!ret) {
windows_errno(create_error);
os_syslog(OS_LVL_INFO, "failed to create process '%s' for command, errno=%s(%d)", command, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
CloseHandle(stdin_read_handle);
CloseHandle(stdin_write_handle);
CloseHandle(nul);
@ -3470,6 +3680,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to assign command process to job object, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3482,6 +3693,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (ResumeThread(pi.hThread) == (DWORD)-1) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to resume thread for command, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3489,6 +3701,9 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
return -1;
}
pid_t pid = (intptr_t)pi.hProcess;
pid_publish(pid_slot, pid);
/* write the string to the child's STDIN */
if (stdin_text != 0) {
size_t len = strlen(stdin_text);
@ -3516,6 +3731,8 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
DWORD status;
GetExitCodeProcess(pi.hProcess, &status);
pid_unpublish(pid_slot, pid);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3673,7 +3890,10 @@ bail:
return new_env;
}
int os_script(char** argv, char** envp, const char* run_as_user)
/**
* Execute an external script synchronously and wait for termination.
*/
int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot)
{
wchar_t conv[CONV_MAX];
PROCESS_INFORMATION pi;
@ -3738,6 +3958,10 @@ int os_script(char** argv, char** envp, const char* run_as_user)
*/
const wchar_t* cwd = L"C:\\";
if (pid_spawn_begin(pid_slot) != 0) {
return -1;
}
/*
* No drop of privilege requested
* Run exactly as the parent daemon
@ -3752,6 +3976,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!base_env) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to get environment strings, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
return -1;
}
combined_env = env_combine(base_env, envp);
@ -3759,6 +3984,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!combined_env) {
errno = ENOMEM;
os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)");
pid_unpublish(pid_slot, 0);
return -1;
}
creation_flags |= CREATE_UNICODE_ENVIRONMENT;
@ -3789,12 +4015,14 @@ int os_script(char** argv, char** envp, const char* run_as_user)
*/
if (_stricmp(run_as_user, "LocalService") != 0 && _stricmp(run_as_user, "NetworkService") != 0) {
os_syslog(OS_LVL_INFO, "only supported users are LocalService and NetworkService");
pid_unpublish(pid_slot, 0);
return -1;
}
if (!LogonUserW(u8tou16(conv, run_as_user), L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &h_token)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to logon user %s, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
return -1;
}
@ -3805,6 +4033,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!CreateEnvironmentBlock(&env, h_token, FALSE)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to get user %s environment, errno=%s(%d)", run_as_user, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
CloseHandle(h_token);
return -1;
}
@ -3815,6 +4044,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!combined_env) {
errno = ENOMEM;
os_syslog(OS_LVL_INFO, "failed to combine environment strings (out of memory)");
pid_unpublish(pid_slot, 0);
DestroyEnvironmentBlock(env);
CloseHandle(h_token);
return -1;
@ -3841,6 +4071,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!ret) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to create process '%s' for script, errno=%s(%d)", u16tou8_force(cmd_buffer_conv, sizeof(cmd_buffer_conv), cmd_buffer, wcslen(cmd_buffer) + 1, 0), strerror(errno), errno);
pid_unpublish(pid_slot, 0);
return -1;
}
@ -3849,6 +4080,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (!AssignProcessToJobObject(os_job_handle, pi.hProcess)) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to assign script process to job object, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -3860,17 +4092,23 @@ int os_script(char** argv, char** envp, const char* run_as_user)
if (ResumeThread(pi.hThread) == (DWORD)-1) {
windows_errno(GetLastError());
os_syslog(OS_LVL_INFO, "failed to resume thread for script, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, 0);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return -1;
}
pid_t pid = (intptr_t)pi.hProcess;
pid_publish(pid_slot, pid);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD status;
GetExitCodeProcess(pi.hProcess, &status);
pid_unpublish(pid_slot, pid);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
@ -4093,6 +4331,11 @@ void os_init(unsigned opt)
os_exit();
}
if (windows_mutex_init(&exec_mutex, 0) != 0) {
os_syslog(OS_LVL_CRITICAL, "error calling windows_mutex_init()");
os_exit();
}
ntdll = GetModuleHandle("NTDLL.DLL");
if (!ntdll) {
os_syslog(OS_LVL_CRITICAL, "error loading the NTDLL module");
@ -4161,6 +4404,7 @@ void os_done(void)
/* delete the thread local storage for strerror() */
windows_key_delete(last_error);
windows_mutex_destroy(&exec_mutex);
windows_mutex_destroy(&tick_lock);
/* restore the normal execution level */

83
os/os.h
View file

@ -116,33 +116,56 @@ int os_pclose(OS_FILE* stream);
/**
* Spawn a new process with the specified argument vector, optionally capturing stdout and/or stderr.
*
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
*
* Process Reference Slots (pid_slot):
* If pid_slot is not NULL, process creation and the resulting process reference are published through it
* to enable safe, race-free process tracking and termination (os_term, os_kill) across concurrent threads.
* If os_kill() was called on pid_slot, process creation is rejected immediately and this function
* returns -1 with errno set to ECANCELED without spawning a child process. In contrast, os_term() on an
* idle slot is a no-op and does not prevent subsequent process creation.
* The same pid_slot pointer should be passed to os_wait() to unpublish it when the process terminates.
*
* \param argv Array of command line arguments.
* \param stdout_read_fd Pointer to store file descriptor for stdout, or NULL to redirect to /dev/null.
* \param stderr_read_fd Pointer to store file descriptor for stderr, or NULL to redirect to /dev/null.
* \param run_as_user User to run script as (NULL for current user).
* \param pid_slot Optional pointer used to publish the process reference, or NULL if concurrent termination is not required.
* \return Process reference of spawned process, or -1 on failure.
*/
pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user);
pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user, pid_t* pid_slot);
/**
* Get the operating system process ID suitable for display.
* Get the operating system process ID suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
* On Unix the process reference is already the PID.
* On Windows the process reference is a HANDLE and the actual PID is obtained with GetProcessId().
* \param pid Process reference returned by os_spawn().
* \return Operating system process ID, or 0 if unavailable.
*/
uint64_t os_display_pid(pid_t pid);
uint64_t os_pid(pid_t pid);
/**
* Get the operating system process ID of a published process slot suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
* Internal synchronization states (including creation in progress or persistent kill) always return 0.
* \param pid_slot Pointer to a published process reference slot.
* \return Operating system process ID, or 0 if no process is currently published.
*/
uint64_t os_slot_pid(const pid_t* pid_slot);
/**
* Wait for the child process to terminate.
* If pid_slot is not NULL, the process reference is unpublished before this function returns.
* This function does not release the process reference returned by os_spawn().
* The caller must eventually call os_dispose().
* \param pid Process reference returned by os_spawn().
* \param status Pointer to store the exit status.
* \param pid_slot Optional pointer previously passed to os_spawn(), or NULL if the process was not published.
* \return Child process reference on success, -1 on failure.
*/
pid_t os_wait(pid_t pid, int* status);
pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot);
/**
* Release the process reference returned by os_spawn().
@ -152,20 +175,36 @@ pid_t os_wait(pid_t pid, int* status);
void os_dispose(pid_t pid);
/**
* Terminate gracefully a process. Intended for daemon/service processes controlling child processes.
* The caller retains ownership and must eventually call both os_wait() and os_dispose() as appropriate.
* \param pid Process reference of the process to terminate.
* \return 0 on success, -1 on failure.
* Gracefully terminate a published process reference.
*
* If a process is currently running in the slot, it is gracefully requested to terminate.
* If process creation is in progress, the termination request is recorded and applied when the process is published.
* If no process is active (slot is 0/idle), there is nothing to terminate: this function is a harmless no-op,
* returning 0 without modifying the slot state or preventing subsequent process creation.
*
* The caller retains ownership for spawned processes and must eventually call both os_wait() and os_dispose()
* as appropriate.
* \param pid_slot Pointer to the published process reference slot.
* \return 0 on success or if no process is active, -1 on failure.
*/
int os_term(pid_t pid);
int os_term(pid_t* pid_slot);
/**
* Forcibly terminate a process.
* The caller retains ownership and must eventually call both os_wait() and os_dispose() as appropriate.
* \param pid Process reference of the process to kill.
* \return 0 on success, -1 on failure.
* Forcibly terminate a published process reference.
*
* If a process is currently running in the slot, it is terminated immediately.
* If process creation is in progress, the kill request is recorded and the child is killed upon publication.
* If no process is active (slot is idle), the kill request is recorded persistently in the slot so that
* any future process creation attempted with this slot is rejected immediately without spawning.
*
* The slot retains the persistent kill state until explicitly reset by the caller (e.g. *pid_slot = 0).
* The caller retains ownership for spawned processes and must eventually call both os_wait() and os_dispose()
* as appropriate.
*
* \param pid_slot Pointer to the published process reference slot.
* \return 0 on success or if recorded in the slot, -1 on failure.
*/
int os_kill(pid_t pid);
int os_kill(pid_t* pid_slot);
/**
* Fork and execute a verified executable, discarding all I/O.
@ -188,22 +227,30 @@ int os_spawn_and_wait(const char** argv);
/**
* Execute a system command with optional user context and input.
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
* If pid_slot is not NULL, the child process is published for concurrent termination while blocked.
* If os_kill() was called on pid_slot, process creation is rejected immediately and this function
* returns -1 with errno set to ECANCELED without executing the command.
* \param command Command to execute.
* \param run_as_user User to run command as (NULL for current user).
* \param stdin_text Text to provide as stdin (NULL for no input).
* \return Exit status of command.
* \param pid_slot Optional pointer to publish the process reference while active, or NULL.
* \return Exit status of command, or -1 on failure.
*/
int os_command(const char* command, const char* run_as_user, const char* stdin_text);
int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot);
/**
* Execute a script file with specified user context.
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
* If pid_slot is not NULL, the child process is published for concurrent termination while blocked.
* If os_kill() was called on pid_slot, process creation is rejected immediately and this function
* returns -1 with errno set to ECANCELED without executing the script.
* \param argv Array of command line arguments.
* \param envp Environment variables (NULL-terminated list of strings).
* \param run_as_user User to run script as (NULL for current user).
* \return Exit status of script.
* \param pid_slot Optional pointer to publish the process reference while active, or NULL.
* \return Exit status of script, or -1 on failure.
*/
int os_script(char** argv, char** envp, const char* run_as_user);
int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot);
/**
* Validates a string for exec.

370
os/unix.c
View file

@ -876,11 +876,148 @@ bail:
#endif
}
#define PID_NONE ((pid_t)0) /**< No process is active. */
#define PID_SPAWN ((pid_t)-1) /**< Process creation is in progress. */
#define PID_TERM ((pid_t)-2) /**< Process termination was requested while creation is in progress. */
#define PID_KILL ((pid_t)-3) /**< Process kill was requested; rejects future spawns or terminates in-progress/active processes. */
static pthread_mutex_t exec_mutex = PTHREAD_MUTEX_INITIALIZER;
/**
* Acquire the process execution mutex.
*/
static void exec_lock(void)
{
pthread_mutex_lock(&exec_mutex);
}
/**
* Release the process execution mutex.
*/
static void exec_unlock(void)
{
pthread_mutex_unlock(&exec_mutex);
}
/**
* Forcibly terminate the specified raw process reference.
*/
static int pid_kill(pid_t pid)
{
if (pid <= 0) {
errno = EINVAL;
return -1;
}
/* Send SIGKILL signal to the negative PID to target the entire Process Group */
int ret = kill(-pid, SIGKILL);
/*
* If setpgid(0, 0) has not yet executed in the child, process group -pid
* does not exist and kill() returns ESRCH. Fallback to targeting the process directly.
*/
if (ret < 0 && errno == ESRCH) {
ret = kill(pid, SIGKILL);
}
return ret;
}
/**
* Gracefully terminate the specified raw process reference.
*/
static int pid_term(pid_t pid)
{
if (pid <= 0) {
errno = EINVAL;
return -1;
}
/*
* Send signal to the negative PID to target the entire Process Group.
* This ensures that SnapRAID and any programs it may have spawned are
* terminated together, preventing orphaned worker processes.
*/
int ret = kill(-pid, SIGTERM);
/*
* If setpgid(0, 0) has not yet executed in the child, process group -pid
* does not exist and kill() returns ESRCH. Fallback to targeting the process directly.
*/
if (ret < 0 && errno == ESRCH) {
ret = kill(pid, SIGTERM);
}
return ret;
}
/**
* Mark the start of process creation in a process-reference slot.
*/
static int pid_spawn_begin(pid_t* pid_slot)
{
if (pid_slot == 0)
return 0;
exec_lock();
if (*pid_slot == PID_KILL) {
exec_unlock();
errno = ECANCELED;
return -1;
}
*pid_slot = PID_SPAWN;
exec_unlock();
return 0;
}
/**
* Publish a newly created process reference, or terminate it immediately if canceled during spawn.
*/
static void pid_publish(pid_t* pid_slot, pid_t pid)
{
if (pid_slot == 0)
return;
exec_lock();
if (pid <= 0) {
if (*pid_slot != PID_KILL)
*pid_slot = PID_NONE;
} else {
pid_t prev = *pid_slot;
if (prev == PID_KILL) {
*pid_slot = PID_KILL;
pid_kill(pid);
} else {
*pid_slot = pid;
if (prev == PID_TERM)
pid_term(pid);
}
}
exec_unlock();
}
/**
* Unpublish a process reference from a slot.
*/
static void pid_unpublish(pid_t* pid_slot, pid_t pid)
{
if (pid_slot == 0)
return;
exec_lock();
if (*pid_slot == pid || (*pid_slot < 0 && *pid_slot != PID_KILL))
*pid_slot = PID_NONE;
exec_unlock();
}
/**
* Waits for a process with a timeout, killing the entire process group if exceeded.
* Observes process termination without reaping (via waitid WNOWAIT), leaving the process
* as a zombie until the caller unpublishes the process slot and performs the final reap.
* Optionally delivers input to input_fd using non-blocking writes.
*/
static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int input_fd, const char* input, int* timed_out, int* input_error)
static pid_t waitpid_timeout_group(pid_t pid, int64_t start, int input_fd, const char* input, int* timed_out, int* input_error)
{
size_t input_len = input != 0 ? strlen(input) : 0;
size_t input_pos = 0;
@ -892,15 +1029,23 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in
*input_error = 0;
while (1) {
ret = waitpid(pid, status, WNOHANG);
/*
* Poll child termination with WNOWAIT so the terminated child remains
* in zombie state with its PID reserved in the kernel until unpublished.
* POSIX specifies that with WNOHANG, waitid() returns 0 and sets
* si_pid to 0 if the child state has not changed.
*/
siginfo_t info;
memset(&info, 0, sizeof(info));
int wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOHANG | WNOWAIT);
if (ret == pid) {
if (wret == 0 && info.si_pid == pid) {
if (input_fd != -1)
close(input_fd);
return ret;
return pid;
}
if (ret < 0) {
if (wret < 0) {
if (errno == EINTR)
continue;
@ -959,15 +1104,22 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in
input_fd = -1;
}
if (os_kill(pid) != 0 && errno != ESRCH)
if (pid_kill(pid) != 0 && errno != ESRCH)
return -1;
/* reap the process-group leader */
/*
* Wait for the process-group leader to terminate after SIGKILL.
* Use WNOWAIT so the child remains a zombie until slot unpublication.
*/
do {
ret = waitpid(pid, status, 0);
memset(&info, 0, sizeof(info));
ret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT);
} while (ret < 0 && errno == EINTR);
return ret;
if (ret < 0)
return -1;
return pid;
}
os_usleep(100000);
@ -977,7 +1129,7 @@ static pid_t waitpid_timeout_group(pid_t pid, int* status, int64_t start, int in
/**
* Executes a script directly via its file descriptor.
*/
int os_script(char** argv, char** envp, const char* run_as_user)
int os_script(char** argv, char** envp, const char* run_as_user, pid_t* pid_slot)
{
char resolved_path[PATH_MAX];
pid_t pid;
@ -1008,9 +1160,15 @@ int os_script(char** argv, char** envp, const char* run_as_user)
pid_t ppid = getpid();
#endif
if (pid_spawn_begin(pid_slot) != 0) {
close(fd);
return -1;
}
pid = fork();
if (pid < 0) {
os_syslog(OS_LVL_INFO, "failed to fork script=%s, errno=%s(%d)", resolved_path, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
close(fd);
return -1;
}
@ -1114,7 +1272,24 @@ int os_script(char** argv, char** envp, const char* run_as_user)
/* parent process */
close(fd);
ret = waitpid_timeout_group(pid, &status, start, -1, 0, &timed_out, 0);
pid_publish(pid_slot, pid);
ret = waitpid_timeout_group(pid, start, -1, 0, &timed_out, 0);
/*
* Unpublish the PID slot before reaping the zombie process.
* While the child is a zombie, the OS kernel keeps the PID reserved,
* preventing any new process from reusing it. Unpublishing the slot first
* guarantees that concurrent os_term()/os_kill() calls will not target
* a recycled PID once waitpid() finally reaps the process.
*/
pid_unpublish(pid_slot, pid);
if (ret != -1) {
do {
ret = waitpid(pid, &status, 0);
} while (ret == -1 && errno == EINTR);
}
if (ret == -1) {
os_syslog(OS_LVL_INFO, "failed to wait for script, path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno);
@ -1153,7 +1328,7 @@ int os_script(char** argv, char** envp, const char* run_as_user)
}
}
int os_command(const char* command, const char* run_as_user, const char* stdin_text)
int os_command(const char* command, const char* run_as_user, const char* stdin_text, pid_t* pid_slot)
{
pid_t pid;
int ret;
@ -1195,9 +1370,18 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
pid_t ppid = getpid();
#endif
if (pid_spawn_begin(pid_slot) != 0) {
if (pipe_fds[0] != -1) {
close(pipe_fds[0]);
close(pipe_fds[1]);
}
return -1;
}
pid = fork();
if (pid < 0) {
os_syslog(OS_LVL_INFO, "failed to fork command=%s, errno=%s(%d)", command, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
if (pipe_fds[0] != -1) {
close(pipe_fds[0]);
close(pipe_fds[1]);
@ -1279,12 +1463,29 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (pipe_fds[0] != -1)
close(pipe_fds[0]); /* close unused read end */
ret = waitpid_timeout_group(pid, &status, start, pipe_fds[1], stdin_text, &timed_out, &input_error);
pid_publish(pid_slot, pid);
ret = waitpid_timeout_group(pid, start, pipe_fds[1], stdin_text, &timed_out, &input_error);
pipe_fds[1] = -1;
/*
* Unpublish the PID slot before reaping the zombie process.
* While the child is a zombie, the OS kernel keeps the PID reserved,
* preventing any new process from reusing it. Unpublishing the slot first
* guarantees that concurrent os_term()/os_kill() calls will not target
* a recycled PID once waitpid() finally reaps the process.
*/
pid_unpublish(pid_slot, pid);
if (input_error != 0)
os_syslog(OS_LVL_INFO, "failed to write stdin to command %s, errno=%s(%d)", command, strerror(input_error), input_error);
if (ret != -1) {
do {
ret = waitpid(pid, &status, 0);
} while (ret == -1 && errno == EINTR);
}
if (ret == -1) {
os_syslog(OS_LVL_INFO, "failed to wait for command, command=%s, errno=%s(%d)", command, strerror(errno), errno);
return -1;
@ -1333,7 +1534,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
*
* Returns the child PID on success, or -1 on failure.
*/
pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user)
pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user, pid_t* pid_slot)
{
char resolved_path[PATH_MAX];
int out_pipe[2];
@ -1385,9 +1586,23 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char
pid_t ppid = getpid();
#endif
if (pid_spawn_begin(pid_slot) != 0) {
if (has_out) {
close(out_pipe[0]);
close(out_pipe[1]);
}
if (has_err) {
close(err_pipe[0]);
close(err_pipe[1]);
}
close(fd);
return -1;
}
pid = fork();
if (pid < 0) {
os_syslog(OS_LVL_INFO, "failed to fork path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno);
pid_unpublish(pid_slot, 0);
if (has_out) {
close(out_pipe[0]);
close(out_pipe[1]);
@ -1484,6 +1699,8 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char
/* parent */
close(fd);
pid_publish(pid_slot, pid);
if (has_out) {
#ifdef F_SETPIPE_SZ
if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) {
@ -1507,7 +1724,11 @@ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char
return pid;
}
uint64_t os_display_pid(pid_t pid)
/**
* Get the operating system process ID suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
*/
uint64_t os_pid(pid_t pid)
{
if (pid <= 0)
return 0;
@ -1515,10 +1736,51 @@ uint64_t os_display_pid(pid_t pid)
return (uint64_t)pid;
}
pid_t os_wait(pid_t pid, int* status)
/**
* Get the operating system process ID of a published process slot suitable for display or logging.
* This function is intended for display purposes only and not as a process reference.
*/
uint64_t os_slot_pid(const pid_t* pid_slot)
{
if (pid_slot == 0)
return 0;
exec_lock();
uint64_t pid = os_pid(*pid_slot);
exec_unlock();
return pid;
}
/**
* Wait for the child process to terminate.
*/
pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot)
{
pid_t ret;
/*
* If published in a slot, first wait for termination without reaping (WNOWAIT).
* The child enters zombie state, holding its PID reserved in the kernel so
* no newly created process can receive the same PID. We then unpublish
* the slot before the final waitpid() reap, ensuring concurrent termination
* requests (os_term/os_kill) never signal a recycled PID.
*/
if (pid_slot != 0) {
siginfo_t info;
int wret;
do {
memset(&info, 0, sizeof(info));
wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT);
} while (wret == -1 && errno == EINTR);
if (wret == -1)
os_syslog(OS_LVL_INFO, "failed to waitid, errno=%s(%d)", strerror(errno), errno);
pid_unpublish(pid_slot, pid);
}
do {
ret = waitpid(pid, status, 0);
} while (ret == -1 && errno == EINTR);
@ -1530,60 +1792,70 @@ pid_t os_wait(pid_t pid, int* status)
return ret;
}
/**
* Release the process reference returned by os_spawn().
*/
void os_dispose(pid_t pid)
{
(void)pid;
}
int os_term(pid_t pid)
/**
* Gracefully terminate a published process.
*/
int os_term(pid_t* pid_slot)
{
if (pid <= 0) {
if (pid_slot == 0) {
errno = EINVAL;
return -1;
}
/*
* Send signal to the negative PID to target the entire Process Group.
* This ensures that SnapRAID and any programs it may have spawned are
* terminated together, preventing orphaned worker processes.
*/
int ret = kill(-pid, SIGTERM);
/*
* If setpgid(0, 0) has not yet executed in the child, process group -pid
* does not exist and kill() returns ESRCH. Fallback to targeting the process directly.
*/
if (ret < 0 && errno == ESRCH) {
ret = kill(pid, SIGTERM);
exec_lock();
pid_t pid = *pid_slot;
if (pid == PID_SPAWN) {
*pid_slot = PID_TERM;
exec_unlock();
return 0;
}
if (pid == PID_NONE || pid == PID_TERM || pid == PID_KILL) {
exec_unlock();
return 0;
}
int ret = pid_term(pid);
exec_unlock();
return ret;
}
int os_kill(pid_t pid)
/**
* Forcibly terminate a published process.
*/
int os_kill(pid_t* pid_slot)
{
if (pid <= 0) {
if (pid_slot == 0) {
errno = EINVAL;
return -1;
}
/* Send SIGKILL signal to the negative PID to target the entire Process Group */
int ret = kill(-pid, SIGKILL);
/*
* If setpgid(0, 0) has not yet executed in the child, process group -pid
* does not exist and kill() returns ESRCH. Fallback to targeting the process directly.
*/
if (ret < 0 && errno == ESRCH) {
ret = kill(pid, SIGKILL);
exec_lock();
pid_t pid = *pid_slot;
if (pid == PID_NONE || pid == PID_SPAWN || pid == PID_TERM) {
*pid_slot = PID_KILL;
exec_unlock();
return 0;
}
if (pid == PID_KILL) {
exec_unlock();
return 0;
}
*pid_slot = PID_KILL;
int ret = pid_kill(pid);
exec_unlock();
return ret;
}
int os_spawn_and_wait(const char** argv)
{
pid_t pid = os_spawn((char**)argv, 0, 0, 0);
pid_t pid = os_spawn((char**)argv, 0, 0, 0, 0);
if (pid < 0) {
/* LCOV_EXCL_START */
return -1;
@ -1591,7 +1863,7 @@ int os_spawn_and_wait(const char** argv)
}
int status;
int ret = os_wait(pid, &status);
int ret = os_wait(pid, &status, 0);
os_dispose(pid);
if (ret == -1) {
/* LCOV_EXCL_START */
@ -1615,7 +1887,7 @@ OS_FILE* os_popen(const char** argv)
if (!os_file)
return 0;
pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0);
pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0, 0);
if (pid < 0) {
free(os_file);
return 0;
@ -1627,7 +1899,7 @@ OS_FILE* os_popen(const char** argv)
int saved_errno = errno;
close(stdout_fd);
int status;
os_wait(pid, &status);
os_wait(pid, &status, 0);
os_dispose(pid);
errno = saved_errno;
free(os_file);
@ -1657,7 +1929,7 @@ int os_pclose(OS_FILE* stream)
fclose(fp);
int status = 0;
int ret = os_wait(pid, &status);
int ret = os_wait(pid, &status, 0);
os_dispose(pid);
if (ret < 0) {
return -1;