diff --git a/Makefile.am b/Makefile.am index ebe42541..838dd729 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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) diff --git a/os/mingw.c b/os/mingw.c index a8ff1fd6..00bccf5f 100644 --- a/os/mingw.c +++ b/os/mingw.c @@ -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,131 @@ 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 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; +} + +/** + * Case-insensitive comparison of environment variable names (keys only). + * + * Compares two environment strings up to the '=' character separating the key from the value. + * Returns negative if a < b, positive if a > b, or 0 if key names match. + */ +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; + + size_t len_a = env_key_len(a->str); + size_t len_b = env_key_len(b->str); + size_t min_len = len_a < len_b ? len_a : len_b; + + int cmp = _wcsnicmp(a->str, b->str, min_len); + if (cmp != 0) + return cmp; + + if (len_a < len_b) + return -1; + if (len_a > len_b) + return 1; + + return 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; + if (total_len == 0) { + WCHAR* empty_env = malloc(2 * sizeof(WCHAR)); + if (empty_env) { + empty_env[0] = 0; + empty_env[1] = 0; + } + return empty_env; } - 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)); + tommy_list_sort(&list, env_item_compare); + + 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; + + /* skip earlier duplicates so only the latest value is kept */ + if (env_item_compare(item, next_item) == 0) + 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; diff --git a/os/os.h b/os/os.h index 37e6c758..16e4bbd1 100644 --- a/os/os.h +++ b/os/os.h @@ -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). diff --git a/os/unix.c b/os/unix.c index 28ab58d4..92ba5a1f 100644 --- a/os/unix.c +++ b/os/unix.c @@ -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]);