mirror of
https://github.com/troglobit/finit.git
synced 2026-10-01 05:22:48 +07:00
Refactor print() to emit description + final status in a single call to cprintf(), preventing kernel messages from splitting the two parts. For two-phase print(-1,...) + print_result() sequences used by, e.g., run_interactive, save the last description and re-print it before the [ OK ] / [FAIL] output so the status is never left stranded on a blank line when command output or kernel messages have scrolled away the original description. Finally, add print_exit() which drains the console output buffer with tcdrain(2) and resets ANSI SGR attributes + cursor visibility before the kernel takes back the console on reboot/halt, preventing escape code leakage into bootloader or early-kernel output. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
616 lines
14 KiB
C
616 lines
14 KiB
C
/* Misc. utility functions for finit and its plugins
|
|
*
|
|
* Copyright (c) 2008-2010 Claudio Matsuoka <cmatsuoka@gmail.com>
|
|
* Copyright (c) 2008-2025 Joachim Wiberg <troglobit@gmail.com>
|
|
*
|
|
* 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 <ctype.h> /* isblank() */
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <net/if.h>
|
|
#include <netinet/in.h>
|
|
#include <stdarg.h>
|
|
#include <sysexits.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/prctl.h>
|
|
#ifdef _LIBITE_LITE
|
|
# include <libite/lite.h>
|
|
#else
|
|
# include <lite/lite.h>
|
|
#endif
|
|
|
|
#include "finit.h"
|
|
#include "helpers.h"
|
|
#include "conf.h"
|
|
#include "log.h"
|
|
#include "private.h"
|
|
#include "sig.h"
|
|
#include "util.h"
|
|
#include "utmp-api.h"
|
|
|
|
static pstyle_t progress_onoff = PROGRESS_DEFAULT;
|
|
static pstyle_t progress_style = PROGRESS_DEFAULT;
|
|
static char last_desc[512]; /* saved description for re-print after interruption */
|
|
|
|
#ifndef HOSTNAME_PATH
|
|
#define HOSTNAME_PATH "/etc/hostname"
|
|
#endif
|
|
|
|
/*
|
|
* Note: the pending status (⋯) must be last item. Also, ⋯ is not
|
|
* off-by-one, it's a multi-byte character.
|
|
*/
|
|
#define STATUS_CLASS { \
|
|
CHOOSE(" OK ", " OK ", "\e[1;32m"), /* Green */ \
|
|
CHOOSE("FAIL", "FAIL", "\e[1;31m"), /* Red */ \
|
|
CHOOSE("WARN", "WARN", "\e[1;33m"), /* Yellow */ \
|
|
CHOOSE(" \\/ ", " ⋯ ", "\e[1;33m"), /* Yellow */ \
|
|
}
|
|
|
|
#define CHOOSE(x,y,z) x
|
|
static const char *status1[] = STATUS_CLASS;
|
|
#undef CHOOSE
|
|
|
|
#define CHOOSE(x,y,z) y
|
|
static const char *status2[] = STATUS_CLASS;
|
|
#undef CHOOSE
|
|
|
|
#define CHOOSE(x,y,z) z
|
|
static const char *color[] = STATUS_CLASS;
|
|
|
|
char *console(void)
|
|
{
|
|
return _PATH_CONSOLE;
|
|
}
|
|
|
|
void console_init(void)
|
|
{
|
|
/* Enable line wrap, if disabled previously, e.g., qemu */
|
|
dprint(STDOUT_FILENO, "\033[?7h", 5);
|
|
|
|
/* Reset attributes, background and foreground color */
|
|
dprint(STDOUT_FILENO, "\033[49m\033[39m\e[2J", 14);
|
|
|
|
/* Hide cursor while we boot up. */
|
|
dprint(STDOUT_FILENO, "\033[?25l", 6);
|
|
|
|
log_init();
|
|
}
|
|
|
|
ssize_t cprintf(const char *fmt, ...)
|
|
{
|
|
const size_t len = strlen(fmt) * 2;
|
|
char buf[len < 256 ? 256 : len];
|
|
size_t size;
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
size = vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
|
|
dprint(STDERR_FILENO, buf, size);
|
|
|
|
return size;
|
|
}
|
|
|
|
void tabstospaces(char *line)
|
|
{
|
|
if (!line)
|
|
return;
|
|
|
|
for (int i = 0; line[i]; i++) {
|
|
if (line[i] == '\t')
|
|
line[i] = ' ';
|
|
}
|
|
}
|
|
|
|
char *strip_line(char *line)
|
|
{
|
|
char *ptr;
|
|
|
|
/* Trim leading whitespace */
|
|
while (*line && isblank(*line))
|
|
line++;
|
|
|
|
/* Strip any comment at end of line */
|
|
ptr = line;
|
|
while (*ptr && *ptr != '#')
|
|
ptr++;
|
|
*ptr = 0;
|
|
|
|
return line;
|
|
}
|
|
|
|
void enable_progress(int onoff)
|
|
{
|
|
if (onoff)
|
|
progress_style = progress_onoff;
|
|
else
|
|
progress_style = PROGRESS_SILENT;
|
|
}
|
|
|
|
void show_progress(pstyle_t style)
|
|
{
|
|
progress_onoff = style;
|
|
}
|
|
|
|
/*
|
|
* Find PRETTY_NAME in /etc/os-release to use as heading at boot
|
|
* and on runlevel changes. Fallback: "Finit vX.YY"
|
|
*/
|
|
char *release_heading(void)
|
|
{
|
|
char *name = NULL;
|
|
char buf[256];
|
|
FILE *fp;
|
|
|
|
fp = fopen("/etc/os-release", "r");
|
|
if (fp) {
|
|
while (fgets(buf, sizeof(buf), fp)) {
|
|
char *ptr = &buf[12];
|
|
|
|
if (strncmp(buf, "PRETTY_NAME=", 12))
|
|
continue;
|
|
|
|
if (*ptr == '"' || *ptr == '\'') {
|
|
char *end = &ptr[strlen(ptr) - 1];
|
|
char q = *ptr;
|
|
|
|
while (end > ptr && isspace(*end))
|
|
*end-- = 0;
|
|
|
|
if (*end == q) {
|
|
*end = 0;
|
|
ptr++;
|
|
}
|
|
}
|
|
|
|
name = strdup(ptr);
|
|
break;
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
if (!name)
|
|
name = strdup(PACKAGE_NAME " v" PACKAGE_VERSION);
|
|
|
|
return name;
|
|
}
|
|
|
|
/*
|
|
* Return screen length of string, not counting escape chars, and
|
|
* accounting for unicode characters as only one screen byte wide
|
|
*/
|
|
size_t slen(char *string)
|
|
{
|
|
unsigned char *buf = (unsigned char *)string;
|
|
size_t len = 0;
|
|
|
|
while (*buf) {
|
|
/* Skip ANSI escape sequences */
|
|
if (*buf == '\e') {
|
|
while (*buf && !isalpha(*buf))
|
|
buf++;
|
|
continue;
|
|
}
|
|
/* Skip 3-byte unicode */
|
|
if (*buf == 0xe2) {
|
|
for (int cnt = 3; *buf && cnt >= 0; cnt--)
|
|
buf++;
|
|
continue;
|
|
}
|
|
|
|
len++;
|
|
buf++;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
/*
|
|
* ch may be an ascii or unicode character
|
|
*/
|
|
static char *pad(char *buf, size_t len, char *ch, size_t width)
|
|
{
|
|
size_t i = slen(buf);
|
|
|
|
strlcat(buf, " ", len);
|
|
|
|
width -= 8; /* Skip leading '[ STAT ]' */
|
|
while (i < width) {
|
|
strlcat(buf, ch, len);
|
|
i++;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
void print_banner(const char *heading)
|
|
{
|
|
char buf[4 * ttcols];
|
|
|
|
if (progress_style == PROGRESS_SILENT)
|
|
return;
|
|
|
|
buf[0] = 0;
|
|
strlcat(buf, "\r\e[K", sizeof(buf));
|
|
|
|
if (progress_style == PROGRESS_CLASSIC) {
|
|
strlcat(buf, "\e[1m", sizeof(buf));
|
|
strlcat(buf, heading, sizeof(buf));
|
|
pad(buf, sizeof(buf), "=", ttcols + 10);
|
|
} else {
|
|
size_t wmax = 80 <= ttcols ? 80 : ttcols;
|
|
|
|
/* • • • Foo System ═══════════════════════════════════════════════ */
|
|
/* ⬤ ⬤ ⬤ Foo System ═══════════════════════════════════════════════ */
|
|
/* ⚉ ⚉ ⚉ Foo System ═══════════════════════════════════════════════ */
|
|
/* o o o Foo System ═══════════════════════════════════════════════ */
|
|
/* ◉ ◉ ◉ Foo System ═══════════════════════════════════════════════ */
|
|
/* ⦿⦿⦿ Foo System ═══════════════════════════════════════════════ */
|
|
strlcat(buf, "\e[1;31m● \e[1;33m● \e[1;32m● \e[0m\e[1m ", sizeof(buf));
|
|
strlcat(buf, heading, sizeof(buf));
|
|
|
|
/*
|
|
* Padding with full-width '═' sign from unicode,
|
|
* we could also use '―' or something else.
|
|
*/
|
|
pad(buf, sizeof(buf), "═", wmax);
|
|
}
|
|
strlcat(buf, "\e[0m\n", sizeof(buf));
|
|
cprintf("%s", buf);
|
|
}
|
|
|
|
static size_t print_timestamp(char *buf, size_t len)
|
|
{
|
|
#if defined(CONFIG_PRINTK_TIME)
|
|
FILE *fp;
|
|
float stamp, dummy;
|
|
|
|
fp = fopen("/proc/uptime", "r");
|
|
if (!fp)
|
|
return;
|
|
|
|
fgets(buf, len, fp);
|
|
fclose(fp);
|
|
|
|
sscanf(buf, "%f %f", &stamp, &dummy);
|
|
return snprintf(buf, len, "[ %.6f ]", stamp);
|
|
#else
|
|
(void)buf;
|
|
(void)len;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static char *status(int rc)
|
|
{
|
|
static char buf[64];
|
|
|
|
if (rc < 0 || rc >= (int)NELEMS(status1))
|
|
rc = NELEMS(status1) - 1; /* Default to "⋯" (pending) */
|
|
|
|
if (progress_style == PROGRESS_CLASSIC) {
|
|
int hl = 1;
|
|
|
|
if (rc == 1 || rc == 2)
|
|
hl = 7;
|
|
|
|
snprintf(buf, sizeof(buf), "\e[%dm[%s]\e[0m", hl, status1[rc]);
|
|
} else
|
|
snprintf(buf, sizeof(buf), "\e[1m[%s%s\e[0m\e[1m]\e[0m ", color[rc], status2[rc]);
|
|
|
|
return buf;
|
|
}
|
|
|
|
void printv(const char *fmt, va_list ap)
|
|
{
|
|
char buf[ttcols];
|
|
size_t len;
|
|
|
|
if (!fmt || progress_style == PROGRESS_SILENT)
|
|
return;
|
|
|
|
delline();
|
|
|
|
buf[0] = 0;
|
|
len = print_timestamp(buf, sizeof(buf));
|
|
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
|
|
|
|
strlcpy(last_desc, &buf[len], sizeof(last_desc)); /* save for re-print */
|
|
|
|
if (progress_style == PROGRESS_CLASSIC)
|
|
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
|
|
else
|
|
cprintf("\r\e[K%s%s", status(3), buf);
|
|
}
|
|
|
|
void print(int rc, const char *fmt, ...)
|
|
{
|
|
char buf[ttcols];
|
|
size_t len;
|
|
|
|
if (progress_style == PROGRESS_SILENT)
|
|
return;
|
|
|
|
buf[0] = 0;
|
|
len = print_timestamp(buf, sizeof(buf));
|
|
|
|
if (fmt) {
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
|
|
va_end(ap);
|
|
strlcpy(last_desc, &buf[len], sizeof(last_desc));
|
|
} else if (rc >= 0 && last_desc[0]) {
|
|
/*
|
|
* No new description, but we have one saved from an earlier
|
|
* print(-1, ...) call. Re-print it so the final status is not
|
|
* left stranded if command output or a kernel message scrolled
|
|
* away the original description line.
|
|
*/
|
|
strlcpy(&buf[len], last_desc, sizeof(buf) - len);
|
|
} else {
|
|
buf[0] = 0;
|
|
}
|
|
|
|
if (rc < 0) {
|
|
/* Pending state: show description with spinner, no final status yet */
|
|
if (!buf[0])
|
|
return;
|
|
delline();
|
|
if (progress_style == PROGRESS_CLASSIC)
|
|
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
|
|
else
|
|
cprintf("\r\e[K%s%s", status(3), buf);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Final status. Emit description + status in a single cprintf() so
|
|
* both reach the console in one write(), preventing kernel messages or
|
|
* command output from splitting the description from its [ OK ]/[FAIL].
|
|
*/
|
|
last_desc[0] = 0;
|
|
|
|
if (buf[0]) {
|
|
delline();
|
|
if (progress_style == PROGRESS_CLASSIC)
|
|
cprintf("\r%s %s\n", pad(buf, sizeof(buf), ".", sizeof(buf)), status(rc));
|
|
else
|
|
cprintf("\r\e[K%s%s\r%s\n", status(3), buf, status(rc));
|
|
} else {
|
|
if (progress_style == PROGRESS_CLASSIC)
|
|
cprintf("%s\n", status(rc));
|
|
else
|
|
cprintf("\r%s\n", status(rc));
|
|
}
|
|
}
|
|
|
|
void print_desc(char *action, char *desc)
|
|
{
|
|
print(-1, "%s%s", action ?: "", desc ?: "");
|
|
}
|
|
|
|
int print_result(int fail)
|
|
{
|
|
print(!!fail, NULL);
|
|
return fail;
|
|
}
|
|
|
|
/*
|
|
* Reset console state and drain the output buffer before kernel takeover.
|
|
* Called just before reboot()/halt() to prevent ANSI escape codes from
|
|
* leaking into bootloader or early-kernel output.
|
|
*/
|
|
void print_exit(void)
|
|
{
|
|
tcdrain(STDERR_FILENO);
|
|
dprint(STDERR_FILENO, "\e[0m\e[?25h", 10); /* reset SGR, show cursor */
|
|
tcdrain(STDERR_FILENO);
|
|
}
|
|
|
|
void set_hostname(char **hostname)
|
|
{
|
|
FILE *fp;
|
|
|
|
if (rescue)
|
|
goto done;
|
|
|
|
fp = fopen(HOSTNAME_PATH, "r");
|
|
if (fp) {
|
|
struct stat st;
|
|
|
|
if (fstat(fileno(fp), &st)) {
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
|
|
*hostname = realloc(*hostname, st.st_size);
|
|
if (!*hostname) {
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
|
|
if (fgets(*hostname, st.st_size, fp))
|
|
chomp(*hostname);
|
|
fclose(fp);
|
|
}
|
|
|
|
done:
|
|
if (*hostname) {
|
|
if (sethostname(*hostname, strlen(*hostname)))
|
|
err(1, "Failed sethostname(%s)", *hostname);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Bring up networking, but only if not single-user or rescue mode
|
|
*/
|
|
void networking(int updown)
|
|
{
|
|
/* No need to report errors if network is already down */
|
|
if (!prevlevel && !updown)
|
|
return;
|
|
|
|
if (updown)
|
|
dbg("Setting up networking ...");
|
|
else
|
|
dbg("Taking down networking ...");
|
|
|
|
/* Run user network start script if enabled */
|
|
if (updown && network) {
|
|
run_interactive(network, "Starting networking: %s", network);
|
|
goto done;
|
|
}
|
|
|
|
/* Debian/Ubuntu/Busybox/RH/Suse */
|
|
if (!whichp("ifup"))
|
|
goto fallback;
|
|
|
|
if (fexist("/etc/network/interfaces")) {
|
|
pid_t pid;
|
|
|
|
pid = fork();
|
|
if (pid == 0) {
|
|
const char *cmd;
|
|
char buf[256];
|
|
FILE *pp;
|
|
int rc;
|
|
|
|
setsid();
|
|
sig_unblock();
|
|
|
|
if (updown)
|
|
cmd = "ifup -a 2>&1";
|
|
else if (whichp("ifquery"))
|
|
/* Regular ifodwn supports --force but not -f */
|
|
cmd = "ifdown -a --force 2>&1";
|
|
else
|
|
/* Busybox ifdown support -f, but not --force */
|
|
cmd = "ifdown -a -f 2>&1";
|
|
|
|
pp = popen(cmd, "r");
|
|
if (!pp)
|
|
_exit(EX_OSERR);
|
|
|
|
while (fgets(buf, sizeof(buf), pp))
|
|
logit(LOG_NOTICE, "network: %s", chomp(buf));
|
|
|
|
rc = pclose(pp);
|
|
if (rc == -1)
|
|
_exit(EX_OSERR);
|
|
_exit(WEXITSTATUS(rc));
|
|
}
|
|
cgroup_service("network", pid, NULL, NULL, NULL);
|
|
print(pid > 0 ? 0 : 1, "%s network interfaces ...",
|
|
updown ? "Bringing up" : "Taking down");
|
|
|
|
goto done;
|
|
}
|
|
|
|
fallback:
|
|
/* Fall back to bring up at least loopback */
|
|
ifconfig("lo", "127.0.0.1", "255.0.0.0", updown);
|
|
done:
|
|
|
|
/* Hooks that rely on loopback, or basic networking being up. */
|
|
if (updown) {
|
|
dbg("Calling all network up hooks ...");
|
|
plugin_run_hooks(HOOK_NETWORK_UP);
|
|
} else {
|
|
dbg("Calling all network down hooks ...");
|
|
plugin_run_hooks(HOOK_NETWORK_DN);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Very basic, and incomplete, check if we're running in a container.
|
|
*/
|
|
int in_container(void)
|
|
{
|
|
const char *containers[] = {
|
|
"lxc",
|
|
"docker",
|
|
"kubepod",
|
|
"unshare",
|
|
"systemd-nspawn"
|
|
};
|
|
const char *files[] = {
|
|
"/run/.containerenv",
|
|
"/.dockerenv"
|
|
};
|
|
static int incont = -1;
|
|
size_t i;
|
|
char *c;
|
|
|
|
if (incont > 0)
|
|
return incont;
|
|
|
|
c = getenv("container");
|
|
if (c) {
|
|
for (i = 0; i < NELEMS(containers); i++) {
|
|
if (!strcmp(containers[i], c))
|
|
return incont = 1;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < NELEMS(files); i++) {
|
|
if (!access(files[i], F_OK))
|
|
return incont = 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* setprocnm - Set process name in argv[0] for ps/pidof
|
|
* @name: New process name to set
|
|
*
|
|
* Changes the process name by modifying argv[0]. This is particularly
|
|
* useful for compatibility with BusyBox's pidof which reads cmdline
|
|
* from /proc/pid/cmdline rather than /proc/pid/comm.
|
|
*/
|
|
void setprocnm(const char *name)
|
|
{
|
|
extern char *arg0;
|
|
size_t len;
|
|
|
|
if (!arg0 || !name)
|
|
return;
|
|
|
|
prctl(PR_SET_NAME, name, 0, 0, 0);
|
|
|
|
len = strlen(arg0);
|
|
memset(arg0, 0, len);
|
|
strlcpy(arg0, name, len + 1);
|
|
}
|
|
|
|
/**
|
|
* Local Variables:
|
|
* indent-tabs-mode: t
|
|
* c-file-style: "linux"
|
|
* End:
|
|
*/
|