Files
wanderbooks/crates/ledger-host/src/start.rs
T
swaitsandClaude Fable 5.1 7749afc557
check / check (push) Canceled after 0s
check / portable (macos-latest) (push) Canceled after 0s
check / portable (windows-latest) (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s
fix: Windows reads of lock files, update paths cfg(unix), merge main again (P11c)
spec_vault swept every file under a book, lock files included; on Windows
a held LockFileEx lock is mandatory and the read failed. The locks are
already dedicated files (never data), so the test skips *.lock. Merged
main (P13 step 6, the macOS server.log assert, P11a's AppImage identity):
host.rs keeps main's five-field identity on Unix and size/last-write on
Windows; the update poll and set_program_version are Unix-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
2026-10-29 17:03:30 -06:00

678 lines
26 KiB
Rust

//! Starting the host cleanly (plan 3.9, "Starting the host cleanly").
//!
//! ```text
//! GOAL: the host never inherits anything from whoever opened the books:
//! not their environment, descriptors, session, umask, or the app's
//! scope (which would take the host down with the app).
//! SOURCE: plan/served.md 3.9; test host_starts_clean, host_survives_app_scope.
//! INVARIANTS:
//! 1. The host's environment is a fixed list: PATH (fixed), HOME (from
//! the account's record, never the opener's), and XDG_RUNTIME_DIR
//! and DBUS_SESSION_BUS_ADDRESS only when they point at this
//! account's own private runtime folder, and SSH_AUTH_SOCK only when
//! it names a socket this account owns (the owner's ssh agent, so the
//! server's backups reach a private git repository, D-30). Nothing
//! else passes (a scope's own identifiers, such as INVOCATION_ID, are
//! systemd's). One more: APPIMAGE, only when it names an absolute
//! file, so the host knows its own AppImage (below).
//! 2. Its standard input and output are null, its errors go to
//! run/host.log (0600); it starts a new session with umask 077. Every
//! other descriptor is closed (std opens them close-on-exec, and the
//! host closes any stray one at start).
//! 3. Under a systemd app scope it starts through a transient
//! `systemd-run --user --scope` (nothing installed or kept); if the
//! scope couldn't be made (systemd-run failed without the host ever
//! writing its first line), or there is no such scope, directly. A
//! host that ran and stopped is never started a second time.
//! 4. It runs this executable's file as it is on disk now; for an
//! AppImage that is the image file (APPIMAGE), not the program inside
//! its mount, which goes away with the image it came from. The host
//! reads that path about itself and never tells it to an agent (I-2),
//! so a replaced image restarts the books like any replaced program.
//! 5. The opener doesn't wait for it, and reaps it when it ends.
//! ```
use crate::knobs::Knobs;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[cfg(unix)]
use std::time::{Duration, Instant};
/// The host's PATH (INVARIANT 1).
#[cfg(unix)]
const PATH: &str = "/usr/local/bin:/usr/bin:/bin";
/// Where systemd-run may be (never looked up on the opener's PATH).
#[cfg(unix)]
const SYSTEMD_RUN: [&str; 2] = ["/usr/bin/systemd-run", "/bin/systemd-run"];
/// Windows: a process of its own, without this console, its Ctrl+C or its
/// window (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP): what `setsid`
/// is on Unix.
#[cfg(windows)]
pub(crate) const DETACHED_HOST: u32 = 0x0000_0008 | 0x0000_0200;
/// Windows: leave the job the opener runs in, if it lets its processes
/// (CREATE_BREAKAWAY_FROM_JOB): a host must not end with an agent's job,
/// as it must not end with an app's scope on Linux (INVARIANT 3).
#[cfg(windows)]
const BREAKAWAY: u32 = 0x0100_0000;
/// The host's log, in the root's `run/`.
pub const HOST_LOG: &str = "host.log";
/// A log larger than this is moved aside when a host starts.
const LOG_LIMIT: u64 = 8 * 1024 * 1024;
/// This executable's file as it is on disk now (INVARIANT 4): on Linux a
/// replaced file reads as "… (deleted)"; the path, not the old file, is
/// what a new host runs.
pub fn this_executable() -> Result<PathBuf, String> {
if let Some(image) = own_appimage(std::env::var_os("APPIMAGE").as_deref()) {
return Ok(PathBuf::from(image));
}
let exe = std::env::current_exe().map_err(|e| format!("can't find this program: {e}"))?;
let text = exe.to_string_lossy();
if let Some(path) = text.strip_suffix(" (deleted)")
&& !exe.exists()
{
return Ok(PathBuf::from(path));
}
Ok(exe)
}
/// The AppImage this process runs from: `APPIMAGE` when it is an
/// absolute path to a file.
pub fn own_appimage(value: Option<&std::ffi::OsStr>) -> Option<std::ffi::OsString> {
let path = Path::new(value?);
(path.is_absolute() && path.is_file()).then(|| path.as_os_str().to_os_string())
}
/// This account's user ID.
#[cfg(unix)]
fn own_uid() -> u32 {
// SAFETY: geteuid has no preconditions and can't fail.
unsafe { libc::geteuid() }
}
#[cfg(unix)]
/// `XDG_RUNTIME_DIR` from `value`, if it is this account's own private
/// runtime folder: absolute, a real folder (no link), owned by this
/// account, and closed to everyone else.
pub fn own_runtime_dir(value: Option<&std::ffi::OsStr>) -> Option<PathBuf> {
// Written plainly (no trailing slash, no `.`), as a bus address
// names it.
let dir: PathBuf = PathBuf::from(value?).components().collect();
if !dir.is_absolute() {
return None;
}
let meta = std::fs::symlink_metadata(&dir).ok()?;
(meta.is_dir() && meta.uid() == own_uid() && meta.mode() & 0o077 == 0).then_some(dir)
}
#[cfg(unix)]
/// `DBUS_SESSION_BUS_ADDRESS` from `value`, if it is the bus in `runtime`
/// (`unix:path=<runtime>/bus`, perhaps with a guid after a comma).
pub fn own_bus(value: Option<&std::ffi::OsStr>, runtime: &Path) -> Option<String> {
let value = value?.to_str()?;
let want = format!("unix:path={}/bus", runtime.display());
let rest = value.strip_prefix(&want)?;
(rest.is_empty() || rest.starts_with(',')).then(|| value.to_string())
}
#[cfg(unix)]
/// This account's runtime folder: the one named in the environment if it
/// is ours and private; else, when the opener's environment has none (a
/// client that scrubs it, as Codex does), the system's for this account,
/// `/run/user/<uid>`, under the same checks.
fn runtime_dir() -> Option<PathBuf> {
// The one named, if it passes; else (none named, or one that isn't
// ours or private) the system's for this account.
std::env::var_os("XDG_RUNTIME_DIR")
.and_then(|named| own_runtime_dir(Some(&named)))
.or_else(|| own_runtime_dir(Some(format!("/run/user/{}", own_uid()).as_ref())))
}
#[cfg(unix)]
/// The session bus in `runtime`: the one named in the environment if it
/// is there, else its usual socket there, if one exists.
fn session_bus(runtime: &Path) -> Option<String> {
match std::env::var_os("DBUS_SESSION_BUS_ADDRESS") {
Some(named) => own_bus(Some(&named), runtime),
None => {
use std::os::unix::fs::FileTypeExt;
let socket = runtime.join("bus");
std::fs::symlink_metadata(&socket)
.is_ok_and(|m| m.file_type().is_socket() && m.uid() == own_uid())
.then(|| format!("unix:path={}", socket.display()))
}
}
}
#[cfg(unix)]
/// The ssh agent's socket named in `named`, if it is a socket this
/// account owns at an absolute path (INVARIANT 1).
fn own_agent(named: Option<&std::ffi::OsStr>) -> Option<std::ffi::OsString> {
use std::os::unix::fs::FileTypeExt;
let path = Path::new(named?);
(path.is_absolute()
&& std::fs::symlink_metadata(path)
.is_ok_and(|m| m.file_type().is_socket() && m.uid() == own_uid()))
.then(|| path.as_os_str().to_os_string())
}
#[cfg(unix)]
/// The host's environment (INVARIANT 1), given the opener's
/// `SSH_AUTH_SOCK`.
fn host_env(
home: &Path,
ssh_agent: Option<&std::ffi::OsStr>,
appimage: Option<&std::ffi::OsStr>,
) -> Vec<(String, std::ffi::OsString)> {
let mut env = vec![
("PATH".to_string(), PATH.into()),
("HOME".to_string(), home.as_os_str().to_os_string()),
];
if let Some(runtime) = runtime_dir() {
if let Some(bus) = session_bus(&runtime) {
env.push(("DBUS_SESSION_BUS_ADDRESS".into(), bus.into()));
}
env.push(("XDG_RUNTIME_DIR".into(), runtime.into_os_string()));
}
if let Some(agent) = own_agent(ssh_agent) {
env.push(("SSH_AUTH_SOCK".into(), agent));
}
if let Some(image) = own_appimage(appimage) {
env.push(("APPIMAGE".into(), image));
}
env
}
#[cfg(unix)]
/// Whether this process sits in a unit under this account's user manager
/// (an app's scope or service, as uwsm makes for every app), whose stop
/// would take the host with it; going by the unified cgroup line of
/// `/proc/self/cgroup`. The manager's own `init.scope` is not such a
/// unit.
pub fn in_app_scope(cgroup: &str, uid: u32) -> bool {
let manager = format!("/user@{uid}.service/");
cgroup.lines().any(|line| {
line.strip_prefix("0::").is_some_and(|path| {
path.split_once(manager.as_str())
.is_some_and(|(_, unit)| !unit.is_empty() && unit != "init.scope")
})
})
}
#[cfg(unix)]
/// systemd-run, if this process is in an app scope and the user manager
/// can be reached (INVARIANT 3).
fn systemd_run() -> Option<&'static str> {
let cgroup = std::fs::read_to_string("/proc/self/cgroup").ok()?;
if !in_app_scope(&cgroup, own_uid()) {
return None;
}
// systemd-run reaches the user manager through the runtime folder,
// which host_env gives it even when the opener's environment didn't.
runtime_dir()?;
SYSTEMD_RUN.into_iter().find(|p| Path::new(p).is_file())
}
#[cfg(unix)]
/// Start a host for the root (`root`, else the one the host finds
/// itself), with these knobs and these server flags (INVARIANTS 1-5).
pub fn start_host(
root: Option<&Path>,
knobs: &Knobs,
server_flags: &[String],
) -> Result<(), String> {
let found = ledger_store::root::Root::find(root).map_err(|e| e.to_string())?;
let run = found.run_dir().map_err(|e| e.to_string())?;
let home = ledger_store::root::account_home().map_err(|e| e.to_string())?;
// A staged update is applied here and nowhere while books are open:
// no host runs now, so no call is cut off (update.rs INVARIANT 9). A
// failure leaves the program as it was.
let _ = crate::update::apply_staged(&found, crate::update::program_version());
let exe = this_executable()?;
let mut args: Vec<std::ffi::OsString> = vec!["host".into()];
// An explicit root goes on as found (absolute): the host starts in /.
if root.is_some() {
args.push("--root".into());
args.push(found.path().as_os_str().to_os_string());
}
args.extend(knobs.args().into_iter().map(Into::into));
// What the server this host starts is told (P9a): the same flags
// `serve` was given.
args.extend(server_flags.iter().map(Into::into));
let log = || {
let path = run.join(HOST_LOG);
// A large log is moved aside first (to `.1`), never from under a
// host writing it.
if std::fs::metadata(&path).is_ok_and(|m| m.len() > LOG_LIMIT)
&& !crate::opener::host_lock_held(&run)
{
let _ = std::fs::rename(&path, path.with_extension("log.1"));
}
std::fs::OpenOptions::new()
.create(true)
.read(true)
.append(true)
.mode(0o600)
.open(&path)
.map_err(|e| format!("can't open the host's log: {}", e.kind()))
};
// Through systemd-run (`Some`), or directly.
let command = |through: Option<&str>, log: std::fs::File| -> Result<Command, String> {
let mut c = match through {
Some(systemd_run) => {
let mut c = Command::new(systemd_run);
c.args(["--user", "--scope", "--quiet", "--collect", "--"])
.arg(&exe);
c
}
None => Command::new(&exe),
};
c.args(&args)
.env_clear()
.envs(host_env(
&home,
std::env::var_os("SSH_AUTH_SOCK").as_deref(),
std::env::var_os("APPIMAGE").as_deref(),
))
.current_dir("/")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(log);
// SAFETY: setsid and umask are async-signal-safe and touch only
// the child.
unsafe {
c.pre_exec(|| {
libc::setsid();
libc::umask(0o077);
Ok(())
});
}
Ok(c)
};
if let Some(systemd_run) = systemd_run() {
// The log as this start opened it, and where it stands now: what
// the host writes after this is read back from that same file,
// even if another opener moves the log aside meanwhile.
let file = log()?;
let from = file.metadata().map_or(0, |m| m.len());
let watch = file
.try_clone()
.map_err(|e| format!("can't open the host's log: {}", e.kind()))?;
if let Ok(mut child) = command(Some(systemd_run), file)?.spawn()
&& scope_made(&mut child, &watch, from)
{
reap(child);
return Ok(());
}
}
let child = command(None, log()?)?
.spawn()
.map_err(|e| format!("can't start the books' host: {e}"))?;
reap(child);
Ok(())
}
#[cfg(unix)]
/// How long systemd-run has to make its scope and run the host.
const SCOPE_WAIT: Duration = Duration::from_secs(2);
#[cfg(unix)]
/// Whether systemd-run made its scope and ran the host (INVARIANT 3): the
/// host's first line is in its log after `from`, or systemd-run is still
/// running when SCOPE_WAIT is up (it became the host). Only systemd-run
/// failing with no host line means it didn't: then the host is started
/// directly. (systemd-run's own complaints go to the same log, so the
/// log growing proves nothing; the host's line does.)
fn scope_made(child: &mut std::process::Child, log: &std::fs::File, from: u64) -> bool {
let host_ran = || {
use std::os::unix::fs::FileExt;
let len = log.metadata().map_or(0, |m| m.len());
let mut tail = vec![0u8; len.saturating_sub(from).min(64 * 1024) as usize];
log.read_exact_at(&mut tail, from).is_ok()
&& String::from_utf8_lossy(&tail).contains(crate::host::STARTED)
};
let deadline = Instant::now() + SCOPE_WAIT;
loop {
if host_ran() {
return true;
}
match child.try_wait() {
Ok(Some(status)) => return status.success() || host_ran(),
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(10));
}
_ => return true,
}
}
}
/// The host's environment on Windows (INVARIANT 1's counterpart): the
/// system's own variables, the account's profile, and a PATH fixed to the
/// system folders and Git for Windows (the backups run `git`). Nothing
/// else of the opener's passes. (CHOICE, P11c: no authority states a
/// Windows list; this is the least a Windows program and `git` need.)
#[cfg(windows)]
fn host_env(home: &Path) -> Vec<(String, std::ffi::OsString)> {
let var = |name: &str| std::env::var_os(name);
let system_root: PathBuf = var("SystemRoot")
.or_else(|| var("windir"))
.map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);
let program_files: PathBuf =
var("ProgramFiles").map_or_else(|| PathBuf::from(r"C:\Program Files"), PathBuf::from);
let path = std::env::join_paths([
system_root.join("System32"),
system_root.clone(),
program_files.join("Git").join("cmd"),
])
.unwrap_or_default();
let mut env: Vec<(String, std::ffi::OsString)> = vec![
("PATH".into(), path),
("SystemRoot".into(), system_root.clone().into_os_string()),
("windir".into(), system_root.into_os_string()),
("USERPROFILE".into(), home.as_os_str().to_os_string()),
("HOME".into(), home.as_os_str().to_os_string()),
("TEMP".into(), std::env::temp_dir().into_os_string()),
("TMP".into(), std::env::temp_dir().into_os_string()),
];
if let Some(local) = ledger_store::winsec::local_app_data() {
env.push(("LOCALAPPDATA".into(), local.into_os_string()));
}
for name in ["APPDATA", "ComSpec", "PATHEXT", "ProgramFiles"] {
if let Some(value) = var(name) {
env.push((name.into(), value));
}
}
env
}
/// Start a host for the root (`root`, else the one the host finds
/// itself), with these knobs and these server flags (INVARIANTS 1, 2, 4
/// and 5; there is no scope to leave, but a job to break away from).
#[cfg(windows)]
pub fn start_host(
root: Option<&Path>,
knobs: &Knobs,
server_flags: &[String],
) -> Result<(), String> {
use std::os::windows::process::CommandExt;
let found = ledger_store::root::Root::find(root).map_err(|e| e.to_string())?;
let run = found.run_dir().map_err(|e| e.to_string())?;
let home = ledger_store::root::account_home().map_err(|e| e.to_string())?;
let exe = this_executable()?;
let mut args: Vec<std::ffi::OsString> = vec!["host".into()];
if root.is_some() {
args.push("--root".into());
args.push(found.path().as_os_str().to_os_string());
}
args.extend(knobs.args().into_iter().map(Into::into));
args.extend(server_flags.iter().map(Into::into));
let log = || {
let path = run.join(HOST_LOG);
if std::fs::metadata(&path).is_ok_and(|m| m.len() > LOG_LIMIT)
&& !crate::opener::host_lock_held(&run)
{
let _ = std::fs::rename(&path, path.with_extension("log.1"));
}
std::fs::OpenOptions::new()
.create(true)
.read(true)
.append(true)
.open(&path)
.map_err(|e| format!("can't open the host's log: {}", e.kind()))
};
let spawn = |flags: u32| -> Result<std::process::Child, String> {
Command::new(&exe)
.args(&args)
.env_clear()
.envs(host_env(&home))
.current_dir(&run)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(log()?)
.creation_flags(flags)
.spawn()
.map_err(|e| {
// Named for the caller below: breakaway refused.
if e.kind() == std::io::ErrorKind::PermissionDenied {
"denied".to_string()
} else {
format!("can't start the books' host: {e}")
}
})
};
let child = match spawn(DETACHED_HOST | BREAKAWAY) {
Ok(child) => child,
// The job the opener runs in doesn't let a process leave it: the
// host starts inside it, as a host without a scope does on Linux.
Err(why) if why == "denied" => spawn(DETACHED_HOST)?,
Err(why) => return Err(why),
};
reap(child);
Ok(())
}
/// Reap a started host when it ends, without waiting for it (INVARIANT 5).
fn reap(mut child: std::process::Child) {
let _ = std::thread::Builder::new()
.name("reap-host".into())
.spawn(move || {
let _ = child.wait();
});
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn an_app_scope_is_known_by_its_unified_cgroup() {
let uwsm = "0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-Hyprland-kitty-1234.scope\n";
assert!(in_app_scope(uwsm, 1000));
assert!(!in_app_scope(uwsm, 1001), "another account's manager");
// An app run as a service unit (uwsm's app-…@….service) too.
assert!(in_app_scope(
"0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-foo@x.service\n",
1000
));
for not in [
"0::/user.slice/user-1000.slice/session-2.scope\n",
"0::/user.slice/user-1000.slice/user@1000.service/init.scope\n",
"0::/user.slice/user-1000.slice/user@1000.service/\n",
"0::/\n",
"1:name=systemd:/user.slice/user-1000.slice/user@1000.service/app.slice/x.scope\n",
"",
] {
assert!(!in_app_scope(not, 1000), "{not}");
}
}
#[test]
fn only_our_own_private_runtime_folder_passes() {
let dir = tempfile::tempdir().unwrap();
let rt = dir.path().join("rt");
std::fs::create_dir(&rt).unwrap();
std::fs::set_permissions(&rt, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(own_runtime_dir(Some(rt.as_os_str())), Some(rt.clone()));
std::fs::set_permissions(&rt, std::fs::Permissions::from_mode(0o750)).unwrap();
assert_eq!(
own_runtime_dir(Some(rt.as_os_str())),
None,
"group can enter"
);
std::fs::set_permissions(&rt, std::fs::Permissions::from_mode(0o700)).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&rt, &link).unwrap();
assert_eq!(own_runtime_dir(Some(link.as_os_str())), None, "a link");
assert_eq!(own_runtime_dir(Some("relative/rt".as_ref())), None);
assert_eq!(
own_runtime_dir(Some(dir.path().join("none").as_os_str())),
None
);
assert_eq!(own_runtime_dir(None), None);
// Owned by root, not us.
if own_uid() != 0 {
assert_eq!(own_runtime_dir(Some("/".as_ref())), None);
}
}
#[test]
fn only_the_bus_in_our_runtime_folder_passes() {
let rt = Path::new("/run/user/1000");
assert_eq!(
own_bus(Some("unix:path=/run/user/1000/bus".as_ref()), rt).as_deref(),
Some("unix:path=/run/user/1000/bus")
);
assert!(own_bus(Some("unix:path=/run/user/1000/bus,guid=ab".as_ref()), rt).is_some());
for bad in [
"unix:path=/tmp/evil/bus",
"unix:path=/run/user/1000/bus2",
"unix:path=/run/user/1000/busx,guid=1",
"tcp:host=evil.example,port=1",
"unix:abstract=/run/user/1000/bus",
] {
assert_eq!(own_bus(Some(bad.as_ref()), rt), None, "{bad}");
}
assert_eq!(own_bus(None, rt), None);
}
#[test]
fn the_host_environment_carries_nothing_else() {
let env = host_env(Path::new("/home/someone"), None, None);
let names: Vec<&str> = env.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(&names[..2], ["PATH", "HOME"]);
for n in &names[2..] {
assert!(
[
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
"SSH_AUTH_SOCK",
"APPIMAGE"
]
.contains(n),
"{n}"
);
}
assert_eq!(env[0].1, PATH);
assert_eq!(env[1].1, "/home/someone");
}
#[test]
fn only_an_absolute_file_passes_as_the_appimage() {
let dir = tempfile::tempdir().unwrap();
let image = dir.path().join("W.AppImage");
std::fs::write(&image, b"x").unwrap();
let home = Path::new("/home/someone");
let has = |v: Option<&std::ffi::OsStr>| {
host_env(home, None, v).iter().any(|(n, _)| n == "APPIMAGE")
};
assert!(has(Some(image.as_os_str())));
assert!(!has(None));
assert!(!has(Some("W.AppImage".as_ref())), "relative");
assert!(!has(Some(dir.path().as_os_str())), "a folder");
assert!(!has(Some(dir.path().join("gone").as_os_str())));
}
#[test]
fn only_an_own_ssh_agent_socket_passes() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("agent.sock");
let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
assert_eq!(
own_agent(Some(sock.as_os_str())),
Some(sock.clone().into_os_string())
);
// It reaches the host's environment, once.
let env = host_env(Path::new("/home/someone"), Some(sock.as_os_str()), None);
let agents: Vec<_> = env.iter().filter(|(n, _)| n == "SSH_AUTH_SOCK").collect();
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1, sock.as_os_str());
// A link to it is not it (whoever made the link chose where it
// points).
let link = dir.path().join("link.sock");
std::os::unix::fs::symlink(&sock, &link).unwrap();
assert_eq!(own_agent(Some(link.as_os_str())), None);
let env = host_env(Path::new("/home/someone"), Some(link.as_os_str()), None);
assert!(env.iter().all(|(n, _)| n != "SSH_AUTH_SOCK"));
// Named relative to here: not taken, even where it leads to one.
let here = std::env::current_dir().unwrap();
let up = here.components().count() - 1;
let relative: std::path::PathBuf = std::iter::repeat_n("..", up)
.collect::<std::path::PathBuf>()
.join(sock.strip_prefix("/").unwrap());
assert!(relative.exists() && relative.is_relative(), "{relative:?}");
assert_eq!(own_agent(Some(relative.as_os_str())), None);
// Another account's socket (the system's bus, owned by root, where
// there is one): not ours.
let system = Path::new("/run/dbus/system_bus_socket");
if system.exists() && own_uid() != 0 {
assert_eq!(own_agent(Some(system.as_os_str())), None);
}
let file = dir.path().join("plain");
std::fs::write(&file, "x").unwrap();
for bad in [
file.as_os_str(),
"agent.sock".as_ref(),
"/nonexistent/agent.sock".as_ref(),
"".as_ref(),
] {
assert_eq!(own_agent(Some(bad)), None, "{bad:?}");
}
assert_eq!(own_agent(None), None);
}
#[test]
fn this_executable_is_a_file_on_disk() {
let exe = this_executable().unwrap();
assert!(exe.is_file(), "{exe:?}");
}
}
#[cfg(all(test, windows))]
mod windows_tests {
use super::*;
#[test]
fn this_executable_is_a_file_on_disk() {
let exe = this_executable().unwrap();
assert!(exe.is_file(), "{exe:?}");
}
#[test]
fn the_host_environment_carries_nothing_of_the_openers_choosing() {
let env = host_env(Path::new(r"C:\Users\someone"));
let names: Vec<&str> = env.iter().map(|(n, _)| n.as_str()).collect();
for n in &names {
assert!(
[
"PATH",
"SystemRoot",
"windir",
"USERPROFILE",
"HOME",
"TEMP",
"TMP",
"LOCALAPPDATA",
"APPDATA",
"ComSpec",
"PATHEXT",
"ProgramFiles"
]
.contains(n),
"{n}"
);
}
let home = env.iter().find(|(n, _)| n == "HOME").unwrap();
assert_eq!(home.1, r"C:\Users\someone");
let path = env.iter().find(|(n, _)| n == "PATH").unwrap();
assert!(path.1.to_string_lossy().to_lowercase().contains("system32"));
}
}