f904d33a11
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
168 lines
5.1 KiB
C++
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;
|
|
}
|