diff --git a/TODO b/TODO
index e480f70ef7..4d2fd411dc 100644
--- a/TODO
+++ b/TODO
@@ -132,6 +132,10 @@ Deprecations and removals:
Features:
+* add a new ExecStart= flag that inserts the configured user's shell as first
+ word in the comand line. (maybe use character '.'). Usecase: tool such as
+ uid0 can use that to spawn the target user's default shell.
+
* varlink: figure out how to do docs for our varlink interfaces. Idea: install
interface files augmented with docs in /usr/share/ somewhere. And have
functionality in varlinkctl to merge interface info extracted from binaries
@@ -140,14 +144,6 @@ Features:
* introduce mntid_t, and make it 64bit, as apparently the kernel switched to
64bit mount ids
-* Add an alias to systemd-run maybe called "uid0" or so, which tries to mimic
- the sudo/su command lines to some level, but is backed by transient services,
- and proper security isolate/tty forwarding. This would then allow us to run
- systems with NNP turned on reasonably nicely. To make this extra nice and
- pretty, in ptyfwd rewrite every NL we pass through so that it resets the bg
- color to some reddish tone, and erase the whole coming line first, so that
- the background color indicates when operating with privileges and when not.
-
* use udev rule networkd ownership property to take ownership of network
interfaces nspawn creates
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 3d63cf1131..622921f8d6 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1257,6 +1257,7 @@ manpages = [
''],
['udev_new', '3', ['udev_ref', 'udev_unref'], ''],
['udevadm', '8', [], ''],
+ ['uid0', '1', [], ''],
['ukify', '1', [], 'ENABLE_UKIFY'],
['user@.service',
'5',
diff --git a/man/systemd-run.xml b/man/systemd-run.xml
index 5be9823c37..e6cf2a3bcb 100644
--- a/man/systemd-run.xml
+++ b/man/systemd-run.xml
@@ -176,11 +176,14 @@
Make the new .service or .scope unit part
- of the inherited slice. This option can be combined with .
+ of the slice the systemd-run itself has been invoked in. This option may be
+ combined with , in which case the slice specified via
+ is placed within the slice the systemd-run command is
+ invoked in.
- An inherited slice is located within systemd-run slice. Example: if
- systemd-run slice is foo.slice, and the
- argument is bar, the unit will be placed under the
+ Example: consider systemd-run being invoked in the slice
+ foo.slice, and the argument is
+ bar. The unit will then be placed under
foo-bar.slice.
@@ -485,6 +488,31 @@
+
+
+
+ By default, if the specified command fails the invoked unit will be marked failed
+ (though possibly still unloaded, see , above), and this is reported in the
+ logs. If this switch is specified this is suppressed and any non-success exit status/code of the
+ command is treated as success.
+
+
+
+
+
+
+
+ Change the terminal background color to the specified ANSI color as long as the
+ session lasts. 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.
+
+
+
+
+
@@ -677,7 +705,8 @@ $ systemd-run --user --wait -p SuccessExitStatus=SIGUSR1 --expand-environment=no
systemd.resource-control5,
systemd.timer5,
systemd-mount1,
- machinectl1
+ machinectl1,
+ uid01
diff --git a/man/uid0.xml b/man/uid0.xml
new file mode 100644
index 0000000000..0d36ca77a4
--- /dev/null
+++ b/man/uid0.xml
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+ uid0
+ systemd
+
+
+
+ uid0
+ 1
+
+
+
+ uid0
+ Elevate privileges
+
+
+
+
+ uid0
+ OPTIONS
+ COMMAND
+
+
+
+
+ Description
+
+ uid0 may be used to temporarily and interactively acquire elavated or different
+ privileges. It serves a similar purpose as sudo8, but
+ operates differently in a couple of key areas:
+
+
+ No execution or security context credentials are inherited from the caller into the
+ invoked commands, as they are invoked from a fresh, isolated service forked off the service
+ manager.
+
+ Authentication takes place via polkit, thus isolating the
+ authentication prompt from the terminal (if possible).
+
+ An independent pseudo-tty is allocated for the invoked command, detaching its lifecycle and
+ isolating it for security.
+
+ No SetUID/SetGID file access bit functionality is used for the implementation.
+
+
+ Altogether this should provide a safer and more robust alternative to the sudo
+ mechanism, in particular in OS environments where SetUID/SetGID support is not available (for example by
+ setting the NoNewPrivileges= variable in
+ systemd-system.conf5).
+
+ Any session invoked via uid0 will run through the
+ systemd-uid0 PAM stack.
+
+ Note that uid0 is implemented as an alternative multi-call invocation of
+ systemd-run1.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+
+ Do not query the user for authentication for privileged operations.
+
+
+
+
+
+
+
+ Use this unit name instead of an automatically generated one.
+
+
+
+
+
+
+
+ Sets a property on the service unit that is created. This option takes an assignment
+ in the same format as
+ systemctl1's
+ set-property command.
+
+
+
+
+
+
+
+
+ Provide a description for the service unit that is invoked. If not specified,
+ the command itself will be used as a description. See Description= in
+ systemd.unit5.
+
+
+
+
+
+
+
+
+ Make the new .service unit part of the specified slice, instead
+ of user.slice.
+
+
+
+
+
+
+
+
+ Make the new .service unit part of the slice the
+ uid0 itself has been invoked in. This option may be combined with
+ , in which case the slice specified via is placed
+ within the slice the uid0 command is invoked in.
+
+ Example: consider uid0 being invoked in the slice
+ foo.slice, and the argument is
+ bar. The unit will then be placed under
+ foo-bar.slice.
+
+
+
+
+
+
+
+
+
+
+
+
+ Switches to the specified user/group instead of root.
+
+
+
+
+
+
+
+
+ Runs the invoked session with the specified nice level.
+
+
+
+
+
+
+
+
+
+ Runs the invoked session with the specified working directory. If not specified
+ defaults to the client's current working directory if switching to the root user, or the target
+ user's home directory otherwise.
+
+
+
+
+
+
+
+
+ Runs the invoked session with the specified environment variable set. This parameter
+ may be used more than once to set multiple variables. When = and
+ VALUE are omitted, the value of the variable with the same name in the
+ invoking environment will be used.
+
+
+
+
+
+
+
+
+ Change the terminal background color to the specified ANSI color as long as the
+ session lasts. If not specified, the background will be tinted in a reddish tone when operating as
+ root, and in a yellowish tone when operating under another UID, as reminder of the changed
+ privileges. 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. Set to an empty string to disable.
+
+ Example: --background=44 for a blue background.
+
+
+
+
+
+
+
+
+
+
+ All command line arguments after the first non-option argument become part of the command line of
+ the launched process. If no command line is specified an interactive shell is invoked. The shell to
+ invoke may be controlled via and currently defaults to the
+ originating user's shell (i.e. not the target user's!) if operating locally, or
+ /bin/sh when operating with .
+
+
+
+ Exit status
+
+ On success, 0 is returned. If uid0 failed to start the session or the specified command fails, a
+ non-zero return value will be returned.
+
+
+
+ See Also
+
+ systemd1,
+ systemd-run1,
+ sudo8,
+ machinectl1
+
+
+
+
diff --git a/mkosi.images/base/mkosi.build.chroot b/mkosi.images/base/mkosi.build.chroot
index b60ed8d731..9524fdb103 100755
--- a/mkosi.images/base/mkosi.build.chroot
+++ b/mkosi.images/base/mkosi.build.chroot
@@ -70,7 +70,7 @@ if [ ! -f "$BUILDDIR"/build.ninja ]; then
-D tests=unsafe
-D slow-tests="${SLOW_TESTS:-false}"
-D create-log-dirs=false
- -D pamconfdir=no
+ -D pamconfdir=/usr/lib/pam.d/
-D utmp=true
-D hibernate=true
-D ldconfig=true
diff --git a/src/basic/env-util.c b/src/basic/env-util.c
index d3bf73385f..7ac47732ba 100644
--- a/src/basic/env-util.c
+++ b/src/basic/env-util.c
@@ -458,6 +458,35 @@ int strv_env_assign(char ***l, const char *key, const char *value) {
return strv_env_replace_consume(l, p);
}
+int strv_env_assignf(char ***l, const char *key, const char *valuef, ...) {
+ int r;
+
+ assert(l);
+ assert(key);
+
+ if (!env_name_is_valid(key))
+ return -EINVAL;
+
+ if (!valuef) {
+ strv_env_unset(*l, key);
+ return 0;
+ }
+
+ _cleanup_free_ char *value = NULL;
+ va_list ap;
+ va_start(ap, valuef);
+ r = vasprintf(&value, valuef, ap);
+ va_end(ap);
+ if (r < 0)
+ return -ENOMEM;
+
+ char *p = strjoin(key, "=", value);
+ if (!p)
+ return -ENOMEM;
+
+ return strv_env_replace_consume(l, p);
+}
+
int _strv_env_assign_many(char ***l, ...) {
va_list ap;
int r;
diff --git a/src/basic/env-util.h b/src/basic/env-util.h
index f7fb1e9082..8e77cc71d6 100644
--- a/src/basic/env-util.h
+++ b/src/basic/env-util.h
@@ -49,6 +49,7 @@ int strv_env_replace_consume(char ***l, char *p); /* In place ... */
int strv_env_replace_strdup(char ***l, const char *assignment);
int strv_env_replace_strdup_passthrough(char ***l, const char *assignment);
int strv_env_assign(char ***l, const char *key, const char *value);
+int strv_env_assignf(char ***l, const char *key, const char *valuef, ...) _printf_(3, 4);
int _strv_env_assign_many(char ***l, ...) _sentinel_;
#define strv_env_assign_many(l, ...) _strv_env_assign_many(l, __VA_ARGS__, NULL)
diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c
index 3355b749cc..b740b49775 100644
--- a/src/basic/terminal-util.c
+++ b/src/basic/terminal-util.c
@@ -27,6 +27,7 @@
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
+#include "hexdecoct.h"
#include "inotify-util.h"
#include "io-util.h"
#include "log.h"
@@ -1551,3 +1552,218 @@ int set_terminal_cursor_position(int fd, unsigned int row, unsigned int column)
return 0;
}
+
+void termios_disable_echo(struct termios *termios) {
+ assert(termios);
+
+ termios->c_lflag &= ~(ICANON|ECHO);
+ termios->c_cc[VMIN] = 1;
+ termios->c_cc[VTIME] = 0;
+}
+
+typedef enum BackgroundColorState {
+ BACKGROUND_TEXT,
+ BACKGROUND_ESCAPE,
+ BACKGROUND_BRACKET,
+ BACKGROUND_FIRST_ONE,
+ BACKGROUND_SECOND_ONE,
+ BACKGROUND_SEMICOLON,
+ BACKGROUND_R,
+ BACKGROUND_G,
+ BACKGROUND_B,
+ BACKGROUND_RED,
+ BACKGROUND_GREEN,
+ BACKGROUND_BLUE,
+} BackgroundColorState;
+
+typedef struct BackgroundColorContext {
+ BackgroundColorState state;
+ uint32_t red, green, blue;
+ unsigned red_bits, green_bits, blue_bits;
+} BackgroundColorContext;
+
+static int scan_background_color_response(
+ BackgroundColorContext *context,
+ const char *buf,
+ size_t size) {
+
+ assert(context);
+ assert(buf || size == 0);
+
+ for (size_t i = 0; i < size; i++) {
+ char c = buf[i];
+
+ switch (context->state) {
+
+ case BACKGROUND_TEXT:
+ context->state = c == '\x1B' ? BACKGROUND_ESCAPE : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_ESCAPE:
+ context->state = c == ']' ? BACKGROUND_BRACKET : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_BRACKET:
+ context->state = c == '1' ? BACKGROUND_FIRST_ONE : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_FIRST_ONE:
+ context->state = c == '1' ? BACKGROUND_SECOND_ONE : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_SECOND_ONE:
+ context->state = c == ';' ? BACKGROUND_SEMICOLON : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_SEMICOLON:
+ context->state = c == 'r' ? BACKGROUND_R : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_R:
+ context->state = c == 'g' ? BACKGROUND_G : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_G:
+ context->state = c == 'b' ? BACKGROUND_B : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_B:
+ context->state = c == ':' ? BACKGROUND_RED : BACKGROUND_TEXT;
+ break;
+
+ case BACKGROUND_RED:
+ if (c == '/')
+ context->state = context->red_bits > 0 ? BACKGROUND_GREEN : BACKGROUND_TEXT;
+ else {
+ int d = unhexchar(c);
+ if (d < 0 || context->red_bits >= sizeof(context->red)*8)
+ context->state = BACKGROUND_TEXT;
+ else {
+ context->red = (context->red << 4) | d;
+ context->red_bits += 4;
+ }
+ }
+ break;
+
+ case BACKGROUND_GREEN:
+ if (c == '/')
+ context->state = context->green_bits > 0 ? BACKGROUND_BLUE : BACKGROUND_TEXT;
+ else {
+ int d = unhexchar(c);
+ if (d < 0 || context->green_bits >= sizeof(context->green)*8)
+ context->state = BACKGROUND_TEXT;
+ else {
+ context->green = (context->green << 4) | d;
+ context->green_bits += 4;
+ }
+ }
+ break;
+
+ case BACKGROUND_BLUE:
+ if (c == '\x07') {
+ if (context->blue_bits > 0)
+ return 1; /* success! */
+
+ context->state = BACKGROUND_TEXT;
+ } else {
+ int d = unhexchar(c);
+ if (d < 0 || context->blue_bits >= sizeof(context->blue)*8)
+ context->state = BACKGROUND_TEXT;
+ else {
+ context->blue = (context->blue << 4) | d;
+ context->blue_bits += 4;
+ }
+ }
+ break;
+ }
+
+ /* Reset any colors we might have picked up */
+ if (context->state == BACKGROUND_TEXT) {
+ /* reset color */
+ context->red = context->green = context->blue = 0;
+ context->red_bits = context->green_bits = context->blue_bits = 0;
+ }
+ }
+
+ return 0; /* all good, but not enough data yet */
+}
+
+int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue) {
+ int r;
+
+ assert(ret_red);
+ assert(ret_green);
+ assert(ret_blue);
+
+ if (!colors_enabled())
+ return -EOPNOTSUPP;
+
+ if (isatty(STDOUT_FILENO) < 1 || isatty(STDIN_FILENO) < 1)
+ return -EOPNOTSUPP;
+
+ if (streq_ptr(getenv("TERM"), "linux")) {
+ /* Linux console is black */
+ *ret_red = *ret_green = *ret_blue = 0.0;
+ return 0;
+ }
+
+ struct termios old_termios;
+ if (tcgetattr(STDIN_FILENO, &old_termios) < 0)
+ return -errno;
+
+ struct termios new_termios = old_termios;
+ termios_disable_echo(&new_termios);
+
+ if (tcsetattr(STDOUT_FILENO, TCSADRAIN, &new_termios) < 0)
+ return -errno;
+
+ r = loop_write(STDOUT_FILENO, "\x1B]11;?\x07", SIZE_MAX);
+ if (r < 0)
+ goto finish;
+
+ usec_t end = usec_add(now(CLOCK_MONOTONIC), 100 * USEC_PER_MSEC);
+ char buf[256];
+ size_t buf_full = 0;
+ BackgroundColorContext context = {};
+
+ for (;;) {
+ usec_t n = now(CLOCK_MONOTONIC);
+
+ if (n >= end) {
+ r = -EOPNOTSUPP;
+ goto finish;
+ }
+
+ r = fd_wait_for_event(STDIN_FILENO, POLLIN, usec_sub_unsigned(end, n));
+ if (r < 0)
+ goto finish;
+
+ ssize_t l;
+ l = read(STDIN_FILENO, buf, sizeof(buf) - buf_full);
+ if (l < 0) {
+ r = -errno;
+ goto finish;
+ }
+
+ buf_full += l;
+ assert(buf_full <= sizeof(buf));
+
+ r = scan_background_color_response(&context, buf, buf_full);
+ if (r < 0)
+ goto finish;
+ if (r > 0) {
+ assert(context.red_bits > 0);
+ *ret_red = (double) context.red / ((UINT64_C(1) << context.red_bits) - 1);
+ assert(context.green_bits > 0);
+ *ret_green = (double) context.green / ((UINT64_C(1) << context.green_bits) - 1);
+ assert(context.blue_bits > 0);
+ *ret_blue = (double) context.blue / ((UINT64_C(1) << context.blue_bits) - 1);
+ r = 0;
+ goto finish;
+ }
+ }
+
+finish:
+ (void) tcsetattr(STDOUT_FILENO, TCSADRAIN, &old_termios);
+ return r;
+}
diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h
index cae42887c4..1ec057c2dd 100644
--- a/src/basic/terminal-util.h
+++ b/src/basic/terminal-util.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include "macro.h"
#include "time-util.h"
@@ -168,7 +169,6 @@ bool underline_enabled(void);
bool dev_console_colors_enabled(void);
static inline bool colors_enabled(void) {
-
/* Returns true if colors are considered supported on our stdout. */
return get_color_mode() != COLOR_OFF;
}
@@ -286,3 +286,7 @@ static inline const char* ansi_highlight_green_red(bool b) {
/* This assumes there is a 'tty' group */
#define TTY_MODE 0620
+
+void termios_disable_echo(struct termios *termios);
+
+int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue);
diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c
index 5bac04519b..23bdab0bef 100644
--- a/src/pcrlock/pcrlock.c
+++ b/src/pcrlock/pcrlock.c
@@ -11,6 +11,7 @@
#include "blockdev-util.h"
#include "build.h"
#include "chase.h"
+#include "color-util.h"
#include "conf-files.h"
#include "efi-api.h"
#include "env-util.h"
@@ -1932,40 +1933,6 @@ static int event_log_map_components(EventLog *el) {
return event_log_validate_fully_recognized(el);
}
-static void hsv_to_rgb(
- double h, double s, double v,
- uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) {
-
- double c, x, m, r, g, b;
-
- assert(s >= 0 && s <= 100);
- assert(v >= 0 && v <= 100);
- assert(ret_r);
- assert(ret_g);
- assert(ret_b);
-
- c = (s / 100.0) * (v / 100.0);
- x = c * (1 - fabs(fmod(h / 60.0, 2) - 1));
- m = (v / 100) - c;
-
- if (h >= 0 && h < 60)
- r = c, g = x, b = 0.0;
- else if (h >= 60 && h < 120)
- r = x, g = c, b = 0.0;
- else if (h >= 120 && h < 180)
- r = 0.0, g = c, b = x;
- else if (h >= 180 && h < 240)
- r = 0.0, g = x, b = c;
- else if (h >= 240 && h < 300)
- r = x, g = 0.0, b = c;
- else
- r = c, g = 0.0, b = x;
-
- *ret_r = (uint8_t) ((r + m) * 255);
- *ret_g = (uint8_t) ((g + m) * 255);
- *ret_b = (uint8_t) ((b + m) * 255);
-}
-
#define ANSI_TRUE_COLOR_MAX (7U + 3U + 1U + 3U + 1U + 3U + 2U)
static const char *ansi_true_color(uint8_t r, uint8_t g, uint8_t b, char ret[static ANSI_TRUE_COLOR_MAX]) {
diff --git a/src/run/meson.build b/src/run/meson.build
index 597a25abeb..95336b86fe 100644
--- a/src/run/meson.build
+++ b/src/run/meson.build
@@ -7,3 +7,17 @@ executables += [
'sources' : files('run.c'),
},
]
+
+install_emptydir(bindir)
+
+meson.add_install_script(sh, '-c',
+ ln_s.format(bindir / 'systemd-run',
+ bindir / 'uid0'))
+
+custom_target(
+ 'systemd-uid0',
+ input : 'systemd-uid0.in',
+ output : 'systemd-uid0',
+ command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
+ install : pamconfdir != 'no',
+ install_dir : pamconfdir)
diff --git a/src/run/run.c b/src/run/run.c
index 06c00cbf56..45da75f889 100644
--- a/src/run/run.c
+++ b/src/run/run.c
@@ -17,6 +17,7 @@
#include "bus-unit-util.h"
#include "bus-wait-for-jobs.h"
#include "calendarspec.h"
+#include "color-util.h"
#include "env-util.h"
#include "escape.h"
#include "exit-status.h"
@@ -73,6 +74,9 @@ static bool arg_aggressive_gc = false;
static char *arg_working_directory = NULL;
static bool arg_shell = false;
static char **arg_cmdline = NULL;
+static char *arg_exec_path = NULL;
+static bool arg_ignore_failure = false;
+static char *arg_background = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_description, freep);
STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep);
@@ -82,6 +86,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_socket_property, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_timer_property, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_working_directory, freep);
STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_exec_path, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
static int help(void) {
_cleanup_free_ char *link = NULL;
@@ -104,7 +110,7 @@ static int help(void) {
" -p --property=NAME=VALUE Set service or scope unit property\n"
" --description=TEXT Description for unit\n"
" --slice=SLICE Run in the specified slice\n"
- " --slice-inherit Inherit the slice\n"
+ " --slice-inherit Inherit the slice from the caller\n"
" --expand-environment=BOOL Control expansion of environment variables\n"
" --no-block Do not wait until operation finished\n"
" -r --remain-after-exit Leave service around until explicitly stopped\n"
@@ -123,6 +129,8 @@ static int help(void) {
" -q --quiet Suppress information messages during runtime\n"
" -G --collect Unload unit after it ran, even when failed\n"
" -S --shell Invoke a $SHELL interactively\n"
+ " --ignore-failure Ignore the exit status of the invoked process\n"
+ " --background=COLOR Set ANSI color for background\n"
"\n%3$sPath options:%4$s\n"
" --path-property=NAME=VALUE Set path unit property\n"
"\n%3$sSocket options:%4$s\n"
@@ -146,6 +154,40 @@ static int help(void) {
return 0;
}
+static int help_sudo_mode(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("uid0", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND [ARGUMENTS...]\n"
+ "\n%sElevate privileges interactively.%s\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " --no-ask-password Do not prompt for password\n"
+ " --machine=CONTAINER Operate on local container\n"
+ " --unit=UNIT Run under the specified unit name\n"
+ " --property=NAME=VALUE Set service or scope unit property\n"
+ " --description=TEXT Description for unit\n"
+ " --slice=SLICE Run in the specified slice\n"
+ " --slice-inherit Inherit the slice\n"
+ " -u --user=USER Run as system user\n"
+ " -g --group=GROUP Run as system group\n"
+ " --nice=NICE Nice level\n"
+ " -D --chdir=PATH Set working directory\n"
+ " --setenv=NAME[=VALUE] Set environment variable\n"
+ " --background=COLOR Set ANSI color for background\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
static int add_timer_property(const char *name, const char *val) {
char *p;
@@ -162,6 +204,18 @@ static int add_timer_property(const char *name, const char *val) {
return 0;
}
+static char **make_login_shell_cmdline(const char *shell) {
+ _cleanup_free_ char *argv0 = NULL;
+
+ assert(shell);
+
+ argv0 = strjoin("-", shell); /* The - is how shells determine if they shall be consider login shells */
+ if (!argv0)
+ return NULL;
+
+ return strv_new(argv0);
+}
+
static int parse_argv(int argc, char *argv[]) {
enum {
@@ -194,6 +248,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_WAIT,
ARG_WORKING_DIRECTORY,
ARG_SHELL,
+ ARG_IGNORE_FAILURE,
+ ARG_BACKGROUND,
};
static const struct option options[] = {
@@ -239,6 +295,8 @@ static int parse_argv(int argc, char *argv[]) {
{ "working-directory", required_argument, NULL, ARG_WORKING_DIRECTORY },
{ "same-dir", no_argument, NULL, 'd' },
{ "shell", no_argument, NULL, 'S' },
+ { "ignore-failure", no_argument, NULL, ARG_IGNORE_FAILURE },
+ { "background", no_argument, NULL, ARG_BACKGROUND },
{},
};
@@ -282,7 +340,7 @@ static int parse_argv(int argc, char *argv[]) {
break;
case ARG_DESCRIPTION:
- r = free_and_strdup(&arg_description, optarg);
+ r = free_and_strdup_warn(&arg_description, optarg);
if (r < 0)
return r;
break;
@@ -524,6 +582,16 @@ static int parse_argv(int argc, char *argv[]) {
arg_shell = true;
break;
+ case ARG_IGNORE_FAILURE:
+ arg_ignore_failure = true;
+ break;
+
+ case ARG_BACKGROUND:
+ r = free_and_strdup_warn(&arg_background, optarg);
+ if (r < 0)
+ return r;
+ break;
+
case '?':
return -EINVAL;
@@ -651,6 +719,259 @@ static int parse_argv(int argc, char *argv[]) {
return 1;
}
+static int parse_argv_sudo_mode(int argc, char *argv[]) {
+
+ enum {
+ ARG_NO_ASK_PASSWORD = 0x100,
+ ARG_HOST,
+ ARG_MACHINE,
+ ARG_UNIT,
+ ARG_PROPERTY,
+ ARG_DESCRIPTION,
+ ARG_SLICE,
+ ARG_SLICE_INHERIT,
+ ARG_NICE,
+ ARG_SETENV,
+ ARG_BACKGROUND,
+ };
+
+ /* If invoked as "uid0" binary, let's expose a more sudo-like interface. We add various extensions
+ * though (but limit the extension to long options). */
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
+ { "machine", required_argument, NULL, ARG_MACHINE },
+ { "unit", required_argument, NULL, ARG_UNIT },
+ { "property", required_argument, NULL, ARG_PROPERTY },
+ { "description", required_argument, NULL, ARG_DESCRIPTION },
+ { "slice", required_argument, NULL, ARG_SLICE },
+ { "slice-inherit", no_argument, NULL, ARG_SLICE_INHERIT },
+ { "user", required_argument, NULL, 'u' },
+ { "group", required_argument, NULL, 'g' },
+ { "nice", required_argument, NULL, ARG_NICE },
+ { "chdir", required_argument, NULL, 'D' },
+ { "setenv", required_argument, NULL, ARG_SETENV },
+ { "background", required_argument, NULL, ARG_BACKGROUND },
+ {},
+ };
+
+ int r, c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
+ * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
+ optind = 0;
+ while ((c = getopt_long(argc, argv, "+hVu:g:D:", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help_sudo_mode();
+
+ case 'V':
+ return version();
+
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ case ARG_MACHINE:
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case ARG_UNIT:
+ arg_unit = optarg;
+ break;
+
+ case ARG_PROPERTY:
+ if (strv_extend(&arg_property, optarg) < 0)
+ return log_oom();
+
+ break;
+
+ case ARG_DESCRIPTION:
+ r = free_and_strdup_warn(&arg_description, optarg);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_SLICE:
+ arg_slice = optarg;
+ break;
+
+ case ARG_SLICE_INHERIT:
+ arg_slice_inherit = true;
+ break;
+
+ case 'u':
+ arg_exec_user = optarg;
+ break;
+
+ case 'g':
+ arg_exec_group = optarg;
+ break;
+
+ case ARG_NICE:
+ r = parse_nice(optarg, &arg_nice);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse nice value: %s", optarg);
+
+ arg_nice_set = true;
+ break;
+
+ case 'D':
+ r = parse_path_argument(optarg, true, &arg_working_directory);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_SETENV:
+ r = strv_env_replace_strdup_passthrough(&arg_environment, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg);
+
+ break;
+
+ case ARG_BACKGROUND:
+ r = free_and_strdup_warn(&arg_background, optarg);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (!arg_working_directory) {
+ if (arg_exec_user) {
+ /* When switching to a specific user, also switch to its home directory. */
+ arg_working_directory = strdup("~");
+ if (!arg_working_directory)
+ return log_oom();
+ } else {
+ /* When switching to root without this being specified, then stay in the current directory */
+ r = safe_getcwd(&arg_working_directory);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get current working directory: %m");
+ }
+ }
+
+ arg_service_type = "exec";
+ arg_quiet = true;
+ arg_wait = true;
+ arg_aggressive_gc = true;
+
+ arg_stdio = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) && isatty(STDERR_FILENO) ? ARG_STDIO_PTY : ARG_STDIO_DIRECT;
+ arg_expand_environment = false;
+ arg_send_sighup = true;
+
+ _cleanup_strv_free_ char **l = NULL;
+ if (argc > optind)
+ l = strv_copy(argv + optind);
+ else {
+ const char *e;
+
+ e = strv_env_get(arg_environment, "SHELL");
+ if (e)
+ arg_exec_path = strdup(e);
+ else {
+ if (arg_transport == BUS_TRANSPORT_LOCAL) {
+ r = get_shell(&arg_exec_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine shell: %m");
+ } else
+ arg_exec_path = strdup("/bin/sh");
+ }
+ if (!arg_exec_path)
+ return log_oom();
+
+ l = make_login_shell_cmdline(arg_exec_path);
+ }
+ if (!l)
+ return log_oom();
+
+ strv_free_and_replace(arg_cmdline, l);
+
+ if (!arg_slice) {
+ arg_slice = strdup("user.slice");
+ if (!arg_slice)
+ return log_oom();
+ }
+
+ _cleanup_free_ char *un = NULL;
+ un = getusername_malloc();
+ if (!un)
+ return log_oom();
+
+ /* Set a bunch of environment variables in a roughly sudo-compatible way */
+ r = strv_env_assign(&arg_environment, "SUDO_USER", un);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set $SUDO_USER environment variable: %m");
+
+ r = strv_env_assignf(&arg_environment, "SUDO_UID", UID_FMT, getuid());
+ if (r < 0)
+ return log_error_errno(r, "Failed to set $SUDO_UID environment variable: %m");
+
+ r = strv_env_assignf(&arg_environment, "SUDO_GID", GID_FMT, getgid());
+ if (r < 0)
+ return log_error_errno(r, "Failed to set $SUDO_GID environment variable: %m");
+
+ if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_UID=" UID_FMT, getuid()) < 0)
+ return log_oom();
+
+ if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_GID=" GID_FMT, getgid()) < 0)
+ return log_oom();
+
+ if (strv_extendf(&arg_property, "LogExtraFields=ELEVATED_USER=%s", un) < 0)
+ return log_oom();
+
+ if (strv_extend(&arg_property, "PAMName=systemd-uid0") < 0)
+ return log_oom();
+
+ if (!arg_background && arg_stdio == ARG_STDIO_PTY) {
+ double red, green, blue;
+
+ r = get_default_background_color(&red, &green, &blue);
+ if (r < 0)
+ log_debug_errno(r, "Unable to get terminal background color, not tinting background: %m");
+ else {
+ double h, s, v;
+
+ rgb_to_hsv(red, green, blue, &h, &s, &v);
+
+ if (!arg_exec_user || STR_IN_SET(arg_exec_user, "root", "0"))
+ h = 0; /* red */
+ else
+ h = 60 /* yellow */;
+
+ if (v > 50) /* If the background is bright, then pull down saturation */
+ s = 25;
+ else /* otherwise pump it up */
+ s = 75;
+
+ v = MAX(30, v); /* Make sure we don't hide the color in black */
+
+ uint8_t r8, g8, b8;
+ hsv_to_rgb(h, s, v, &r8, &g8, &b8);
+
+ if (asprintf(&arg_background, "48;2;%u;%u;%u", r8, g8, b8) < 0)
+ return log_oom();
+ }
+ }
+
+ return 1;
+}
+
static int transient_unit_set_properties(sd_bus_message *m, UnitType t, char **properties) {
int r;
@@ -899,7 +1220,7 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p
if (r < 0)
return bus_log_create_error(r);
- r = sd_bus_message_append(m, "s", arg_cmdline[0]);
+ r = sd_bus_message_append(m, "s", arg_exec_path ?: arg_cmdline[0]);
if (r < 0)
return bus_log_create_error(r);
@@ -910,9 +1231,10 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p
if (use_ex_prop)
r = sd_bus_message_append_strv(
m,
- STRV_MAKE(arg_expand_environment > 0 ? NULL : "no-env-expand"));
+ STRV_MAKE(arg_expand_environment > 0 ? NULL : "no-env-expand",
+ arg_ignore_failure ? "ignore-failure" : NULL));
else
- r = sd_bus_message_append(m, "b", false);
+ r = sd_bus_message_append(m, "b", arg_ignore_failure);
if (r < 0)
return bus_log_create_error(r);
@@ -1432,6 +1754,9 @@ static int start_transient_service(sd_bus *bus) {
/* Make sure to process any TTY events before we process bus events */
(void) pty_forward_set_priority(c.forward, SD_EVENT_PRIORITY_IMPORTANT);
+
+ if (!isempty(arg_background))
+ (void) pty_forward_set_background_color(c.forward, arg_background);
}
path = unit_dbus_path_from_name(service);
@@ -1899,6 +2224,8 @@ static int start_transient_trigger(sd_bus *bus, const char *suffix) {
}
static bool shall_make_executable_absolute(void) {
+ if (arg_exec_path)
+ return false;
if (strv_isempty(arg_cmdline))
return false;
if (arg_transport != BUS_TRANSPORT_LOCAL)
@@ -1919,7 +2246,10 @@ static int run(int argc, char* argv[]) {
log_parse_environment();
log_open();
- r = parse_argv(argc, argv);
+ if (invoked_as(argv, "uid0"))
+ r = parse_argv_sudo_mode(argc, argv);
+ else
+ r = parse_argv(argc, argv);
if (r <= 0)
return r;
diff --git a/src/run/systemd-uid0.in b/src/run/systemd-uid0.in
new file mode 100644
index 0000000000..57bd5e38b9
--- /dev/null
+++ b/src/run/systemd-uid0.in
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# This file is part of systemd.
+#
+# Used by uid0 sessions
+
+{% if ENABLE_HOMED %}
+-account sufficient pam_systemd_home.so
+{% endif %}
+account required pam_unix.so
+
+{% if HAVE_SELINUX %}
+session required pam_selinux.so close
+session required pam_selinux.so open
+{% endif %}
+session required pam_loginuid.so
+session optional pam_keyinit.so force revoke
+session required pam_namespace.so
+{% if ENABLE_HOMED %}
+-session optional pam_systemd_home.so
+{% endif %}
+session optional pam_umask.so silent
+session optional pam_systemd.so
+session required pam_unix.so
diff --git a/src/shared/ask-password-api.c b/src/shared/ask-password-api.c
index 0e323f4644..6d71245549 100644
--- a/src/shared/ask-password-api.c
+++ b/src/shared/ask-password-api.c
@@ -443,9 +443,7 @@ int ask_password_tty(
(void) loop_write(ttyfd, ANSI_NORMAL, SIZE_MAX);
new_termios = old_termios;
- new_termios.c_lflag &= ~(ICANON|ECHO);
- new_termios.c_cc[VMIN] = 1;
- new_termios.c_cc[VTIME] = 0;
+ termios_disable_echo(&new_termios);
r = RET_NERRNO(tcsetattr(ttyfd, TCSADRAIN, &new_termios));
if (r < 0)
diff --git a/src/shared/color-util.c b/src/shared/color-util.c
new file mode 100644
index 0000000000..776445ecfc
--- /dev/null
+++ b/src/shared/color-util.c
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "color-util.h"
+#include "macro.h"
+
+void rgb_to_hsv(double r, double g, double b,
+ double *ret_h, double *ret_s, double *ret_v) {
+
+ assert(r >= 0 && r <= 1);
+ assert(g >= 0 && g <= 1);
+ assert(b >= 0 && b <= 1);
+ assert(ret_h);
+ assert(ret_s);
+ assert(ret_v);
+
+ double max_color = fmax(r, fmax(g, b));
+ double min_color = fmin(r, fmin(g, b));
+ double delta = max_color - min_color;
+
+ *ret_v = max_color * 100.0;
+
+ if (max_color > 0)
+ *ret_s = delta / max_color * 100.0;
+ else {
+ *ret_s = 0;
+ *ret_h = NAN;
+ return;
+ }
+
+ if (delta > 0) {
+ if (r >= max_color)
+ *ret_h = 60 * fmod((g - b) / delta, 6);
+ else if (g >= max_color)
+ *ret_h = 60 * (((b - r) / delta) + 2);
+ else if (b >= max_color)
+ *ret_h = 60 * (((r - g) / delta) + 4);
+
+ *ret_h = fmod(*ret_h, 360);
+ } else
+ *ret_h = NAN;
+}
+
+void hsv_to_rgb(double h, double s, double v,
+ uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) {
+
+ double c, x, m, r, g, b;
+
+ assert(s >= 0 && s <= 100);
+ assert(v >= 0 && v <= 100);
+ assert(ret_r);
+ assert(ret_g);
+ assert(ret_b);
+
+ h = fmod(h, 360);
+ c = (s / 100.0) * (v / 100.0);
+ x = c * (1 - fabs(fmod(h / 60.0, 2) - 1));
+ m = (v / 100) - c;
+
+ if (h >= 0 && h < 60)
+ r = c, g = x, b = 0.0;
+ else if (h >= 60 && h < 120)
+ r = x, g = c, b = 0.0;
+ else if (h >= 120 && h < 180)
+ r = 0.0, g = c, b = x;
+ else if (h >= 180 && h < 240)
+ r = 0.0, g = x, b = c;
+ else if (h >= 240 && h < 300)
+ r = x, g = 0.0, b = c;
+ else
+ r = c, g = 0.0, b = x;
+
+ *ret_r = (uint8_t) ((r + m) * 255);
+ *ret_g = (uint8_t) ((g + m) * 255);
+ *ret_b = (uint8_t) ((b + m) * 255);
+}
diff --git a/src/shared/color-util.h b/src/shared/color-util.h
new file mode 100644
index 0000000000..a4ae9eba09
--- /dev/null
+++ b/src/shared/color-util.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include
+
+void rgb_to_hsv(double r, double g, double b,
+ double *ret_h, double *ret_s, double *ret_v);
+
+void hsv_to_rgb(
+ double h, double s, double v,
+ uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 1ff2a2d995..bcd02d4cfb 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -39,6 +39,7 @@ shared_sources = files(
'chown-recursive.c',
'clean-ipc.c',
'clock-util.c',
+ 'color-util.c',
'common-signal.c',
'compare-operator.c',
'condition.c',
diff --git a/src/shared/ptyfwd.c b/src/shared/ptyfwd.c
index 1a92d7ec26..03660ad417 100644
--- a/src/shared/ptyfwd.c
+++ b/src/shared/ptyfwd.c
@@ -18,15 +18,26 @@
#include "alloc-util.h"
#include "errno-util.h"
+#include "extract-word.h"
#include "fd-util.h"
#include "io-util.h"
#include "log.h"
#include "macro.h"
#include "ptyfwd.h"
#include "stat-util.h"
+#include "strv.h"
#include "terminal-util.h"
#include "time-util.h"
+typedef enum AnsiColorState {
+ ANSI_COLOR_STATE_TEXT,
+ ANSI_COLOR_STATE_ESC,
+ ANSI_COLOR_STATE_CSI_SEQUENCE,
+ ANSI_COLOR_STATE_NEWLINE,
+ _ANSI_COLOR_STATE_MAX,
+ _ANSI_COLOR_STATE_INVALID = -EINVAL,
+} AnsiColorState;
+
struct PTYForward {
sd_event *event;
@@ -67,7 +78,8 @@ struct PTYForward {
bool last_char_set:1;
char last_char;
- char in_buffer[LINE_MAX], out_buffer[LINE_MAX];
+ char in_buffer[LINE_MAX], *out_buffer;
+ size_t out_buffer_size;
size_t in_buffer_full, out_buffer_full;
usec_t escape_timestamp;
@@ -75,6 +87,10 @@ struct PTYForward {
PTYForwardHandler handler;
void *userdata;
+
+ char *background_color;
+ AnsiColorState ansi_color_state;
+ char *csi_sequence;
};
#define ESCAPE_USEC (1*USEC_PER_SEC)
@@ -115,6 +131,14 @@ static void pty_forward_disconnect(PTYForward *f) {
}
f->saved_stdout = f->saved_stdin = false;
+
+ f->out_buffer = mfree(f->out_buffer);
+ f->out_buffer_size = 0;
+ f->out_buffer_full = 0;
+ f->in_buffer_full = 0;
+
+ f->csi_sequence = mfree(f->csi_sequence);
+ f->ansi_color_state = _ANSI_COLOR_STATE_INVALID;
}
static int pty_forward_done(PTYForward *f, int rcode) {
@@ -202,10 +226,243 @@ static bool drained(PTYForward *f) {
return true;
}
-static int shovel(PTYForward *f) {
- ssize_t k;
+static char *background_color_sequence(PTYForward *f) {
+ assert(f);
+ assert(f->background_color);
+
+ /* This sets the background color to the desired one, and erase the rest of the line with it */
+
+ return strjoin("\x1B[", f->background_color, "m", ANSI_ERASE_TO_END_OF_LINE);
+}
+
+static int insert_string(PTYForward *f, size_t offset, const char *s) {
+ assert(f);
+ assert(offset <= f->out_buffer_full);
+ assert(s);
+
+ size_t l = strlen(s);
+ assert(l <= INT_MAX); /* Make sure we can still return this */
+
+ void *p = realloc(f->out_buffer, MAX(f->out_buffer_full + l, (size_t) LINE_MAX));
+ if (!p)
+ return -ENOMEM;
+
+ f->out_buffer = p;
+ f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer);
+
+ memmove(f->out_buffer + offset + l, f->out_buffer + offset, f->out_buffer_full - offset);
+ memcpy(f->out_buffer + offset, s, l);
+ f->out_buffer_full += l;
+
+ return (int) l;
+}
+
+static int insert_erase_newline(PTYForward *f, size_t offset) {
+ _cleanup_free_ char *s = NULL;
assert(f);
+ assert(f->background_color);
+
+ s = background_color_sequence(f);
+ if (!s)
+ return -ENOMEM;
+
+ return insert_string(f, offset, s);
+}
+
+static int is_csi_background_reset_sequence(const char *seq) {
+ enum {
+ COLOR_TOKEN_NO,
+ COLOR_TOKEN_START,
+ COLOR_TOKEN_8BIT,
+ COLOR_TOKEN_24BIT_R,
+ COLOR_TOKEN_24BIT_G,
+ COLOR_TOKEN_24BIT_B,
+ } token_state = COLOR_TOKEN_NO;
+
+ bool b = false;
+ int r;
+
+ assert(seq);
+
+ /* This parses CSI "m" sequences, and determines if they reset the background color. If so returns
+ * 1. This can then be used to insert another sequence that sets the color to the desired
+ * replacement. */
+
+ for (;;) {
+ _cleanup_free_ char *token = NULL;
+
+ r = extract_first_word(&seq, &token, ";", EXTRACT_RELAX|EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ switch (token_state) {
+
+ case COLOR_TOKEN_NO:
+
+ if (STR_IN_SET(token, "", "0", "00", "49"))
+ b = true; /* These tokens set the background back to normal */
+ else if (STR_IN_SET(token, "40", "41", "42", "43", "44", "45", "46", "47", "48"))
+ b = false; /* And these tokens set them to something other than normal */
+
+ if (STR_IN_SET(token, "38", "48", "58"))
+ token_state = COLOR_TOKEN_START; /* These tokens mean an 8bit or 24bit color will follow */
+ break;
+
+ case COLOR_TOKEN_START:
+
+ if (STR_IN_SET(token, "5", "05"))
+ token_state = COLOR_TOKEN_8BIT; /* 8bit color */
+ else if (STR_IN_SET(token, "2", "02"))
+ token_state = COLOR_TOKEN_24BIT_R; /* 24bit color */
+ else
+ token_state = COLOR_TOKEN_NO; /* something weird? */
+ break;
+
+ case COLOR_TOKEN_24BIT_R:
+ token_state = COLOR_TOKEN_24BIT_G;
+ break;
+
+ case COLOR_TOKEN_24BIT_G:
+ token_state = COLOR_TOKEN_24BIT_B;
+ break;
+
+ case COLOR_TOKEN_8BIT:
+ case COLOR_TOKEN_24BIT_B:
+ token_state = COLOR_TOKEN_NO;
+ break;
+ }
+ }
+
+ return b;
+}
+
+static int insert_background_fix(PTYForward *f, size_t offset) {
+ assert(f);
+ assert(f->background_color);
+
+ if (!is_csi_background_reset_sequence(strempty(f->csi_sequence)))
+ return 0;
+
+ _cleanup_free_ char *s = NULL;
+ s = strjoin(";", f->background_color);
+ if (!s)
+ return -ENOMEM;
+
+ return insert_string(f, offset, s);
+}
+
+static int pty_forward_ansi_process(PTYForward *f, size_t offset) {
+ int r;
+
+ assert(f);
+ assert(offset <= f->out_buffer_full);
+
+ if (!f->background_color)
+ return 0;
+
+ for (size_t i = offset; i < f->out_buffer_full; i++) {
+ char c = f->out_buffer[i];
+
+ switch (f->ansi_color_state) {
+
+ case ANSI_COLOR_STATE_TEXT:
+ if (c == '\n')
+ f->ansi_color_state = ANSI_COLOR_STATE_NEWLINE;
+ if (c == 0x1B) /* ESC */
+ f->ansi_color_state = ANSI_COLOR_STATE_ESC;
+ break;
+
+ case ANSI_COLOR_STATE_NEWLINE: {
+ /* Immediately after a newline insert an ANSI sequence to erase the line with a background color */
+
+ r = insert_erase_newline(f, i);
+ if (r < 0)
+ return r;
+
+ i += r;
+
+ f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
+ break;
+ }
+
+ case ANSI_COLOR_STATE_ESC: {
+
+ if (c == '[')
+ f->ansi_color_state = ANSI_COLOR_STATE_CSI_SEQUENCE;
+ else
+ f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
+
+ break;
+ }
+
+ case ANSI_COLOR_STATE_CSI_SEQUENCE: {
+
+ if (c >= 0x20 && c <= 0x3F) {
+ /* If this is a "parameter" or "intermediary" byte (i.e. ranges 0x20…0x2F and
+ * 0x30…0x3F) then we are still in the CSI sequence */
+
+ if (strlen_ptr(f->csi_sequence) >= 64) {
+ /* Safety check: lets not accept unbounded CSI sequences */
+
+ f->csi_sequence = mfree(f->csi_sequence);
+ f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
+ } else if (!strextend(&f->csi_sequence, CHAR_TO_STR(c)))
+ return -ENOMEM;
+ } else {
+ /* Otherwise, the CSI sequence is over */
+
+ if (c == 'm') {
+ /* This is an "SGR" (Select Graphic Rendition) sequence. Patch in our background color. */
+ r = insert_background_fix(f, i);
+ if (r < 0)
+ return r;
+
+ i += r;
+ }
+
+ f->csi_sequence = mfree(f->csi_sequence);
+ f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
+ }
+
+ break;
+ }
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ return 0;
+}
+
+static int shovel(PTYForward *f) {
+ ssize_t k;
+ int r;
+
+ assert(f);
+
+ if (f->out_buffer_size == 0 && f->background_color) {
+ /* Erase the first line when we start */
+ f->out_buffer = background_color_sequence(f);
+ if (!f->out_buffer)
+ return pty_forward_done(f, log_oom());
+
+ f->out_buffer_full = strlen(f->out_buffer);
+ f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer);
+ }
+
+ if (f->out_buffer_size < LINE_MAX) {
+ /* Make sure we always have room for at least one "line" */
+ void *p = realloc(f->out_buffer, LINE_MAX);
+ if (!p)
+ return pty_forward_done(f, log_oom());
+
+ f->out_buffer = p;
+ f->out_buffer_size = MALLOC_SIZEOF_SAFE(p);
+ }
while ((f->stdin_readable && f->in_buffer_full <= 0) ||
(f->master_writable && f->in_buffer_full > 0) ||
@@ -267,9 +524,9 @@ static int shovel(PTYForward *f) {
}
}
- if (f->master_readable && f->out_buffer_full < LINE_MAX) {
+ if (f->master_readable && f->out_buffer_full < MIN(f->out_buffer_size, (size_t) LINE_MAX)) {
- k = read(f->master, f->out_buffer + f->out_buffer_full, LINE_MAX - f->out_buffer_full);
+ k = read(f->master, f->out_buffer + f->out_buffer_full, f->out_buffer_size - f->out_buffer_full);
if (k < 0) {
/* Note that EIO on the master device might be caused by vhangup() or
@@ -289,7 +546,12 @@ static int shovel(PTYForward *f) {
}
} else {
f->read_from_master = true;
+ size_t scan_index = f->out_buffer_full;
f->out_buffer_full += (size_t) k;
+
+ r = pty_forward_ansi_process(f, scan_index);
+ if (r < 0)
+ return pty_forward_done(f, log_error_errno(r, "Failed to scan for ANSI sequences: %m"));
}
}
@@ -555,6 +817,7 @@ PTYForward *pty_forward_free(PTYForward *f) {
if (!f)
return NULL;
pty_forward_disconnect(f);
+ free(f->background_color);
return mfree(f);
}
@@ -689,3 +952,9 @@ int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height)
return 0;
}
+
+int pty_forward_set_background_color(PTYForward *f, const char *color) {
+ assert(f);
+
+ return free_and_strdup(&f->background_color, color);
+}
diff --git a/src/shared/ptyfwd.h b/src/shared/ptyfwd.h
index f0ae6e949d..bae8d3591e 100644
--- a/src/shared/ptyfwd.h
+++ b/src/shared/ptyfwd.h
@@ -39,4 +39,6 @@ int pty_forward_set_priority(PTYForward *f, int64_t priority);
int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height);
+int pty_forward_set_background_color(PTYForward *f, const char *color);
+
DEFINE_TRIVIAL_CLEANUP_FUNC(PTYForward*, pty_forward_free);
diff --git a/src/test/meson.build b/src/test/meson.build
index b0a92bb43b..aec125d483 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -57,6 +57,7 @@ simple_tests += files(
'test-cgroup.c',
'test-chase.c',
'test-clock.c',
+ 'test-color-util.c',
'test-compare-operator.c',
'test-condition.c',
'test-conf-files.c',
diff --git a/src/test/test-color-util.c b/src/test/test-color-util.c
new file mode 100644
index 0000000000..3d00d8719d
--- /dev/null
+++ b/src/test/test-color-util.c
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "color-util.h"
+#include "tests.h"
+
+TEST(hsv_to_rgb) {
+ uint8_t r, g, b;
+
+ hsv_to_rgb(0, 0, 0, &r, &g, &b);
+ assert(r == 0 && g == 0 && b == 0);
+
+ hsv_to_rgb(60, 0, 0, &r, &g, &b);
+ assert(r == 0 && g == 0 && b == 0);
+
+ hsv_to_rgb(0, 0, 100, &r, &g, &b);
+ assert(r == 255 && g == 255 && b == 255);
+
+ hsv_to_rgb(0, 100, 100, &r, &g, &b);
+ assert(r == 255 && g == 0 && b == 0);
+
+ hsv_to_rgb(120, 100, 100, &r, &g, &b);
+ assert(r == 0 && g == 255 && b == 0);
+
+ hsv_to_rgb(240, 100, 100, &r, &g, &b);
+ assert(r == 0 && g == 0 && b == 255);
+
+ hsv_to_rgb(311, 52, 62, &r, &g, &b);
+ assert(r == 158 && g == 75 && b == 143);
+}
+
+TEST(rgb_to_hsv) {
+
+ double h, s, v;
+ rgb_to_hsv(0, 0, 0, &h, &s, &v);
+ assert(s <= 0);
+ assert(v <= 0);
+
+ rgb_to_hsv(1, 1, 1, &h, &s, &v);
+ assert(s <= 0);
+ assert(v >= 100);
+
+ rgb_to_hsv(1, 0, 0, &h, &s, &v);
+ assert(h >= 359 || h <= 1);
+ assert(s >= 100);
+ assert(v >= 100);
+
+ rgb_to_hsv(0, 1, 0, &h, &s, &v);
+ assert(h >= 119 && h <= 121);
+ assert(s >= 100);
+ assert(v >= 100);
+
+ rgb_to_hsv(0, 0, 1, &h, &s, &v);
+ assert(h >= 239 && h <= 241);
+ assert(s >= 100);
+ assert(v >= 100);
+
+ rgb_to_hsv(0.5, 0.6, 0.7, &h, &s, &v);
+ assert(h >= 209 && h <= 211);
+ assert(s >= 28 && s <= 31);
+ assert(v >= 69 && v <= 71);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-env-util.c b/src/test/test-env-util.c
index dffbad6327..c267c2e341 100644
--- a/src/test/test-env-util.c
+++ b/src/test/test-env-util.c
@@ -133,6 +133,27 @@ TEST(strv_env_assign) {
assert_se(streq(a[0], "a=A"));
}
+TEST(strv_env_assignf) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(strv_env_assignf(&a, "a", "a") > 0);
+ assert_se(strv_env_assignf(&a, "a", "%c", 'a') == 0);
+
+ assert_se(strv_env_assignf(&a, "c", "xxx%iyyy", 5) > 0);
+ assert_se(strv_length(a) == 2);
+ assert_se(strv_equal(a, STRV_MAKE("a=a", "c=xxx5yyy")));
+ assert_se(strv_env_assignf(&a, "c", NULL) == 0);
+
+ assert_se(strv_env_assignf(&a, "b", "b") > 0);
+ assert_se(strv_env_assignf(&a, "a", "A") == 0);
+ assert_se(strv_env_assignf(&a, "b", NULL) == 0);
+
+ assert_se(strv_env_assignf(&a, "a=", "B") == -EINVAL);
+
+ assert_se(strv_length(a) == 1);
+ assert_se(streq(a[0], "a=A"));
+}
+
TEST(strv_env_assign_many) {
_cleanup_strv_free_ char **a = NULL;
diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c
index a2b7101573..0b46cad982 100644
--- a/src/test/test-terminal-util.c
+++ b/src/test/test-terminal-util.c
@@ -165,4 +165,15 @@ TEST(get_ctty) {
log_notice("Not invoked with stdin == ctty, cutting get_ctty() test short");
}
+TEST(get_default_background_color) {
+ double red, green, blue;
+ int r;
+
+ r = get_default_background_color(&red, &green, &blue);
+ if (r < 0)
+ log_notice_errno(r, "Can't get terminal default background color: %m");
+ else
+ log_notice("R=%g G=%g B=%g", red, green, blue);
+}
+
DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/test/units/testsuite-74.run.sh b/test/units/testsuite-74.run.sh
index 2caca7a5a2..d43d17ca61 100755
--- a/test/units/testsuite-74.run.sh
+++ b/test/units/testsuite-74.run.sh
@@ -227,5 +227,9 @@ B=$(systemd-run -q --wait --pipe -p ProtectProc=invisible cat /proc/version)
assert_eq "$A" "$B"
+# Check that invoking the tool under the uid0 alias name works
+uid0 ls /
+echo "$(uid0 echo foo)" = "foo"
+
umount /proc/version
rm "$V"