diff --git a/man/ptyctl.xml b/man/ptyctl.xml
new file mode 100644
index 00000000000..e11eab6b36b
--- /dev/null
+++ b/man/ptyctl.xml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+ ptyctl
+ systemd
+
+
+
+ ptyctl
+ 1
+
+
+
+ ptyctl
+ List, monitor and hang up pseudo TTYs registered with the pseudo TTY broker
+
+
+
+
+ ptyctl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+
+ Description
+
+ ptyctl may be used to introspect and control the pseudo TTY allocations
+ registered with the pseudo TTY broker,
+ systemd-ptybrokerd.service8.
+ The broker keeps the frontend (i.e. the master) side of registered pseudo TTYs open,
+ while the payload — a service, a container, a virtual machine console, or similar — runs connected to the
+ backend (i.e. the slave) side. ptyctl permits listing these pseudo
+ TTYs, interactively connecting to them, and hanging them up.
+
+ Pseudo TTYs are registered with the broker for example via of
+ systemd-pty-forward1,
+ systemd-nspawn1 and
+ systemd-vmspawn1, or
+ via StandardOutput=broker in service units, see
+ systemd.exec5.
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+ list
+
+ List the pseudo TTYs currently registered with the broker, showing their name,
+ description, log tag, frontend and backend types, and backend TTY path. This command is the default
+ if no command is specified.
+
+
+
+
+
+ monitor NAME
+
+ Connect interactively to the pseudo TTY registered under the specified name: its
+ output is forwarded to the local terminal, and local input is forwarded back to it. To disconnect,
+ press Ctrl] three times within one
+ second. Disconnecting only ends the monitor connection, the pseudo TTY itself remains registered with
+ the broker and the payload continues to run.
+
+
+
+
+
+ hangup NAME…
+
+ Hang up the pseudo TTYs registered under the specified names. This executes the
+ equivalent of
+ vhangup2
+ on the pseudo TTY, disconnecting the session running on its backend side.
+
+
+
+
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+
+
+ Selects whether to talk to the system-wide instance or the per-user instance of
+ systemd-ptybrokerd.service8.
+ Defaults to .
+
+
+
+
+
+
+
+ Change the terminal background color to the specified ANSI color while monitoring a
+ pseudo TTY. 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. Only has an effect on the monitor
+ command.
+
+ Example: --background=44 for a blue background.
+
+
+
+
+
+
+
+ Change the terminal title to the specified string while monitoring a pseudo TTY. Only
+ has an effect on the monitor command.
+
+
+
+
+
+
+
+
+ Suppresses additional informational output while running.
+
+
+
+
+
+
+
+
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+ See Also
+
+ systemd1
+ systemd-ptybrokerd.service8
+ systemd-pty-forward1
+ systemd-nspawn1
+ systemd-vmspawn1
+ systemd.exec5
+
+
+
+
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 3bf1c4ed842..4445ef296e9 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -82,6 +82,7 @@ manpages = [
['portablectl', '1', [], 'ENABLE_PORTABLED'],
['poweroff', '8', ['halt', 'reboot'], ''],
['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'],
+ ['ptyctl', '1', [], ''],
['repart.d', '5', [], 'ENABLE_REPART'],
['report-geoip.conf',
'5',
diff --git a/shell-completion/bash/meson.build b/shell-completion/bash/meson.build
index 98dd8d8a282..fc4c6052752 100644
--- a/shell-completion/bash/meson.build
+++ b/shell-completion/bash/meson.build
@@ -45,6 +45,7 @@ foreach item : [
['networkctl', 'ENABLE_NETWORKD'],
['oomctl', 'ENABLE_OOMD'],
['portablectl', 'ENABLE_PORTABLED'],
+ ['ptyctl', ''],
['resolvectl', 'ENABLE_RESOLVE'],
['run0', ''],
['storagectl', ''],
diff --git a/shell-completion/bash/ptyctl b/shell-completion/bash/ptyctl
new file mode 100644
index 00000000000..8508466d603
--- /dev/null
+++ b/shell-completion/bash/ptyctl
@@ -0,0 +1,84 @@
+# shellcheck shell=bash
+# ptyctl(1) completion -*- shell-script -*-
+# 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.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see .
+
+__contains_word () {
+ local w word=$1; shift
+ for w in "$@"; do
+ [[ $w = "$word" ]] && return
+ done
+}
+
+__get_ptys() {
+ local mode=$1
+ ptyctl "$mode" --quiet list 2>/dev/null | awk 'NR > 1 { print $1 }'
+}
+
+_ptyctl() {
+ local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword
+ local i verb comps mode
+
+ local -A OPTS=(
+ [STANDALONE]='-h --help --version -q --quiet --no-ask-password --system --user'
+ [ARG]='--background --title'
+ )
+
+ _init_completion || return
+
+ if __contains_word "--user" ${COMP_WORDS[*]}; then
+ mode=--user
+ else
+ mode=--system
+ fi
+
+ if __contains_word "$prev" ${OPTS[ARG]}; then
+ comps=''
+ COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+ return 0
+ fi
+
+ if [[ "$cur" = -* ]]; then
+ COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
+ return 0
+ fi
+
+ local -A VERBS=(
+ [STANDALONE]='list'
+ [PTYS]='monitor hangup'
+ )
+
+ for ((i=0; i < COMP_CWORD; i++)); do
+ if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]}; then
+ verb=${COMP_WORDS[i]}
+ break
+ fi
+ done
+
+ if [[ -z ${verb-} ]]; then
+ comps=${VERBS[*]}
+ elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
+ comps=''
+ elif __contains_word "$verb" ${VERBS[PTYS]}; then
+ comps=$(__get_ptys $mode)
+ fi
+
+ COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+ return 0
+}
+
+complete -F _ptyctl ptyctl
diff --git a/shell-completion/zsh/_ptyctl b/shell-completion/zsh/_ptyctl
new file mode 100644
index 00000000000..5126b3ced13
--- /dev/null
+++ b/shell-completion/zsh/_ptyctl
@@ -0,0 +1,49 @@
+#compdef ptyctl
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_ptyctl_ptys] )) || _ptyctl_ptys()
+{
+ local mode
+ if (( $words[(I)--user] )); then
+ mode=--user
+ else
+ mode=--system
+ fi
+
+ local -a _ptys
+ _ptys=( ${(f)"$(ptyctl $mode --quiet list 2>/dev/null | awk 'NR > 1 { print $1 }')"} )
+ _describe -t ptys 'pty' _ptys
+}
+
+(( $+functions[_ptyctl_commands] )) || _ptyctl_commands()
+{
+ local -a _ptyctl_cmds
+ _ptyctl_cmds=(
+ "list:List the PTYs registered with the broker"
+ "monitor:Monitor the specified PTY"
+ "hangup:Hang up the specified PTY(s)"
+ )
+ if (( CURRENT == 1 )); then
+ _describe -t commands 'ptyctl command' _ptyctl_cmds
+ else
+ case "$words[1]" in
+ monitor|hangup)
+ _ptyctl_ptys
+ ;;
+ *)
+ _message "no more options"
+ ;;
+ esac
+ fi
+}
+
+_arguments \
+ '(- *)'{-h,--help}'[Show this help]' \
+ '(- *)--version[Show package version]' \
+ '(--quiet -q)'{-q,--quiet}'[Suppress information messages during runtime]' \
+ '--no-ask-password[Do not prompt for password]' \
+ '(--user)--system[Talk to system PTY broker]' \
+ '(--system)--user[Talk to per-user PTY broker]' \
+ '--background=[Set ANSI color for background (when monitoring)]: : _message "background color"' \
+ '--title=[Set terminal title (when monitoring)]: : _message "terminal title"' \
+ '*::ptyctl command:_ptyctl_commands'
diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build
index 6fe0d1e1830..a75896c8ebf 100644
--- a/shell-completion/zsh/meson.build
+++ b/shell-completion/zsh/meson.build
@@ -37,6 +37,7 @@ foreach item : [
['_machinectl', 'ENABLE_MACHINED'],
['_networkctl', 'ENABLE_NETWORKD'],
['_oomctl', 'ENABLE_OOMD'],
+ ['_ptyctl', ''],
['_resolvectl', 'ENABLE_RESOLVE'],
['_run0', ''],
['_sd_bus_address', ''],
diff --git a/src/ptybroker/meson.build b/src/ptybroker/meson.build
index dee8b88c7ab..a7ba3617a05 100644
--- a/src/ptybroker/meson.build
+++ b/src/ptybroker/meson.build
@@ -10,6 +10,11 @@ executables += [
'ptybroker-util.c',
),
},
+ executable_template + {
+ 'name' : 'ptyctl',
+ 'public' : true,
+ 'sources' : files('ptyctl.c'),
+ },
]
install_data('io.systemd.PTYBroker.policy',
diff --git a/src/ptybroker/ptyctl.c b/src/ptybroker/ptyctl.c
new file mode 100644
index 00000000000..30508a0e234
--- /dev/null
+++ b/src/ptybroker/ptyctl.c
@@ -0,0 +1,444 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-event.h"
+#include "sd-json.h"
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "ansi-color.h"
+#include "build.h"
+#include "bus-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "json-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "options.h"
+#include "parse-argument.h"
+#include "pidref.h"
+#include "polkit-agent.h"
+#include "ptybroker-client.h"
+#include "ptyfwd.h"
+#include "runtime-scope.h"
+#include "set.h"
+#include "string-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "varlink-util.h"
+#include "verbs.h"
+
+static bool arg_quiet = false;
+static bool arg_ask_password = true;
+static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+static char *arg_background = NULL;
+static char *arg_title = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_title, freep);
+
+COMMAND(
+ "ptyctl\0",
+ "Interact with the pseudo TTY broker.",
+ .man_pages = "ptyctl(1)\0",
+);
+
+VERB_COMMON_HELP_AUTO_HIDDEN();
+
+typedef struct ListPtyEntry {
+ const char *name;
+ const char *description;
+ const char *tag;
+ const char *frontend_type;
+ const char *backend_type;
+ const char *backend_path;
+ PidRef owner;
+ uid_t owner_uid;
+} ListPtyEntry;
+
+static void list_pty_entry_done(ListPtyEntry *e) {
+ assert(e);
+
+ pidref_done(&e->owner);
+}
+
+VERB_DEFAULT_NOARG(verb_list, "list", "List the PTYs registered with the broker");
+static int verb_list(int argc, char *argv[], uintptr_t data, void *userdata) {
+ int r;
+
+ (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = pty_broker_connect(arg_runtime_scope, &vl);
+ if (r < 0)
+ return r;
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_collectbo(
+ vl,
+ "io.systemd.PTYBroker.ListPty",
+ &reply,
+ &error_id,
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+ if (r < 0)
+ return log_error_errno(r, "Failed to call ListPty(): %m");
+ if (error_id && !streq(error_id, "io.systemd.PTYBroker.NoSuchPty"))
+ return log_error_errno(sd_varlink_error_to_errno(error_id, reply),
+ "Failed to list PTYs: %s", error_id);
+
+ _cleanup_(table_unrefp) Table *table = table_new("name", "description", "tag", "frontend", "backend", "backend path", "uid", "pid");
+ if (!table)
+ return log_oom();
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+ (void) table_set_sort(table, (size_t) 0);
+
+ sd_json_variant *i;
+ JSON_VARIANT_ARRAY_FOREACH(i, reply) {
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, name), 0 },
+ { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, description), 0 },
+ { "tag", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, tag), 0 },
+ { "frontendType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, frontend_type), 0 },
+ { "backendType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, backend_type), 0 },
+ { "backendPath", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListPtyEntry, backend_path), 0 },
+ { "owner", SD_JSON_VARIANT_OBJECT, json_dispatch_pidref, offsetof(ListPtyEntry, owner), 0 },
+ { "ownerUID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(ListPtyEntry, owner_uid), 0 },
+ {}
+ };
+
+ _cleanup_(list_pty_entry_done) ListPtyEntry e = {
+ .owner = PIDREF_NULL,
+ .owner_uid = UID_INVALID,
+ };
+
+ r = sd_json_dispatch(i, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse ListPty() reply: %m");
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, e.name,
+ TABLE_STRING, e.description,
+ TABLE_STRING, e.tag,
+ TABLE_STRING, e.frontend_type,
+ TABLE_STRING, e.backend_type,
+ TABLE_STRING, e.backend_path,
+ TABLE_UID, e.owner_uid,
+ TABLE_PID, e.owner.pid);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (table_isempty(table)) {
+ if (!arg_quiet)
+ log_info("No PTYs.");
+ return 0;
+ }
+
+ return table_print_or_warn(table);
+}
+
+static int monitor_broker_pty(const char *name, int *ret_monitor_fd, char ***ret_track_buffer) {
+ int r;
+
+ assert(name);
+ assert(ret_monitor_fd);
+ assert(ret_track_buffer);
+
+ /* Attach to an *existing* pseudo TTY allocation via ptybrokerd's MonitorPty(). We neither allocate a
+ * new PTY nor spawn a child: MonitorPty() upgrades the connection to a bidirectional socket that
+ * carries the monitored frontend's output to us and forwards our input back to it. We use it as the
+ * PTY forwarder "master". */
+
+ (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = pty_broker_connect(arg_runtime_scope, &vl);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *ts = NULL;
+ r = pty_broker_terminal_settings_to_json(&ts);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build terminal settings: %m");
+
+ _cleanup_close_ int input_fd = -EBADF, output_fd = -EBADF;
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_call_and_upgradebo(
+ vl,
+ "io.systemd.PTYBroker.MonitorPty",
+ &reply,
+ &error_id,
+ &input_fd,
+ &output_fd,
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ SD_JSON_BUILD_PAIR_UNSIGNED("trackBufferLines", MAX(5U, lines() / 2U)),
+ SD_JSON_BUILD_PAIR_VARIANT("terminalSettings", ts),
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+ if (r < 0)
+ return log_error_errno(r, "Failed to call MonitorPty(): %m");
+ if (error_id)
+ return log_error_errno(sd_varlink_error_to_errno(error_id, reply),
+ "Failed to monitor PTY via ptybrokerd: %s", error_id);
+
+ r = same_fd(input_fd, output_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if input/output file descriptors match: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Input/output file descriptor of monitor connection do not match.");
+
+ _cleanup_(strv_freep) char **tb = NULL;
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "trackBuffer", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, 0, 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &tb);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse MonitorPty() reply: %m");
+
+ /* The upgraded monitor connection is a single bidirectional socket, handed to us as two dup'ed fds.
+ * The PTY forwarder reads and writes a single "master" fd, so we keep the input side for that and
+ * drop the redundant output side. */
+ *ret_monitor_fd = TAKE_FD(input_fd);
+ *ret_track_buffer = TAKE_PTR(tb);
+
+ return 0;
+}
+
+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);
+ }
+ if (rcode < 0) {
+ (void) sd_event_exit(e, EXIT_FAILURE);
+ return log_error_errno(rcode, "Error on PTY forwarding logic: %m");
+ }
+
+ /* rcode == 0: the monitored PTY has been hung up or released. Unlike systemd-pty-forward we manage no
+ * child process whose exit would terminate the event loop, hence exit on hangup here. */
+ log_debug("Monitored PTY was released, exiting.");
+ return sd_event_exit(e, EXIT_SUCCESS);
+}
+
+VERB(verb_monitor, "monitor", "NAME\0", 2, 2, 0, "Monitor the specified PTY");
+static int verb_monitor(int argc, char *argv[], uintptr_t data, void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_close_ int monitor_fd = -EBADF;
+ _cleanup_(pty_forward_freep) PTYForward *forward = NULL;
+ int r;
+
+ _cleanup_(strv_freep) char **track_buffer = NULL;
+ r = monitor_broker_pty(argv[1], &monitor_fd, &track_buffer);
+ if (r < 0)
+ return r;
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ if (!arg_quiet)
+ log_info("Press ^] three times within 1s to disconnect TTY.");
+
+ STRV_FOREACH(i, track_buffer)
+ printf("%s%s%s\n", ansi_grey(), *i, ansi_normal());
+ fflush(stdout);
+
+ r = pty_forward_new(event, monitor_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_hangup_handler(forward, pty_forward_handler, event);
+
+ return sd_event_loop(event);
+}
+
+typedef struct HangUpContext {
+ sd_event *event;
+ Set *connections;
+ int error;
+} HangUpContext;
+
+static void hangup_context_done(HangUpContext *c) {
+ assert(c);
+
+ /* Free the connections before the event loop, as they hold event sources attached to it. */
+ set_free(c->connections);
+ sd_event_unref(c->event);
+}
+
+static int on_hangup_reply(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ const char *error_id,
+ sd_varlink_reply_flags_t flags,
+ void *userdata) {
+
+ HangUpContext *context = ASSERT_PTR(userdata);
+
+ assert(link);
+
+ /* The name of the PTY this connection hangs up is stashed in the connection's description. */
+ const char *name = sd_varlink_get_description(link);
+
+ if (error_id) {
+ int e = sd_varlink_error_to_errno(error_id, parameters);
+ log_error_errno(e, "Failed to hang up PTY '%s': %s", strna(name), error_id);
+ if (context->error == 0)
+ context->error = e;
+ }
+
+ /* Drop the completed connection from the set. sd_varlink_process() holds a reference for the duration
+ * of this callback, so unreffing here is safe. Once the set has drained we are done. */
+ assert_se(set_remove(context->connections, link) == link);
+ sd_varlink_unref(link);
+
+ if (set_isempty(context->connections))
+ (void) sd_event_exit(context->event, 0); /* Errors are propagated via context->error instead */
+
+ return 0;
+}
+
+VERB(verb_hangup, "hangup", "NAME…\0", 2, VERB_ANY, 0, "Hang up the specified PTY(s)");
+static int verb_hangup(int argc, char *argv[], uintptr_t data, void *userdata) {
+ _cleanup_(hangup_context_done) HangUpContext context = {};
+ int r;
+
+ (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
+
+ r = sd_event_default(&context.event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ /* Establish a separate connection to the broker for each PTY to hang up, keep all of them alive in a
+ * Set, and enqueue the HangUpPty() calls in parallel. Each reply drops its connection from the set;
+ * we only return once the set has drained. */
+
+ STRV_FOREACH(name, strv_skip(argv, 1)) {
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = pty_broker_connect(arg_runtime_scope, &vl);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_set_description(vl, *name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set Varlink connection description: %m");
+
+ r = sd_varlink_attach_event(vl, context.event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach Varlink connection to event loop: %m");
+
+ sd_varlink_set_userdata(vl, &context);
+
+ r = sd_varlink_bind_reply(vl, on_hangup_reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind reply callback: %m");
+
+ r = sd_varlink_invokebo(
+ vl,
+ "io.systemd.PTYBroker.HangUpPty",
+ SD_JSON_BUILD_PAIR_STRING("name", *name),
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+ if (r < 0)
+ return log_error_errno(r, "Failed to enqueue HangUpPty() call: %m");
+
+ r = set_ensure_put(&context.connections, &varlink_hash_ops, vl);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(vl); /* The set owns the connection now. */
+ }
+
+ r = sd_event_loop(context.event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ return context.error;
+}
+
+static int parse_argv(int argc, char *argv[], char ***ret_args) {
+ assert(argc >= 0);
+ assert(argv);
+ assert(ret_args);
+
+ OptionParser opts = { argc, argv };
+ int r;
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_COMMON_HELP:
+ return command_print_help();
+
+ OPTION_COMMON_VERSION:
+ return version();
+
+ OPTION('q', "quiet", NULL, "Suppress information messages during runtime"):
+ arg_quiet = true;
+ break;
+
+ OPTION_COMMON_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ OPTION_LONG("system", NULL, "Talk to system PTY broker"):
+ arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+ break;
+
+ OPTION_LONG("user", NULL, "Talk to per-user PTY broker"):
+ arg_runtime_scope = RUNTIME_SCOPE_USER;
+ break;
+
+ OPTION_LONG("background", "COLOR", "Set ANSI color for background (when monitoring)"):
+ r = parse_background_argument(opts.arg, &arg_background);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_LONG("title", "TITLE", "Set terminal title (when monitoring)"):
+ r = free_and_strdup_warn(&arg_title, opts.arg);
+ if (r < 0)
+ return r;
+ break;
+
+ OPTION_COMMON_INTROSPECT_CLI:
+ return introspect_cli(SD_JSON_FORMAT_OFF);
+ }
+
+ *ret_args = option_parser_get_args(&opts);
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ char **args = NULL;
+ r = parse_argv(argc, argv, &args);
+ if (r <= 0)
+ return r;
+
+ return dispatch_verb(args, NULL);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh
index 3a791984088..aba41ca8516 100755
--- a/test/units/TEST-74-AUX-UTILS.introspect-cli.sh
+++ b/test/units/TEST-74-AUX-UTILS.introspect-cli.sh
@@ -48,6 +48,7 @@ INTROSPECTABLE=(
oomctl
portablectl
poweroff
+ ptyctl
reboot
resolvectl
run0