lunchctl/
control.rs

1use crate::agent::LaunchAgent;
2use crate::os::{get_user_id, run_shell};
3use crate::LaunchctlResult;
4
5/// Trait for controlling launch agents via launchctl.
6pub trait LaunchControllable {
7    /// Bootstrap the launch agent.
8    fn bootstrap(&self) -> LaunchctlResult<()>;
9
10    /// Boot out the launch agent.
11    fn boot_out(&self) -> LaunchctlResult<()>;
12
13    /// Check if the launch agent is running.
14    fn is_running(&self) -> LaunchctlResult<bool>;
15}
16
17impl LaunchAgent {
18    /// Format a launchctl command.
19    /// If the command is empty, it will return an empty string.
20    fn format_command(&self, command: &str) -> String {
21        if command.is_empty() {
22            return String::new();
23        }
24        format!(
25            "launchctl {} gui/{} '{}'",
26            command,
27            get_user_id(),
28            self.path().display()
29        )
30    }
31
32    fn format_bootstrap_command(&self) -> String {
33        self.format_command("bootstrap")
34    }
35
36    fn format_boot_out_command(&self) -> String {
37        self.format_command("bootout")
38    }
39
40    fn format_print_command(&self) -> String {
41        format!("launchctl print gui/{}/{}", get_user_id(), self.label)
42    }
43
44    /// Check if the output contains agent is running indicator.
45    fn check_is_running(output: &str) -> bool {
46        output.contains("state = running")
47    }
48}
49
50impl LaunchControllable for LaunchAgent {
51    /// Bootstrap the launch agent.
52    fn bootstrap(&self) -> LaunchctlResult<()> {
53        let cmd = self.format_bootstrap_command();
54        run_shell(&cmd).map(|_| ())
55    }
56
57    /// Boot out the launch agent.
58    /// It means not only stop, but also deactivate the launch agent.
59    fn boot_out(&self) -> LaunchctlResult<()> {
60        let cmd = self.format_boot_out_command();
61        run_shell(&cmd).map(|_| ())
62    }
63
64    /// Check if the launch agent is running.
65    fn is_running(&self) -> LaunchctlResult<bool> {
66        let cmd = self.format_print_command();
67
68        let output = run_shell(&cmd)?;
69        Ok(LaunchAgent::check_is_running(&output))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_format_command() {
79        let agent = LaunchAgent::new("test");
80        let agent_path = agent.path().display().to_string();
81        let user_id = get_user_id();
82
83        assert_eq!(
84            agent.format_command("subcommand"),
85            format!("launchctl subcommand gui/{user_id} '{agent_path}'")
86        );
87        assert_eq!(
88            agent.format_command("manageruid"),
89            format!("launchctl manageruid gui/{user_id} '{agent_path}'")
90        );
91        assert_eq!(agent.format_command(""), "");
92    }
93
94    #[test]
95    fn test_format_bootstrap_command() {
96        let agent = LaunchAgent::new("test");
97        let user_id = get_user_id();
98        let agent_path = agent.path().display().to_string();
99
100        assert_eq!(
101            agent.format_bootstrap_command(),
102            format!("launchctl bootstrap gui/{user_id} '{agent_path}'")
103        );
104    }
105
106    #[test]
107    fn test_format_bootout_command() {
108        let agent = LaunchAgent::new("test");
109        let user_id = get_user_id();
110        let agent_path = agent.path().display().to_string();
111
112        assert_eq!(
113            agent.format_boot_out_command(),
114            format!("launchctl bootout gui/{user_id} '{agent_path}'")
115        );
116    }
117
118    #[test]
119    fn test_check_info_command() {
120        let agent = LaunchAgent::new("test");
121        let user_id = get_user_id();
122
123        assert_eq!(
124            agent.format_print_command(),
125            format!("launchctl print gui/{user_id}/test")
126        );
127    }
128
129    #[test]
130    fn test_check_is_running() {
131        let output = "
132{
133        domain = gui/501 [100003]
134        asid = 100003
135
136        jetsam memory limit (active) = (unlimited)
137        jetsam memory limit (inactive) = (unlimited)
138        jetsamproperties category = daemon
139        jetsam thread limit = 32
140        cpumon = default
141        job state = running
142        probabilistic guard malloc policy = {
143                activation rate = 1/1000
144                sample rate = 1/0
145        }
146
147        properties = keepalive | runatload | inferred program | managed LWCR | has LWCR
148}
149        ";
150        assert!(LaunchAgent::check_is_running(output));
151
152        let output = "
153        {
154            domain = gui/501 [100003]
155            asid = 100003
156        }
157        ";
158        assert!(!LaunchAgent::check_is_running(output));
159    }
160}