diff --git a/src/setup.rs b/src/setup.rs index a4cd7ab..9a381ed 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -30,6 +30,33 @@ const DIM: Color = Color::DarkGrey; const SPIN: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const UNIT: &str = "anybot.service"; +/// Write a file all at once, as far as the filesystem allows: tmp file + rename. A Ctrl+C in +/// the middle of installing used to leave a half-written launcher/unit behind, and systemd +/// then ran (or refused to run) a broken service with no easy way to see why. +fn write_atomic(path: &std::path::Path, body: String) -> std::io::Result<()> { + let tmp = path.with_extension("anybot-tmp"); + std::fs::write(&tmp, body)?; + std::fs::rename(&tmp, path)?; + Ok(()) +} + +/// Quote a path for a systemd ExecStart line: a home folder with spaces (or a quote sign) +/// made the unit fail to start with no way to see why. +fn systemd_quote(p: &std::path::Path) -> String { + let mut out = String::from("\""); + for c in p.display().to_string().chars() { + match c { + '"' | '$' | '\\' | '`' => { + out.push('\\'); + out.push(c); + } + c => out.push(c), + } + } + out.push('"'); + out +} + #[derive(Clone, Copy, PartialEq, Debug)] enum Role { Hands, @@ -766,7 +793,7 @@ impl Plan { let bin = home.join(".local/bin"); std::fs::create_dir_all(&bin).map_err(|e| format!("建立 {} 失敗:{e}", bin.display()))?; let launcher = bin.join("anybot-run"); - std::fs::write(&launcher, launcher_script(self.role, &exe, &self.env)).map_err(|e| format!("寫入 {} 失敗:{e}", launcher.display()))?; + write_atomic(&launcher, launcher_script(self.role, &exe, &self.env)).map_err(|e| format!("寫入 {} 失敗:{e}", launcher.display()))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -785,9 +812,9 @@ impl Plan { }; let unit = format!( "[Unit]\nDescription=AnyBot({what})\nAfter=network-online.target graphical-session.target\nWants=network-online.target\n\n[Service]\nExecStart={}\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n", - launcher.display() + systemd_quote(&launcher) ); - std::fs::write(units.join(UNIT), unit).map_err(|e| e.to_string())?; + write_atomic(&units.join(UNIT), unit).map_err(|e| e.to_string())?; systemctl(&["daemon-reload"])?; systemctl(&["enable", UNIT])?; systemctl(&["restart", UNIT])?; @@ -815,7 +842,7 @@ fn refresh_service() -> i32 { return 1; } }; - if let Err(e) = std::fs::write(&launcher, launcher_script(role, &exe, &env)) { + if let Err(e) = write_atomic(&launcher, launcher_script(role, &exe, &env)) { eprintln!("✗ 寫入 {} 失敗:{e}", launcher.display()); return 1; }