use std::ffi::OsString;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::cli::{self, InstallOptions, RunOptions, ALLOW_ROOT_FLAG, CONFIG_FLAG};
use crate::config::{self, Config};
use crate::configure;
pub const DEFAULT_UNIT: &str = "mobux";
const RUNTIME_DIR_VAR: &str = "XDG_RUNTIME_DIR";
const SUDO_USER_VAR: &str = "SUDO_USER";
const PASSWD_FILE: &str = "/etc/passwd";
const CHECK_HINT: &str = "check it with: mobux service status";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitSpec {
pub unit: String,
pub exec_start: String,
pub config_path: String,
pub port: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Create,
Update,
Unchanged,
}
pub fn unit_name(service_name: Option<String>) -> String {
service_name
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_UNIT.to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Auth {
Required,
DelegatedToProxy,
}
pub fn resolve_unit_spec(
settings: &Config,
exec_start: &Path,
config_path: &Path,
auth: Auth,
) -> Result<UnitSpec, String> {
config::check(settings)?;
if auth == Auth::Required && settings.credentials().is_none() {
return Err(cli::MISSING_CREDENTIALS.to_string());
}
let spec = UnitSpec {
unit: unit_name(Some(settings.app.service_name.clone())),
exec_start: exec_start.to_string_lossy().into_owned(),
config_path: config_path.to_string_lossy().into_owned(),
port: settings.server.port,
};
check_unit_value("the binary path", &spec.exec_start)?;
check_unit_value("the config file path", &spec.config_path)?;
check_unit_value("app.service_name", &spec.unit)?;
Ok(spec)
}
fn check_unit_value(label: &str, value: &str) -> Result<(), String> {
let offender = value
.chars()
.find(|c| c.is_whitespace() || c.is_control() || matches!(c, '"' | '\'' | '\\' | '$'));
match offender {
None => Ok(()),
Some(c) => Err(format!(
"{label} contains {c:?}; a systemd unit can't carry whitespace, quotes, \
backslashes or $ in a value. Use a plainer value."
)),
}
}
pub fn render_unit(spec: &UnitSpec) -> String {
let UnitSpec {
unit,
exec_start,
config_path,
port,
} = spec;
let mut out = String::new();
let _ = write!(
out,
"[Unit]
Description=mobux — mobile tmux web frontend (:{port})
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart={exec_start} {CONFIG_FLAG} {config_path}
"
);
if unit != DEFAULT_UNIT {
let _ = writeln!(out, "Environment=MOBUX_SERVICE_NAME={unit}");
}
let _ = write!(
out,
"# The self-updater runs `cargo install`; the default unit PATH lacks ~/.cargo/bin.
Environment=PATH=%h/.cargo/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin
Restart=on-failure
RestartSec=5
# Only kill the mobux process itself — the tmux server it spawned lives in the
# same cgroup, and the default would kill it (and every session) on restart.
KillMode=process
[Install]
WantedBy=default.target
"
);
out
}
pub fn decide(existing: Option<&str>, desired: &str) -> Decision {
match existing {
None => Decision::Create,
Some(current) if current == desired => Decision::Unchanged,
Some(_) => Decision::Update,
}
}
pub fn unit_path(config_dir: &Path, unit: &str) -> PathBuf {
config_dir
.join("systemd/user")
.join(format!("{unit}.service"))
}
pub fn run(command: &crate::cli::ServiceCommand) -> i32 {
match command {
crate::cli::ServiceCommand::Install(options) => report(install(options)),
crate::cli::ServiceCommand::Uninstall => report(uninstall()),
crate::cli::ServiceCommand::Status => match status() {
Ok(code) => code,
Err(message) => {
eprintln!("mobux: {message}");
1
}
},
}
}
fn report(result: Result<(), String>) -> i32 {
match result {
Ok(()) => 0,
Err(message) => {
eprintln!("mobux: {message}");
1
}
}
}
fn install(options: &InstallOptions) -> Result<(), String> {
if let Some(refusal) = root_refusal(
current_uid(),
std::env::var(SUDO_USER_VAR).ok(),
options.allow_root,
) {
return Err(refusal);
}
let exe = std::env::current_exe()
.map_err(|e| format!("could not resolve this binary's own path: {e}"))?;
let config_path = options
.run
.config_path
.clone()
.unwrap_or_else(config::config_file_path);
let settings = drop_credentials(
resolve_settings(&config_path, &options.run)?,
options.no_auth,
);
let spec = resolve_unit_spec(&settings, &exe, &config_path, auth_mode(options.no_auth))?;
preflight()?;
let unit_file = unit_path(&config_dir()?, &spec.unit);
let desired = render_unit(&spec);
let unit_change = decide(read(&unit_file).as_deref(), &desired);
let config_change = decide(
read(&config_path).as_deref(),
&configure::document(&settings),
);
if unit_change == Decision::Unchanged && config_change == Decision::Unchanged {
return Err(format!(
"{} and {} already describe this exact service — nothing to do. Pass \
--port/--user/--pin to change it, or `mobux service uninstall` to remove it.",
config_path.display(),
unit_file.display()
));
}
if config_change != Decision::Unchanged {
configure::write_file(&config_path, &settings, true)
.map_err(|e| format!("writing {}: {e}", config_path.display()))?;
println!(
"{} {} (mode 600)",
verb(config_change),
config_path.display()
);
}
if unit_change != Decision::Unchanged {
write_unit(&unit_file, &desired)?;
println!("{} {} (mode 600)", verb(unit_change), unit_file.display());
}
systemctl(&["daemon-reload"])?;
systemctl(&["enable", &spec.unit])?;
systemctl(&["restart", &spec.unit])?;
enable_linger();
let scheme = if settings.tls.enabled {
"https"
} else {
"http"
};
match settings.credentials() {
Some(credentials) => println!(
"mobux listens on {scheme}://<this-host>:{} as user {:?}",
spec.port, credentials.user
),
None => println!("mobux listens on {scheme}://<this-host>:{}", spec.port),
}
if !settings.tls.enabled {
println!("rerun `mobux service install --tls` to serve HTTPS instead");
}
if settings.credentials().is_none() {
eprintln!("{}", config::NO_AUTH_WARNING);
}
println!("{CHECK_HINT}");
Ok(())
}
fn auth_mode(no_auth: bool) -> Auth {
if no_auth {
return Auth::DelegatedToProxy;
}
Auth::Required
}
fn drop_credentials(settings: Config, no_auth: bool) -> Config {
if !no_auth {
return settings;
}
Config {
auth: config::AuthConfig::default(),
..settings
}
}
fn root_refusal(uid: Option<u32>, sudo_user: Option<String>, allow_root: bool) -> Option<String> {
if allow_root || uid != Some(0) {
return None;
}
let user = sudo_user
.filter(|name| !name.is_empty() && name != "root")
.unwrap_or_else(|| "<user>".to_string());
Some(format!(
"refusing to install as root: this would write /root/.config/mobux and linger root, \
not {user}, and leave a second copy of the service behind.\n\
Run it as {user}: mobux service install --port <port> --user <name> --pin <pin>\n\
If that install cannot enable linger on its own, grant only that step: \
sudo loginctl enable-linger {user}\n\
Pass {ALLOW_ROOT_FLAG} to install root's own service on purpose."
))
}
fn read(path: &Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
fn resolve_settings(path: &Path, options: &RunOptions) -> Result<Config, String> {
let file = config::load_partial_from(path)
.map_err(|error| error.to_string())?
.unwrap_or_default();
Ok(config::resolve(
Config::default(),
file,
&config::EnvSnapshot::from_env(),
options.overrides.clone(),
))
}
fn installed_unit() -> String {
let path = config::config_file_path();
let file = config::load_partial_from(&path)
.ok()
.flatten()
.unwrap_or_default();
let settings = config::resolve(
Config::default(),
file,
&config::EnvSnapshot::from_env(),
config::PartialConfig::default(),
);
unit_name(Some(settings.app.service_name))
}
fn verb(decision: Decision) -> &'static str {
match decision {
Decision::Update => "updated",
_ => "wrote",
}
}
fn uninstall() -> Result<(), String> {
preflight()?;
let unit = installed_unit();
let path = unit_path(&config_dir()?, &unit);
if !path.exists() {
return Err(format!(
"{} does not exist — nothing to uninstall.",
path.display()
));
}
systemctl(&["disable", "--now", &unit])?;
std::fs::remove_file(&path).map_err(|e| format!("removing {}: {e}", path.display()))?;
systemctl(&["daemon-reload"])?;
println!("removed {}", path.display());
println!("linger is left enabled — other user services may depend on it. Turn it off with:");
println!(" loginctl disable-linger {}", whoami());
Ok(())
}
fn status() -> Result<i32, String> {
preflight()?;
let unit = installed_unit();
let status = user_systemctl()
.args(["status", &unit, "--no-pager"])
.status()
.map_err(|e| format!("running systemctl: {e}"))?;
Ok(status.code().unwrap_or(1))
}
pub fn config_dir() -> Result<PathBuf, String> {
directories::BaseDirs::new()
.map(|dirs| dirs.config_dir().to_path_buf())
.ok_or_else(|| "could not resolve your home directory (is $HOME set?)".to_string())
}
fn write_unit(path: &Path, contents: &str) -> Result<(), String> {
use std::io::Write as _;
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
let dir = path
.parent()
.ok_or_else(|| format!("{} has no parent directory", path.display()))?;
std::fs::create_dir_all(dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.map_err(|e| format!("writing {}: {e}", path.display()))?;
file.write_all(contents.as_bytes())
.map_err(|e| format!("writing {}: {e}", path.display()))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|e| format!("securing {}: {e}", path.display()))
}
fn runtime_dir_default(
current: Option<OsString>,
uid: u32,
is_dir: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
if current.is_some_and(|value| !value.is_empty()) {
return None;
}
let dir = PathBuf::from(format!("/run/user/{uid}"));
is_dir(&dir).then_some(dir)
}
fn current_uid() -> Option<u32> {
use std::os::unix::fs::MetadataExt as _;
std::fs::metadata("/proc/self").ok().map(|meta| meta.uid())
}
fn session_runtime_dir() -> Option<PathBuf> {
runtime_dir_default(std::env::var_os(RUNTIME_DIR_VAR), current_uid()?, |path| {
path.is_dir()
})
}
fn runtime_dir_path() -> String {
std::env::var(RUNTIME_DIR_VAR)
.ok()
.filter(|value| !value.is_empty())
.or_else(|| current_uid().map(|uid| format!("/run/user/{uid}")))
.unwrap_or_else(|| "/run/user/<uid>".to_string())
}
fn systemctl_command(runtime_dir: Option<PathBuf>) -> Command {
let mut command = Command::new("systemctl");
if let Some(dir) = runtime_dir {
command.env(RUNTIME_DIR_VAR, dir);
}
command.arg("--user");
command
}
fn user_systemctl() -> Command {
systemctl_command(session_runtime_dir())
}
fn no_bus_error(probe: &str, runtime_dir: &str, user: &str) -> String {
format!(
"no systemd --user bus for this session: {}\n\
{runtime_dir} is missing, so no user manager is running for {user}. Start one with: \
loginctl enable-linger {user}\n\
then log in again — mobux sets XDG_RUNTIME_DIR itself once that directory exists.",
probe.trim()
)
}
fn preflight() -> Result<(), String> {
if !cfg!(target_os = "linux") {
return Err(
"`mobux service` manages a systemd --user unit, which exists only on Linux. \
Run mobux under your platform's own service manager instead."
.to_string(),
);
}
let probe = user_systemctl().arg("is-system-running").output();
let output = match probe {
Ok(output) => output,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(
"systemctl is not on PATH, so this host isn't running systemd. \
Start mobux from your init system (or a tmux session) instead."
.to_string(),
)
}
Err(e) => return Err(format!("running systemctl: {e}")),
};
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
if combined.contains("Failed to connect to") || combined.contains("Failed to get D-Bus") {
return Err(no_bus_error(&combined, &runtime_dir_path(), &whoami()));
}
Ok(())
}
fn systemctl(args: &[&str]) -> Result<(), String> {
let output = user_systemctl()
.args(args)
.output()
.map_err(|e| format!("running systemctl --user {}: {e}", args.join(" ")))?;
if output.status.success() {
return Ok(());
}
Err(format!(
"systemctl --user {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
))
}
fn enable_linger() {
let user = whoami();
let outcome = Command::new("loginctl")
.args(["enable-linger", &user])
.output();
match outcome {
Ok(output) if output.status.success() => {
println!("linger enabled for {user} — the service starts at boot without a login")
}
Ok(output) => eprintln!(
"{}",
linger_failure_hint(&user, &String::from_utf8_lossy(&output.stderr))
),
Err(e) => eprintln!(
"warning: could not run loginctl enable-linger {user}: {e}\n\
mobux will start when you log in, not at boot."
),
}
}
fn linger_failure_hint(user: &str, stderr: &str) -> String {
format!(
"warning: loginctl enable-linger {user} failed: {}\n\
mobux will start when you log in, not at boot. Enable it with: \
sudo loginctl enable-linger {user}\n\
Leave the install itself alone — installing it under sudo gives root its own second \
copy of the service.",
stderr.trim()
)
}
fn whoami() -> String {
resolve_user(
current_uid(),
|uid| user_for_uid(&read(Path::new(PASSWD_FILE)).unwrap_or_default(), uid),
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.ok(),
)
}
fn resolve_user(
uid: Option<u32>,
name_for_uid: impl Fn(u32) -> Option<String>,
env_name: Option<String>,
) -> String {
uid.and_then(name_for_uid)
.or_else(|| env_name.filter(|name| !name.is_empty()))
.or_else(|| uid.map(|uid| uid.to_string()))
.unwrap_or_else(|| "$USER".to_string())
}
fn user_for_uid(passwd: &str, uid: u32) -> Option<String> {
passwd.lines().find_map(|line| {
let mut fields = line.split(':');
let name = fields.next()?;
let entry: u32 = fields.nth(1)?.trim().parse().ok()?;
(entry == uid && !name.is_empty()).then(|| name.to_string())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::CliOverrides;
use std::os::unix::fs::PermissionsExt as _;
const EXE: &str = "/home/me/.local/bin/mobux";
const CONFIG: &str = "/home/me/.config/mobux/config.json";
const OLD_STYLE_UNIT: &str = "\
[Unit]
Description=mobux — mobile tmux web frontend (HTTPS on :5151)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/home/me/.local/bin/mobux
Environment=MOBUX_PORT=5151
Environment=MOBUX_AUTH_USER=walker
Environment=MOBUX_PIN=99999
Environment=PATH=%h/.cargo/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin
Restart=on-failure
RestartSec=5
KillMode=process
[Install]
WantedBy=default.target
";
fn overrides(port: Option<u16>, user: Option<&str>, pin: Option<&str>) -> CliOverrides {
CliOverrides {
server: port.map(|port| config::PartialServerConfig {
port: Some(port),
..Default::default()
}),
auth: (user.is_some() || pin.is_some()).then(|| config::PartialAuthConfig {
user: user.map(str::to_string),
pin: pin.map(str::to_string),
..Default::default()
}),
..Default::default()
}
}
fn settings(overrides: CliOverrides) -> Config {
config::resolve(
Config::default(),
config::PartialConfig::default(),
&config::EnvSnapshot::default(),
overrides,
)
}
fn authed() -> Config {
settings(overrides(None, Some("me"), Some("12345")))
}
fn spec_from(settings: &Config) -> UnitSpec {
resolve_unit_spec(settings, Path::new(EXE), Path::new(CONFIG), Auth::Required)
.expect("spec should resolve")
}
fn unit_environment(unit: &str) -> config::EnvSnapshot {
config::EnvSnapshot::new(
unit.lines()
.filter_map(|line| line.strip_prefix("Environment=")?.split_once('=')),
)
}
#[test]
fn the_unit_names_the_config_file_and_carries_no_credentials() {
let unit = render_unit(&spec_from(&settings(overrides(
Some(5151),
Some("walker"),
Some("99999"),
))));
assert!(
unit.contains(&format!("ExecStart={EXE} --config {CONFIG}")),
"{unit}"
);
assert!(!unit.contains("MOBUX_PIN"), "{unit}");
assert!(!unit.contains("MOBUX_AUTH_USER"), "{unit}");
assert!(!unit.contains("MOBUX_PORT"), "{unit}");
assert!(!unit.contains("99999"), "the PIN reached the unit: {unit}");
assert!(unit.contains("Description=mobux"), "{unit}");
assert!(unit.contains("Restart=on-failure"), "{unit}");
assert!(unit.contains("WantedBy=default.target"), "{unit}");
assert!(unit.contains("KillMode=process"), "{unit}");
}
#[test]
fn unit_path_extends_the_default_path_with_cargo_bin() {
let unit = render_unit(&spec_from(&authed()));
assert!(
unit.contains("Environment=PATH=%h/.cargo/bin:"),
"the self-updater shells out to cargo: {unit}"
);
}
#[test]
fn no_unit_without_auth() {
let error = resolve_unit_spec(
&settings(overrides(None, Some("me"), None)),
Path::new(EXE),
Path::new(CONFIG),
Auth::Required,
)
.expect_err("an auth-less boot service must be refused");
assert!(error.contains("--pin"), "{error}");
}
#[test]
fn no_auth_installs_without_credentials() {
let settings = drop_credentials(authed(), true);
assert_eq!(settings.credentials(), None);
assert_eq!(
settings.server.port,
authed().server.port,
"only the credentials go"
);
assert_eq!(auth_mode(true), Auth::DelegatedToProxy);
assert_eq!(auth_mode(false), Auth::Required);
let spec = resolve_unit_spec(
&settings,
Path::new(EXE),
Path::new(CONFIG),
Auth::DelegatedToProxy,
)
.expect("a proxy-gated service installs without credentials");
assert_eq!(spec.unit, DEFAULT_UNIT);
assert_eq!(
drop_credentials(authed(), false),
authed(),
"without --no-auth the credentials stand"
);
assert!(
config::NO_AUTH_WARNING.contains("authentication is OFF"),
"both the install and the server say it: {}",
config::NO_AUTH_WARNING
);
}
#[test]
fn a_pin_that_systemd_would_mangle_is_refused() {
for pin in ["hunter 2", "hunter\"2", "hunter$2", "hunter\\2"] {
let error = resolve_unit_spec(
&settings(overrides(None, Some("me"), Some(pin))),
Path::new(EXE),
Path::new(CONFIG),
Auth::Required,
)
.expect_err("a PIN systemd would mangle must be refused");
assert!(error.contains("auth.pin"), "{error}");
}
}
#[test]
fn a_config_path_the_unit_cannot_carry_is_refused() {
let error = resolve_unit_spec(
&authed(),
Path::new(EXE),
Path::new("/home/my configs/config.json"),
Auth::Required,
)
.expect_err("a path systemd would split must be refused");
assert!(error.contains("config file path"), "{error}");
}
#[test]
fn the_default_unit_is_the_one_the_self_updater_restarts() {
assert_eq!(unit_name(None), "mobux");
assert_eq!(unit_name(Some(" ".to_string())), "mobux");
assert_eq!(unit_name(Some("mobux-dev".to_string())), "mobux-dev");
assert_eq!(
unit_path(Path::new("/home/me/.config"), "mobux"),
Path::new("/home/me/.config/systemd/user/mobux.service")
);
}
#[test]
fn a_renamed_unit_tells_the_self_updater_its_own_name() {
let default = render_unit(&spec_from(&authed()));
assert!(!default.contains("MOBUX_SERVICE_NAME"), "{default}");
let mut renamed = authed();
renamed.app.service_name = "mobux-dev".to_string();
let unit = render_unit(&spec_from(&renamed));
assert!(
unit.contains("Environment=MOBUX_SERVICE_NAME=mobux-dev"),
"{unit}"
);
}
#[test]
fn an_identical_unit_is_refused_and_an_old_style_one_is_an_update() {
let unit = render_unit(&spec_from(&authed()));
assert_eq!(decide(None, &unit), Decision::Create);
assert_eq!(decide(Some(&unit), &unit), Decision::Unchanged);
assert_eq!(decide(Some(OLD_STYLE_UNIT), &unit), Decision::Update);
}
#[test]
fn an_old_style_unit_resolves_to_the_settings_it_always_did() {
let resolved = config::resolve(
Config::default(),
config::PartialConfig::default(),
&unit_environment(OLD_STYLE_UNIT),
config::PartialConfig::default(),
);
assert_eq!(
resolved,
settings(overrides(Some(5151), Some("walker"), Some("99999")))
);
assert_eq!(resolved.server.port, 5151);
assert_eq!(
resolved.credentials(),
Some(config::Credentials {
user: "walker".to_string(),
pass: "99999".to_string(),
})
);
}
#[test]
fn an_ssh_session_without_a_runtime_dir_gets_the_one_systemd_made() {
let made = |path: &Path| path == Path::new("/run/user/1001");
assert_eq!(
runtime_dir_default(None, 1001, made),
Some(PathBuf::from("/run/user/1001")),
"an SSH session inherits no XDG_RUNTIME_DIR and must not fail on it"
);
assert_eq!(
runtime_dir_default(Some(OsString::new()), 1001, made),
Some(PathBuf::from("/run/user/1001"))
);
assert_eq!(
runtime_dir_default(Some(OsString::from("/run/user/0")), 1001, made),
None,
"a session that already has one keeps it"
);
assert_eq!(
runtime_dir_default(None, 1001, |_| false),
None,
"nothing to default to when systemd made no runtime dir"
);
}
#[test]
fn every_user_call_carries_the_defaulted_runtime_dir() {
use std::ffi::OsStr;
let command = systemctl_command(Some(PathBuf::from("/run/user/1001")));
let env: Vec<_> = command.get_envs().collect();
assert_eq!(
env,
vec![(
OsStr::new("XDG_RUNTIME_DIR"),
Some(OsStr::new("/run/user/1001"))
)]
);
assert!(command.get_args().eq([OsStr::new("--user")]));
assert_eq!(systemctl_command(None).get_envs().count(), 0);
}
#[test]
fn the_missing_bus_hint_names_this_user_and_the_runtime_dir() {
let error = no_bus_error(
"Failed to connect to bus: No medium found\n",
"/run/user/1001",
"walker",
);
assert!(error.contains("loginctl enable-linger walker"), "{error}");
assert!(!error.contains("enable-linger root"), "{error}");
assert!(error.contains("/run/user/1001 is missing"), "{error}");
assert!(
!error.contains("export XDG_RUNTIME_DIR"),
"mobux sets it itself now: {error}"
);
}
const PASSWD: &str = "\
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
walker:x:1001:1001:Walker:/home/walker:/bin/bash
";
#[test]
fn linger_targets_the_invoking_user_and_never_root() {
let passwd = |uid| user_for_uid(PASSWD, uid);
assert_eq!(
resolve_user(Some(1001), passwd, Some("root".to_string())),
"walker",
"the uid decides, not an environment that says otherwise"
);
assert_eq!(resolve_user(Some(0), passwd, None), "root");
assert_eq!(
resolve_user(Some(1002), passwd, Some("walker".to_string())),
"walker",
"a uid with no passwd entry falls back to the environment"
);
assert_eq!(
resolve_user(Some(1002), passwd, Some(String::new())),
"1002"
);
assert_eq!(resolve_user(None, passwd, None), "$USER");
}
#[test]
fn the_linger_hint_asks_for_sudo_on_that_one_command() {
let hint = linger_failure_hint(
"walker",
"Could not enable linger: Interactive authentication required.\n",
);
assert!(
hint.contains("sudo loginctl enable-linger walker"),
"{hint}"
);
assert!(
hint.contains("Interactive authentication required."),
"the real failure is still reported: {hint}"
);
assert!(
!hint.to_lowercase().contains("again as root"),
"rerunning the install as root installs a second service: {hint}"
);
assert!(!hint.contains("enable-linger root"), "{hint}");
}
#[test]
fn installing_under_sudo_is_refused_and_names_the_right_invocation() {
let refusal = root_refusal(Some(0), Some("walker".to_string()), false)
.expect("an install as root must be refused");
assert!(refusal.contains("Run it as walker"), "{refusal}");
assert!(
refusal.contains("sudo loginctl enable-linger walker"),
"{refusal}"
);
assert!(refusal.contains("--allow-root"), "{refusal}");
assert!(
root_refusal(Some(0), None, false).is_some(),
"a root shell installs the same second copy a sudo one does"
);
assert_eq!(
root_refusal(Some(0), Some("walker".to_string()), true),
None,
"--allow-root is the deliberate root install"
);
assert_eq!(root_refusal(Some(1001), None, false), None);
assert_eq!(root_refusal(None, None, false), None);
}
#[test]
fn the_check_hint_goes_through_mobux_not_systemctl() {
assert!(CHECK_HINT.contains("mobux service status"), "{CHECK_HINT}");
assert!(
!CHECK_HINT.contains("systemctl"),
"raw systemctl --user has no bus in an SSH session: {CHECK_HINT}"
);
}
#[test]
fn the_config_the_unit_names_is_readable_by_its_owner_alone() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(config::CONFIG_FILE_NAME);
let settings = settings(overrides(Some(5151), Some("walker"), Some("99999")));
configure::write_file(&path, &settings, true).expect("install writes the config");
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "mode was {:o}", mode & 0o777);
assert_eq!(config::load_from(&path).expect("it reads back"), settings);
}
}