Add -D/--daemonize to run -r/--run sessions in the background
Single-fork daemonize: the child calls setsid() itself rather than
re-enabling bwrap's own --new-session, which was previously removed
(and stays that way) because it only detaches the deeply-nested
sandboxed command, leaving bwrap/nsenter/slocker-lite itself still
attached to the original session -- not real daemonization. Calling
setsid() in slocker-lite's own forked child, before it execs into
nsenter/bwrap, detaches the whole chain at once, since exec() never
changes session membership -- confirmed via ps -o sid,pgid,tty against
a running daemonized session.
The child also ignores SIGHUP (confirmed to survive exec() into bwrap,
unlike a real handler, which exec() resets) and redirects stdin to
/dev/null and stdout/stderr to a log file under
$XDG_STATE_HOME/slocker-lite/logs/ (session_log_file_path(), new
sibling to the existing session_pid_file_path() in pid_file.{h,cpp}).
The original process blocks briefly on a pipe until the child reports
the real bwrap pid (or exits without doing so), then prints it and
exits -- keeping "pid" meaning the same thing everywhere in this
codebase (the same one --list-processes/-e/--exec already use), rather
than introducing a separate daemon-supervisor pid. run_bwrap() gained
an on_bwrap_pid_known callback (bwrap.{h,cpp}) for this, invoked
alongside the existing session-lock creation at the same instant.
The daemonized child is what runs run_container()'s entire existing
body afterward, including the unmount/cleanup that already ran once
bwrap exits -- no separate watcher/reaper process.
Testing caught a real bug before this was correct: the log file gets
renamed from its initial (daemon-pid-named) filename to the final
<container_name>-<bwrap-pid>.log once the real pid is known, but the
parent had already been told the pre-rename path and was never updated
-- fixed by re-reporting the path over the same pipe after the rename.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -37,6 +37,20 @@ Source layout (all under `src/`):
|
||||
(`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.
|
||||
`-D/--daemonize` (has a short form; `'D'` was free) is a plain boolean flag
|
||||
(`daemonize_flag`, set in its own `case 'D':`, same pattern as
|
||||
`-n/--no-nsenter`) threaded through to `run_container()`, which computes
|
||||
`container_name` *before* `mount_image()` now (moved up from right before
|
||||
the `run_bwrap()` call — it only ever depended on `image_tar`, a parameter
|
||||
available from the start, so this is a pure reordering) and, if daemonizing,
|
||||
calls `daemonize(container_name)` (`daemonize.{h,cpp}`, see below)
|
||||
immediately after: a returned value means this is the original (parent)
|
||||
process (or a hard daemonize failure) — print it and `return` right away;
|
||||
`nullopt` means this is the now-detached child, which falls through into the
|
||||
rest of `run_container()`'s existing body completely unchanged, including
|
||||
the unmount/cleanup that already runs after `run_bwrap()` returns (no
|
||||
separate watcher/reaper — the daemonized child *is* what runs the whole
|
||||
session, start to finish).
|
||||
`inspect_image_command()` implements `-i/--inspect
|
||||
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
|
||||
volumes, default command) without mounting or running the image — extend it
|
||||
@@ -190,6 +204,12 @@ Source layout (all under `src/`):
|
||||
*sandboxed command's own* lookup is affected by a `--env PATH=...` override
|
||||
(as expected — same as overriding `PATH` in any real shell before running a
|
||||
bare command name), and `bwrap`/`nsenter` are always found regardless.
|
||||
`run_bwrap()` also takes an optional `on_bwrap_pid_known` callback, invoked
|
||||
alongside (not instead of) the session-lock-creation lambda, at the exact
|
||||
same `on_start` timing — `-D/--daemonize` (`daemonize.{h,cpp}`, see below)
|
||||
hooks in here via `report_daemon_started()` to learn the real pid at the
|
||||
same instant everything else that needs it does, rather than needing its own
|
||||
separate pid-discovery mechanism.
|
||||
- `priv_drop_helper.cpp` → the separate `slocker-lite-priv-drop` binary (its own
|
||||
`executable()` target in `meson.build`, **built with `-static`**). Deliberately
|
||||
has zero dependencies on the rest of this project (no fmt/spdlog/etc.) and is
|
||||
@@ -250,7 +270,11 @@ Source layout (all under `src/`):
|
||||
`$HOME/.local/state/...` when `XDG_STATE_HOME` is unset/empty — same
|
||||
resolution pattern as `config_file_path()` below, for state instead of
|
||||
config), sanitizing `container_name` first (anything outside `[A-Za-z0-9._-]`
|
||||
→ `_`, since an image name/tag can contain `/` or `:`). `create_session_lock()`
|
||||
→ `_`, since an image name/tag can contain `/` or `:`). `session_log_file_path()`
|
||||
is a sibling resolving to `$XDG_STATE_HOME/slocker-lite/logs/<container_name>-<pid>.log`
|
||||
instead — same sanitization, same `$XDG_STATE_HOME`/`$HOME` fallback, just a
|
||||
different subdirectory and a `.log` extension — used by `daemonize.{h,cpp}`
|
||||
(see below) for `-D/--daemonize`'s log file. `create_session_lock()`
|
||||
creates the file (`O_CREAT|O_WRONLY|O_TRUNC|O_CLOEXEC`, mode 0644 — `O_CLOEXEC`
|
||||
matters: this fd must never leak into the sandboxed command's own fd table),
|
||||
writes the pid as text, and takes an exclusive, non-blocking `flock()` on it —
|
||||
@@ -285,6 +309,56 @@ Source layout (all under `src/`):
|
||||
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
|
||||
left untouched and not reported.
|
||||
- `daemonize.{h,cpp}` — implements `-D/--daemonize`'s fork/detach mechanics.
|
||||
`daemonize(container_name)` sets up a `pipe2(..., O_CLOEXEC)` pair (so it
|
||||
never leaks into `bwrap`/the sandboxed command, same reasoning as the pid
|
||||
file's own `O_CLOEXEC`) and `fork()`s. The **child** calls `setsid()` —
|
||||
deliberately here, not via re-adding bwrap's own `--new-session` (removed
|
||||
earlier, see the `build_bwrap_args()` comment): `--new-session` only calls
|
||||
`setsid()` for the deeply-nested sandboxed command *inside* bwrap's own
|
||||
namespace setup, leaving the outer `bwrap`/`nsenter`/`slocker-lite` processes
|
||||
still attached to the *original* session and still receiving its signals
|
||||
(e.g. a `SIGHUP` when the controlling terminal closes) — not real
|
||||
daemonization. Calling `setsid()` in our own forked child, *before* it execs
|
||||
into `nsenter`/`bwrap`, detaches the entire chain at once, since `exec()`
|
||||
never changes session membership — confirmed by direct testing
|
||||
(`ps -o pid,sid,pgid,tty`): the daemon child becomes its own session leader
|
||||
with no controlling tty, and `bwrap` (a later descendant) shares that same
|
||||
session, also with no tty. The child also `sigaction()`s `SIGHUP` to
|
||||
`SIG_IGN` (survives the later `exec()` into `nsenter`/`bwrap`, unlike a real
|
||||
handler, which `exec()` resets to default — confirmed by sending `SIGHUP`
|
||||
directly to a running daemonized `bwrap` pid and it staying alive), then
|
||||
redirects stdin to `/dev/null` and stdout/stderr to a log file at
|
||||
`session_log_file_path(container_name, getpid())` (`pid_file.h`) — named
|
||||
after its *own* pid since the real session pid (`bwrap`'s) isn't known yet.
|
||||
If the log directory/file can't be set up at all, that's a **hard** failure
|
||||
here (`_exit(1)`), not best-effort — silently losing the very output
|
||||
`--daemonize` was asked to capture would defeat the point of the flag. The
|
||||
child reports `"LOG <path>\n"` over the pipe immediately (so the parent can
|
||||
show a useful location even on failure) and returns `nullopt` to its caller
|
||||
(`run_container()`, `main.cpp`), which then falls through into the rest of
|
||||
that function's existing body completely unchanged — **the daemonized child
|
||||
is what runs the whole rest of `run_container()`, including the unmount/
|
||||
cleanup that already existed after `run_bwrap()` returns; no separate
|
||||
watcher/reaper process exists**. The **parent** blocks reading the pipe until
|
||||
EOF, returning the accumulated `"LOG "`/`"PID "` lines as a `DaemonizeResult`
|
||||
— the caller then prints it and exits immediately without running any
|
||||
session logic itself. `report_daemon_started(container_name, pid)` (called
|
||||
from `run_bwrap()`'s new `on_bwrap_pid_known` callback — see `bwrap.{h,cpp}`
|
||||
below — the instant the real `bwrap` pid is known) renames the pid-named log
|
||||
file to `<container_name>-<pid>.log`, re-reports the *updated* `"LOG "` line
|
||||
(a real bug caught by testing: the parent's first `"LOG "` line names the
|
||||
pre-rename, daemon-pid-named path — without a second one, the parent would
|
||||
print a stale filename that doesn't match where the file actually ends up),
|
||||
then `"PID <pid>\n"` and closes its own end of the pipe — must happen here,
|
||||
explicitly, rather than waiting for the pipe to close naturally at the end of
|
||||
the (potentially very long) daemon's lifetime, or the parent would block for
|
||||
as long as the session runs instead of returning promptly. The pipe's write
|
||||
fd and the current log path are tracked as private file-scope state in
|
||||
`daemonize.cpp` (matching `process.cpp`'s own `g_foreground_child_pid`
|
||||
pattern for "there's only ever one of these per process" runtime state),
|
||||
since `report_daemon_started()` is called later, from a different function,
|
||||
not threaded explicitly through every call in between.
|
||||
- `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
|
||||
@@ -398,8 +472,8 @@ Build directory is `buildDir/` (already configured).
|
||||
(see `priv_drop_helper.cpp` in "Project state")
|
||||
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
|
||||
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `--user`, `--group`,
|
||||
`--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `-D/--daemonize`,
|
||||
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`,
|
||||
`-h/--help`, `-V/--version`)
|
||||
- Run tests: `meson test -C buildDir`
|
||||
|
||||
@@ -77,6 +77,7 @@ slocker-lite -V|--version
|
||||
| `-u, --umount <layer-id>` | Unmount a previously mounted layer (the ID printed by `--mount`/`--run`, or from `containers-storage layers`). |
|
||||
| `-c, --cleanup <layer-id>` | Delete a layer and its ancestor chain from local storage (unmount it first). |
|
||||
| `-n, --no-nsenter` | With `--run`, bind the mount directly instead of `nsenter`-ing into `fuse-overlayfs`'s namespace. Automatic when running as root; use this to force it off otherwise. |
|
||||
| `-D, --daemonize` | With `--run`, fork into the background: detaches from the controlling terminal (`setsid()`), ignores `SIGHUP`, and redirects stdin from `/dev/null` and stdout/stderr to a log file under `$XDG_STATE_HOME/slocker-lite/logs/`. Prints the session's pid and log path, then returns — the same pid `--list-processes`/`-e/--exec` use. |
|
||||
| `--user <user>` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. |
|
||||
| `--group <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. |
|
||||
| `--hostname <name>` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. |
|
||||
@@ -117,6 +118,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
|
||||
# Run with extra environment variables, from flags and/or a file
|
||||
./buildDir/slocker-lite -r myimage.tar --env FOO=bar --env-file ./app.env
|
||||
|
||||
# Run in the background; prints its pid and log file, then returns
|
||||
./buildDir/slocker-lite -r myimage.tar -D
|
||||
|
||||
# List every OCI image tar in a directory
|
||||
./buildDir/slocker-lite -l ./images
|
||||
|
||||
@@ -208,6 +212,20 @@ 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.
|
||||
|
||||
`-D/--daemonize` forks and detaches into the background by calling `setsid()`
|
||||
itself, rather than re-enabling `bwrap`'s own `--new-session` — that flag only
|
||||
detaches the deeply-nested sandboxed command, leaving `bwrap`/`nsenter` still
|
||||
attached to the original session. Calling `setsid()` in `slocker-lite`'s own
|
||||
forked child, before it execs into `nsenter`/`bwrap`, detaches the whole chain
|
||||
at once (`exec()` never changes session membership), and correctly scopes
|
||||
`bwrap`'s own `--die-with-parent` to that child. The child ignores `SIGHUP` and
|
||||
redirects output to a log file before doing anything else; the original,
|
||||
still-foreground process waits only long enough to learn the real session pid
|
||||
(the same one `--list-processes`/`-e/--exec` use) before printing it and
|
||||
returning — the detached child is what runs the entire session afterward,
|
||||
including the same unmount/cleanup that always ran once the sandboxed command
|
||||
exits.
|
||||
|
||||
See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the
|
||||
reasoning behind each of the above, and known gaps).
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ configure_file(output : 'config.h', configuration : conf_data)
|
||||
slocker_lite = executable('slocker-lite',
|
||||
['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/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp'],
|
||||
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp'],
|
||||
include_directories : include_directories('.'),
|
||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||
install : true)
|
||||
|
||||
+9
-2
@@ -333,7 +333,8 @@ std::optional<std::vector<std::string>> wrap_for_root_namespace(const std::strin
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
||||
const std::optional<std::string>& hostname, const std::string& container_name,
|
||||
const std::vector<std::pair<std::string, std::string>>& extra_env) {
|
||||
const std::vector<std::pair<std::string, std::string>>& extra_env,
|
||||
const std::function<void(pid_t)>& on_bwrap_pid_known) {
|
||||
if (user && !find_priv_drop_helper()) {
|
||||
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
|
||||
kPrivDropHelperName);
|
||||
@@ -348,7 +349,13 @@ int run_bwrap(const std::string& root, const std::vector<std::string>& command,
|
||||
|
||||
std::optional<SessionLock> session_lock;
|
||||
int exit_code = run_process_foreground(
|
||||
*argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); },
|
||||
*argv,
|
||||
[&](pid_t pid) {
|
||||
session_lock = create_session_lock(container_name, pid);
|
||||
if (on_bwrap_pid_known) {
|
||||
on_bwrap_pid_known(pid);
|
||||
}
|
||||
},
|
||||
build_sandbox_env(user, extra_env));
|
||||
|
||||
if (session_lock) {
|
||||
|
||||
+10
-3
@@ -16,11 +16,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "volume_mount.h"
|
||||
|
||||
// Probes the running kernel for which Linux namespace types can actually be
|
||||
@@ -83,9 +86,13 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
// with `extra_env` (from --env/--env-file, see env_spec.h) appended after the
|
||||
// built-in PATH/HOME/PWD/TERM, and passed to run_process_foreground()'s own env
|
||||
// override, rather than relying on bwrap's own --clearenv/--setenv (which
|
||||
// build_bwrap_args() no longer uses). Returns bwrap's exit code, or -1 on
|
||||
// failure to launch.
|
||||
// build_bwrap_args() no longer uses). If `on_bwrap_pid_known` is set, it's
|
||||
// called (alongside, not instead of, the session-lock creation above) at the
|
||||
// same instant the real pid becomes known -- e.g. -D/--daemonize uses this to
|
||||
// report that pid back to the still-waiting original process (see
|
||||
// daemonize.h). Returns bwrap's exit code, or -1 on failure to launch.
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
||||
const std::optional<std::string>& hostname, const std::string& container_name,
|
||||
const std::vector<std::pair<std::string, std::string>>& extra_env);
|
||||
const std::vector<std::pair<std::string, std::string>>& extra_env,
|
||||
const std::function<void(pid_t)>& on_bwrap_pid_known = nullptr);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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 "daemonize.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "pid_file.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Only ever one daemonize() call per process, same "singleton runtime state"
|
||||
// pattern as process.cpp's own g_foreground_child_pid.
|
||||
int g_report_fd = -1;
|
||||
std::filesystem::path g_log_path;
|
||||
|
||||
// Reads whatever the child reports over the pipe until EOF, extracting the
|
||||
// "LOG <path>" and (if present) "PID <pid>" lines. Parent-side only.
|
||||
DaemonizeResult read_daemonize_report(int fd) {
|
||||
std::string buffer;
|
||||
char chunk[256];
|
||||
ssize_t n;
|
||||
while ((n = read(fd, chunk, sizeof(chunk))) > 0) {
|
||||
buffer.append(chunk, static_cast<size_t>(n));
|
||||
}
|
||||
|
||||
DaemonizeResult result;
|
||||
size_t pos = 0;
|
||||
while (pos < buffer.size()) {
|
||||
size_t eol = buffer.find('\n', pos);
|
||||
if (eol == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
std::string line = buffer.substr(pos, eol - pos);
|
||||
pos = eol + 1;
|
||||
|
||||
if (line.rfind("LOG ", 0) == 0) {
|
||||
result.log_path = line.substr(4);
|
||||
} else if (line.rfind("PID ", 0) == 0) {
|
||||
result.pid = static_cast<pid_t>(std::atoi(line.c_str() + 4));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void report_line(const std::string& line) {
|
||||
if (g_report_fd >= 0) {
|
||||
write(g_report_fd, line.data(), line.size());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<DaemonizeResult> daemonize(const std::string& container_name) {
|
||||
int fds[2];
|
||||
if (pipe2(fds, O_CLOEXEC) != 0) {
|
||||
spdlog::error("failed to set up daemonize pipe: {}", strerror(errno));
|
||||
return DaemonizeResult{std::nullopt, ""};
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
spdlog::error("failed to fork for --daemonize: {}", strerror(errno));
|
||||
close(fds[0]);
|
||||
close(fds[1]);
|
||||
return DaemonizeResult{std::nullopt, ""};
|
||||
}
|
||||
|
||||
if (pid > 0) {
|
||||
// Parent: block until the child either reports the real pid or exits
|
||||
// without ever doing so (a hard failure before bwrap ever started).
|
||||
close(fds[1]);
|
||||
auto result = read_daemonize_report(fds[0]);
|
||||
close(fds[0]);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Child: detach from the controlling terminal and everything it implies
|
||||
// (see daemonize.h for why this, not bwrap's own --new-session).
|
||||
close(fds[0]);
|
||||
g_report_fd = fds[1];
|
||||
|
||||
setsid();
|
||||
|
||||
struct sigaction ignore_sighup = {};
|
||||
ignore_sighup.sa_handler = SIG_IGN;
|
||||
sigemptyset(&ignore_sighup.sa_mask);
|
||||
sigaction(SIGHUP, &ignore_sighup, nullptr);
|
||||
|
||||
g_log_path = session_log_file_path(container_name, getpid());
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(g_log_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
spdlog::error("failed to create log directory {}: {}", g_log_path.parent_path().string(), ec.message());
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
int log_fd = open(g_log_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
|
||||
if (log_fd < 0) {
|
||||
spdlog::error("failed to open log file {}: {}", g_log_path.string(), strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
int null_fd = open("/dev/null", O_RDONLY);
|
||||
if (null_fd >= 0) {
|
||||
dup2(null_fd, STDIN_FILENO);
|
||||
close(null_fd);
|
||||
}
|
||||
dup2(log_fd, STDOUT_FILENO);
|
||||
dup2(log_fd, STDERR_FILENO);
|
||||
close(log_fd);
|
||||
|
||||
report_line(fmt::format("LOG {}\n", g_log_path.string()));
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void report_daemon_started(const std::string& container_name, pid_t pid) {
|
||||
if (g_report_fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto new_path = session_log_file_path(container_name, pid);
|
||||
if (new_path != g_log_path) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(g_log_path, new_path, ec);
|
||||
if (ec) {
|
||||
spdlog::warn("failed to rename log file {} to {}: {}", g_log_path.string(), new_path.string(),
|
||||
ec.message());
|
||||
} else {
|
||||
g_log_path = new_path;
|
||||
// The parent already got an earlier "LOG" line naming the pre-rename
|
||||
// path (sent before the real pid was known) -- report the updated one
|
||||
// so it prints the path the file actually ends up at, not a stale one.
|
||||
report_line(fmt::format("LOG {}\n", g_log_path.string()));
|
||||
}
|
||||
}
|
||||
|
||||
report_line(fmt::format("PID {}\n", pid));
|
||||
close(g_report_fd);
|
||||
g_report_fd = -1;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 <optional>
|
||||
#include <string>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
struct DaemonizeResult {
|
||||
std::optional<pid_t> pid; // the real session pid, if the child got that far
|
||||
std::string log_path; // where the daemon's output ended up
|
||||
};
|
||||
|
||||
// Forks and detaches the calling process into a background daemon: the child
|
||||
// calls setsid() -- becoming a new session leader, detaching the *entire*
|
||||
// nsenter/bwrap/sandboxed-command chain from the controlling terminal at once,
|
||||
// since exec() never changes session membership. This is deliberately done
|
||||
// here rather than via bwrap's own --new-session, which only calls setsid()
|
||||
// for the deeply-nested sandboxed command itself, leaving bwrap/nsenter/this
|
||||
// process still attached to the original session (and still receiving its
|
||||
// signals, e.g. a SIGHUP when the controlling terminal closes) -- not real
|
||||
// daemonization. The child also ignores SIGHUP (inherited across the later
|
||||
// exec() into nsenter/bwrap, since SIG_IGN -- unlike a real handler -- survives
|
||||
// exec()) and redirects stdin to /dev/null and stdout/stderr to a log file
|
||||
// under $XDG_STATE_HOME/slocker-lite/logs/ (session_log_file_path(), see
|
||||
// pid_file.h), initially named after its own pid since the real session pid
|
||||
// isn't known yet (see report_daemon_started() below). If the log
|
||||
// file/directory can't be set up at all, that's a hard failure, not
|
||||
// best-effort -- silently losing the very output --daemonize was asked to
|
||||
// capture would defeat the point of the flag.
|
||||
//
|
||||
// Returns nullopt in the child -- the caller should continue on to do the
|
||||
// real work. In the parent, blocks until the child either reports the real
|
||||
// session pid (via report_daemon_started()) or exits without ever doing so,
|
||||
// then returns a result describing what happened -- the caller should print
|
||||
// it and exit immediately, without running any further session logic.
|
||||
std::optional<DaemonizeResult> daemonize(const std::string& container_name);
|
||||
|
||||
// Called from the daemonized child once the real session pid is known (from
|
||||
// run_bwrap()'s on_bwrap_pid_known callback): renames the pid-named log file
|
||||
// to <container_name>-<pid>.log and reports success back to the parent still
|
||||
// blocked in daemonize(), which then returns and can exit. A failed rename is
|
||||
// only a warning -- the daemon keeps running under the old filename, matching
|
||||
// this project's established best-effort-on-auxiliary-state-tracking
|
||||
// convention (e.g. pid_file.cpp's own lock/removal warnings).
|
||||
void report_daemon_started(const std::string& container_name, pid_t pid);
|
||||
+48
-8
@@ -18,6 +18,7 @@
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <getopt.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -34,6 +35,7 @@
|
||||
#include "config.h"
|
||||
#include "config_file.h"
|
||||
#include "containers_storage.h"
|
||||
#include "daemonize.h"
|
||||
#include "env_spec.h"
|
||||
#include "exec_session.h"
|
||||
#include "oci_image.h"
|
||||
@@ -81,7 +83,7 @@ constexpr int kCleanProcessesOpt = 264;
|
||||
constexpr int kEnvOpt = 265;
|
||||
constexpr int kEnvFileOpt = 266;
|
||||
|
||||
constexpr std::array<struct option, 24> kLongOptions = {{
|
||||
constexpr std::array<struct option, 25> kLongOptions = {{
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
{"version", no_argument, nullptr, 'V'},
|
||||
{"test", no_argument, nullptr, 't'},
|
||||
@@ -91,6 +93,7 @@ constexpr std::array<struct option, 24> kLongOptions = {{
|
||||
{"run", required_argument, nullptr, 'r'},
|
||||
{"cleanup", required_argument, nullptr, 'c'},
|
||||
{"no-nsenter", no_argument, nullptr, 'n'},
|
||||
{"daemonize", no_argument, nullptr, 'D'},
|
||||
{"list-images", required_argument, nullptr, 'l'},
|
||||
{"user", required_argument, nullptr, kUserOpt},
|
||||
{"group", required_argument, nullptr, kGroupOpt},
|
||||
@@ -144,6 +147,14 @@ void print_usage(const char* prog) {
|
||||
" (this is automatic when running as root, where\n"
|
||||
" the mount is already directly visible; pass\n"
|
||||
" this to force it off otherwise)\n"
|
||||
" -D, --daemonize with --run, fork into the background: detaches\n"
|
||||
" from the controlling terminal (setsid()) and\n"
|
||||
" ignores SIGHUP, redirecting stdin from /dev/null\n"
|
||||
" and stdout/stderr to a log file under\n"
|
||||
" $XDG_STATE_HOME/slocker-lite/logs/. Prints the\n"
|
||||
" session's pid and log path, then returns\n"
|
||||
" immediately -- the same pid --list-processes/\n"
|
||||
" -e/--exec use\n"
|
||||
" --user <user> with --run, run the command as this user (name\n"
|
||||
" or numeric uid) instead of the image's own\n"
|
||||
" declared user (or root, if it declares none),\n"
|
||||
@@ -540,7 +551,31 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
const std::optional<std::string>& user, const std::optional<std::string>& group,
|
||||
const std::optional<std::string>& hostname,
|
||||
const std::vector<std::pair<std::string, std::string>>& volume_specs,
|
||||
const std::vector<EnvSpec>& env_specs, const AppConfig& app_config) {
|
||||
const std::vector<EnvSpec>& env_specs, bool daemonize_flag, const AppConfig& app_config) {
|
||||
// Only depends on image_tar, so this can run before mount_image() -- moved
|
||||
// up here (rather than right before the run_bwrap() call, as before) so
|
||||
// daemonize() below can use the real container name for the log file from
|
||||
// its very first line, not just after a later rename.
|
||||
auto image_ref = read_image_ref(image_tar);
|
||||
std::string container_name =
|
||||
image_ref ? fmt::format("{}:{}", image_ref->name, image_ref->tag) : image_tar.stem().string();
|
||||
|
||||
if (daemonize_flag) {
|
||||
auto result = daemonize(container_name);
|
||||
if (result) {
|
||||
// Parent (or a hard daemonize failure before ever forking) -- report
|
||||
// and return immediately. The child falls through below instead.
|
||||
if (result->pid) {
|
||||
fmt::print("started in background: pid {}, log: {}\n", *result->pid, result->log_path);
|
||||
return 0;
|
||||
}
|
||||
spdlog::error("failed to start in background{}", result->log_path.empty()
|
||||
? ""
|
||||
: fmt::format(" (see log: {})", result->log_path));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
auto mounted = mount_image(image_tar);
|
||||
if (!mounted) {
|
||||
return 1;
|
||||
@@ -606,14 +641,15 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
: std::vector<std::string>{"/bin/sh"};
|
||||
}
|
||||
|
||||
auto image_ref = read_image_ref(image_tar);
|
||||
std::string container_name =
|
||||
image_ref ? fmt::format("{}:{}", image_ref->name, image_ref->tag) : image_tar.stem().string();
|
||||
std::function<void(pid_t)> on_bwrap_pid_known;
|
||||
if (daemonize_flag) {
|
||||
on_bwrap_pid_known = [&](pid_t pid) { report_daemon_started(container_name, pid); };
|
||||
}
|
||||
|
||||
int exit_code = -1;
|
||||
if (ok) {
|
||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname,
|
||||
container_name, *resolved_env);
|
||||
container_name, *resolved_env, on_bwrap_pid_known);
|
||||
if (exit_code < 0) {
|
||||
spdlog::error("failed to run bwrap");
|
||||
}
|
||||
@@ -644,6 +680,7 @@ int main(int argc, char* argv[]) {
|
||||
Mode mode = Mode::kNone;
|
||||
std::string mode_arg;
|
||||
bool disable_nsenter = false;
|
||||
bool daemonize_flag = false;
|
||||
std::optional<std::string> user_flag;
|
||||
std::optional<std::string> group_flag;
|
||||
std::optional<std::string> hostname_flag;
|
||||
@@ -652,7 +689,7 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
opterr = 0;
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:", kLongOptions.data(), nullptr)) != -1) {
|
||||
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:D", kLongOptions.data(), nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
@@ -744,6 +781,9 @@ int main(int argc, char* argv[]) {
|
||||
case 'n':
|
||||
disable_nsenter = true;
|
||||
break;
|
||||
case 'D':
|
||||
daemonize_flag = true;
|
||||
break;
|
||||
case kLogLevelOpt:
|
||||
if (!apply_log_level(optarg)) {
|
||||
return 1;
|
||||
@@ -861,7 +901,7 @@ int main(int argc, char* argv[]) {
|
||||
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
||||
}
|
||||
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, hostname_flag, volume_specs,
|
||||
env_specs, *config);
|
||||
env_specs, daemonize_flag, *config);
|
||||
}
|
||||
|
||||
auto mounted = mount_image(mode_arg);
|
||||
|
||||
+10
-2
@@ -44,7 +44,7 @@ std::string sanitize_for_filename(std::string_view name) {
|
||||
return result.empty() ? "container" : result;
|
||||
}
|
||||
|
||||
std::filesystem::path session_run_dir() {
|
||||
std::filesystem::path xdg_state_dir() {
|
||||
const char* xdg_state_home = std::getenv("XDG_STATE_HOME");
|
||||
std::filesystem::path state_home;
|
||||
if (xdg_state_home && *xdg_state_home) {
|
||||
@@ -53,7 +53,11 @@ std::filesystem::path session_run_dir() {
|
||||
const char* home = std::getenv("HOME");
|
||||
state_home = std::filesystem::path(home ? home : "") / ".local" / "state";
|
||||
}
|
||||
return state_home / "slocker-lite" / "run";
|
||||
return state_home / "slocker-lite";
|
||||
}
|
||||
|
||||
std::filesystem::path session_run_dir() {
|
||||
return xdg_state_dir() / "run";
|
||||
}
|
||||
|
||||
struct ParsedSessionFile {
|
||||
@@ -102,6 +106,10 @@ std::filesystem::path session_pid_file_path(std::string_view container_name, pid
|
||||
return session_run_dir() / fmt::format("{}-{}", sanitize_for_filename(container_name), pid);
|
||||
}
|
||||
|
||||
std::filesystem::path session_log_file_path(std::string_view container_name, pid_t pid) {
|
||||
return xdg_state_dir() / "logs" / fmt::format("{}-{}.log", sanitize_for_filename(container_name), pid);
|
||||
}
|
||||
|
||||
std::optional<SessionLock> create_session_lock(std::string_view container_name, pid_t pid) {
|
||||
auto path = session_pid_file_path(container_name, pid);
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ struct SessionLock {
|
||||
// names/tags can contain '/' (registry paths) or ':'.
|
||||
std::filesystem::path session_pid_file_path(std::string_view container_name, pid_t pid);
|
||||
|
||||
// $XDG_STATE_HOME/slocker-lite/logs/<container_name>-<pid>.log (or the
|
||||
// $HOME/.local/state/... fallback), sibling to session_pid_file_path() above --
|
||||
// same sanitization, same directory resolution, just a different subdirectory
|
||||
// and a .log extension. Used by daemonize.{h,cpp} for -D/--daemonize's log file.
|
||||
std::filesystem::path session_log_file_path(std::string_view container_name, pid_t pid);
|
||||
|
||||
// Creates the pid file for (container_name, pid) (creating its parent directory if
|
||||
// needed), writes `pid` as text, and takes an exclusive advisory flock() on it (see
|
||||
// SessionLock above). Returns nullopt (logging a warning, never fatal -- session
|
||||
|
||||
Reference in New Issue
Block a user