mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 19:01:57 -04:00
Encapsulate race-safe process slot synchronization and termination in OS layer for Unix and Windows
This commit is contained in:
parent
d7e06271d9
commit
9f48c87dd4
5 changed files with 756 additions and 172 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
442
os/mingw.c
442
os/mingw.c
|
|
@ -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
83
os/os.h
|
|
@ -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
370
os/unix.c
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue