diff --git a/configure.ac b/configure.ac index f4d6cc3b..e37fad69 100644 --- a/configure.ac +++ b/configure.ac @@ -116,6 +116,7 @@ AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon]) AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock]) AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial]) AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown]) +AC_PLUGIN([plymouth], [no], [Plymouth boot splash integration]) AC_PLUGIN([testserv], [no], [Test plugin to start test serv daemon]) # Check for extra arguments or packages diff --git a/plugins/Makefile.am b/plugins/Makefile.am index 746707b9..b082eded 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -29,6 +29,10 @@ if BUILD_NETLINK_PLUGIN libplug_la_SOURCES += netlink.c endif +if BUILD_PLYMOUTH_PLUGIN +libplug_la_SOURCES += plymouth.c +endif + if BUILD_RESOLVCONF_PLUGIN libplug_la_SOURCES += resolvconf.c endif @@ -76,6 +80,10 @@ if BUILD_NETLINK_PLUGIN pkglib_LTLIBRARIES += netlink.la endif +if BUILD_PLYMOUTH_PLUGIN +pkglib_LTLIBRARIES += plymouth.la +endif + if BUILD_RESOLVCONF_PLUGIN pkglib_LTLIBRARIES += resolvconf.la endif diff --git a/plugins/plymouth.c b/plugins/plymouth.c new file mode 100644 index 00000000..f987081f --- /dev/null +++ b/plugins/plymouth.c @@ -0,0 +1,293 @@ +/* Plymouth boot splash plugin for Finit + * + * Copyright (c) 2012-2026 Aaron Andersen + * + * 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. + */ + +/* + * This plugin integrates the Plymouth boot splash screen with Finit. + * It manages the plymouthd lifecycle across the full boot process: + * + * - HOOK_BANNER: Start plymouthd early, before console output. + * In initramfs, starts fresh. In stage 2, reuses + * the daemon carried over from initramfs if alive. + * - HOOK_ROOTFS_UP .. HOOK_SYSTEM_UP: Display status messages. + * - HOOK_SVC_UP: Tear down plymouth once boot is complete. + * - HOOK_SWITCH_ROOT: Notify plymouth of root filesystem change so + * the daemon survives the initramfs -> rootfs + * transition. + * - HOOK_SHUTDOWN: Restart plymouthd in shutdown mode for a + * splash during poweroff/reboot. + * + * The plugin is only activated when "splash" is present on the kernel + * command line. Plymouth requires devpts for VT takeover; fs_init() + * mounts it early enough for HOOK_BANNER. + * + * NOTE: The initramfs must include /etc/initrd-release. Plymouth + * checks for this file and, when present, prefixes its argv[0] with + * '@' so that the process is not killed during switch_root. Without + * it, plymouthd will not survive the initramfs-to-rootfs transition. + */ + +#include "config.h" +#include "finit.h" +#include "helpers.h" +#include "conf.h" +#include "pid.h" +#include "plugin.h" +#include "sig.h" +#include "util.h" +#include "log.h" + +#ifndef PLYMOUTH_PATH +#define PLYMOUTH_PATH "/sbin/plymouth" +#endif +#ifndef PLYMOUTHD_PATH +#define PLYMOUTHD_PATH "/sbin/plymouthd" +#endif + +#define PLYMOUTH_PIDFILE "/run/plymouthd.pid" + +static pid_t daemon_pid; +static int switching_root; +static int in_initramfs; + +static int plymouth_cmd(const char *action) +{ + char cmd[256]; + + snprintf(cmd, sizeof(cmd), PLYMOUTH_PATH " %s", action); + return run(cmd, NULL); +} + +static void plymouth_message(const char *msg) +{ + pid_t pid; + + pid = fork(); + if (pid == 0) { + sig_unblock(); + execl(PLYMOUTH_PATH, PLYMOUTH_PATH, + "display-message", "--text", msg, NULL); + _exit(EX_OSERR); + } +} + +static int plymouth_alive(void) +{ + return daemon_pid > 0 && pid_alive(daemon_pid); +} + +/* Start plymouthd in the given mode ("boot" or "shutdown"). */ +static void plymouth_start(const char *mode) +{ + char cmd[256]; + int rc; + + if (plymouth_alive()) + return; + + snprintf(cmd, sizeof(cmd), + PLYMOUTHD_PATH " --attach-to-session --mode %s --pid-file %s", + mode, PLYMOUTH_PIDFILE); + rc = run(cmd, NULL); + if (rc) { + warnx("plymouthd failed to start (exit %d)", rc); + return; + } + + daemon_pid = pid_file_read(PLYMOUTH_PIDFILE); + if (daemon_pid <= 0) { + warnx("plymouthd started but no PID in %s", PLYMOUTH_PIDFILE); + return; + } + + rc = plymouth_cmd("show-splash"); + if (rc) + warnx("plymouth show-splash failed (exit %d)", rc); +} + +static void plymouth_stop(void) +{ + if (!plymouth_alive()) + return; + + plymouth_cmd("quit"); + + /* + * Don't poll -- we're in a finit hook, so finit's event loop + * is blocked and can't reap children. Trust that plymouthd + * exits after receiving the quit command. + */ + daemon_pid = 0; + unlink(PLYMOUTH_PIDFILE); +} + +/* + * HOOK_BANNER - earliest possible hook, before any console output. + * + * In initramfs: start plymouthd fresh. + * In stage 2: reuse plymouthd from initramfs if still alive, + * otherwise start a new instance. + */ +static void plymouth_boot(void *arg) +{ + in_initramfs = fexist("/etc/initrd-release"); + + if (rescue) + return; + + enable_progress(0); + + if (!in_initramfs) { + if (plymouth_cmd("--ping") == 0) { + daemon_pid = pid_file_read(PLYMOUTH_PIDFILE); + if (daemon_pid <= 0) + daemon_pid = 1; /* alive but unknown pid */ + return; + } + } + + plymouth_start("boot"); +} + +/* + * HOOK_SVC_UP - all services launched. + * + * In initramfs: keep splash alive for switch_root. + * In stage 2: boot is done, tear down plymouth. + */ +static void plymouth_boot_done(void *arg) +{ + if (in_initramfs) + return; + + plymouth_stop(); + enable_progress(1); +} + +/* HOOK_SWITCH_ROOT - initramfs transitioning to real root. */ +static void plymouth_switchroot(void *arg) +{ + switching_root = 1; + + plymouth_message("Switching to root filesystem..."); + + if (plymouth_alive()) + run(PLYMOUTH_PATH " update-root-fs --new-root-dir=/sysroot", NULL); + + enable_progress(1); +} + +/* HOOK_SHUTDOWN - entering runlevel 0 or 6. */ +static void plymouth_shutdown(void *arg) +{ + if (rescue || switching_root) + return; + + enable_progress(0); + plymouth_start("shutdown"); +} + +static void on_rootfs_up(void *arg) +{ + plymouth_message("Root filesystem mounted"); +} + +static void on_mount_post(void *arg) +{ + plymouth_message("Mounting filesystems..."); +} + +static void on_basefs_up(void *arg) +{ + plymouth_message("All filesystems mounted"); +} + +static void on_network_up(void *arg) +{ + plymouth_message("Network is up"); +} + +static void on_system_up(void *arg) +{ + plymouth_message("System ready"); +} + +static plugin_t plugin = { + .name = "plymouth", + .hook[HOOK_BANNER] = { .cb = plymouth_boot }, + .hook[HOOK_ROOTFS_UP] = { .cb = on_rootfs_up }, + .hook[HOOK_MOUNT_POST] = { .cb = on_mount_post }, + .hook[HOOK_BASEFS_UP] = { .cb = on_basefs_up }, + .hook[HOOK_NETWORK_UP] = { .cb = on_network_up }, + .hook[HOOK_SYSTEM_UP] = { .cb = on_system_up }, + .hook[HOOK_SVC_UP] = { .cb = plymouth_boot_done }, + .hook[HOOK_SWITCH_ROOT] = { .cb = plymouth_switchroot }, + .hook[HOOK_SHUTDOWN] = { .cb = plymouth_shutdown }, +}; + +/* + * Check kernel command line for "splash" argument. Plymouth should + * only be activated when the user explicitly requests it. + */ +static int has_splash_arg(void) +{ + char line[LINE_SIZE], *tok, *saveptr; + FILE *fp; + + fp = fopen("/proc/cmdline", "r"); + if (!fp) + return 0; + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return 0; + } + fclose(fp); + + for (tok = strtok_r(line, " \t\n", &saveptr); tok; + tok = strtok_r(NULL, " \t\n", &saveptr)) { + if (!strcmp(tok, "splash")) + return 1; + } + + return 0; +} + +PLUGIN_INIT(__init) +{ + if (!has_splash_arg()) + return; + + plugin_register(&plugin); +} + +PLUGIN_EXIT(__exit) +{ + plugin_unregister(&plugin); +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/finit.c b/src/finit.c index 9dcb17db..63df48a1 100644 --- a/src/finit.c +++ b/src/finit.c @@ -370,8 +370,8 @@ static void fs_finalize(void) fs_mount("shm", "/dev/shm", "tmpfs", flags | MS_NODEV, "mode=1777"); } - /* Modern systems use /dev/pts */ - if (!fismnt("/dev/pts")) { + /* Remount devpts with proper gid/mode now that /etc/group is available */ + { char opts[32]; int gid; @@ -383,7 +383,8 @@ static void fs_finalize(void) snprintf(opts, sizeof(opts), "gid=%d,mode=0620,ptmxmode=0666", gid); makedir("/dev/pts", 0755); - fs_mount("devpts", "/dev/pts", "devpts", flags, opts); + fs_mount("devpts", "/dev/pts", "devpts", + flags | MS_REMOUNT, opts); } /* Needed on systems like Alpine Linux */ @@ -529,6 +530,13 @@ static void fs_init(void) fs_mount(fs[i].spec, fs[i].file, fs[i].type, 0, NULL); } + + /* Early devpts mount for plugins that need PTY access (e.g. plymouth) */ + if (!fismnt("/dev/pts")) { + makedir("/dev/pts", 0755); + fs_mount("devpts", "/dev/pts", "devpts", + MS_NOSUID | MS_NOEXEC, "ptmxmode=0666"); + } }