use std::{
fs,
io::{self, ErrorKind, Read, Write},
net::Shutdown,
os::unix::{
fs::{DirBuilderExt, MetadataExt, PermissionsExt},
net::{UnixListener, UnixStream},
process::CommandExt,
},
panic::{AssertUnwindSafe, catch_unwind},
path::{Path, PathBuf},
process::Stdio,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::channel,
},
thread,
time::{Duration, Instant},
};
use nix::{
fcntl::{Flock, FlockArg},
sys::signal::{Signal, kill},
unistd::Pid,
};
use crate::{
core::{LoopExit, Wake, run_loop},
frame::{MAX_FRAME, SEND_TIMEOUT, read_frame, write_frame},
protocol::{
Command, Event, LaunchContext, PROTOCOL_VERSION, decode_command, decode_event,
decode_hello, encode_command, encode_event, encode_hello, hello_version,
},
supervisor::{self, Supervisor},
};
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
const HELLO_PROBE: Duration = Duration::from_secs(1);
pub const FLEETCOM_RUNTIME_DIR: &str = "FLEETCOM_RUNTIME_DIR";
fn runtime_dir() -> PathBuf {
resolve_runtime_dir(
std::env::var(FLEETCOM_RUNTIME_DIR).ok(),
std::env::var("XDG_RUNTIME_DIR").ok(),
std::env::temp_dir(),
nix::unistd::getuid().as_raw(),
)
}
fn resolve_runtime_dir(
override_dir: Option<String>,
xdg: Option<String>,
tmp: PathBuf,
uid: u32,
) -> PathBuf {
if let Some(d) = override_dir {
return PathBuf::from(d);
}
if let Some(d) = xdg
&& !d.is_empty()
{
return PathBuf::from(d).join("fleetcom");
}
tmp.join(format!("fleetcom-{uid}"))
}
fn socket_path() -> PathBuf {
socket_in(&runtime_dir())
}
fn socket_in(dir: &Path) -> PathBuf {
dir.join("default.sock")
}
fn ensure_runtime_dir(dir: &Path) -> io::Result<()> {
match fs::symlink_metadata(dir) {
Ok(md) => validate_runtime_dir(dir, &md),
Err(e) if e.kind() == ErrorKind::NotFound => {
fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(dir)?;
let md = fs::symlink_metadata(dir)?;
validate_runtime_dir(dir, &md)
}
Err(e) => Err(e),
}
}
fn validate_runtime_dir(dir: &Path, md: &fs::Metadata) -> io::Result<()> {
if !md.file_type().is_dir() {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
"runtime path exists but is not a directory",
));
}
if md.uid() != nix::unistd::getuid().as_raw() {
return Err(io::Error::new(
ErrorKind::PermissionDenied,
"runtime dir is not owned by this user",
));
}
let mode = md.permissions().mode() & 0o777;
if mode & 0o022 != 0 {
return Err(io::Error::new(
ErrorKind::PermissionDenied,
"runtime dir is writable by other users; refusing to trust its contents",
));
}
if mode & 0o077 != 0 {
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
fn read_frame_bounded(stream: &mut UnixStream, timeout: Duration) -> io::Result<(u8, Vec<u8>)> {
stream.set_read_timeout(Some(timeout))?;
let res = read_frame(stream);
stream.set_read_timeout(None)?;
res
}
fn is_timeout(e: &io::Error) -> bool {
matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut)
}
fn hello_read_error(e: io::Error) -> io::Error {
if e.kind() == ErrorKind::UnexpectedEof {
io::Error::new(
ErrorKind::ConnectionAborted,
"the daemon closed the connection during the handshake (it may be \
shutting down); rerun fleetcom to start a fresh one",
)
} else {
e
}
}
fn check_hello_ack(kind: u8, payload: &[u8]) -> io::Result<()> {
match decode_event(kind, payload) {
Some(Event::HelloOk) => Ok(()),
Some(Event::Status(msg)) => Err(io::Error::other(msg)),
_ => Err(io::Error::other(
"daemon predates the protocol handshake (stale daemon from an older \
fleetcom); run 'fleetcom --kill' and retry",
)),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DaemonOrigin {
AlreadyRunning,
Autostarted,
}
fn scrollback_notice(origin: DaemonOrigin, flag: Option<usize>) -> Option<String> {
match (origin, flag) {
(DaemonOrigin::AlreadyRunning, Some(lines)) => Some(format!(
"--scrollback {lines} ignored: the daemon was already running and \
keeps its scrollback until 'fleetcom --kill'"
)),
_ => None,
}
}
pub fn ignored_scrollback_notice(origin: DaemonOrigin) -> Option<String> {
scrollback_notice(origin, supervisor::scrollback_flag())
}
pub fn connect_ready() -> io::Result<(UnixStream, DaemonOrigin)> {
let (mut stream, origin) = connect_or_autostart()?;
let done = Arc::new(AtomicBool::new(false));
{
let done = Arc::clone(&done);
thread::spawn(move || {
thread::sleep(HELLO_PROBE);
if !done.load(Ordering::Relaxed) {
eprintln!(
"fleetcom: the daemon is serving another client; waiting \
to attach (Ctrl-C to abort)"
);
}
});
}
let (kind, payload) = encode_hello(&LaunchContext::here());
write_frame(&mut stream, kind, &payload)?;
let reply = read_frame(&mut stream);
done.store(true, Ordering::Relaxed);
let (kind, payload) = reply.map_err(hello_read_error)?;
check_hello_ack(kind, &payload)?;
Ok((stream, origin))
}
fn busy_daemon_error(e: io::Error) -> io::Error {
if is_timeout(&e) {
io::Error::new(
ErrorKind::TimedOut,
"the daemon is serving another client; retry after it detaches",
)
} else {
e
}
}
pub fn connect_ready_bounded() -> io::Result<UnixStream> {
let (mut stream, _) = connect_or_autostart()?;
stream.set_write_timeout(Some(HANDSHAKE_TIMEOUT))?;
let (kind, payload) = encode_hello(&LaunchContext::here());
write_frame(&mut stream, kind, &payload).map_err(busy_daemon_error)?;
stream.set_write_timeout(None)?;
let (kind, payload) = read_frame_bounded(&mut stream, HANDSHAKE_TIMEOUT)
.map_err(|e| hello_read_error(busy_daemon_error(e)))?;
check_hello_ack(kind, &payload)?;
Ok(stream)
}
fn connect_or_autostart() -> io::Result<(UnixStream, DaemonOrigin)> {
connect_or_autostart_in(&runtime_dir())
}
fn connect_or_autostart_in(dir: &Path) -> io::Result<(UnixStream, DaemonOrigin)> {
ensure_runtime_dir(dir)?;
let path = socket_in(dir);
if let Ok(s) = UnixStream::connect(&path) {
return Ok((s, DaemonOrigin::AlreadyRunning));
}
spawn_daemon()?;
for _ in 0..100 {
if let Ok(s) = UnixStream::connect(&path) {
return Ok((s, DaemonOrigin::Autostarted));
}
thread::sleep(Duration::from_millis(10));
}
Err(io::Error::new(
ErrorKind::TimedOut,
format!(
"daemon did not come up; check {}",
dir.join("daemon.log").display()
),
))
}
fn spawn_daemon() -> io::Result<()> {
let exe = std::env::current_exe()?;
let dir = runtime_dir();
ensure_runtime_dir(&dir)?;
let log = fs::File::create(dir.join("daemon.log")).ok();
let mut cmd = std::process::Command::new(exe);
cmd.arg("--daemon")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(log.map(Stdio::from).unwrap_or_else(Stdio::null))
.process_group(0);
if let Some(lines) = supervisor::scrollback_flag() {
cmd.env(supervisor::FLEETCOM_SCROLLBACK, lines.to_string());
}
cmd.spawn()?;
Ok(())
}
pub fn run_kill() -> io::Result<()> {
let dir = runtime_dir();
ensure_runtime_dir(&dir)?;
let lock_path = dir.join("daemon.lock");
let Ok(file) = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&lock_path)
else {
eprintln!("fleetcom: no daemon running");
return Ok(());
};
let mut file = match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(_held) => {
eprintln!("fleetcom: no daemon running");
return Ok(());
}
Err((file, _)) => file,
};
let mut pid_str = String::new();
file.read_to_string(&mut pid_str)?;
let Some(pid) = pid_str.trim().parse::<i32>().ok().filter(|p| *p > 0) else {
return kill_via_socket();
};
match kill(Pid::from_raw(pid), Signal::SIGTERM) {
Ok(()) | Err(nix::errno::Errno::ESRCH) => {}
Err(e) => return Err(io::Error::other(e)),
}
for _ in 0..200 {
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
Ok(_held) => return Ok(()),
Err((f, _)) => file = f,
}
thread::sleep(Duration::from_millis(50));
}
Err(io::Error::new(
ErrorKind::TimedOut,
"daemon did not exit after SIGTERM",
))
}
const KILL_SOCKET_TIMEOUT: Duration = Duration::from_secs(10);
fn kill_handshake_timeout() -> io::Error {
io::Error::new(
ErrorKind::TimedOut,
"the daemon is running but did not complete the kill handshake in \
time (another client may be attached); retry after it detaches, or \
send SIGTERM to the daemon process directly",
)
}
fn arm_read_deadline(s: &UnixStream, deadline: Instant) -> io::Result<()> {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(kill_handshake_timeout());
}
s.set_read_timeout(Some(left))
}
fn deadline_mapped(e: io::Error) -> io::Error {
if is_timeout(&e) {
kill_handshake_timeout()
} else {
e
}
}
fn kill_via_socket() -> io::Result<()> {
kill_via_socket_at(&socket_path(), KILL_SOCKET_TIMEOUT)
}
fn kill_via_socket_at(path: &Path, budget: Duration) -> io::Result<()> {
match UnixStream::connect(path) {
Ok(mut s) => kill_over_stream(&mut s, budget),
Err(_) => {
eprintln!("fleetcom: no daemon running");
Ok(())
}
}
}
fn kill_over_stream(s: &mut UnixStream, budget: Duration) -> io::Result<()> {
let (kind, payload) = encode_hello(&LaunchContext::here());
kill_exchange(s, budget, kind, &payload)
}
fn kill_exchange(
s: &mut UnixStream,
budget: Duration,
hello_kind: u8,
hello_payload: &[u8],
) -> io::Result<()> {
let deadline = Instant::now() + budget;
s.set_write_timeout(Some(budget))?;
write_frame(s, hello_kind, hello_payload).map_err(deadline_mapped)?;
arm_read_deadline(s, deadline)?;
let (kind, payload) = read_frame(s).map_err(deadline_mapped)?;
check_hello_ack(kind, &payload)?;
let (kind, payload) = encode_command(&Command::Shutdown);
write_frame(s, kind, &payload).map_err(deadline_mapped)?;
let mut buf = [0u8; 256];
loop {
arm_read_deadline(s, deadline)?;
match s.read(&mut buf) {
Ok(0) => return Ok(()),
Ok(_) => {}
Err(e) if is_timeout(&e) => return Err(kill_handshake_timeout()),
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e)
if matches!(
e.kind(),
ErrorKind::ConnectionReset | ErrorKind::BrokenPipe | ErrorKind::UnexpectedEof
) =>
{
return Ok(());
}
Err(e) => return Err(e),
}
}
}
pub fn run_daemon() -> io::Result<()> {
let dir = runtime_dir();
ensure_runtime_dir(&dir)?; let path = socket_path();
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false) .open(dir.join("daemon.lock"))?;
let mut lock = match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
Ok(l) => l,
Err(_) => return Ok(()), };
lock.set_len(0)?;
lock.write_all(std::process::id().to_string().as_bytes())?;
let _ = fs::remove_file(&path);
let listener = UnixListener::bind(&path)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
let mut sup = Supervisor::new(24, 80, supervisor::resolve_scrollback());
let term = Arc::new(AtomicBool::new(false));
crate::install_signal_handlers(Arc::clone(&term))?;
listener.set_nonblocking(true)?;
const IDLE_REAP: Duration = Duration::from_millis(100);
loop {
if term.load(Ordering::Relaxed) {
sup.apply(Command::Shutdown);
break;
}
match listener.accept() {
Ok((stream, _)) => {
if let Err(e) = stream.set_nonblocking(false) {
eprintln!("fleetcom: dropping client, cannot set stream blocking: {e}");
continue;
}
if serve_client(&mut sup, stream, &term) == ServeOutcome::Shutdown {
break;
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock || transient_accept_error(&e) => {
sup.reap();
sup.recovery_maintenance();
thread::sleep(IDLE_REAP);
}
Err(e) => {
eprintln!("fleetcom: accept failed, shutting down: {e}");
break;
}
}
}
let _ = fs::remove_file(&path);
Ok(())
}
fn transient_accept_error(e: &io::Error) -> bool {
use nix::errno::Errno;
matches!(
e.raw_os_error(),
Some(code) if code == Errno::EMFILE as i32
|| code == Errno::ENFILE as i32
|| code == Errno::EINTR as i32
|| code == Errno::ECONNABORTED as i32
)
}
#[derive(PartialEq)]
enum ServeOutcome {
Disconnected,
Shutdown,
}
fn handshake(stream: &mut UnixStream) -> Result<LaunchContext, String> {
let (kind, payload) = read_frame_bounded(stream, HANDSHAKE_TIMEOUT)
.map_err(|e| format!("no valid hello received: {e}"))?;
let mismatch = |version: u32| {
format!(
"protocol mismatch: daemon {} speaks v{PROTOCOL_VERSION}, client speaks \
v{version}; run 'fleetcom --kill' and retry",
env!("CARGO_PKG_VERSION"),
)
};
match decode_hello(kind, &payload) {
Some((PROTOCOL_VERSION, ctx)) => Ok(ctx),
Some((version, _)) => Err(mismatch(version)),
None => match hello_version(kind, &payload) {
Some(version) if version != PROTOCOL_VERSION => Err(mismatch(version)),
_ => Err(format!(
"daemon {} requires a hello handshake (older client?); upgrade the \
client or run 'fleetcom --kill' and retry",
env!("CARGO_PKG_VERSION"),
)),
},
}
}
fn send_event(write: &mut impl Write, ev: &Event) -> bool {
let (kind, payload) = encode_event(ev);
if payload.len() > MAX_FRAME as usize {
return true;
}
write_frame(write, kind, &payload).is_ok()
}
fn serve_client(sup: &mut Supervisor, stream: UnixStream, stop: &AtomicBool) -> ServeOutcome {
let mut stream = stream;
match handshake(&mut stream) {
Ok(ctx) => {
sup.set_launch_context(ctx);
let (kind, payload) = encode_event(&Event::HelloOk);
if write_frame(&mut stream, kind, &payload).is_err() {
return ServeOutcome::Disconnected;
}
}
Err(reason) => {
eprintln!("fleetcom: refusing client: {reason}");
let (kind, payload) = encode_event(&Event::Status(reason));
let _ = write_frame(&mut stream, kind, &payload);
return ServeOutcome::Disconnected;
}
}
let Ok(read) = stream.try_clone() else {
return ServeOutcome::Disconnected;
};
let (wake_tx, wake_rx) = channel::<Wake>();
sup.set_waker(wake_tx.clone());
let reader = thread::spawn(move || {
let mut read = read;
while let Ok((kind, payload)) = read_frame(&mut read) {
if let Some(cmd) = decode_command(kind, &payload)
&& wake_tx.send(Wake::Cmd(cmd)).is_err()
{
return;
}
}
let _ = wake_tx.send(Wake::Hangup);
});
let mut write = stream;
let _ = write.set_write_timeout(Some(SEND_TIMEOUT));
let outcome = catch_unwind(AssertUnwindSafe(|| {
run_loop(sup, &wake_rx, stop, |ev| send_event(&mut write, ev))
}));
sup.clear_waker();
sup.clear_watch();
let _ = write.shutdown(Shutdown::Both);
let _ = reader.join();
match outcome {
Ok(LoopExit::Shutdown) => ServeOutcome::Shutdown,
Ok(LoopExit::ClientGone) => ServeOutcome::Disconnected,
Err(_) => {
eprintln!("fleetcom: serve loop panicked; client dropped, fleet kept");
ServeOutcome::Disconnected
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::temp;
#[test]
fn ensure_runtime_dir_rejects_symlink() {
let base = temp("daemon_symlink");
let target = base.join("target");
fs::create_dir(&target).unwrap();
let link = base.join("runtime");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(ensure_runtime_dir(&link).is_err());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn ensure_runtime_dir_rejects_plain_file() {
let base = temp("daemon_file");
let path = base.join("runtime");
fs::write(&path, b"x").unwrap();
assert!(ensure_runtime_dir(&path).is_err());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn connect_refuses_untrusted_runtime_dir() {
let base = temp("daemon_connect_untrusted");
let target = base.join("target");
fs::create_dir(&target).unwrap();
let link = base.join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(connect_or_autostart_in(&link).is_err());
let file = base.join("file");
fs::write(&file, b"x").unwrap();
assert!(connect_or_autostart_in(&file).is_err());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn oversized_event_is_skipped_not_fatal() {
use crate::protocol::ScreenView;
let oversized = Event::Screen(ScreenView {
id: 1,
lines: Vec::new(),
formatted: vec![b'x'; MAX_FRAME as usize + 1],
cursor: (0, 0),
hide_cursor: false,
wants_mouse: false,
alt_screen: false,
alt_scroll: false,
scrollback: 0,
});
let mut buf: Vec<u8> = Vec::new();
assert!(
send_event(&mut buf, &oversized),
"an oversized event must not read as a dead client"
);
assert!(buf.is_empty(), "no partial frame may reach the stream");
assert!(send_event(&mut buf, &Event::Status("ok".into())));
let (kind, payload) = read_frame(&mut io::Cursor::new(&buf)).unwrap();
assert_eq!(
decode_event(kind, &payload),
Some(Event::Status("ok".into())),
"ordinary events still flow after a skip"
);
}
#[test]
fn transient_accept_errors_are_classified() {
use nix::errno::Errno;
for errno in [
Errno::EMFILE,
Errno::ENFILE,
Errno::EINTR,
Errno::ECONNABORTED,
] {
assert!(
transient_accept_error(&io::Error::from_raw_os_error(errno as i32)),
"{errno} should be transient"
);
}
assert!(!transient_accept_error(&io::Error::from_raw_os_error(
Errno::EBADF as i32
)));
assert!(!transient_accept_error(&io::Error::other("no raw errno")));
}
#[test]
fn runtime_dir_resolution_order() {
let tmp = PathBuf::from("/tmpdir");
assert_eq!(
resolve_runtime_dir(
Some("/override".into()),
Some("/xdg".into()),
tmp.clone(),
501
),
PathBuf::from("/override")
);
assert_eq!(
resolve_runtime_dir(None, Some("/run/user/501".into()), tmp.clone(), 501),
PathBuf::from("/run/user/501/fleetcom")
);
assert_eq!(
resolve_runtime_dir(None, Some(String::new()), tmp.clone(), 501),
PathBuf::from("/tmpdir/fleetcom-501")
);
assert_eq!(
resolve_runtime_dir(None, None, tmp, 42),
PathBuf::from("/tmpdir/fleetcom-42")
);
}
#[test]
fn ensure_runtime_dir_creates_private_dir() {
let base = temp("daemon_create");
let path = base.join("runtime");
ensure_runtime_dir(&path).unwrap();
let mode = fs::symlink_metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700, "dir must be private");
ensure_runtime_dir(&path).unwrap();
let _ = fs::remove_dir_all(&base);
}
#[test]
fn ensure_runtime_dir_rejects_a_writable_dir() {
let base = temp("daemon_writable");
for bits in [0o020, 0o002, 0o022] {
let path = base.join(format!("dir_{bits:o}"));
fs::create_dir_all(&path).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o700 | bits)).unwrap();
assert!(
ensure_runtime_dir(&path).is_err(),
"mode 0o{:o} must be refused",
0o700 | bits
);
}
let _ = fs::remove_dir_all(&base);
}
#[test]
fn scrollback_notice_requires_flag_and_preexisting_daemon() {
assert_eq!(
scrollback_notice(DaemonOrigin::Autostarted, Some(50_000)),
None
);
assert_eq!(scrollback_notice(DaemonOrigin::Autostarted, None), None);
assert_eq!(scrollback_notice(DaemonOrigin::AlreadyRunning, None), None);
let notice = scrollback_notice(DaemonOrigin::AlreadyRunning, Some(50_000)).unwrap();
assert!(notice.contains("--scrollback 50000"), "{notice}");
assert!(notice.contains("--kill"), "{notice}");
}
#[test]
fn kill_via_socket_bounds_the_handshake_wait() {
let base = temp("kill_socket_mute");
fs::create_dir_all(&base).unwrap();
let sock = base.join("mute.sock");
let _listener = UnixListener::bind(&sock).unwrap();
let start = Instant::now();
let err = kill_via_socket_at(&sock, Duration::from_millis(200)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::TimedOut);
assert!(err.to_string().contains("kill handshake"), "{err}");
assert!(
start.elapsed() < Duration::from_secs(5),
"the deadline must fire, not the test's timeout"
);
let _ = fs::remove_dir_all(&base);
}
#[test]
fn kill_exchange_maps_a_write_timeout() {
let base = temp("kill_socket_bigenv");
fs::create_dir_all(&base).unwrap();
let sock = base.join("mute.sock");
let _listener = UnixListener::bind(&sock).unwrap();
let mut s = UnixStream::connect(&sock).unwrap();
let oversized = vec![0u8; 8 * 1024 * 1024];
let err = kill_exchange(&mut s, Duration::from_millis(200), 0, &oversized).unwrap_err();
assert_eq!(err.kind(), ErrorKind::TimedOut);
assert!(err.to_string().contains("kill handshake"), "{err}");
let _ = fs::remove_dir_all(&base);
}
#[test]
fn kill_via_socket_bounds_the_drain_wait() {
let base = temp("kill_socket_drain");
fs::create_dir_all(&base).unwrap();
let sock = base.join("stuck.sock");
let listener = UnixListener::bind(&sock).unwrap();
let server = thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let _ = read_frame(&mut s); let (kind, payload) = encode_event(&Event::HelloOk);
let _ = write_frame(&mut s, kind, &payload);
let _ = read_frame(&mut s); let _ = s.read(&mut [0u8; 16]);
});
let err = kill_via_socket_at(&sock, Duration::from_millis(300)).unwrap_err();
assert_eq!(err.kind(), ErrorKind::TimedOut);
server.join().unwrap();
let _ = fs::remove_dir_all(&base);
}
#[test]
fn kill_via_socket_without_a_socket_is_a_noop() {
let base = temp("kill_socket_absent");
fs::create_dir_all(&base).unwrap();
assert!(kill_via_socket_at(&base.join("absent.sock"), Duration::from_millis(100)).is_ok());
let _ = fs::remove_dir_all(&base);
}
#[test]
fn ensure_runtime_dir_tightens_harmless_bits() {
let base = temp("daemon_tighten");
let path = base.join("runtime");
fs::create_dir_all(&path).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
ensure_runtime_dir(&path).unwrap();
let mode = fs::symlink_metadata(&path).unwrap().permissions().mode();
assert_eq!(
mode & 0o777,
0o700,
"harmless bits must be tightened to 0700"
);
let _ = fs::remove_dir_all(&base);
}
}