Setup quotes service paths and writes launcher and unit files atomically

This commit is contained in:
王性驊 2026-11-05 15:48:22 +08:00
parent 7391e3a101
commit dabb4113c2
1 changed files with 31 additions and 4 deletions

View File

@ -30,6 +30,33 @@ const DIM: Color = Color::DarkGrey;
const SPIN: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const SPIN: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const UNIT: &str = "anybot.service"; 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)] #[derive(Clone, Copy, PartialEq, Debug)]
enum Role { enum Role {
Hands, Hands,
@ -766,7 +793,7 @@ impl Plan {
let bin = home.join(".local/bin"); let bin = home.join(".local/bin");
std::fs::create_dir_all(&bin).map_err(|e| format!("建立 {} 失敗:{e}", bin.display()))?; std::fs::create_dir_all(&bin).map_err(|e| format!("建立 {} 失敗:{e}", bin.display()))?;
let launcher = bin.join("anybot-run"); 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)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
@ -785,9 +812,9 @@ impl Plan {
}; };
let unit = format!( 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", "[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(&["daemon-reload"])?;
systemctl(&["enable", UNIT])?; systemctl(&["enable", UNIT])?;
systemctl(&["restart", UNIT])?; systemctl(&["restart", UNIT])?;
@ -815,7 +842,7 @@ fn refresh_service() -> i32 {
return 1; 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()); eprintln!("✗ 寫入 {} 失敗:{e}", launcher.display());
return 1; return 1;
} }