From: Lennart Poettering Date: Fri, 23 Feb 2024 11:20:55 +0000 (+0100) Subject: vmspawn: use our own ptyfwd code for the console of a VM X-Git-Tag: v256-rc1~732^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=795ec90cda93f26e42a3cb73fceca84a61e585e0;p=thirdparty%2Fsystemd.git vmspawn: use our own ptyfwd code for the console of a VM Let's make systemd-nspawn use our own ptyfwd logic to handle the TTY by default. This adds a new setting --console=, inspired by nspawn's setting of the same name. If --console=interactive= is used, then we'll do the TTY dance on our own via ptyfwd, and thus get tinting, our usual hotkey handling and similar. Since qemu's own console is useful too, let's keep it around via --console=native. FInally, replace the --qemu-gui switch by --console=gui. --- diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index ed4dfc8bfab..d7fee0538ac 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -205,14 +205,6 @@ - - - - Start QEMU in graphical mode. - - - - @@ -361,6 +353,42 @@ + + Input/Output Options + + + + MODE + + Configures how to set up the console of the VM. Takes one of + interactive, read-only, native, + gui. Defaults to interactive. interactive + provides an interactive terminal interface to the VM. read-only is similar, but + is strictly read-only, i.e. does not accept any input from the user. native also + provides a TTY-based interface, but uses qemu native implementation (which means the qemu monitor + is available). gui shows the qemu graphical UI. + + + + + + + + Change the terminal background color to the specified ANSI color as long as the VM + runs. The color specified should be an ANSI X3.64 SGR background color, i.e. strings such as + 40, 41, …, 47, 48;2;…, + 48;5;…. See ANSI + Escape Code (Wikipedia) for details. Assign an empty string to disable any coloring. This + only has an effect in and + modes. + + + + + + + Credentials diff --git a/src/basic/glyph-util.c b/src/basic/glyph-util.c index 2cec3d82cf0..b6b0f40ca6f 100644 --- a/src/basic/glyph-util.c +++ b/src/basic/glyph-util.c @@ -77,6 +77,7 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) { [SPECIAL_GLYPH_RED_CIRCLE] = "o", [SPECIAL_GLYPH_YELLOW_CIRCLE] = "o", [SPECIAL_GLYPH_BLUE_CIRCLE] = "o", + [SPECIAL_GLYPH_GREEN_CIRCLE] = "o", }, /* UTF-8 */ @@ -143,6 +144,7 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) { [SPECIAL_GLYPH_RED_CIRCLE] = u8"🔴", [SPECIAL_GLYPH_YELLOW_CIRCLE] = u8"🟡", [SPECIAL_GLYPH_BLUE_CIRCLE] = u8"🔵", + [SPECIAL_GLYPH_GREEN_CIRCLE] = u8"🟢", }, }; diff --git a/src/basic/glyph-util.h b/src/basic/glyph-util.h index e476fefe943..2f70b187fcd 100644 --- a/src/basic/glyph-util.h +++ b/src/basic/glyph-util.h @@ -52,6 +52,7 @@ typedef enum SpecialGlyph { SPECIAL_GLYPH_RED_CIRCLE, SPECIAL_GLYPH_YELLOW_CIRCLE, SPECIAL_GLYPH_BLUE_CIRCLE, + SPECIAL_GLYPH_GREEN_CIRCLE, _SPECIAL_GLYPH_MAX, _SPECIAL_GLYPH_INVALID = -EINVAL, } SpecialGlyph; diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c index dd9a8134bf2..67d9c7e65cd 100644 --- a/src/test/test-locale-util.c +++ b/src/test/test-locale-util.c @@ -82,7 +82,7 @@ TEST(keymaps) { #define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x)) TEST(dump_special_glyphs) { - assert_cc(SPECIAL_GLYPH_BLUE_CIRCLE + 1 == _SPECIAL_GLYPH_MAX); + assert_cc(SPECIAL_GLYPH_GREEN_CIRCLE + 1 == _SPECIAL_GLYPH_MAX); log_info("is_locale_utf8: %s", yes_no(is_locale_utf8())); @@ -130,6 +130,7 @@ TEST(dump_special_glyphs) { dump_glyph(SPECIAL_GLYPH_RED_CIRCLE); dump_glyph(SPECIAL_GLYPH_YELLOW_CIRCLE); dump_glyph(SPECIAL_GLYPH_BLUE_CIRCLE); + dump_glyph(SPECIAL_GLYPH_GREEN_CIRCLE); } DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index cb1a463781b..780df553aac 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -1,3 +1,13 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "string-table.h" #include "vmspawn-settings.h" + +static const char *const console_mode_table[_CONSOLE_MODE_MAX] = { + [CONSOLE_INTERACTIVE] = "interactive", + [CONSOLE_READ_ONLY] = "read-only", + [CONSOLE_NATIVE] = "native", + [CONSOLE_GUI] = "gui", +}; + +DEFINE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 60ea10e6de0..fe23aa23cf7 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -1,8 +1,20 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include #include +#include "macro.h" + +typedef enum ConsoleMode { + CONSOLE_INTERACTIVE, /* ptyfwd */ + CONSOLE_READ_ONLY, /* ptyfwd, but in read-only mode */ + CONSOLE_NATIVE, /* qemu's native TTY handling */ + CONSOLE_GUI, /* qemu's graphical UI */ + _CONSOLE_MODE_MAX, + _CONSOLE_MODE_INVALID = -EINVAL, +} ConsoleMode; + typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_BIND_MOUNTS = UINT64_C(1) << 11, @@ -10,3 +22,6 @@ typedef enum SettingsMask { SETTING_CREDENTIALS = UINT64_C(1) << 30, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; + +const char *console_mode_to_string(ConsoleMode m) _const_; +ConsoleMode console_mode_from_string(const char *s) _pure_; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 6c2d943daa9..ce7f1ef2e3e 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -46,6 +46,7 @@ #include "path-util.h" #include "pretty-print.h" #include "process-util.h" +#include "ptyfwd.h" #include "random-util.h" #include "rm-rf.h" #include "signal-util.h" @@ -73,7 +74,7 @@ static unsigned arg_vsock_cid = VMADDR_CID_ANY; static int arg_tpm = -1; static char *arg_linux = NULL; static char **arg_initrds = NULL; -static bool arg_qemu_gui = false; +static ConsoleMode arg_console_mode = CONSOLE_INTERACTIVE; static NetworkStack arg_network_stack = NETWORK_STACK_NONE; static int arg_secure_boot = -1; static MachineCredentialContext arg_credentials = {}; @@ -87,6 +88,7 @@ static bool arg_runtime_directory_created = false; static bool arg_privileged = false; static char **arg_kernel_cmdline_extra = NULL; static char **arg_extra_drives = NULL; +static char *arg_background = NULL; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); @@ -101,6 +103,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_runtime_mounts, runtime_mount_context_done); STATIC_DESTRUCTOR_REGISTER(arg_forward_journal, freep); STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_background, freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -130,7 +133,6 @@ static int help(void) { " --tpm=BOOL Enable use of a virtual TPM\n" " --linux=PATH Specify the linux kernel for direct kernel boot\n" " --initrd=PATH Specify the initrd for direct kernel boot\n" - " --qemu-gui Start QEMU in graphical mode\n" " -n --network-tap Create a TAP device for networking\n" " --network-user-mode Use user mode networking\n" " --secure-boot=BOOL Enable searching for firmware supporting SecureBoot\n" @@ -150,6 +152,9 @@ static int help(void) { "\n%3$sIntegration:%4$s\n" " --forward-journal=FILE|DIR\n" " Forward the VM's journal to the host\n" + "\n%3$sInput/Output:%4$s\n" + " --console=MODE Console mode (interactive, native, gui)\n" + " --background=COLOR Set ANSI color for background\n" "\n%3$sCredentials:%4$s\n" " --set-credential=ID:VALUE\n" " Pass a credential with literal value to the VM\n" @@ -190,6 +195,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_SET_CREDENTIAL, ARG_LOAD_CREDENTIAL, ARG_FIRMWARE, + ARG_CONSOLE, + ARG_BACKGROUND, }; static const struct option options[] = { @@ -212,7 +219,8 @@ static int parse_argv(int argc, char *argv[]) { { "tpm", required_argument, NULL, ARG_TPM }, { "linux", required_argument, NULL, ARG_LINUX }, { "initrd", required_argument, NULL, ARG_INITRD }, - { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, + { "console", required_argument, NULL, ARG_CONSOLE }, + { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, /* compat option */ { "network-tap", no_argument, NULL, 'n' }, { "network-user-mode", no_argument, NULL, ARG_NETWORK_USER_MODE }, { "bind", required_argument, NULL, ARG_BIND }, @@ -224,6 +232,7 @@ static int parse_argv(int argc, char *argv[]) { { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, { "firmware", required_argument, NULL, ARG_FIRMWARE }, + { "background", required_argument, NULL, ARG_BACKGROUND }, {} }; @@ -344,8 +353,15 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_CONSOLE: + arg_console_mode = console_mode_from_string(optarg); + if (arg_console_mode < 0) + return log_error_errno(arg_console_mode, "Failed to parse specified console mode: %s", optarg); + + break; + case ARG_QEMU_GUI: - arg_qemu_gui = true; + arg_console_mode = CONSOLE_GUI; break; case 'n': @@ -438,6 +454,12 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_BACKGROUND: + r = free_and_strdup_warn(&arg_background, optarg); + if (r < 0) + return r; + break; + case '?': return -EINVAL; @@ -1030,6 +1052,25 @@ static int merge_initrds(char **ret) { return 0; } +static void set_window_title(PTYForward *f) { + _cleanup_free_ char *hn = NULL, *dot = NULL; + + assert(f); + + (void) gethostname_strict(&hn); + + if (emoji_enabled()) + dot = strjoin(special_glyph(SPECIAL_GLYPH_GREEN_CIRCLE), " "); + + if (hn) + (void) pty_forward_set_titlef(f, "%sVirtual Machine %s on %s", strempty(dot), arg_machine, hn); + else + (void) pty_forward_set_titlef(f, "%sVirtual Machine %s", strempty(dot), arg_machine); + + if (dot) + (void) pty_forward_set_title_prefix(f, dot); +} + static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; @@ -1222,12 +1263,54 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return log_oom(); - if (arg_qemu_gui) + _cleanup_close_ int master = -EBADF; + PTYForwardFlags ptyfwd_flags = 0; + switch (arg_console_mode) { + + case CONSOLE_READ_ONLY: + ptyfwd_flags |= PTY_FORWARD_READ_ONLY; + + _fallthrough_; + + case CONSOLE_INTERACTIVE: { + _cleanup_free_ char *pty_path = NULL; + + master = posix_openpt(O_RDWR|O_NOCTTY|O_CLOEXEC|O_NONBLOCK); + if (master < 0) + return log_error_errno(errno, "Failed to acquire pseudo tty: %m"); + + r = ptsname_malloc(master, &pty_path); + if (r < 0) + return log_error_errno(r, "Failed to determine tty name: %m"); + + if (unlockpt(master) < 0) + return log_error_errno(errno, "Failed to unlock tty: %m"); + + if (strv_extend_many( + &cmdline, + "-nographic", + "-nodefaults", + "-chardev") < 0) + return log_oom(); + + if (strv_extendf(&cmdline, + "serial,id=console,path=%s", pty_path) < 0) + return log_oom(); + + r = strv_extend_many( + &cmdline, + "-serial", "chardev:console"); + break; + } + + case CONSOLE_GUI: r = strv_extend_many( &cmdline, "-vga", "virtio"); - else + break; + + case CONSOLE_NATIVE: r = strv_extend_many( &cmdline, "-nographic", @@ -1235,6 +1318,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { "-chardev", "stdio,mux=on,id=console,signal=off", "-serial", "chardev:console", "-mon", "console"); + break; + + default: + assert_not_reached(); + } if (r < 0) return log_oom(); @@ -1583,7 +1671,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { log_debug("Executing: %s", joined); } - assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + assert_se(sigprocmask_many(SIG_BLOCK, /* old_sigset=*/ NULL, SIGCHLD, SIGWINCH) >= 0); _cleanup_(sd_event_source_unrefp) sd_event_source *notify_event_source = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -1635,6 +1723,26 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { /* Exit when the child exits */ (void) event_add_child_pidref(event, NULL, &child_pidref, WEXITED, on_child_exit, NULL); + _cleanup_(pty_forward_freep) PTYForward *forward = NULL; + if (master >= 0) { + r = pty_forward_new(event, master, ptyfwd_flags, &forward); + if (r < 0) + return log_error_errno(r, "Failed to create PTY forwarder: %m"); + + if (!arg_background) { + _cleanup_free_ char *bg = NULL; + + r = terminal_tint_color(130 /* green */, &bg); + if (r < 0) + log_debug_errno(r, "Failed to determine terminal background color, not tinting."); + else + (void) pty_forward_set_background_color(forward, bg); + } else if (!isempty(arg_background)) + (void) pty_forward_set_background_color(forward, arg_background); + + set_window_title(forward); + } + r = sd_event_loop(event); if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); @@ -1740,15 +1848,20 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - if (!arg_quiet) { + if (!arg_quiet && arg_console_mode != CONSOLE_GUI) { _cleanup_free_ char *u = NULL; const char *vm_path = arg_image ?: arg_directory; (void) terminal_urlify_path(vm_path, vm_path, &u); - log_info("%s %sSpawning VM %s on %s.%s\n" - "%s %sPress %sCtrl-a x%s to kill VM.%s", - special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: vm_path, ansi_normal(), - special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal()); + log_info("%s %sSpawning VM %s on %s.%s", + special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: vm_path, ansi_normal()); + + if (arg_console_mode == CONSOLE_INTERACTIVE) + log_info("%s %sPress %sCtrl-]%s three times within 1s to kill VM.%s", + special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal()); + else if (arg_console_mode == CONSOLE_NATIVE) + log_info("%s %sPress %sCtrl-a x%s to kill VM.%s", + special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal()); } r = sd_listen_fds_with_names(true, &names);