Files
slocker-lite/src/daemonize.cpp
T
ceamac f904d33a11 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
2026-08-23 16:41:07 +00:00

168 lines
5.1 KiB
C++

// 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;
}