From 45c04464dcae75d842fe0f7819290bb43c3eca32 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Sep 2025 18:18:31 +0200 Subject: [PATCH 1/7] shutdown: move printk changing code int generic code --- src/shared/meson.build | 1 + src/shared/printk-util.c | 42 ++++++++++++++++++++++++++++++++++++++++ src/shared/printk-util.h | 5 +++++ src/shutdown/shutdown.c | 33 +++---------------------------- 4 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 src/shared/printk-util.c create mode 100644 src/shared/printk-util.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 9acb83a47e..71735aaaa2 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -154,6 +154,7 @@ shared_sources = files( 'polkit-agent.c', 'portable-util.c', 'pretty-print.c', + 'printk-util.c', 'prompt-util.c', 'ptyfwd.c', 'qrcode-util.c', diff --git a/src/shared/printk-util.c b/src/shared/printk-util.c new file mode 100644 index 0000000000..0f2221b3b2 --- /dev/null +++ b/src/shared/printk-util.c @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "log.h" +#include "parse-util.h" +#include "printk-util.h" +#include "sysctl-util.h" + +int sysctl_printk_read(void) { + int r; + + _cleanup_free_ char *sysctl_printk_vals = NULL; + r = sysctl_read("kernel/printk", &sysctl_printk_vals); + if (r < 0) + return log_debug_errno(r, "Cannot read sysctl kernel.printk: %m"); + + _cleanup_free_ char *sysctl_printk_curr = NULL; + const char *p = sysctl_printk_vals; + r = extract_first_word(&p, &sysctl_printk_curr, NULL, 0); + if (r < 0) + return log_debug_errno(r, "Failed to split out kernel printk priority: %m"); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Short read while reading kernel.printk sysctl"); + + int current_lvl; + r = safe_atoi(sysctl_printk_curr, ¤t_lvl); + if (r < 0) + return log_debug_errno(r, "Failed to parse kernel.printk sysctl: %s", sysctl_printk_vals); + + return current_lvl; +} + +int sysctl_printk_write(int l) { + int r; + + r = sysctl_writef("kernel/printk", "%i", l); + if (r < 0) + return log_debug_errno(r, "Failed to set kernel.printk to %i: %m", l); + + return 0; +} diff --git a/src/shared/printk-util.h b/src/shared/printk-util.h new file mode 100644 index 0000000000..e2bd0db46a --- /dev/null +++ b/src/shared/printk-util.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int sysctl_printk_read(void); +int sysctl_printk_write(int l); diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c index 9a114d9be5..97ae7d119c 100644 --- a/src/shutdown/shutdown.c +++ b/src/shutdown/shutdown.c @@ -36,6 +36,7 @@ #include "log.h" #include "parse-util.h" #include "pidref.h" +#include "printk-util.h" #include "process-util.h" #include "reboot-util.h" #include "rlimit-util.h" @@ -272,42 +273,14 @@ int sync_with_progress(int fd) { return r; } -static int read_current_sysctl_printk_log_level(void) { - _cleanup_free_ char *sysctl_printk_vals = NULL, *sysctl_printk_curr = NULL; - int current_lvl; - const char *p; - int r; - - r = sysctl_read("kernel/printk", &sysctl_printk_vals); - if (r < 0) - return log_debug_errno(r, "Cannot read sysctl kernel.printk: %m"); - - p = sysctl_printk_vals; - r = extract_first_word(&p, &sysctl_printk_curr, NULL, 0); - if (r < 0) - return log_debug_errno(r, "Failed to split out kernel printk priority: %m"); - if (r == 0) - return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Short read while reading kernel.printk sysctl"); - - r = safe_atoi(sysctl_printk_curr, ¤t_lvl); - if (r < 0) - return log_debug_errno(r, "Failed to parse kernel.printk sysctl: %s", sysctl_printk_vals); - - return current_lvl; -} - static void bump_sysctl_printk_log_level(int min_level) { - int current_lvl, r; - /* Set the logging level to be able to see messages with log level smaller or equal to min_level */ - current_lvl = read_current_sysctl_printk_log_level(); + int current_lvl = sysctl_printk_read(); if (current_lvl < 0 || current_lvl >= min_level + 1) return; - r = sysctl_writef("kernel/printk", "%i", min_level + 1); - if (r < 0) - log_debug_errno(r, "Failed to bump kernel.printk to %i: %m", min_level + 1); + (void) sysctl_printk_write(min_level + 1); } static void init_watchdog(void) { From ac63a04bd6211f25b8029b56720228456800fcb6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Sep 2025 18:19:22 +0200 Subject: [PATCH 2/7] mute-console: add simple varlink service that can disable log/status spew to kernel console For "wizard" style interactive tools it's very annoying if they are interrupted by kernel log output or PID1's status output. let's add some infra to disable this temporarily. I decided to implement this as an IPC service so that we can make this robust: if the client request the muting dies we can automatically unmute again. This is hence a tiny varlink service, but it can also be started directly from the cmdline. --- man/rules/meson.build | 4 + man/systemd-mute-console.xml | 79 ++++ meson.build | 1 + src/mute-console/meson.build | 9 + src/mute-console/mute-console.c | 419 ++++++++++++++++++ src/shared/meson.build | 1 + src/shared/varlink-io.systemd.MuteConsole.c | 19 + src/shared/varlink-io.systemd.MuteConsole.h | 6 + src/test/test-varlink-idl.c | 3 + units/meson.build | 6 + ...system-systemd\\x2dmute\\x2dconsole.slice" | 19 + units/systemd-mute-console.socket | 22 + units/systemd-mute-console@.service | 18 + 13 files changed, 606 insertions(+) create mode 100644 man/systemd-mute-console.xml create mode 100644 src/mute-console/meson.build create mode 100644 src/mute-console/mute-console.c create mode 100644 src/shared/varlink-io.systemd.MuteConsole.c create mode 100644 src/shared/varlink-io.systemd.MuteConsole.h create mode 100644 "units/system-systemd\\x2dmute\\x2dconsole.slice" create mode 100644 units/systemd-mute-console.socket create mode 100644 units/systemd-mute-console@.service diff --git a/man/rules/meson.build b/man/rules/meson.build index bb5830eaf6..76aa77ff87 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1050,6 +1050,10 @@ manpages = [ ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], ['systemd-mountfsd.service', '8', ['systemd-mountfsd'], 'ENABLE_MOUNTFSD'], + ['systemd-mute-console', + '1', + ['systemd-mute-console.socket', 'systemd-mute-console@.service'], + ''], ['systemd-network-generator.service', '8', ['systemd-network-generator'], ''], ['systemd-networkd-wait-online.service', '8', diff --git a/man/systemd-mute-console.xml b/man/systemd-mute-console.xml new file mode 100644 index 0000000000..cfab5e9165 --- /dev/null +++ b/man/systemd-mute-console.xml @@ -0,0 +1,79 @@ + + + + + + + + systemd-mute-console + systemd + + + + systemd-mute-console + 1 + + + + systemd-mute-console + systemd-mute-console@.service + systemd-mute-console.socket + Temporarily mute kernel log output and service manager status output to the system console + + + + + systemd-mute-console + OPTIONS + + + systemd-mute-console@.service + systemd-mute-console.socket + + + + Description + + The systemd-mute-console tool and service may be used to + temporarily mute the log output of the kernel as well as the status output of the service manager to + the system console. It may be used by tools running on the console to ensure their terminal output is not + interrupted by unrelated messages. + + The tool can be invoked directly in which case it will mute the two outputs and then issue an + sd_notify3 + READY=1 notification once that is completed. On SIGINT or + SIGTERM output is unmuted again. Alternatively it can be invoked via Varlink + IPC. + + + + Options + + The following options are understood: + + + + + + Individually controls which output to mute. If true is specified the respective + output is muted, if false the output is left as is. Defaults to true. + + + + + + + + + + + See Also + + systemd1 + systemd-firstboot1 + + + + diff --git a/meson.build b/meson.build index 8ec025c8d6..3d728e88a1 100644 --- a/meson.build +++ b/meson.build @@ -2383,6 +2383,7 @@ subdir('src/measure') subdir('src/modules-load') subdir('src/mount') subdir('src/mountfsd') +subdir('src/mute-console') subdir('src/network') subdir('src/notify') subdir('src/nspawn') diff --git a/src/mute-console/meson.build b/src/mute-console/meson.build new file mode 100644 index 0000000000..f0179da21f --- /dev/null +++ b/src/mute-console/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-mute-console', + 'public' : true, + 'sources' : files('mute-console.c'), + }, +] diff --git a/src/mute-console/mute-console.c b/src/mute-console/mute-console.c new file mode 100644 index 0000000000..7f0b211d3f --- /dev/null +++ b/src/mute-console/mute-console.c @@ -0,0 +1,419 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "sd-bus.h" +#include "sd-event.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "ansi-color.h" +#include "build.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-util.h" +#include "daemon-util.h" +#include "errno-util.h" +#include "log.h" +#include "main-func.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "printk-util.h" +#include "varlink-io.systemd.MuteConsole.h" +#include "varlink-util.h" +#include "virt.h" + +static bool arg_mute_pid1 = true; +static bool arg_mute_kernel = true; +static bool arg_varlink = false; + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-mute-console", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...]\n" + "\n%sMute status output to the console.%s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --kernel=BOOL Mute kernel log output\n" + " --pid1=BOOL Mute PID 1 status output\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_KERNEL, + ARG_PID1, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "kernel", required_argument, NULL, ARG_KERNEL }, + { "pid1", required_argument, NULL, ARG_PID1 }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hq", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_PID1: + r = parse_boolean_argument("--pid1=", optarg, &arg_mute_pid1); + if (r < 0) + return r; + + break; + + case ARG_KERNEL: + r = parse_boolean_argument("--kernel=", optarg, &arg_mute_kernel); + if (r < 0) + return r; + + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + arg_varlink = true; + + return 1; +} + +static int set_show_status(const char *value) { + int r; + assert(value); + + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + r = bus_connect_system_systemd(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to systemd: %m"); + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + r = bus_call_method(bus, bus_systemd_mgr, "SetShowStatus", &error, /* ret_reply= */ NULL, "s", value); + if (r < 0) + return log_error_errno(r, "Failed to issue SetShowStatus() method call: %s", bus_error_message(&error, r)); + + return 0; +} + +typedef struct Context { + bool mute_pid1; + bool mute_kernel; + + bool muted_pid1; + int saved_kernel; + + sd_varlink *link; +} Context; + +static int mute_pid1(Context *c) { + int r; + + assert(c); + + if (!c->mute_pid1) { + log_debug("Muting of PID 1 status console output disabled."); + c->muted_pid1 = false; + return 0; + } + + r = set_show_status("no"); + if (r < 0) + return r; + + log_debug("Successfully muted PID 1 status console output."); + + c->muted_pid1 = true; + return 0; +} + +static int unmute_pid1(Context *c) { + int r; + + assert(c); + + if (!c->muted_pid1) { + log_debug("Not restoring PID 1 status console output level."); + return 0; + } + + r = set_show_status(""); + if (r < 0) + return r; + + log_debug("Successfully unmuted PID 1 status console output."); + c->muted_pid1 = false; + return 0; +} + +static int mute_kernel(Context *c) { + int r; + + assert(c); + + if (!arg_mute_kernel) { + log_debug("Muting of kernel printk() console output disabled."); + c->saved_kernel = -1; + return 0; + } + + if (detect_container() > 0) { + log_debug("Skipping muting of print() console output, because running in a container."); + c->saved_kernel = -1; + return 0; + } + + int level = sysctl_printk_read(); + if (level < 0) + return log_error_errno(level, "Failed to read kernel printk() console output level: %m"); + + if (level == 0) { + log_info("Not muting kernel printk() console output, since it is already disabled."); + c->saved_kernel = -1; /* don't bother with restoring */ + } else { + r = sysctl_printk_write(0); + if (r < 0) + return log_error_errno(r, "Failed to change kernel printk() console output level: %m"); + + log_debug("Successfully muted kernel printk() console output."); + c->saved_kernel = level; + } + + return 0; +} + +static int unmute_kernel(Context *c) { + int r; + + assert(c); + + if (c->saved_kernel < 0) { + log_debug("Not restoring kernel printk() console output level."); + return 0; + } + + int level = sysctl_printk_read(); + if (level < 0) + return log_error_errno(level, "Failed to read kernel printk() console output level: %m"); + + if (level != 0) { + log_info("Not unmuting kernel printk() console output, since it has been changed externally in the meantime."); + return 0; + } + + r = sysctl_printk_write(c->saved_kernel); + if (r < 0) + return log_error_errno(r, "Failed to unmute kernel printk() console output level: %m"); + + log_debug("Successfully unmuted kernel printk() console output."); + c->saved_kernel = -1; + return 0; +} + +static void context_done(Context *c) { + assert(c); + + (void) unmute_pid1(c); + (void) unmute_kernel(c); + + if (c->link) { + (void) sd_varlink_set_userdata(c->link, NULL); + c->link = sd_varlink_flush_close_unref(c->link); + } +} + +static Context* context_free(Context *c) { + if (!c) + return NULL; + + context_done(c); + return mfree(c); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Context*, context_free); + +static void vl_on_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { + assert(link); + + Context *c = sd_varlink_get_userdata(link); + if (!c) + return; + + context_free(c); +} + +static int vl_method_mute( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(link); + + _cleanup_(context_freep) Context *nc = new(Context, 1); + if (!nc) + return -ENOMEM; + + *nc = (Context) { + .mute_pid1 = true, + .mute_kernel = true, + .saved_kernel = -1, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "kernel", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Context, mute_kernel), 0 }, + { "pid1", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Context, mute_pid1), 0 }, + {} + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, nc); + if (r != 0) + return r; + + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL); + + r = sd_varlink_server_bind_disconnect(sd_varlink_get_server(link), vl_on_disconnect); + if (r < 0) + return r; + + (void) sd_varlink_set_userdata(link, nc); + nc->link = sd_varlink_ref(link); + Context *c = TAKE_PTR(nc); /* the Context object is now managed by the disconnect handler, not us anymore */ + + r = 0; + RET_GATHER(r, mute_pid1(c)); + RET_GATHER(r, mute_kernel(c)); + if (r < 0) + return r; + + /* Let client know we are muted now. We use sd_varlink_notify() here (rather than sd_varlink_reply()) + * because we want to keep the method call open, as we want that the lifetime of the + * connection/method call to determine how long we keep the console muted. */ + r = sd_varlink_notify(link, /* parameters= */ NULL); + if (r < 0) + return r; + + return 0; +} + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; + int r; + + /* Invocation as Varlink service */ + + r = varlink_server_new( + &varlink_server, + SD_VARLINK_SERVER_ROOT_ONLY| + SD_VARLINK_SERVER_HANDLE_SIGINT| + SD_VARLINK_SERVER_HANDLE_SIGTERM, + /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_MuteConsole); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + varlink_server, + "io.systemd.MuteConsole.Mute", vl_method_mute); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + r = sd_varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} + +static int run(int argc, char* argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_varlink) + return vl_server(); + + if (!arg_mute_pid1 && !arg_mute_kernel) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not asked to mute anything, refusing."); + + _cleanup_(context_done) Context c = { + .mute_pid1 = arg_mute_pid1, + .mute_kernel = arg_mute_kernel, + .saved_kernel = -1, + }; + + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + r = sd_event_new(&event); + if (r < 0) + return log_error_errno(r, "Failed to get default event source: %m"); + + (void) sd_event_set_watchdog(event, true); + (void) sd_event_set_signal_exit(event, true); + + int ret = 0; + RET_GATHER(ret, mute_pid1(&c)); + RET_GATHER(ret, mute_kernel(&c)); + + /* Now tell service manager we area ready to go */ + _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = + notify_start("READY=1\n" + "STATUS=Console status output muted temporarily.", + "STOPPING=1\n" + "STATUS=Console status output unmuted."); + + /* Now wait for SIGINT/SIGTERM */ + r = sd_event_loop(event); + if (r < 0) + RET_GATHER(ret, log_error_errno(r, "Failed to run event loop: %m")); + + RET_GATHER(ret, unmute_pid1(&c)); + RET_GATHER(ret, unmute_kernel(&c)); + + return ret; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/shared/meson.build b/src/shared/meson.build index 71735aaaa2..10fc6742a2 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -202,6 +202,7 @@ shared_sources = files( 'varlink-io.systemd.ManagedOOM.c', 'varlink-io.systemd.Manager.c', 'varlink-io.systemd.MountFileSystem.c', + 'varlink-io.systemd.MuteConsole.c', 'varlink-io.systemd.NamespaceResource.c', 'varlink-io.systemd.Network.c', 'varlink-io.systemd.PCRExtend.c', diff --git a/src/shared/varlink-io.systemd.MuteConsole.c b/src/shared/varlink-io.systemd.MuteConsole.c new file mode 100644 index 0000000000..0cea5b8554 --- /dev/null +++ b/src/shared/varlink-io.systemd.MuteConsole.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink-idl.h" + +#include "varlink-io.systemd.MuteConsole.h" + +static SD_VARLINK_DEFINE_METHOD( + Mute, + SD_VARLINK_FIELD_COMMENT("Whether to mute the kernel's output to the console (defaults to true)."), + SD_VARLINK_DEFINE_INPUT(kernel, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether to mute PID1's output to the console (defaults to true)."), + SD_VARLINK_DEFINE_INPUT(pid1, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_MuteConsole, + "io.systemd.MuteConsole", + SD_VARLINK_INTERFACE_COMMENT("API for temporarily muting noisy output to the main kernel console"), + SD_VARLINK_SYMBOL_COMMENT("Mute kernel and PID 1 output to the main kernel console"), + &vl_method_Mute); diff --git a/src/shared/varlink-io.systemd.MuteConsole.h b/src/shared/varlink-io.systemd.MuteConsole.h new file mode 100644 index 0000000000..9957ed1a5f --- /dev/null +++ b/src/shared/varlink-io.systemd.MuteConsole.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_MuteConsole; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index 6b3449f777..1d328bd3e2 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -24,6 +24,7 @@ #include "varlink-io.systemd.Manager.h" #include "varlink-io.systemd.ManagedOOM.h" #include "varlink-io.systemd.MountFileSystem.h" +#include "varlink-io.systemd.MuteConsole.h" #include "varlink-io.systemd.NamespaceResource.h" #include "varlink-io.systemd.Network.h" #include "varlink-io.systemd.PCRExtend.h" @@ -166,6 +167,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_UserDatabase); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_MuteConsole); + print_separator(); test_parse_format_one(&vl_interface_io_systemd_NamespaceResource); print_separator(); test_parse_format_one(&vl_interface_io_systemd_Journal); diff --git a/units/meson.build b/units/meson.build index c5b99e4e04..ba2dfcab06 100644 --- a/units/meson.build +++ b/units/meson.build @@ -143,6 +143,12 @@ units = [ }, { 'file' : 'modprobe@.service' }, { 'file' : 'multi-user.target' }, + { + 'file' : 'systemd-mute-console.socket', + 'symlinks' : ['sockets.target.wants/'] + }, + { 'file' : 'systemd-mute-console@.service' }, + { 'file' : 'system-systemd\\x2dmute\\x2dconsole.slice' }, { 'file' : 'network-online.target' }, { 'file' : 'network-pre.target' }, { 'file' : 'network.target' }, diff --git "a/units/system-systemd\\x2dmute\\x2dconsole.slice" "b/units/system-systemd\\x2dmute\\x2dconsole.slice" new file mode 100644 index 0000000000..7819eb91a3 --- /dev/null +++ "b/units/system-systemd\\x2dmute\\x2dconsole.slice" @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Console Output Muting Service Slice +Documentation=man:systemd-mute-console(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Slice] +# Serialize requests to mute the console. +ConcurrencySoftMax=1 diff --git a/units/systemd-mute-console.socket b/units/systemd-mute-console.socket new file mode 100644 index 0000000000..6223dc033c --- /dev/null +++ b/units/systemd-mute-console.socket @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Console Output Muting Service Socket +Documentation=man:systemd-mute-console(8) +DefaultDependencies=no +Before=sockets.target +Conflicts=shutdown.target +Before=shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.MuteConsole +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes diff --git a/units/systemd-mute-console@.service b/units/systemd-mute-console@.service new file mode 100644 index 0000000000..d43766c70b --- /dev/null +++ b/units/systemd-mute-console@.service @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Console Output Muting Service +Documentation=man:systemd-mute-console(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +ExecStart=systemd-mute-console From 40e742be3ce1e944beb39d160919f49d3e13230a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Sep 2025 22:36:17 +0200 Subject: [PATCH 3/7] prompt-util: add client helper for muting the console --- src/shared/prompt-util.c | 59 ++++++++++++++++++++++++++++++++++++++++ src/shared/prompt-util.h | 2 ++ 2 files changed, 61 insertions(+) diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 927ef0770e..28435b2da1 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -2,6 +2,8 @@ #include +#include + #include "alloc-util.h" #include "glyph-util.h" #include "log.h" @@ -330,3 +332,60 @@ void chrome_hide(void) { fflush(stdout); } + +static int vl_on_reply(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) { + assert(link); + + /* We want to keep the link around (since its lifetime defines the lifetime of the console muting), + * hence let's detach it from the event loop now, and then exit the event loop. */ + + _cleanup_(sd_event_unrefp) sd_event *e = sd_event_ref(ASSERT_PTR(sd_varlink_get_event(link))); + sd_varlink_detach_event(link); + (void) sd_event_exit(e, (error_id || !FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES)) ? -EBADR : 0); + + return 0; +} + +int mute_console(sd_varlink **ret_link) { + int r; + + assert(ret_link); + + /* Talks to the MuteConsole service, and asks for output to the console to be muted, as long as the + * connection is retained */ + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *link = NULL; + r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.MuteConsole"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to console muting service: %m"); + + _cleanup_(sd_event_unrefp) sd_event* event = NULL; + r = sd_event_new(&event); + if (r < 0) + return r; + + r = sd_varlink_attach_event(link, event, /* priority= */ 0); + if (r < 0) + return r; + + r = sd_varlink_bind_reply(link, vl_on_reply); + if (r < 0) + return r; + + r = sd_varlink_set_relative_timeout(link, UINT64_MAX); + if (r < 0) + return log_debug_errno(r, "Failed to disable method call time-out: %m"); + + r = sd_varlink_observe(link, "io.systemd.MuteConsole.Mute", /* parameters= */ NULL); + if (r < 0) + return log_debug_errno(r, "Failed to issue Mute() call to io.systemd.MuteConsole: %m"); + + /* Now run the event loop, it will exit on the first reply, which is when we know the console output + * is now muted. */ + r = sd_event_loop(event); + if (r < 0) + return r; + + *ret_link = TAKE_PTR(link); + return 0; +} diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h index 06dd2c7f0c..70f8f0215d 100644 --- a/src/shared/prompt-util.h +++ b/src/shared/prompt-util.h @@ -29,3 +29,5 @@ int prompt_loop(const char *text, int chrome_show(const char *top, const char *bottom); void chrome_hide(void); + +int mute_console(sd_varlink **ret_link); From 0d2cc656c8f54896f11ef00df0cb7ee40a95ae3b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Sep 2025 09:27:07 +0200 Subject: [PATCH 4/7] firstboot: mute console while running on the console at boot Fixes: #34448 --- man/systemd-firstboot.xml | 10 +++++ src/firstboot/firstboot.c | 76 ++++++++++++++++++++------------- units/systemd-firstboot.service | 3 +- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index fe50022d73..cc4ddc0cf6 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -354,6 +354,16 @@ + + + + Takes a boolean argument. If true kernel log output and service manager status output + to the system console is temporarily disabled while systemd-firstboot is running, + so that its own output is not interrupted. Defaults to false. + + + + diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index f92011b614..71fc8539d8 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -6,6 +6,7 @@ #include "sd-bus.h" #include "sd-id128.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "ask-password-api.h" @@ -86,6 +87,7 @@ static bool arg_welcome = true; static bool arg_reset = false; static ImagePolicy *arg_image_policy = NULL; static bool arg_chrome = true; +static bool arg_mute_console = false; STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); @@ -99,13 +101,17 @@ STATIC_DESTRUCTOR_REGISTER(arg_root_shell, freep); STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); -static void print_welcome(int rfd) { +static void print_welcome(int rfd, sd_varlink **mute_console_link) { _cleanup_free_ char *pretty_name = NULL, *os_name = NULL, *ansi_color = NULL; static bool done = false; const char *pn, *ac; int r; assert(rfd >= 0); + assert(mute_console_link); + + if (!*mute_console_link && arg_mute_console) + (void) mute_console(mute_console_link); if (!arg_welcome) return; @@ -227,7 +233,7 @@ static int locale_is_ok(const char *name, void *userdata) { return r != 0 ? locale_is_installed(name) > 0 : locale_is_valid(name); } -static int prompt_locale(int rfd) { +static int prompt_locale(int rfd, sd_varlink **mute_console_link) { _cleanup_strv_free_ char **locales = NULL; bool acquired_from_creds = false; int r; @@ -279,7 +285,7 @@ static int prompt_locale(int rfd) { /* Not setting arg_locale_message here, since it defaults to LANG anyway */ } } else { - print_welcome(rfd); + print_welcome(rfd, mute_console_link); r = prompt_loop("Please enter the new system locale name or number", GLYPH_WORLD, @@ -321,7 +327,7 @@ static int prompt_locale(int rfd) { return 0; } -static int process_locale(int rfd) { +static int process_locale(int rfd, sd_varlink **mute_console_link) { _cleanup_close_ int pfd = -EBADF; _cleanup_free_ char *f = NULL; char* locales[3]; @@ -357,7 +363,7 @@ static int process_locale(int rfd) { } } - r = prompt_locale(rfd); + r = prompt_locale(rfd, mute_console_link); if (r < 0) return r; @@ -394,7 +400,7 @@ static int keymap_is_ok(const char* name, void *userdata) { return r != 0 ? keymap_exists(name) > 0 : keymap_is_valid(name); } -static int prompt_keymap(int rfd) { +static int prompt_keymap(int rfd, sd_varlink **mute_console_link) { _cleanup_strv_free_ char **kmaps = NULL; int r; @@ -422,7 +428,7 @@ static int prompt_keymap(int rfd) { if (r < 0) return log_error_errno(r, "Failed to read keymaps: %m"); - print_welcome(rfd); + print_welcome(rfd, mute_console_link); return prompt_loop( "Please enter the new keymap name or number", @@ -439,7 +445,7 @@ static int prompt_keymap(int rfd) { &arg_keymap); } -static int process_keymap(int rfd) { +static int process_keymap(int rfd, sd_varlink **mute_console_link) { _cleanup_close_ int pfd = -EBADF; _cleanup_free_ char *f = NULL; _cleanup_strv_free_ char **keymap = NULL; @@ -474,7 +480,7 @@ static int process_keymap(int rfd) { } } - r = prompt_keymap(rfd); + r = prompt_keymap(rfd, mute_console_link); if (r == -ENOENT) return 0; /* don't fail if no keymaps are installed */ if (r < 0) @@ -508,7 +514,7 @@ static int timezone_is_ok(const char *name, void *userdata) { return timezone_is_valid(name, LOG_DEBUG); } -static int prompt_timezone(int rfd) { +static int prompt_timezone(int rfd, sd_varlink **mute_console_link) { _cleanup_strv_free_ char **zones = NULL; int r; @@ -534,7 +540,7 @@ static int prompt_timezone(int rfd) { if (r < 0) return log_error_errno(r, "Cannot query timezone list: %m"); - print_welcome(rfd); + print_welcome(rfd, mute_console_link); return prompt_loop( "Please enter the new timezone name or number", @@ -551,7 +557,7 @@ static int prompt_timezone(int rfd) { &arg_timezone); } -static int process_timezone(int rfd) { +static int process_timezone(int rfd, sd_varlink **mute_console_link) { _cleanup_close_ int pfd = -EBADF; _cleanup_free_ char *f = NULL, *relpath = NULL; const char *e; @@ -592,7 +598,7 @@ static int process_timezone(int rfd) { } } - r = prompt_timezone(rfd); + r = prompt_timezone(rfd, mute_console_link); if (r < 0) return r; @@ -616,7 +622,7 @@ static int hostname_is_ok(const char *name, void *userdata) { return hostname_is_valid(name, VALID_HOSTNAME_TRAILING_DOT); } -static int prompt_hostname(int rfd) { +static int prompt_hostname(int rfd, sd_varlink **mute_console_link) { int r; assert(rfd >= 0); @@ -629,7 +635,7 @@ static int prompt_hostname(int rfd) { return 0; } - print_welcome(rfd); + print_welcome(rfd, mute_console_link); r = prompt_loop("Please enter the new hostname", GLYPH_LABEL, @@ -652,7 +658,7 @@ static int prompt_hostname(int rfd) { return 0; } -static int process_hostname(int rfd) { +static int process_hostname(int rfd, sd_varlink **mute_console_link) { _cleanup_close_ int pfd = -EBADF; _cleanup_free_ char *f = NULL; int r; @@ -671,7 +677,7 @@ static int process_hostname(int rfd) { if (r <= 0) return r; - r = prompt_hostname(rfd); + r = prompt_hostname(rfd, mute_console_link); if (r < 0) return r; @@ -720,7 +726,7 @@ static int process_machine_id(int rfd) { return 0; } -static int prompt_root_password(int rfd) { +static int prompt_root_password(int rfd, sd_varlink **mute_console_link) { const char *msg1, *msg2; int r; @@ -737,7 +743,7 @@ static int prompt_root_password(int rfd) { return 0; } - print_welcome(rfd); + print_welcome(rfd, mute_console_link); msg1 = strjoina("Please enter the new root password (empty to skip):"); msg2 = strjoina("Please enter the new root password again:"); @@ -817,7 +823,7 @@ static int shell_is_ok(const char *path, void *userdata) { return find_shell(rfd, path) >= 0; } -static int prompt_root_shell(int rfd) { +static int prompt_root_shell(int rfd, sd_varlink **mute_console_link) { int r; assert(rfd >= 0); @@ -838,7 +844,7 @@ static int prompt_root_shell(int rfd) { return 0; } - print_welcome(rfd); + print_welcome(rfd, mute_console_link); return prompt_loop( "Please enter the new root shell", @@ -1005,7 +1011,7 @@ static int write_root_shadow(int etc_fd, const char *hashed_password) { return 0; } -static int process_root_account(int rfd) { +static int process_root_account(int rfd, sd_varlink **mute_console_link) { _cleanup_close_ int pfd = -EBADF; _cleanup_(release_lock_file) LockFile lock = LOCK_FILE_INIT; _cleanup_(erase_and_freep) char *_hashed_password = NULL; @@ -1059,7 +1065,7 @@ static int process_root_account(int rfd) { return log_oom(); } - r = prompt_root_shell(rfd); + r = prompt_root_shell(rfd, mute_console_link); if (r < 0) return r; @@ -1078,7 +1084,7 @@ static int process_root_account(int rfd) { arg_root_password_is_hashed = true; } - r = prompt_root_password(rfd); + r = prompt_root_password(rfd, mute_console_link); if (r < 0) return r; @@ -1246,6 +1252,8 @@ static int help(void) { " --welcome=no Disable the welcome text\n" " --chrome=no Don't show color bar at top and bottom of\n" " terminal\n" + " --mute-console=yes Tell kernel/PID 1 to not write to the console\n" + " while running\n" " --reset Remove existing files\n" "\nSee the %2$s for details.\n", program_invocation_short_name, @@ -1293,6 +1301,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_WELCOME, ARG_CHROME, ARG_RESET, + ARG_MUTE_CONSOLE, }; static const struct option options[] = { @@ -1331,6 +1340,7 @@ static int parse_argv(int argc, char *argv[]) { { "welcome", required_argument, NULL, ARG_WELCOME }, { "chrome", required_argument, NULL, ARG_CHROME }, { "reset", no_argument, NULL, ARG_RESET }, + { "mute-console", required_argument, NULL, ARG_MUTE_CONSOLE }, {} }; @@ -1550,6 +1560,13 @@ static int parse_argv(int argc, char *argv[]) { arg_reset = true; break; + case ARG_MUTE_CONSOLE: + r = parse_boolean_argument("--mute-console=", optarg, &arg_mute_console); + if (r < 0) + return r; + + break; + case '?': return -EINVAL; @@ -1712,27 +1729,28 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - r = process_locale(rfd); + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; + r = process_locale(rfd, &mute_console_link); if (r < 0) return r; if (r > 0 && !offline) (void) reload_system_manager(&bus); - r = process_keymap(rfd); + r = process_keymap(rfd, &mute_console_link); if (r < 0) return r; if (r > 0 && !offline) (void) reload_vconsole(&bus); - r = process_timezone(rfd); + r = process_timezone(rfd, &mute_console_link); if (r < 0) return r; - r = process_hostname(rfd); + r = process_hostname(rfd, &mute_console_link); if (r < 0) return r; - r = process_root_account(rfd); + r = process_root_account(rfd, &mute_console_link); if (r < 0) return r; diff --git a/units/systemd-firstboot.service b/units/systemd-firstboot.service index ce6f984f93..5ba9c8ba03 100644 --- a/units/systemd-firstboot.service +++ b/units/systemd-firstboot.service @@ -22,6 +22,7 @@ After=systemd-remount-fs.service After=systemd-sysusers.service systemd-tmpfiles-setup.service # Let vconsole-setup do its setup before starting user interaction: After=systemd-vconsole-setup.service +After=systemd-mute-console.socket Wants=first-boot-complete.target Before=first-boot-complete.target sysinit.target @@ -31,7 +32,7 @@ Before=shutdown.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=systemd-firstboot --prompt-locale --prompt-keymap --prompt-timezone --prompt-root-password +ExecStart=systemd-firstboot --prompt-locale --prompt-keymap --prompt-timezone --prompt-root-password --mute-console=yes StandardOutput=tty StandardInput=tty StandardError=tty From 2eb0820d862bde366b12f820b609db2a9cb465c2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Sep 2025 18:01:04 +0200 Subject: [PATCH 5/7] varlinkctl: send READY=1 for --more calls once we received first reply --- src/varlinkctl/varlinkctl.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 9e32156239..808ae95348 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -587,8 +587,13 @@ static int reply_callback( else r = *ret = log_error_errno(SYNTHETIC_ERRNO(EBADE), "Method call failed: %s", error); } - } else + } else { + /* Let the caller know we have received at least one reply now. This is useful for + * subscription style interfaces where the first reply indicates the subscription being + * successfully enabled. */ + (void) sd_notify(/* unset_environment= */ false, "READY=1"); r = 0; + } if (!arg_quiet) sd_json_variant_dump(parameters, arg_json_format_flags, stdout, NULL); From 82a48eb1db49139ea5b46375d80676e7c0737096 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 24 Sep 2025 18:01:20 +0200 Subject: [PATCH 6/7] test: add simple test case for mute-console service --- test/units/TEST-74-AUX-UTILS.mute-console.sh | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100755 test/units/TEST-74-AUX-UTILS.mute-console.sh diff --git a/test/units/TEST-74-AUX-UTILS.mute-console.sh b/test/units/TEST-74-AUX-UTILS.mute-console.sh new file mode 100755 index 0000000000..e15be1ef32 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.mute-console.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! command -v systemd-mute-console >/dev/null; then + echo "systemd-mute-console is not installed, skipping the test" + exit 0 +fi + +PID="$(systemd-notify --fork -- systemd-mute-console)" +sleep .5 +kill "$PID" +unset PID + +(! systemd-mute-console --kernel=no --pid1=no) + +PID="$(systemd-notify --fork -- systemd-mute-console --kernel=yes --pid1=yes)" +sleep .5 +kill "$PID" +unset PID + +varlinkctl introspect "$(which systemd-mute-console)" + +PID="$(systemd-notify --fork -- varlinkctl call -E "$(which systemd-mute-console)" io.systemd.MuteConsole.Mute '{}')" +sleep .5 +kill "$PID" +unset PID + +PID="$(systemd-notify --fork -- varlinkctl call -E "$(which systemd-mute-console)" io.systemd.MuteConsole.Mute '{"pid1":true, "kernel":true}')" +sleep .5 +kill "$PID" +unset PID + +varlinkctl introspect /run/systemd/io.systemd.MuteConsole + +PID="$(systemd-notify --fork -- varlinkctl call -E /run/systemd/io.systemd.MuteConsole io.systemd.MuteConsole.Mute '{}')" +sleep .5 +kill "$PID" +unset PID From 971637c47db4044c9b50c140abf7ecb7c0e7fcf2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Sep 2025 09:43:45 +0200 Subject: [PATCH 7/7] firstboot: get rid of pointless strjoina() --- src/firstboot/firstboot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 71fc8539d8..6a2e8ee284 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -745,8 +745,8 @@ static int prompt_root_password(int rfd, sd_varlink **mute_console_link) { print_welcome(rfd, mute_console_link); - msg1 = strjoina("Please enter the new root password (empty to skip):"); - msg2 = strjoina("Please enter the new root password again:"); + msg1 = "Please enter the new root password (empty to skip):"; + msg2 = "Please enter the new root password again:"; suggest_passwords();