diff --git a/daemons/pacemakerd/pcmkd_subdaemons.c b/daemons/pacemakerd/pcmkd_subdaemons.c index 15ef6a4977..f24d14216d 100644 --- a/daemons/pacemakerd/pcmkd_subdaemons.c +++ b/daemons/pacemakerd/pcmkd_subdaemons.c @@ -1,944 +1,947 @@ /* * Copyright 2010-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include "pacemakerd.h" #if SUPPORT_COROSYNC #include "pcmkd_corosync.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include enum child_daemon_flags { child_none = 0, child_respawn = 1 << 0, child_needs_cluster = 1 << 1, child_needs_retry = 1 << 2, child_active_before_startup = 1 << 3, child_shutting_down = 1 << 4, }; typedef struct pcmk_child_s { enum pcmk_ipc_server server; pid_t pid; int respawn_count; const char *uid; int check_count; uint32_t flags; } pcmk_child_t; #define PCMK_PROCESS_CHECK_INTERVAL 1000 /* 1s */ #define PCMK_PROCESS_CHECK_RETRIES 5 #define SHUTDOWN_ESCALATION_PERIOD 180000 /* 3m */ /* Index into the array below */ #define PCMK_CHILD_CONTROLD 5 static pcmk_child_t pcmk_children[] = { { pcmk_ipc_based, 0, 0, CRM_DAEMON_USER, 0, child_respawn | child_needs_cluster }, { pcmk_ipc_fenced, 0, 0, NULL, 0, child_respawn | child_needs_cluster }, { pcmk_ipc_execd, 0, 0, NULL, 0, child_respawn }, { pcmk_ipc_attrd, 0, 0, CRM_DAEMON_USER, 0, child_respawn | child_needs_cluster }, { pcmk_ipc_schedulerd, 0, 0, CRM_DAEMON_USER, 0, child_respawn }, { pcmk_ipc_controld, 0, 0, CRM_DAEMON_USER, 0, child_respawn | child_needs_cluster }, }; crm_trigger_t *shutdown_trigger = NULL; crm_trigger_t *startup_trigger = NULL; time_t subdaemon_check_progress = 0; // Whether we need root group access to talk to cluster layer static bool need_root_group = true; /* When contacted via pacemakerd-api by a client having sbd in * the name we assume it is sbd-daemon which wants to know * if pacemakerd shutdown gracefully. * Thus when everything is shutdown properly pacemakerd * waits till it has reported the graceful completion of * shutdown to sbd and just when sbd-client closes the * connection we can assume that the report has arrived * properly so that pacemakerd can finally exit. * Following two variables are used to track that handshake. */ unsigned int shutdown_complete_state_reported_to = 0; bool shutdown_complete_state_reported_client_closed = false; /* state we report when asked via pacemakerd-api status-ping */ const char *pacemakerd_state = PCMK__VALUE_INIT; bool running_with_sbd = false; GMainLoop *mainloop = NULL; static bool fatal_error = false; static int child_liveness(pcmk_child_t *child); static gboolean escalate_shutdown(gpointer data); static int start_child(pcmk_child_t * child); static void pcmk_child_exit(mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode); static void pcmk_process_exit(pcmk_child_t * child); static gboolean pcmk_shutdown_worker(gpointer user_data); static void stop_child(pcmk_child_t *child, int signal); static void for_each_child(void (*fn)(pcmk_child_t *child)) { for (int i = 0; i < PCMK__NELEM(pcmk_children); i++) { fn(&pcmk_children[i]); } } /*! * \internal * \brief Get path to subdaemon executable * * \param[in] subdaemon Subdaemon to get path for * * \return Newly allocated string with path to subdaemon executable * \note It is the caller's responsibility to free() the return value */ static inline char * subdaemon_path(pcmk_child_t *subdaemon) { return crm_strdup_printf(CRM_DAEMON_DIR "/%s", pcmk__server_name(subdaemon->server)); } static bool pcmkd_cluster_connected(void) { #if SUPPORT_COROSYNC return pcmkd_corosync_connected(); #else return true; #endif } static gboolean check_next_subdaemon(gpointer user_data) { static int next_child = 0; pcmk_child_t *child = &(pcmk_children[next_child]); const char *name = pcmk__server_name(child->server); const long long pid = PCMK__SPECIAL_PID_AS_0(child->pid); int rc = child_liveness(child); crm_trace("Checked subdaemon %s[%lld]: %s (%d)", name, pid, pcmk_rc_str(rc), rc); switch (rc) { case pcmk_rc_ok: child->check_count = 0; subdaemon_check_progress = time(NULL); break; case pcmk_rc_ipc_pid_only: // Child was previously OK if (pcmk_is_set(child->flags, child_shutting_down)) { crm_notice("Subdaemon %s[%lld] has stopped accepting IPC " "connections during shutdown", name, pid); } else if (++(child->check_count) >= PCMK_PROCESS_CHECK_RETRIES) { // cts-lab looks for this message crm_crit("Subdaemon %s[%lld] is unresponsive to IPC " "after %d attempt%s and will now be killed", name, pid, child->check_count, pcmk__plural_s(child->check_count)); stop_child(child, SIGKILL); if (pcmk_is_set(child->flags, child_respawn)) { // Respawn limit hasn't been reached, so retry another round child->check_count = 0; } } else { crm_notice("Subdaemon %s[%lld] is unresponsive to IPC " "after %d attempt%s (will recheck later)", name, pid, child->check_count, pcmk__plural_s(child->check_count)); if (pcmk_is_set(child->flags, child_respawn)) { /* as long as the respawn-limit isn't reached and we haven't run out of connect retries we account this as progress we are willing to tell to sbd */ subdaemon_check_progress = time(NULL); } } /* go to the next child and see if we can make progress there */ break; case pcmk_rc_ipc_unresponsive: if (!pcmk_is_set(child->flags, child_respawn)) { /* if a subdaemon is down and we don't want it to be restarted this is a success during shutdown. if it isn't restarted anymore due to MAX_RESPAWN it is rather no success. */ if (child->respawn_count <= MAX_RESPAWN) { subdaemon_check_progress = time(NULL); } } if (!pcmk_is_set(child->flags, child_active_before_startup)) { crm_trace("Subdaemon %s[%lld] terminated", name, pid); break; } if (pcmk_is_set(child->flags, child_respawn)) { // cts-lab looks for this message crm_err("Subdaemon %s[%lld] terminated", name, pid); } else { /* orderly shutdown */ crm_notice("Subdaemon %s[%lld] terminated", name, pid); } pcmk_process_exit(child); break; default: crm_exit(CRM_EX_FATAL); break; /* static analysis/noreturn */ } if (++next_child >= PCMK__NELEM(pcmk_children)) { next_child = 0; } return G_SOURCE_CONTINUE; } static gboolean escalate_shutdown(gpointer data) { pcmk_child_t *child = data; if (child->pid == PCMK__SPECIAL_PID) { pcmk_process_exit(child); } else if (child->pid != 0) { /* Use SIGSEGV instead of SIGKILL to create a core so we can see what it was up to */ crm_err("Subdaemon %s not terminating in a timely manner, forcing", pcmk__server_name(child->server)); stop_child(child, SIGSEGV); } return G_SOURCE_REMOVE; } static void pcmk_child_exit(mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode) { pcmk_child_t *child = mainloop_child_userdata(p); const char *name = mainloop_child_name(p); if (signo) { // cts-lab looks for this message do_crm_log(((signo == SIGKILL)? LOG_WARNING : LOG_ERR), "%s[%d] terminated with signal %d (%s)%s", name, pid, signo, strsignal(signo), (core? " and dumped core" : "")); pcmk_process_exit(child); return; } switch(exitcode) { case CRM_EX_OK: crm_info("%s[%d] exited with status %d (%s)", name, pid, exitcode, crm_exit_str(exitcode)); break; case CRM_EX_FATAL: crm_warn("Shutting cluster down because %s[%d] had fatal failure", name, pid); child->flags &= ~child_respawn; fatal_error = true; pcmk_shutdown(SIGTERM); break; case CRM_EX_PANIC: { char *msg = NULL; child->flags &= ~child_respawn; fatal_error = true; msg = crm_strdup_printf("Subdaemon %s[%d] requested panic", name, pid); pcmk__panic(msg); // Should never get here free(msg); pcmk_shutdown(SIGTERM); } break; default: // cts-lab looks for this message crm_err("%s[%d] exited with status %d (%s)", name, pid, exitcode, crm_exit_str(exitcode)); break; } pcmk_process_exit(child); } static void pcmk_process_exit(pcmk_child_t * child) { const char *name = pcmk__server_name(child->server); child->pid = 0; child->flags &= ~child_active_before_startup; child->check_count = 0; child->respawn_count += 1; if (child->respawn_count > MAX_RESPAWN) { crm_err("Subdaemon %s exceeded maximum respawn count", name); child->flags &= ~child_respawn; } if (shutdown_trigger) { /* resume step-wise shutdown (returned TRUE yields no parallelizing) */ mainloop_set_trigger(shutdown_trigger); } else if (!pcmk_is_set(child->flags, child_respawn)) { /* nothing to do */ } else if (crm_is_true(pcmk__env_option(PCMK__ENV_FAIL_FAST))) { pcmk__panic("Subdaemon failed"); } else if (child_liveness(child) == pcmk_rc_ok) { crm_warn("Not respawning subdaemon %s because IPC endpoint %s is OK", name, pcmk__server_ipc_name(child->server)); } else if (pcmk_is_set(child->flags, child_needs_cluster) && !pcmkd_cluster_connected()) { crm_notice("Not respawning subdaemon %s until cluster returns", name); child->flags |= child_needs_retry; } else { // cts-lab looks for this message crm_notice("Respawning subdaemon %s after unexpected exit", name); start_child(child); } } static gboolean pcmk_shutdown_worker(gpointer user_data) { static int phase = PCMK__NELEM(pcmk_children) - 1; static time_t next_log = 0; if (phase == PCMK__NELEM(pcmk_children) - 1) { crm_notice("Shutting down Pacemaker"); pacemakerd_state = PCMK__VALUE_SHUTTING_DOWN; } for (; phase >= 0; phase--) { pcmk_child_t *child = &(pcmk_children[phase]); const char *name = pcmk__server_name(child->server); time_t now = 0; if (child->pid == 0) { /* cleanup */ crm_debug("Subdaemon %s confirmed stopped", name); child->pid = 0; continue; } now = time(NULL); if (pcmk_is_set(child->flags, child_respawn)) { if (child->pid == PCMK__SPECIAL_PID) { crm_warn("Subdaemon %s cannot be terminated (shutdown " "will be escalated after %ld seconds if it does " "not terminate on its own; set PCMK_" PCMK__ENV_FAIL_FAST "=1 to exit immediately " "instead)", name, (long) SHUTDOWN_ESCALATION_PERIOD); } next_log = now + 30; child->flags &= ~child_respawn; stop_child(child, SIGTERM); if (phase < PCMK_CHILD_CONTROLD) { pcmk__create_timer(SHUTDOWN_ESCALATION_PERIOD, escalate_shutdown, child); } } else if (now >= next_log) { next_log = now + 30; crm_notice("Still waiting for subdaemon %s to terminate " QB_XS " pid=%lld", name, (long long) child->pid); } return G_SOURCE_CONTINUE; } crm_notice("Shutdown complete"); pacemakerd_state = PCMK__VALUE_SHUTDOWN_COMPLETE; if (!fatal_error && running_with_sbd && pcmk__get_sbd_sync_resource_startup() && !shutdown_complete_state_reported_client_closed) { crm_notice("Waiting for SBD to pick up shutdown-complete-state."); return G_SOURCE_CONTINUE; } g_main_loop_quit(mainloop); if (fatal_error) { crm_notice("Shutting down and staying down after fatal error"); #if SUPPORT_COROSYNC pcmkd_shutdown_corosync(); #endif crm_exit(CRM_EX_FATAL); } return G_SOURCE_CONTINUE; } /* TODO once libqb is taught to juggle with IPC end-points carried over as bare file descriptor (https://github.com/ClusterLabs/libqb/issues/325) it shall hand over these descriptors here if/once they are successfully pre-opened in (presumably) child_liveness(), to avoid any remaining room for races */ // \return Standard Pacemaker return code static int start_child(pcmk_child_t * child) { uid_t uid = 0; gid_t gid = 0; bool use_valgrind = false; bool use_callgrind = false; const char *name = pcmk__server_name(child->server); const char *env_valgrind = pcmk__env_option(PCMK__ENV_VALGRIND_ENABLED); const char *env_callgrind = pcmk__env_option(PCMK__ENV_CALLGRIND_ENABLED); child->flags &= ~(child_active_before_startup | child_shutting_down); child->check_count = 0; if (env_callgrind != NULL && crm_is_true(env_callgrind)) { use_callgrind = true; use_valgrind = true; } else if ((env_callgrind != NULL) && (strstr(env_callgrind, name) != NULL)) { use_callgrind = true; use_valgrind = true; } else if (env_valgrind != NULL && crm_is_true(env_valgrind)) { use_valgrind = true; } else if ((env_valgrind != NULL) && (strstr(env_valgrind, name) != NULL)) { use_valgrind = true; } if (use_valgrind && strlen(PCMK__VALGRIND_EXEC) == 0) { crm_warn("Cannot enable valgrind for subdaemon %s: valgrind not found", name); use_valgrind = false; } if ((child->uid != NULL) && (crm_user_lookup(child->uid, &uid, &gid) < 0)) { crm_err("Invalid user (%s) for subdaemon %s: not found", child->uid, name); return EACCES; } child->pid = fork(); pcmk__assert(child->pid != -1); if (child->pid > 0) { /* parent */ mainloop_child_add(child->pid, 0, name, child, pcmk_child_exit); if (use_valgrind) { crm_info("Forked process %lld using user %lu (%s) and group %lu " "for subdaemon %s (valgrind enabled: %s)", (long long) child->pid, (unsigned long) uid, pcmk__s(child->uid, "root"), (unsigned long) gid, name, PCMK__VALGRIND_EXEC); } else { crm_info("Forked process %lld using user %lu (%s) and group %lu " "for subdaemon %s", (long long) child->pid, (unsigned long) uid, pcmk__s(child->uid, "root"), (unsigned long) gid, name); } return pcmk_rc_ok; } else { char *path = subdaemon_path(child); /* Start a new session */ setsid(); if(gid) { // Drop root group access if not needed if (!need_root_group && (setgid(gid) < 0)) { crm_warn("Could not set subdaemon %s group to %lu: %s", name, (unsigned long) gid, strerror(errno)); } /* Initialize supplementary groups to only those always granted to * the user, plus haclient (so we can access IPC). */ if (initgroups(child->uid, gid) < 0) { crm_err("Cannot initialize system groups for subdaemon %s: %s " QB_XS " errno=%d", name, pcmk_rc_str(errno), errno); } } if (uid && setuid(uid) < 0) { crm_warn("Could not set subdaemon %s user to %s: %s " QB_XS " uid=%lu errno=%d", name, strerror(errno), child->uid, (unsigned long) uid, errno); } - pcmk__close_fds_in_child(true); + pcmk__close_fds_in_child(); + close(STDIN_FILENO); pcmk__open_devnull(O_RDONLY); // stdin (fd 0) + close(STDOUT_FILENO); pcmk__open_devnull(O_WRONLY); // stdout (fd 1) + close(STDERR_FILENO); pcmk__open_devnull(O_WRONLY); // stderr (fd 2) if (use_callgrind) { char *out_file = pcmk__str_copy("--callgrind-out-file=" CRM_STATE_DIR "/callgrind.opt.%p"); execlp(PCMK__VALGRIND_EXEC, PCMK__VALGRIND_EXEC, "--tool=callgrind", out_file, path, (char *) NULL); free(out_file); } else if (use_valgrind) { execlp(PCMK__VALGRIND_EXEC, PCMK__VALGRIND_EXEC, path, (char *) NULL); } else { execlp(path, path, (char *) NULL); } free(path); crm_crit("Could not execute subdaemon %s: %s", name, strerror(errno)); crm_exit(CRM_EX_FATAL); } return pcmk_rc_ok; /* never reached */ } /*! * \internal * \brief Check the liveness of the child based on IPC name and PID if tracked * * \param[in,out] child Child tracked data * * \return Standard Pacemaker return code * * \note Return codes of particular interest include pcmk_rc_ipc_unresponsive * indicating that no trace of IPC liveness was detected, * pcmk_rc_ipc_unauthorized indicating that the IPC endpoint is blocked by * an unauthorized process, and pcmk_rc_ipc_pid_only indicating that * the child is up by PID but not IPC end-point (possibly starting). * \note This function doesn't modify any of \p child members but \c pid, * and is not actively toying with processes as such but invoking * \c stop_child in one particular case (there's for some reason * a different authentic holder of the IPC end-point). */ static int child_liveness(pcmk_child_t *child) { uid_t cl_uid = 0; gid_t cl_gid = 0; const uid_t root_uid = 0; const gid_t root_gid = 0; const uid_t *ref_uid; const gid_t *ref_gid; const char *name = pcmk__server_name(child->server); int rc = pcmk_rc_ipc_unresponsive; int pid_active = pcmk_rc_ok; int legacy_rc = pcmk_ok; pid_t ipc_pid = 0; if (child->uid == NULL) { ref_uid = &root_uid; ref_gid = &root_gid; } else { ref_uid = &cl_uid; ref_gid = &cl_gid; legacy_rc = pcmk_daemon_user(&cl_uid, &cl_gid); } if (legacy_rc < 0) { rc = pcmk_legacy2rc(legacy_rc); crm_err("Could not find user and group IDs for user %s: %s " QB_XS " rc=%d", CRM_DAEMON_USER, pcmk_rc_str(rc), rc); } else { const char *ipc_name = pcmk__server_ipc_name(child->server); rc = pcmk__ipc_is_authentic_process_active(ipc_name, *ref_uid, *ref_gid, &ipc_pid); if ((rc == pcmk_rc_ok) || (rc == pcmk_rc_ipc_unresponsive)) { if (child->pid <= 0) { /* If rc is pcmk_rc_ok, ipc_pid is nonzero and this * initializes a new child. If rc is * pcmk_rc_ipc_unresponsive, ipc_pid is zero, and we will * investigate further. */ child->pid = ipc_pid; } else if ((ipc_pid != 0) && (child->pid != ipc_pid)) { /* An unexpected (but authorized) process is responding to * IPC. Investigate further. */ rc = pcmk_rc_ipc_unresponsive; } } } if (rc != pcmk_rc_ipc_unresponsive) { return rc; } /* If we get here, a child without IPC is being tracked, no IPC liveness * has been detected, or IPC liveness has been detected with an * unexpected (but authorized) process. This is safe on FreeBSD since * the only change possible from a proper child's PID into "special" PID * of 1 behind more loosely related process. */ pid_active = pcmk__pid_active(child->pid, name); if ((ipc_pid != 0) && ((pid_active != pcmk_rc_ok) || (ipc_pid == PCMK__SPECIAL_PID) || (pcmk__pid_active(ipc_pid, name) == pcmk_rc_ok))) { /* An unexpected (but authorized) process was detected at the IPC * endpoint, and either it is active, or the child we're tracking is * not. */ if (pid_active == pcmk_rc_ok) { /* The child we're tracking is active. Kill it, and adopt the * detected process. This assumes that our children don't fork * (thus getting a different PID owning the IPC), but rather the * tracking got out of sync because of some means external to * Pacemaker, and adopting the detected process is better than * killing it and possibly having to spawn a new child. */ /* not possessing IPC, afterall (what about corosync CPG?) */ stop_child(child, SIGKILL); } rc = pcmk_rc_ok; child->pid = ipc_pid; } else if (pid_active == pcmk_rc_ok) { // Our tracked child's PID was found active, but not its IPC rc = pcmk_rc_ipc_pid_only; } else if ((child->pid == 0) && (pid_active == EINVAL)) { // FreeBSD can return EINVAL rc = pcmk_rc_ipc_unresponsive; } else if (pid_active == EACCES) { rc = pcmk_rc_ipc_unauthorized; } else if (pid_active == ESRCH) { rc = pcmk_rc_ipc_unresponsive; } else { rc = pid_active; } return rc; } static void reset_respawn_count(pcmk_child_t *child) { /* Restore pristine state */ child->respawn_count = 0; } #define WAIT_TRIES 4 /* together with interleaved sleeps, worst case ~ 1s */ static int child_up_but_no_ipc(pcmk_child_t *child) { const char *ipc_name = pcmk__server_ipc_name(child->server); if (child->respawn_count == WAIT_TRIES) { crm_crit("%s IPC endpoint for existing process %lld did not (re)appear", ipc_name, (long long) PCMK__SPECIAL_PID_AS_0(child->pid)); return pcmk_rc_ipc_pid_only; } crm_warn("Cannot find %s IPC endpoint for existing process %ld, could still " "reappear in %d attempts", ipc_name, (long long) PCMK__SPECIAL_PID_AS_0(child->pid), WAIT_TRIES - child->respawn_count); return EAGAIN; } static int child_alive(pcmk_child_t *child) { const char *name = pcmk__server_name(child->server); if (child->pid == PCMK__SPECIAL_PID) { if (crm_is_true(pcmk__env_option(PCMK__ENV_FAIL_FAST))) { crm_crit("Cannot track pre-existing process for %s IPC on this " "platform and PCMK_" PCMK__ENV_FAIL_FAST " requested", name); return EOPNOTSUPP; } else if (child->respawn_count == WAIT_TRIES) { /* Because PCMK__ENV_FAIL_FAST wasn't requested, we can't bail * out. Instead, switch to IPC liveness monitoring which is not * very suitable for heavy system load. */ crm_notice("Cannot track pre-existing process for %s IPC on this " "platform but assuming it is stable and using liveness " "monitoring", name); crm_warn("The process for %s IPC cannot be terminated, so " "shutdown will be delayed by %d s to allow time for it " "to terminate on its own", name, SHUTDOWN_ESCALATION_PERIOD); } else { crm_warn("Cannot track pre-existing process for %s IPC on this " "platform; checking %d more times", name, WAIT_TRIES - child->respawn_count); return EAGAIN; } } crm_notice("Tracking existing %s process (pid=%lld)", name, (long long) PCMK__SPECIAL_PID_AS_0(child->pid)); child->respawn_count = -1; /* 0~keep watching */ child->flags |= child_active_before_startup; return pcmk_rc_ok; } static int find_and_track_child(pcmk_child_t *child, int rounds, bool *wait_in_progress) { int rc = pcmk_rc_ok; const char *name = pcmk__server_name(child->server); if (child->respawn_count < 0) { return EAGAIN; } rc = child_liveness(child); if (rc == pcmk_rc_ipc_unresponsive) { /* As a speculation, don't give up if there are more rounds to * come for other reasons, but don't artificially wait just * because of this, since we would preferably start ASAP. */ return EAGAIN; } child->respawn_count = rounds; if (rc == pcmk_rc_ok) { rc = child_alive(child); if (rc == EAGAIN) { *wait_in_progress = true; } } else if (rc == pcmk_rc_ipc_pid_only) { rc = child_up_but_no_ipc(child); if (rc == EAGAIN) { *wait_in_progress = true; } } else { crm_crit("Checked liveness of %s: %s " QB_XS " rc=%d", name, pcmk_rc_str(rc), rc); } return rc; } /*! * \internal * \brief Initial one-off check of the pre-existing "child" processes * * With "child" process, we mean the subdaemon that defines an API end-point * (all of them do as of the comment) -- the possible complement is skipped * as it is deemed it has no such shared resources to cause conflicts about, * hence it can presumably be started anew without hesitation. * If that won't hold true in the future, the concept of a shared resource * will have to be generalized beyond the API end-point. * * For boundary cases that the "child" is still starting (IPC end-point is yet * to be witnessed), or more rarely (practically FreeBSD only), when there's * a pre-existing "untrackable" authentic process, we give the situation some * time to possibly unfold in the right direction, meaning that said socket * will appear or the unattainable process will disappear per the observable * IPC, respectively. * * \return Standard Pacemaker return code * * \note Since this gets run at the very start, \c respawn_count fields * for particular children get temporarily overloaded with "rounds * of waiting" tracking, restored once we are about to finish with * success (i.e. returning value >=0) and will remain unrestored * otherwise. One way to suppress liveness detection logic for * particular child is to set the said value to a negative number. */ int find_and_track_existing_processes(void) { bool wait_in_progress; size_t i, rounds; for (rounds = 1; rounds <= WAIT_TRIES; rounds++) { wait_in_progress = false; for (i = 0; i < PCMK__NELEM(pcmk_children); i++) { int rc = find_and_track_child(&pcmk_children[i], rounds, &wait_in_progress); if (rc == pcmk_rc_ok) { break; } else if (rc != EAGAIN) { return rc; } } if (!wait_in_progress) { break; } pcmk__sleep_ms(250); // Wait a bit for changes to possibly happen } for_each_child(reset_respawn_count); pcmk__create_timer(PCMK_PROCESS_CHECK_INTERVAL, check_next_subdaemon, NULL); return pcmk_rc_ok; } static void start_subdaemon(pcmk_child_t *child) { if (child->pid != 0) { /* We are already tracking this process */ return; } start_child(child); } gboolean init_children_processes(void *user_data) { if (pcmk_get_cluster_layer() == pcmk_cluster_layer_corosync) { /* Corosync clusters can drop root group access, because we set * uidgid.gid.${gid}=1 via CMAP, which allows these processes to connect * to corosync. */ need_root_group = false; } /* start any children that have not been detected */ for_each_child(start_subdaemon); /* From this point on, any daemons being started will be due to * respawning rather than node start. * * This may be useful for the daemons to know */ pcmk__set_env_option(PCMK__ENV_RESPAWNED, PCMK_VALUE_TRUE, false); pacemakerd_state = PCMK__VALUE_RUNNING; return G_SOURCE_CONTINUE; } void pcmk_shutdown(int nsig) { if (shutdown_trigger == NULL) { shutdown_trigger = mainloop_add_trigger(G_PRIORITY_HIGH, pcmk_shutdown_worker, NULL); } mainloop_set_trigger(shutdown_trigger); } static void restart_subdaemon(pcmk_child_t *child) { if (!pcmk_is_set(child->flags, child_needs_retry) || child->pid != 0) { return; } crm_notice("Respawning cluster-based subdaemon %s", pcmk__server_name(child->server)); if (start_child(child)) { child->flags &= ~child_needs_retry; } } void restart_cluster_subdaemons(void) { for_each_child(restart_subdaemon); } static void stop_child(pcmk_child_t *child, int signal) { const char *name = pcmk__server_name(child->server); if (signal == 0) { signal = SIGTERM; } /* why to skip PID of 1? - FreeBSD ~ how untrackable process behind IPC is masqueraded as - elsewhere: how "init" task is designated; in particular, in systemd arrangement of socket-based activation, this is pretty real */ if (child->pid == PCMK__SPECIAL_PID) { crm_debug("Nothing to do to stop subdaemon %s[%lld]", name, (long long) PCMK__SPECIAL_PID_AS_0(child->pid)); return; } if (child->pid <= 0) { crm_trace("Nothing to do to stop subdaemon %s: Not running", name); return; } errno = 0; if (kill(child->pid, signal) == 0) { crm_notice("Stopping subdaemon %s " QB_XS " via signal %d to process %lld", name, signal, (long long) child->pid); child->flags |= child_shutting_down; } else { crm_err("Could not stop subdaemon %s[%lld] with signal %d: %s", name, (long long) child->pid, signal, strerror(errno)); } } diff --git a/include/crm/common/io_internal.h b/include/crm/common/io_internal.h index e9c68f6eb8..db9eced65a 100644 --- a/include/crm/common/io_internal.h +++ b/include/crm/common/io_internal.h @@ -1,63 +1,63 @@ /* - * Copyright 2022-2024 the Pacemaker project contributors + * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_IO_INTERNAL__H #define PCMK__CRM_COMMON_IO_INTERNAL__H #include // open() #include // bool #include // uid_t, gid_t #ifdef __cplusplus extern "C" { #endif int pcmk__real_path(const char *path, char **resolved_path); char *pcmk__series_filename(const char *directory, const char *series, unsigned int sequence, bool bzip); int pcmk__read_series_sequence(const char *directory, const char *series, unsigned int *seq); void pcmk__write_series_sequence(const char *directory, const char *series, unsigned int sequence, int max); int pcmk__chown_series_sequence(const char *directory, const char *series, uid_t uid, gid_t gid); int pcmk__build_path(const char *path_c, mode_t mode); char *pcmk__full_path(const char *filename, const char *dirname); bool pcmk__daemon_can_write(const char *dir, const char *file); void pcmk__sync_directory(const char *name); int pcmk__file_contents(const char *filename, char **contents); int pcmk__write_sync(int fd, const char *contents); int pcmk__set_nonblocking(int fd); const char *pcmk__get_tmpdir(void); -void pcmk__close_fds_in_child(bool); +void pcmk__close_fds_in_child(void); /*! * \internal * \brief Open /dev/null to consume next available file descriptor * * Open /dev/null, disregarding the result. This is intended when daemonizing to * be able to null stdin, stdout, and stderr. * * \param[in] flags O_RDONLY (stdin) or O_WRONLY (stdout and stderr) */ static inline void pcmk__open_devnull(int flags) { (void) open("/dev/null", flags); } #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_IO_INTERNAL__H diff --git a/lib/common/io.c b/lib/common/io.c index 9e40a9908f..ff5e58d61d 100644 --- a/lib/common/io.c +++ b/lib/common/io.c @@ -1,637 +1,635 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*! * \internal * \brief Create a directory, including any parent directories needed * * \param[in] path_c Pathname of the directory to create * \param[in] mode Permissions to be used (with current umask) when creating * * \return Standard Pacemaker return code */ int pcmk__build_path(const char *path_c, mode_t mode) { int offset = 1, len = 0; int rc = pcmk_rc_ok; char *path = strdup(path_c); CRM_CHECK(path != NULL, return -ENOMEM); for (len = strlen(path); offset < len; offset++) { if (path[offset] == '/') { path[offset] = 0; if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { rc = errno; goto done; } path[offset] = '/'; } } if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { rc = errno; } done: free(path); return rc; } /*! * \internal * \brief Return canonicalized form of a path name * * \param[in] path Pathname to canonicalize * \param[out] resolved_path Where to store canonicalized pathname * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p resolved_path on success. * \note This function exists because not all C library versions of * realpath(path, resolved_path) support a NULL resolved_path. */ int pcmk__real_path(const char *path, char **resolved_path) { CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL); #if _POSIX_VERSION >= 200809L /* Recent C libraries can dynamically allocate memory as needed */ *resolved_path = realpath(path, NULL); return (*resolved_path == NULL)? errno : pcmk_rc_ok; #elif defined(PATH_MAX) /* Older implementations require pre-allocated memory */ /* (this is less desirable because PATH_MAX may be huge or not defined) */ *resolved_path = malloc(PATH_MAX); if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) { return errno; } return pcmk_rc_ok; #else *resolved_path = NULL; return ENOTSUP; #endif } /*! * \internal * \brief Create a file name using a sequence number * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[in] sequence Sequence number * \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension * * \return Newly allocated file path (asserts on error, so always non-NULL) * \note The caller is responsible for freeing the return value. */ char * pcmk__series_filename(const char *directory, const char *series, unsigned int sequence, bool bzip) { pcmk__assert((directory != NULL) && (series != NULL)); return crm_strdup_printf("%s/%s-%u.%s", directory, series, sequence, (bzip? "bz2" : "raw")); } /*! * \internal * \brief Read sequence number stored in a file series' .last file * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[out] seq Where to store the sequence number * * \return Standard Pacemaker return code */ int pcmk__read_series_sequence(const char *directory, const char *series, unsigned int *seq) { int rc; FILE *fp = NULL; char *series_file = NULL; if ((directory == NULL) || (series == NULL) || (seq == NULL)) { return EINVAL; } series_file = crm_strdup_printf("%s/%s.last", directory, series); fp = fopen(series_file, "r"); if (fp == NULL) { rc = errno; crm_debug("Could not open series file %s: %s", series_file, strerror(rc)); free(series_file); return rc; } errno = 0; if (fscanf(fp, "%u", seq) != 1) { rc = (errno == 0)? ENODATA : errno; crm_debug("Could not read sequence number from series file %s: %s", series_file, pcmk_rc_str(rc)); fclose(fp); return rc; } fclose(fp); crm_trace("Found last sequence number %u in series file %s", *seq, series_file); free(series_file); return pcmk_rc_ok; } /*! * \internal * \brief Write sequence number to a file series' .last file * * \param[in] directory Directory that contains the file series * \param[in] series Start of file name * \param[in] sequence Sequence number to write * \param[in] max Maximum sequence value, after which it is reset to 0 * * \note This function logs some errors but does not return any to the caller */ void pcmk__write_series_sequence(const char *directory, const char *series, unsigned int sequence, int max) { FILE *file_strm = NULL; char *series_file = NULL; CRM_CHECK(directory != NULL, return); CRM_CHECK(series != NULL, return); if (max == 0) { return; } if (max > 0 && sequence >= max) { sequence = 0; } series_file = crm_strdup_printf("%s/%s.last", directory, series); file_strm = fopen(series_file, "w"); if (file_strm != NULL) { int rc = fprintf(file_strm, "%u", sequence); if (rc < 0) { crm_err("Cannot write to series file %s", series_file); } fflush(file_strm); fclose(file_strm); } else { crm_err("Cannot open series file %s for writing", series_file); } free(series_file); } /*! * \internal * \brief Change the owner and group of a file series' .last file * * \param[in] directory Directory that contains series * \param[in] series Series to change * \param[in] uid User ID of desired file owner * \param[in] gid Group ID of desired file group * * \return Standard Pacemaker return code * \note The caller must have the appropriate privileges. */ int pcmk__chown_series_sequence(const char *directory, const char *series, uid_t uid, gid_t gid) { char *series_file = NULL; int rc = pcmk_rc_ok; if ((directory == NULL) || (series == NULL)) { return EINVAL; } series_file = crm_strdup_printf("%s/%s.last", directory, series); if (chown(series_file, uid, gid) < 0) { rc = errno; } free(series_file); return rc; } static bool pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat) { struct passwd *sys_user = NULL; errno = 0; sys_user = getpwnam(CRM_DAEMON_USER); if (sys_user == NULL) { crm_notice("Could not find user %s: %s", CRM_DAEMON_USER, pcmk_rc_str(errno)); return FALSE; } if (target_stat->st_uid != sys_user->pw_uid) { crm_notice("%s is not owned by user %s " QB_XS " uid %d != %d", target_name, CRM_DAEMON_USER, sys_user->pw_uid, target_stat->st_uid); return FALSE; } if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) { crm_notice("%s is not readable and writable by user %s " QB_XS " st_mode=0%lo", target_name, CRM_DAEMON_USER, (unsigned long) target_stat->st_mode); return FALSE; } return TRUE; } static bool pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat) { struct group *sys_grp = NULL; errno = 0; sys_grp = getgrnam(CRM_DAEMON_GROUP); if (sys_grp == NULL) { crm_notice("Could not find group %s: %s", CRM_DAEMON_GROUP, pcmk_rc_str(errno)); return FALSE; } if (target_stat->st_gid != sys_grp->gr_gid) { crm_notice("%s is not owned by group %s " QB_XS " uid %d != %d", target_name, CRM_DAEMON_GROUP, sys_grp->gr_gid, target_stat->st_gid); return FALSE; } if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) { crm_notice("%s is not readable and writable by group %s " QB_XS " st_mode=0%lo", target_name, CRM_DAEMON_GROUP, (unsigned long) target_stat->st_mode); return FALSE; } return TRUE; } /*! * \internal * \brief Check whether a directory or file is writable by the cluster daemon * * Return true if either the cluster daemon user or cluster daemon group has * write permission on a specified file or directory. * * \param[in] dir Directory to check (this argument must be specified, and * the directory must exist) * \param[in] file File to check (only the directory will be checked if this * argument is not specified or the file does not exist) * * \return true if target is writable by cluster daemon, false otherwise */ bool pcmk__daemon_can_write(const char *dir, const char *file) { int s_res = 0; struct stat buf; char *full_file = NULL; const char *target = NULL; // Caller must supply directory pcmk__assert(dir != NULL); // If file is given, check whether it exists as a regular file if (file != NULL) { full_file = crm_strdup_printf("%s/%s", dir, file); target = full_file; s_res = stat(full_file, &buf); if (s_res < 0) { crm_notice("%s not found: %s", target, pcmk_rc_str(errno)); free(full_file); full_file = NULL; target = NULL; } else if (S_ISREG(buf.st_mode) == FALSE) { crm_err("%s must be a regular file " QB_XS " st_mode=0%lo", target, (unsigned long) buf.st_mode); free(full_file); return false; } } // If file is not given, ensure dir exists as directory if (target == NULL) { target = dir; s_res = stat(dir, &buf); if (s_res < 0) { crm_err("%s not found: %s", dir, pcmk_rc_str(errno)); return false; } else if (S_ISDIR(buf.st_mode) == FALSE) { crm_err("%s must be a directory " QB_XS " st_mode=0%lo", dir, (unsigned long) buf.st_mode); return false; } } if (!pcmk__daemon_user_can_write(target, &buf) && !pcmk__daemon_group_can_write(target, &buf)) { crm_err("%s must be owned and writable by either user %s or group %s " QB_XS " st_mode=0%lo", target, CRM_DAEMON_USER, CRM_DAEMON_GROUP, (unsigned long) buf.st_mode); free(full_file); return false; } free(full_file); return true; } /*! * \internal * \brief Flush and sync a directory to disk * * \param[in] name Directory to flush and sync * \note This function logs errors but does not return them to the caller */ void pcmk__sync_directory(const char *name) { int fd; DIR *directory; directory = opendir(name); if (directory == NULL) { crm_err("Could not open %s for syncing: %s", name, strerror(errno)); return; } fd = dirfd(directory); if (fd < 0) { crm_err("Could not obtain file descriptor for %s: %s", name, strerror(errno)); return; } if (fsync(fd) < 0) { crm_err("Could not sync %s: %s", name, strerror(errno)); } if (closedir(directory) < 0) { crm_err("Could not close %s after fsync: %s", name, strerror(errno)); } } /*! * \internal * \brief Read the contents of a file * * \param[in] filename Name of file to read * \param[out] contents Where to store file contents * * \return Standard Pacemaker return code * \note On success, the caller is responsible for freeing contents. */ int pcmk__file_contents(const char *filename, char **contents) { FILE *fp; int length, read_len; int rc = pcmk_rc_ok; if ((filename == NULL) || (contents == NULL)) { return EINVAL; } fp = fopen(filename, "r"); if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) { rc = errno; goto bail; } length = ftell(fp); if (length < 0) { rc = errno; goto bail; } if (length == 0) { *contents = NULL; } else { *contents = calloc(length + 1, sizeof(char)); if (*contents == NULL) { rc = errno; goto bail; } errno = 0; rewind(fp); if (errno != 0) { rc = errno; goto bail; } read_len = fread(*contents, 1, length, fp); if (read_len != length) { free(*contents); *contents = NULL; rc = EIO; } else { /* Coverity thinks *contents isn't null-terminated. It doesn't * understand calloc(). */ (*contents)[length] = '\0'; } } bail: if (fp != NULL) { fclose(fp); } return rc; } /*! * \internal * \brief Write text to a file, flush and sync it to disk, then close the file * * \param[in] fd File descriptor opened for writing * \param[in] contents String to write to file * * \return Standard Pacemaker return code */ int pcmk__write_sync(int fd, const char *contents) { int rc = 0; FILE *fp = fdopen(fd, "w"); if (fp == NULL) { return errno; } if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) { rc = EIO; } if (fflush(fp) != 0) { rc = errno; } if (fsync(fileno(fp)) < 0) { rc = errno; } fclose(fp); return rc; } /*! * \internal * \brief Set a file descriptor to non-blocking * * \param[in] fd File descriptor to use * * \return Standard Pacemaker return code */ int pcmk__set_nonblocking(int fd) { int flag = fcntl(fd, F_GETFL); if (flag < 0) { return errno; } if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) { return errno; } return pcmk_rc_ok; } /*! * \internal * \brief Get directory name for temporary files * * Return the value of the TMPDIR environment variable if it is set to a * full path, otherwise return "/tmp". * * \return Name of directory to be used for temporary files */ const char * pcmk__get_tmpdir(void) { const char *dir = getenv("TMPDIR"); return (dir && (*dir == '/'))? dir : "/tmp"; } /*! * \internal - * \brief Close open file descriptors + * \brief Close open file descriptors except standard streams * - * Close all file descriptors (except optionally stdin, stdout, and stderr), - * which is a best practice for a new child process forked for the purpose of - * executing an external program. - * - * \param[in] bool If true, close stdin, stdout, and stderr as well + * Close all file descriptors (except stdin, stdout, and stderr), which is a + * best practice for a new child process forked for the purpose of executing an + * external program. */ void -pcmk__close_fds_in_child(bool all) +pcmk__close_fds_in_child(void) { DIR *dir; struct rlimit rlim; rlim_t max_fd; - int min_fd = (all? 0 : (STDERR_FILENO + 1)); + const int min_fd = STDERR_FILENO + 1; /* Find the current process's (soft) limit for open files. getrlimit() * should always work, but have a fallback just in case. */ if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) { max_fd = rlim.rlim_cur - 1; } else { long conf_max = sysconf(_SC_OPEN_MAX); max_fd = (conf_max > 0)? conf_max : 1024; } /* First try /proc. If that returns NULL (either because opening the * directory failed, or because procfs isn't supported on this platform), * fall back to /dev/fd. */ dir = pcmk__procfs_fd_dir(); if (dir == NULL) { dir = opendir("/dev/fd"); } if (dir != NULL) { struct dirent *entry; int dir_fd = dirfd(dir); while ((entry = readdir(dir)) != NULL) { int lpc = atoi(entry->d_name); /* How could one of these entries be higher than max_fd, you ask? * It isn't possible in normal operation, but when run under * valgrind, valgrind can open high-numbered file descriptors for * its own use that are higher than the process's soft limit. * These will show up in the fd directory but aren't closable. */ if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) { close(lpc); } } closedir(dir); return; } /* If no fd directory is available, iterate over all possible descriptors. * This is less efficient due to the overhead of many system calls. */ for (int lpc = max_fd; lpc >= min_fd; lpc--) { close(lpc); } } /*! * \brief Duplicate a file path, inserting a prefix if not absolute * * \param[in] filename File path to duplicate * \param[in] dirname If filename is not absolute, prefix to add * * \return Newly allocated memory with full path (guaranteed non-NULL) */ char * pcmk__full_path(const char *filename, const char *dirname) { pcmk__assert(filename != NULL); if (filename[0] == '/') { return pcmk__str_copy(filename); } pcmk__assert(dirname != NULL); return crm_strdup_printf("%s/%s", dirname, filename); } diff --git a/lib/services/services_linux.c b/lib/services/services_linux.c index 0f4fe0dcb2..fcd979734b 100644 --- a/lib/services/services_linux.c +++ b/lib/services/services_linux.c @@ -1,1505 +1,1505 @@ /* * Copyright 2010-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include "crm/crm.h" #include "crm/common/mainloop.h" #include "crm/services.h" #include "crm/services_internal.h" #include "services_private.h" static void close_pipe(int fildes[]); /* We have two alternative ways of handling SIGCHLD when synchronously waiting * for spawned processes to complete. Both rely on polling a file descriptor to * discover SIGCHLD events. * * If sys/signalfd.h is available (e.g. on Linux), we call signalfd() to * generate the file descriptor. Otherwise, we use the "self-pipe trick" * (opening a pipe and writing a byte to it when SIGCHLD is received). */ #ifdef HAVE_SYS_SIGNALFD_H // signalfd() implementation #include // Everything needed to manage SIGCHLD handling struct sigchld_data_s { sigset_t mask; // Signals to block now (including SIGCHLD) sigset_t old_mask; // Previous set of blocked signals bool ignored; // If SIGCHLD for another child has been ignored }; // Initialize SIGCHLD data and prepare for use static bool sigchld_setup(struct sigchld_data_s *data) { sigemptyset(&(data->mask)); sigaddset(&(data->mask), SIGCHLD); sigemptyset(&(data->old_mask)); // Block SIGCHLD (saving previous set of blocked signals to restore later) if (sigprocmask(SIG_BLOCK, &(data->mask), &(data->old_mask)) < 0) { crm_info("Wait for child process completion failed: %s " QB_XS " source=sigprocmask", pcmk_rc_str(errno)); return false; } data->ignored = false; return true; } // Get a file descriptor suitable for polling for SIGCHLD events static int sigchld_open(struct sigchld_data_s *data) { int fd; CRM_CHECK(data != NULL, return -1); fd = signalfd(-1, &(data->mask), SFD_NONBLOCK); if (fd < 0) { crm_info("Wait for child process completion failed: %s " QB_XS " source=signalfd", pcmk_rc_str(errno)); } return fd; } // Close a file descriptor returned by sigchld_open() static void sigchld_close(int fd) { if (fd > 0) { close(fd); } } // Return true if SIGCHLD was received from polled fd static bool sigchld_received(int fd, int pid, struct sigchld_data_s *data) { struct signalfd_siginfo fdsi; ssize_t s; if (fd < 0) { return false; } s = read(fd, &fdsi, sizeof(struct signalfd_siginfo)); if (s != sizeof(struct signalfd_siginfo)) { crm_info("Wait for child process completion failed: %s " QB_XS " source=read", pcmk_rc_str(errno)); } else if (fdsi.ssi_signo == SIGCHLD) { if (fdsi.ssi_pid == pid) { return true; } else { /* This SIGCHLD is for another child. We have to ignore it here but * will still need to resend it after this synchronous action has * completed and SIGCHLD has been restored to be handled by the * previous SIGCHLD handler, so that it will be handled. */ data->ignored = true; return false; } } return false; } // Do anything needed after done waiting for SIGCHLD static void sigchld_cleanup(struct sigchld_data_s *data) { // Restore the original set of blocked signals if ((sigismember(&(data->old_mask), SIGCHLD) == 0) && (sigprocmask(SIG_UNBLOCK, &(data->mask), NULL) < 0)) { crm_warn("Could not clean up after child process completion: %s", pcmk_rc_str(errno)); } // Resend any ignored SIGCHLD for other children so that they'll be handled. if (data->ignored && kill(getpid(), SIGCHLD) != 0) { crm_warn("Could not resend ignored SIGCHLD to ourselves: %s", pcmk_rc_str(errno)); } } #else // HAVE_SYS_SIGNALFD_H not defined // Self-pipe implementation (see above for function descriptions) struct sigchld_data_s { int pipe_fd[2]; // Pipe file descriptors struct sigaction sa; // Signal handling info (with SIGCHLD) struct sigaction old_sa; // Previous signal handling info bool ignored; // If SIGCHLD for another child has been ignored }; // We need a global to use in the signal handler volatile struct sigchld_data_s *last_sigchld_data = NULL; static void sigchld_handler(void) { // We received a SIGCHLD, so trigger pipe polling if ((last_sigchld_data != NULL) && (last_sigchld_data->pipe_fd[1] >= 0) && (write(last_sigchld_data->pipe_fd[1], "", 1) == -1)) { crm_info("Wait for child process completion failed: %s " QB_XS " source=write", pcmk_rc_str(errno)); } } static bool sigchld_setup(struct sigchld_data_s *data) { int rc; data->pipe_fd[0] = data->pipe_fd[1] = -1; if (pipe(data->pipe_fd) == -1) { crm_info("Wait for child process completion failed: %s " QB_XS " source=pipe", pcmk_rc_str(errno)); return false; } rc = pcmk__set_nonblocking(data->pipe_fd[0]); if (rc != pcmk_rc_ok) { crm_info("Could not set pipe input non-blocking: %s " QB_XS " rc=%d", pcmk_rc_str(rc), rc); } rc = pcmk__set_nonblocking(data->pipe_fd[1]); if (rc != pcmk_rc_ok) { crm_info("Could not set pipe output non-blocking: %s " QB_XS " rc=%d", pcmk_rc_str(rc), rc); } // Set SIGCHLD handler data->sa.sa_handler = (sighandler_t) sigchld_handler; data->sa.sa_flags = 0; sigemptyset(&(data->sa.sa_mask)); if (sigaction(SIGCHLD, &(data->sa), &(data->old_sa)) < 0) { crm_info("Wait for child process completion failed: %s " QB_XS " source=sigaction", pcmk_rc_str(errno)); } data->ignored = false; // Remember data for use in signal handler last_sigchld_data = data; return true; } static int sigchld_open(struct sigchld_data_s *data) { CRM_CHECK(data != NULL, return -1); return data->pipe_fd[0]; } static void sigchld_close(int fd) { // Pipe will be closed in sigchld_cleanup() return; } static bool sigchld_received(int fd, int pid, struct sigchld_data_s *data) { char ch; if (fd < 0) { return false; } // Clear out the self-pipe while (read(fd, &ch, 1) == 1) /*omit*/; return true; } static void sigchld_cleanup(struct sigchld_data_s *data) { // Restore the previous SIGCHLD handler if (sigaction(SIGCHLD, &(data->old_sa), NULL) < 0) { crm_warn("Could not clean up after child process completion: %s", pcmk_rc_str(errno)); } close_pipe(data->pipe_fd); // Resend any ignored SIGCHLD for other children so that they'll be handled. if (data->ignored && kill(getpid(), SIGCHLD) != 0) { crm_warn("Could not resend ignored SIGCHLD to ourselves: %s", pcmk_rc_str(errno)); } } #endif /*! * \internal * \brief Close the two file descriptors of a pipe * * \param[in,out] fildes Array of file descriptors opened by pipe() */ static void close_pipe(int fildes[]) { if (fildes[0] >= 0) { close(fildes[0]); fildes[0] = -1; } if (fildes[1] >= 0) { close(fildes[1]); fildes[1] = -1; } } #define out_type(is_stderr) ((is_stderr)? "stderr" : "stdout") // Maximum number of bytes of stdout or stderr we'll accept #define MAX_OUTPUT (10 * 1024 * 1024) static gboolean svc_read_output(int fd, svc_action_t * op, bool is_stderr) { char *data = NULL; ssize_t rc = 0; size_t len = 0; size_t discarded = 0; char buf[500]; static const size_t buf_read_len = sizeof(buf) - 1; if (fd < 0) { crm_trace("No fd for %s", op->id); return FALSE; } if (is_stderr && op->stderr_data) { len = strlen(op->stderr_data); data = op->stderr_data; crm_trace("Reading %s stderr into offset %zu", op->id, len); } else if (is_stderr == FALSE && op->stdout_data) { len = strlen(op->stdout_data); data = op->stdout_data; crm_trace("Reading %s stdout into offset %zu", op->id, len); } else { crm_trace("Reading %s %s", op->id, out_type(is_stderr)); } do { errno = 0; rc = read(fd, buf, buf_read_len); if (rc > 0) { if (len < MAX_OUTPUT) { buf[rc] = 0; crm_trace("Received %zd bytes of %s %s: %.80s", rc, op->id, out_type(is_stderr), buf); data = pcmk__realloc(data, len + rc + 1); strcpy(data + len, buf); len += rc; } else { discarded += rc; } } else if (errno != EINTR) { // Fatal error or EOF rc = 0; break; } } while ((rc == buf_read_len) || (rc < 0)); if (discarded > 0) { crm_warn("Truncated %s %s to %zu bytes (discarded %zu)", op->id, out_type(is_stderr), len, discarded); } if (is_stderr) { op->stderr_data = data; } else { op->stdout_data = data; } return rc != 0; } static int dispatch_stdout(gpointer userdata) { svc_action_t *op = (svc_action_t *) userdata; return svc_read_output(op->opaque->stdout_fd, op, FALSE); } static int dispatch_stderr(gpointer userdata) { svc_action_t *op = (svc_action_t *) userdata; return svc_read_output(op->opaque->stderr_fd, op, TRUE); } static void pipe_out_done(gpointer user_data) { svc_action_t *op = (svc_action_t *) user_data; crm_trace("%p", op); op->opaque->stdout_gsource = NULL; if (op->opaque->stdout_fd > STDOUT_FILENO) { close(op->opaque->stdout_fd); } op->opaque->stdout_fd = -1; } static void pipe_err_done(gpointer user_data) { svc_action_t *op = (svc_action_t *) user_data; op->opaque->stderr_gsource = NULL; if (op->opaque->stderr_fd > STDERR_FILENO) { close(op->opaque->stderr_fd); } op->opaque->stderr_fd = -1; } static struct mainloop_fd_callbacks stdout_callbacks = { .dispatch = dispatch_stdout, .destroy = pipe_out_done, }; static struct mainloop_fd_callbacks stderr_callbacks = { .dispatch = dispatch_stderr, .destroy = pipe_err_done, }; static void set_ocf_env(const char *key, const char *value, gpointer user_data) { // @FIXME @COMPAT This seems like it should be a fatal error if (setenv(key, value, 1) != 0) { int rc = errno; crm_err("setenv failed for key='%s' and value='%s': %s", pcmk__s(key, ""), pcmk__s(value, ""), strerror(rc)); } } static void set_ocf_env_with_prefix(gpointer key, gpointer value, gpointer user_data) { const char *ckey = key; if (pcmk__str_eq(ckey, "OCF_CHECK_LEVEL", pcmk__str_none)) { set_ocf_env(ckey, value, user_data); } else { char *buffer = crm_strdup_printf("OCF_RESKEY_%s", ckey); set_ocf_env(buffer, value, user_data); free(buffer); } } static void set_alert_env(gpointer key, gpointer value, gpointer user_data) { int rc; if (value != NULL) { rc = setenv(key, value, 1); } else { rc = unsetenv(key); } if (rc < 0) { // @FIXME @COMPAT This seems like it should be a fatal error rc = errno; if (value != NULL) { crm_err("setenv %s='%s' failed: %s", (const char *) key, (const char *) value, strerror(rc)); } else { crm_err("unsetenv %s failed: %s", (const char *) key, strerror(rc)); } } else { if (value != NULL) { crm_trace("setenv %s='%s'", (const char *) key, (const char *) value); } else { crm_trace("unsetenv %s", (const char *) key); } } } /*! * \internal * \brief Add environment variables suitable for an action * * \param[in] op Action to use */ static void add_action_env_vars(const svc_action_t *op) { void (*env_setter)(gpointer, gpointer, gpointer) = NULL; if (op->agent == NULL) { env_setter = set_alert_env; /* we deal with alert handler */ } else if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { env_setter = set_ocf_env_with_prefix; } if (env_setter != NULL && op->params != NULL) { g_hash_table_foreach(op->params, env_setter, NULL); } if (env_setter == NULL || env_setter == set_alert_env) { return; } set_ocf_env("OCF_RA_VERSION_MAJOR", PCMK_OCF_MAJOR_VERSION, NULL); set_ocf_env("OCF_RA_VERSION_MINOR", PCMK_OCF_MINOR_VERSION, NULL); set_ocf_env("OCF_ROOT", PCMK_OCF_ROOT, NULL); set_ocf_env("OCF_EXIT_REASON_PREFIX", PCMK_OCF_REASON_PREFIX, NULL); if (op->rsc) { set_ocf_env("OCF_RESOURCE_INSTANCE", op->rsc, NULL); } if (op->agent != NULL) { set_ocf_env("OCF_RESOURCE_TYPE", op->agent, NULL); } /* Notes: this is not added to specification yet. Sept 10,2004 */ if (op->provider != NULL) { set_ocf_env("OCF_RESOURCE_PROVIDER", op->provider, NULL); } } static void pipe_in_single_parameter(gpointer key, gpointer value, gpointer user_data) { svc_action_t *op = user_data; char *buffer = crm_strdup_printf("%s=%s\n", (char *)key, (char *) value); size_t len = strlen(buffer); size_t total = 0; ssize_t ret = 0; do { errno = 0; ret = write(op->opaque->stdin_fd, buffer + total, len - total); if (ret > 0) { total += ret; } } while ((errno == EINTR) && (total < len)); free(buffer); } /*! * \internal * \brief Pipe parameters in via stdin for action * * \param[in] op Action to use */ static void pipe_in_action_stdin_parameters(const svc_action_t *op) { if (op->params) { g_hash_table_foreach(op->params, pipe_in_single_parameter, (gpointer) op); } } gboolean recurring_action_timer(gpointer data) { svc_action_t *op = data; crm_debug("Scheduling another invocation of %s", op->id); /* Clean out the old result */ free(op->stdout_data); op->stdout_data = NULL; free(op->stderr_data); op->stderr_data = NULL; op->opaque->repeat_timer = 0; services_action_async(op, NULL); return FALSE; } /*! * \internal * \brief Finalize handling of an asynchronous operation * * Given a completed asynchronous operation, cancel or reschedule it as * appropriate if recurring, call its callback if registered, stop tracking it, * and clean it up. * * \param[in,out] op Operation to finalize * * \return Standard Pacemaker return code * \retval EINVAL Caller supplied NULL or invalid \p op * \retval EBUSY Uncanceled recurring action has only been cleaned up * \retval pcmk_rc_ok Action has been freed * * \note If the return value is not pcmk_rc_ok, the caller is responsible for * freeing the action. */ int services__finalize_async_op(svc_action_t *op) { CRM_CHECK((op != NULL) && !(op->synchronous), return EINVAL); if (op->interval_ms != 0) { // Recurring operations must be either cancelled or rescheduled if (op->cancel) { services__set_cancelled(op); cancel_recurring_action(op); } else { op->opaque->repeat_timer = pcmk__create_timer(op->interval_ms, recurring_action_timer, op); } } if (op->opaque->callback != NULL) { op->opaque->callback(op); } // Stop tracking the operation (as in-flight or blocked) op->pid = 0; services_untrack_op(op); if ((op->interval_ms != 0) && !(op->cancel)) { // Do not free recurring actions (they will get freed when cancelled) services_action_cleanup(op); return EBUSY; } services_action_free(op); return pcmk_rc_ok; } static void close_op_input(svc_action_t *op) { if (op->opaque->stdin_fd >= 0) { close(op->opaque->stdin_fd); } } static void finish_op_output(svc_action_t *op, bool is_stderr) { mainloop_io_t **source; int fd; if (is_stderr) { source = &(op->opaque->stderr_gsource); fd = op->opaque->stderr_fd; } else { source = &(op->opaque->stdout_gsource); fd = op->opaque->stdout_fd; } if (op->synchronous || *source) { crm_trace("Finish reading %s[%d] %s", op->id, op->pid, (is_stderr? "stderr" : "stdout")); svc_read_output(fd, op, is_stderr); if (op->synchronous) { close(fd); } else { mainloop_del_fd(*source); *source = NULL; } } } // Log an operation's stdout and stderr static void log_op_output(svc_action_t *op) { char *prefix = crm_strdup_printf("%s[%d] error output", op->id, op->pid); /* The library caller has better context to know how important the output * is, so log it at info and debug severity here. They can log it again at * higher severity if appropriate. */ crm_log_output(LOG_INFO, prefix, op->stderr_data); strcpy(prefix + strlen(prefix) - strlen("error output"), "output"); crm_log_output(LOG_DEBUG, prefix, op->stdout_data); free(prefix); } // Truncate exit reasons at this many characters #define EXIT_REASON_MAX_LEN 128 static void parse_exit_reason_from_stderr(svc_action_t *op) { const char *reason_start = NULL; const char *reason_end = NULL; const int prefix_len = strlen(PCMK_OCF_REASON_PREFIX); if ((op->stderr_data == NULL) || // Only OCF agents have exit reasons in stderr !pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_none)) { return; } // Find the last occurrence of the magic string indicating an exit reason for (const char *cur = strstr(op->stderr_data, PCMK_OCF_REASON_PREFIX); cur != NULL; cur = strstr(cur, PCMK_OCF_REASON_PREFIX)) { cur += prefix_len; // Skip over magic string reason_start = cur; } if ((reason_start == NULL) || (reason_start[0] == '\n') || (reason_start[0] == '\0')) { return; // No or empty exit reason } // Exit reason goes to end of line (or end of output) reason_end = strchr(reason_start, '\n'); if (reason_end == NULL) { reason_end = reason_start + strlen(reason_start); } // Limit size of exit reason to something reasonable if (reason_end > (reason_start + EXIT_REASON_MAX_LEN)) { reason_end = reason_start + EXIT_REASON_MAX_LEN; } free(op->opaque->exit_reason); op->opaque->exit_reason = strndup(reason_start, reason_end - reason_start); } /*! * \internal * \brief Process the completion of an asynchronous child process * * \param[in,out] p Child process that completed * \param[in] pid Process ID of child * \param[in] core (Unused) * \param[in] signo Signal that interrupted child, if any * \param[in] exitcode Exit status of child process */ static void async_action_complete(mainloop_child_t *p, pid_t pid, int core, int signo, int exitcode) { svc_action_t *op = mainloop_child_userdata(p); mainloop_clear_child_userdata(p); CRM_CHECK(op->pid == pid, services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, "Bug in mainloop handling"); return); /* Depending on the priority the mainloop gives the stdout and stderr * file descriptors, this function could be called before everything has * been read from them, so force a final read now. */ finish_op_output(op, true); finish_op_output(op, false); close_op_input(op); if (signo == 0) { crm_debug("%s[%d] exited with status %d", op->id, op->pid, exitcode); services__set_result(op, exitcode, PCMK_EXEC_DONE, NULL); log_op_output(op); parse_exit_reason_from_stderr(op); } else if (mainloop_child_timeout(p)) { const char *kind = services__action_kind(op); crm_info("%s %s[%d] timed out after %s", kind, op->id, op->pid, pcmk__readable_interval(op->timeout)); services__format_result(op, services__generic_error(op), PCMK_EXEC_TIMEOUT, "%s did not complete within %s", kind, pcmk__readable_interval(op->timeout)); } else if (op->cancel) { /* If an in-flight recurring operation was killed because it was * cancelled, don't treat that as a failure. */ crm_info("%s[%d] terminated with signal %d (%s)", op->id, op->pid, signo, strsignal(signo)); services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_CANCELLED, NULL); } else { crm_info("%s[%d] terminated with signal %d (%s)", op->id, op->pid, signo, strsignal(signo)); services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, "%s interrupted by %s signal", services__action_kind(op), strsignal(signo)); } services__finalize_async_op(op); } /*! * \internal * \brief Return agent standard's exit status for "generic error" * * When returning an internal error for an action, a value that is appropriate * to the action's agent standard must be used. This function returns a value * appropriate for errors in general. * * \param[in] op Action that error is for * * \return Exit status appropriate to agent standard * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. */ int services__generic_error(const svc_action_t *op) { if ((op == NULL) || (op->standard == NULL)) { return PCMK_OCF_UNKNOWN_ERROR; } #if PCMK__ENABLE_LSB if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) && pcmk__str_eq(op->action, PCMK_ACTION_STATUS, pcmk__str_casei)) { return PCMK_LSB_STATUS_UNKNOWN; } #endif return PCMK_OCF_UNKNOWN_ERROR; } /*! * \internal * \brief Return agent standard's exit status for "not installed" * * When returning an internal error for an action, a value that is appropriate * to the action's agent standard must be used. This function returns a value * appropriate for "not installed" errors. * * \param[in] op Action that error is for * * \return Exit status appropriate to agent standard * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. */ int services__not_installed_error(const svc_action_t *op) { if ((op == NULL) || (op->standard == NULL)) { return PCMK_OCF_UNKNOWN_ERROR; } #if PCMK__ENABLE_LSB if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) && pcmk__str_eq(op->action, PCMK_ACTION_STATUS, pcmk__str_casei)) { return PCMK_LSB_STATUS_NOT_INSTALLED; } #endif return PCMK_OCF_NOT_INSTALLED; } /*! * \internal * \brief Return agent standard's exit status for "insufficient privileges" * * When returning an internal error for an action, a value that is appropriate * to the action's agent standard must be used. This function returns a value * appropriate for "insufficient privileges" errors. * * \param[in] op Action that error is for * * \return Exit status appropriate to agent standard * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. */ int services__authorization_error(const svc_action_t *op) { if ((op == NULL) || (op->standard == NULL)) { return PCMK_OCF_UNKNOWN_ERROR; } #if PCMK__ENABLE_LSB if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) && pcmk__str_eq(op->action, PCMK_ACTION_STATUS, pcmk__str_casei)) { return PCMK_LSB_STATUS_INSUFFICIENT_PRIV; } #endif return PCMK_OCF_INSUFFICIENT_PRIV; } /*! * \internal * \brief Return agent standard's exit status for "not configured" * * When returning an internal error for an action, a value that is appropriate * to the action's agent standard must be used. This function returns a value * appropriate for "not configured" errors. * * \param[in] op Action that error is for * \param[in] is_fatal Whether problem is cluster-wide instead of only local * * \return Exit status appropriate to agent standard * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. */ int services__configuration_error(const svc_action_t *op, bool is_fatal) { if ((op == NULL) || (op->standard == NULL)) { return PCMK_OCF_UNKNOWN_ERROR; } #if PCMK__ENABLE_LSB if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) && pcmk__str_eq(op->action, PCMK_ACTION_STATUS, pcmk__str_casei)) { return PCMK_LSB_NOT_CONFIGURED; } #endif return is_fatal? PCMK_OCF_NOT_CONFIGURED : PCMK_OCF_INVALID_PARAM; } /*! * \internal * \brief Set operation rc and status per errno from stat(), fork() or execvp() * * \param[in,out] op Operation to set rc and status for * \param[in] error Value of errno after system call * * \return void */ void services__handle_exec_error(svc_action_t * op, int error) { const char *name = op->opaque->exec; if (name == NULL) { name = op->agent; if (name == NULL) { name = op->id; } } switch (error) { /* see execve(2), stat(2) and fork(2) */ case ENOENT: /* No such file or directory */ case EISDIR: /* Is a directory */ case ENOTDIR: /* Path component is not a directory */ case EINVAL: /* Invalid executable format */ case ENOEXEC: /* Invalid executable format */ services__format_result(op, services__not_installed_error(op), PCMK_EXEC_NOT_INSTALLED, "%s: %s", name, pcmk_rc_str(error)); break; case EACCES: /* permission denied (various errors) */ case EPERM: /* permission denied (various errors) */ services__format_result(op, services__authorization_error(op), PCMK_EXEC_ERROR, "%s: %s", name, pcmk_rc_str(error)); break; default: services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, pcmk_rc_str(error)); } } /*! * \internal * \brief Exit a child process that failed before executing agent * * \param[in] op Action that failed * \param[in] exit_status Exit status code to use * \param[in] exit_reason Exit reason to output if for OCF agent */ static void exit_child(const svc_action_t *op, int exit_status, const char *exit_reason) { if ((op != NULL) && (exit_reason != NULL) && pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_none)) { fprintf(stderr, PCMK_OCF_REASON_PREFIX "%s\n", exit_reason); } pcmk_common_cleanup(); _exit(exit_status); } static void action_launch_child(svc_action_t *op) { int rc; /* SIGPIPE is ignored (which is different from signal blocking) by the gnutls library. * Depending on the libqb version in use, libqb may set SIGPIPE to be ignored as well. * We do not want this to be inherited by the child process. By resetting this the signal * to the default behavior, we avoid some potential odd problems that occur during OCF * scripts when SIGPIPE is ignored by the environment. */ signal(SIGPIPE, SIG_DFL); if (sched_getscheduler(0) != SCHED_OTHER) { struct sched_param sp; memset(&sp, 0, sizeof(sp)); sp.sched_priority = 0; if (sched_setscheduler(0, SCHED_OTHER, &sp) == -1) { crm_info("Could not reset scheduling policy for %s", op->id); } } if (setpriority(PRIO_PROCESS, 0, 0) == -1) { crm_info("Could not reset process priority for %s", op->id); } /* Man: The call setpgrp() is equivalent to setpgid(0,0) * _and_ compiles on BSD variants too * need to investigate if it works the same too. */ setpgid(0, 0); - pcmk__close_fds_in_child(false); + pcmk__close_fds_in_child(); /* It would be nice if errors in this function could be reported as * execution status (for example, PCMK_EXEC_NO_SECRETS for the secrets error * below) instead of exit status. However, we've already forked, so * exit status is all we have. At least for OCF actions, we can output an * exit reason for the parent to parse. * * @TODO It might be better to substitute secrets in the parent before * forking, so that if it fails, we can give a better message and result, * and avoid the fork. */ #if PCMK__ENABLE_CIBSECRETS rc = pcmk__substitute_secrets(op->rsc, op->params); if (rc != pcmk_rc_ok) { if (pcmk__str_eq(op->action, PCMK_ACTION_STOP, pcmk__str_casei)) { crm_info("Proceeding with stop operation for %s " "despite being unable to load CIB secrets (%s)", op->rsc, pcmk_rc_str(rc)); } else { crm_err("Considering %s unconfigured " "because unable to load CIB secrets: %s", op->rsc, pcmk_rc_str(rc)); exit_child(op, services__configuration_error(op, false), "Unable to load CIB secrets"); } } #endif add_action_env_vars(op); /* Become the desired user */ if (op->opaque->uid && (geteuid() == 0)) { // If requested, set effective group if (op->opaque->gid && (setgid(op->opaque->gid) < 0)) { crm_err("Considering %s unauthorized because could not set " "child group to %d: %s", op->id, op->opaque->gid, strerror(errno)); exit_child(op, services__authorization_error(op), "Could not set group for child process"); } // Erase supplementary group list // (We could do initgroups() if we kept a copy of the username) if (setgroups(0, NULL) < 0) { crm_err("Considering %s unauthorized because could not " "clear supplementary groups: %s", op->id, strerror(errno)); exit_child(op, services__authorization_error(op), "Could not clear supplementary groups for child process"); } // Set effective user if (setuid(op->opaque->uid) < 0) { crm_err("Considering %s unauthorized because could not set user " "to %d: %s", op->id, op->opaque->uid, strerror(errno)); exit_child(op, services__authorization_error(op), "Could not set user for child process"); } } // Execute the agent (doesn't return if successful) execvp(op->opaque->exec, op->opaque->args); // An earlier stat() should have avoided most possible errors rc = errno; services__handle_exec_error(op, rc); crm_err("Unable to execute %s: %s", op->id, strerror(rc)); exit_child(op, op->rc, "Child process was unable to execute file"); } /*! * \internal * \brief Wait for synchronous action to complete, and set its result * * \param[in,out] op Action to wait for * \param[in,out] data Child signal data */ static void wait_for_sync_result(svc_action_t *op, struct sigchld_data_s *data) { int status = 0; int timeout = op->timeout; time_t start = time(NULL); struct pollfd fds[3]; int wait_rc = 0; const char *wait_reason = NULL; fds[0].fd = op->opaque->stdout_fd; fds[0].events = POLLIN; fds[0].revents = 0; fds[1].fd = op->opaque->stderr_fd; fds[1].events = POLLIN; fds[1].revents = 0; fds[2].fd = sigchld_open(data); fds[2].events = POLLIN; fds[2].revents = 0; crm_trace("Waiting for %s[%d]", op->id, op->pid); do { int poll_rc = poll(fds, 3, timeout); wait_reason = NULL; if (poll_rc > 0) { if (fds[0].revents & POLLIN) { svc_read_output(op->opaque->stdout_fd, op, FALSE); } if (fds[1].revents & POLLIN) { svc_read_output(op->opaque->stderr_fd, op, TRUE); } if ((fds[2].revents & POLLIN) && sigchld_received(fds[2].fd, op->pid, data)) { wait_rc = waitpid(op->pid, &status, WNOHANG); if ((wait_rc > 0) || ((wait_rc < 0) && (errno == ECHILD))) { // Child process exited or doesn't exist break; } else if (wait_rc < 0) { wait_reason = pcmk_rc_str(errno); crm_info("Wait for completion of %s[%d] failed: %s " QB_XS " source=waitpid", op->id, op->pid, wait_reason); wait_rc = 0; // Act as if process is still running #ifndef HAVE_SYS_SIGNALFD_H } else { /* The child hasn't exited, so this SIGCHLD could be for * another child. We have to ignore it here but will still * need to resend it after this synchronous action has * completed and SIGCHLD has been restored to be handled by * the previous handler, so that it will be handled. */ data->ignored = true; #endif } } } else if (poll_rc == 0) { // Poll timed out with no descriptors ready timeout = 0; break; } else if ((poll_rc < 0) && (errno != EINTR)) { wait_reason = pcmk_rc_str(errno); crm_info("Wait for completion of %s[%d] failed: %s " QB_XS " source=poll", op->id, op->pid, wait_reason); break; } timeout = op->timeout - (time(NULL) - start) * 1000; } while ((op->timeout < 0 || timeout > 0)); crm_trace("Stopped waiting for %s[%d]", op->id, op->pid); finish_op_output(op, true); finish_op_output(op, false); close_op_input(op); sigchld_close(fds[2].fd); if (wait_rc <= 0) { if ((op->timeout > 0) && (timeout <= 0)) { services__format_result(op, services__generic_error(op), PCMK_EXEC_TIMEOUT, "%s did not exit within specified timeout", services__action_kind(op)); crm_info("%s[%d] timed out after %dms", op->id, op->pid, op->timeout); } else { services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, wait_reason); } /* If only child hasn't been successfully waited for, yet. This is to limit killing wrong target a bit more. */ if ((wait_rc == 0) && (waitpid(op->pid, &status, WNOHANG) == 0)) { if (kill(op->pid, SIGKILL)) { crm_warn("Could not kill rogue child %s[%d]: %s", op->id, op->pid, pcmk_rc_str(errno)); } /* Safe to skip WNOHANG here as we sent non-ignorable signal. */ while ((waitpid(op->pid, &status, 0) == (pid_t) -1) && (errno == EINTR)) { /* keep waiting */; } } } else if (WIFEXITED(status)) { services__set_result(op, WEXITSTATUS(status), PCMK_EXEC_DONE, NULL); parse_exit_reason_from_stderr(op); crm_info("%s[%d] exited with status %d", op->id, op->pid, op->rc); } else if (WIFSIGNALED(status)) { int signo = WTERMSIG(status); services__format_result(op, services__generic_error(op), PCMK_EXEC_ERROR, "%s interrupted by %s signal", services__action_kind(op), strsignal(signo)); crm_info("%s[%d] terminated with signal %d (%s)", op->id, op->pid, signo, strsignal(signo)); #ifdef WCOREDUMP if (WCOREDUMP(status)) { crm_warn("%s[%d] dumped core", op->id, op->pid); } #endif } else { // Shouldn't be possible to get here services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, "Unable to wait for child to complete"); } } /*! * \internal * \brief Execute an action whose standard uses executable files * * \param[in,out] op Action to execute * * \return Standard Pacemaker return value * \retval EBUSY Recurring operation could not be initiated * \retval pcmk_rc_error Synchronous action failed * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action * should not be freed (because it's pending or because * it failed to execute and was already freed) * * \note If the return value for an asynchronous action is not pcmk_rc_ok, the * caller is responsible for freeing the action. */ int services__execute_file(svc_action_t *op) { int stdout_fd[2]; int stderr_fd[2]; int stdin_fd[2] = {-1, -1}; int rc; struct stat st; struct sigchld_data_s data = { .ignored = false }; // Catch common failure conditions early if (stat(op->opaque->exec, &st) != 0) { rc = errno; crm_info("Cannot execute '%s': %s " QB_XS " stat rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); services__handle_exec_error(op, rc); goto done; } if (pipe(stdout_fd) < 0) { rc = errno; crm_info("Cannot execute '%s': %s " QB_XS " pipe(stdout) rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); services__handle_exec_error(op, rc); goto done; } if (pipe(stderr_fd) < 0) { rc = errno; close_pipe(stdout_fd); crm_info("Cannot execute '%s': %s " QB_XS " pipe(stderr) rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); services__handle_exec_error(op, rc); goto done; } if (pcmk_is_set(pcmk_get_ra_caps(op->standard), pcmk_ra_cap_stdin)) { if (pipe(stdin_fd) < 0) { rc = errno; close_pipe(stdout_fd); close_pipe(stderr_fd); crm_info("Cannot execute '%s': %s " QB_XS " pipe(stdin) rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); services__handle_exec_error(op, rc); goto done; } } if (op->synchronous && !sigchld_setup(&data)) { close_pipe(stdin_fd); close_pipe(stdout_fd); close_pipe(stderr_fd); sigchld_cleanup(&data); services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, "Could not manage signals for child process"); goto done; } op->pid = fork(); switch (op->pid) { case -1: rc = errno; close_pipe(stdin_fd); close_pipe(stdout_fd); close_pipe(stderr_fd); crm_info("Cannot execute '%s': %s " QB_XS " fork rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); services__handle_exec_error(op, rc); if (op->synchronous) { sigchld_cleanup(&data); } goto done; break; case 0: /* Child */ close(stdout_fd[0]); close(stderr_fd[0]); if (stdin_fd[1] >= 0) { close(stdin_fd[1]); } if (STDOUT_FILENO != stdout_fd[1]) { if (dup2(stdout_fd[1], STDOUT_FILENO) != STDOUT_FILENO) { crm_warn("Can't redirect output from '%s': %s " QB_XS " errno=%d", op->opaque->exec, pcmk_rc_str(errno), errno); } close(stdout_fd[1]); } if (STDERR_FILENO != stderr_fd[1]) { if (dup2(stderr_fd[1], STDERR_FILENO) != STDERR_FILENO) { crm_warn("Can't redirect error output from '%s': %s " QB_XS " errno=%d", op->opaque->exec, pcmk_rc_str(errno), errno); } close(stderr_fd[1]); } if ((stdin_fd[0] >= 0) && (STDIN_FILENO != stdin_fd[0])) { if (dup2(stdin_fd[0], STDIN_FILENO) != STDIN_FILENO) { crm_warn("Can't redirect input to '%s': %s " QB_XS " errno=%d", op->opaque->exec, pcmk_rc_str(errno), errno); } close(stdin_fd[0]); } if (op->synchronous) { sigchld_cleanup(&data); } action_launch_child(op); pcmk__assert(false); // action_launch_child() should not return } /* Only the parent reaches here */ close(stdout_fd[1]); close(stderr_fd[1]); if (stdin_fd[0] >= 0) { close(stdin_fd[0]); } op->opaque->stdout_fd = stdout_fd[0]; rc = pcmk__set_nonblocking(op->opaque->stdout_fd); if (rc != pcmk_rc_ok) { crm_info("Could not set '%s' output non-blocking: %s " QB_XS " rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); } op->opaque->stderr_fd = stderr_fd[0]; rc = pcmk__set_nonblocking(op->opaque->stderr_fd); if (rc != pcmk_rc_ok) { crm_info("Could not set '%s' error output non-blocking: %s " QB_XS " rc=%d", op->opaque->exec, pcmk_rc_str(rc), rc); } op->opaque->stdin_fd = stdin_fd[1]; if (op->opaque->stdin_fd >= 0) { // using buffer behind non-blocking-fd here - that could be improved // as long as no other standard uses stdin_fd assume stonith rc = pcmk__set_nonblocking(op->opaque->stdin_fd); if (rc != pcmk_rc_ok) { crm_info("Could not set '%s' input non-blocking: %s " QB_XS " fd=%d,rc=%d", op->opaque->exec, pcmk_rc_str(rc), op->opaque->stdin_fd, rc); } pipe_in_action_stdin_parameters(op); // as long as we are handling parameters directly in here just close close(op->opaque->stdin_fd); op->opaque->stdin_fd = -1; } // after fds are setup properly and before we plug anything into mainloop if (op->opaque->fork_callback) { op->opaque->fork_callback(op); } if (op->synchronous) { wait_for_sync_result(op, &data); sigchld_cleanup(&data); goto done; } crm_trace("Waiting async for '%s'[%d]", op->opaque->exec, op->pid); mainloop_child_add_with_flags(op->pid, op->timeout, op->id, op, pcmk_is_set(op->flags, SVC_ACTION_LEAVE_GROUP)? mainloop_leave_pid_group : 0, async_action_complete); op->opaque->stdout_gsource = mainloop_add_fd(op->id, G_PRIORITY_LOW, op->opaque->stdout_fd, op, &stdout_callbacks); op->opaque->stderr_gsource = mainloop_add_fd(op->id, G_PRIORITY_LOW, op->opaque->stderr_fd, op, &stderr_callbacks); services_add_inflight_op(op); return pcmk_rc_ok; done: if (op->synchronous) { return (op->rc == PCMK_OCF_OK)? pcmk_rc_ok : pcmk_rc_error; } else { return services__finalize_async_op(op); } } GList * services_os_get_single_directory_list(const char *root, gboolean files, gboolean executable) { GList *list = NULL; struct dirent **namelist = NULL; int entries = 0, lpc = 0; entries = scandir(root, &namelist, NULL, alphasort); if (entries <= 0) { return list; } for (lpc = 0; lpc < entries; lpc++) { char *buffer = NULL; struct stat sb; int rc = 0; if ('.' == namelist[lpc]->d_name[0]) { free(namelist[lpc]); continue; } buffer = crm_strdup_printf("%s/%s", root, namelist[lpc]->d_name); rc = stat(buffer, &sb); free(buffer); if (rc != 0) { continue; } if (S_ISDIR(sb.st_mode)) { if (files) { free(namelist[lpc]); continue; } } else if (S_ISREG(sb.st_mode)) { if (files == FALSE) { free(namelist[lpc]); continue; } else if (executable && (sb.st_mode & S_IXUSR) == 0 && (sb.st_mode & S_IXGRP) == 0 && (sb.st_mode & S_IXOTH) == 0) { free(namelist[lpc]); continue; } } list = g_list_append(list, strdup(namelist[lpc]->d_name)); free(namelist[lpc]); } free(namelist); return list; } GList * services_os_get_directory_list(const char *root, gboolean files, gboolean executable) { GList *result = NULL; char *dirs = strdup(root); char *dir = NULL; if (pcmk__str_empty(dirs)) { free(dirs); return result; } for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { GList *tmp = services_os_get_single_directory_list(dir, files, executable); if (tmp) { result = g_list_concat(result, tmp); } } free(dirs); return result; }