Skip to main content

rightkit_daemon/
install.rs

1//! Install, start, stop, query and remove a user-level background service (rightkit#405):
2//! launchd on macOS, `systemd --user` on Linux and a Task Scheduler task on Windows, behind one
3//! trait.
4//!
5//! Every manager works through a [`CommandRunner`], the one place a command line leaves the
6//! process, so a host or a test can supply its own and the OS service manager is never needed to
7//! exercise the code that builds the unit file, the plist or the task XML. [`SystemRunner`] is the
8//! real one.
9//!
10//! What is written and run is built from a [`ServiceSpec`] and nothing else: the service name is
11//! restricted to `[A-Za-z0-9._-]`, every value is escaped for the file format it lands in (XML,
12//! plist, systemd), and a program path must be absolute. The unit files are written atomically,
13//! owner-only. Nothing here asks for administrator rights: the services are per-user.
14
15use crate::record::write_atomic;
16use std::io;
17use std::path::{Path, PathBuf};
18
19/// When the service is started again after it exits.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21#[non_exhaustive]
22pub enum RestartPolicy {
23    /// Never (run at login only).
24    Never,
25    /// After a non-zero exit.
26    #[default]
27    OnFailure,
28    /// After any exit.
29    Always,
30}
31
32/// What to run.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ServiceSpec {
35    /// The service name: `[A-Za-z0-9._-]`, at most 128 characters. launchd uses it as the label.
36    pub name: String,
37    /// The program: an absolute path.
38    pub exec: PathBuf,
39    pub args: Vec<String>,
40    /// Environment variables for the service. Not supported by Task Scheduler (refused there).
41    pub env: Vec<(String, String)>,
42    pub restart: RestartPolicy,
43}
44
45impl ServiceSpec {
46    pub fn new(name: impl Into<String>, exec: impl Into<PathBuf>) -> Self {
47        Self {
48            name: name.into(),
49            exec: exec.into(),
50            args: Vec::new(),
51            env: Vec::new(),
52            restart: RestartPolicy::default(),
53        }
54    }
55
56    fn validate(&self) -> io::Result<()> {
57        let bad = |what: &str| {
58            Err(io::Error::new(
59                io::ErrorKind::InvalidInput,
60                what.to_string(),
61            ))
62        };
63        if self.name.is_empty()
64            || self.name.len() > 128
65            || !self
66                .name
67                .bytes()
68                .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
69        {
70            return bad("a service name is 1 to 128 characters of letters, digits, . _ -");
71        }
72        if !self.exec.is_absolute() {
73            return bad("the service program must be an absolute path");
74        }
75        let text_ok = |s: &str| !s.contains('\0') && !s.contains('\n') && !s.contains('\r');
76        if !text_ok(&self.exec.to_string_lossy())
77            || !self.args.iter().all(|a| text_ok(a))
78            || !self
79                .env
80                .iter()
81                .all(|(k, v)| text_ok(v) && !k.is_empty() && !k.contains('=') && text_ok(k))
82        {
83            return bad("service arguments and environment hold no NUL or line break");
84        }
85        Ok(())
86    }
87}
88
89/// What a manager says about a service.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum ServiceState {
93    NotInstalled,
94    /// Installed and not running.
95    Stopped,
96    /// Running; the process id when the manager reports one.
97    Running {
98        pid: Option<u32>,
99    },
100}
101
102/// The result of one command.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct CommandOutput {
105    pub code: i32,
106    pub stdout: String,
107    pub stderr: String,
108}
109
110impl CommandOutput {
111    pub fn success(&self) -> bool {
112        self.code == 0
113    }
114}
115
116/// Runs a command line for a manager. The seam: replace it to observe or fake the OS tools.
117pub trait CommandRunner: Send + Sync {
118    fn run(&self, program: &str, args: &[String]) -> io::Result<CommandOutput>;
119}
120
121/// Runs the real command (no shell, no window on Windows).
122#[derive(Debug, Clone, Copy, Default)]
123pub struct SystemRunner;
124
125impl CommandRunner for SystemRunner {
126    fn run(&self, program: &str, args: &[String]) -> io::Result<CommandOutput> {
127        let mut cmd = std::process::Command::new(program);
128        cmd.args(args);
129        #[cfg(windows)]
130        {
131            use std::os::windows::process::CommandExt;
132            cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
133        }
134        let out = cmd.output()?;
135        Ok(CommandOutput {
136            code: out.status.code().unwrap_or(-1),
137            stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
138            stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
139        })
140    }
141}
142
143/// One user-level service manager.
144pub trait ServiceManager: Send + Sync {
145    /// Write the definition and register it (it starts at the next login, or at `start`).
146    fn install(&self, spec: &ServiceSpec) -> io::Result<()>;
147    fn start(&self, name: &str) -> io::Result<()>;
148    fn stop(&self, name: &str) -> io::Result<()>;
149    fn status(&self, name: &str) -> io::Result<ServiceState>;
150    /// Stop, unregister and delete the definition. A service that is not installed is not an
151    /// error.
152    fn uninstall(&self, name: &str) -> io::Result<()>;
153}
154
155fn check_name(name: &str) -> io::Result<()> {
156    ServiceSpec::new(name, "/x").validate()
157}
158
159fn failed(what: &str, out: &CommandOutput) -> io::Error {
160    io::Error::other(format!(
161        "{what} failed ({}): {}",
162        out.code,
163        out.stderr.trim()
164    ))
165}
166
167fn run_ok(runner: &dyn CommandRunner, what: &str, program: &str, args: &[&str]) -> io::Result<()> {
168    let args: Vec<String> = args.iter().map(|a| (*a).to_string()).collect();
169    let out = runner.run(program, &args)?;
170    if out.success() {
171        Ok(())
172    } else {
173        Err(failed(what, &out))
174    }
175}
176
177fn xml_escape(text: &str) -> String {
178    let mut out = String::with_capacity(text.len());
179    for c in text.chars() {
180        match c {
181            '&' => out.push_str("&amp;"),
182            '<' => out.push_str("&lt;"),
183            '>' => out.push_str("&gt;"),
184            '"' => out.push_str("&quot;"),
185            '\'' => out.push_str("&apos;"),
186            c => out.push(c),
187        }
188    }
189    out
190}
191
192// ---------------------------------------------------------------------------------------------
193// launchd (macOS)
194// ---------------------------------------------------------------------------------------------
195
196/// A launchd user agent: `~/Library/LaunchAgents/<name>.plist`, loaded into `gui/<uid>`.
197pub struct LaunchAgent {
198    runner: Box<dyn CommandRunner>,
199    agents_dir: PathBuf,
200    uid: u32,
201}
202
203impl LaunchAgent {
204    /// The real manager for the current user.
205    #[cfg(unix)]
206    pub fn for_current_user() -> io::Result<Self> {
207        let home = std::env::var_os("HOME")
208            .map(PathBuf::from)
209            .filter(|h| h.is_absolute())
210            .ok_or_else(|| io::Error::other("no absolute HOME"))?;
211        Ok(Self::with_parts(
212            Box::new(SystemRunner),
213            home.join("Library").join("LaunchAgents"),
214            crate::transport::current_uid(),
215        ))
216    }
217
218    /// A manager over an explicit runner, agents directory and uid.
219    pub fn with_parts(runner: Box<dyn CommandRunner>, agents_dir: PathBuf, uid: u32) -> Self {
220        Self {
221            runner,
222            agents_dir,
223            uid,
224        }
225    }
226
227    fn plist_path(&self, name: &str) -> PathBuf {
228        self.agents_dir.join(format!("{name}.plist"))
229    }
230
231    fn domain(&self) -> String {
232        format!("gui/{}", self.uid)
233    }
234
235    fn target(&self, name: &str) -> String {
236        format!("gui/{}/{name}", self.uid)
237    }
238
239    /// The plist text for `spec`.
240    pub fn plist(spec: &ServiceSpec) -> String {
241        let mut out = String::from(
242            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n",
243        );
244        out.push_str(&format!(
245            "  <key>Label</key>\n  <string>{}</string>\n  <key>ProgramArguments</key>\n  <array>\n    <string>{}</string>\n",
246            xml_escape(&spec.name),
247            xml_escape(&spec.exec.to_string_lossy())
248        ));
249        for arg in &spec.args {
250            out.push_str(&format!("    <string>{}</string>\n", xml_escape(arg)));
251        }
252        out.push_str("  </array>\n  <key>RunAtLoad</key>\n  <true/>\n");
253        if !spec.env.is_empty() {
254            out.push_str("  <key>EnvironmentVariables</key>\n  <dict>\n");
255            for (k, v) in &spec.env {
256                out.push_str(&format!(
257                    "    <key>{}</key>\n    <string>{}</string>\n",
258                    xml_escape(k),
259                    xml_escape(v)
260                ));
261            }
262            out.push_str("  </dict>\n");
263        }
264        match spec.restart {
265            RestartPolicy::Never => {}
266            RestartPolicy::Always => out.push_str("  <key>KeepAlive</key>\n  <true/>\n"),
267            RestartPolicy::OnFailure => out.push_str(
268                "  <key>KeepAlive</key>\n  <dict>\n    <key>SuccessfulExit</key>\n    <false/>\n  </dict>\n",
269            ),
270        }
271        out.push_str("</dict>\n</plist>\n");
272        out
273    }
274}
275
276impl ServiceManager for LaunchAgent {
277    fn install(&self, spec: &ServiceSpec) -> io::Result<()> {
278        spec.validate()?;
279        std::fs::create_dir_all(&self.agents_dir)?;
280        let path = self.plist_path(&spec.name);
281        write_atomic(&path, Self::plist(spec).as_bytes())?;
282        // Replace a loaded copy: bootout fails when nothing is loaded, which is fine.
283        let _ = self
284            .runner
285            .run("launchctl", &["bootout".into(), self.target(&spec.name)]);
286        run_ok(
287            &*self.runner,
288            "launchctl bootstrap",
289            "launchctl",
290            &["bootstrap", &self.domain(), &path.to_string_lossy()],
291        )
292    }
293
294    fn start(&self, name: &str) -> io::Result<()> {
295        check_name(name)?;
296        run_ok(
297            &*self.runner,
298            "launchctl kickstart",
299            "launchctl",
300            &["kickstart", "-k", &self.target(name)],
301        )
302    }
303
304    fn stop(&self, name: &str) -> io::Result<()> {
305        check_name(name)?;
306        // `kill` ends the process; KeepAlive may start it again, which is the policy's call. A
307        // service that is not loaded is already stopped.
308        let out = self.runner.run(
309            "launchctl",
310            &["kill".into(), "SIGTERM".into(), self.target(name)],
311        )?;
312        if out.success() || !matches!(self.status(name)?, ServiceState::Running { .. }) {
313            Ok(())
314        } else {
315            Err(failed("launchctl kill", &out))
316        }
317    }
318
319    fn status(&self, name: &str) -> io::Result<ServiceState> {
320        check_name(name)?;
321        let out = self
322            .runner
323            .run("launchctl", &["print".into(), self.target(name)])?;
324        if !out.success() {
325            return Ok(if self.plist_path(name).exists() {
326                ServiceState::Stopped
327            } else {
328                ServiceState::NotInstalled
329            });
330        }
331        let pid = out
332            .stdout
333            .lines()
334            .filter_map(|l| l.trim().strip_prefix("pid = "))
335            .find_map(|p| p.trim().parse::<u32>().ok());
336        Ok(match pid {
337            Some(pid) => ServiceState::Running { pid: Some(pid) },
338            None => ServiceState::Stopped,
339        })
340    }
341
342    fn uninstall(&self, name: &str) -> io::Result<()> {
343        check_name(name)?;
344        let _ = self
345            .runner
346            .run("launchctl", &["bootout".into(), self.target(name)]);
347        match std::fs::remove_file(self.plist_path(name)) {
348            Ok(()) => Ok(()),
349            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
350            Err(e) => Err(e),
351        }
352    }
353}
354
355// ---------------------------------------------------------------------------------------------
356// systemd --user (Linux)
357// ---------------------------------------------------------------------------------------------
358
359/// A `systemd --user` service: `<config dir>/systemd/user/<name>.service`.
360pub struct SystemdUser {
361    runner: Box<dyn CommandRunner>,
362    unit_dir: PathBuf,
363}
364
365impl SystemdUser {
366    /// The real manager for the current user (`$XDG_CONFIG_HOME` or `~/.config`).
367    pub fn for_current_user() -> io::Result<Self> {
368        let base = std::env::var_os("XDG_CONFIG_HOME")
369            .map(PathBuf::from)
370            .filter(|p| p.is_absolute())
371            .or_else(|| {
372                std::env::var_os("HOME")
373                    .map(PathBuf::from)
374                    .filter(|h| h.is_absolute())
375                    .map(|h| h.join(".config"))
376            })
377            .ok_or_else(|| io::Error::other("no absolute HOME or XDG_CONFIG_HOME"))?;
378        Ok(Self::with_parts(
379            Box::new(SystemRunner),
380            base.join("systemd").join("user"),
381        ))
382    }
383
384    pub fn with_parts(runner: Box<dyn CommandRunner>, unit_dir: PathBuf) -> Self {
385        Self { runner, unit_dir }
386    }
387
388    fn unit_path(&self, name: &str) -> PathBuf {
389        self.unit_dir.join(format!("{name}.service"))
390    }
391
392    fn quote(arg: &str) -> String {
393        let mut out = String::from("\"");
394        for c in arg.chars() {
395            match c {
396                '"' => out.push_str("\\\""),
397                '\\' => out.push_str("\\\\"),
398                '%' => out.push_str("%%"),
399                '$' => out.push_str("$$"),
400                c => out.push(c),
401            }
402        }
403        out.push('"');
404        out
405    }
406
407    /// The unit text for `spec`.
408    pub fn unit(spec: &ServiceSpec) -> String {
409        let mut exec = Self::quote(&spec.exec.to_string_lossy());
410        for arg in &spec.args {
411            exec.push(' ');
412            exec.push_str(&Self::quote(arg));
413        }
414        let mut out = format!(
415            "[Unit]\nDescription={}\n\n[Service]\nExecStart={exec}\n",
416            spec.name
417        );
418        for (k, v) in &spec.env {
419            out.push_str(&format!(
420                "Environment={}\n",
421                Self::quote(&format!("{k}={v}"))
422            ));
423        }
424        out.push_str(match spec.restart {
425            RestartPolicy::Never => "Restart=no\n",
426            RestartPolicy::OnFailure => "Restart=on-failure\n",
427            RestartPolicy::Always => "Restart=always\n",
428        });
429        out.push_str("\n[Install]\nWantedBy=default.target\n");
430        out
431    }
432}
433
434impl ServiceManager for SystemdUser {
435    fn install(&self, spec: &ServiceSpec) -> io::Result<()> {
436        spec.validate()?;
437        std::fs::create_dir_all(&self.unit_dir)?;
438        write_atomic(&self.unit_path(&spec.name), Self::unit(spec).as_bytes())?;
439        run_ok(
440            &*self.runner,
441            "systemctl daemon-reload",
442            "systemctl",
443            &["--user", "daemon-reload"],
444        )?;
445        run_ok(
446            &*self.runner,
447            "systemctl enable",
448            "systemctl",
449            &["--user", "enable", &format!("{}.service", spec.name)],
450        )
451    }
452
453    fn start(&self, name: &str) -> io::Result<()> {
454        check_name(name)?;
455        run_ok(
456            &*self.runner,
457            "systemctl start",
458            "systemctl",
459            &["--user", "start", &format!("{name}.service")],
460        )
461    }
462
463    fn stop(&self, name: &str) -> io::Result<()> {
464        check_name(name)?;
465        run_ok(
466            &*self.runner,
467            "systemctl stop",
468            "systemctl",
469            &["--user", "stop", &format!("{name}.service")],
470        )
471    }
472
473    fn status(&self, name: &str) -> io::Result<ServiceState> {
474        check_name(name)?;
475        if !self.unit_path(name).exists() {
476            return Ok(ServiceState::NotInstalled);
477        }
478        let unit = format!("{name}.service");
479        let active = self.runner.run(
480            "systemctl",
481            &["--user".into(), "is-active".into(), unit.clone()],
482        )?;
483        if active.stdout.trim() != "active" {
484            return Ok(ServiceState::Stopped);
485        }
486        let pid = self
487            .runner
488            .run(
489                "systemctl",
490                &[
491                    "--user".into(),
492                    "show".into(),
493                    "-p".into(),
494                    "MainPID".into(),
495                    "--value".into(),
496                    unit,
497                ],
498            )
499            .ok()
500            .and_then(|o| o.stdout.trim().parse::<u32>().ok())
501            .filter(|p| *p != 0);
502        Ok(ServiceState::Running { pid })
503    }
504
505    fn uninstall(&self, name: &str) -> io::Result<()> {
506        check_name(name)?;
507        let unit = format!("{name}.service");
508        let _ = self
509            .runner
510            .run("systemctl", &["--user".into(), "stop".into(), unit.clone()]);
511        let _ = self
512            .runner
513            .run("systemctl", &["--user".into(), "disable".into(), unit]);
514        match std::fs::remove_file(self.unit_path(name)) {
515            Ok(()) => {}
516            Err(e) if e.kind() == io::ErrorKind::NotFound => {}
517            Err(e) => return Err(e),
518        }
519        let _ = self
520            .runner
521            .run("systemctl", &["--user".into(), "daemon-reload".into()]);
522        Ok(())
523    }
524}
525
526// ---------------------------------------------------------------------------------------------
527// Task Scheduler (Windows)
528// ---------------------------------------------------------------------------------------------
529
530/// A Task Scheduler task that starts the program at the user's logon, in the user's own session
531/// with the user's own rights (`LeastPrivilege`): no administrator rights are asked or granted.
532/// The task definition is written as XML under `xml_dir` and registered with `schtasks`.
533pub struct ScheduledTask {
534    runner: Box<dyn CommandRunner>,
535    xml_dir: PathBuf,
536}
537
538impl ScheduledTask {
539    /// The real manager; the XML is staged under `xml_dir` (an existing owner-only directory).
540    pub fn new(xml_dir: impl Into<PathBuf>) -> Self {
541        Self::with_parts(Box::new(SystemRunner), xml_dir.into())
542    }
543
544    pub fn with_parts(runner: Box<dyn CommandRunner>, xml_dir: PathBuf) -> Self {
545        Self { runner, xml_dir }
546    }
547
548    /// One Windows command-line argument, quoted as `CommandLineToArgvW` reads it.
549    fn quote(arg: &str) -> String {
550        if !arg.is_empty() && !arg.contains([' ', '\t', '"']) {
551            return arg.to_string();
552        }
553        let mut out = String::from("\"");
554        let mut backslashes = 0;
555        for c in arg.chars() {
556            match c {
557                '\\' => backslashes += 1,
558                '"' => {
559                    out.push_str(&"\\".repeat(backslashes * 2 + 1));
560                    out.push('"');
561                    backslashes = 0;
562                }
563                c => {
564                    out.push_str(&"\\".repeat(backslashes));
565                    backslashes = 0;
566                    out.push(c);
567                }
568            }
569        }
570        out.push_str(&"\\".repeat(backslashes * 2));
571        out.push('"');
572        out
573    }
574
575    /// The task XML for `spec`. A spec with environment variables is refused: a scheduled task
576    /// has none.
577    pub fn task_xml(spec: &ServiceSpec) -> io::Result<String> {
578        spec.validate()?;
579        if !spec.env.is_empty() {
580            return Err(io::Error::new(
581                io::ErrorKind::InvalidInput,
582                "a scheduled task cannot set environment variables",
583            ));
584        }
585        let arguments = spec
586            .args
587            .iter()
588            .map(|a| Self::quote(a))
589            .collect::<Vec<_>>()
590            .join(" ");
591        let restart = match spec.restart {
592            RestartPolicy::Never => String::new(),
593            _ => "<RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>"
594                .to_string(),
595        };
596        Ok(format!(
597            "<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n  <RegistrationInfo><Description>{name}</Description></RegistrationInfo>\n  <Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>\n  <Principals><Principal id=\"Author\"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>\n  <Settings>\n    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n    <Hidden>true</Hidden>\n    {restart}\n  </Settings>\n  <Actions Context=\"Author\"><Exec><Command>{exec}</Command><Arguments>{arguments}</Arguments></Exec></Actions>\n</Task>\n",
598            name = xml_escape(&spec.name),
599            exec = xml_escape(&spec.exec.to_string_lossy()),
600            arguments = xml_escape(&arguments),
601        ))
602    }
603}
604
605/// `schtasks /Create /XML` reads the file in its own encoding; the file is UTF-16 LE with a BOM,
606/// which the declaration above says.
607fn utf16_le_with_bom(text: &str) -> Vec<u8> {
608    let mut out = vec![0xFF, 0xFE];
609    for unit in text.encode_utf16() {
610        out.extend_from_slice(&unit.to_le_bytes());
611    }
612    out
613}
614
615impl ServiceManager for ScheduledTask {
616    fn install(&self, spec: &ServiceSpec) -> io::Result<()> {
617        let xml = Self::task_xml(spec)?;
618        std::fs::create_dir_all(&self.xml_dir)?;
619        let path = self.xml_dir.join(format!("{}.xml", spec.name));
620        write_atomic(&path, &utf16_le_with_bom(&xml))?;
621        let result = run_ok(
622            &*self.runner,
623            "schtasks /Create",
624            "schtasks",
625            &[
626                "/Create",
627                "/TN",
628                &spec.name,
629                "/XML",
630                &path.to_string_lossy(),
631                "/F",
632            ],
633        );
634        let _ = std::fs::remove_file(&path);
635        result
636    }
637
638    fn start(&self, name: &str) -> io::Result<()> {
639        check_name(name)?;
640        run_ok(
641            &*self.runner,
642            "schtasks /Run",
643            "schtasks",
644            &["/Run", "/TN", name],
645        )
646    }
647
648    fn stop(&self, name: &str) -> io::Result<()> {
649        check_name(name)?;
650        let out = self
651            .runner
652            .run("schtasks", &["/End".into(), "/TN".into(), name.to_string()])?;
653        // A task that is not running is already stopped.
654        if out.success() || !matches!(self.status(name)?, ServiceState::Running { .. }) {
655            Ok(())
656        } else {
657            Err(failed("schtasks /End", &out))
658        }
659    }
660
661    fn status(&self, name: &str) -> io::Result<ServiceState> {
662        check_name(name)?;
663        let out = self.runner.run(
664            "schtasks",
665            &[
666                "/Query".into(),
667                "/TN".into(),
668                name.to_string(),
669                "/FO".into(),
670                "LIST".into(),
671                "/V".into(),
672            ],
673        )?;
674        if !out.success() {
675            return Ok(ServiceState::NotInstalled);
676        }
677        let running = out.stdout.lines().any(|l| {
678            l.split_once(':').is_some_and(|(k, v)| {
679                k.trim().eq_ignore_ascii_case("Status") && v.trim().eq_ignore_ascii_case("Running")
680            })
681        });
682        Ok(if running {
683            ServiceState::Running { pid: None }
684        } else {
685            ServiceState::Stopped
686        })
687    }
688
689    fn uninstall(&self, name: &str) -> io::Result<()> {
690        check_name(name)?;
691        let _ = self
692            .runner
693            .run("schtasks", &["/End".into(), "/TN".into(), name.to_string()]);
694        let out = self.runner.run(
695            "schtasks",
696            &[
697                "/Delete".into(),
698                "/TN".into(),
699                name.to_string(),
700                "/F".into(),
701            ],
702        )?;
703        if out.success() || matches!(self.status(name)?, ServiceState::NotInstalled) {
704            Ok(())
705        } else {
706            Err(failed("schtasks /Delete", &out))
707        }
708    }
709}
710
711/// The manager for this platform: launchd, `systemd --user` or Task Scheduler (its XML is staged
712/// under `xml_dir`, which only Windows uses).
713pub fn current_manager(xml_dir: &Path) -> io::Result<Box<dyn ServiceManager>> {
714    #[cfg(target_os = "macos")]
715    {
716        let _ = xml_dir;
717        Ok(Box::new(LaunchAgent::for_current_user()?))
718    }
719    #[cfg(all(unix, not(target_os = "macos")))]
720    {
721        let _ = xml_dir;
722        Ok(Box::new(SystemdUser::for_current_user()?))
723    }
724    #[cfg(windows)]
725    {
726        Ok(Box::new(ScheduledTask::new(xml_dir)))
727    }
728    #[cfg(not(any(unix, windows)))]
729    {
730        let _ = xml_dir;
731        Err(io::Error::new(
732            io::ErrorKind::Unsupported,
733            "no user service manager for this platform",
734        ))
735    }
736}