Sync the OS interface with the daemon

This commit is contained in:
Andrea Mazzoleni 2026-07-06 17:55:28 +02:00
parent f078eede75
commit 06eb51bca5
4 changed files with 129 additions and 59 deletions

View file

@ -129,7 +129,8 @@ mktest_SOURCES = \
cmdline/mingwapp.c \
os/unix.c \
cmdline/unixapp.c \
cmdline/support.c
cmdline/support.c \
tommyds/tommy.c
mkstream_SOURCES = \
cmdline/mkstream.c \
@ -140,7 +141,8 @@ mkstream_SOURCES = \
cmdline/support.c \
cmdline/util.c \
cmdline/stream.c \
raid/memory.c
raid/memory.c \
tommyds/tommy.c
# Checker binary share the snapraid variables, with the exception of the checker.c source
checker_SOURCES = $(snapraid_SOURCES:cmdline/snapraid.c=cmdline/checker.c)

View file

@ -2732,28 +2732,43 @@ pid_t os_wait(pid_t pid, int* status)
return -1;
}
/**
* Terminate a running child process gracefully on Windows.
*
* Child processes spawned via os_spawn() with CREATE_NO_WINDOW have an invisible
* console host allocated by the OS. Attaching to the child's console allows
* sending a CTRL_BREAK_EVENT signal so SnapRAID CLI catches it and flushes state/checkpoints.
*/
int os_term(pid_t pid)
{
HANDLE h = (void*)pid;
DWORD id = GetProcessId(h);
if (id == 0)
if (id == 0) {
errno = EINVAL;
return -1;
}
/* detach from current console (if any) */
FreeConsole();
/* attach to the child's console */
if (!AttachConsole(id))
return -1;
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 */
/* disable Ctrl-C for the PARENT so we don't kill ourselves */
SetConsoleCtrlHandler(0, TRUE);
/* This will now reach the child's SetConsoleCtrlHandler */
/* this will now reach the child's SetConsoleCtrlHandler */
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, id);
/* Clean up */
/* clean up */
FreeConsole();
return 0;
@ -2951,76 +2966,112 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
}
}
struct env_item {
tommy_node node;
WCHAR str[1];
};
static int env_item_compare(const void* void_a, const void* void_b)
{
const struct env_item* a = void_a;
const struct env_item* b = void_b;
return _wcsicmp(a->str, b->str);
}
static size_t env_key_len(const WCHAR* s)
{
size_t i = 0;
if (s[0] == '=')
i = 1;
while (s[i] != 0 && s[i] != '=')
++i;
return i;
}
static int env_key_equal(const WCHAR* a, const WCHAR* b)
{
size_t len_a = env_key_len(a);
size_t len_b = env_key_len(b);
if (len_a != len_b)
return 0;
return _wcsnicmp(a, b, len_a) == 0;
}
/**
* Combine base environment strings with extra envp variables into a single block.
*
* Win32 CreateProcessW requires environment blocks to be sorted alphabetically
* (case-insensitive) by variable name. Unsorted environment blocks cause Win32 CRT
* variable lookups (e.g. GetEnvironmentVariableW) in child processes to fail.
*/
static WCHAR* env_combine(const WCHAR* base_env, char** envp)
{
size_t base_len = 0;
tommy_list list;
tommy_list_init(&list);
size_t total_len = 0;
WCHAR* new_env = NULL;
/* add base_env strings */
if (base_env) {
const WCHAR* p = base_env;
while (*p != 0) {
p += wcslen(p) + 1;
size_t len = wcslen(p);
struct env_item* item = malloc(sizeof(struct env_item) + len * sizeof(WCHAR));
if (!item)
goto bail;
memcpy(item->str, p, (len + 1) * sizeof(WCHAR));
tommy_list_insert_tail(&list, &item->node, item);
total_len += len + 1;
p += len + 1;
}
base_len = p - base_env + 1;
} else {
base_len = 1;
}
size_t envv_w_total_len = 0;
int envv_count = 0;
/* add envp strings */
if (envp) {
while (envp[envv_count] != NULL) {
envv_count++;
}
}
WCHAR** envv_w = NULL;
WCHAR* new_env = NULL;
if (envv_count > 0) {
envv_w = calloc(envv_count, sizeof(WCHAR*));
if (!envv_w) {
goto bail;
}
for (int i = 0; i < envv_count; ++i) {
for (int i = 0; envp[i] != NULL; ++i) {
wchar_t conv[CONV_MAX];
u8tou16(conv, envp[i]);
size_t len = wcslen(conv);
envv_w[i] = malloc((len + 1) * sizeof(WCHAR));
if (!envv_w[i]) {
errno = ENOMEM;
struct env_item* item = malloc(sizeof(struct env_item) + len * sizeof(WCHAR));
if (!item)
goto bail;
}
memcpy(envv_w[i], conv, (len + 1) * sizeof(WCHAR));
envv_w_total_len += len + 1;
memcpy(item->str, conv, (len + 1) * sizeof(WCHAR));
tommy_list_insert_tail(&list, &item->node, item);
total_len += len + 1;
}
}
size_t total_len = base_len + envv_w_total_len + 1;
new_env = malloc(total_len * sizeof(WCHAR));
if (!new_env) {
errno = ENOMEM;
goto bail;
}
tommy_list_sort(&list, env_item_compare);
WCHAR* dst = new_env;
if (base_len > 1 && base_env) {
memcpy(dst, base_env, (base_len - 1) * sizeof(WCHAR));
dst += base_len - 1;
}
for (int i = 0; i < envv_count; ++i) {
if (envv_w && envv_w[i]) {
size_t len = wcslen(envv_w[i]);
memcpy(dst, envv_w[i], (len + 1) * sizeof(WCHAR));
new_env = malloc((total_len + 1) * sizeof(WCHAR));
if (new_env) {
WCHAR* dst = new_env;
for (tommy_node* node = tommy_list_head(&list); node; node = node->next) {
struct env_item* item = node->data;
if (node->next) {
struct env_item* next_item = node->next->data;
/* do not insert duplicates */
if (env_key_equal(item->str, next_item->str))
continue;
}
size_t len = wcslen(item->str);
memcpy(dst, item->str, (len + 1) * sizeof(WCHAR));
dst += len + 1;
}
*dst = 0; /* double-null terminator */
}
*dst = 0;
bail:
if (envv_w) {
for (int i = 0; i < envv_count; ++i) {
free(envv_w[i]);
}
free(envv_w);
/* free all list items */
while (tommy_list_head(&list)) {
tommy_node* head = tommy_list_head(&list);
struct env_item* item = head->data;
tommy_list_remove_existing(&list, head);
free(item);
}
return new_env;

View file

@ -78,6 +78,8 @@ typedef struct OS_FILE {
* with its stdin and stderr redirected to /dev/null. The underlying pipe is
* created with O_CLOEXEC set to prevent leakage across concurrent forks.
*
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
*
* \param argv A NULL-terminated array of strings representing the argument vector.
* argv[0] must contain the absolute path to the verified executable.
* \return A pointer to an initialized OS_FILE structure on success, or NULL on failure.
@ -114,6 +116,7 @@ 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.
* \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.
@ -147,6 +150,8 @@ int os_term(pid_t pid);
* The child is placed in its own process group (setpgid) to isolate it
* from signals sent to the daemon's process group.
*
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
*
* \param argv NULL-terminated argument vector. argv[0] must be the absolute path
* to the executable.
* \return The child exit status on success, or -1 on failure.
@ -155,6 +160,7 @@ 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.
* \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).
@ -164,6 +170,7 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
/**
* Execute a script file with specified user context.
* The caller is expected to call os_privileges_acquire() before this operation if permission is needed.
* \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).

View file

@ -1029,9 +1029,19 @@ int os_command(const char* command, const char* run_as_user, const char* stdin_t
if (pipe_fds[1] != -1) {
/* write text to child's stdin */
ssize_t len = strlen(stdin_text);
if (write(pipe_fds[1], stdin_text, len) != len) {
os_syslog(OS_LVL_INFO, "failed to write full stdin to command %s", command);
size_t len = strlen(stdin_text);
size_t written = 0;
while (written < len) {
ssize_t ret_write = write(pipe_fds[1], stdin_text + written, len - written);
if (ret_write < 0) {
if (errno == EINTR)
continue;
if (errno == EPIPE)
break; /* child closed stdin early */
os_syslog(OS_LVL_INFO, "failed to write stdin to command %s, errno=%s(%d)", command, strerror(errno), errno);
break;
}
written += (size_t)ret_write;
}
/* closing the pipe sends EOF to the child (e.g., tells curl data is done) */
close(pipe_fds[1]);