1use anyhow::{Context, Result};
4use std::net::SocketAddr;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7
8pub const WINDOWS_TASK: &str = "DeadDropDaemon";
9pub const SYSTEMD_UNIT: &str = "deaddrop.service";
10pub const LAUNCH_LABEL: &str = "com.deaddrop.daemon";
11
12pub fn daemon_executable() -> Result<PathBuf> {
13 let me = std::env::current_exe().context("current executable")?;
14 let sibling = me.with_file_name(if cfg!(windows) {
15 "dd-daemon.exe"
16 } else {
17 "dd-daemon"
18 });
19 if sibling.is_file() {
20 Ok(sibling)
21 } else {
22 Ok(me)
23 }
24}
25
26pub fn spawn_background(data_dir: &Path, listen: SocketAddr, peers: &[SocketAddr]) -> Result<u32> {
27 std::fs::create_dir_all(data_dir)?;
28 let log_path = data_dir.join("daemon.log");
29 let log = std::fs::OpenOptions::new()
30 .create(true)
31 .append(true)
32 .open(&log_path)
33 .with_context(|| format!("open {}", log_path.display()))?;
34 let log_err = log.try_clone()?;
35 let bin = daemon_executable()?;
36 let mut cmd = Command::new(&bin);
37 let stem = bin.file_stem().and_then(|s| s.to_str()).unwrap_or("");
38 if stem == "dd" {
39 cmd.arg("--data-dir")
40 .arg(data_dir)
41 .arg("daemon")
42 .arg("start")
43 .arg("--foreground")
44 .arg("--listen")
45 .arg(listen.to_string());
46 for p in peers {
47 cmd.arg("--peer").arg(p.to_string());
48 }
49 } else {
50 cmd.arg("--data-dir")
51 .arg(data_dir)
52 .arg("--listen")
53 .arg(listen.to_string());
54 for p in peers {
55 cmd.arg("--peer").arg(p.to_string());
56 }
57 }
58 cmd.stdin(Stdio::null())
59 .stdout(Stdio::from(log))
60 .stderr(Stdio::from(log_err));
61 #[cfg(windows)]
62 {
63 use std::os::windows::process::CommandExt;
64 const DETACHED_PROCESS: u32 = 0x00000008;
65 const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
66 const CREATE_NO_WINDOW: u32 = 0x08000000;
67 cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
68 }
69 #[cfg(unix)]
70 {
71 std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
72 }
73 let child = cmd.spawn().context("spawn daemon")?;
74 Ok(child.id())
75}
76
77pub fn daemon_command_line(exec: &Path, data_dir: &Path, listen: SocketAddr) -> Vec<String> {
78 let stem = exec.file_stem().and_then(|s| s.to_str()).unwrap_or("");
79 let mut args = vec![exec.display().to_string()];
80 if stem == "dd" {
81 args.extend([
82 "--data-dir".into(),
83 data_dir.display().to_string(),
84 "daemon".into(),
85 "start".into(),
86 "--foreground".into(),
87 "--listen".into(),
88 listen.to_string(),
89 ]);
90 } else {
91 args.extend([
92 "--data-dir".into(),
93 data_dir.display().to_string(),
94 "--listen".into(),
95 listen.to_string(),
96 ]);
97 }
98 args
99}
100
101pub fn systemd_unit(exec: &Path, data_dir: &Path, listen: SocketAddr) -> String {
102 let line = daemon_command_line(exec, data_dir, listen).join(" ");
103 format!(
104 "[Unit]\nDescription=DeadDrop DDP/2 node\nAfter=network.target\n\n[Service]\nType=simple\nExecStart={line}\nRestart=on-failure\nRestartSec=3\n\n[Install]\nWantedBy=default.target\n"
105 )
106}
107
108pub fn launchd_plist(exec: &Path, data_dir: &Path, listen: SocketAddr) -> String {
109 let log = data_dir.join("daemon.log");
110 let args = daemon_command_line(exec, data_dir, listen);
111 let args_xml: String = args
112 .iter()
113 .map(|a| format!(" <string>{}</string>\n", a))
114 .collect();
115 format!(
116 r#"<?xml version="1.0" encoding="UTF-8"?>
117<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
118<plist version="1.0">
119<dict>
120 <key>Label</key><string>{LAUNCH_LABEL}</string>
121 <key>ProgramArguments</key>
122 <array>
123{args_xml} </array>
124 <key>RunAtLoad</key><true/>
125 <key>KeepAlive</key><true/>
126 <key>StandardOutPath</key><string>{}</string>
127 <key>StandardErrorPath</key><string>{}</string>
128</dict>
129</plist>
130"#,
131 log.display(),
132 log.display()
133 )
134}
135
136pub fn install(data_dir: &Path, listen: SocketAddr) -> Result<String> {
137 let exec = daemon_executable()?;
138 let abs_dir = std::fs::canonicalize(data_dir).unwrap_or_else(|_| data_dir.to_path_buf());
139 install_os(&exec, &abs_dir, listen)
140}
141
142#[cfg(target_os = "linux")]
143fn install_os(exec: &Path, abs_dir: &Path, listen: SocketAddr) -> Result<String> {
144 let unit_dir = dirs_config()?.join("systemd/user");
145 std::fs::create_dir_all(&unit_dir)?;
146 let path = unit_dir.join(SYSTEMD_UNIT);
147 std::fs::write(&path, systemd_unit(exec, abs_dir, listen))?;
148 let _ = Command::new("systemctl")
149 .args(["--user", "daemon-reload"])
150 .status();
151 let _ = Command::new("systemctl")
152 .args(["--user", "enable", "--now", SYSTEMD_UNIT])
153 .status();
154 Ok(format!(
155 "installed systemd user unit {}\nstart/stop: systemctl --user start|stop {SYSTEMD_UNIT}",
156 path.display()
157 ))
158}
159
160#[cfg(target_os = "macos")]
161fn install_os(exec: &Path, abs_dir: &Path, listen: SocketAddr) -> Result<String> {
162 let dir = home_dir()?.join("Library/LaunchAgents");
163 std::fs::create_dir_all(&dir)?;
164 let path = dir.join(format!("{LAUNCH_LABEL}.plist"));
165 std::fs::write(&path, launchd_plist(exec, abs_dir, listen))?;
166 let _ = Command::new("launchctl")
167 .args(["unload", &path.to_string_lossy()])
168 .status();
169 let st = Command::new("launchctl")
170 .args(["load", &path.to_string_lossy()])
171 .status();
172 let extra = if st.map(|s| s.success()).unwrap_or(false) {
173 "loaded"
174 } else {
175 "wrote plist; run: launchctl load ~/Library/LaunchAgents/com.deaddrop.daemon.plist"
176 };
177 Ok(format!(
178 "installed LaunchAgent {} ({extra})",
179 path.display()
180 ))
181}
182
183#[cfg(windows)]
184fn install_os(exec: &Path, abs_dir: &Path, listen: SocketAddr) -> Result<String> {
185 let parts = daemon_command_line(exec, abs_dir, listen);
186 let tr = format!(
187 "\"{}\" {}",
188 parts[0],
189 parts[1..]
190 .iter()
191 .map(|a| format!("\"{a}\""))
192 .collect::<Vec<_>>()
193 .join(" ")
194 );
195 let status = Command::new("schtasks")
196 .args([
197 "/Create",
198 "/F",
199 "/SC",
200 "ONLOGON",
201 "/RL",
202 "LIMITED",
203 "/TN",
204 WINDOWS_TASK,
205 "/TR",
206 &tr,
207 ])
208 .status()
209 .context("schtasks")?;
210 if !status.success() {
211 anyhow::bail!(
212 "schtasks failed (try from an elevated prompt). Task command would be:\n {tr}"
213 );
214 }
215 let _ = Command::new("schtasks")
216 .args(["/Run", "/TN", WINDOWS_TASK])
217 .status();
218 Ok(format!(
219 "installed Windows logon task {WINDOWS_TASK} (Task Scheduler, no admin service)"
220 ))
221}
222
223#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
224fn install_os(_exec: &Path, _abs_dir: &Path, _listen: SocketAddr) -> Result<String> {
225 anyhow::bail!("service install is supported on Linux, macOS, and Windows")
226}
227
228pub fn uninstall() -> Result<String> {
229 uninstall_os()
230}
231
232#[cfg(target_os = "linux")]
233fn uninstall_os() -> Result<String> {
234 let _ = Command::new("systemctl")
235 .args(["--user", "disable", "--now", SYSTEMD_UNIT])
236 .status();
237 if let Ok(dir) = dirs_config() {
238 let path = dir.join("systemd/user").join(SYSTEMD_UNIT);
239 let _ = std::fs::remove_file(&path);
240 return Ok(format!("removed {}", path.display()));
241 }
242 Ok("disabled deaddrop.service".into())
243}
244
245#[cfg(target_os = "macos")]
246fn uninstall_os() -> Result<String> {
247 if let Ok(home) = home_dir() {
248 let path = home
249 .join("Library/LaunchAgents")
250 .join(format!("{LAUNCH_LABEL}.plist"));
251 let _ = Command::new("launchctl")
252 .args(["unload", &path.to_string_lossy()])
253 .status();
254 let _ = std::fs::remove_file(&path);
255 return Ok(format!("removed {}", path.display()));
256 }
257 Ok("unloaded LaunchAgent".into())
258}
259
260#[cfg(windows)]
261fn uninstall_os() -> Result<String> {
262 let _ = Command::new("schtasks")
263 .args(["/End", "/TN", WINDOWS_TASK])
264 .status();
265 let status = Command::new("schtasks")
266 .args(["/Delete", "/F", "/TN", WINDOWS_TASK])
267 .status()
268 .context("schtasks delete")?;
269 if !status.success() {
270 anyhow::bail!("no task {WINDOWS_TASK} (already uninstalled?)");
271 }
272 Ok(format!("removed Windows task {WINDOWS_TASK}"))
273}
274
275#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
276fn uninstall_os() -> Result<String> {
277 anyhow::bail!("service uninstall is supported on Linux, macOS, and Windows")
278}
279
280#[cfg(unix)]
281fn home_dir() -> Result<PathBuf> {
282 std::env::var_os("HOME")
283 .map(PathBuf::from)
284 .context("HOME is unset")
285}
286
287#[cfg(target_os = "linux")]
288fn dirs_config() -> Result<PathBuf> {
289 if let Some(x) = std::env::var_os("XDG_CONFIG_HOME") {
290 return Ok(PathBuf::from(x));
291 }
292 Ok(home_dir()?.join(".config"))
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use std::path::Path;
299
300 #[test]
301 fn systemd_unit_mentions_listen() {
302 let u = systemd_unit(
303 Path::new("/usr/bin/dd-daemon"),
304 Path::new("/var/lib/deaddrop"),
305 "0.0.0.0:7947".parse().unwrap(),
306 );
307 assert!(u.contains("0.0.0.0:7947"));
308 assert!(u.contains("/var/lib/deaddrop"));
309 }
310
311 #[test]
312 fn launchd_plist_is_xml() {
313 let p = launchd_plist(
314 Path::new("/usr/local/bin/dd-daemon"),
315 Path::new("/tmp/dd"),
316 "127.0.0.1:7947".parse().unwrap(),
317 );
318 assert!(p.contains(LAUNCH_LABEL));
319 assert!(p.contains("127.0.0.1:7947"));
320 }
321}