/* Finit state machine * * Copyright (c) 2016 Jonas Johansson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "config.h" /* Generated by configure script */ #include #include #include #include #include "finit.h" #include "cgroup.h" #include "cond.h" #include "conf.h" #include "devmon.h" #include "log.h" #include "helpers.h" #include "private.h" #include "schedule.h" #include "service.h" #include "sig.h" #include "tty.h" #include "sm.h" #include "utmp-api.h" typedef enum { SM_BOOTSTRAP_STATE = 0, /* Init state, bootstrap services */ SM_BOOTSTRAP_WAIT_STATE, /* Waiting for bootstrap to complete */ SM_RUNNING_STATE, /* Normal state, services running */ SM_RUNLEVEL_CHANGE_STATE, /* A runlevel change has occurred */ SM_RUNLEVEL_WAIT_STATE, /* Waiting for all stopped processes to halt */ SM_RUNLEVEL_CLEAN_STATE, /* Wait for post:scripts and cleanup:scripts */ SM_RELOAD_CHANGE_STATE, /* A reload event has occurred */ SM_RELOAD_WAIT_STATE, /* Waiting for all stopped processes to halt */ SM_RELOAD_CLEAN_STATE, /* Wait for post:scripts and cleanup:scripts */ } sm_state_t; static struct { sm_state_t state; /* Running, Changed, Waiting, ... */ int newlevel; /* Set to new runlevel on change, -1 otherwise */ int reload; /* Set on reload event, else 0 */ int in_reload; /* Set when waiting for all processes to be halted */ int skip_bootstrap; /* Set to skip bootstrap waiting */ } sm; /* Shutdown trace: set SHUTDOWN_DEBUG to 1 in sm.h to enable */ #define shutdbg(fmt, args...) do { \ if (SHUTDOWN_DEBUG && (runlevel == 0 || runlevel == 6)) \ cprintf("shutdown[%c]: " fmt "\n", \ sm_rl2ch(runlevel), ##args); \ } while (0) static uev_t console_watcher; static int console_fd = -1; /* * Shutdown watchdog: if graceful service stop takes too long, * force do_shutdown() directly from the event loop callback. * Default timeout is 30 seconds, adjustable at build time. */ #ifndef SHUTDOWN_WDT_TIMEOUT #define SHUTDOWN_WDT_TIMEOUT 30 #endif static uev_t shutdown_wdt; static void shutdown_wdt_cb(uev_t *w, void *arg, int events) { (void)w; (void)arg; (void)events; logit(LOG_CONSOLE | LOG_CRIT, "Timed out (%d sec) waiting for services to stop, forcing shutdown.", SHUTDOWN_WDT_TIMEOUT); do_shutdown(halt); } /* * Console input callback - handles Ctrl-C during bootstrap wait */ static void console_input_cb(uev_t *w, void *arg, int events) { ssize_t len; char ch; (void)arg; if (UEV_ERROR == events) { warnx("Error in console watcher"); return; } /* Read and check for Ctrl-C (ASCII 3) */ len = read(w->fd, &ch, 1); if (len == 1 && ch == 3) { /* Ctrl-C */ sm.skip_bootstrap = 1; print(2, "Bootstrap wait interrupted by Ctrl-C, resuming ..."); } } /* * Stop and cleanup the console watcher */ static void console_watcher_stop(void) { if (console_fd >= 0) { uev_io_stop(&console_watcher); close(console_fd); console_fd = -1; } } /* * Setup console watcher for Ctrl-C during bootstrap */ static void console_watcher_start(uev_ctx_t *ctx) { struct termios tio; /* Open console in non-blocking mode */ console_fd = open(console(), O_RDONLY | O_NONBLOCK); if (console_fd < 0) { dbg("Failed opening console for input monitoring: %s", strerror(errno)); return; } /* Get current terminal settings */ if (tcgetattr(console_fd, &tio) == 0) { /* Put terminal in raw mode to catch Ctrl-C */ tio.c_lflag &= ~(ICANON | ECHO | ISIG); tio.c_cc[VMIN] = 0; tio.c_cc[VTIME] = 0; tcsetattr(console_fd, TCSANOW, &tio); } /* Register I/O watcher */ if (uev_io_init(ctx, &console_watcher, console_input_cb, NULL, console_fd, UEV_READ)) { dbg("Failed setting up console watcher"); close(console_fd); console_fd = -1; } else { dbg("Console watcher active, press Ctrl-C to skip bootstrap wait"); } } /* * Wait for system bootstrap to complete, all SVC_TYPE_RUNTASK must be * allowed to complete their work in [S], or timeout, before we switch * to the configured runlevel and call finalize(), should not take more * than 120 sec. */ static void sm_check_bootstrap(void *work) { static int timeout = 120; int bootstrap_done; int level = cfglevel; svc_t *svc = NULL; dbg("Step all services ..."); service_step_all(SVC_TYPE_ANY); bootstrap_done = service_completed(&svc); if (timeout-- > 0 && !bootstrap_done && !sm.skip_bootstrap) { dbg("Not all bootstrap run/tasks have completed yet ... %d", timeout); schedule_work(work); return; } /* Stop console watcher before proceeding */ console_watcher_stop(); if (sm.skip_bootstrap) { logit(LOG_WARNING, "Ctrl-C received, skipped waiting for the following:"); service_log_incomplete(); } else if (timeout > 0) { dbg("All run/task have completed, resuming bootstrap."); } else { dbg("Timeout, resuming bootstrap."); if (svc) print(2, "Timeout waiting for %s to run, resuming bootstrap", svc_ident(svc, NULL, 0)); else print(2, "Timeout waiting for unknown run/task, resuming bootstrap"); } dbg("Flushing pending .conf file events ..."); conf_flush_events(); /* * Start all tasks/services in the configured runlevel, or jump * into the runlevel selected from the command line. */ if (cmdlevel) { dbg("Runlevel %d requested from command line, starting all services ...", cmdlevel); level = cmdlevel; } else dbg("Change to default runlevel(%d), starting all services ...", cfglevel); sm_runlevel(level); /* Clean up bootstrap-only tasks/services that never started */ dbg("Clean up all bootstrap-only tasks/services ..."); svc_prune_bootstrap(); /* Clean up empty cgroups from bootstrap tasks (e.g., udevadm, coldplug) */ dbg("Pruning empty cgroups from bootstrap ..."); cgroup_prune(); /* All services/tasks/etc. in configure runlevel have started */ dbg("Running svc up hooks ..."); plugin_run_hooks(HOOK_SVC_UP); } static char *sm_status(sm_state_t state) { switch (state) { case SM_BOOTSTRAP_STATE: return "bootstrap"; case SM_BOOTSTRAP_WAIT_STATE: return "bootstrap/wait"; case SM_RUNNING_STATE: return "running"; case SM_RUNLEVEL_CHANGE_STATE: return "runlevel/change"; case SM_RUNLEVEL_WAIT_STATE: return "runlevel/wait"; case SM_RUNLEVEL_CLEAN_STATE: return "runlevel/clean"; case SM_RELOAD_CHANGE_STATE: return "reload/change"; case SM_RELOAD_WAIT_STATE: return "reload/wait"; case SM_RELOAD_CLEAN_STATE: return "reload/clean"; } return "unknown"; } static char sm_rl2ch(int lvl) { if (lvl == INIT_LEVEL) return 'S'; return lvl + '0'; } /* * Disable login in single user mode and shutdown/reboot * * Re-enable only when going from these runlevels, this way a user can * manage FINIT_NOLOGIN_PATH manually within the other runlevels without * us pulling the rug from under their feet. */ static void nologin(void) { if (runlevel == 1 || IS_RESERVED_RUNLEVEL(runlevel)) touch(FINIT_NOLOGIN_PATH); if (prevlevel == 1 || IS_RESERVED_RUNLEVEL(prevlevel)) erase(FINIT_NOLOGIN_PATH); } void sm_init(void) { static struct wq work = { .cb = sm_check_bootstrap, .delay = 1000 }; sm.state = SM_BOOTSTRAP_STATE; sm.newlevel = -1; sm.reload = 0; sm.in_reload = 0; sm.skip_bootstrap = 0; dbg("Starting bootstrap finalize timer ..."); schedule_work(&work); } void sm_step(void) { sm_state_t old_state; svc_t *svc; restart: old_state = sm.state; dbg("state: %s, runlevel: %c, newlevel: %d, teardown: %d, reload: %d", sm_status(sm.state), sm_rl2ch(runlevel), sm.newlevel, sm.in_reload, sm.reload); switch (sm.state) { case SM_BOOTSTRAP_STATE: dbg("Bootstrapping all services in runlevel S from %s", finit_conf); service_step_all(SVC_TYPE_RUNTASK | SVC_TYPE_SERVICE | SVC_TYPE_SYSV); sm.state = SM_BOOTSTRAP_WAIT_STATE; console_watcher_start(ctx); break; /* * Handle bootstrap transition to configured runlevel, start TTYs * * This is the final stage of bootstrap. It changes to the default * (configured) runlevel, calls all external start scripts and final * bootstrap hooks before bringing up TTYs. * * We must ensure that all declared `task [S]` and `run [S]` jobs in * finit.conf, or *.conf in finit.d/, run to completion before we * finalize the bootstrap process by calling this function. */ case SM_BOOTSTRAP_WAIT_STATE: /* We come here from bootstrap, runlevel change and conf reload */ service_step_all(SVC_TYPE_ANY); /* Allow runparts to start */ cond_set_oneshot("int/bootstrap"); if (sm.newlevel == -1) break; /* Ensure console watcher is stopped before proceeding */ console_watcher_stop(); /* Hooks that should run at the very end */ dbg("Calling all system up hooks ..."); plugin_run_hooks(HOOK_SYSTEM_UP); service_step_all(SVC_TYPE_ANY); /* Disable progress output at normal runtime */ enable_progress(0); /* System bootstrapped, launch TTYs et al */ bootstrap = 0; service_step_all(SVC_TYPE_RESPAWN); sm.state = SM_RUNNING_STATE; break; case SM_RUNNING_STATE: /* We come here from bootstrap, runlevel change and conf reload */ service_step_all(SVC_TYPE_ANY); /* runlevel changed? */ if (sm.newlevel >= 0 && sm.newlevel <= 9) { if (runlevel == sm.newlevel) { sm.newlevel = -1; break; } sm.state = SM_RUNLEVEL_CHANGE_STATE; break; } /* reload ? */ if (sm.reload) { shutdbg("RUNNING got reload request!"); sm.reload = 0; sm.state = SM_RELOAD_CHANGE_STATE; } break; case SM_RUNLEVEL_CHANGE_STATE: prevlevel = runlevel; runlevel = sm.newlevel; sm.newlevel = -1; /* Restore terse mode and run hooks before shutdown */ if (runlevel == 0 || runlevel == 6) { shutdbg("entering CHANGE state, starting %d sec watchdog", SHUTDOWN_WDT_TIMEOUT); uev_timer_init(ctx, &shutdown_wdt, shutdown_wdt_cb, NULL, SHUTDOWN_WDT_TIMEOUT * 1000, 0); api_exit(); log_exit(); shutdbg("running HOOK_SHUTDOWN ..."); plugin_run_hooks(HOOK_SHUTDOWN); shutdbg("HOOK_SHUTDOWN done"); } dbg("Setting new runlevel --> %c <-- previous %c", sm_rl2ch(runlevel), sm_rl2ch(prevlevel)); if (osheading) logit(LOG_CONSOLE | LOG_NOTICE, "%s, entering runlevel %c", osheading, sm_rl2ch(runlevel)); else logit(LOG_CONSOLE | LOG_NOTICE, "Entering runlevel %c", sm_rl2ch(runlevel)); runlevel_set(prevlevel, runlevel); /* Disable login in single-user mode as well as shutdown/reboot */ nologin(); if (runlevel != 0 && runlevel != 6) { /* Make sure to (re)load all *.conf in /etc/finit.d/ */ if (conf_any_change()) conf_reload(); } /* Reset once flag of runtasks */ shutdbg("runtask_clean + step_all ..."); service_runtask_clean(); dbg("Stopping services not allowed in new runlevel ..."); sm.in_reload = 1; service_step_all(SVC_TYPE_ANY); shutdbg("CHANGE done, entering WAIT"); sm.state = SM_RUNLEVEL_WAIT_STATE; break; case SM_RUNLEVEL_WAIT_STATE: /* * Need to wait for any services to stop? If so, exit early * and perform second stage from service_monitor later. */ svc = svc_stop_completed(); if (svc) { shutdbg("WAIT, collecting %s[%d] ...", svc_ident(svc, NULL, 0), svc->pid); dbg("Waiting to collect %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid); break; } shutdbg("WAIT done, all services stopped"); /* Prev runlevel services stopped, call hooks before starting new runlevel ... */ dbg("All services have been stopped, calling runlevel change hooks ..."); plugin_run_hooks(HOOK_RUNLEVEL_CHANGE); /* Reconfigure HW/VLANs/etc here */ /* Clean up empty cgroups from previous runlevel */ dbg("Pruning empty cgroups from previous runlevel ..."); cgroup_prune(); dbg("Starting services new to this runlevel ..."); sm.in_reload = 0; service_step_all(SVC_TYPE_ANY); shutdbg("entering CLEAN"); sm.state = SM_RUNLEVEL_CLEAN_STATE; break; case SM_RUNLEVEL_CLEAN_STATE: /* * Wait for post:script or cleanup:script to be collected, * which moves the svc to HALTED or DEAD state. We will * be called by the service_monitor() on collect. */ svc = svc_clean_completed(); if (svc) { shutdbg("CLEAN, waiting for %s[%d] (%s) ...", svc_ident(svc, NULL, 0), svc->pid, svc_status(svc)); dbg("Waiting to collect post/cleanup script for %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid); break; } shutdbg("CLEAN done, all post/cleanup scripts collected"); /* Cleanup stale services */ svc_clean_dynamic(service_unregister); #ifdef FINIT_RC_LOCAL /* Compat SysV init */ if (prevlevel == INIT_LEVEL && !rescue) run_bg(FINIT_RC_LOCAL, NULL); #endif /* * "I've seen things you people wouldn't believe. Attack ships on fire off * the shoulder of Orion. I watched C-beams glitter in the dark near the * Tannhäuser Gate. All those .. moments .. will be lost in time, like * tears ... in ... rain." */ if (runlevel == 0 || runlevel == 6) { shutdbg("calling do_shutdown()"); uev_timer_stop(&shutdown_wdt); do_shutdown(halt); shutdbg("do_shutdown() returned to SM!"); } shutdbg("entering RUNNING (reload=%d, newlevel=%d)", sm.reload, sm.newlevel); sm.state = SM_RUNNING_STATE; break; case SM_RELOAD_CHANGE_STATE: sm.in_reload = 1; /* First reload all *.conf in /etc/finit.d/ */ conf_reload(); /* Handle remain tasks that need to run post script before restart */ service_runtask_clean(); /* * Then, mark all affected service conditions as in-flux and * let all affected services move to WAITING/HALTED */ dbg("Stopping services not allowed after reconf ..."); service_step_all(SVC_TYPE_ANY); /* Step the generation ... */ cond_reload(); sm.state = SM_RELOAD_WAIT_STATE; break; case SM_RELOAD_WAIT_STATE: /* * Need to wait for any services to stop? If so, exit early * and perform second stage from service_monitor later. */ svc = svc_stop_completed(); if (svc) { dbg("Waiting to collect %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid); break; } sm.in_reload = 0; dbg("Starting services after reconf ..."); service_step_all(SVC_TYPE_ANY); sm.state = SM_RELOAD_CLEAN_STATE; break; case SM_RELOAD_CLEAN_STATE: /* * Wait for post:script or cleanup:script to be collected, * which moves the svc to HALTED or DEAD state. We will * be called by the service_monitor() on collect. */ svc = svc_clean_completed(); if (svc) { dbg("Waiting to collect post/cleanup script for %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid); break; } /* Cleanup stale services */ svc_clean_dynamic(service_unregister); dbg("Step all services waiting for reasserted conditions ..."); service_step_all(SVC_TYPE_ANY); dbg("Calling reconf hooks ..."); plugin_run_hooks(HOOK_SVC_RECONF); dbg("Step all services waiting for reasserted conditions ..."); service_step_all(SVC_TYPE_ANY); dbg("Update configuration generation of device conditions ..."); devmon_reconf(); dbg("Step all services waiting for reasserted conditions ..."); service_step_all(SVC_TYPE_ANY); dbg("Update configuration generation of unmodified non-native services ..."); service_notify_reconf(); dbg("Step all services waiting for reasserted conditions ..."); service_step_all(SVC_TYPE_ANY); dbg("Reconfiguration done"); sm.state = SM_RUNNING_STATE; break; } if (sm.state != old_state) goto restart; } /** * sm_in_reload - System currently in reload or runlevel change? * * Returns: * %TRUE(1) or %FALSE(0) */ int sm_in_reload(void) { return sm.in_reload; } /** * sm_reload - Called on SIGHUP, 'init q' or 'initctl reload' * * This function is called when Finit has received SIGHUP to reload * .conf files in /etc/finit.d. It is responsible for starting, * stopping and reloading (forwarding SIGHUP) to processes affected. */ void sm_reload(void) { sm.reload = 1; dbg("Flushing pending .conf file events ..."); conf_flush_events(); sm_step(); } /** * sm_runlevel - Change to a new runlevel * @newlevel: New runlevel to activate * * Stops all services not in @newlevel and starts, or lets continue to run, * those in @newlevel. Also updates @prevlevel and active @runlevel. */ void sm_runlevel(int newlevel) { if (!rescue && (runlevel == 1 || runlevel == INIT_LEVEL) && !IS_RESERVED_RUNLEVEL(newlevel)) networking(1); sm.newlevel = newlevel; dbg("Flushing pending .conf file events ..."); conf_flush_events(); sm_step(); if (!rescue && IS_RESERVED_RUNLEVEL(runlevel)) networking(0); } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */