| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "spawn_server_internals.h" |
| 4 | |
| 5 | #if defined(SPAWN_SERVER_VERSION_POSIX_SPAWN) |
| 6 | |
| 7 | #ifdef __APPLE__ |
| 8 | #include <crt_externs.h> |
| 9 | #define environ (*_NSGetEnviron()) |
| 10 | #else |
| 11 | extern char **environ; |
| 12 | #endif |
| 13 | |
| 14 | int spawn_server_instance_read_fd(SPAWN_INSTANCE *si) { return si->read_fd; } |
| 15 | int spawn_server_instance_write_fd(SPAWN_INSTANCE *si) { return si->write_fd; } |
| 16 | void spawn_server_instance_read_fd_unset(SPAWN_INSTANCE *si) { si->read_fd = -1; } |
| 17 | void spawn_server_instance_write_fd_unset(SPAWN_INSTANCE *si) { si->write_fd = -1; } |
| 18 | pid_t spawn_server_instance_pid(SPAWN_INSTANCE *si) { return si->child_pid; } |
| 19 | |
| 20 | static struct { |
| 21 | bool sigchld_initialized; |
| 22 | SPINLOCK spinlock; |
| 23 | SPAWN_INSTANCE *instances; |
| 24 | } spawn_globals = { |
| 25 | .spinlock = SPINLOCK_INITIALIZER, |
| 26 | .instances = NULL, |
| 27 | }; |
| 28 | |
| 29 | //static void sigchld_handler(int signum __maybe_unused) { |
| 30 | // pid_t pid; |
| 31 | // int status; |
| 32 | // |
| 33 | // while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { |
| 34 | // // Find the SPAWN_INSTANCE corresponding to this pid |
| 35 | // spinlock_lock(&spawn_globals.spinlock); |
| 36 | // for(SPAWN_INSTANCE *si = spawn_globals.instances; si ;si = si->next) { |
| 37 | // if (si->child_pid == pid) { |
| 38 | // __atomic_store_n(&si->waitpid_status, status, __ATOMIC_RELAXED); |
| 39 | // __atomic_store_n(&si->exited, true, __ATOMIC_RELAXED); |
| 40 | // break; |
| 41 | // } |
| 42 | // } |
| 43 | // spinlock_unlock(&spawn_globals.spinlock); |
| 44 | // } |
| 45 | //} |
| 46 | |
| 47 | SPAWN_SERVER* spawn_server_create(SPAWN_SERVER_OPTIONS options __maybe_unused, const char *name, spawn_request_callback_t cb __maybe_unused, int argc __maybe_unused, const char **argv __maybe_unused) { |
| 48 | SPAWN_SERVER* server = callocz(1, sizeof(SPAWN_SERVER)); |
| 49 | |
| 50 | if (name) |
| 51 | server->name = strdupz(name); |
| 52 | else |
| 53 | server->name = strdupz("unnamed"); |
| 54 | |
| 55 | if(!spawn_globals.sigchld_initialized) { |
| 56 | spawn_globals.sigchld_initialized = true; |
| 57 | |
| 58 | // struct sigaction sa; |
| 59 | // sa.sa_handler = sigchld_handler; |
| 60 | // sigemptyset(&sa.sa_mask); |
| 61 | // sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; |
| 62 | // if (sigaction(SIGCHLD, &sa, NULL) == -1) { |
| 63 | // nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: Failed to set SIGCHLD handler"); |
| 64 | // freez((void *)server->name); |
| 65 | // freez(server); |
| 66 | // return NULL; |
| 67 | // } |
| 68 | } |
| 69 | |
| 70 | return server; |
| 71 | } |
| 72 | |
| 73 | void spawn_server_destroy(SPAWN_SERVER *server) { |
| 74 | if (!server) return; |
| 75 | freez((void *)server->name); |
| 76 | freez(server); |
| 77 | } |
| 78 | |
| 79 | SPAWN_INSTANCE* spawn_server_exec(SPAWN_SERVER *server, int stderr_fd, int custom_fd __maybe_unused, const char **argv, const void *data __maybe_unused, size_t data_size __maybe_unused, SPAWN_INSTANCE_TYPE type) { |
| 80 | if (type != SPAWN_INSTANCE_TYPE_EXEC) |
| 81 | return NULL; |
| 82 | |
| 83 | CLEAN_BUFFER *cmdline_wb = argv_to_cmdline_buffer(argv); |
| 84 | const char *cmdline = buffer_tostring(cmdline_wb); |
| 85 | |
| 86 | SPAWN_INSTANCE *si = callocz(1, sizeof(SPAWN_INSTANCE)); |
| 87 | si->child_pid = -1; |
| 88 | si->request_id = __atomic_add_fetch(&server->request_id, 1, __ATOMIC_RELAXED); |
| 89 | |
| 90 | int stdin_pipe[2] = { -1, -1 }; |
| 91 | int stdout_pipe[2] = { -1, -1 }; |
| 92 | |
| 93 | if (pipe(stdin_pipe) == -1) { |
| 94 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: stdin pipe() failed: %s", cmdline); |
| 95 | freez(si); |
| 96 | return NULL; |
| 97 | } |
| 98 | |
| 99 | if (pipe(stdout_pipe) == -1) { |
| 100 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: stdout pipe() failed: %s", cmdline); |
| 101 | close(stdin_pipe[PIPE_READ]); |
| 102 | close(stdin_pipe[PIPE_WRITE]); |
| 103 | freez(si); |
| 104 | return NULL; |
| 105 | } |
| 106 | |
| 107 | posix_spawn_file_actions_t file_actions; |
| 108 | posix_spawnattr_t attr; |
| 109 | |
| 110 | if (posix_spawn_file_actions_init(&file_actions) != 0) { |
| 111 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: posix_spawn_file_actions_init() failed: %s", cmdline); |
| 112 | close(stdin_pipe[PIPE_READ]); |
| 113 | close(stdin_pipe[PIPE_WRITE]); |
| 114 | close(stdout_pipe[PIPE_READ]); |
| 115 | close(stdout_pipe[PIPE_WRITE]); |
| 116 | freez(si); |
| 117 | return NULL; |
| 118 | } |
| 119 | |
| 120 | posix_spawn_file_actions_adddup2(&file_actions, stdin_pipe[PIPE_READ], STDIN_FILENO); |
| 121 | posix_spawn_file_actions_adddup2(&file_actions, stdout_pipe[PIPE_WRITE], STDOUT_FILENO); |
| 122 | posix_spawn_file_actions_addclose(&file_actions, stdin_pipe[PIPE_READ]); |
| 123 | posix_spawn_file_actions_addclose(&file_actions, stdin_pipe[PIPE_WRITE]); |
| 124 | posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[PIPE_READ]); |
| 125 | posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[PIPE_WRITE]); |
| 126 | if(stderr_fd != STDERR_FILENO) { |
| 127 | posix_spawn_file_actions_adddup2(&file_actions, stderr_fd, STDERR_FILENO); |
| 128 | posix_spawn_file_actions_addclose(&file_actions, stderr_fd); |
| 129 | } |
| 130 | |
| 131 | if (posix_spawnattr_init(&attr) != 0) { |
| 132 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: posix_spawnattr_init() failed: %s", cmdline); |
| 133 | posix_spawn_file_actions_destroy(&file_actions); |
| 134 | close(stdin_pipe[PIPE_READ]); |
| 135 | close(stdin_pipe[PIPE_WRITE]); |
| 136 | close(stdout_pipe[PIPE_READ]); |
| 137 | close(stdout_pipe[PIPE_WRITE]); |
| 138 | freez(si); |
| 139 | return NULL; |
| 140 | } |
| 141 | |
| 142 | // Set the flags to reset the signal mask and signal actions |
| 143 | sigset_t empty_mask; |
| 144 | sigemptyset(&empty_mask); |
| 145 | if (posix_spawnattr_setsigmask(&attr, &empty_mask) != 0) { |
| 146 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: posix_spawnattr_setsigmask() failed: %s", cmdline); |
| 147 | posix_spawn_file_actions_destroy(&file_actions); |
| 148 | posix_spawnattr_destroy(&attr); |
| 149 | return false; |
| 150 | } |
| 151 | |
| 152 | short flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; |
| 153 | if (posix_spawnattr_setflags(&attr, flags) != 0) { |
| 154 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: posix_spawnattr_setflags() failed: %s", cmdline); |
| 155 | posix_spawn_file_actions_destroy(&file_actions); |
| 156 | posix_spawnattr_destroy(&attr); |
| 157 | return false; |
| 158 | } |
| 159 | |
| 160 | spinlock_lock(&spawn_globals.spinlock); |
| 161 | DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(spawn_globals.instances, si, prev, next); |
| 162 | spinlock_unlock(&spawn_globals.spinlock); |
| 163 | |
| 164 | // unfortunately, on CYGWIN/MSYS posix_spawn() is not thread safe |
| 165 | // so, we run it one by one. |
| 166 | static SPINLOCK spinlock = SPINLOCK_INITIALIZER; |
| 167 | spinlock_lock(&spinlock); |
| 168 | |
| 169 | int fds[3] = { stdin_pipe[PIPE_READ], stdout_pipe[PIPE_WRITE], stderr_fd }; |
| 170 | os_close_all_non_std_open_fds_except(fds, 3, CLOSE_RANGE_CLOEXEC); |
| 171 | |
| 172 | errno_clear(); |
| 173 | if (posix_spawn(&si->child_pid, argv[0], &file_actions, &attr, (char * const *)argv, environ) != 0) { |
| 174 | spinlock_unlock(&spinlock); |
| 175 | nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN SERVER: posix_spawn() failed: %s", cmdline); |
| 176 | |
| 177 | spinlock_lock(&spawn_globals.spinlock); |
| 178 | DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(spawn_globals.instances, si, prev, next); |
| 179 | spinlock_unlock(&spawn_globals.spinlock); |
| 180 | |
| 181 | posix_spawnattr_destroy(&attr); |
| 182 | posix_spawn_file_actions_destroy(&file_actions); |
| 183 | |
| 184 | close(stdin_pipe[PIPE_READ]); |
| 185 | close(stdin_pipe[PIPE_WRITE]); |
| 186 | close(stdout_pipe[PIPE_READ]); |
| 187 | close(stdout_pipe[PIPE_WRITE]); |
| 188 | freez(si); |
| 189 | return NULL; |
| 190 | } |
| 191 | spinlock_unlock(&spinlock); |
| 192 | |
| 193 | // Destroy the posix_spawnattr_t and posix_spawn_file_actions_t structures |
| 194 | posix_spawnattr_destroy(&attr); |
| 195 | posix_spawn_file_actions_destroy(&file_actions); |
| 196 | |
| 197 | // Close the read end of the stdin pipe and the write end of the stdout pipe in the parent process |
| 198 | close(stdin_pipe[PIPE_READ]); |
| 199 | close(stdout_pipe[PIPE_WRITE]); |
| 200 | |
| 201 | si->write_fd = stdin_pipe[PIPE_WRITE]; |
| 202 | si->read_fd = stdout_pipe[PIPE_READ]; |
| 203 | si->cmdline = strdupz(cmdline); |
| 204 | |
| 205 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 206 | "SPAWN SERVER: process created with pid %d: %s", |
| 207 | si->child_pid, cmdline); |
| 208 | return si; |
| 209 | } |
| 210 | |
| 211 | int spawn_server_exec_kill(SPAWN_SERVER *server, SPAWN_INSTANCE *si, int timeout_ms) { |
| 212 | if (!si) return -1; |
| 213 | |
| 214 | if (kill(si->child_pid, SIGTERM)) |
| 215 | nd_log(NDLS_COLLECTORS, NDLP_ERR, |
| 216 | "SPAWN PARENT: kill() of pid %d failed: %s", |
| 217 | si->child_pid, si->cmdline); |
| 218 | |
| 219 | // escalate to SIGKILL if the child does not exit promptly after SIGTERM (or if the wait could |
| 220 | // not be completed), so a SIGTERM-ignoring child cannot make the final wait block forever. |
| 221 | // the caller's timeout_ms is the SIGTERM grace; fall back to a default when not specified. |
| 222 | int grace_ms = timeout_ms > 0 ? timeout_ms : SPAWN_KILL_DEFAULT_GRACE_MS; |
| 223 | int status; |
| 224 | if(spawn_server_exec_timedwait(server, si, grace_ms, &status) != SPAWN_TIMEDWAIT_EXITED) { |
| 225 | if(kill(si->child_pid, SIGKILL) != 0) |
| 226 | // a failed SIGKILL almost always means the child is already gone (ESRCH); the wait |
| 227 | // below then returns immediately. SIGKILL is uncatchable, so it cannot be ignored by |
| 228 | // a live child - the only unbounded case left is uninterruptible (D-state) sleep, |
| 229 | // which no signal or timeout can resolve. |
| 230 | nd_log(NDLS_COLLECTORS, NDLP_ERR, |
| 231 | "SPAWN PARENT: SIGKILL of pid %d failed: %s", si->child_pid, si->cmdline); |
| 232 | } |
| 233 | else |
| 234 | return status; |
| 235 | |
| 236 | return spawn_server_exec_wait(server, si); |
| 237 | } |
| 238 | |
| 239 | static int spawn_server_waitpid(SPAWN_INSTANCE *si) { |
| 240 | int status; |
| 241 | pid_t pid; |
| 242 | |
| 243 | pid = waitpid(si->child_pid, &status, 0); |
| 244 | |
| 245 | if(pid != si->child_pid) { |
| 246 | nd_log(NDLS_COLLECTORS, NDLP_ERR, |
| 247 | "SPAWN PARENT: failed to wait for pid %d: %s", |
| 248 | si->child_pid, si->cmdline); |
| 249 | |
| 250 | return -1; |
| 251 | } |
| 252 | |
| 253 | errno_clear(); |
| 254 | |
| 255 | if(WIFEXITED(status)) { |
| 256 | if(WEXITSTATUS(status)) |
| 257 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 258 | "SPAWN SERVER: child with pid %d (request %zu) exited with exit code %d: %s", |
| 259 | pid, si->request_id, WEXITSTATUS(status), si->cmdline); |
| 260 | } |
| 261 | else if(WIFSIGNALED(status)) { |
| 262 | if(WCOREDUMP(status)) |
| 263 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 264 | "SPAWN SERVER: child with pid %d (request %zu) coredump'd due to signal %d: %s", |
| 265 | pid, si->request_id, WTERMSIG(status), si->cmdline); |
| 266 | else |
| 267 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 268 | "SPAWN SERVER: child with pid %d (request %zu) killed by signal %d: %s", |
| 269 | pid, si->request_id, WTERMSIG(status), si->cmdline); |
| 270 | } |
| 271 | else if(WIFSTOPPED(status)) { |
| 272 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 273 | "SPAWN SERVER: child with pid %d (request %zu) stopped due to signal %d: %s", |
| 274 | pid, si->request_id, WSTOPSIG(status), si->cmdline); |
| 275 | } |
| 276 | else if(WIFCONTINUED(status)) { |
| 277 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 278 | "SPAWN SERVER: child with pid %d (request %zu) continued due to signal %d: %s", |
| 279 | pid, si->request_id, SIGCONT, si->cmdline); |
| 280 | } |
| 281 | else { |
| 282 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 283 | "SPAWN SERVER: child with pid %d (request %zu) reports unhandled status: %s", |
| 284 | pid, si->request_id, si->cmdline); |
| 285 | } |
| 286 | |
| 287 | return status; |
| 288 | } |
| 289 | |
| 290 | SPAWN_TIMEDWAIT_RESULT spawn_server_exec_timedwait(SPAWN_SERVER *server, SPAWN_INSTANCE *si, int timeout_ms, int *status) { |
| 291 | if (!si) { if(status) *status = -1; return SPAWN_TIMEDWAIT_EXITED; } |
| 292 | |
| 293 | // close the child pipes to force it to exit, matching spawn_server_exec_wait and the |
| 294 | // other backends; otherwise a child blocked on stdin/stdout would never see EOF and |
| 295 | // would stay alive until the deadline forces a SIGKILL |
| 296 | if (si->read_fd != -1) { close(si->read_fd); si->read_fd = -1; } |
| 297 | if (si->write_fd != -1) { close(si->write_fd); si->write_fd = -1; } |
| 298 | |
| 299 | // a negative timeout would become a huge usec_t deadline (= unbounded wait); clamp to poll-once |
| 300 | if(timeout_ms < 0) timeout_ms = 0; |
| 301 | usec_t deadline_ut = now_monotonic_usec() + (usec_t)timeout_ms * USEC_PER_MS; |
| 302 | |
| 303 | while(!__atomic_load_n(&si->exited, __ATOMIC_RELAXED)) { |
| 304 | int wstatus = 0; |
| 305 | pid_t pid = waitpid(si->child_pid, &wstatus, WNOHANG); |
| 306 | if(pid == si->child_pid) { |
| 307 | __atomic_store_n(&si->waitpid_status, wstatus, __ATOMIC_RELAXED); |
| 308 | __atomic_store_n(&si->exited, true, __ATOMIC_RELAXED); |
| 309 | break; |
| 310 | } |
| 311 | |
| 312 | if(pid < 0 && errno != EINTR) |
| 313 | // child reaped elsewhere (e.g. ECHILD) - let the blocking wait resolve it immediately |
| 314 | break; |
| 315 | |
| 316 | // pid == 0 (still running) or EINTR (interrupted before any state change): |
| 317 | // keep waiting, but never past the deadline |
| 318 | if(now_monotonic_usec() >= deadline_ut) |
| 319 | return SPAWN_TIMEDWAIT_RUNNING; |
| 320 | |
| 321 | sleep_usec(10 * USEC_PER_MS); |
| 322 | } |
| 323 | |
| 324 | int st = spawn_server_exec_wait(server, si); |
| 325 | if(status) *status = st; |
| 326 | return SPAWN_TIMEDWAIT_EXITED; |
| 327 | } |
| 328 | |
| 329 | int spawn_server_exec_wait(SPAWN_SERVER *server __maybe_unused, SPAWN_INSTANCE *si) { |
| 330 | if (!si) return -1; |
| 331 | |
| 332 | // Close all pipe descriptors to force the child to exit |
| 333 | if (si->read_fd != -1) close(si->read_fd); |
| 334 | if (si->write_fd != -1) close(si->write_fd); |
| 335 | |
| 336 | // Wait for the process to exit |
| 337 | int status = __atomic_load_n(&si->waitpid_status, __ATOMIC_RELAXED); |
| 338 | bool exited = __atomic_load_n(&si->exited, __ATOMIC_RELAXED); |
| 339 | if(!exited) |
| 340 | status = spawn_server_waitpid(si); |
| 341 | else |
| 342 | nd_log(NDLS_COLLECTORS, NDLP_INFO, |
| 343 | "SPAWN PARENT: child with pid %d exited with status %d (sighandler): %s", |
| 344 | si->child_pid, status, si->cmdline); |
| 345 | |
| 346 | spinlock_lock(&spawn_globals.spinlock); |
| 347 | DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(spawn_globals.instances, si, prev, next); |
| 348 | spinlock_unlock(&spawn_globals.spinlock); |
| 349 | |
| 350 | freez((void *)si->cmdline); |
| 351 | freez(si); |
| 352 | return status; |
| 353 | } |
| 354 | |
| 355 | #endif |