Introduce systemd-pty-forward (#35761)

This allows running a command with tinted terminal background.
This commit is contained in:
Daan De Meyer
2025-01-03 19:34:42 +01:00
committed by GitHub
13 changed files with 327 additions and 6 deletions

View File

@@ -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'],

View File

@@ -0,0 +1,81 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-pty-forward"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-pty-forward</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-pty-forward</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-pty-forward</refname>
<refpurpose>Run a command with a custom terminal background color or title</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>systemd-pty-forward</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="req">COMMAND</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-pty-forward</command> can be used to run a command with a custom terminal
background color or title.</para>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--background=<replaceable>COLOR</replaceable></option></term>
<listitem><para>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
<literal>40</literal>, <literal>41</literal>, …, <literal>47</literal>, <literal>48;2;…</literal>,
<literal>48;5;…</literal>. See <ulink
url="https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters">ANSI
Escape Code (Wikipedia)</ulink> for details.</para>
<para>Example: <literal>--background=44</literal> for a blue background.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--title=<replaceable>TITLE</replaceable></option></term>
<listitem><para>Change the terminal title to the specified string as long as the command runs.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--quiet</option></term>
<term><option>-q</option></term>
<listitem><para>Suppresses additional informational output while running.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help"/>
<xi:include href="standard-options.xml" xpointer="version"/>
</variablelist>
</refsect1>
</refentry>

View File

@@ -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')

View File

@@ -9,9 +9,11 @@ ToolsTreePackages=
github-cli
libcap
libmicrohttpd
libxslt
mypy
perl-json-xs
python-jinja
python-lxml
python-pytest
ruff
shellcheck

View File

@@ -15,6 +15,9 @@ ToolsTreePackages=
pkgconfig(mount)
tpm2-tss-devel
python3-jinja2
python3-lxml
python3-mypy
python3-pytest
shellcheck
xsltproc
docbook-style-xsl

View File

@@ -17,5 +17,7 @@ ToolsTreePackages=
libtss2-dev
mypy
python3-jinja2
python3-lxml
python3-pytest
shellcheck
xsltproc

View File

@@ -19,5 +19,7 @@ ToolsTreePackages=
python3-ruff
tpm2-0-tss-devel
python3-jinja2
python3-lxml
python3-pytest
xsltproc
ShellCheck

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

9
src/ptyfwd/meson.build Normal file
View File

@@ -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'),
},
]

214
src/ptyfwd/ptyfwd-tool.c Normal file
View File

@@ -0,0 +1,214 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include <unistd.h>
#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);

View File

@@ -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