Add -e/--exec to join a running -r/--run session

Validates the given pid against the same tracked-session liveness
check --list-processes/--clean-processes already use, then joins its
namespaces via nsenter and runs a command there in the foreground.

Two things discovered only by testing against a live session, not
assumed up front:

- The tracked pid is bwrap's own outer process. It sets up the
  mount/user namespaces itself, then clone()s the actual sandboxed
  command into fresh pid/uts/ipc/cgroup namespaces -- clone()'s
  namespace flags only ever affect the new child, never the caller,
  so the outer process itself never enters those namespaces at all.
  exec_in_session() resolves that real child via
  /proc/<pid>/task/<pid>/children and joins its namespaces instead,
  falling back to the outer pid if that can't be read.

- Rather than nsenter -a (which would hit a known "Invalid argument"
  failure re-entering an identical namespace -- this project already
  worked around exactly that once, for the containers-storage mount
  path), each namespace type is only joined if
  /proc/<pid>/ns/<type> actually differs from this process's own.
  nsenter also needs --preserve-credentials, or it tries to
  setuid/setgid/setgroups to the target's identity, which fails
  outright against the setgroups-denied unprivileged user namespace
  bwrap creates whenever -r/--run isn't root.

Verified end-to-end: joined shell gets the container's own hostname,
process tree (ps shows only container processes), and root
filesystem; untracked/stale pids error out cleanly without touching
nsenter; Ctrl-C during the joined command doesn't disturb the
original session; no leftover mounts after either exits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-22 09:41:32 +00:00
parent d4c0e8f0a1
commit 00482b6b42
6 changed files with 275 additions and 7 deletions
+46 -2
View File
@@ -29,7 +29,14 @@ Source layout (all under `src/`):
(`pid_file.{h,cpp}`) and prints one `removed stale pid file for '<name>' (pid (`pid_file.{h,cpp}`) and prints one `removed stale pid file for '<name>' (pid
<pid>)` line per file actually removed — nothing is printed for sessions still <pid>)` line per file actually removed — nothing is printed for sessions still
running, and an empty result (nothing stale) is silent success, same running, and an empty result (nothing stale) is silent success, same
convention as the rest of this file's list/delete commands. convention as the rest of this file's list/delete commands. `-e/--exec <pid>`
(has a short form, unlike the rest of the process-tracking flags) dispatches
straight to `exec_in_session()` (`exec_session.{h,cpp}`, see below): `pid`
lands in `mode_arg` (parsed as a positive integer, erroring out otherwise) the
same way `-r`'s image path does, and the trailing command
(`argv[optind:]`, required — errors out if empty) is collected the same way
`-r`'s own command is, sharing that mode's exemption from the "no leftover
positional args" check.
`inspect_image_command()` implements `-i/--inspect `inspect_image_command()` implements `-i/--inspect
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env, <image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
volumes, default command) without mounting or running the image — extend it volumes, default command) without mounting or running the image — extend it
@@ -222,6 +229,43 @@ Source layout (all under `src/`):
a check and a later removal. Only files it actually removes are reported back a check and a later removal. Only files it actually removes are reported back
(as `SessionInfo`s with `running=false`); still-locked (running) files are (as `SessionInfo`s with `running=false`); still-locked (running) files are
left untouched and not reported. left untouched and not reported.
- `exec_session.{h,cpp}` — implements `-e/--exec <pid>`: joins an already-running
`-r/--run` session's namespaces via `nsenter` and runs a command inside it in
the foreground. `exec_in_session()` first confirms `pid` is a tracked, running
session via `list_sessions()` (`pid_file.h`) — same liveness check
`--list-processes`/`--clean-processes` already use, no new logic needed there.
**Key discovery, confirmed by direct testing, not assumed**: `pid` (the one
`run_process_foreground()` captured and pid-file-tracked when `-r` launched
`bwrap`) is bwrap's own *outer* process — it sets up the mount and user
namespaces itself, then `clone()`s the actual sandboxed command into fresh
pid/uts/ipc/cgroup namespaces, and `clone()`'s namespace-creation flags only
ever affect the newly created child, never the caller. So the outer process
itself never actually enters those namespaces — comparing
`/proc/<outer_pid>/ns/{pid,uts,ipc,cgroup}` against this process's own showed
them identical, while only `mnt`/`user` differed. `resolve_namespace_pid()`
reads `/proc/<pid>/task/<pid>/children` (the direct-children list `procfs`
exposes) to find that real inner process and joins *its* namespaces instead —
falls back to `pid` itself (best-effort, not fatal) if that file can't be
read. For each of `{mnt→--mount, uts→--uts, ipc→--ipc, pid→--pid,
cgroup→--cgroup, user→--user}` (`net` deliberately excluded — this project
never isolates networking, see `bwrap.cpp` below), `readlink()`s both
`/proc/<ns_pid>/ns/<type>` and `/proc/self/ns/<type>` and only passes
nsenter's corresponding `--type=/proc/<ns_pid>/ns/<type>` flag when they
differ — an identical-namespace re-entry attempt can fail outright
(`setns()`'s own `EINVAL` restriction on re-entering a namespace you're
already in), so skipping is deliberate, not just an optimization. `mnt` is the
one type where a *read* failure (permission denied, or the process vanished)
is treated as fatal, since without it "joining the container" is meaningless;
every other type just degrades to a skip. Always appends
`--preserve-credentials`: without it, `nsenter --user` also tries to
`setuid()`/`setgid()`/`setgroups()` to the target's identity within the new
user namespace, which fails outright (`setgroups failed: Operation not
permitted`) against the `setgroups`-denied unprivileged user namespace bwrap
creates whenever `-r/--run` isn't root — confirmed by hitting this exact
failure during manual testing before adding the flag. Runs the final
`nsenter ... -- <command>` via the existing `run_process_foreground()`
(`process.h`) — same inherited stdio and SIGINT/SIGTERM forwarding as every
other foreground external command, no new process-running logic needed.
- `config_file.{h,cpp}``load_config_file()` reads and parses (via libyaml's - `config_file.{h,cpp}``load_config_file()` reads and parses (via libyaml's
document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`, config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
@@ -298,7 +342,7 @@ Build directory is `buildDir/` (already configured).
(see `priv_drop_helper.cpp` in "Project state") (see `priv_drop_helper.cpp` in "Project state")
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the - Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`, full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
`-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`, `-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `--user`, `--group`,
`--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`, `--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`, `--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`,
`-h/--help`, `-V/--version`) `-h/--help`, `-V/--version`)
+16
View File
@@ -58,6 +58,7 @@ slocker-lite -u|--umount <layer-id>
slocker-lite -c|--cleanup <layer-id> slocker-lite -c|--cleanup <layer-id>
slocker-lite -l|--list-images <directory> slocker-lite -l|--list-images <directory>
slocker-lite -i|--inspect <image.tar> slocker-lite -i|--inspect <image.tar>
slocker-lite -e|--exec <pid> [-- <command> [args...]]
slocker-lite -v|--volume <name> <directory> slocker-lite -v|--volume <name> <directory>
slocker-lite --list-volumes slocker-lite --list-volumes
slocker-lite --delete-volume <name> slocker-lite --delete-volume <name>
@@ -81,6 +82,7 @@ slocker-lite -V|--version
| `--hostname <name>` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. | | `--hostname <name>` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. |
| `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. | | `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. |
| `-i, --inspect <image.tar>` | Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. | | `-i, --inspect <image.tar>` | Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. |
| `-e, --exec <pid>` | Join an already-running `--run` session (`<pid>` must be one `--list-processes` shows as `running`) and run a command inside its container. Pass `-- <command> [args...]` to specify it. |
| `-v, --volume <name> <dir>` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. Volume names can't contain `/`. With `--run`, instead mounts a volume into the sandbox (repeatable): `<name>` is an existing named volume, or, if it contains `/`, a host directory path (created if missing); `<dir>` is the absolute path inside the container to mount it at. If the host directory is empty and the image already has content there, that content is copied in first, preserving numeric ownership/permissions/links and, where the host filesystem supports them, extended attributes/ACLs (skipped with a warning otherwise). | | `-v, --volume <name> <dir>` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. Volume names can't contain `/`. With `--run`, instead mounts a volume into the sandbox (repeatable): `<name>` is an existing named volume, or, if it contains `/`, a host directory path (created if missing); `<dir>` is the absolute path inside the container to mount it at. If the host directory is empty and the image already has content there, that content is copied in first, preserving numeric ownership/permissions/links and, where the host filesystem supports them, extended attributes/ACLs (skipped with a warning otherwise). |
| `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. | | `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. |
| `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. | | `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. |
@@ -116,6 +118,12 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
# Inspect an image's declared config without mounting or running it # Inspect an image's declared config without mounting or running it
./buildDir/slocker-lite -i myimage.tar ./buildDir/slocker-lite -i myimage.tar
# Start a busybox container in the background, then get a shell inside it from
# another terminal (find its pid with --list-processes)
./buildDir/slocker-lite -r busybox.tar &
./buildDir/slocker-lite --list-processes
./buildDir/slocker-lite -e 12345 -- /bin/sh
# Create a named volume backed by a host directory # Create a named volume backed by a host directory
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata ./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata
@@ -187,6 +195,14 @@ when its own session ends, but `--clean-processes` removes any stale ones left
behind (e.g. after a crash) using that same check, atomically per file, so it behind (e.g. after a crash) using that same check, atomically per file, so it
never removes one that's still genuinely running. never removes one that's still genuinely running.
`-e/--exec <pid>` joins a running session's namespaces with `nsenter` and runs a
command there. Because `bwrap` itself sets up the sandbox's mount/user namespaces
and then hands the actual sandboxed command off to a child process in fresh
pid/uts/ipc/cgroup namespaces, `-e` resolves that real child first (via `/proc`)
rather than joining the outer `bwrap` process's own namespaces, so the joined
command sees the container's process tree and hostname too, not just its
filesystem.
See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the
reasoning behind each of the above, and known gaps). reasoning behind each of the above, and known gaps).
+1 -1
View File
@@ -19,7 +19,7 @@ configure_file(output : 'config.h', configuration : conf_data)
slocker_lite = executable('slocker-lite', slocker_lite = executable('slocker-lite',
['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp', ['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp',
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp', 'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
'src/pid_file.cpp'], 'src/pid_file.cpp', 'src/exec_session.cpp'],
include_directories : include_directories('.'), include_directories : include_directories('.'),
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep], dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
install : true) install : true)
+147
View File
@@ -0,0 +1,147 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "exec_session.h"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <optional>
#include <system_error>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include "pid_file.h"
#include "process.h"
namespace {
// bwrap's own outer process (the one tracked in the session pid file) sets up the
// mount/user namespaces itself, then clone()s the actual sandboxed command into
// fresh pid/uts/ipc/cgroup/net namespaces -- clone()'s namespace-creation flags
// only ever affect the newly created child, never the caller, so the outer
// process itself never actually enters those namespaces (confirmed by direct
// testing: /proc/<outer_pid>/ns/{pid,uts,ipc,cgroup,net} all matched this
// process's own, while only mnt/user differed). The real sandboxed command is
// that direct child, which is what actually needs to be nsenter-target for a
// faithful join. Returns `pid` itself (best-effort fallback, not fatal) if the
// child can't be determined -- callers still get a mount/user-namespace join out
// of that, just not the rest.
pid_t resolve_namespace_pid(pid_t pid) {
std::ifstream children(fmt::format("/proc/{}/task/{}/children", pid, pid));
pid_t child = 0;
if (children >> child && child > 0) {
return child;
}
spdlog::debug("could not determine pid {}'s sandboxed child process; joining its own namespaces only",
pid);
return pid;
}
struct JoinableNamespace {
const char* proc_name; // matches /proc/<pid>/ns/<proc_name>
const char* nsenter_flag;
bool required; // fatal (not just skipped) if it can't be read
};
// net is deliberately excluded: this project never isolates networking either
// (see build_bwrap_args() dropping --unshare-net), so there's nothing meaningful
// to join there.
constexpr std::array<JoinableNamespace, 6> kJoinableNamespaces = {{
{"mnt", "--mount", true},
{"uts", "--uts", false},
{"ipc", "--ipc", false},
{"pid", "--pid", false},
{"cgroup", "--cgroup", false},
{"user", "--user", false},
}};
std::optional<std::string> read_ns_link(const std::filesystem::path& path) {
std::error_code ec;
auto target = std::filesystem::read_symlink(path, ec);
if (ec) {
return std::nullopt;
}
return target.string();
}
} // namespace
int exec_in_session(pid_t pid, const std::vector<std::string>& command) {
if (pid <= 0) {
spdlog::error("invalid pid: {}", pid);
return 1;
}
auto sessions = list_sessions();
auto it = std::find_if(sessions.begin(), sessions.end(),
[&](const SessionInfo& session) { return session.pid == pid; });
if (it == sessions.end()) {
spdlog::error("no tracked slocker-lite session with pid {}", pid);
return 1;
}
if (!it->running) {
spdlog::error("session '{}' (pid {}) is no longer running", it->container_name, pid);
return 1;
}
pid_t ns_pid = resolve_namespace_pid(pid);
std::vector<std::string> argv = {"nsenter"};
for (const auto& ns : kJoinableNamespaces) {
auto target_ns = read_ns_link(fmt::format("/proc/{}/ns/{}", ns_pid, ns.proc_name));
if (!target_ns) {
if (ns.required) {
spdlog::error(
"could not access pid {}'s {} namespace (permission denied, or it has since exited)",
ns_pid, ns.proc_name);
return 1;
}
spdlog::debug("skipping {} namespace for pid {}: not accessible", ns.proc_name, ns_pid);
continue;
}
auto own_ns = read_ns_link(fmt::format("/proc/self/ns/{}", ns.proc_name));
if (own_ns && *own_ns == *target_ns) {
// Already in the same namespace -- nsenter can fail ("Invalid
// argument") if asked to re-enter it, so skip rather than risk that.
continue;
}
argv.push_back(fmt::format("{}=/proc/{}/ns/{}", ns.nsenter_flag, ns_pid, ns.proc_name));
}
if (!find_in_path("nsenter")) {
spdlog::error("nsenter not found in PATH");
return 1;
}
// Without this, nsenter --user tries to setgroups()/setuid()/setgid() to the
// target's identity within the new user namespace, which fails outright
// ("setgroups failed: Operation not permitted") when that namespace has
// setgroups denied -- the kernel-enforced default for any unprivileged user
// namespace, which is exactly what bwrap creates when run_container() isn't
// root. We don't want nsenter changing our credentials anyway -- just join
// the namespaces and keep running as whatever this process already is.
argv.push_back("--preserve-credentials");
argv.push_back("--");
argv.insert(argv.end(), command.begin(), command.end());
return run_process_foreground(argv);
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#pragma once
#include <string>
#include <vector>
#include <sys/types.h>
// Validates that `pid` is a currently-tracked, running -r/--run session (see
// list_sessions() in pid_file.h) and, if so, joins its namespaces via nsenter and
// runs `command` inside it in the foreground, inheriting stdio (see
// run_process_foreground() in process.h). `pid` is bwrap's own outer/tracked
// process, but the sandboxed command actually runs in a *child* of it (bwrap sets
// up the mount/user namespaces itself, then clone()s the real command into fresh
// pid/uts/ipc/cgroup namespaces -- the parent never enters those itself), so this
// resolves that child internally before building nsenter's argv. Returns the exit
// code, or 1 if `pid` isn't a tracked/running session (logged with
// spdlog::error), or -1 if nsenter itself couldn't be launched.
int exec_in_session(pid_t pid, const std::vector<std::string>& command);
+31 -4
View File
@@ -34,6 +34,7 @@
#include "config.h" #include "config.h"
#include "config_file.h" #include "config_file.h"
#include "containers_storage.h" #include "containers_storage.h"
#include "exec_session.h"
#include "oci_image.h" #include "oci_image.h"
#include "pid_file.h" #include "pid_file.h"
#include "process.h" #include "process.h"
@@ -58,7 +59,8 @@ enum class Mode {
kDeleteVolumeFull, kDeleteVolumeFull,
kInspect, kInspect,
kListProcesses, kListProcesses,
kCleanProcesses kCleanProcesses,
kExec
}; };
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/ // --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/
@@ -76,7 +78,7 @@ constexpr int kHostnameOpt = 262;
constexpr int kListProcessesOpt = 263; constexpr int kListProcessesOpt = 263;
constexpr int kCleanProcessesOpt = 264; constexpr int kCleanProcessesOpt = 264;
constexpr std::array<struct option, 21> kLongOptions = {{ constexpr std::array<struct option, 22> kLongOptions = {{
{"help", no_argument, nullptr, 'h'}, {"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'}, {"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'}, {"test", no_argument, nullptr, 't'},
@@ -94,6 +96,7 @@ constexpr std::array<struct option, 21> kLongOptions = {{
{"delete-volume", required_argument, nullptr, kDeleteVolumeOpt}, {"delete-volume", required_argument, nullptr, kDeleteVolumeOpt},
{"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt}, {"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt},
{"inspect", required_argument, nullptr, 'i'}, {"inspect", required_argument, nullptr, 'i'},
{"exec", required_argument, nullptr, 'e'},
{"hostname", required_argument, nullptr, kHostnameOpt}, {"hostname", required_argument, nullptr, kHostnameOpt},
{"list-processes", no_argument, nullptr, kListProcessesOpt}, {"list-processes", no_argument, nullptr, kListProcessesOpt},
{"clean-processes", no_argument, nullptr, kCleanProcessesOpt}, {"clean-processes", no_argument, nullptr, kCleanProcessesOpt},
@@ -108,6 +111,7 @@ void print_usage(const char* prog) {
" {0} -c|--cleanup <layer-id>\n" " {0} -c|--cleanup <layer-id>\n"
" {0} -l|--list-images <directory>\n" " {0} -l|--list-images <directory>\n"
" {0} -i|--inspect <image.tar>\n" " {0} -i|--inspect <image.tar>\n"
" {0} -e|--exec <pid> [-- <command> [args...]]\n"
" {0} -v|--volume <name> <directory>\n" " {0} -v|--volume <name> <directory>\n"
" {0} --list-volumes\n" " {0} --list-volumes\n"
" {0} --delete-volume <name>\n" " {0} --delete-volume <name>\n"
@@ -152,6 +156,10 @@ void print_usage(const char* prog) {
" -i, --inspect <image.tar> print an image's declared user, exposed ports,\n" " -i, --inspect <image.tar> print an image's declared user, exposed ports,\n"
" env, volumes, and default command, without\n" " env, volumes, and default command, without\n"
" mounting or running it\n" " mounting or running it\n"
" -e, --exec <pid> join a running --run session (pid must be one\n"
" shown by --list-processes as \"running\") and run\n"
" a command inside its container; pass\n"
" -- <command> [args...] to specify it\n"
" -v, --volume <name> <dir> create a named volume mapped to a host directory\n" " -v, --volume <name> <dir> create a named volume mapped to a host directory\n"
" (created if missing), recorded in the config\n" " (created if missing), recorded in the config\n"
" file's volumes section. With --run, instead\n" " file's volumes section. With --run, instead\n"
@@ -624,7 +632,7 @@ int main(int argc, char* argv[]) {
opterr = 0; opterr = 0;
int opt; int opt;
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:", kLongOptions.data(), nullptr)) != -1) { while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:", kLongOptions.data(), nullptr)) != -1) {
switch (opt) { switch (opt) {
case 'h': case 'h':
print_usage(argv[0]); print_usage(argv[0]);
@@ -639,6 +647,7 @@ int main(int argc, char* argv[]) {
case 'c': case 'c':
case 'l': case 'l':
case 'i': case 'i':
case 'e':
case kListVolumesOpt: case kListVolumesOpt:
case kDeleteVolumeOpt: case kDeleteVolumeOpt:
case kDeleteVolumeFullOpt: case kDeleteVolumeFullOpt:
@@ -667,6 +676,9 @@ int main(int argc, char* argv[]) {
case 'i': case 'i':
requested = Mode::kInspect; requested = Mode::kInspect;
break; break;
case 'e':
requested = Mode::kExec;
break;
case kListVolumesOpt: case kListVolumesOpt:
requested = Mode::kListVolumes; requested = Mode::kListVolumes;
break; break;
@@ -756,7 +768,7 @@ int main(int argc, char* argv[]) {
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;
} }
if (mode != Mode::kRun && optind != argc) { if (mode != Mode::kRun && mode != Mode::kExec && optind != argc) {
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;
} }
@@ -797,6 +809,21 @@ int main(int argc, char* argv[]) {
if (mode == Mode::kCleanProcesses) { if (mode == Mode::kCleanProcesses) {
return clean_processes_command(); return clean_processes_command();
} }
if (mode == Mode::kExec) {
std::vector<std::string> command(argv + optind, argv + argc);
if (command.empty()) {
spdlog::error("--exec requires a command to run");
print_usage(argv[0]);
return 1;
}
char* end = nullptr;
long parsed = std::strtol(mode_arg.c_str(), &end, 10);
if (mode_arg.empty() || !end || *end != '\0' || parsed <= 0) {
spdlog::error("invalid pid: {}", mode_arg);
return 1;
}
return exec_in_session(static_cast<pid_t>(parsed), command);
}
if (mode == Mode::kRun) { if (mode == Mode::kRun) {
std::vector<std::string> command(argv + optind, argv + argc); std::vector<std::string> command(argv + optind, argv + argc);
// As root, containers-storage mount doesn't need to reexec into a private // As root, containers-storage mount doesn't need to reexec into a private