diff --git a/man/rules/meson.build b/man/rules/meson.build index df4af1e543..d281842396 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1052,6 +1052,7 @@ manpages = [ 'systemd-shutdown'], ''], ['systemd-pstore.service', '8', ['systemd-pstore'], 'ENABLE_PSTORE'], + ['systemd-pty-forward', '1', [], ''], ['systemd-quotacheck.service', '8', ['systemd-quotacheck'], diff --git a/man/systemd-pty-forward.xml b/man/systemd-pty-forward.xml new file mode 100644 index 0000000000..84e73fc161 --- /dev/null +++ b/man/systemd-pty-forward.xml @@ -0,0 +1,81 @@ + + + + + + + systemd-pty-forward + systemd + + + + systemd-pty-forward + 1 + + + + systemd-pty-forward + Run a command with a custom terminal background color or title + + + + + systemd-pty-forward + OPTIONS + COMMAND + + + + + Description + + systemd-pty-forward can be used to run a command with a custom terminal + background color or title. + + + + Options + The following options are understood: + + + + + + Change the terminal background color to the specified ANSI color as long as the + command 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. + + Example: --background=44 for a blue background. + + + + + + + + + Change the terminal title to the specified string as long as the command runs. + + + + + + + + + + Suppresses additional informational output while running. + + + + + + + + + diff --git a/meson.build b/meson.build index 538e776ab4..933133e6b1 100644 --- a/meson.build +++ b/meson.build @@ -2402,6 +2402,7 @@ subdir('src/pcrextend') subdir('src/pcrlock') subdir('src/portable') subdir('src/pstore') +subdir('src/ptyfwd') subdir('src/quotacheck') subdir('src/random-seed') subdir('src/rc-local-generator') diff --git a/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf b/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf index 5787aa8f44..c6ec83a6b0 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf.d/arch.conf @@ -9,9 +9,11 @@ ToolsTreePackages= github-cli libcap libmicrohttpd + libxslt mypy perl-json-xs python-jinja + python-lxml python-pytest ruff shellcheck diff --git a/mkosi.conf.d/05-tools/mkosi.conf.d/centos-fedora.conf b/mkosi.conf.d/05-tools/mkosi.conf.d/centos-fedora.conf index 23731f7376..21d3741504 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf.d/centos-fedora.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf.d/centos-fedora.conf @@ -15,6 +15,9 @@ ToolsTreePackages= pkgconfig(mount) tpm2-tss-devel python3-jinja2 + python3-lxml python3-mypy python3-pytest shellcheck + xsltproc + docbook-style-xsl diff --git a/mkosi.conf.d/05-tools/mkosi.conf.d/debian-ubuntu.conf b/mkosi.conf.d/05-tools/mkosi.conf.d/debian-ubuntu.conf index 46204fe8f6..106072f541 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf.d/debian-ubuntu.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf.d/debian-ubuntu.conf @@ -17,5 +17,7 @@ ToolsTreePackages= libtss2-dev mypy python3-jinja2 + python3-lxml python3-pytest shellcheck + xsltproc diff --git a/mkosi.conf.d/05-tools/mkosi.conf.d/opensuse.conf b/mkosi.conf.d/05-tools/mkosi.conf.d/opensuse.conf index d486875fab..8a659360fa 100644 --- a/mkosi.conf.d/05-tools/mkosi.conf.d/opensuse.conf +++ b/mkosi.conf.d/05-tools/mkosi.conf.d/opensuse.conf @@ -19,5 +19,7 @@ ToolsTreePackages= python3-ruff tpm2-0-tss-devel python3-jinja2 + python3-lxml python3-pytest + xsltproc ShellCheck diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 82df771cec..833c6617de 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -142,8 +142,8 @@ int getttyname_harder(int fd, char **ret); int ptsname_malloc(int fd, char **ret); -int openpt_allocate(int flags, char **ret_slave); -int openpt_allocate_in_namespace(pid_t pid, int flags, char **ret_slave); +int openpt_allocate(int flags, char **ret_peer); +int openpt_allocate_in_namespace(pid_t pid, int flags, char **ret_peer); int open_terminal_in_namespace(pid_t pid, const char *name, int mode); int vt_restore(int fd); diff --git a/src/machine/machine.c b/src/machine/machine.c index 9edea3dfa4..d94805c418 100644 --- a/src/machine/machine.c +++ b/src/machine/machine.c @@ -668,19 +668,19 @@ int machine_kill(Machine *m, KillWhom whom, int signo) { return manager_kill_unit(m->manager, m->unit, signo, NULL); } -int machine_openpt(Machine *m, int flags, char **ret_slave) { +int machine_openpt(Machine *m, int flags, char **ret_peer) { assert(m); switch (m->class) { case MACHINE_HOST: - return openpt_allocate(flags, ret_slave); + return openpt_allocate(flags, ret_peer); case MACHINE_CONTAINER: if (!pidref_is_set(&m->leader)) return -EINVAL; - return openpt_allocate_in_namespace(m->leader.pid, flags, ret_slave); + return openpt_allocate_in_namespace(m->leader.pid, flags, ret_peer); default: return -EOPNOTSUPP; diff --git a/src/machine/machine.h b/src/machine/machine.h index bf265143f0..30e4a6a466 100644 --- a/src/machine/machine.h +++ b/src/machine/machine.h @@ -100,7 +100,7 @@ MachineState machine_state_from_string(const char *s) _pure_; const char* kill_whom_to_string(KillWhom k) _const_; KillWhom kill_whom_from_string(const char *s) _pure_; -int machine_openpt(Machine *m, int flags, char **ret_slave); +int machine_openpt(Machine *m, int flags, char **ret_peer); int machine_open_terminal(Machine *m, const char *path, int mode); int machine_start_getty(Machine *m, const char *ptmx_name, sd_bus_error *error); int machine_start_shell(Machine *m, int ptmx_fd, const char *ptmx_name, const char *user, const char *path, char **args, char **env, sd_bus_error *error); diff --git a/src/ptyfwd/meson.build b/src/ptyfwd/meson.build new file mode 100644 index 0000000000..f615285933 --- /dev/null +++ b/src/ptyfwd/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-pty-forward', + 'public' : true, + 'sources' : files('ptyfwd-tool.c'), + }, +] diff --git a/src/ptyfwd/ptyfwd-tool.c b/src/ptyfwd/ptyfwd-tool.c new file mode 100644 index 0000000000..faa4071fac --- /dev/null +++ b/src/ptyfwd/ptyfwd-tool.c @@ -0,0 +1,214 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "build.h" +#include "event-util.h" +#include "fd-util.h" +#include "main-func.h" +#include "pretty-print.h" +#include "ptyfwd.h" +#include "strv.h" + +static bool arg_quiet = false; +static char *arg_background = NULL; +static char *arg_title = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_background, freep); +STATIC_DESTRUCTOR_REGISTER(arg_title, freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-pty-forward", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sRun command with a custom terminal background color or title.%6$s\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " -q --quiet Suppress information messages during runtime\n" + " --background=COLOR Set ANSI color for background\n" + " --title=TITLE Set terminal title\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_BACKGROUND, + ARG_TITLE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "quiet", no_argument, NULL, 'q' }, + { "background", required_argument, NULL, ARG_BACKGROUND }, + { "title", required_argument, NULL, ARG_TITLE }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + optind = 0; + while ((c = getopt_long(argc, argv, "+hq", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case 'q': + arg_quiet = true; + break; + + case ARG_BACKGROUND: + r = free_and_strdup_warn(&arg_background, optarg); + if (r < 0) + return r; + break; + + case ARG_TITLE: + r = free_and_strdup_warn(&arg_title, optarg); + if (r < 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int pty_forward_handler(PTYForward *f, int rcode, void *userdata) { + sd_event *e = ASSERT_PTR(userdata); + + assert(f); + + if (rcode == -ECANCELED) { + log_debug_errno(rcode, "PTY forwarder disconnected."); + return sd_event_exit(e, EXIT_SUCCESS); + } else if (rcode < 0) { + (void) sd_event_exit(e, EXIT_FAILURE); + return log_error_errno(rcode, "Error on PTY forwarding logic: %m"); + } + + return 0; +} + +static int helper_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + /* Add 128 to signal exit statuses to mimick shells. */ + return sd_event_exit(sd_event_source_get_event(s), si->si_status + (si->si_code == CLD_EXITED ? 0 : 128)); +} + +static int run(int argc, char *argv[]) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_close_ int pty_fd = -EBADF, peer_fd = -EBADF; + _cleanup_(pty_forward_freep) PTYForward *forward = NULL; + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *exit_source = NULL; + int r; + + log_setup(); + + assert_se(sigprocmask_many(SIG_BLOCK, /*ret_old_mask=*/ NULL, SIGCHLD) >= 0); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + _cleanup_strv_free_ char **l = strv_copy(argv + optind); + if (!l) + return log_oom(); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + (void) sd_event_set_signal_exit(event, true); + + pty_fd = openpt_allocate(O_RDWR|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, /*ret_peer=*/ NULL); + if (pty_fd < 0) + return log_error_errno(pty_fd, "Failed to acquire pseudo tty: %m"); + + peer_fd = pty_open_peer(pty_fd, O_RDWR|O_NOCTTY|O_CLOEXEC); + if (peer_fd < 0) + return log_error_errno(peer_fd, "Failed to open pty peer: %m"); + + if (!arg_quiet) + log_info("Press ^] three times within 1s to disconnect TTY."); + + r = pty_forward_new(event, pty_fd, /*flags=*/ 0, &forward); + if (r < 0) + return log_error_errno(r, "Failed to create PTY forwarder: %m"); + + if (!isempty(arg_background)) { + r = pty_forward_set_background_color(forward, arg_background); + if (r < 0) + return log_error_errno(r, "Failed to set background color: %m"); + } + + if (shall_set_terminal_title() && !isempty(arg_title)) { + r = pty_forward_set_title(forward, arg_title); + if (r < 0) + return log_error_errno(r, "Failed to set title: %m"); + } + + pty_forward_set_handler(forward, pty_forward_handler, &event); + + r = pidref_safe_fork_full( + "(sd-ptyfwd)", + (int[]) { peer_fd, peer_fd, peer_fd }, + /* except_fds= */ NULL, + /* n_except_fds= */ 0, + /* flags= */ FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REARRANGE_STDIO, + &pidref); + if (r < 0) + return log_error_errno(r, "Failed to fork child: %m"); + if (r == 0) { + r = terminal_new_session(); + if (r < 0) + return log_error_errno(r, "Failed to create new session: %m"); + + (void) execvp(l[0], l); + log_error_errno(errno, "Failed to execute %s: %m", l[0]); + _exit(EXIT_FAILURE); + } + + peer_fd = safe_close(peer_fd); + + r = event_add_child_pidref(event, &exit_source, &pidref, WEXITED, helper_on_exit, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add child event source: %m"); + + r = sd_event_source_set_child_process_own(exit_source, true); + if (r < 0) + return log_error_errno(r, "Failed to take ownership of child process: %m"); + + return sd_event_loop(event); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/test/units/TEST-74-AUX-UTILS.pty-forward.sh b/test/units/TEST-74-AUX-UTILS.pty-forward.sh new file mode 100755 index 0000000000..9b9b14a18a --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.pty-forward.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +systemd-pty-forward --background 41 --title test echo foobar