├── .github ├── CODEOWNERS └── workflows │ └── build.yaml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── cmdlet ├── .gitignore ├── Cargo.toml ├── build.rs └── src │ ├── main.rs │ ├── pwsh.rs │ └── service.rs ├── examples └── foobar │ ├── Cargo.toml │ └── src │ ├── cli.yml │ └── main.rs └── src ├── controller.rs ├── controller ├── dummy.rs ├── linux.rs ├── macos.rs └── windows.rs ├── lib.rs └── session.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # File auto-generated and managed by Devops 2 | /.github/ @devolutions/devops 3 | /.github/dependabot.yml @devolutions/security-managers 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: ['pc-windows-msvc', 'apple-darwin', 'unknown-linux-gnu'] 12 | arch: [x86_64, aarch64] 13 | include: 14 | - runner: ubuntu-latest 15 | os: unknown-linux-gnu 16 | - runner: windows-latest 17 | os: pc-windows-msvc 18 | - runner: macos-latest 19 | os: apple-darwin 20 | runs-on: ${{ matrix.runner }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Configure runner 26 | run: rustup target add ${{matrix.arch}}-${{matrix.os}} 27 | 28 | - name: Configure runner 29 | if: matrix.os == 'unknown-linux-gnu' 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get -o Acquire:retries=3 install libsystemd-dev 33 | 34 | - name: Build 35 | shell: pwsh 36 | run: cargo build --target ${{matrix.arch}}-${{matrix.os}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .idea/ 6 | .vscode/ 7 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | os: 6 | - osx 7 | - windows 8 | # Cache settings based on https://levans.fr/rust_travis_cache.html 9 | cache: 10 | directories: 11 | - /home/travis/.cargo 12 | before_cache: 13 | - rm -rf /home/travis/.cargo/registry 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ceviche" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license = "MIT/Apache-2.0" 6 | homepage = "https://github.com/devolutions/ceviche-rs" 7 | repository = "https://github.com/devolutions/ceviche-rs" 8 | authors = ["Marc-André Moreau ", "Sébastien Duquette ", "Richard Markiewicz "] 9 | keywords = ["daemon", "service"] 10 | description = "Rust daemon/service wrapper" 11 | exclude = [ 12 | ".*", 13 | "appveyor.yml" 14 | ] 15 | 16 | [dependencies] 17 | cfg-if = "1" 18 | ctrlc = { version = "3.1", features = ["termination"] } 19 | log = "0.4" 20 | 21 | [target.'cfg(windows)'.dependencies] 22 | widestring = "0.4.3" 23 | windows-sys = { version = "0.52", features = ["Win32_Foundation", "Win32_System_RemoteDesktop", "Win32_System_LibraryLoader", "Win32_UI_WindowsAndMessaging", 24 | "Win32_Security", "Win32_System_Services", "Win32_System_Diagnostics_Debug"]} 25 | 26 | [target.'cfg(target_os = "linux")'.dependencies] 27 | systemd-rs = { version="^0.1.2", optional = true } 28 | 29 | [target.'cfg(target_os = "macos")'.dependencies] 30 | core-foundation = "0.9" 31 | core-foundation-sys = "0.8" 32 | system-configuration-sys = "0.5" 33 | timer = "0.2" 34 | chrono = "0.4" 35 | libc = "0.2" 36 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ceviche-rs 2 | 3 | [![Cargo](https://img.shields.io/crates/v/ceviche.svg)](https://crates.io/crates/ceviche) 4 | [![Documentation](https://docs.rs/ceviche/badge.svg)](https://docs.rs/ceviche) 5 | ![Build](https://github.com/devolutions/ceviche-rs/actions/workflows/build.yaml/badge.svg) 6 | 7 | Service/daemon wrapper. Supports Windows, Linux (systemd) and macOS. 8 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - TARGET: x86_64-pc-windows-msvc 4 | CHANNEL: stable 5 | - TARGET: i686-pc-windows-msvc 6 | CHANNEL: stable 7 | install: 8 | - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" 9 | - rust-nightly-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" 10 | - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin 11 | - SET PATH=%PATH%;C:\MinGW\bin 12 | - rustc -V 13 | - cargo -V 14 | build: false 15 | test_script: 16 | - cargo test --verbose --jobs 4 17 | -------------------------------------------------------------------------------- /cmdlet/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /cmdlet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cmdlet_service" 3 | version = "0.3.0" 4 | edition = "2018" 5 | license = "MIT/Apache-2.0" 6 | homepage = "https://github.com/Devolutions/ceviche-rs" 7 | repository = "https://github.com/Devolutions/ceviche-rs" 8 | authors = ["Marc-André Moreau "] 9 | build = "build.rs" 10 | 11 | [dependencies] 12 | libc = "0" 13 | log = "0.4" 14 | log4rs = "1.3" 15 | ctrlc = "3.1" 16 | cfg-if = "0.1" 17 | base64 = "0.12" 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | which = { version = "3.0", default-features = false, features = [] } 21 | expand_str = "0.1" 22 | 23 | [target.'cfg(windows)'.build-dependencies] 24 | embed-resource = "1.3" 25 | 26 | [dependencies.ceviche] 27 | path = ".." 28 | -------------------------------------------------------------------------------- /cmdlet/build.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | 4 | #[cfg(target_os = "windows")] 5 | extern crate embed_resource; 6 | 7 | use std::env; 8 | use std::fs::File; 9 | use std::io::Write; 10 | 11 | #[cfg(target_os = "windows")] 12 | fn generate_version_rc() -> String { 13 | let output_name = "CmdletService"; 14 | let filename = format!("{}.exe", output_name); 15 | let company_name = "Devolutions Inc."; 16 | let legal_copyright = format!("Copyright 2020 {}", company_name); 17 | 18 | let version_number = env::var("CARGO_PKG_VERSION").unwrap() + ".0"; 19 | let version_commas = version_number.replace(".", ","); 20 | let file_description = output_name.clone(); 21 | let file_version = version_number.clone(); 22 | let internal_name = filename.clone(); 23 | let original_filename = filename.clone(); 24 | let product_name = output_name.clone(); 25 | let product_version = version_number.clone(); 26 | let vs_file_version = version_commas.clone(); 27 | let vs_product_version = version_commas.clone(); 28 | 29 | let version_rc = format!(r#" 30 | #include 31 | VS_VERSION_INFO VERSIONINFO 32 | FILEVERSION {vs_file_version} 33 | PRODUCTVERSION {vs_product_version} 34 | FILEFLAGSMASK 0x3fL 35 | #ifdef _DEBUG 36 | FILEFLAGS 0x1L 37 | #else 38 | FILEFLAGS 0x0L 39 | #endif 40 | FILEOS 0x40004L 41 | FILETYPE 0x1L 42 | FILESUBTYPE 0x0L 43 | BEGIN 44 | BLOCK "StringFileInfo" 45 | BEGIN 46 | BLOCK "040904b0" 47 | BEGIN 48 | VALUE "CompanyName", "{company_name}" 49 | VALUE "FileDescription", "{file_description}" 50 | VALUE "FileVersion", "{file_version}" 51 | VALUE "InternalName", "{internal_name}" 52 | VALUE "LegalCopyright", "{legal_copyright}" 53 | VALUE "OriginalFilename", "{original_filename}" 54 | VALUE "ProductName", "{product_name}" 55 | VALUE "ProductVersion", "{product_version}" 56 | END 57 | END 58 | BLOCK "VarFileInfo" 59 | BEGIN 60 | VALUE "Translation", 0x409, 1200 61 | END 62 | END 63 | "#, 64 | vs_file_version = vs_file_version, 65 | vs_product_version = vs_product_version, 66 | company_name = company_name, 67 | file_description = file_description, 68 | file_version = file_version, 69 | internal_name = internal_name, 70 | legal_copyright = legal_copyright, 71 | original_filename = original_filename, 72 | product_name = product_name, 73 | product_version = product_version); 74 | 75 | version_rc 76 | } 77 | 78 | fn main() { 79 | #[cfg(target_os = "windows")] { 80 | let out_dir = env::var("OUT_DIR").unwrap(); 81 | let version_rc_file = format!("{}/version.rc", out_dir); 82 | let version_rc_data = generate_version_rc(); 83 | let mut file = File::create(&version_rc_file).expect("cannot create version.rc file"); 84 | file.write(version_rc_data.as_bytes()).unwrap(); 85 | embed_resource::compile(&version_rc_file); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cmdlet/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | extern crate which; 4 | 5 | extern crate serde; 6 | extern crate serde_json; 7 | 8 | extern crate base64; 9 | 10 | use std::env; 11 | use std::sync::mpsc; 12 | 13 | use ceviche::controller::*; 14 | use ceviche::{Service, ServiceEvent}; 15 | 16 | use log::LevelFilter; 17 | use log4rs::append::console::ConsoleAppender; 18 | use log4rs::append::file::FileAppender; 19 | use log4rs::config::{Appender, Config, Root}; 20 | use log4rs::encode::pattern::PatternEncoder; 21 | 22 | mod pwsh; 23 | mod service; 24 | 25 | use service::CmdletService; 26 | 27 | enum CustomServiceEvent {} 28 | 29 | fn init_logging(service: &CmdletService, standalone_mode: bool) -> Option<()> { 30 | let mut log_path = service.get_working_dir()?; 31 | let log_file = service.get_log_file(); 32 | log_path.push(log_file); 33 | 34 | if standalone_mode { 35 | let stdout = ConsoleAppender::builder().build(); 36 | 37 | let config = Config::builder() 38 | .appender(Appender::builder().build("stdout", Box::new(stdout))) 39 | .build(Root::builder().appender("stdout").build(LevelFilter::Info)) 40 | .ok()?; 41 | 42 | log4rs::init_config(config).ok()?; 43 | } else { 44 | let file_appender = FileAppender::builder() 45 | .encoder(Box::new(PatternEncoder::new( 46 | "{d(%Y-%m-%d %H:%M:%S)} {M} [{h({l})}] - {m}{n}", 47 | ))) 48 | .build(log_path) 49 | .ok()?; 50 | 51 | let config = Config::builder() 52 | .appender(Appender::builder().build("file_appender", Box::new(file_appender))) 53 | .build( 54 | Root::builder() 55 | .appender("file_appender") 56 | .build(LevelFilter::Info), 57 | ) 58 | .ok()?; 59 | 60 | log4rs::init_config(config).ok()?; 61 | } 62 | Some(()) 63 | } 64 | 65 | fn cmdlet_service_main( 66 | rx: mpsc::Receiver>, 67 | _tx: mpsc::Sender>, 68 | args: Vec, 69 | standalone_mode: bool, 70 | ) -> u32 { 71 | let service = CmdletService::load().expect("unable to load service manifest"); 72 | init_logging(&service, standalone_mode); 73 | info!("{} service started", service.get_service_name()); 74 | info!("args: {:?}", args); 75 | 76 | service.start(); 77 | 78 | loop { 79 | if let Ok(control_code) = rx.recv() { 80 | info!("Received control code: {}", control_code); 81 | match control_code { 82 | ServiceEvent::Stop => { 83 | service.stop(); 84 | break 85 | } 86 | _ => (), 87 | } 88 | } 89 | } 90 | 91 | info!("{} service stopping", service.get_service_name()); 92 | 0 93 | } 94 | 95 | Service!("cmdlet", cmdlet_service_main); 96 | 97 | fn main() { 98 | let service = CmdletService::load().expect("unable to load cmdlet service"); 99 | let mut controller = Controller::new(service.get_service_name(), 100 | service.get_display_name(), service.get_description()); 101 | 102 | if let Some(cmd) = env::args().nth(1) { 103 | match cmd.as_str() { 104 | "create" => { 105 | if let Err(e) = controller.create() { 106 | println!("{}", e); 107 | } 108 | } 109 | "delete" => { 110 | if let Err(e) = controller.delete() { 111 | println!("{}", e); 112 | } 113 | } 114 | "start" => { 115 | if let Err(e) = controller.start() { 116 | println!("{}", e); 117 | } 118 | } 119 | "stop" => { 120 | if let Err(e) = controller.stop() { 121 | println!("{}", e); 122 | } 123 | } 124 | "run" => { 125 | let (tx, rx) = mpsc::channel(); 126 | let _tx = tx.clone(); 127 | 128 | ctrlc::set_handler(move || { 129 | let _ = tx.send(ServiceEvent::Stop); 130 | }).expect("Failed to register Ctrl-C handler"); 131 | 132 | cmdlet_service_main(rx, _tx, vec![], true); 133 | } 134 | _ => { 135 | println!("invalid command: {}", cmd); 136 | } 137 | } 138 | } else { 139 | let _result = controller.register(service_main_wrapper); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /cmdlet/src/pwsh.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::path::{PathBuf}; 3 | use std::process::{Command}; 4 | use serde::{Serialize, Deserialize}; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct PSModuleManifest { 8 | #[serde(rename = "ModuleVersion")] 9 | pub module_version: String, 10 | #[serde(rename = "CompanyName")] 11 | pub company_name: String, 12 | #[serde(rename = "Description")] 13 | pub description: String, 14 | } 15 | 16 | pub fn find_powershell() -> Option { 17 | if let Ok(powershell) = which::which("pwsh") { 18 | return Some(powershell); 19 | } 20 | which::which("powershell").ok() 21 | } 22 | 23 | pub fn encode_command(command: &str) -> String { 24 | let mut command_bytes: Vec = Vec::new(); 25 | for c in command.encode_utf16() { 26 | let b = c.to_le_bytes(); 27 | command_bytes.push(b[0]); 28 | command_bytes.push(b[1]); 29 | } 30 | base64::encode(command_bytes.as_slice()) 31 | } 32 | 33 | pub fn find_cmdlet_base(module_name: &str) -> Option { 34 | let powershell = find_powershell()?; 35 | 36 | let command = format!( 37 | "Get-Module -Name {} -ListAvailable | Select-Object -First 1 | foreach {{ $_.ModuleBase }}", 38 | module_name); 39 | 40 | let encoded_command = encode_command(command.as_str()); 41 | 42 | let output = Command::new(&powershell) 43 | .arg("-EncodedCommand").arg(encoded_command.as_str()) 44 | .output().ok()?; 45 | 46 | let module_base = String::from_utf8(output.stdout).ok()?; 47 | return Some(PathBuf::from(module_base.trim())); 48 | } 49 | 50 | pub fn get_module_manifest(module_name: &str) -> Option { 51 | let powershell = find_powershell()?; 52 | let manifest_path = find_cmdlet_base(module_name)?; 53 | let manifest_path = manifest_path.as_path().to_str()?; 54 | 55 | let command = format!( 56 | "Import-PowerShellDataFile -Path \"{}\\{}.psd1\" | ConvertTo-Json", 57 | manifest_path, module_name); 58 | 59 | let encoded_command = encode_command(command.as_str()); 60 | 61 | let output = Command::new(&powershell) 62 | .arg("-EncodedCommand").arg(encoded_command.as_str()) 63 | .output().ok()?; 64 | 65 | let json_output = String::from_utf8(output.stdout).ok()?; 66 | serde_json::from_str(json_output.as_str()).ok() 67 | } 68 | -------------------------------------------------------------------------------- /cmdlet/src/service.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::path::{PathBuf}; 5 | use std::process::{Command}; 6 | use serde::{Serialize, Deserialize}; 7 | 8 | use crate::pwsh::*; 9 | 10 | pub struct CmdletService { 11 | pub service_name: String, 12 | pub display_name: String, 13 | pub description: String, 14 | pub company_name: String, 15 | pub working_dir: String, 16 | pub module_name: String, 17 | pub start_command: String, 18 | pub stop_command: String, 19 | pub log_file: String, 20 | } 21 | 22 | #[derive(Serialize, Deserialize)] 23 | pub struct ServiceManifest { 24 | #[serde(rename = "ServiceName")] 25 | pub service_name: String, 26 | #[serde(rename = "DisplayName")] 27 | pub display_name: Option, 28 | #[serde(rename = "Description")] 29 | pub description: Option, 30 | #[serde(rename = "CompanyName")] 31 | pub company_name: Option, 32 | #[serde(rename = "WorkingDir")] 33 | pub working_dir: String, 34 | #[serde(rename = "ModuleName")] 35 | pub module_name: Option, 36 | #[serde(rename = "StartCommand")] 37 | pub start_command: String, 38 | #[serde(rename = "StopCommand")] 39 | pub stop_command: String, 40 | #[serde(rename = "LogFile")] 41 | pub log_file: Option, 42 | } 43 | 44 | impl ServiceManifest { 45 | pub fn get_module_name(&self) -> &str { 46 | if let Some(module_name) = &self.module_name { 47 | return module_name.as_str(); 48 | } 49 | return self.service_name.as_str(); 50 | } 51 | } 52 | 53 | pub fn get_base_name() -> Option { 54 | let current_exe = std::env::current_exe().ok()?; 55 | let base_name = current_exe.as_path().file_stem()?.to_str()?; 56 | return Some(base_name.to_string()); 57 | } 58 | 59 | pub fn get_service_manifest() -> Option { 60 | let base_name = get_base_name()?; 61 | let mut manifest_path = std::env::current_exe().ok()?; 62 | let manifest_name = format!("{}.service.json", base_name); 63 | manifest_path.set_file_name(manifest_name.as_str()); 64 | if !manifest_path.exists() { 65 | let manifest_name = "service.json".to_string(); 66 | manifest_path.set_file_name(manifest_name.as_str()); 67 | } 68 | let file = File::open(manifest_path.as_path()).ok()?; 69 | let result = serde_json::from_reader(BufReader::new(file)); 70 | result.ok() 71 | } 72 | 73 | impl CmdletService { 74 | pub fn load() -> Option { 75 | let service_manifest = get_service_manifest()?; 76 | let module_name = service_manifest.get_module_name().to_string(); 77 | let module_manifest = get_module_manifest(&module_name)?; 78 | 79 | let service_name = service_manifest.service_name.to_string(); 80 | let display_name = service_manifest.display_name.unwrap_or(service_name.to_string()); 81 | let description = service_manifest.description.unwrap_or(module_manifest.description.to_string()); 82 | let company_name = service_manifest.company_name.unwrap_or(module_manifest.company_name.to_string()); 83 | let working_dir = service_manifest.working_dir.to_string(); 84 | let start_command = service_manifest.start_command.to_string(); 85 | let stop_command = service_manifest.stop_command.to_string(); 86 | let log_file = service_manifest.log_file.unwrap_or(format!("{}.log", service_name.as_str())); 87 | 88 | Some(CmdletService { 89 | service_name: service_name.to_string(), 90 | display_name: display_name.to_string(), 91 | description: description.to_string(), 92 | company_name: company_name.to_string(), 93 | working_dir: working_dir.to_string(), 94 | module_name: module_name.to_string(), 95 | start_command: start_command.to_string(), 96 | stop_command: stop_command.to_string(), 97 | log_file: log_file.to_string(), 98 | }) 99 | } 100 | 101 | pub fn get_working_dir(&self) -> Option { 102 | let working_dir = expand_str::expand_string_with_env(self.working_dir.as_str()).ok()?; 103 | return Some(PathBuf::from(working_dir)); 104 | } 105 | 106 | pub fn get_log_file(&self) -> PathBuf { 107 | PathBuf::from(self.log_file.as_str()) 108 | } 109 | 110 | pub fn get_service_name(&self) -> &str { 111 | self.service_name.as_str() 112 | } 113 | 114 | pub fn get_display_name(&self) -> &str { 115 | self.display_name.as_str() 116 | } 117 | 118 | pub fn get_description(&self) -> &str { 119 | self.service_name.as_str() 120 | } 121 | 122 | pub fn get_module_name(&self) -> &str { 123 | self.module_name.as_str() 124 | } 125 | 126 | pub fn get_start_command(&self) -> &str { 127 | self.start_command.as_str() 128 | } 129 | 130 | pub fn get_stop_command(&self) -> &str { 131 | self.stop_command.as_str() 132 | } 133 | 134 | pub fn start(&self) { 135 | let cmdlet_name = self.get_module_name(); 136 | let function = self.get_start_command(); 137 | let output = run_cmdlet_function(self, cmdlet_name, &function).expect("unable to run cmdlet function"); 138 | let stdout = String::from_utf8(output.stdout).expect("unable to convert output.stdout"); 139 | let stderr = String::from_utf8(output.stderr).expect("unable to convert output.stderr"); 140 | info!("{}:\n {} {}", function, stdout, stderr); 141 | } 142 | 143 | pub fn stop(&self) { 144 | let cmdlet_name = self.get_module_name(); 145 | let function = self.get_stop_command(); 146 | let output = run_cmdlet_function(self, cmdlet_name, &function).expect("unable to run cmdlet function"); 147 | let stdout = String::from_utf8(output.stdout).expect("unable to convert output.stdout"); 148 | let stderr = String::from_utf8(output.stderr).expect("unable to convert output.stderr"); 149 | info!("{}:\n {} {}", function, stdout, stderr); 150 | } 151 | } 152 | 153 | fn run_cmdlet_function(service: &CmdletService, cmdlet: &str, function: &str) -> std::io::Result { 154 | let powershell = find_powershell().expect("unable to find PowerShell"); 155 | let working_dir = service.get_working_dir().expect("unable to get working directory"); 156 | 157 | let command = format!( 158 | "Import-Module -Name {};\n\ 159 | {}", cmdlet, function); 160 | 161 | let encoded_command = encode_command(command.as_str()); 162 | 163 | Command::new(&powershell) 164 | .arg("-EncodedCommand").arg(encoded_command.as_str()) 165 | .current_dir(working_dir) 166 | .output() 167 | } 168 | -------------------------------------------------------------------------------- /examples/foobar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foobar_service" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT/Apache-2.0" 6 | homepage = "https://github.com/devolutions/ceviche-rs" 7 | repository = "https://github.com/devolutions/ceviche-rs" 8 | authors = ["Marc-André Moreau "] 9 | keywords = ["daemon", "service"] 10 | description = "Rust daemon/service wrapper" 11 | 12 | [[bin]] 13 | name = "foobar" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | libc = "0" 18 | log = "0.4" 19 | log4rs = "1.3" 20 | clap = { version = "2.31", features = ["yaml"] } 21 | ctrlc = "3.1" 22 | 23 | [dependencies.ceviche] 24 | path = "../.." 25 | -------------------------------------------------------------------------------- /examples/foobar/src/cli.yml: -------------------------------------------------------------------------------- 1 | name: ceviche 2 | author: Marc-André Moreau 3 | about: rust daemon/service wrapper 4 | args: 5 | - cmd: 6 | long: cmd 7 | value_name: cmd 8 | help: service command 9 | takes_value: true 10 | - verbose: 11 | short: v 12 | multiple: true 13 | help: Sets the level of verbosity 14 | -------------------------------------------------------------------------------- /examples/foobar/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | #[macro_use] 4 | extern crate log; 5 | 6 | use std::sync::mpsc; 7 | 8 | use ceviche::controller::*; 9 | use ceviche::{Service, ServiceEvent}; 10 | use clap::App; 11 | use log::LevelFilter; 12 | use log4rs::append::console::ConsoleAppender; 13 | use log4rs::append::file::FileAppender; 14 | use log4rs::config::{Appender, Config, Root}; 15 | use log4rs::encode::pattern::PatternEncoder; 16 | 17 | static SERVICE_NAME: &'static str = "foobar"; 18 | static DISPLAY_NAME: &'static str = "FooBar Service"; 19 | static DESCRIPTION: &'static str = "This is the FooBar service"; 20 | 21 | #[cfg(windows)] 22 | const LOG_PATH: &'static str = "C:\\Windows\\Temp\\foobar.log"; 23 | #[cfg(any(unix, target_os = "macos"))] 24 | const LOG_PATH: &'static str = "/tmp/foobar.log"; 25 | 26 | enum CustomServiceEvent {} 27 | 28 | fn init_logging(standalone_mode: bool) -> Option<()> { 29 | if standalone_mode { 30 | let stdout = ConsoleAppender::builder().build(); 31 | 32 | let config = Config::builder() 33 | .appender(Appender::builder().build("stdout", Box::new(stdout))) 34 | .build(Root::builder().appender("stdout").build(LevelFilter::Info)) 35 | .ok()?; 36 | 37 | log4rs::init_config(config).ok()?; 38 | } else { 39 | let file_appender = FileAppender::builder() 40 | .encoder(Box::new(PatternEncoder::new( 41 | "{d(%Y-%m-%d %H:%M:%S)} {M} [{h({l})}] - {m}{n}", 42 | ))) 43 | .build(LOG_PATH) 44 | .ok()?; 45 | 46 | let config = Config::builder() 47 | .appender(Appender::builder().build("file_appender", Box::new(file_appender))) 48 | .build( 49 | Root::builder() 50 | .appender("file_appender") 51 | .build(LevelFilter::Info), 52 | ) 53 | .ok()?; 54 | 55 | log4rs::init_config(config).ok()?; 56 | } 57 | Some(()) 58 | } 59 | 60 | fn my_service_main( 61 | rx: mpsc::Receiver>, 62 | _tx: mpsc::Sender>, 63 | args: Vec, 64 | standalone_mode: bool, 65 | ) -> u32 { 66 | init_logging(standalone_mode); 67 | info!("foobar service started"); 68 | info!("args: {:?}", args); 69 | 70 | loop { 71 | if let Ok(control_code) = rx.recv() { 72 | info!("Received control code: {}", control_code); 73 | match control_code { 74 | ServiceEvent::Stop => break, 75 | _ => (), 76 | } 77 | } 78 | } 79 | 80 | info!("foobar service stopping"); 81 | 0 82 | } 83 | 84 | Service!("Foobar", my_service_main); 85 | 86 | fn main() { 87 | let yaml = load_yaml!("cli.yml"); 88 | let app = App::from_yaml(yaml); 89 | let matches = app.version(crate_version!()).get_matches(); 90 | let cmd = matches.value_of("cmd").unwrap_or("").to_string(); 91 | 92 | let mut controller = Controller::new(SERVICE_NAME, DISPLAY_NAME, DESCRIPTION); 93 | 94 | match cmd.as_str() { 95 | "create" => { 96 | if let Err(e) = controller.create() { 97 | println!("{}", e); 98 | } 99 | } 100 | "delete" => { 101 | if let Err(e) = controller.delete() { 102 | println!("{}", e); 103 | } 104 | } 105 | "start" => { 106 | if let Err(e) = controller.start() { 107 | println!("{}", e); 108 | } 109 | } 110 | "stop" => { 111 | if let Err(e) = controller.stop() { 112 | println!("{}", e); 113 | } 114 | } 115 | "standalone" => { 116 | let (tx, rx) = mpsc::channel(); 117 | let _tx = tx.clone(); 118 | 119 | ctrlc::set_handler(move || { 120 | let _ = tx.send(ServiceEvent::Stop); 121 | }).expect("Failed to register Ctrl-C handler"); 122 | 123 | my_service_main(rx, _tx, vec![], true); 124 | } 125 | _ => { 126 | let _result = controller.register(service_main_wrapper); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/controller.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | use crate::Error; 4 | use crate::ServiceEvent; 5 | 6 | cfg_if! { 7 | if #[cfg(windows)] { 8 | mod windows; 9 | pub use self::windows::WindowsController as Controller; 10 | pub use self::windows::Session as Session; 11 | pub use self::windows::dispatch; 12 | } else if #[cfg(target_os = "macos")] { 13 | mod macos; 14 | pub use self::macos::MacosController as Controller; 15 | pub use self::macos::Session as Session; 16 | pub use self::macos::dispatch; 17 | pub use self::macos::LaunchAgentTargetSesssion; 18 | } else if #[cfg(target_os = "linux")] { 19 | mod linux; 20 | pub use self::linux::LinuxController as Controller; 21 | pub use self::linux::Session as Session; 22 | pub use self::linux::dispatch; 23 | } else { 24 | mod dummy; 25 | pub use self::dummy::DummyController as Controller; 26 | pub use self::dummy::Session as Session; 27 | } 28 | } 29 | 30 | /// Signature of the service main function. 31 | /// `rx` receives the events that are sent to the service. `tx` can be used to send custom events on the channel. 32 | /// `args` is the list or arguments that were passed to the service. When `standalone_mode` is true, the service 33 | /// main function is being called directly (outside of the system service support). 34 | pub type ServiceMainFn = fn( 35 | rx: mpsc::Receiver>, 36 | tx: mpsc::Sender>, 37 | args: Vec, 38 | standalone_mode: bool, 39 | ) -> u32; 40 | 41 | /// Controllers implement this interface. They also need to implement the `register()` method; because the signature 42 | /// of service_main_wrapper depends on the system the method is not part of the interface. 43 | pub trait ControllerInterface { 44 | /// Creates the service on the system. 45 | fn create(&mut self) -> Result<(), Error>; 46 | /// Deletes the service. 47 | fn delete(&mut self) -> Result<(), Error>; 48 | /// Starts the service. 49 | fn start(&mut self) -> Result<(), Error>; 50 | /// Stops the service. 51 | fn stop(&mut self) -> Result<(), Error>; 52 | cfg_if! { 53 | if #[cfg(target_os = "macos")] { 54 | /// Loads the agent service. 55 | fn load(&mut self) -> Result<(), Error>; 56 | /// Unloads the agent service. 57 | fn unload(&mut self) -> Result<(), Error>; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/controller/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::ControllerInterface; 2 | use crate::session; 3 | /// The dummy controller is a mock controller, the only operation that as an 4 | /// effect is calling it in standalone mode. 5 | use crate::Error; 6 | 7 | pub type Session = session::Session_; 8 | 9 | pub struct DummyController {} 10 | 11 | impl ControllerInterface for DummyController { 12 | fn create(&mut self) -> Result<(), Error> { 13 | println!("Dummy controller: creating service (this has no effect on the system)"); 14 | Ok(()) 15 | } 16 | 17 | fn delete(&mut self) -> Result<(), Error> { 18 | println!("Dummy controller: deleting service (this has no effect on the system)"); 19 | Ok(()) 20 | } 21 | 22 | fn start(&mut self) -> Result<(), Error> { 23 | println!("Dummy controller: starting service (this has no effect on the system)"); 24 | Ok(()) 25 | } 26 | 27 | fn stop(&mut self) -> Result<(), Error> { 28 | println!("Dummy controller: stopping service (this has no effect on the system)"); 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl DummyController { 34 | pub fn new(_service_name: &str, _display_name: &str, _description: &str) -> DummyController { 35 | DummyController {} 36 | } 37 | 38 | pub fn register(&mut self, _service_main_wrapper: fn()) -> Result<(), Error> { 39 | unimplemented!(); 40 | } 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! Service { 45 | ($name:expr, $function:ident) => { 46 | fn service_main_wrapper() { 47 | () 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/controller/linux.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::{self, File}; 3 | use std::io::Write; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | use std::sync::mpsc; 7 | 8 | use ctrlc; 9 | use log::{debug, info}; 10 | 11 | use crate::controller::{ControllerInterface, ServiceMainFn}; 12 | use crate::session; 13 | use crate::Error; 14 | use crate::ServiceEvent; 15 | 16 | #[cfg(feature = "systemd-rs")] 17 | use { 18 | systemd_rs::login::monitor::{Category, Monitor}, 19 | systemd_rs::login::session as login_session, 20 | }; 21 | 22 | type LinuxServiceMainWrapperFn = extern "system" fn(args: Vec); 23 | pub type Session = session::Session_; 24 | 25 | fn systemctl_execute(args: &[&str]) -> Result<(), Error> { 26 | let mut process = Command::new("systemctl"); 27 | process.args(args); 28 | 29 | let output = process 30 | .output() 31 | .map_err(|e| Error::new(&format!("Failed to execute command {}: {}", args[0], e)))?; 32 | 33 | if !output.status.success() { 34 | return Err(Error::new(&format!( 35 | "Command \"{}\" failed ({}): {}", 36 | args[0], 37 | output.status.code().expect("Process terminated by signal"), 38 | std::str::from_utf8(&output.stderr).unwrap_or_default() 39 | ))); 40 | } 41 | 42 | if !output.stdout.is_empty() { 43 | info!("{}", String::from_utf8_lossy(&output.stdout)); 44 | } 45 | 46 | Ok(()) 47 | } 48 | 49 | fn systemd_install_daemon(name: &str) -> Result<(), Error> { 50 | systemctl_execute(&["daemon-reload"])?; 51 | systemctl_execute(&["enable", name]) 52 | } 53 | 54 | fn systemd_uninstall_daemon(name: &str) -> Result<(), Error> { 55 | systemctl_execute(&["disable", name])?; 56 | systemctl_execute(&["daemon-reload"]) 57 | .map_err(|e| debug!("{}", e)) 58 | .ok(); 59 | systemctl_execute(&["reset-failed"]) 60 | .map_err(|e| debug!("{}", e)) 61 | .ok(); 62 | 63 | Ok(()) 64 | } 65 | 66 | fn systemd_start_daemon(name: &str) -> Result<(), Error> { 67 | systemctl_execute(&["start", name]) 68 | } 69 | 70 | fn systemd_stop_daemon(name: &str) -> Result<(), Error> { 71 | systemctl_execute(&["stop", name]) 72 | } 73 | 74 | pub struct LinuxController { 75 | pub service_name: String, 76 | pub display_name: String, 77 | pub description: String, 78 | pub config: Option, 79 | } 80 | 81 | impl LinuxController { 82 | pub fn new(service_name: &str, display_name: &str, description: &str) -> LinuxController { 83 | LinuxController { 84 | service_name: service_name.to_string(), 85 | display_name: display_name.to_string(), 86 | description: description.to_string(), 87 | config: None, 88 | } 89 | } 90 | 91 | pub fn register( 92 | &mut self, 93 | service_main_wrapper: LinuxServiceMainWrapperFn, 94 | ) -> Result<(), Error> { 95 | service_main_wrapper(env::args().collect()); 96 | Ok(()) 97 | } 98 | 99 | fn get_service_file_name(&self) -> String { 100 | format!("{}.service", &self.service_name) 101 | } 102 | 103 | fn get_service_unit_path(&self) -> PathBuf { 104 | Path::new("/lib/systemd/system/").join(self.get_service_file_name()) 105 | } 106 | 107 | fn get_service_dropin_dir(&self) -> PathBuf { 108 | Path::new("/lib/systemd/system/").join(format!("{}.d", self.get_service_file_name())) 109 | } 110 | 111 | fn get_service_unit_content(&self) -> Result { 112 | Ok(format!( 113 | r#" 114 | [Unit] 115 | Description={} 116 | 117 | [Service] 118 | ExecStart={} 119 | 120 | [Install] 121 | WantedBy=multi-user.target"#, 122 | self.service_name, 123 | fs::read_link("/proc/self/exe") 124 | .map_err(|e| Error::new(&format!("Failed to read /proc/self/exe: {}", e)))? 125 | .to_str() 126 | .ok_or("Failed to parse /proc/self/exe")? 127 | )) 128 | } 129 | 130 | fn write_service_config(&self) -> Result<(), Error> { 131 | let path = self.get_service_unit_path(); 132 | let content = self.get_service_unit_content()?; 133 | info!("Writing service file {}", path.display()); 134 | File::create(&path) 135 | .and_then(|mut file| file.write_all(content.as_bytes())) 136 | .map_err(|e| Error::new(&format!("Failed to write {}: {}", path.display(), e)))?; 137 | 138 | if let Some(ref config) = self.config { 139 | let dropin_dir = self.get_service_dropin_dir(); 140 | let path = dropin_dir.join(format!("{}.conf", self.service_name)); 141 | 142 | if !Path::exists(&dropin_dir) { 143 | fs::create_dir(dropin_dir).map_err(|e| { 144 | Error::new(&format!("Failed to create {}: {}", path.display(), e)) 145 | })?; 146 | } 147 | info!("Writing config file {}", path.display()); 148 | File::create(&path) 149 | .and_then(|mut file| file.write_all(config.as_bytes())) 150 | .map_err(|e| Error::new(&format!("Failed to write {}: {}", path.display(), e)))?; 151 | } 152 | 153 | Ok(()) 154 | } 155 | } 156 | 157 | impl ControllerInterface for LinuxController { 158 | fn create(&mut self) -> Result<(), Error> { 159 | self.write_service_config()?; 160 | 161 | systemd_install_daemon(&self.service_name) 162 | } 163 | 164 | fn delete(&mut self) -> Result<(), Error> { 165 | systemd_uninstall_daemon(&self.service_name)?; 166 | 167 | let path = self.get_service_unit_path(); 168 | fs::remove_file(&path) 169 | .map_err(|e| debug!("Failed to delete {}: {}", path.display(), e)) 170 | .ok(); 171 | 172 | let path = self.get_service_dropin_dir(); 173 | fs::remove_dir_all(self.get_service_dropin_dir()) 174 | .map_err(|e| debug!("Failed to delete {}: {}", path.display(), e)) 175 | .ok(); 176 | 177 | Ok(()) 178 | } 179 | 180 | fn start(&mut self) -> Result<(), Error> { 181 | systemd_start_daemon(&self.service_name) 182 | } 183 | 184 | fn stop(&mut self) -> Result<(), Error> { 185 | systemd_stop_daemon(&self.service_name) 186 | } 187 | } 188 | 189 | #[cfg(feature = "systemd-rs")] 190 | fn run_monitor( 191 | tx: mpsc::Sender>, 192 | ) -> Result { 193 | let monitor = Monitor::new()?; 194 | 195 | let mut current_session = match login_session::get_active_session() { 196 | Ok(s) => Some(s), 197 | Err(e) => { 198 | debug!("Failed to get active session {}", e); 199 | None 200 | } 201 | }; 202 | 203 | monitor.init(Category::Sessions, move || { 204 | let active_session = match login_session::get_active_session() { 205 | Ok(s) => Some(s), 206 | Err(e) => { 207 | debug!("Failed to get active session {}", e); 208 | None 209 | } 210 | }; 211 | 212 | let session_changed = match (¤t_session, &active_session) { 213 | (Some(current_session), Some(active_session)) => current_session != active_session, 214 | (None, None) => false, 215 | _ => true, 216 | }; 217 | 218 | if session_changed { 219 | if let Some(active_session) = active_session.as_ref() { 220 | let _ = tx.send(ServiceEvent::SessionConnect(Session::new( 221 | active_session.identifier.to_string(), 222 | ))); 223 | } 224 | 225 | if let Some(current_session) = current_session.as_ref() { 226 | let _ = tx.send(ServiceEvent::SessionDisconnect(Session::new( 227 | current_session.identifier.to_string(), 228 | ))); 229 | } 230 | } 231 | 232 | current_session = active_session; 233 | })?; 234 | 235 | Ok(monitor) 236 | } 237 | 238 | #[macro_export] 239 | macro_rules! Service { 240 | ($name:expr, $function:ident) => { 241 | extern "system" fn service_main_wrapper(args: Vec) { 242 | dispatch($function, args); 243 | } 244 | }; 245 | } 246 | 247 | #[doc(hidden)] 248 | pub fn dispatch(service_main: ServiceMainFn, args: Vec) { 249 | let (tx, rx) = mpsc::channel(); 250 | 251 | #[cfg(feature = "systemd-rs")] 252 | { 253 | let _monitor = run_monitor(tx.clone()).expect("Failed to run session monitor"); 254 | } 255 | 256 | let _tx = tx.clone(); 257 | 258 | ctrlc::set_handler(move || { 259 | let _ = tx.send(ServiceEvent::Stop); 260 | }) 261 | .expect("Failed to register Ctrl-C handler"); 262 | service_main(rx, _tx, args, false); 263 | } 264 | -------------------------------------------------------------------------------- /src/controller/macos.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | ffi::c_void, 4 | fmt, 5 | fs::{self, File}, 6 | io::Write, 7 | path::{Path, PathBuf}, 8 | process::Command, 9 | ptr, 10 | sync::{ 11 | atomic::{AtomicBool, Ordering}, 12 | mpsc, Arc, Mutex, 13 | }, 14 | thread, 15 | }; 16 | 17 | use chrono; 18 | use ctrlc; 19 | use log::info; 20 | use timer; 21 | 22 | use core_foundation::{ 23 | array::{kCFTypeArrayCallBacks, CFArray, CFArrayCreate, CFArrayRef}, 24 | base::{CFAllocatorRef, CFRelease, CFType, CFTypeRef, TCFType, ToVoid}, 25 | runloop::{ 26 | kCFRunLoopDefaultMode, CFRunLoopAddSource, CFRunLoopGetCurrent, CFRunLoopRef, CFRunLoopRun, 27 | CFRunLoopStop, 28 | }, 29 | string::{CFString, CFStringRef}, 30 | }; 31 | 32 | use system_configuration_sys::{ 33 | dynamic_store::{ 34 | SCDynamicStoreContext, SCDynamicStoreCreate, SCDynamicStoreCreateRunLoopSource, 35 | SCDynamicStoreRef, SCDynamicStoreSetNotificationKeys, 36 | }, 37 | dynamic_store_copy_specific::{uid_t, SCDynamicStoreCopyConsoleUser}, 38 | }; 39 | 40 | use crate::controller::{ControllerInterface, ServiceMainFn}; 41 | use crate::session; 42 | use crate::Error; 43 | use crate::ServiceEvent; 44 | 45 | type MacosServiceMainWrapperFn = extern "system" fn(args: Vec); 46 | pub type Session = session::Session_; 47 | 48 | pub enum LaunchAgentTargetSesssion { 49 | GUI, 50 | NonGUI, 51 | PerUser, 52 | PreLogin, 53 | } 54 | 55 | impl fmt::Display for LaunchAgentTargetSesssion { 56 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 57 | match self { 58 | LaunchAgentTargetSesssion::GUI => write!(f, "Aqua"), 59 | LaunchAgentTargetSesssion::NonGUI => write!(f, "StandardIO"), 60 | LaunchAgentTargetSesssion::PerUser => write!(f, "Background"), 61 | LaunchAgentTargetSesssion::PreLogin => write!(f, "LoginWindow"), 62 | } 63 | } 64 | } 65 | 66 | fn launchctl_load_daemon(plist_path: &Path) -> Result<(), Error> { 67 | let output = Command::new("launchctl") 68 | .arg("load") 69 | .arg(&plist_path.to_str().unwrap()) 70 | .output() 71 | .map_err(|e| { 72 | Error::new(&format!( 73 | "Failed to load plist {}: {}", 74 | plist_path.display(), 75 | e 76 | )) 77 | })?; 78 | if output.stdout.len() > 0 { 79 | info!("{}", String::from_utf8_lossy(&output.stdout)); 80 | } 81 | Ok(()) 82 | } 83 | 84 | fn launchctl_unload_daemon(plist_path: &Path) -> Result<(), Error> { 85 | let output = Command::new("launchctl") 86 | .arg("unload") 87 | .arg(&plist_path.to_str().unwrap()) 88 | .output() 89 | .map_err(|e| { 90 | Error::new(&format!( 91 | "Failed to unload plist {}: {}", 92 | plist_path.display(), 93 | e 94 | )) 95 | })?; 96 | if output.stdout.len() > 0 { 97 | info!("{}", String::from_utf8_lossy(&output.stdout)); 98 | } 99 | Ok(()) 100 | } 101 | 102 | fn launchctl_start_daemon(name: &str) -> Result<(), Error> { 103 | let output = Command::new("launchctl") 104 | .arg("start") 105 | .arg(name) 106 | .output() 107 | .map_err(|e| Error::new(&format!("Failed to start {}: {}", name, e)))?; 108 | if output.stdout.len() > 0 { 109 | info!("{}", String::from_utf8_lossy(&output.stdout)); 110 | } 111 | Ok(()) 112 | } 113 | 114 | fn launchctl_stop_daemon(name: &str) -> Result<(), Error> { 115 | let output = Command::new("launchctl") 116 | .arg("stop") 117 | .arg(name) 118 | .output() 119 | .map_err(|e| Error::new(&format!("Failed to stop {}: {}", name, e)))?; 120 | if output.stdout.len() > 0 { 121 | info!("{}", String::from_utf8_lossy(&output.stdout)); 122 | } 123 | Ok(()) 124 | } 125 | 126 | pub struct MacosController { 127 | /// Manages the service on the system. 128 | pub service_name: String, 129 | pub display_name: String, 130 | pub description: String, 131 | pub is_agent: bool, 132 | pub session_types: Option>, 133 | pub keep_alive: bool, 134 | } 135 | 136 | impl MacosController { 137 | pub fn new(service_name: &str, display_name: &str, description: &str) -> MacosController { 138 | MacosController { 139 | service_name: service_name.to_string(), 140 | display_name: display_name.to_string(), 141 | description: description.to_string(), 142 | is_agent: false, 143 | session_types: None, 144 | keep_alive: true, 145 | } 146 | } 147 | 148 | /// Register the `service_main_wrapper` function, this function is generated by the `Service!` macro. 149 | pub fn register( 150 | &mut self, 151 | service_main_wrapper: MacosServiceMainWrapperFn, 152 | ) -> Result<(), Error> { 153 | service_main_wrapper(env::args().collect()); 154 | Ok(()) 155 | } 156 | 157 | fn get_plist_content(&self) -> Result { 158 | let mut current_exe = env::current_exe() 159 | .map_err(|e| Error::new(&format!("env::current_exe() failed: {}", e)))?; 160 | let current_exe_str = current_exe 161 | .to_str() 162 | .expect("current_exe path to be unicode") 163 | .to_string(); 164 | 165 | current_exe.pop(); 166 | let working_dir_str = current_exe 167 | .to_str() 168 | .expect("working_dir path to be unicode"); 169 | 170 | let mut plist = String::new(); 171 | plist.push_str(r#" 172 | 173 | 174 | 175 | "#); 176 | 177 | plist.push_str(&format!( 178 | r#" 179 | Disabled 180 | 181 | Label 182 | {} 183 | ProgramArguments 184 | 185 | {} 186 | 187 | WorkingDirectory 188 | {} 189 | RunAtLoad 190 | "#, 191 | self.service_name, current_exe_str, working_dir_str, 192 | )); 193 | 194 | if self.is_agent { 195 | if let Some(session_types) = self.session_types.as_ref() { 196 | plist.push_str( 197 | r#" 198 | LimitLoadToSessionType 199 | "#, 200 | ); 201 | 202 | for session_type in session_types { 203 | plist.push_str(&format!( 204 | r#" 205 | {}"#, 206 | session_type 207 | )); 208 | } 209 | 210 | plist.push_str( 211 | r#" 212 | "#, 213 | ); 214 | } 215 | } 216 | 217 | if self.keep_alive { 218 | plist.push_str( 219 | r#" 220 | KeepAlive 221 | "#, 222 | ); 223 | } 224 | 225 | plist.push_str( 226 | r#" 227 | 228 | "#, 229 | ); 230 | 231 | Ok(plist) 232 | } 233 | 234 | fn write_plist(&self, path: &Path) -> Result<(), Error> { 235 | info!("Writing plist file {}", path.display()); 236 | let content = self.get_plist_content()?; 237 | File::create(path) 238 | .and_then(|mut file| file.write_all(content.as_bytes())) 239 | .map_err(|e| Error::new(&format!("Failed to write {}: {}", path.display(), e))) 240 | } 241 | 242 | fn plist_path(&mut self) -> PathBuf { 243 | Path::new("/Library/") 244 | .join(if self.is_agent { 245 | "LaunchAgents/" 246 | } else { 247 | "LaunchDaemons/" 248 | }) 249 | .join(format!("{}.plist", &self.service_name)) 250 | } 251 | } 252 | 253 | impl ControllerInterface for MacosController { 254 | /// Creates the service on the system. 255 | fn create(&mut self) -> Result<(), Error> { 256 | let plist_path = self.plist_path(); 257 | 258 | self.write_plist(&plist_path)?; 259 | if !self.is_agent { 260 | return launchctl_load_daemon(&plist_path); 261 | } 262 | Ok(()) 263 | } 264 | /// Deletes the service. 265 | fn delete(&mut self) -> Result<(), Error> { 266 | let plist_path = self.plist_path(); 267 | if !self.is_agent { 268 | launchctl_unload_daemon(&plist_path)?; 269 | } 270 | fs::remove_file(&plist_path) 271 | .map_err(|e| Error::new(&format!("Failed to delete {}: {}", plist_path.display(), e))) 272 | } 273 | /// Starts the service. 274 | fn start(&mut self) -> Result<(), Error> { 275 | launchctl_start_daemon(&self.service_name) 276 | } 277 | /// Stops the service. 278 | fn stop(&mut self) -> Result<(), Error> { 279 | launchctl_stop_daemon(&self.service_name) 280 | } 281 | // Loads the agent service. 282 | fn load(&mut self) -> Result<(), Error> { 283 | launchctl_load_daemon(&self.plist_path()) 284 | } 285 | // Loads the agent service. 286 | fn unload(&mut self) -> Result<(), Error> { 287 | launchctl_unload_daemon(&self.plist_path()) 288 | } 289 | } 290 | 291 | /// Generates a `service_main_wrapper` that wraps the provided service main function. 292 | #[macro_export] 293 | macro_rules! Service { 294 | ($name:expr, $function:ident) => { 295 | extern "system" fn service_main_wrapper(args: Vec) { 296 | dispatch($function, args); 297 | } 298 | }; 299 | } 300 | 301 | fn active_session_uid(store_ref: Option) -> u32 { 302 | let mut uid: uid_t = 0; 303 | let store = store_ref.unwrap_or(ptr::null()); 304 | 305 | let user = unsafe { SCDynamicStoreCopyConsoleUser(store, &mut uid, ptr::null_mut()) }; 306 | if user.is_null() { 307 | return 0; 308 | } 309 | let _cf_user: CFString = unsafe { TCFType::wrap_under_create_rule(user) }; 310 | return uid; 311 | } 312 | 313 | unsafe extern "C" fn on_sc_console_user_change( 314 | store: SCDynamicStoreRef, 315 | _keys: CFArrayRef, 316 | info: *mut c_void, 317 | ) where 318 | F: FnMut(u32, EventType) + Send + 'static, 319 | { 320 | let uid = active_session_uid(Some(store)); 321 | let ctx_box = Box::from_raw(info as *mut Arc>); 322 | let ctx_ptr = Box::leak(ctx_box); 323 | let mut ctx = ctx_ptr.lock().unwrap(); 324 | 325 | let old_uid = ctx.uid; 326 | if uid != ctx.uid { 327 | ctx.uid = uid; 328 | if old_uid != 0 || ctx.last_was_logout.load(Ordering::SeqCst) { 329 | ctx.last_was_logout.store(false, Ordering::SeqCst); 330 | (ctx.callback)(old_uid, EventType::Disconnect); 331 | } 332 | 333 | if uid != 0 { 334 | ctx.pending_connect.store(false, Ordering::SeqCst); 335 | (ctx.callback)(uid, EventType::Connect); 336 | } else { 337 | ctx.pending_connect.store(true, Ordering::SeqCst); 338 | let ctx_weak = Arc::downgrade(ctx_ptr); 339 | thread::spawn(move || { 340 | let timer = timer::Timer::new(); 341 | let (tx, rx) = mpsc::channel(); 342 | 343 | let _guard = timer.schedule_with_delay(chrono::Duration::seconds(3), move || { 344 | let _ignored = tx.send(()); 345 | }); 346 | rx.recv().unwrap(); 347 | match ctx_weak.upgrade() { 348 | Some(ctx_ptr) => { 349 | let mut ctx = ctx_ptr.lock().unwrap(); 350 | if ctx.pending_connect.load(Ordering::SeqCst) { 351 | ctx.last_was_logout.store(true, Ordering::SeqCst); 352 | (ctx.callback)(uid, EventType::Connect); 353 | } 354 | } 355 | None => return, 356 | }; 357 | }); 358 | } 359 | } 360 | } 361 | 362 | #[link(name = "SystemConfiguration", kind = "framework")] 363 | extern "C" { 364 | pub fn SCDynamicStoreKeyCreateConsoleUser(allocator: CFAllocatorRef) -> CFStringRef; 365 | } 366 | 367 | pub enum EventType { 368 | Connect, 369 | Disconnect, 370 | } 371 | 372 | pub struct SessionContext { 373 | uid: u32, 374 | callback: F, 375 | pending_connect: AtomicBool, 376 | last_was_logout: AtomicBool, 377 | } 378 | 379 | impl SessionContext { 380 | pub fn new(cb: F) -> Self 381 | where 382 | F: FnMut(u32, EventType) + Send + 'static, 383 | { 384 | Self { 385 | uid: active_session_uid(None), 386 | callback: cb, 387 | pending_connect: AtomicBool::new(false), 388 | last_was_logout: AtomicBool::new(true), 389 | } 390 | } 391 | } 392 | 393 | pub type SyncSessionContext = Mutex>; 394 | 395 | pub struct Monitor { 396 | context: *mut Arc>, 397 | } 398 | 399 | impl Drop for Monitor { 400 | fn drop(&mut self) { 401 | let _ = unsafe { Box::from_raw(self.context as *mut Arc>) }; 402 | } 403 | } 404 | 405 | impl Monitor { 406 | pub fn new(cb: F) -> Result 407 | where 408 | F: FnMut(u32, EventType) + Send + 'static, 409 | { 410 | let session_ctx = Box::new(Arc::new(Mutex::new(SessionContext::new(cb)))); 411 | let session_ctx_ptr = Box::into_raw(session_ctx); 412 | 413 | let mut ctx = SCDynamicStoreContext { 414 | version: 0, 415 | info: session_ctx_ptr as *mut c_void, 416 | retain: None, 417 | release: None, 418 | copyDescription: None, 419 | }; 420 | 421 | unsafe { 422 | let name = CFString::from_static_string("kCGSSessionUserNameKey"); 423 | let store_ref = SCDynamicStoreCreate( 424 | ptr::null_mut(), 425 | name.to_void() as CFStringRef, 426 | Some(on_sc_console_user_change::), 427 | &mut ctx, 428 | ); 429 | let key = SCDynamicStoreKeyCreateConsoleUser(ptr::null()); 430 | let keys = CFArrayCreate(ptr::null(), &key.to_void(), 1, &kCFTypeArrayCallBacks); 431 | let _ = SCDynamicStoreSetNotificationKeys(store_ref, keys, ptr::null_mut()); 432 | // releases array 433 | let _: CFArray = TCFType::wrap_under_create_rule(keys); 434 | 435 | let rls = SCDynamicStoreCreateRunLoopSource(ptr::null_mut(), store_ref, 0); 436 | CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode); 437 | 438 | CFRelease(rls as CFTypeRef); 439 | CFRelease(store_ref as CFTypeRef); 440 | 441 | Ok(Monitor { 442 | context: session_ctx_ptr, 443 | }) 444 | } 445 | } 446 | } 447 | 448 | pub struct MonitorLoopRef { 449 | loop_ref: CFRunLoopRef, 450 | } 451 | unsafe impl Send for MonitorLoopRef {} 452 | 453 | impl MonitorLoopRef { 454 | pub fn stop(&mut self) { 455 | unsafe { CFRunLoopStop(self.loop_ref) }; 456 | } 457 | } 458 | 459 | pub fn run_monitor( 460 | tx: mpsc::Sender>, 461 | ) -> Result { 462 | let (_tx, rx) = mpsc::channel(); 463 | thread::spawn(move || { 464 | let mon = Monitor::new(move |uid: u32, event: EventType| { 465 | match event { 466 | EventType::Connect => { 467 | let _ = tx.send(ServiceEvent::SessionConnect(Session::new(uid))); 468 | } 469 | EventType::Disconnect => { 470 | let _ = tx.send(ServiceEvent::SessionDisconnect(Session::new(uid))); 471 | } 472 | }; 473 | }); 474 | 475 | let loop_ref = unsafe { CFRunLoopGetCurrent() }; 476 | let mon_loop_ref = MonitorLoopRef { loop_ref: loop_ref }; 477 | _tx.send(mon_loop_ref).unwrap(); 478 | unsafe { CFRunLoopRun() }; 479 | drop(mon); 480 | }); 481 | 482 | let received = rx.recv().unwrap(); 483 | 484 | return Ok(received); 485 | } 486 | 487 | #[doc(hidden)] 488 | pub fn dispatch(service_main: ServiceMainFn, args: Vec) { 489 | let (tx, rx) = mpsc::channel(); 490 | 491 | let mut session_monitor = run_monitor(tx.clone()).expect("Failed to run session monitor"); 492 | let _tx = tx.clone(); 493 | 494 | ctrlc::set_handler(move || { 495 | let _ = tx.send(ServiceEvent::Stop); 496 | }) 497 | .expect("Failed to register Ctrl-C handler"); 498 | service_main(rx, _tx, args, false); 499 | 500 | session_monitor.stop(); 501 | } 502 | -------------------------------------------------------------------------------- /src/controller/windows.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_void, OsStr}; 2 | use std::iter::once; 3 | use std::os::windows::ffi::OsStrExt; 4 | use std::ptr; 5 | use std::sync::mpsc; 6 | use std::{thread, time}; 7 | 8 | use widestring::WideCString; 9 | use windows_sys::core::PWSTR; 10 | use windows_sys::Win32::{ 11 | Foundation::{GetLastError, ERROR_CALL_NOT_IMPLEMENTED, MAX_PATH}, 12 | Security::SC_HANDLE, 13 | System::{ 14 | Diagnostics::Debug::{FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM}, 15 | LibraryLoader::GetModuleFileNameW, 16 | RemoteDesktop::WTSSESSION_NOTIFICATION, 17 | Services::*, 18 | }, 19 | UI::WindowsAndMessaging::{ 20 | WTS_CONSOLE_CONNECT, WTS_CONSOLE_DISCONNECT, WTS_REMOTE_CONNECT, WTS_REMOTE_DISCONNECT, 21 | WTS_SESSION_LOCK, WTS_SESSION_LOGOFF, WTS_SESSION_LOGON, WTS_SESSION_UNLOCK, 22 | }, 23 | }; 24 | 25 | use crate::controller::{ControllerInterface, ServiceMainFn}; 26 | use crate::session; 27 | use crate::Error; 28 | use crate::ServiceEvent; 29 | 30 | type DWORD = u32; 31 | type LPVOID = *mut c_void; 32 | 33 | static mut SERVICE_CONTROL_HANDLE: SERVICE_STATUS_HANDLE = 0; 34 | 35 | type WindowsServiceMainWrapperFn = extern "system" fn(argc: DWORD, argv: *mut PWSTR); 36 | pub type Session = session::Session_; 37 | 38 | struct Service { 39 | pub handle: SC_HANDLE, 40 | } 41 | 42 | impl Drop for Service { 43 | fn drop(&mut self) { 44 | if !(self.handle == 0) { 45 | unsafe { CloseServiceHandle(self.handle) }; 46 | } 47 | } 48 | } 49 | 50 | struct ServiceControlManager { 51 | pub handle: SC_HANDLE, 52 | } 53 | 54 | impl ServiceControlManager { 55 | fn open(desired_access: DWORD) -> Result { 56 | let handle = unsafe { OpenSCManagerW(ptr::null_mut(), ptr::null_mut(), desired_access) }; 57 | 58 | if handle == 0 { 59 | Err(Error::new(&format!( 60 | "OpenSCManager: {}", 61 | get_last_error_text() 62 | ))) 63 | } else { 64 | Ok(ServiceControlManager { handle }) 65 | } 66 | } 67 | 68 | fn open_service(&self, service_name: &str, desired_access: DWORD) -> Result { 69 | let handle = unsafe { 70 | OpenServiceW( 71 | self.handle, 72 | get_utf16(service_name).as_ptr(), 73 | desired_access, 74 | ) 75 | }; 76 | 77 | if handle == 0 { 78 | Err(Error::new(&format!( 79 | "OpenServiceW: {}", 80 | get_last_error_text() 81 | ))) 82 | } else { 83 | Ok(Service { handle }) 84 | } 85 | } 86 | } 87 | 88 | impl Drop for ServiceControlManager { 89 | fn drop(&mut self) { 90 | if !(self.handle == 0) { 91 | unsafe { CloseServiceHandle(self.handle) }; 92 | } 93 | } 94 | } 95 | 96 | /// Manages the service on the system. 97 | pub struct WindowsController { 98 | pub service_name: String, 99 | pub display_name: String, 100 | pub description: String, 101 | pub desired_access: DWORD, 102 | pub service_type: DWORD, 103 | pub start_type: DWORD, 104 | pub error_control: DWORD, 105 | pub tag_id: DWORD, 106 | pub load_order_group: String, 107 | pub dependencies: String, 108 | pub account_name: String, 109 | pub password: String, 110 | pub service_status: SERVICE_STATUS, 111 | pub status_handle: SERVICE_STATUS_HANDLE, 112 | pub controls_accepted: DWORD, 113 | } 114 | 115 | impl ControllerInterface for WindowsController { 116 | fn create(&mut self) -> Result<(), Error> { 117 | unsafe { 118 | let service_manager = ServiceControlManager::open(SC_MANAGER_ALL_ACCESS)?; 119 | 120 | let filename = get_filename(); 121 | let tag_id = 0; 122 | 123 | let service = CreateServiceW( 124 | service_manager.handle, 125 | get_utf16(self.service_name.as_str()).as_ptr(), 126 | get_utf16(self.display_name.as_str()).as_ptr(), 127 | self.desired_access, 128 | self.service_type, 129 | self.start_type, 130 | self.error_control, 131 | get_utf16(filename.as_str()).as_ptr(), 132 | ptr::null_mut(), 133 | ptr::null_mut(), 134 | ptr::null_mut(), 135 | ptr::null_mut(), 136 | ptr::null_mut(), 137 | ); 138 | 139 | if service == 0 { 140 | return Err(Error::new(&format!( 141 | "CreateService: {}", 142 | get_last_error_text() 143 | ))); 144 | } 145 | 146 | self.tag_id = tag_id; 147 | 148 | let mut description = get_utf16(self.description.as_str()); 149 | 150 | let mut sd = SERVICE_DESCRIPTIONW { 151 | lpDescription: description.as_mut_ptr(), 152 | }; 153 | 154 | let p_sd = &mut sd as *mut _ as *mut c_void; 155 | ChangeServiceConfig2W(service, SERVICE_CONFIG_DESCRIPTION, p_sd); 156 | CloseServiceHandle(service); 157 | 158 | Ok(()) 159 | } 160 | } 161 | 162 | fn delete(&mut self) -> Result<(), Error> { 163 | unsafe { 164 | let service_manager = ServiceControlManager::open(SC_MANAGER_ALL_ACCESS)?; 165 | let service = service_manager.open_service(&self.service_name, SERVICE_ALL_ACCESS)?; 166 | 167 | if ControlService( 168 | service.handle, 169 | SERVICE_CONTROL_STOP, 170 | &mut self.service_status, 171 | ) != 0 172 | { 173 | while QueryServiceStatus(service.handle, &mut self.service_status) != 0 { 174 | if self.service_status.dwCurrentState != SERVICE_STOP_PENDING { 175 | break; 176 | } 177 | thread::sleep(time::Duration::from_millis(250)); 178 | } 179 | } 180 | 181 | if DeleteService(service.handle) == 0 { 182 | return Err(Error::new(&format!( 183 | "DeleteService: {}", 184 | get_last_error_text() 185 | ))); 186 | } 187 | 188 | Ok(()) 189 | } 190 | } 191 | 192 | fn start(&mut self) -> Result<(), Error> { 193 | unsafe { 194 | let service_manager = ServiceControlManager::open(SC_MANAGER_ALL_ACCESS)?; 195 | let service = service_manager.open_service(&self.service_name, SERVICE_ALL_ACCESS)?; 196 | 197 | if StartServiceW(service.handle, 0, ptr::null_mut()) != 0 { 198 | while QueryServiceStatus(service.handle, &mut self.service_status) != 0 { 199 | if self.service_status.dwCurrentState != SERVICE_START_PENDING { 200 | break; 201 | } 202 | thread::sleep(time::Duration::from_millis(250)); 203 | } 204 | } 205 | 206 | if self.service_status.dwCurrentState != SERVICE_RUNNING { 207 | return Err(Error::new("Failed to start service")); 208 | } 209 | 210 | Ok(()) 211 | } 212 | } 213 | 214 | fn stop(&mut self) -> Result<(), Error> { 215 | unsafe { 216 | let service_manager = ServiceControlManager::open(SC_MANAGER_ALL_ACCESS)?; 217 | let service = service_manager.open_service(&self.service_name, SERVICE_ALL_ACCESS)?; 218 | 219 | if ControlService( 220 | service.handle, 221 | SERVICE_CONTROL_STOP, 222 | &mut self.service_status, 223 | ) != 0 224 | { 225 | while QueryServiceStatus(service.handle, &mut self.service_status) != 0 { 226 | if self.service_status.dwCurrentState != SERVICE_STOP_PENDING { 227 | break; 228 | } 229 | thread::sleep(time::Duration::from_millis(250)); 230 | } 231 | } else { 232 | return Err(Error::new("ControlService: failed to stop service")); 233 | } 234 | 235 | if self.service_status.dwCurrentState != SERVICE_STOPPED { 236 | return Err(Error::new("Failed to stop service")); 237 | } 238 | 239 | Ok(()) 240 | } 241 | } 242 | } 243 | 244 | impl WindowsController { 245 | pub fn new(service_name: &str, display_name: &str, description: &str) -> WindowsController { 246 | WindowsController { 247 | service_name: service_name.to_string(), 248 | display_name: display_name.to_string(), 249 | description: description.to_string(), 250 | desired_access: SERVICE_ALL_ACCESS, 251 | service_type: SERVICE_WIN32_OWN_PROCESS, 252 | start_type: SERVICE_AUTO_START, 253 | error_control: SERVICE_ERROR_NORMAL, 254 | tag_id: 0, 255 | load_order_group: "".to_string(), 256 | dependencies: "".to_string(), 257 | account_name: "".to_string(), 258 | password: "".to_string(), 259 | service_status: SERVICE_STATUS { 260 | dwServiceType: SERVICE_WIN32_OWN_PROCESS, 261 | dwCurrentState: SERVICE_STOPPED, 262 | dwControlsAccepted: 0, 263 | dwWin32ExitCode: 0, 264 | dwServiceSpecificExitCode: 0, 265 | dwCheckPoint: 0, 266 | dwWaitHint: 0, 267 | }, 268 | status_handle: 0, 269 | controls_accepted: SERVICE_ACCEPT_STOP, 270 | } 271 | } 272 | 273 | /// Register the `service_main_wrapper` function, this function is generated by the `Service!` macro. 274 | pub fn register( 275 | &mut self, 276 | service_main_wrapper: WindowsServiceMainWrapperFn, 277 | ) -> Result<(), Error> { 278 | unsafe { 279 | let mut service_name = get_utf16(self.service_name.as_str()); 280 | 281 | let service_table: &[*const SERVICE_TABLE_ENTRYW] = &[ 282 | &SERVICE_TABLE_ENTRYW { 283 | lpServiceName: service_name.as_mut_ptr(), 284 | lpServiceProc: Some(service_main_wrapper), 285 | }, 286 | ptr::null(), 287 | ]; 288 | 289 | match StartServiceCtrlDispatcherW(*service_table.as_ptr()) { 290 | 0 => Err(Error::new("StartServiceCtrlDispatcher")), 291 | _ => Ok(()), 292 | } 293 | } 294 | } 295 | } 296 | 297 | fn set_service_status( 298 | status_handle: SERVICE_STATUS_HANDLE, 299 | current_state: DWORD, 300 | wait_hint: DWORD, 301 | ) { 302 | let mut service_status = SERVICE_STATUS { 303 | dwServiceType: SERVICE_WIN32_OWN_PROCESS, 304 | dwCurrentState: current_state, 305 | dwControlsAccepted: SERVICE_ACCEPT_STOP 306 | | SERVICE_ACCEPT_SHUTDOWN 307 | | SERVICE_ACCEPT_PAUSE_CONTINUE 308 | | SERVICE_ACCEPT_SESSIONCHANGE, 309 | dwWin32ExitCode: 0, 310 | dwServiceSpecificExitCode: 0, 311 | dwCheckPoint: 0, 312 | dwWaitHint: wait_hint, 313 | }; 314 | unsafe { 315 | SetServiceStatus(status_handle, &mut service_status); 316 | } 317 | } 318 | 319 | unsafe extern "system" fn service_handler( 320 | control: DWORD, 321 | event_type: DWORD, 322 | event_data: LPVOID, 323 | context: LPVOID, 324 | ) -> DWORD { 325 | let tx = context as *mut mpsc::Sender>; 326 | 327 | match control { 328 | SERVICE_CONTROL_STOP | SERVICE_CONTROL_SHUTDOWN => { 329 | set_service_status(SERVICE_CONTROL_HANDLE, SERVICE_STOP_PENDING, 10); 330 | let _ = (*tx).send(ServiceEvent::Stop); 331 | 0 332 | } 333 | SERVICE_CONTROL_PAUSE => { 334 | let _ = (*tx).send(ServiceEvent::Pause); 335 | 0 336 | } 337 | SERVICE_CONTROL_CONTINUE => { 338 | let _ = (*tx).send(ServiceEvent::Continue); 339 | 0 340 | } 341 | SERVICE_CONTROL_SESSIONCHANGE => { 342 | let event = event_type; 343 | let session_notification = event_data as *const WTSSESSION_NOTIFICATION; 344 | let session_id = (*session_notification).dwSessionId; 345 | let session = Session::new(session_id); 346 | 347 | if event == WTS_CONSOLE_CONNECT { 348 | let _ = (*tx).send(ServiceEvent::SessionConnect(session)); 349 | 0 350 | } else if event == WTS_CONSOLE_DISCONNECT { 351 | let _ = (*tx).send(ServiceEvent::SessionDisconnect(session)); 352 | 0 353 | } else if event == WTS_REMOTE_CONNECT { 354 | let _ = (*tx).send(ServiceEvent::SessionRemoteConnect(session)); 355 | 0 356 | } else if event == WTS_REMOTE_DISCONNECT { 357 | let _ = (*tx).send(ServiceEvent::SessionRemoteDisconnect(session)); 358 | 0 359 | } else if event == WTS_SESSION_LOGON { 360 | let _ = (*tx).send(ServiceEvent::SessionLogon(session)); 361 | 0 362 | } else if event == WTS_SESSION_LOGOFF { 363 | let _ = (*tx).send(ServiceEvent::SessionLogoff(session)); 364 | 0 365 | } else if event == WTS_SESSION_LOCK { 366 | let _ = (*tx).send(ServiceEvent::SessionLock(session)); 367 | 0 368 | } else if event == WTS_SESSION_UNLOCK { 369 | let _ = (*tx).send(ServiceEvent::SessionUnlock(session)); 370 | 0 371 | } else { 372 | 0 373 | } 374 | } 375 | _ => ERROR_CALL_NOT_IMPLEMENTED, 376 | } 377 | } 378 | 379 | fn get_args(argc: DWORD, argv: *mut PWSTR) -> Vec { 380 | let mut args = Vec::new(); 381 | for i in 0..argc { 382 | unsafe { 383 | let s = *argv.add(i as usize); 384 | let widestr = WideCString::from_ptr_str(s); 385 | args.push(widestr.to_string_lossy()); 386 | } 387 | } 388 | args 389 | } 390 | 391 | pub fn get_utf16(value: &str) -> Vec { 392 | OsStr::new(value).encode_wide().chain(once(0)).collect() 393 | } 394 | 395 | pub fn get_filename() -> String { 396 | unsafe { 397 | let mut filename = [0u16; MAX_PATH as usize]; 398 | let _size = GetModuleFileNameW(0, filename.as_mut_ptr(), filename.len() as DWORD); 399 | String::from_utf16(&filename).unwrap_or_else(|_| String::from("")) 400 | } 401 | } 402 | 403 | pub fn get_last_error_text() -> String { 404 | unsafe { 405 | let mut message = [0u16; 512]; 406 | let length = FormatMessageW( 407 | FORMAT_MESSAGE_FROM_SYSTEM, 408 | ptr::null(), 409 | GetLastError(), 410 | 0, 411 | message.as_mut_ptr(), 412 | message.len() as u32, 413 | ptr::null_mut(), 414 | ); 415 | String::from_utf16(&message[0..length as usize]).unwrap_or_else(|_| String::from("")) 416 | } 417 | } 418 | 419 | /// Generates a `service_main_wrapper` that wraps the provided service main function. 420 | #[macro_export] 421 | macro_rules! Service { 422 | ($name:expr, $function:ident) => { 423 | use std::ffi::c_void; 424 | type DWORD = u32; 425 | type PWSTR = *mut u16; 426 | 427 | extern "system" fn service_main_wrapper(argc: DWORD, argv: *mut PWSTR) { 428 | dispatch($function, $name, argc, argv); 429 | } 430 | }; 431 | } 432 | 433 | #[doc(hidden)] 434 | pub fn dispatch(service_main: ServiceMainFn, name: &str, argc: DWORD, argv: *mut PWSTR) { 435 | let args = get_args(argc, argv); 436 | let service_name = get_utf16(name); 437 | let (mut tx, rx) = mpsc::channel(); 438 | let _tx = tx.clone(); 439 | let ctrl_handle = unsafe { 440 | RegisterServiceCtrlHandlerExW( 441 | service_name.as_ptr(), 442 | Some(service_handler::), 443 | &mut tx as *mut _ as LPVOID, 444 | ) 445 | }; 446 | unsafe { SERVICE_CONTROL_HANDLE = ctrl_handle }; 447 | set_service_status(ctrl_handle, SERVICE_START_PENDING, 0); 448 | set_service_status(ctrl_handle, SERVICE_RUNNING, 0); 449 | service_main(rx, _tx, args, false); 450 | set_service_status(ctrl_handle, SERVICE_STOPPED, 0); 451 | } 452 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ceviche is a wrapper to write a service/daemon. 2 | //! 3 | //! At the moment only Windows services are supported. The Service macro is inspired 4 | //! from the [winservice](https://crates.io/crates/winservice) crate. 5 | //! 6 | //! A service implements a service main function and is generated by invoking 7 | //! the `Service!` macro. The events are sent to the service over the `rx` channel. 8 | //! 9 | //! ```rust,ignore 10 | //! enum CustomServiceEvent {} 11 | //! 12 | //! fn my_service_main( 13 | //! rx: mpsc::Receiver>, 14 | //! _tx: mpsc::Sender>, 15 | //! args: Vec, 16 | //! standalone_mode: bool) -> u32 { 17 | //! loop { 18 | //! if let Ok(control_code) = rx.recv() { 19 | //! match control_code { 20 | //! ServiceEvent::Stop => break, 21 | //! _ => (), 22 | //! } 23 | //! } 24 | //! } 25 | //! 0 26 | //! } 27 | //! 28 | //! Service!("Foobar", my_service_main); 29 | //! ``` 30 | //! 31 | //! The Controller is a helper to create, remove, start or stop the service 32 | //! on the system. ceviche also supports a standalone mode were the service 33 | //! code runs as a normal executable which can be useful for development and 34 | //! debugging. 35 | //! 36 | //! ```rust,ignore 37 | //! static SERVICE_NAME: &'static str = "foobar"; 38 | //! static DISPLAY_NAME: &'static str = "FooBar Service"; 39 | //! static DESCRIPTION: &'static str = "This is the FooBar service"; 40 | //! 41 | //! fn main() { 42 | //! let yaml = load_yaml!("cli.yml"); 43 | //! let app = App::from_yaml(yaml); 44 | //! let matches = app.version(crate_version!()).get_matches(); 45 | //! let cmd = matches.value_of("cmd").unwrap_or("").to_string(); 46 | //! 47 | //! let mut controller = Controller::new(SERVICE_NAME, DISPLAY_NAME, DESCRIPTION); 48 | //! 49 | //! match cmd.as_str() { 50 | //! "create" => controller.create(), 51 | //! "delete" => controller.delete(), 52 | //! "start" => controller.start(), 53 | //! "stop" => controller.stop(), 54 | //! "standalone" => { 55 | //! let (tx, rx) = mpsc::channel(); 56 | //! 57 | //! ctrlc::set_handler(move || { 58 | //! let _ = tx.send(ServiceEvent::Stop); 59 | //! }).expect("Failed to register Ctrl-C handler"); 60 | //! 61 | //! my_service_main(rx, vec![], true); 62 | //! } 63 | //! _ => { 64 | //! let _result = controller.register(service_main_wrapper); 65 | //! } 66 | //! } 67 | //! } 68 | //! 69 | //! ``` 70 | 71 | #[macro_use] 72 | extern crate cfg_if; 73 | 74 | /// Manages the service on the system. 75 | pub mod controller; 76 | pub mod session; 77 | 78 | #[cfg(windows)] 79 | pub use windows_sys; 80 | 81 | use self::controller::Session; 82 | use std::fmt; 83 | 84 | /// Service errors 85 | #[derive(Debug)] 86 | pub struct Error { 87 | pub message: String, 88 | } 89 | 90 | impl From<&str> for Error { 91 | fn from(message: &str) -> Self { 92 | Error { 93 | message: message.to_string(), 94 | } 95 | } 96 | } 97 | 98 | impl fmt::Display for Error { 99 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 100 | write!(f, "{}", self.message,) 101 | } 102 | } 103 | 104 | impl std::error::Error for Error { 105 | fn description(&self) -> &str { 106 | &self.message 107 | } 108 | } 109 | 110 | impl Error { 111 | pub fn new(message: &str) -> Error { 112 | Error { 113 | message: String::from(message), 114 | } 115 | } 116 | } 117 | 118 | /// Events that are sent to the service. 119 | pub enum ServiceEvent { 120 | Continue, 121 | Pause, 122 | Stop, 123 | SessionConnect(Session), 124 | SessionDisconnect(Session), 125 | SessionRemoteConnect(Session), 126 | SessionRemoteDisconnect(Session), 127 | SessionLogon(Session), 128 | SessionLogoff(Session), 129 | SessionLock(Session), 130 | SessionUnlock(Session), 131 | Custom(T), 132 | } 133 | 134 | impl fmt::Display for ServiceEvent { 135 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 136 | match self { 137 | ServiceEvent::Continue => write!(f, "Continue"), 138 | ServiceEvent::Pause => write!(f, "Pause"), 139 | ServiceEvent::Stop => write!(f, "Stop"), 140 | ServiceEvent::SessionConnect(id) => write!(f, "SessionConnect({})", id), 141 | ServiceEvent::SessionDisconnect(id) => write!(f, "SessionDisconnect({})", id), 142 | ServiceEvent::SessionRemoteConnect(id) => write!(f, "SessionRemoteConnect({})", id), 143 | ServiceEvent::SessionRemoteDisconnect(id) => { 144 | write!(f, "SessionRemoteDisconnect({})", id) 145 | } 146 | ServiceEvent::SessionLogon(id) => write!(f, "SessionLogon({})", id), 147 | ServiceEvent::SessionLogoff(id) => write!(f, "SessionLogoff({})", id), 148 | ServiceEvent::SessionLock(id) => write!(f, "SessionLock({})", id), 149 | ServiceEvent::SessionUnlock(id) => write!(f, "SessionUnlock({})", id), 150 | ServiceEvent::Custom(_) => write!(f, "Custom"), 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | #[non_exhaustive] 4 | pub struct Session_ { 5 | pub id: T, 6 | } 7 | 8 | impl Display for Session_ 9 | where 10 | T: Display + PartialEq, 11 | { 12 | fn fmt(&self, f: &mut Formatter) -> Result { 13 | write!(f, "{}", self.id) 14 | } 15 | } 16 | 17 | impl PartialEq for Session_ 18 | where 19 | T: Display + PartialEq, 20 | { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.id == other.id 23 | } 24 | } 25 | 26 | impl Session_ 27 | where 28 | T: Display + PartialEq, 29 | { 30 | pub fn new(id: T) -> Self { 31 | Session_ { id } 32 | } 33 | } 34 | --------------------------------------------------------------------------------