├── .gitignore ├── src ├── tests │ ├── mod.rs │ └── service.rs └── lib.rs ├── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod service; 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "launchctl" 3 | version = "0.3.2" 4 | edition = "2021" 5 | authors = ["Sylvan Franklin"] 6 | description = "A simple library for managing system services on MacOS" 7 | repository = "https://github.com/sylvanfranklin/launchctl" 8 | keywords = ["system", "MacOS", "service", "launcher"] 9 | license = "MIT" 10 | 11 | [dependencies] 12 | bon = "3.1.1" 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /src/tests/service.rs: -------------------------------------------------------------------------------- 1 | use crate::Service; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use super::*; 6 | 7 | // make these tests more comprehensive 8 | #[test] 9 | fn creation_basic() { 10 | let service = Service::builder() 11 | .name("com..") 12 | .build(); 13 | 14 | assert_eq!(service.name, "com.."); 15 | assert_eq!(service.uid, "501"); 16 | assert_eq!(service.domain_target, "gui/501"); 17 | assert_eq!( 18 | service.service_target, 19 | "gui/501/com.." 20 | ); 21 | assert_eq!( 22 | service.plist_path, 23 | "~/Library/LaunchAgents/com...plist" 24 | ); 25 | assert_eq!( 26 | service.error_log_path, 27 | "/tmp/com.._501.err.log" 28 | ); 29 | assert_eq!( 30 | service.out_log_path, 31 | "/tmp/com.._501.out.log" 32 | ); 33 | } 34 | #[test] 35 | fn creation_advanced() { 36 | let service = Service::builder() 37 | .name("some weird unconventional name") 38 | .uid("401") 39 | .build(); 40 | 41 | assert_eq!(service.name, "some weird unconventional name"); 42 | assert_eq!(service.uid, "401"); 43 | assert_eq!(service.domain_target, "gui/401"); 44 | assert_eq!( 45 | service.service_target, 46 | "gui/401/some weird unconventional name" 47 | ); 48 | assert_eq!( 49 | service.plist_path, 50 | "~/Library/LaunchAgents/some weird unconventional name.plist" 51 | ); 52 | assert_eq!( 53 | service.error_log_path, 54 | "/tmp/some weird unconventional name_401.err.log" 55 | ); 56 | assert_eq!( 57 | service.out_log_path, 58 | "/tmp/some weird unconventional name_401.out.log" 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Launchctl 2 | Tiny Rust wrapper library for MacOS service launcher `launchctl`. This library 3 | offers a more intuitive interface for managing services on MacOS, **cuz I'm not finna syscall that confusing shi every time I want to start a service**. Other Rust 4 | crates exist for interfacing with cross platform launch services. This library 5 | is specifically for MacOS. For more info about `launchctl` and `launchd` see 6 | the [official apple docs](https://ss64.com/mac/launchctl.html). 7 | 8 | ### Install 9 | ```sh 10 | cargo add launchctl 11 | ``` 12 | > [!NOTE] 13 | > Coming soon is a CLI version which will make it easier to automate creating a service. 14 | 15 | ### Usage 16 | The Service struct is the main entry point of this library. It uses a `bon` builder. 17 | ```rust 18 | fn main() { 19 | // basic construction of a service 20 | let basic_service = Service::builder() 21 | .name("com..") 22 | .build(); 23 | 24 | // more advanced construction of a service 25 | let more_custom = Service::builder() 26 | .name("com..") 27 | . 28 | .build(); 29 | 30 | // create a .plist file for the service 31 | // ... 32 | 33 | basic.start().unwrap(); 34 | custom.stop().unwrap(); 35 | } 36 | 37 | ``` 38 | 39 | ### Limitations 40 | Currently this crate does not support creating or modifying plist files. There 41 | are other crates that can give you this behavior 42 | [https://github.com/koenichiwa/launchd](https://github.com/koenichiwa/launchd), or you can hard code them as strings 43 | which is what I prefer. 44 | 45 | Here is an example of how I do that in my [srhd](https://github.com/sylvanfranklin/srhd) crate. 46 | 47 | ```rs 48 | pub fn install(ctl: &launchctl::Service) -> Result<(), Error> { 49 | let plist = format!( 50 | " 51 | 52 | 53 | 54 | Label 55 | {} 56 | ProgramArguments 57 | 58 | {} 59 | 60 | RunAtLoad 61 | 62 | KeepAlive 63 | 64 | SuccessfulExit 65 | 66 | Crashed 67 | 68 | 69 | StandardOutPath 70 | /tmp/srhd_sylvanfranklin.out.log 71 | StandardErrorPath 72 | /tmp/srhd_sylvanfranklin.err.log 73 | ProcessType 74 | Interactive 75 | Nice 76 | -20 77 | 78 | ", 79 | ctl.name, 80 | ctl.bin_path.to_str().unwrap(), 81 | ); 82 | 83 | Ok(fs::write(ctl.plist_path.clone(), plist)?) 84 | } 85 | ``` 86 | 87 | # Contribution 88 | Bro I love when people contribute or even submit issues. It's good for 89 | everyone's career and understanding of everything, by all means open an issue or 90 | a PR! 91 | 92 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bon" 7 | version = "3.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e47d5c63335658326076cf7c81795af665c534ea552da69526d6cef51b12ed9" 10 | dependencies = [ 11 | "bon-macros", 12 | "rustversion", 13 | ] 14 | 15 | [[package]] 16 | name = "bon-macros" 17 | version = "3.1.1" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "b162272b6d55562ea30cc937d74ef4d07399e507bfd6eb3860f6a845c7264eef" 20 | dependencies = [ 21 | "darling", 22 | "ident_case", 23 | "prettyplease", 24 | "proc-macro2", 25 | "quote", 26 | "rustversion", 27 | "syn", 28 | ] 29 | 30 | [[package]] 31 | name = "darling" 32 | version = "0.20.10" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 35 | dependencies = [ 36 | "darling_core", 37 | "darling_macro", 38 | ] 39 | 40 | [[package]] 41 | name = "darling_core" 42 | version = "0.20.10" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 45 | dependencies = [ 46 | "fnv", 47 | "ident_case", 48 | "proc-macro2", 49 | "quote", 50 | "strsim", 51 | "syn", 52 | ] 53 | 54 | [[package]] 55 | name = "darling_macro" 56 | version = "0.20.10" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 59 | dependencies = [ 60 | "darling_core", 61 | "quote", 62 | "syn", 63 | ] 64 | 65 | [[package]] 66 | name = "fnv" 67 | version = "1.0.7" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 70 | 71 | [[package]] 72 | name = "ident_case" 73 | version = "1.0.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 76 | 77 | [[package]] 78 | name = "launchctl" 79 | version = "0.3.2" 80 | dependencies = [ 81 | "bon", 82 | ] 83 | 84 | [[package]] 85 | name = "prettyplease" 86 | version = "0.2.25" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" 89 | dependencies = [ 90 | "proc-macro2", 91 | "syn", 92 | ] 93 | 94 | [[package]] 95 | name = "proc-macro2" 96 | version = "1.0.92" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 99 | dependencies = [ 100 | "unicode-ident", 101 | ] 102 | 103 | [[package]] 104 | name = "quote" 105 | version = "1.0.37" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 108 | dependencies = [ 109 | "proc-macro2", 110 | ] 111 | 112 | [[package]] 113 | name = "rustversion" 114 | version = "1.0.18" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 117 | 118 | [[package]] 119 | name = "strsim" 120 | version = "0.11.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 123 | 124 | [[package]] 125 | name = "syn" 126 | version = "2.0.90" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 129 | dependencies = [ 130 | "proc-macro2", 131 | "quote", 132 | "unicode-ident", 133 | ] 134 | 135 | [[package]] 136 | name = "unicode-ident" 137 | version = "1.0.14" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 140 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use bon::Builder; 2 | use std::{fs, io::Error, process::Command}; 3 | 4 | #[cfg(test)] 5 | mod tests; 6 | 7 | /// Wrapper for the launchctl 8 | /// for more information about services on mac see https://ss64.com/mac/launchctl.html 9 | #[derive(Debug)] 10 | #[allow(dead_code)] 11 | #[derive(Builder)] 12 | pub struct Service { 13 | /// Name of the service typically (com..) 14 | #[builder(into)] 15 | pub name: String, 16 | /// id of logged in user typically 501 17 | #[builder(into, default = "501")] 18 | pub uid: String, 19 | /// The target of the domain (gui/) 20 | #[builder(into, default = format!("gui/{}", uid))] 21 | pub domain_target: String, 22 | /// The target of the service (gui//) 23 | #[builder(into, default = format!("{}/{}", domain_target, name))] 24 | pub service_target: String, 25 | /// Path to the plist file typically ~/Library/LaunchAgents/.plist 26 | #[builder(into, default = format!("~/Library/LaunchAgents/{}.plist", name))] 27 | pub plist_path: String, 28 | /// Path to the error log file default (/tmp/_.err.log) 29 | #[builder(into, default = format!("/tmp/{}_{}.err.log", name, uid))] 30 | pub error_log_path: String, 31 | /// Path to the out log file default (/tmp/_.out.log) 32 | #[builder(into, default = format!("/tmp/{}_{}.out.log", name, uid))] 33 | pub out_log_path: String, 34 | } 35 | 36 | #[allow(dead_code)] 37 | impl Service { 38 | /// Effectively the same as calling start and then stop 39 | pub fn restart(&self) -> Result<(), Error> { 40 | self.stop()?; 41 | self.start() 42 | } 43 | 44 | /// Attempts to stop the service 45 | pub fn stop(&self) -> Result<(), Error> { 46 | if !self.is_bootstrapped() { 47 | // in this case we just try to kill the service just in case it is running 48 | // without being bootstrapped, it will fail silently if it is not running 49 | self.cmd() 50 | .args(vec!["kill", "SIGTERM", &self.service_target]) 51 | .status()?; 52 | } else { 53 | // safely bootout the service 54 | self.cmd() 55 | .args(vec!["bootout", &self.domain_target, &self.plist_path]) 56 | .status()?; 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | /// Attempts to start the service 63 | pub fn start(&self) -> Result<(), Error> { 64 | self.create_log_files()?; 65 | 66 | if !self.is_bootstrapped() { 67 | // first enable the service, then it can be bootstrapped 68 | self.cmd() 69 | .args(vec!["enable", &self.service_target]) 70 | .status()?; 71 | self.cmd() 72 | .args(vec!["bootstrap", &self.domain_target, &self.plist_path]) 73 | .status()?; 74 | } else { 75 | // since we already have the service bootstrapped we can just kickstart it 76 | self.cmd() 77 | .args(vec!["kickstart", &self.plist_path]) 78 | .status()?; 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | fn cmd(&self) -> Command { 85 | // This makes an assumption that launchctl will always be in /bin 86 | // it also takes self in case this needs to be changed 87 | let mut command = Command::new("/bin/launchctl"); 88 | command 89 | .stdout(std::process::Stdio::null()) 90 | .stderr(std::process::Stdio::null()); 91 | 92 | return command; 93 | } 94 | 95 | /// checks if the log files exist, if not, creates them 96 | fn create_log_files(&self) -> Result<(), Error> { 97 | if !fs::metadata(&self.error_log_path).is_ok() { 98 | fs::write(&self.error_log_path, "")?; 99 | } 100 | 101 | if !fs::metadata(&self.out_log_path).is_ok() { 102 | fs::write(&self.out_log_path, "")?; 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | /// attempts to get the bootstrapped status of the service. Possibly can fail if permissions 109 | /// are not adequate or the target domain is malformed. 110 | fn is_bootstrapped(&self) -> bool { 111 | self.cmd() 112 | .args(vec!["print", &self.service_target]) 113 | .status() 114 | .unwrap_or_else(|_| panic!("Failed to check bootstrap for: {}", &self.name)) 115 | .success() 116 | } 117 | } 118 | --------------------------------------------------------------------------------