├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE-2.0 ├── LICENSE-MIT ├── README.md ├── examples ├── id128.rs ├── notify.rs ├── persistent_state.rs └── watchdog.rs ├── src ├── activation.rs ├── credentials.rs ├── daemon.rs ├── errors.rs ├── id128.rs ├── lib.rs ├── logging.rs ├── sysusers │ ├── format.rs │ ├── mod.rs │ ├── parse.rs │ └── serialization.rs └── unit.rs └── tests ├── connected_to_journal.rs ├── journal.rs └── persistent_state.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rust 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | # Pinned toolchain for linting 8 | ACTION_LINTS_TOOLCHAIN: '1.69.0' 9 | 10 | jobs: 11 | tests-stable: 12 | name: "Tests, stable toolchain" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: dtolnay/rust-toolchain@stable 17 | - run: cargo build 18 | - run: cargo test 19 | - run: cargo build --release 20 | tests-minimum-toolchain: 21 | name: "Tests, minimum supported toolchain (MSRV)" 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Extract MSRV 26 | run: echo "ACTION_MSRV_TOOLCHAIN=$(grep 'rust-version' Cargo.toml | cut -d '"' -f2)" >> $GITHUB_ENV 27 | - uses: dtolnay/rust-toolchain@master 28 | with: 29 | toolchain: ${{ env['ACTION_MSRV_TOOLCHAIN'] }} 30 | - run: cargo build --release 31 | - run: cargo test --release 32 | linting: 33 | name: "Lints, pinned toolchain" 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: dtolnay/rust-toolchain@master 38 | with: 39 | toolchain: ${{ env['ACTION_LINTS_TOOLCHAIN'] }} 40 | components: rustfmt,clippy 41 | - run: cargo clippy -- -D warnings 42 | - run: cargo fmt -- --check -l 43 | tests-other-channels: 44 | name: "Tests, unstable toolchain" 45 | runs-on: ubuntu-latest 46 | continue-on-error: true 47 | strategy: 48 | matrix: 49 | channel: 50 | - "beta" 51 | - "nightly" 52 | steps: 53 | - uses: actions/checkout@v3 54 | - uses: dtolnay/rust-toolchain@master 55 | with: 56 | toolchain: ${{ matrix.channel }} 57 | - run: cargo build 58 | - run: cargo test 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: libsystemd 3 | Source: https://www.github.com/lucab/libsystemd-rs 4 | 5 | Files: * 6 | Copyright: 2017-2019, Project contributors 7 | License: MIT or Apache-2.0 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libsystemd" 3 | version = "0.7.2" 4 | authors = ["Luca Bruno ", "Sebastian Wiesner "] 5 | license = "MIT/Apache-2.0" 6 | repository = "https://github.com/lucab/libsystemd-rs" 7 | documentation = "https://docs.rs/libsystemd" 8 | description = "A pure-Rust client library to interact with systemd" 9 | keywords = ["systemd", "linux"] 10 | categories = ["api-bindings", "os::unix-apis"] 11 | readme = "README.md" 12 | exclude = [ 13 | ".gitignore", 14 | ".travis.yml", 15 | ] 16 | edition = "2021" 17 | rust-version = "1.69" 18 | 19 | [dependencies] 20 | hmac = "^0.12" 21 | libc = "^0.2" 22 | log = "^0.4" 23 | nix = { version = "^0.29", default-features = false, features = ["dir", "fs", "socket", "process", "uio"] } 24 | nom = "8" 25 | serde = { version = "^1.0.91", features = ["derive"] } 26 | sha2 = "^0.10" 27 | thiserror = "^2.0" 28 | uuid = { version = "^1.0", features = ["serde"] } 29 | once_cell = "^1.8" 30 | 31 | [dev-dependencies] 32 | quickcheck = "^1.0" 33 | serde_json = "^1.0" 34 | rand = "^0.9" 35 | pretty_assertions = "^1.0" 36 | 37 | [[test]] 38 | name = "connected_to_journal" 39 | harness = false 40 | 41 | [[test]] 42 | name = "persistent_state" 43 | harness = false 44 | 45 | [package.metadata.release] 46 | publish = false 47 | push = false 48 | pre-release-commit-message = "cargo: libsystemd release {{version}}" 49 | sign-commit = true 50 | sign-tag = true 51 | tag-message = "libsystemd {{version}}" 52 | -------------------------------------------------------------------------------- /LICENSE-APACHE-2.0: -------------------------------------------------------------------------------- 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 person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsystemd 2 | 3 | [![crates.io](https://img.shields.io/crates/v/libsystemd.svg)](https://crates.io/crates/libsystemd) 4 | [![LoC](https://tokei.rs/b1/github/lucab/libsystemd-rs?category=code)](https://github.com/lucab/libsystemd-rs) 5 | [![Documentation](https://docs.rs/libsystemd/badge.svg)](https://docs.rs/libsystemd) 6 | 7 | A pure-Rust client library to work with systemd. 8 | 9 | It provides support to interact with systemd components available 10 | on modern Linux systems. This crate is entirely implemented 11 | in Rust, and does not require the libsystemd C library. 12 | 13 | NB: this crate is not yet features-complete. If you don't care about C dependency, check [rust-systemd](https://github.com/jmesmon/rust-systemd) instead. 14 | 15 | ## Example 16 | 17 | ```rust 18 | extern crate libsystemd; 19 | use libsystemd::daemon::{self, NotifyState}; 20 | 21 | fn main() { 22 | if !daemon::booted() { 23 | panic!("Not running systemd, early exit."); 24 | }; 25 | 26 | let sent = daemon::notify(true, &[NotifyState::Ready]).expect("notify failed"); 27 | if !sent { 28 | panic!("Notification not sent, early exit."); 29 | }; 30 | std::thread::park(); 31 | } 32 | ``` 33 | 34 | Some more examples are available under [examples](examples). 35 | 36 | ## License 37 | 38 | Licensed under either of 39 | 40 | * MIT license - 41 | * Apache License, Version 2.0 - 42 | 43 | at your option. 44 | -------------------------------------------------------------------------------- /examples/id128.rs: -------------------------------------------------------------------------------- 1 | use libsystemd::id128; 2 | 3 | fn main() { 4 | let app_id = id128::Id128::parse_str("47c2a52ec65947ae88b2b08d14ec126b") 5 | .expect("Failed to parse ID string"); 6 | 7 | println!("get_machine: {:?}", id128::get_machine()); 8 | println!( 9 | "get_machine_app_specific: {:?}", 10 | id128::get_machine_app_specific(&app_id) 11 | ); 12 | 13 | println!("get_boot: {:?}", id128::get_boot()); 14 | println!( 15 | "get_boot_app_specific: {:?}", 16 | id128::get_boot_app_specific(&app_id) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/notify.rs: -------------------------------------------------------------------------------- 1 | extern crate libsystemd; 2 | 3 | use libsystemd::daemon::{self, NotifyState}; 4 | use std::{env, thread}; 5 | 6 | /* 7 | cargo build --example notify 8 | systemd-run --user --wait -p Type=notify ./target/debug/examples/notify [CUSTOM_STATUS] 9 | */ 10 | 11 | fn main() { 12 | if !daemon::booted() { 13 | println!("Not running systemd, early exit."); 14 | return; 15 | }; 16 | let state = match env::args().nth(1) { 17 | Some(s) => NotifyState::Status(s), 18 | None => NotifyState::Ready, 19 | }; 20 | 21 | let sent = daemon::notify(true, &[state]).expect("notify failed"); 22 | if !sent { 23 | println!("Notification not sent!"); 24 | } 25 | 26 | thread::park(); 27 | } 28 | -------------------------------------------------------------------------------- /examples/persistent_state.rs: -------------------------------------------------------------------------------- 1 | // File descriptor storing (FDSTORE) example. 2 | // 3 | // Store a piece of state in form of a file descriptor which will survive the life of the process 4 | // by storing the file descriptor in systemd. The stored file descriptors are made available to 5 | // the process on restart. 6 | // 7 | // The example demonstates this by storing a single byte in a memory backed file. The byte is 8 | // sent to systemd and incremented before exiting. Upon automatic restart the state is retrieved 9 | // from systemd and incremented again. If the state byte has been incremented a set amount of 10 | // times (3) the example is considered finished. 11 | // 12 | // In this example systemd must be configured to restart only on certain exit statuses. See the 13 | // example commands below. 14 | // 15 | // ```shell 16 | // cargo build --example persistent_state 17 | // systemd-run --user -p RestartForceExitStatus=2 -p SuccessExitStatus=2 -p FileDescriptorStoreMax=1 --wait ./target/debug/examples/persistent_state 18 | // journalctl --user -ocat -xeu .service 19 | // ``` 20 | // 21 | // The persistent behaviour can be observed in the journalctl log for the unit: 22 | // ```shell 23 | // Started ./target/debug/examples/persistent_state. 24 | // Created new persistent state 25 | // State was: 1, exit and restart 26 | // .service: Scheduled restart job, restart counter is at 1. 27 | // Stopped ./target/debug/examples/persistent_state. 28 | // Started ./target/debug/examples/persistent_state. 29 | // Fetched persistent state from systemd 30 | // State was: 2, exit and restart 31 | // .service: Scheduled restart job, restart counter is at 2. 32 | // Stopped ./target/debug/examples/persistent_state. 33 | // Started ./target/debug/examples/persistent_state. 34 | // Fetched persistent state from systemd 35 | // Exiting normally because persistent state is now 3 36 | // ``` 37 | // 38 | // Please note that this example is also part of the integration suite. 39 | // See tests/persistent_store.rs. Specifically it is important that the test is updated 40 | // if changes are made to the systemd-run command above. 41 | 42 | use std::error::Error; 43 | use std::fs::{self, File, OpenOptions}; 44 | use std::io::{self, ErrorKind, Read, Seek, Write}; 45 | use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd}; 46 | use std::result::Result; 47 | 48 | use libsystemd::activation; 49 | use libsystemd::daemon::{self, NotifyState}; 50 | 51 | /// Create a memory backed file as state and store it in systemd. 52 | fn create_and_store_persistent_state() -> Result> { 53 | let path = format!("/dev/shm/persistent_state-{}", std::process::id()); 54 | let mut f = OpenOptions::new() 55 | .read(true) 56 | .write(true) 57 | .create(true) 58 | .truncate(true) 59 | .open(&path)?; 60 | fs::remove_file(&path)?; 61 | 62 | let nss = [ 63 | NotifyState::Fdname("persistent-state".to_owned()), 64 | NotifyState::Fdstore, 65 | ]; 66 | 67 | daemon::notify_with_fds(false, &nss, &[f.as_raw_fd()])?; 68 | // Set initial state to 0 69 | let state = [0u8, 1]; 70 | f.set_len(state.len() as u64)?; 71 | f.write_all(&state)?; 72 | f.rewind()?; 73 | Ok(f) 74 | } 75 | 76 | fn run() -> Result> { 77 | if !daemon::booted() { 78 | println!("Not running systemd, early exit."); 79 | return Ok(1); 80 | }; 81 | 82 | let mut descriptors = 83 | activation::receive_descriptors_with_names(false).unwrap_or_else(|_| Vec::new()); 84 | 85 | let mut persistent_state = if let Some((fd, name)) = descriptors.pop() { 86 | println!("Fetched persistent state from systemd"); 87 | if name == "persistent-state" { 88 | unsafe { File::from_raw_fd(fd.into_raw_fd()) } 89 | } else { 90 | let err = io::Error::new(ErrorKind::Other, "Got the wrong file descriptor."); 91 | return Err(Box::new(err)); 92 | } 93 | } else { 94 | println!("Got nothing from systemd, create new persistent state"); 95 | create_and_store_persistent_state()? 96 | }; 97 | 98 | // Read and increment state 99 | let mut buf = [0xEFu8; 1]; 100 | persistent_state.read_exact(&mut buf)?; 101 | persistent_state.rewind()?; 102 | 103 | buf[0] += 1; 104 | persistent_state.write_all(&buf)?; 105 | persistent_state.rewind()?; 106 | 107 | // Restart a few times 108 | if buf[0] < 3 { 109 | println!("State was: {}, exit and restart", buf[0]); 110 | // Systemd should have been configured to restart this process on exit status 2. 111 | return Ok(2); 112 | } 113 | 114 | println!( 115 | "Exiting normally because persistent state is now {}", 116 | buf[0] 117 | ); 118 | 119 | let nss = [ 120 | NotifyState::Fdname("persistent-state".to_owned()), 121 | NotifyState::FdstoreRemove, 122 | ]; 123 | 124 | daemon::notify(false, &nss)?; 125 | 126 | Ok(0) 127 | } 128 | 129 | pub fn main() -> Result<(), Box> { 130 | std::process::exit(run()?); 131 | } 132 | -------------------------------------------------------------------------------- /examples/watchdog.rs: -------------------------------------------------------------------------------- 1 | extern crate libsystemd; 2 | 3 | use libsystemd::daemon::{self, NotifyState}; 4 | use std::thread; 5 | 6 | /* 7 | ``` 8 | [Service] 9 | WatchdogSec=1s 10 | ExecStart=/home/user/libsystemd-rs/target/debug/examples/watchdog 11 | ``` 12 | 13 | cargo build --example watchdog ; systemctl start --wait --user watchdog; systemctl status --user watchdog 14 | */ 15 | 16 | fn main() { 17 | if !daemon::booted() { 18 | println!("Not running systemd, early exit."); 19 | return; 20 | }; 21 | 22 | let timeout = daemon::watchdog_enabled(true).expect("watchdog disabled"); 23 | for i in 0..20 { 24 | let _sent = daemon::notify(false, &[NotifyState::Watchdog]).expect("notify failed"); 25 | println!("Notification #{} sent...", i); 26 | thread::sleep(timeout / 2); 27 | } 28 | 29 | println!("Blocking forever!"); 30 | thread::park(); 31 | } 32 | -------------------------------------------------------------------------------- /src/activation.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Context, SdError}; 2 | use nix::fcntl::{fcntl, FdFlag, F_SETFD}; 3 | use nix::sys::socket::getsockname; 4 | use nix::sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}; 5 | use nix::sys::stat::fstat; 6 | use std::convert::TryFrom; 7 | use std::env; 8 | use std::os::unix::io::{IntoRawFd, RawFd}; 9 | use std::process; 10 | 11 | /// Minimum FD number used by systemd for passing sockets. 12 | const SD_LISTEN_FDS_START: RawFd = 3; 13 | 14 | /// Trait for checking the type of a file descriptor. 15 | pub trait IsType { 16 | /// Returns true if a file descriptor is a FIFO. 17 | fn is_fifo(&self) -> bool; 18 | 19 | /// Returns true if a file descriptor is a special file. 20 | fn is_special(&self) -> bool; 21 | 22 | /// Returns true if a file descriptor is a `PF_INET` socket. 23 | fn is_inet(&self) -> bool; 24 | 25 | /// Returns true if a file descriptor is a `PF_UNIX` socket. 26 | fn is_unix(&self) -> bool; 27 | 28 | /// Returns true if a file descriptor is a POSIX message queue descriptor. 29 | fn is_mq(&self) -> bool; 30 | } 31 | 32 | /// File descriptor passed by systemd to socket-activated services. 33 | /// 34 | /// See . 35 | #[derive(Debug, Clone)] 36 | pub struct FileDescriptor(SocketFd); 37 | 38 | /// Possible types of sockets. 39 | #[derive(Debug, Clone)] 40 | enum SocketFd { 41 | /// A FIFO named pipe (see `man 7 fifo`) 42 | Fifo(RawFd), 43 | /// A special file, such as character device nodes or special files in 44 | /// `/proc` and `/sys`. 45 | Special(RawFd), 46 | /// A `PF_INET` socket, such as UDP/TCP sockets. 47 | Inet(RawFd), 48 | /// A `PF_UNIX` socket (see `man 7 unix`). 49 | Unix(RawFd), 50 | /// A POSIX message queue (see `man 7 mq_overview`). 51 | Mq(RawFd), 52 | /// An unknown descriptor (possibly invalid, use with caution). 53 | Unknown(RawFd), 54 | } 55 | 56 | impl IsType for FileDescriptor { 57 | fn is_fifo(&self) -> bool { 58 | matches!(self.0, SocketFd::Fifo(_)) 59 | } 60 | 61 | fn is_special(&self) -> bool { 62 | matches!(self.0, SocketFd::Special(_)) 63 | } 64 | 65 | fn is_unix(&self) -> bool { 66 | matches!(self.0, SocketFd::Unix(_)) 67 | } 68 | 69 | fn is_inet(&self) -> bool { 70 | matches!(self.0, SocketFd::Inet(_)) 71 | } 72 | 73 | fn is_mq(&self) -> bool { 74 | matches!(self.0, SocketFd::Mq(_)) 75 | } 76 | } 77 | 78 | /// Check for file descriptors passed by systemd. 79 | /// 80 | /// Invoked by socket activated daemons to check for file descriptors needed by the service. 81 | /// If `unset_env` is true, the environment variables used by systemd will be cleared. 82 | pub fn receive_descriptors(unset_env: bool) -> Result, SdError> { 83 | let pid = env::var("LISTEN_PID"); 84 | let fds = env::var("LISTEN_FDS"); 85 | log::trace!("LISTEN_PID = {:?}; LISTEN_FDS = {:?}", pid, fds); 86 | 87 | if unset_env { 88 | env::remove_var("LISTEN_PID"); 89 | env::remove_var("LISTEN_FDS"); 90 | env::remove_var("LISTEN_FDNAMES"); 91 | } 92 | 93 | // Parse `$LISTEN_PID` if present. 94 | if let Err(env::VarError::NotPresent) = pid { 95 | return Ok(vec![]); 96 | } 97 | let pid = pid 98 | .context("failed to get LISTEN_PID")? 99 | .parse::() 100 | .context("failed to parse LISTEN_PID")?; 101 | let current_pid = process::id(); 102 | if pid != current_pid { 103 | log::info!( 104 | "Ignoring systemd activation settings ($LISTEN_PID={}), not meant for current process (PID {}).", 105 | pid, 106 | current_pid, 107 | ); 108 | return Ok(vec![]); 109 | } 110 | 111 | // Parse `$LISTEN_FDS` if present. 112 | if let Err(env::VarError::NotPresent) = fds { 113 | return Ok(vec![]); 114 | } 115 | let fds = fds 116 | .context("failed to get LISTEN_FDS")? 117 | .parse::() 118 | .context("failed to parse LISTEN_FDS")?; 119 | 120 | socks_from_fds(fds) 121 | } 122 | 123 | /// Check for named file descriptors passed by systemd. 124 | /// 125 | /// Like `receive_descriptors`, but this will also return a vector of names 126 | /// associated with each file descriptor. 127 | pub fn receive_descriptors_with_names( 128 | unset_env: bool, 129 | ) -> Result, SdError> { 130 | let pid = env::var("LISTEN_PID"); 131 | let fds = env::var("LISTEN_FDS"); 132 | let fdnames = env::var("LISTEN_FDNAMES"); 133 | log::trace!( 134 | "LISTEN_PID = {:?}; LISTEN_FDS = {:?}; LISTEN_FDNAMES = {:?}", 135 | pid, 136 | fds, 137 | fdnames 138 | ); 139 | 140 | if unset_env { 141 | env::remove_var("LISTEN_PID"); 142 | env::remove_var("LISTEN_FDS"); 143 | env::remove_var("LISTEN_FDNAMES"); 144 | } 145 | 146 | // Parse `$LISTEN_PID` if present. 147 | if let Err(env::VarError::NotPresent) = pid { 148 | return Ok(vec![]); 149 | } 150 | let pid = pid 151 | .context("failed to get LISTEN_PID")? 152 | .parse::() 153 | .context("failed to parse LISTEN_PID")?; 154 | let current_pid = process::id(); 155 | if pid != current_pid { 156 | log::info!( 157 | "Ignoring systemd activation settings ($LISTEN_PID={}), not meant for current process (PID {}).", 158 | pid, 159 | current_pid 160 | ); 161 | return Ok(vec![]); 162 | } 163 | 164 | // Parse `$LISTEN_FDS` if present. 165 | if let Err(env::VarError::NotPresent) = fds { 166 | return Ok(vec![]); 167 | } 168 | let fds = fds 169 | .context("failed to get LISTEN_FDS")? 170 | .parse::() 171 | .context("failed to parse LISTEN_FDS")?; 172 | 173 | // Parse `$LISTEN_FDNAMES` if present. 174 | if let Err(env::VarError::NotPresent) = fdnames { 175 | return Ok(vec![]); 176 | } 177 | let fdnames = fdnames.context("failed to get LISTEN_FDNAMES")?; 178 | let names = fdnames.split(':').map(String::from); 179 | 180 | let vec = socks_from_fds(fds).context("failed to get sockets from file descriptor")?; 181 | let out = vec.into_iter().zip(names).collect(); 182 | 183 | Ok(out) 184 | } 185 | 186 | fn socks_from_fds(fd_count: usize) -> Result, SdError> { 187 | let mut descriptors = Vec::with_capacity(fd_count); 188 | for fd_offset in 0..fd_count { 189 | let fd_num = SD_LISTEN_FDS_START 190 | .checked_add(fd_offset as i32) 191 | .with_context(|| format!("overlarge file descriptor index: {}", fd_count))?; 192 | // Set CLOEXEC on the file descriptors we receive so that they aren't 193 | // passed to programs exec'd from here, just like sd_listen_fds does. 194 | if let Err(errno) = fcntl(fd_num, F_SETFD(FdFlag::FD_CLOEXEC)) { 195 | return Err(format!("couldn't set FD_CLOEXEC on {fd_num}: {errno}").into()); 196 | } 197 | let fd = FileDescriptor::try_from(fd_num).unwrap_or_else(|(msg, val)| { 198 | log::warn!("{}", msg); 199 | FileDescriptor(SocketFd::Unknown(val)) 200 | }); 201 | descriptors.push(fd); 202 | } 203 | 204 | Ok(descriptors) 205 | } 206 | 207 | impl IsType for RawFd { 208 | fn is_fifo(&self) -> bool { 209 | fstat(*self) 210 | .map(|stat| (stat.st_mode & 0o0_170_000) == 0o010_000) 211 | .unwrap_or(false) 212 | } 213 | 214 | fn is_special(&self) -> bool { 215 | fstat(*self) 216 | .map(|stat| (stat.st_mode & 0o0_170_000) == 0o100_000) 217 | .unwrap_or(false) 218 | } 219 | 220 | fn is_inet(&self) -> bool { 221 | getsockname::(*self) 222 | .map(|addr| { 223 | matches!( 224 | addr.family(), 225 | Some(AddressFamily::Inet) | Some(AddressFamily::Inet6) 226 | ) 227 | }) 228 | .unwrap_or(false) 229 | } 230 | 231 | fn is_unix(&self) -> bool { 232 | getsockname::(*self) 233 | .map(|addr| matches!(addr.family(), Some(AddressFamily::Unix))) 234 | .unwrap_or(false) 235 | } 236 | 237 | fn is_mq(&self) -> bool { 238 | // `nix` does not enable us to test if a raw fd is a mq, so we must drop to libc here. 239 | // SAFETY: `mq_getattr` is specified to return -1 when passed a fd which is not a mq. 240 | // Furthermore, we ignore `attr` and rely only on the return value. 241 | let mut attr = std::mem::MaybeUninit::::uninit(); 242 | let res = unsafe { libc::mq_getattr(*self, attr.as_mut_ptr()) }; 243 | res == 0 244 | } 245 | } 246 | 247 | impl TryFrom for FileDescriptor { 248 | type Error = (SdError, RawFd); 249 | 250 | fn try_from(value: RawFd) -> Result { 251 | if value.is_fifo() { 252 | return Ok(FileDescriptor(SocketFd::Fifo(value))); 253 | } else if value.is_special() { 254 | return Ok(FileDescriptor(SocketFd::Special(value))); 255 | } else if value.is_inet() { 256 | return Ok(FileDescriptor(SocketFd::Inet(value))); 257 | } else if value.is_unix() { 258 | return Ok(FileDescriptor(SocketFd::Unix(value))); 259 | } else if value.is_mq() { 260 | return Ok(FileDescriptor(SocketFd::Mq(value))); 261 | } 262 | 263 | let err_msg = format!( 264 | "conversion failure, possibly invalid or unknown file descriptor {}", 265 | value 266 | ); 267 | Err((err_msg.into(), value)) 268 | } 269 | } 270 | 271 | // TODO(lucab): replace with multiple safe `TryInto` helpers plus an `unsafe` fallback. 272 | impl IntoRawFd for FileDescriptor { 273 | fn into_raw_fd(self) -> RawFd { 274 | match self.0 { 275 | SocketFd::Fifo(fd) => fd, 276 | SocketFd::Special(fd) => fd, 277 | SocketFd::Inet(fd) => fd, 278 | SocketFd::Unix(fd) => fd, 279 | SocketFd::Mq(fd) => fd, 280 | SocketFd::Unknown(fd) => fd, 281 | } 282 | } 283 | } 284 | 285 | #[cfg(test)] 286 | mod tests { 287 | use super::*; 288 | 289 | #[test] 290 | fn test_socketype_is_unix() { 291 | let sock = FileDescriptor(SocketFd::Unix(0i32)); 292 | assert!(sock.is_unix()); 293 | } 294 | 295 | #[test] 296 | fn test_socketype_is_special() { 297 | let sock = FileDescriptor(SocketFd::Special(0i32)); 298 | assert!(sock.is_special()); 299 | } 300 | 301 | #[test] 302 | fn test_socketype_is_inet() { 303 | let sock = FileDescriptor(SocketFd::Inet(0i32)); 304 | assert!(sock.is_inet()); 305 | } 306 | 307 | #[test] 308 | fn test_socketype_is_fifo() { 309 | let sock = FileDescriptor(SocketFd::Fifo(0i32)); 310 | assert!(sock.is_fifo()); 311 | } 312 | 313 | #[test] 314 | fn test_socketype_is_mq() { 315 | let sock = FileDescriptor(SocketFd::Mq(0i32)); 316 | assert!(sock.is_mq()); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/credentials.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Context, SdError}; 2 | use nix::dir; 3 | use nix::fcntl::OFlag; 4 | use nix::sys::stat::Mode; 5 | use std::env; 6 | use std::fs::File; 7 | use std::path::PathBuf; 8 | 9 | /// Credential loader for units. 10 | /// 11 | /// Credentials are read by systemd on unit startup and exported by their ID. 12 | /// 13 | /// **Note**: only the user associated with the unit and the superuser may access credentials. 14 | /// 15 | /// More documentation: 16 | #[derive(Debug)] 17 | pub struct CredentialsLoader { 18 | path: PathBuf, 19 | _dirfd: dir::Dir, 20 | } 21 | 22 | impl CredentialsLoader { 23 | /// Try to open credentials directory. 24 | pub fn open() -> Result { 25 | let path = Self::path_from_env().ok_or_else(|| { 26 | SdError::from("No valid environment variable 'CREDENTIALS_DIRECTORY' found") 27 | })?; 28 | 29 | // NOTE(lucab): we try to open the directory and then store its dirfd, so 30 | // that we know it exists. We don't further use it now, but in the 31 | // future we may couple it to something like 'cap-std' helpers. 32 | let _dirfd = dir::Dir::open(&path, OFlag::O_RDONLY | OFlag::O_DIRECTORY, Mode::empty()) 33 | .with_context(|| format!("Opening credentials directory at '{}'", path.display()))?; 34 | 35 | let loader = Self { path, _dirfd }; 36 | Ok(loader) 37 | } 38 | 39 | /// Return the location of the credentials directory, if any. 40 | pub fn path_from_env() -> Option { 41 | env::var("CREDENTIALS_DIRECTORY").map(|v| v.into()).ok() 42 | } 43 | 44 | /// Get credential by ID. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ```no_run 49 | /// use libsystemd::credentials::CredentialsLoader; 50 | /// 51 | /// let loader = CredentialsLoader::open()?; 52 | /// let token = loader.get("token")?; 53 | /// let token_metadata = token.metadata()?; 54 | /// println!("token size: {}", token_metadata.len()); 55 | /// # Ok::<(), Box>(()) 56 | /// ``` 57 | pub fn get(&self, id: impl AsRef) -> Result { 58 | let cred_path = self.cred_absolute_path(id.as_ref())?; 59 | File::open(&cred_path).map_err(|e| { 60 | let msg = format!("Opening credential at {}: {}", cred_path.display(), e); 61 | SdError::from(msg) 62 | }) 63 | } 64 | 65 | /// Validate credential ID and return its absolute path. 66 | fn cred_absolute_path(&self, id: &str) -> Result { 67 | if id.contains('/') { 68 | return Err(SdError::from("Invalid credential ID")); 69 | } 70 | 71 | let abs_path = self.path.join(id); 72 | Ok(abs_path) 73 | } 74 | 75 | /// Return an iterator over all existing credentials. 76 | /// 77 | /// # Examples 78 | /// 79 | /// ```no_run 80 | /// use libsystemd::credentials::CredentialsLoader; 81 | /// 82 | /// let loader = CredentialsLoader::open()?; 83 | /// for entry in loader.iter()? { 84 | /// let credential = entry?; 85 | /// println!("Credential ID: {}", credential.file_name().to_string_lossy()); 86 | /// } 87 | /// # Ok::<(), Box>(()) 88 | pub fn iter(&self) -> Result { 89 | std::fs::read_dir(&self.path) 90 | .with_context(|| format!("Opening credential directory at {}", self.path.display())) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Context, SdError}; 2 | use libc::pid_t; 3 | use nix::sys::socket; 4 | use nix::unistd; 5 | use std::io::{self, IoSlice}; 6 | use std::os::unix::io::RawFd; 7 | use std::os::unix::net::UnixDatagram; 8 | use std::os::unix::prelude::AsRawFd; 9 | use std::{env, fmt, fs, time}; 10 | 11 | /// Check for systemd presence at runtime. 12 | /// 13 | /// Return true if the system was booted with systemd. 14 | /// This check is based on the presence of the systemd 15 | /// runtime directory. 16 | pub fn booted() -> bool { 17 | fs::symlink_metadata("/run/systemd/system") 18 | .map(|p| p.is_dir()) 19 | .unwrap_or(false) 20 | } 21 | 22 | /// Check for watchdog support at runtime. 23 | /// 24 | /// Return a timeout before which the watchdog expects a 25 | /// response from the process, or `None` if watchdog support is 26 | /// not enabled. If `unset_env` is true, environment will be cleared. 27 | pub fn watchdog_enabled(unset_env: bool) -> Option { 28 | let env_usec = env::var("WATCHDOG_USEC").ok(); 29 | let env_pid = env::var("WATCHDOG_PID").ok(); 30 | 31 | if unset_env { 32 | env::remove_var("WATCHDOG_USEC"); 33 | env::remove_var("WATCHDOG_PID"); 34 | }; 35 | 36 | let timeout = { 37 | if let Some(usec) = env_usec.and_then(|usec_str| usec_str.parse::().ok()) { 38 | time::Duration::from_millis(usec / 1_000) 39 | } else { 40 | return None; 41 | } 42 | }; 43 | 44 | let pid = { 45 | if let Some(pid_str) = env_pid { 46 | if let Ok(p) = pid_str.parse::() { 47 | unistd::Pid::from_raw(p) 48 | } else { 49 | return None; 50 | } 51 | } else { 52 | return Some(timeout); 53 | } 54 | }; 55 | 56 | if unistd::getpid() == pid { 57 | Some(timeout) 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | /// Notify service manager about status changes. 64 | /// 65 | /// Send a notification to the manager about service status changes. 66 | /// The returned boolean show whether notifications are supported for 67 | /// this service. If `unset_env` is true, environment will be cleared 68 | /// and no further notifications are possible. 69 | /// Also see [`notify_with_fds`] which can send file descriptors to the 70 | /// service manager. 71 | pub fn notify(unset_env: bool, state: &[NotifyState]) -> Result { 72 | notify_with_fds(unset_env, state, &[]) 73 | } 74 | 75 | /// Notify service manager about status changes and send file descriptors. 76 | /// 77 | /// Use this together with [`NotifyState::Fdstore`]. Otherwise works like [`notify`]. 78 | pub fn notify_with_fds( 79 | unset_env: bool, 80 | state: &[NotifyState], 81 | fds: &[RawFd], 82 | ) -> Result { 83 | let env_sock = match env::var("NOTIFY_SOCKET").ok() { 84 | None => return Ok(false), 85 | Some(v) => v, 86 | }; 87 | 88 | if unset_env { 89 | env::remove_var("NOTIFY_SOCKET"); 90 | }; 91 | 92 | sanity_check_state_entries(state)?; 93 | 94 | // If the first character of `$NOTIFY_SOCKET` is '@', the string 95 | // is understood as Linux abstract namespace socket. 96 | let socket_addr = match env_sock.strip_prefix('@') { 97 | Some(stripped_addr) => socket::UnixAddr::new_abstract(stripped_addr.as_bytes()) 98 | .with_context(|| format!("invalid Unix socket abstract address {}", env_sock))?, 99 | None => socket::UnixAddr::new(env_sock.as_str()) 100 | .with_context(|| format!("invalid Unix socket path address {}", env_sock))?, 101 | }; 102 | 103 | let socket = UnixDatagram::unbound().context("failed to open Unix datagram socket")?; 104 | let msg = state 105 | .iter() 106 | .fold(String::new(), |res, s| res + &format!("{}\n", s)) 107 | .into_bytes(); 108 | let msg_len = msg.len(); 109 | let msg_iov = IoSlice::new(&msg); 110 | 111 | let ancillary = if !fds.is_empty() { 112 | vec![socket::ControlMessage::ScmRights(fds)] 113 | } else { 114 | vec![] 115 | }; 116 | 117 | let sent_len = socket::sendmsg( 118 | socket.as_raw_fd(), 119 | &[msg_iov], 120 | &ancillary, 121 | socket::MsgFlags::empty(), 122 | Some(&socket_addr), 123 | ) 124 | .map_err(|e| io::Error::from_raw_os_error(e as i32)) 125 | .context("failed to send notify datagram")?; 126 | 127 | if sent_len != msg_len { 128 | return Err(format!( 129 | "incomplete notify sendmsg, sent {} out of {}", 130 | sent_len, msg_len 131 | ) 132 | .into()); 133 | } 134 | 135 | Ok(true) 136 | } 137 | 138 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 139 | /// Status changes, see `sd_notify(3)`. 140 | pub enum NotifyState { 141 | /// D-Bus error-style error code. 142 | Buserror(String), 143 | /// errno-style error code. 144 | Errno(u8), 145 | /// A name for the submitted file descriptors. 146 | Fdname(String), 147 | /// Stores additional file descriptors in the service manager. Use [`notify_with_fds`] with this. 148 | Fdstore, 149 | /// Remove stored file descriptors. Must be used together with [`NotifyState::Fdname`]. 150 | FdstoreRemove, 151 | /// Tell the service manager to not poll the filedescriptors for errors. This causes 152 | /// systemd to hold on to broken file descriptors which must be removed manually. 153 | /// Must be used together with [`NotifyState::Fdstore`]. 154 | FdpollDisable, 155 | /// The main process ID of the service, in case of forking applications. 156 | Mainpid(unistd::Pid), 157 | /// Custom state change, as a `KEY=VALUE` string. 158 | Other(String), 159 | /// Service startup is finished. 160 | Ready, 161 | /// Service is reloading. 162 | Reloading, 163 | /// Custom status change. 164 | Status(String), 165 | /// Service is beginning to shutdown. 166 | Stopping, 167 | /// Tell the service manager to update the watchdog timestamp. 168 | Watchdog, 169 | /// Reset watchdog timeout value during runtime. 170 | WatchdogUsec(u64), 171 | } 172 | 173 | impl fmt::Display for NotifyState { 174 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 175 | match *self { 176 | NotifyState::Buserror(ref s) => write!(f, "BUSERROR={}", s), 177 | NotifyState::Errno(e) => write!(f, "ERRNO={}", e), 178 | NotifyState::Fdname(ref s) => write!(f, "FDNAME={}", s), 179 | NotifyState::Fdstore => write!(f, "FDSTORE=1"), 180 | NotifyState::FdstoreRemove => write!(f, "FDSTOREREMOVE=1"), 181 | NotifyState::FdpollDisable => write!(f, "FDPOLL=0"), 182 | NotifyState::Mainpid(ref p) => write!(f, "MAINPID={}", p), 183 | NotifyState::Other(ref s) => write!(f, "{}", s), 184 | NotifyState::Ready => write!(f, "READY=1"), 185 | NotifyState::Reloading => write!(f, "RELOADING=1"), 186 | NotifyState::Status(ref s) => write!(f, "STATUS={}", s), 187 | NotifyState::Stopping => write!(f, "STOPPING=1"), 188 | NotifyState::Watchdog => write!(f, "WATCHDOG=1"), 189 | NotifyState::WatchdogUsec(u) => write!(f, "WATCHDOG_USEC={}", u), 190 | } 191 | } 192 | } 193 | 194 | /// Perform some basic sanity checks against state entries. 195 | fn sanity_check_state_entries(state: &[NotifyState]) -> Result<(), SdError> { 196 | for (index, entry) in state.iter().enumerate() { 197 | match entry { 198 | NotifyState::Fdname(ref name) => validate_fdname(name), 199 | _ => Ok(()), 200 | } 201 | .with_context(|| format!("invalid notify state entry #{}", index))?; 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | /// Validate an `FDNAME` according to systemd rules. 208 | /// 209 | /// The name may consist of arbitrary ASCII characters except control 210 | /// characters or ":". It may not be longer than 255 characters. 211 | fn validate_fdname(fdname: &str) -> Result<(), SdError> { 212 | if fdname.len() > 255 { 213 | return Err(format!("fdname '{}' longer than 255 characters", fdname).into()); 214 | } 215 | 216 | for c in fdname.chars() { 217 | if !c.is_ascii() || c == ':' || c.is_ascii_control() { 218 | return Err(format!("invalid character '{}' in fdname '{}'", c, fdname).into()); 219 | } 220 | } 221 | 222 | Ok(()) 223 | } 224 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | /// Library errors. 4 | #[derive(thiserror::Error, Debug)] 5 | #[error("libsystemd error: {msg}")] 6 | pub struct SdError { 7 | pub(crate) kind: ErrorKind, 8 | pub(crate) msg: String, 9 | } 10 | 11 | impl From<&str> for SdError { 12 | fn from(arg: &str) -> Self { 13 | Self { 14 | kind: ErrorKind::Generic, 15 | msg: arg.to_string(), 16 | } 17 | } 18 | } 19 | 20 | impl From for SdError { 21 | fn from(arg: String) -> Self { 22 | Self { 23 | kind: ErrorKind::Generic, 24 | msg: arg, 25 | } 26 | } 27 | } 28 | 29 | /// Markers for recoverable error kinds. 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub(crate) enum ErrorKind { 32 | Generic, 33 | SysusersUnknownType, 34 | } 35 | 36 | /// Context is similar to anyhow::Context, in that it provides a mechanism internally to adapt 37 | /// errors from systemd into SdError, while providing additional context in a readable manner. 38 | pub(crate) trait Context { 39 | /// Prepend the error with context. 40 | fn context(self, context: C) -> Result 41 | where 42 | C: Display + Send + Sync + 'static; 43 | 44 | /// Prepend the error with context that is lazily evaluated. 45 | fn with_context(self, f: F) -> Result 46 | where 47 | C: Display + Send + Sync + 'static, 48 | F: FnOnce() -> C; 49 | } 50 | 51 | impl Context for Result 52 | where 53 | E: std::error::Error + Send + Sync + 'static, 54 | { 55 | fn context(self, context: C) -> Result 56 | where 57 | C: Display + Send + Sync + 'static, 58 | { 59 | self.map_err(|e| format!("{}: {}", context, e).into()) 60 | } 61 | 62 | fn with_context(self, context: F) -> Result 63 | where 64 | C: Display + Send + Sync + 'static, 65 | F: FnOnce() -> C, 66 | { 67 | self.map_err(|e| format!("{}: {}", context(), e).into()) 68 | } 69 | } 70 | 71 | impl Context for Option { 72 | fn context(self, context: C) -> Result 73 | where 74 | C: Display + Send + Sync + 'static, 75 | { 76 | self.ok_or_else(|| format!("{}", context).into()) 77 | } 78 | 79 | fn with_context(self, context: F) -> Result 80 | where 81 | C: Display + Send + Sync + 'static, 82 | F: FnOnce() -> C, 83 | { 84 | self.ok_or_else(|| format!("{}", context()).into()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/id128.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Context, SdError}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::Write; 4 | use std::hash::Hash; 5 | use std::io::Read; 6 | use std::{fmt, fs}; 7 | use uuid::{Bytes, Uuid}; 8 | 9 | /// A 128-bits ID. 10 | #[derive(Clone, Copy, Hash, Eq, PartialEq, Deserialize, Serialize)] 11 | #[serde(transparent)] 12 | pub struct Id128 { 13 | #[serde(flatten, serialize_with = "Id128::ser_uuid")] 14 | uuid_v4: Uuid, 15 | } 16 | 17 | impl Id128 { 18 | /// Build an `Id128` from a slice of bytes. 19 | pub fn try_from_slice(bytes: &[u8]) -> Result { 20 | let uuid_v4 = Uuid::from_slice(bytes).context("failed to parse ID from bytes slice")?; 21 | 22 | // TODO(lucab): check for v4. 23 | Ok(Self { uuid_v4 }) 24 | } 25 | 26 | /// Build an `Id128` from 16 bytes 27 | pub const fn from_bytes(bytes: Bytes) -> Self { 28 | Self { 29 | uuid_v4: Uuid::from_bytes(bytes), 30 | } 31 | } 32 | 33 | /// Parse an `Id128` from string. 34 | pub fn parse_str(input: S) -> Result 35 | where 36 | S: AsRef, 37 | { 38 | let uuid_v4 = Uuid::parse_str(input.as_ref()).context("failed to parse ID from string")?; 39 | 40 | // TODO(lucab): check for v4. 41 | Ok(Self { uuid_v4 }) 42 | } 43 | 44 | /// Hash this ID with an application-specific ID. 45 | pub fn app_specific(&self, app: &Self) -> Result { 46 | use hmac::{Hmac, Mac}; 47 | use sha2::Sha256; 48 | 49 | let mut mac = Hmac::::new_from_slice(self.uuid_v4.as_bytes()) 50 | .map_err(|_| "failed to prepare HMAC")?; 51 | mac.update(app.uuid_v4.as_bytes()); 52 | let mut hashed = mac.finalize().into_bytes(); 53 | 54 | if hashed.len() != 32 { 55 | return Err("short hash".into()); 56 | }; 57 | 58 | // Set version to 4. 59 | hashed[6] = (hashed[6] & 0x0F) | 0x40; 60 | // Set variant to DCE. 61 | hashed[8] = (hashed[8] & 0x3F) | 0x80; 62 | 63 | Self::try_from_slice(&hashed[..16]) 64 | } 65 | 66 | /// Return this ID as a lowercase hexadecimal string, without dashes. 67 | pub fn lower_hex(&self) -> String { 68 | let mut hex = String::new(); 69 | for byte in self.uuid_v4.as_bytes() { 70 | write!(hex, "{byte:02x}").unwrap(); 71 | } 72 | hex 73 | } 74 | 75 | /// Return this ID as a lowercase hexadecimal string, with dashes. 76 | pub fn dashed_hex(&self) -> String { 77 | format!("{}", self.uuid_v4.hyphenated()) 78 | } 79 | 80 | /// Custom serialization (lower hex). 81 | fn ser_uuid(field: &Uuid, s: S) -> ::std::result::Result 82 | where 83 | S: ::serde::Serializer, 84 | { 85 | let mut hex = String::new(); 86 | for byte in field.as_bytes() { 87 | write!(hex, "{byte:02x}").unwrap(); 88 | } 89 | s.serialize_str(&hex) 90 | } 91 | } 92 | 93 | impl fmt::Debug for Id128 { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | write!(f, "{}", self.dashed_hex()) 96 | } 97 | } 98 | 99 | impl From for Id128 { 100 | fn from(uuid_v4: Uuid) -> Self { 101 | Self { uuid_v4 } 102 | } 103 | } 104 | 105 | /// Return this machine unique ID. 106 | pub fn get_machine() -> Result { 107 | let mut buf = String::new(); 108 | let mut fd = fs::File::open("/etc/machine-id").context("failed to open machine-id")?; 109 | fd.read_to_string(&mut buf) 110 | .context("failed to read machine-id")?; 111 | Id128::parse_str(buf.trim_end()) 112 | } 113 | 114 | /// Return this machine unique ID, hashed with an application-specific ID. 115 | pub fn get_machine_app_specific(app_id: &Id128) -> Result { 116 | let machine_id = get_machine()?; 117 | machine_id.app_specific(app_id) 118 | } 119 | 120 | /// Return the unique ID of this boot. 121 | pub fn get_boot() -> Result { 122 | let mut buf = String::new(); 123 | let mut fd = 124 | fs::File::open("/proc/sys/kernel/random/boot_id").context("failed to open boot_id")?; 125 | fd.read_to_string(&mut buf) 126 | .context("failed to read boot_id")?; 127 | Id128::parse_str(buf.trim_end()) 128 | } 129 | 130 | /// Return the unique ID of this boot, hashed with an application-specific ID. 131 | pub fn get_boot_app_specific(app_id: &Id128) -> Result { 132 | get_boot()?.app_specific(app_id) 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use super::*; 138 | 139 | #[test] 140 | fn basic_parse_str() { 141 | let input = "2e074e9b299c41a59923c51ae16f279b"; 142 | let id = Id128::parse_str(input).unwrap(); 143 | assert_eq!(id.lower_hex(), input); 144 | 145 | Id128::parse_str("").unwrap_err(); 146 | } 147 | 148 | #[test] 149 | fn basic_keyed_hash() { 150 | let input = "2e074e9b299c41a59923c51ae16f279b"; 151 | let machine_id = Id128::parse_str(input).unwrap(); 152 | assert_eq!(input, machine_id.lower_hex()); 153 | 154 | let key = "033b1b9b264441fcaa173e9e5bf35c5a"; 155 | let app_id = Id128::parse_str(key).unwrap(); 156 | assert_eq!(key, app_id.lower_hex()); 157 | 158 | let expected = "4d4a86c9c6644a479560ded5d19a30c5"; 159 | let hashed_id = Id128::parse_str(expected).unwrap(); 160 | 161 | let output = machine_id.app_specific(&app_id).unwrap(); 162 | assert_eq!(output, hashed_id); 163 | } 164 | 165 | #[test] 166 | fn basic_from_slice() { 167 | let input_str = "d86a4e9e4dca45c5bcd9846409bfa1ae"; 168 | let input = [ 169 | 0xd8, 0x6a, 0x4e, 0x9e, 0x4d, 0xca, 0x45, 0xc5, 0xbc, 0xd9, 0x84, 0x64, 0x09, 0xbf, 170 | 0xa1, 0xae, 171 | ]; 172 | let id = Id128::try_from_slice(&input).unwrap(); 173 | assert_eq!(input_str, id.lower_hex()); 174 | 175 | Id128::try_from_slice(&[]).unwrap_err(); 176 | } 177 | 178 | #[test] 179 | fn basic_from_bytes() { 180 | let input_str = "d86a4e9e4dca45c5bcd9846409bfa1ae"; 181 | let input = [ 182 | 0xd8, 0x6a, 0x4e, 0x9e, 0x4d, 0xca, 0x45, 0xc5, 0xbc, 0xd9, 0x84, 0x64, 0x09, 0xbf, 183 | 0xa1, 0xae, 184 | ]; 185 | let id = Id128::from_bytes(input); 186 | assert_eq!(input_str, id.lower_hex()); 187 | } 188 | 189 | #[test] 190 | fn basic_debug() { 191 | let input = "0b37f793-aeb9-4d67-99e1-6e678d86781f"; 192 | let id = Id128::parse_str(input).unwrap(); 193 | assert_eq!(id.dashed_hex(), input); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A pure-Rust client library to work with systemd. 2 | //! 3 | //! It provides support to interact with systemd components available 4 | //! on modern Linux systems. This crate is entirely implemented 5 | //! in Rust, and does not require an external libsystemd dynamic library. 6 | //! 7 | //! ```rust 8 | //! use libsystemd::daemon::{self, NotifyState}; 9 | //! 10 | //! fn notify_ready() -> bool { 11 | //! if !daemon::booted() { 12 | //! println!("Not running systemd, early exit."); 13 | //! return false; 14 | //! }; 15 | //! 16 | //! let sent = daemon::notify(true, &[NotifyState::Ready]).expect("notify failed"); 17 | //! if !sent { 18 | //! println!("Notification not sent!"); 19 | //! }; 20 | //! sent 21 | //! } 22 | //! ``` 23 | 24 | /// Interfaces for socket-activated services. 25 | pub mod activation; 26 | /// Helpers for securely passing potentially sensitive data to services. 27 | pub mod credentials; 28 | /// Interfaces for systemd-aware daemons. 29 | pub mod daemon; 30 | /// Error handling. 31 | pub mod errors; 32 | /// APIs for processing 128-bits IDs. 33 | pub mod id128; 34 | /// Helpers for logging to `systemd-journald`. 35 | pub mod logging; 36 | pub mod sysusers; 37 | /// Helpers for working with systemd units. 38 | pub mod unit; 39 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Context, SdError}; 2 | use nix::errno::Errno; 3 | use nix::fcntl::*; 4 | use nix::sys::memfd::MemFdCreateFlag; 5 | use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags, UnixAddr}; 6 | use nix::sys::stat::{fstat, FileStat}; 7 | use once_cell::sync::OnceCell; 8 | use std::collections::HashMap; 9 | use std::ffi::{CStr, CString, OsStr}; 10 | use std::fs::File; 11 | use std::io::prelude::*; 12 | use std::os::unix::io::AsRawFd; 13 | use std::os::unix::net::UnixDatagram; 14 | use std::os::unix::prelude::AsFd; 15 | use std::os::unix::prelude::FromRawFd; 16 | use std::os::unix::prelude::RawFd; 17 | use std::str::FromStr; 18 | 19 | /// Default path of the systemd-journald `AF_UNIX` datagram socket. 20 | pub static SD_JOURNAL_SOCK_PATH: &str = "/run/systemd/journal/socket"; 21 | 22 | /// The shared socket to journald. 23 | static SD_SOCK: OnceCell = OnceCell::new(); 24 | 25 | /// Well-known field names. Their validity is covered in tests. 26 | const PRIORITY: ValidField = ValidField::unchecked("PRIORITY"); 27 | const MESSAGE: ValidField = ValidField::unchecked("MESSAGE"); 28 | 29 | /// Log priority values. 30 | /// 31 | /// See `man 3 syslog`. 32 | #[derive(Clone, Copy, Debug)] 33 | #[repr(u8)] 34 | pub enum Priority { 35 | /// System is unusable. 36 | Emergency = 0, 37 | /// Action must be taken immediately. 38 | Alert, 39 | /// Critical condition, 40 | Critical, 41 | /// Error condition. 42 | Error, 43 | /// Warning condition. 44 | Warning, 45 | /// Normal, but significant, condition. 46 | Notice, 47 | /// Informational message. 48 | Info, 49 | /// Debug message. 50 | Debug, 51 | } 52 | 53 | impl std::convert::From for u8 { 54 | fn from(p: Priority) -> Self { 55 | match p { 56 | Priority::Emergency => 0, 57 | Priority::Alert => 1, 58 | Priority::Critical => 2, 59 | Priority::Error => 3, 60 | Priority::Warning => 4, 61 | Priority::Notice => 5, 62 | Priority::Info => 6, 63 | Priority::Debug => 7, 64 | } 65 | } 66 | } 67 | 68 | impl Priority { 69 | fn numeric_level(&self) -> &str { 70 | match self { 71 | Priority::Emergency => "0", 72 | Priority::Alert => "1", 73 | Priority::Critical => "2", 74 | Priority::Error => "3", 75 | Priority::Warning => "4", 76 | Priority::Notice => "5", 77 | Priority::Info => "6", 78 | Priority::Debug => "7", 79 | } 80 | } 81 | } 82 | 83 | #[inline(always)] 84 | fn is_valid_char(c: char) -> bool { 85 | c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_' 86 | } 87 | 88 | /// The variable name must be in uppercase and consist only of characters, 89 | /// numbers and underscores, and may not begin with an underscore. 90 | /// 91 | /// See 92 | /// for the reference implementation of journal_field_valid. 93 | fn is_valid_field(input: &str) -> bool { 94 | // journald doesn't allow empty fields or fields with more than 64 bytes 95 | if input.is_empty() || 64 < input.len() { 96 | return false; 97 | } 98 | 99 | // Fields starting with underscores are protected by journald 100 | if input.starts_with('_') { 101 | return false; 102 | } 103 | 104 | // Journald doesn't allow fields to start with digits 105 | if input.starts_with(|c: char| c.is_ascii_digit()) { 106 | return false; 107 | } 108 | 109 | input.chars().all(is_valid_char) 110 | } 111 | 112 | /// A helper for functions that want to take fields as parameters that have already been validated. 113 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 114 | struct ValidField<'a> { 115 | field: &'a str, 116 | } 117 | 118 | impl<'a> ValidField<'a> { 119 | /// The field value is checked by [[`is_valid_field`]] and a ValidField is returned if true. 120 | fn validate(field: &'a str) -> Option { 121 | if is_valid_field(field) { 122 | Some(Self { field }) 123 | } else { 124 | None 125 | } 126 | } 127 | 128 | /// Allows for the construction of a potentially invalid ValidField. 129 | /// 130 | /// Since [[`ValidField::is_valid_field`]] cannot reasonably be const, this allows for the 131 | /// construction of known valid field names at compile time. It's expected that the validity is 132 | /// confirmed in tests by [[`ValidField::validate_unchecked`]]. 133 | const fn unchecked(field: &'a str) -> Self { 134 | Self { field } 135 | } 136 | 137 | /// Converts to a byte slice. 138 | fn as_bytes(&self) -> &'a [u8] { 139 | self.field.as_bytes() 140 | } 141 | 142 | /// Returns the length in bytes. 143 | fn len(&self) -> usize { 144 | self.field.len() 145 | } 146 | 147 | /// Validates an object created using [[`ValidField::unchecked`]]. 148 | /// 149 | /// Every unchecked field should have a corresponding test that calls this. 150 | #[cfg(test)] 151 | fn validate_unchecked(&self) -> bool { 152 | is_valid_field(self.field) 153 | } 154 | } 155 | 156 | /// Add `field` and `payload` to journal fields `data` with explicit length encoding. 157 | /// 158 | /// Write 159 | /// 160 | /// 1. the field name, 161 | /// 2. an ASCII newline, 162 | /// 3. the payload size as LE encoded 64 bit integer, 163 | /// 4. the payload, and 164 | /// 5. a final ASCII newline 165 | /// 166 | /// to `data`. 167 | /// 168 | /// See for details. 169 | fn add_field_and_payload_explicit_length(data: &mut Vec, field: ValidField, payload: &str) { 170 | let encoded_len = (payload.len() as u64).to_le_bytes(); 171 | 172 | // Bump the capacity to avoid multiple allocations during the extend/push calls. The 2 is for 173 | // the newline characters. 174 | data.reserve(field.len() + encoded_len.len() + payload.len() + 2); 175 | 176 | data.extend(field.as_bytes()); 177 | data.push(b'\n'); 178 | data.extend(encoded_len); 179 | data.extend(payload.as_bytes()); 180 | data.push(b'\n'); 181 | } 182 | 183 | /// Add a journal `field` and its `payload` to journal fields `data` with appropriate encoding. 184 | /// 185 | /// If `payload` does not contain a newline character use the simple journal field encoding, and 186 | /// write the field name and the payload separated by `=` and suffixed by a final new line. 187 | /// 188 | /// Otherwise encode the payload length explicitly with [[`add_field_and_payload_explicit_length`]]. 189 | /// 190 | /// See for details. 191 | fn add_field_and_payload(data: &mut Vec, field: ValidField, payload: &str) { 192 | if payload.contains('\n') { 193 | add_field_and_payload_explicit_length(data, field, payload); 194 | } else { 195 | // If payload doesn't contain an newline directly write the field name and the payload. Bump 196 | // the capacity to avoid multiple allocations during extend/push calls. The 2 is for the 197 | // two pushed bytes. 198 | data.reserve(field.len() + payload.len() + 2); 199 | 200 | data.extend(field.as_bytes()); 201 | data.push(b'='); 202 | data.extend(payload.as_bytes()); 203 | data.push(b'\n'); 204 | } 205 | } 206 | 207 | /// Send a message with structured properties to the journal. 208 | /// 209 | /// The PRIORITY or MESSAGE fields from the vars iterator are always ignored in favour of the priority and message arguments. 210 | pub fn journal_send( 211 | priority: Priority, 212 | msg: &str, 213 | vars: impl Iterator, 214 | ) -> Result<(), SdError> 215 | where 216 | K: AsRef, 217 | V: AsRef, 218 | { 219 | let sock = SD_SOCK 220 | .get_or_try_init(UnixDatagram::unbound) 221 | .context("failed to open datagram socket")?; 222 | 223 | let mut data = Vec::new(); 224 | add_field_and_payload(&mut data, PRIORITY, priority.numeric_level()); 225 | add_field_and_payload(&mut data, MESSAGE, msg); 226 | for (ref k, ref v) in vars { 227 | if let Some(field) = ValidField::validate(k.as_ref()) { 228 | if field != PRIORITY && field != MESSAGE { 229 | add_field_and_payload(&mut data, field, v.as_ref()) 230 | } 231 | } 232 | } 233 | 234 | // Message sending logic: 235 | // * fast path: data within datagram body. 236 | // * slow path: data in a sealed memfd, which is sent as an FD in ancillary data. 237 | // 238 | // Maximum data size is system dependent, thus this always tries the fast path and 239 | // falls back to the slow path if the former fails with `EMSGSIZE`. 240 | match sock.send_to(&data, SD_JOURNAL_SOCK_PATH) { 241 | Ok(x) => Ok(x), 242 | // `EMSGSIZE` (errno code 90) means the message was too long for a UNIX socket, 243 | Err(ref err) if err.raw_os_error() == Some(90) => { 244 | send_memfd_payload(sock, &data).context("sending with memfd failed") 245 | } 246 | Err(e) => Err(e).context("send_to failed"), 247 | } 248 | .map(|_| ()) 249 | .with_context(|| format!("failed to print to journal at '{}'", SD_JOURNAL_SOCK_PATH)) 250 | } 251 | 252 | /// Print a message to the journal with the given priority. 253 | pub fn journal_print(priority: Priority, msg: &str) -> Result<(), SdError> { 254 | let map: HashMap<&str, &str> = HashMap::new(); 255 | journal_send(priority, msg, map.iter()) 256 | } 257 | 258 | // Implementation of memfd_create() using a syscall instead of calling the libc 259 | // function. 260 | // 261 | // The memfd_create() function is only available in glibc >= 2.27 (and other 262 | // libc implementations). To support older versions of glibc, we perform a raw 263 | // syscall (this will fail in Linux < 3.17, where the syscall was not 264 | // available). 265 | // 266 | // nix::sys::memfd::memfd_create chooses at compile time between calling libc 267 | // and performing a syscall, since platforms such as Android and uclibc don't 268 | // have memfd_create() in libc. Here we always use the syscall. 269 | fn memfd_create(name: &CStr, flags: MemFdCreateFlag) -> Result { 270 | unsafe { 271 | let res = libc::syscall(libc::SYS_memfd_create, name.as_ptr(), flags.bits()); 272 | Errno::result(res).map(|r| { 273 | // SAFETY: `memfd_create` just returned this FD, so we own it now. 274 | File::from_raw_fd(r as RawFd) 275 | }) 276 | } 277 | } 278 | 279 | /// Send an overlarge payload to systemd-journald socket. 280 | /// 281 | /// This is a slow-path for sending a large payload that could not otherwise fit 282 | /// in a UNIX datagram. Payload is thus written to a memfd, which is sent as ancillary 283 | /// data. 284 | fn send_memfd_payload(sock: &UnixDatagram, data: &[u8]) -> Result { 285 | let memfd = { 286 | let fdname = &CString::new("libsystemd-rs-logging").context("unable to create cstring")?; 287 | let mut file = memfd_create(fdname, MemFdCreateFlag::MFD_ALLOW_SEALING) 288 | .context("unable to create memfd")?; 289 | 290 | file.write_all(data).context("failed to write to memfd")?; 291 | file 292 | }; 293 | 294 | // Seal the memfd, so that journald knows it can safely mmap/read it. 295 | fcntl(memfd.as_raw_fd(), FcntlArg::F_ADD_SEALS(SealFlag::all())) 296 | .context("unable to seal memfd")?; 297 | 298 | let fds = &[memfd.as_raw_fd()]; 299 | let ancillary = [ControlMessage::ScmRights(fds)]; 300 | let path = UnixAddr::new(SD_JOURNAL_SOCK_PATH).context("unable to create new unix address")?; 301 | sendmsg( 302 | sock.as_raw_fd(), 303 | &[], 304 | &ancillary, 305 | MsgFlags::empty(), 306 | Some(&path), 307 | ) 308 | .context("sendmsg failed")?; 309 | 310 | // Close our side of the memfd after we send it to systemd. 311 | drop(memfd); 312 | 313 | Ok(data.len()) 314 | } 315 | 316 | /// A systemd journal stream. 317 | #[derive(Debug, Eq, PartialEq)] 318 | pub struct JournalStream { 319 | /// The device number of the journal stream. 320 | device: libc::dev_t, 321 | /// The inode number of the journal stream. 322 | inode: libc::ino_t, 323 | } 324 | 325 | impl JournalStream { 326 | /// Parse the device and inode number from a systemd journal stream specification. 327 | /// 328 | /// See also [`JournalStream::from_env()`]. 329 | pub(crate) fn parse>(value: S) -> Result { 330 | let s = value.as_ref().to_str().with_context(|| { 331 | format!( 332 | "Failed to parse journal stream: Value {:?} not UTF-8 encoded", 333 | value.as_ref() 334 | ) 335 | })?; 336 | let (device_s, inode_s) = 337 | s.find(':') 338 | .map(|i| (&s[..i], &s[i + 1..])) 339 | .with_context(|| { 340 | format!( 341 | "Failed to parse journal stream: Missing separator ':' in value '{}'", 342 | s 343 | ) 344 | })?; 345 | let device = libc::dev_t::from_str(device_s).with_context(|| { 346 | format!( 347 | "Failed to parse journal stream: Device part is not a number '{}'", 348 | device_s 349 | ) 350 | })?; 351 | let inode = libc::ino_t::from_str(inode_s).with_context(|| { 352 | format!( 353 | "Failed to parse journal stream: Inode part is not a number '{}'", 354 | inode_s 355 | ) 356 | })?; 357 | Ok(JournalStream { device, inode }) 358 | } 359 | 360 | /// Parse the device and inode number of the systemd journal stream denoted by the given environment variable. 361 | pub(crate) fn from_env_impl>(key: S) -> Result { 362 | Self::parse(std::env::var_os(&key).with_context(|| { 363 | format!( 364 | "Failed to parse journal stream: Environment variable {:?} unset", 365 | key.as_ref() 366 | ) 367 | })?) 368 | } 369 | 370 | /// Parse the device and inode number of the systemd journal stream denoted by the default `$JOURNAL_STREAM` variable. 371 | /// 372 | /// These values are extracted from `$JOURNAL_STREAM`, and consists of the device and inode 373 | /// numbers of the systemd journal stream, separated by `:`. 374 | pub fn from_env() -> Result { 375 | Self::from_env_impl("JOURNAL_STREAM") 376 | } 377 | 378 | /// Get the journal stream that would correspond to the given file descriptor. 379 | /// 380 | /// Return a journal stream struct containing the device and inode number of the given file descriptor. 381 | pub fn from_fd(fd: F) -> std::io::Result { 382 | fstat(fd.as_fd().as_raw_fd()) 383 | .map_err(Into::into) 384 | .map(Into::into) 385 | } 386 | } 387 | 388 | impl From for JournalStream { 389 | fn from(stat: FileStat) -> Self { 390 | Self { 391 | device: stat.st_dev, 392 | inode: stat.st_ino, 393 | } 394 | } 395 | } 396 | 397 | /// Whether this process can be automatically upgraded to native journal logging. 398 | /// 399 | /// Inspects the `$JOURNAL_STREAM` environment variable and compares the device and inode 400 | /// numbers in this variable against the stderr file descriptor. 401 | /// 402 | /// Return `true` if they match, and `false` otherwise (or in case of any IO error). 403 | /// 404 | /// For services normally logging to stderr but also supporting systemd-style structured 405 | /// logging, it is recommended to perform this check and then upgrade to the native systemd 406 | /// journal protocol if possible. 407 | /// 408 | /// See section “Automatic Protocol Upgrading” in [systemd documentation][1] for more information. 409 | /// 410 | /// [1]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading 411 | pub fn connected_to_journal() -> bool { 412 | JournalStream::from_env().map_or(false, |env_stream| { 413 | JournalStream::from_fd(std::io::stderr()).map_or(false, |o| o == env_stream) 414 | }) 415 | } 416 | 417 | #[cfg(test)] 418 | mod tests { 419 | use super::*; 420 | 421 | fn ensure_journald_socket() -> bool { 422 | match std::fs::metadata(SD_JOURNAL_SOCK_PATH) { 423 | Ok(_) => true, 424 | Err(_) => { 425 | eprintln!( 426 | "skipped, journald socket not found at '{}'", 427 | SD_JOURNAL_SOCK_PATH 428 | ); 429 | false 430 | } 431 | } 432 | } 433 | 434 | const FOO: ValidField = ValidField::unchecked("FOO"); 435 | 436 | #[test] 437 | fn test_priority_numeric_level_matches_to_string() { 438 | let priorities = [ 439 | Priority::Emergency, 440 | Priority::Alert, 441 | Priority::Critical, 442 | Priority::Error, 443 | Priority::Warning, 444 | Priority::Notice, 445 | Priority::Info, 446 | Priority::Debug, 447 | ]; 448 | 449 | for priority in priorities.into_iter() { 450 | assert_eq!(&(u8::from(priority)).to_string(), priority.numeric_level()); 451 | } 452 | } 453 | 454 | #[test] 455 | fn test_journal_print_simple() { 456 | if !ensure_journald_socket() { 457 | return; 458 | } 459 | 460 | journal_print(Priority::Info, "TEST LOG!").unwrap(); 461 | } 462 | 463 | #[test] 464 | fn test_journal_print_large_buffer() { 465 | if !ensure_journald_socket() { 466 | return; 467 | } 468 | 469 | let data = "A".repeat(212995); 470 | journal_print(Priority::Debug, &data).unwrap(); 471 | } 472 | 473 | #[test] 474 | fn test_journal_send_simple() { 475 | if !ensure_journald_socket() { 476 | return; 477 | } 478 | 479 | let mut map: HashMap<&str, &str> = HashMap::new(); 480 | map.insert("TEST_JOURNALD_LOG1", "foo"); 481 | map.insert("TEST_JOURNALD_LOG2", "bar"); 482 | journal_send(Priority::Info, "Test Journald Log", map.iter()).unwrap() 483 | } 484 | #[test] 485 | fn test_journal_skip_fields() { 486 | if !ensure_journald_socket() { 487 | return; 488 | } 489 | 490 | let mut map: HashMap<&str, &str> = HashMap::new(); 491 | let priority = format!("{}", u8::from(Priority::Warning)); 492 | map.insert("TEST_JOURNALD_LOG3", "result"); 493 | map.insert("PRIORITY", &priority); 494 | map.insert("MESSAGE", "Duplicate value"); 495 | journal_send(Priority::Info, "Test Skip Fields", map.iter()).unwrap() 496 | } 497 | 498 | #[test] 499 | fn test_predeclared_fields_are_valid() { 500 | assert!(PRIORITY.validate_unchecked()); 501 | assert!(MESSAGE.validate_unchecked()); 502 | assert!(FOO.validate_unchecked()); 503 | } 504 | 505 | #[test] 506 | fn test_is_valid_field_lowercase_invalid() { 507 | let field = "test"; 508 | assert!(ValidField::validate(field).is_none()); 509 | } 510 | 511 | #[test] 512 | fn test_is_valid_field_uppercase_non_ascii_invalid() { 513 | let field = "TRÖT"; 514 | assert!(ValidField::validate(field).is_none()); 515 | } 516 | 517 | #[test] 518 | fn test_is_valid_field_uppercase_valid() { 519 | let field = "TEST"; 520 | assert_eq!( 521 | ValidField::validate(field).unwrap().as_bytes(), 522 | field.as_bytes() 523 | ); 524 | } 525 | 526 | #[test] 527 | fn test_is_valid_field_uppercase_non_alpha_invalid() { 528 | let field = "TE!ST"; 529 | assert!(ValidField::validate(field).is_none()); 530 | } 531 | 532 | #[test] 533 | fn test_is_valid_field_uppercase_leading_underscore_invalid() { 534 | let field = "_TEST"; 535 | assert!(ValidField::validate(field).is_none()); 536 | } 537 | 538 | #[test] 539 | fn test_is_valid_field_uppercase_leading_digit_invalid() { 540 | let field = "1TEST"; 541 | assert!(ValidField::validate(field).is_none()); 542 | } 543 | 544 | #[test] 545 | fn add_field_and_payload_explicit_length_simple() { 546 | let mut data = Vec::new(); 547 | add_field_and_payload_explicit_length(&mut data, FOO, "BAR"); 548 | assert_eq!( 549 | data, 550 | vec![b'F', b'O', b'O', b'\n', 3, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n'] 551 | ); 552 | } 553 | 554 | #[test] 555 | fn add_field_and_payload_explicit_length_internal_newline() { 556 | let mut data = Vec::new(); 557 | add_field_and_payload_explicit_length(&mut data, FOO, "B\nAR"); 558 | assert_eq!( 559 | data, 560 | vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n'] 561 | ); 562 | } 563 | 564 | #[test] 565 | fn add_field_and_payload_explicit_length_trailing_newline() { 566 | let mut data = Vec::new(); 567 | add_field_and_payload_explicit_length(&mut data, FOO, "BAR\n"); 568 | assert_eq!( 569 | data, 570 | vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n'] 571 | ); 572 | } 573 | 574 | #[test] 575 | fn add_field_and_payload_simple() { 576 | let mut data = Vec::new(); 577 | add_field_and_payload(&mut data, FOO, "BAR"); 578 | assert_eq!(data, "FOO=BAR\n".as_bytes()); 579 | } 580 | 581 | #[test] 582 | fn add_field_and_payload_internal_newline() { 583 | let mut data = Vec::new(); 584 | add_field_and_payload(&mut data, FOO, "B\nAR"); 585 | assert_eq!( 586 | data, 587 | vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n'] 588 | ); 589 | } 590 | 591 | #[test] 592 | fn add_field_and_payload_trailing_newline() { 593 | let mut data = Vec::new(); 594 | add_field_and_payload(&mut data, FOO, "BAR\n"); 595 | assert_eq!( 596 | data, 597 | vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n'] 598 | ); 599 | } 600 | 601 | #[test] 602 | fn journal_stream_from_fd_does_not_claim_ownership_of_fd() { 603 | // Just get hold of some open file which we know exists and can be read by the current user. 604 | let file = File::open(file!()).unwrap(); 605 | let journal_stream = JournalStream::from_fd(&file).unwrap(); 606 | assert_ne!(journal_stream.device, 0); 607 | assert_ne!(journal_stream.inode, 0); 608 | // Easy way to check if a file descriptor is still open, see https://stackoverflow.com/a/12340730/355252 609 | let result = fcntl(file.as_raw_fd(), FcntlArg::F_GETFD); 610 | assert!( 611 | result.is_ok(), 612 | "File descriptor not valid anymore after JournalStream::from_fd: {:?}", 613 | result, 614 | ); 615 | } 616 | } 617 | -------------------------------------------------------------------------------- /src/sysusers/format.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::fmt::{self, Display}; 3 | 4 | impl Display for SysusersEntry { 5 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 6 | match self { 7 | SysusersEntry::AddRange(v) => write!(f, "{}", v), 8 | SysusersEntry::AddUserToGroup(v) => write!(f, "{}", v), 9 | SysusersEntry::CreateGroup(v) => write!(f, "{}", v), 10 | SysusersEntry::CreateUserAndGroup(v) => write!(f, "{}", v), 11 | } 12 | } 13 | } 14 | 15 | impl Display for AddRange { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | write!(f, "r - {}-{} - - -", self.from, self.to) 18 | } 19 | } 20 | 21 | impl Display for AddUserToGroup { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!(f, "m {} {} - - -", self.username, self.groupname,) 24 | } 25 | } 26 | 27 | impl Display for CreateGroup { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | write!(f, "g {} {} - - -", self.groupname, self.gid,) 30 | } 31 | } 32 | 33 | impl Display for CreateUserAndGroup { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | write!( 36 | f, 37 | "u {} {} \"{}\" {} {}", 38 | self.name, 39 | self.id, 40 | self.gecos, 41 | self.home_dir 42 | .as_deref() 43 | .map(|p| p.to_string_lossy()) 44 | .unwrap_or(Cow::Borrowed("-")), 45 | self.shell 46 | .as_deref() 47 | .map(|p| p.to_string_lossy()) 48 | .unwrap_or(Cow::Borrowed("-")), 49 | ) 50 | } 51 | } 52 | 53 | impl Display for IdOrPath { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | match self { 56 | IdOrPath::Id(i) => write!(f, "{}", i), 57 | IdOrPath::UidGid((u, g)) => write!(f, "{}:{}", u, g), 58 | IdOrPath::UidGroupname((u, g)) => write!(f, "{}:{}", u, g), 59 | IdOrPath::Path(p) => write!(f, "{}", p.display()), 60 | IdOrPath::Automatic => write!(f, "-",), 61 | } 62 | } 63 | } 64 | 65 | impl Display for GidOrPath { 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | match self { 68 | GidOrPath::Gid(g) => write!(f, "{}", g), 69 | GidOrPath::Path(p) => write!(f, "{}", p.display()), 70 | GidOrPath::Automatic => write!(f, "-",), 71 | } 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod test { 77 | use super::*; 78 | 79 | #[test] 80 | fn test_formatters() { 81 | { 82 | let type_u = 83 | CreateUserAndGroup::new("foo0".to_string(), "test".to_string(), None, None) 84 | .unwrap(); 85 | let expected = r#"u foo0 - "test" - -"#; 86 | assert_eq!(type_u.to_string(), expected); 87 | } 88 | { 89 | let type_g = CreateGroup::new("foo1".to_string()).unwrap(); 90 | let expected = r#"g foo1 - - - -"#; 91 | assert_eq!(type_g.to_string(), expected); 92 | } 93 | { 94 | let type_r = AddRange::new(10, 20).unwrap(); 95 | let expected = r#"r - 10-20 - - -"#; 96 | assert_eq!(type_r.to_string(), expected); 97 | } 98 | { 99 | let type_m = AddUserToGroup::new("foo3".to_string(), "bar".to_string()).unwrap(); 100 | let expected = r#"m foo3 bar - - -"#; 101 | assert_eq!(type_m.to_string(), expected); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/sysusers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for working with `sysusers.d` configuration files. 2 | //! 3 | //! For the complete documentation see 4 | //! . 5 | //! 6 | //! ## Example 7 | //! 8 | //! ```rust 9 | //! # fn doctest_parse() -> Result<(), libsystemd::errors::SdError> { 10 | //! use libsystemd::sysusers; 11 | //! 12 | //! let config_fragment = r#" 13 | //! #Type Name ID GECOS Home directory Shell 14 | //! u httpd 404 "HTTP User" 15 | //! u _authd /usr/bin/authd "Authorization user" 16 | //! u postgres - "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb 17 | //! g input - - 18 | //! m _authd input 19 | //! u root 0 "Superuser" /root /bin/zsh 20 | //! r - 500-900 21 | //! "#; 22 | //! 23 | //! let mut reader = config_fragment.as_bytes(); 24 | //! let entries = sysusers::parse_from_reader(&mut reader)?; 25 | //! assert_eq!(entries.len(), 7); 26 | //! 27 | //! let users_and_groups: Vec<_> = entries 28 | //! .into_iter() 29 | //! .filter_map(|v| { 30 | //! match v.type_signature() { 31 | //! "u" | "g" => Some(v.name().to_string()), 32 | //! _ => None, 33 | //! } 34 | //! }) 35 | //! .collect(); 36 | //! assert_eq!(users_and_groups, vec!["httpd", "_authd", "postgres", "input", "root"]); 37 | //! # Ok(()) 38 | //! # } 39 | //! # doctest_parse().unwrap(); 40 | //! ``` 41 | 42 | pub(crate) use self::serialization::SysusersData; 43 | use crate::errors::{Context, SdError}; 44 | pub use parse::parse_from_reader; 45 | use serde::{Deserialize, Serialize}; 46 | use std::borrow::Cow; 47 | use std::io::BufRead; 48 | use std::path::PathBuf; 49 | use std::str::FromStr; 50 | 51 | mod format; 52 | mod parse; 53 | mod serialization; 54 | 55 | /// Single entry in `sysusers.d` configuration format. 56 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 57 | #[serde(untagged)] 58 | pub enum SysusersEntry { 59 | AddRange(AddRange), 60 | AddUserToGroup(AddUserToGroup), 61 | CreateGroup(CreateGroup), 62 | CreateUserAndGroup(CreateUserAndGroup), 63 | } 64 | 65 | impl SysusersEntry { 66 | /// Return the single-character signature for the "Type" field of this entry. 67 | pub fn type_signature(&self) -> &str { 68 | match self { 69 | SysusersEntry::AddRange(v) => v.type_signature(), 70 | SysusersEntry::AddUserToGroup(v) => v.type_signature(), 71 | SysusersEntry::CreateGroup(v) => v.type_signature(), 72 | SysusersEntry::CreateUserAndGroup(v) => v.type_signature(), 73 | } 74 | } 75 | 76 | /// Return the value for the "Name" field of this entry. 77 | pub fn name(&self) -> &str { 78 | match self { 79 | SysusersEntry::AddRange(_) => "-", 80 | SysusersEntry::AddUserToGroup(v) => &v.username, 81 | SysusersEntry::CreateGroup(v) => &v.groupname, 82 | SysusersEntry::CreateUserAndGroup(v) => &v.name, 83 | } 84 | } 85 | } 86 | 87 | /// Sysusers entry of type `r`. 88 | #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] 89 | #[serde(try_from = "SysusersData")] 90 | pub struct AddRange { 91 | pub(crate) from: u32, 92 | pub(crate) to: u32, 93 | } 94 | 95 | impl AddRange { 96 | /// Create a new `AddRange` entry. 97 | pub fn new(from: u32, to: u32) -> Result { 98 | Ok(Self { from, to }) 99 | } 100 | 101 | /// Return the single-character signature for the "Type" field of this entry. 102 | pub fn type_signature(&self) -> &str { 103 | "r" 104 | } 105 | 106 | /// Return the lower end for the range of this entry. 107 | pub fn from(&self) -> u32 { 108 | self.from 109 | } 110 | 111 | /// Return the upper end for the range of this entry. 112 | pub fn to(&self) -> u32 { 113 | self.to 114 | } 115 | 116 | pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { 117 | SysusersEntry::AddRange(self) 118 | } 119 | } 120 | 121 | /// Sysusers entry of type `m`. 122 | #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] 123 | #[serde(try_from = "SysusersData")] 124 | pub struct AddUserToGroup { 125 | pub(crate) username: String, 126 | pub(crate) groupname: String, 127 | } 128 | 129 | impl AddUserToGroup { 130 | /// Create a new `AddUserToGroup` entry. 131 | pub fn new(username: String, groupname: String) -> Result { 132 | validate_name_strict(&username)?; 133 | validate_name_strict(&groupname)?; 134 | Ok(Self { 135 | username, 136 | groupname, 137 | }) 138 | } 139 | 140 | /// Return the single-character signature for the "Type" field of this entry. 141 | pub fn type_signature(&self) -> &str { 142 | "m" 143 | } 144 | 145 | /// Return the user name ("Name" field) of this entry. 146 | pub fn username(&self) -> &str { 147 | &self.username 148 | } 149 | 150 | /// Return the group name ("ID" field) of this entry. 151 | pub fn groupname(&self) -> &str { 152 | &self.groupname 153 | } 154 | 155 | pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { 156 | SysusersEntry::AddUserToGroup(self) 157 | } 158 | } 159 | 160 | /// Sysusers entry of type `g`. 161 | #[derive(Clone, Debug, PartialEq, Deserialize)] 162 | #[serde(try_from = "SysusersData")] 163 | pub struct CreateGroup { 164 | pub(crate) groupname: String, 165 | pub(crate) gid: GidOrPath, 166 | } 167 | 168 | impl CreateGroup { 169 | /// Create a new `CreateGroup` entry. 170 | pub fn new(groupname: String) -> Result { 171 | Self::impl_new(groupname, GidOrPath::Automatic) 172 | } 173 | 174 | /// Create a new `CreateGroup` entry, using a numeric ID. 175 | pub fn new_with_gid(groupname: String, gid: u32) -> Result { 176 | Self::impl_new(groupname, GidOrPath::Gid(gid)) 177 | } 178 | 179 | /// Create a new `CreateGroup` entry, using a filepath reference. 180 | pub fn new_with_path(groupname: String, path: PathBuf) -> Result { 181 | Self::impl_new(groupname, GidOrPath::Path(path)) 182 | } 183 | 184 | pub(crate) fn impl_new(groupname: String, gid: GidOrPath) -> Result { 185 | validate_name_strict(&groupname)?; 186 | Ok(Self { groupname, gid }) 187 | } 188 | 189 | /// Return the single-character signature for the "Type" field of this entry. 190 | pub fn type_signature(&self) -> &str { 191 | "g" 192 | } 193 | 194 | /// Return the group name ("Name" field) of this entry. 195 | pub fn groupname(&self) -> &str { 196 | &self.groupname 197 | } 198 | 199 | /// Return whether GID is dynamically allocated at runtime. 200 | pub fn has_dynamic_gid(&self) -> bool { 201 | matches!(self.gid, GidOrPath::Automatic) 202 | } 203 | 204 | /// Return the group identifier (GID) of this entry, if statically set. 205 | pub fn static_gid(&self) -> Option { 206 | match self.gid { 207 | GidOrPath::Gid(n) => Some(n), 208 | _ => None, 209 | } 210 | } 211 | 212 | pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { 213 | SysusersEntry::CreateGroup(self) 214 | } 215 | } 216 | 217 | /// Sysusers entry of type `u`. 218 | #[derive(Clone, Debug, PartialEq, Deserialize)] 219 | #[serde(try_from = "SysusersData")] 220 | pub struct CreateUserAndGroup { 221 | pub(crate) name: String, 222 | pub(crate) id: IdOrPath, 223 | pub(crate) gecos: String, 224 | pub(crate) home_dir: Option, 225 | pub(crate) shell: Option, 226 | } 227 | 228 | impl CreateUserAndGroup { 229 | /// Create a new `CreateUserAndGroup` entry, using a filepath reference. 230 | pub fn new( 231 | name: String, 232 | gecos: String, 233 | home_dir: Option, 234 | shell: Option, 235 | ) -> Result { 236 | Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Automatic) 237 | } 238 | 239 | /// Create a new `CreateUserAndrGroup` entry, using a numeric ID. 240 | pub fn new_with_id( 241 | name: String, 242 | id: u32, 243 | gecos: String, 244 | home_dir: Option, 245 | shell: Option, 246 | ) -> Result { 247 | Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Id(id)) 248 | } 249 | 250 | /// Create a new `CreateUserAndGroup` entry, using a UID and a GID. 251 | pub fn new_with_uid_gid( 252 | name: String, 253 | uid: u32, 254 | gid: u32, 255 | gecos: String, 256 | home_dir: Option, 257 | shell: Option, 258 | ) -> Result { 259 | Self::impl_new(name, gecos, home_dir, shell, IdOrPath::UidGid((uid, gid))) 260 | } 261 | 262 | /// Create a new `CreateUserAndGroup` entry, using a UID and a groupname. 263 | pub fn new_with_uid_groupname( 264 | name: String, 265 | uid: u32, 266 | groupname: String, 267 | gecos: String, 268 | home_dir: Option, 269 | shell: Option, 270 | ) -> Result { 271 | validate_name_strict(&groupname)?; 272 | Self::impl_new( 273 | name, 274 | gecos, 275 | home_dir, 276 | shell, 277 | IdOrPath::UidGroupname((uid, groupname)), 278 | ) 279 | } 280 | 281 | /// Create a new `CreateUserAndGroup` entry, using a filepath reference. 282 | pub fn new_with_path( 283 | name: String, 284 | path: PathBuf, 285 | gecos: String, 286 | home_dir: Option, 287 | shell: Option, 288 | ) -> Result { 289 | Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Path(path)) 290 | } 291 | 292 | pub(crate) fn impl_new( 293 | name: String, 294 | gecos: String, 295 | home_dir: Option, 296 | shell: Option, 297 | id: IdOrPath, 298 | ) -> Result { 299 | validate_name_strict(&name)?; 300 | Ok(Self { 301 | name, 302 | id, 303 | gecos, 304 | home_dir, 305 | shell, 306 | }) 307 | } 308 | 309 | /// Return the single-character signature for the "Type" field of this entry. 310 | pub fn type_signature(&self) -> &str { 311 | "u" 312 | } 313 | 314 | /// Return the user and group name ("Name" field) of this entry. 315 | pub fn name(&self) -> &str { 316 | &self.name 317 | } 318 | 319 | /// Return whether UID and GID are dynamically allocated at runtime. 320 | pub fn has_dynamic_ids(&self) -> bool { 321 | matches!(self.id, IdOrPath::Automatic) 322 | } 323 | 324 | /// Return the user identifier (UID) of this entry, if statically set. 325 | pub fn static_uid(&self) -> Option { 326 | match self.id { 327 | IdOrPath::Id(n) => Some(n), 328 | IdOrPath::UidGid((n, _)) => Some(n), 329 | IdOrPath::UidGroupname((n, _)) => Some(n), 330 | _ => None, 331 | } 332 | } 333 | 334 | /// Return the groups identifier (GID) of this entry, if statically set. 335 | pub fn static_gid(&self) -> Option { 336 | match self.id { 337 | IdOrPath::Id(n) => Some(n), 338 | IdOrPath::UidGid((_, n)) => Some(n), 339 | _ => None, 340 | } 341 | } 342 | 343 | pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { 344 | SysusersEntry::CreateUserAndGroup(self) 345 | } 346 | } 347 | 348 | /// ID entity for `CreateUserAndGroup`. 349 | #[derive(Clone, Debug, PartialEq)] 350 | pub(crate) enum IdOrPath { 351 | Id(u32), 352 | UidGid((u32, u32)), 353 | UidGroupname((u32, String)), 354 | Path(PathBuf), 355 | Automatic, 356 | } 357 | 358 | impl FromStr for IdOrPath { 359 | type Err = SdError; 360 | 361 | fn from_str(value: &str) -> Result { 362 | if value == "-" { 363 | return Ok(IdOrPath::Automatic); 364 | } 365 | if value.starts_with('/') { 366 | return Ok(IdOrPath::Path(value.into())); 367 | } 368 | if let Ok(single_id) = value.parse() { 369 | return Ok(IdOrPath::Id(single_id)); 370 | } 371 | let tokens: Vec<_> = value.split(':').filter(|s| !s.is_empty()).collect(); 372 | if tokens.len() == 2 { 373 | let uid: u32 = tokens[0].parse().context("invalid user id")?; 374 | let id = match tokens[1].parse() { 375 | Ok(gid) => IdOrPath::UidGid((uid, gid)), 376 | _ => { 377 | let groupname = tokens[1].to_string(); 378 | validate_name_strict(&groupname).context("name failed validation")?; 379 | IdOrPath::UidGroupname((uid, groupname)) 380 | } 381 | }; 382 | return Ok(id); 383 | } 384 | 385 | Err(format!("unexpected user ID '{}'", value).into()) 386 | } 387 | } 388 | 389 | /// ID entity for `CreateGroup`. 390 | #[derive(Clone, Debug, PartialEq)] 391 | pub(crate) enum GidOrPath { 392 | Gid(u32), 393 | Path(PathBuf), 394 | Automatic, 395 | } 396 | 397 | impl FromStr for GidOrPath { 398 | type Err = SdError; 399 | 400 | fn from_str(value: &str) -> Result { 401 | if value == "-" { 402 | return Ok(GidOrPath::Automatic); 403 | } 404 | if value.starts_with('/') { 405 | return Ok(GidOrPath::Path(value.into())); 406 | } 407 | if let Ok(parsed_gid) = value.parse() { 408 | return Ok(GidOrPath::Gid(parsed_gid)); 409 | } 410 | 411 | Err(format!("unexpected group ID '{}'", value).into()) 412 | } 413 | } 414 | 415 | /// Validate a sysusers name in strict mode. 416 | pub fn validate_name_strict(input: &str) -> Result<(), SdError> { 417 | if input.is_empty() { 418 | return Err(SdError::from("empty name")); 419 | } 420 | 421 | if input.len() > 31 { 422 | let err_msg = format!( 423 | "overlong sysusers name '{}' (more than 31 characters)", 424 | input 425 | ); 426 | return Err(SdError::from(err_msg)); 427 | } 428 | 429 | for (index, ch) in input.char_indices() { 430 | if index == 0 { 431 | if !(ch.is_ascii_alphabetic() || ch == '_') { 432 | let err_msg = format!( 433 | "invalid starting character '{}' in sysusers name '{}'", 434 | ch, input 435 | ); 436 | return Err(SdError::from(err_msg)); 437 | } 438 | } else if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') { 439 | let err_msg = format!("invalid character '{}' in sysusers name '{}'", ch, input); 440 | return Err(SdError::from(err_msg)); 441 | } 442 | } 443 | 444 | Ok(()) 445 | } 446 | 447 | #[cfg(test)] 448 | mod test { 449 | use super::*; 450 | 451 | #[test] 452 | fn test_validate_name_strict() { 453 | let err_cases = vec!["-foo", "10bar", "42"]; 454 | for entry in err_cases { 455 | validate_name_strict(entry).unwrap_err(); 456 | } 457 | 458 | let ok_cases = vec!["_authd", "httpd"]; 459 | for entry in ok_cases { 460 | validate_name_strict(entry).unwrap(); 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/sysusers/parse.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use nom::bytes::complete::{tag, take_while1}; 3 | use nom::character::complete::{anychar, multispace0, multispace1}; 4 | use nom::{Finish, IResult}; 5 | use std::convert::TryInto; 6 | use std::str::FromStr; 7 | 8 | /// Parse `sysusers.d` configuration entries from a buffered reader. 9 | pub fn parse_from_reader(bufrd: &mut impl BufRead) -> Result, SdError> { 10 | use crate::errors::ErrorKind; 11 | 12 | let mut output = vec![]; 13 | for (index, item) in bufrd.lines().enumerate() { 14 | let linenumber = index.saturating_add(1); 15 | let line = item.map_err(|e| format!("failed to read line {}: {}", linenumber, e))?; 16 | 17 | let data = line.trim(); 18 | // Skip empty lines and comments. 19 | if data.is_empty() || data.starts_with('#') { 20 | continue; 21 | } 22 | 23 | match data.parse() { 24 | Ok(entry) => output.push(entry), 25 | Err(SdError { 26 | kind: ErrorKind::SysusersUnknownType, 27 | msg, 28 | }) => { 29 | log::warn!("skipped line {}: {}", linenumber, msg); 30 | } 31 | Err(e) => { 32 | let msg = format!( 33 | "failed to parse sysusers entry at line {}: {}", 34 | linenumber, e.msg 35 | ); 36 | return Err(msg.into()); 37 | } 38 | }; 39 | } 40 | 41 | Ok(output) 42 | } 43 | 44 | impl FromStr for SysusersEntry { 45 | type Err = SdError; 46 | 47 | fn from_str(s: &str) -> Result { 48 | use crate::errors::ErrorKind; 49 | 50 | let input = s.trim(); 51 | match input.chars().next() { 52 | Some('g') => CreateGroup::from_str(input).map(|v| v.into_sysusers_entry()), 53 | Some('m') => AddUserToGroup::from_str(input).map(|v| v.into_sysusers_entry()), 54 | Some('r') => AddRange::from_str(input).map(|v| v.into_sysusers_entry()), 55 | Some('u') => CreateUserAndGroup::from_str(input).map(|v| v.into_sysusers_entry()), 56 | Some(t) => { 57 | let unknown = SdError { 58 | kind: ErrorKind::SysusersUnknownType, 59 | msg: format!("unknown sysusers type signature '{}'", t), 60 | }; 61 | Err(unknown) 62 | } 63 | None => Err("missing sysusers type signature".into()), 64 | } 65 | } 66 | } 67 | 68 | impl FromStr for AddRange { 69 | type Err = SdError; 70 | 71 | fn from_str(s: &str) -> Result { 72 | let data = parse_to_sysusers_data(s)?; 73 | data.try_into() 74 | } 75 | } 76 | 77 | impl FromStr for AddUserToGroup { 78 | type Err = SdError; 79 | 80 | fn from_str(s: &str) -> Result { 81 | let data = parse_to_sysusers_data(s)?; 82 | data.try_into() 83 | } 84 | } 85 | 86 | impl FromStr for CreateGroup { 87 | type Err = SdError; 88 | 89 | fn from_str(s: &str) -> Result { 90 | let data = parse_to_sysusers_data(s)?; 91 | data.try_into() 92 | } 93 | } 94 | 95 | impl FromStr for CreateUserAndGroup { 96 | type Err = SdError; 97 | 98 | fn from_str(s: &str) -> Result { 99 | let data = parse_to_sysusers_data(s)?; 100 | data.try_into() 101 | } 102 | } 103 | 104 | /// Parse the content of a sysusers entry as `SysusersData`. 105 | fn parse_to_sysusers_data(line: &str) -> Result { 106 | let (rest, data) = parse_line(line).finish().map_err(|e| { 107 | format!( 108 | "parsing failed due to '{}' at '{}'", 109 | e.code.description(), 110 | e.input 111 | ) 112 | })?; 113 | if !rest.is_empty() { 114 | return Err(format!("invalid trailing data: '{}'", rest).into()); 115 | } 116 | Ok(data) 117 | } 118 | 119 | fn parse_line(input: &str) -> IResult<&str, SysusersData> { 120 | let rest = input; 121 | let (rest, kind) = { 122 | let (rest, kind) = anychar(rest)?; 123 | let (rest, _) = multispace1(rest)?; 124 | (rest, kind.to_string()) 125 | }; 126 | let (rest, name) = { 127 | let (rest, name) = take_while1(|c: char| !c.is_ascii_whitespace())(rest)?; 128 | let (rest, _) = multispace1(rest)?; 129 | (rest, name.to_string()) 130 | }; 131 | let (rest, id) = { 132 | let (rest, id) = take_while1(|c: char| !c.is_ascii_whitespace())(rest)?; 133 | let (rest, _) = multispace0(rest)?; 134 | (rest, id.to_string()) 135 | }; 136 | let (rest, gecos) = { 137 | let (rest, gecos) = parse_opt_string(rest)?; 138 | let (rest, _) = multispace0(rest)?; 139 | (rest, gecos.map(|s| s.to_string())) 140 | }; 141 | let (rest, home_dir) = { 142 | let (rest, home_dir) = parse_opt_string(rest)?; 143 | let (rest, _) = multispace0(rest)?; 144 | (rest, home_dir.map(|s| s.to_string())) 145 | }; 146 | let (rest, shell) = { 147 | let (rest, shell) = parse_opt_string(rest)?; 148 | let (rest, _) = multispace0(rest)?; 149 | (rest, shell.map(|s| s.to_string())) 150 | }; 151 | 152 | let data = SysusersData { 153 | kind, 154 | name, 155 | id, 156 | gecos, 157 | home_dir, 158 | shell, 159 | }; 160 | Ok((rest, data)) 161 | } 162 | 163 | fn parse_opt_string(input: &str) -> IResult<&str, Option<&str>> { 164 | match input.chars().next() { 165 | None => Ok((input, None)), 166 | Some('"') => parse_quoted_string(input), 167 | _ => parse_plain_string(input), 168 | } 169 | } 170 | 171 | // XXX(lucab): should this account for inner escaped quotes? 172 | fn parse_quoted_string(input: &str) -> IResult<&str, Option<&str>> { 173 | let rest = input; 174 | let (rest, _) = tag("\"")(rest)?; 175 | let (rest, txt) = take_while1(|c: char| c != '"')(rest)?; 176 | let (rest, _) = tag("\"")(rest)?; 177 | Ok((rest, Some(txt))) 178 | } 179 | 180 | fn parse_plain_string(input: &str) -> IResult<&str, Option<&str>> { 181 | let rest = input; 182 | let (rest, txt) = take_while1(|c: char| !c.is_ascii_whitespace())(rest)?; 183 | Ok((rest, Some(txt))) 184 | } 185 | 186 | #[cfg(test)] 187 | mod test { 188 | use super::*; 189 | use crate::sysusers; 190 | 191 | #[test] 192 | fn test_type_g() { 193 | { 194 | let input = r#"g input - - - -"#; 195 | let expected = CreateGroup::new("input".to_string()).unwrap(); 196 | let output: CreateGroup = input.parse().unwrap(); 197 | assert_eq!(output, expected); 198 | let line = output.to_string(); 199 | assert_eq!(line, input); 200 | } 201 | } 202 | 203 | #[test] 204 | fn test_type_m() { 205 | { 206 | let input = r#"m _authd input - - -"#; 207 | let expected = AddUserToGroup::new("_authd".to_string(), "input".to_string()).unwrap(); 208 | let output: AddUserToGroup = input.parse().unwrap(); 209 | assert_eq!(output, expected); 210 | let line = output.to_string(); 211 | assert_eq!(line, input); 212 | } 213 | } 214 | 215 | #[test] 216 | fn test_type_r() { 217 | { 218 | let input = r#"r - 500-900 - - -"#; 219 | let expected = AddRange::new(500, 900).unwrap(); 220 | let output: AddRange = input.parse().unwrap(); 221 | assert_eq!(output, expected); 222 | let line = output.to_string(); 223 | assert_eq!(line, input); 224 | } 225 | } 226 | 227 | #[test] 228 | fn test_type_u() { 229 | { 230 | let input = 231 | r#"u postgres - "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb"#; 232 | let expected = CreateUserAndGroup::new( 233 | "postgres".to_string(), 234 | "Postgresql Database".to_string(), 235 | Some("/var/lib/pgsql".to_string().into()), 236 | Some("/usr/libexec/postgresdb".to_string().into()), 237 | ) 238 | .unwrap(); 239 | let output: CreateUserAndGroup = input.parse().unwrap(); 240 | assert_eq!(output, expected); 241 | let line = output.to_string(); 242 | assert_eq!(line, input); 243 | } 244 | } 245 | 246 | #[test] 247 | fn test_parse_from_reader() { 248 | let config_fragment = r#" 249 | #Type Name ID GECOS Home directory Shell 250 | u httpd 404 "HTTP User" 251 | u _authd /usr/bin/authd "Authorization user" 252 | # Test comment 253 | u postgres - "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb 254 | g input - - 255 | 256 | m _authd input 257 | u root 0 "Superuser" /root /bin/zsh 258 | r - 500-900 259 | "#; 260 | 261 | let mut reader = config_fragment.as_bytes(); 262 | let entries = sysusers::parse_from_reader(&mut reader).unwrap(); 263 | assert_eq!(entries.len(), 7); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/sysusers/serialization.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use serde::ser::SerializeStruct; 3 | use serde::{Deserialize, Serialize, Serializer}; 4 | use std::convert::TryFrom; 5 | 6 | /// Number of fields in each sysusers entry. 7 | const SYSUSERS_FIELDS: usize = 6; 8 | 9 | /// Intermediate format holding raw data for deserialization. 10 | #[derive(Clone, Debug, PartialEq, Deserialize)] 11 | pub(crate) struct SysusersData { 12 | #[serde(rename(deserialize = "Type"))] 13 | pub(crate) kind: String, 14 | #[serde(rename(deserialize = "Name"))] 15 | pub(crate) name: String, 16 | #[serde(rename(deserialize = "ID"))] 17 | pub(crate) id: String, 18 | #[serde(rename(deserialize = "GECOS"))] 19 | pub(crate) gecos: Option, 20 | #[serde(rename(deserialize = "Home directory"))] 21 | pub(crate) home_dir: Option, 22 | #[serde(rename(deserialize = "Shell"))] 23 | pub(crate) shell: Option, 24 | } 25 | 26 | impl TryFrom for AddRange { 27 | type Error = SdError; 28 | 29 | fn try_from(value: SysusersData) -> Result { 30 | if value.kind != "r" { 31 | return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); 32 | }; 33 | ensure_field_none_or_automatic("GECOS", &value.gecos)?; 34 | ensure_field_none_or_automatic("Home directory", &value.home_dir)?; 35 | ensure_field_none_or_automatic("Shell", &value.shell)?; 36 | 37 | let tokens: Vec<_> = value.id.split('-').collect(); 38 | let (from, to) = match tokens.len() { 39 | 1 => (tokens[0], tokens[0]), 40 | 2 => (tokens[0], tokens[1]), 41 | _ => return Err(format!("invalid range specifier '{}'", value.id).into()), 42 | }; 43 | let from_id = from.parse().map_err(|_| "invalid starting range ID")?; 44 | let to_id = to.parse().map_err(|_| "invalid ending range ID")?; 45 | Self::new(from_id, to_id) 46 | } 47 | } 48 | 49 | impl TryFrom for AddUserToGroup { 50 | type Error = SdError; 51 | 52 | fn try_from(value: SysusersData) -> Result { 53 | if value.kind != "m" { 54 | return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); 55 | } 56 | ensure_field_none_or_automatic("GECOS", &value.gecos)?; 57 | ensure_field_none_or_automatic("Home directory", &value.home_dir)?; 58 | ensure_field_none_or_automatic("Shell", &value.shell)?; 59 | 60 | Self::new(value.name, value.id) 61 | } 62 | } 63 | 64 | impl TryFrom for CreateGroup { 65 | type Error = SdError; 66 | 67 | fn try_from(value: SysusersData) -> Result { 68 | if value.kind != "g" { 69 | return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); 70 | } 71 | ensure_field_none_or_automatic("GECOS", &value.gecos)?; 72 | ensure_field_none_or_automatic("Home directory", &value.home_dir)?; 73 | ensure_field_none_or_automatic("Shell", &value.shell)?; 74 | 75 | let gid: GidOrPath = value.id.parse()?; 76 | Self::impl_new(value.name, gid) 77 | } 78 | } 79 | 80 | impl TryFrom for CreateUserAndGroup { 81 | type Error = SdError; 82 | 83 | fn try_from(value: SysusersData) -> Result { 84 | if value.kind != "u" { 85 | return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); 86 | } 87 | 88 | let id: IdOrPath = value.id.parse()?; 89 | Self::impl_new( 90 | value.name, 91 | value.gecos.unwrap_or_default(), 92 | value.home_dir.map(Into::into), 93 | value.shell.map(Into::into), 94 | id, 95 | ) 96 | } 97 | } 98 | 99 | impl Serialize for AddRange { 100 | fn serialize(&self, serializer: S) -> Result 101 | where 102 | S: Serializer, 103 | { 104 | let mut state = serializer.serialize_struct("AddRange", SYSUSERS_FIELDS)?; 105 | state.serialize_field("Type", self.type_signature())?; 106 | state.serialize_field("Name", "-")?; 107 | state.serialize_field("ID", &format!("{}-{}", self.from, self.to))?; 108 | state.serialize_field("GECOS", &Option::::None)?; 109 | state.serialize_field("Home directory", &Option::::None)?; 110 | state.serialize_field("Shell", &Option::::None)?; 111 | state.end() 112 | } 113 | } 114 | 115 | impl Serialize for AddUserToGroup { 116 | fn serialize(&self, serializer: S) -> Result 117 | where 118 | S: Serializer, 119 | { 120 | let mut state = serializer.serialize_struct("AddUserToGroup", SYSUSERS_FIELDS)?; 121 | state.serialize_field("Type", self.type_signature())?; 122 | state.serialize_field("Name", &self.username)?; 123 | state.serialize_field("ID", &self.groupname)?; 124 | state.serialize_field("GECOS", &Option::::None)?; 125 | state.serialize_field("Home directory", &Option::::None)?; 126 | state.serialize_field("Shell", &Option::::None)?; 127 | state.end() 128 | } 129 | } 130 | 131 | impl Serialize for CreateGroup { 132 | fn serialize(&self, serializer: S) -> Result 133 | where 134 | S: Serializer, 135 | { 136 | let mut state = serializer.serialize_struct("CreateGroup", SYSUSERS_FIELDS)?; 137 | state.serialize_field("Type", self.type_signature())?; 138 | state.serialize_field("Name", &self.groupname)?; 139 | state.serialize_field("ID", &self.gid)?; 140 | state.serialize_field("GECOS", &Option::::None)?; 141 | state.serialize_field("Home directory", &Option::::None)?; 142 | state.serialize_field("Shell", &Option::::None)?; 143 | state.end() 144 | } 145 | } 146 | 147 | impl Serialize for CreateUserAndGroup { 148 | fn serialize(&self, serializer: S) -> Result 149 | where 150 | S: Serializer, 151 | { 152 | let mut state = serializer.serialize_struct("CreateUserAndGroup", SYSUSERS_FIELDS)?; 153 | state.serialize_field("Type", self.type_signature())?; 154 | state.serialize_field("Name", &self.name)?; 155 | state.serialize_field("ID", &self.id)?; 156 | state.serialize_field("GECOS", &self.gecos)?; 157 | state.serialize_field("Home directory", &self.home_dir)?; 158 | state.serialize_field("Shell", &self.shell)?; 159 | state.end() 160 | } 161 | } 162 | 163 | impl Serialize for IdOrPath { 164 | fn serialize(&self, serializer: S) -> Result 165 | where 166 | S: Serializer, 167 | { 168 | serializer.serialize_str(&self.to_string()) 169 | } 170 | } 171 | 172 | impl Serialize for GidOrPath { 173 | fn serialize(&self, serializer: S) -> Result 174 | where 175 | S: Serializer, 176 | { 177 | serializer.serialize_str(&self.to_string()) 178 | } 179 | } 180 | 181 | /// Ensure that a field value is either missing or using the default value `-`. 182 | fn ensure_field_none_or_automatic( 183 | field_name: &str, 184 | input: &Option>, 185 | ) -> Result<(), SdError> { 186 | if let Some(val) = input { 187 | if val.as_ref() != "-" { 188 | return Err(format!("invalid {} content: '{}'", field_name, val.as_ref()).into()); 189 | } 190 | } 191 | Ok(()) 192 | } 193 | 194 | #[cfg(test)] 195 | mod test { 196 | use super::*; 197 | 198 | #[test] 199 | fn test_serialization() { 200 | { 201 | let input = AddRange::new(10, 20).unwrap(); 202 | let expected = r#"{"Type":"r","Name":"-","ID":"10-20","GECOS":null,"Home directory":null,"Shell":null}"#; 203 | 204 | let output = serde_json::to_string(&input).unwrap(); 205 | assert_eq!(output, expected); 206 | let entry = SysusersEntry::AddRange(input); 207 | let output = serde_json::to_string(&entry).unwrap(); 208 | assert_eq!(output, expected); 209 | } 210 | { 211 | let input = AddUserToGroup::new("foo3".to_string(), "bar".to_string()).unwrap(); 212 | let expected = r#"{"Type":"m","Name":"foo3","ID":"bar","GECOS":null,"Home directory":null,"Shell":null}"#; 213 | 214 | let output = serde_json::to_string(&input).unwrap(); 215 | assert_eq!(output, expected); 216 | let entry = SysusersEntry::AddUserToGroup(input); 217 | let output = serde_json::to_string(&entry).unwrap(); 218 | assert_eq!(output, expected); 219 | } 220 | { 221 | let input = CreateGroup::new("foo1".to_string()).unwrap(); 222 | let expected = r#"{"Type":"g","Name":"foo1","ID":"-","GECOS":null,"Home directory":null,"Shell":null}"#; 223 | 224 | let output = serde_json::to_string(&input).unwrap(); 225 | assert_eq!(output, expected); 226 | let entry = SysusersEntry::CreateGroup(input); 227 | let output = serde_json::to_string(&entry).unwrap(); 228 | assert_eq!(output, expected); 229 | } 230 | { 231 | let input = CreateUserAndGroup::new("foo0".to_string(), "test".to_string(), None, None) 232 | .unwrap(); 233 | let expected = r#"{"Type":"u","Name":"foo0","ID":"-","GECOS":"test","Home directory":null,"Shell":null}"#; 234 | 235 | let output = serde_json::to_string(&input).unwrap(); 236 | assert_eq!(output, expected); 237 | let entry = SysusersEntry::CreateUserAndGroup(input); 238 | let output = serde_json::to_string(&entry).unwrap(); 239 | assert_eq!(output, expected); 240 | } 241 | } 242 | 243 | #[test] 244 | fn test_deserialization() { 245 | { 246 | let input = r#"{"Type":"r","Name":"-","ID":"50-200","GECOS":null,"Home directory":null,"Shell":null}"#; 247 | let expected = AddRange::new(50, 200).unwrap(); 248 | 249 | let output: AddRange = serde_json::from_str(input).unwrap(); 250 | assert_eq!(output, expected); 251 | let output: SysusersEntry = serde_json::from_str(input).unwrap(); 252 | assert_eq!(output, SysusersEntry::AddRange(expected)); 253 | } 254 | { 255 | let input = r#"{"Type":"m","Name":"foo3","ID":"bar","GECOS":null,"Home directory":null,"Shell":null}"#; 256 | let expected = AddUserToGroup::new("foo3".to_string(), "bar".to_string()).unwrap(); 257 | 258 | let output: AddUserToGroup = serde_json::from_str(input).unwrap(); 259 | assert_eq!(output, expected); 260 | let output: SysusersEntry = serde_json::from_str(input).unwrap(); 261 | assert_eq!(output, SysusersEntry::AddUserToGroup(expected)); 262 | } 263 | { 264 | let input = r#"{"Type":"g","Name":"foo1","ID":"-","GECOS":null,"Home directory":null,"Shell":null}"#; 265 | let expected = CreateGroup::new("foo1".to_string()).unwrap(); 266 | 267 | let output: CreateGroup = serde_json::from_str(input).unwrap(); 268 | assert_eq!(output, expected); 269 | let output: SysusersEntry = serde_json::from_str(input).unwrap(); 270 | assert_eq!(output, SysusersEntry::CreateGroup(expected)); 271 | } 272 | { 273 | let input = r#"{"Type":"u","Name":"foo0","ID":"-","GECOS":"test","Home directory":null,"Shell":null}"#; 274 | let expected = 275 | CreateUserAndGroup::new("foo0".to_string(), "test".to_string(), None, None) 276 | .unwrap(); 277 | 278 | let output: CreateUserAndGroup = serde_json::from_str(input).unwrap(); 279 | assert_eq!(output, expected); 280 | let output: SysusersEntry = serde_json::from_str(input).unwrap(); 281 | assert_eq!(output, SysusersEntry::CreateUserAndGroup(expected)); 282 | } 283 | } 284 | 285 | #[test] 286 | fn test_serde_roundtrip() { 287 | { 288 | let input = AddRange::new(10, 20).unwrap(); 289 | 290 | let json = serde_json::to_string(&input).unwrap(); 291 | let output: AddRange = serde_json::from_str(&json).unwrap(); 292 | assert_eq!(output, input); 293 | let output: SysusersEntry = serde_json::from_str(&json).unwrap(); 294 | assert_eq!(output, SysusersEntry::AddRange(input)); 295 | } 296 | { 297 | let input = AddUserToGroup::new("foo3".to_string(), "bar".to_string()).unwrap(); 298 | 299 | let json = serde_json::to_string(&input).unwrap(); 300 | let output: AddUserToGroup = serde_json::from_str(&json).unwrap(); 301 | assert_eq!(output, input); 302 | let output: SysusersEntry = serde_json::from_str(&json).unwrap(); 303 | assert_eq!(output, SysusersEntry::AddUserToGroup(input)); 304 | } 305 | { 306 | let input = CreateGroup::new("foo1".to_string()).unwrap(); 307 | 308 | let json = serde_json::to_string(&input).unwrap(); 309 | let output: CreateGroup = serde_json::from_str(&json).unwrap(); 310 | assert_eq!(output, input); 311 | let output: SysusersEntry = serde_json::from_str(&json).unwrap(); 312 | assert_eq!(output, SysusersEntry::CreateGroup(input)); 313 | } 314 | { 315 | let input = CreateUserAndGroup::new("foo0".to_string(), "test".to_string(), None, None) 316 | .unwrap(); 317 | 318 | let json = serde_json::to_string(&input).unwrap(); 319 | let output: CreateUserAndGroup = serde_json::from_str(&json).unwrap(); 320 | assert_eq!(output, input); 321 | let output: SysusersEntry = serde_json::from_str(&json).unwrap(); 322 | assert_eq!(output, SysusersEntry::CreateUserAndGroup(input)); 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/unit.rs: -------------------------------------------------------------------------------- 1 | /// Unit name escaping, like `systemd-escape`. 2 | pub fn escape_name(name: &str) -> String { 3 | if name.is_empty() { 4 | return "".to_string(); 5 | } 6 | 7 | let parts: Vec = name 8 | .bytes() 9 | .enumerate() 10 | .map(|(n, b)| escape_byte(b, n)) 11 | .collect(); 12 | parts.join("") 13 | } 14 | 15 | /// Path escaping, like `systemd-escape --path`. 16 | pub fn escape_path(name: &str) -> String { 17 | let trimmed = name.trim_matches('/'); 18 | if trimmed.is_empty() { 19 | return "-".to_string(); 20 | } 21 | 22 | let mut slash_seq = false; 23 | let parts: Vec = trimmed 24 | .bytes() 25 | .filter(|b| { 26 | let is_slash = *b == b'/'; 27 | let res = !(is_slash && slash_seq); 28 | slash_seq = is_slash; 29 | res 30 | }) 31 | .enumerate() 32 | .map(|(n, b)| escape_byte(b, n)) 33 | .collect(); 34 | parts.join("") 35 | } 36 | 37 | fn escape_byte(b: u8, index: usize) -> String { 38 | let c = char::from(b); 39 | match c { 40 | '/' => '-'.to_string(), 41 | ':' | '_' | '0'..='9' | 'a'..='z' | 'A'..='Z' => c.to_string(), 42 | '.' if index > 0 => c.to_string(), 43 | _ => format!(r#"\x{:02x}"#, b), 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod test { 49 | use crate::unit::*; 50 | use quickcheck::quickcheck; 51 | 52 | quickcheck! { 53 | fn test_byte_escape_length(xs: u8, n: usize) -> bool { 54 | let out = escape_byte(xs, n); 55 | out.len() == 1 || out.len() == 4 56 | } 57 | } 58 | 59 | #[test] 60 | fn test_name_escape() { 61 | let cases = vec![ 62 | // leave empty string empty 63 | (r#""#, r#""#), 64 | // escape leading dot 65 | (r#".foo/.bar"#, r#"\x2efoo-.bar"#), 66 | // escape disallowed 67 | (r#"///..\-!#??///"#, r#"---..\x5c\x2d\x21\x23\x3f\x3f---"#), 68 | // escape real-world example 69 | ( 70 | r#"user-cloudinit@/var/lib/coreos/vagrant/vagrantfile-user-data.service"#, 71 | r#"user\x2dcloudinit\x40-var-lib-coreos-vagrant-vagrantfile\x2duser\x2ddata.service"#, 72 | ), 73 | ]; 74 | 75 | for t in cases { 76 | let res = escape_name(t.0); 77 | assert_eq!(res, t.1.to_string()); 78 | } 79 | } 80 | 81 | #[test] 82 | fn test_path_escape() { 83 | let cases = vec![ 84 | // turn empty string path into escaped / 85 | (r#""#, r#"-"#), 86 | // turn redundant ////s into single escaped / 87 | (r#"/////////"#, r#"-"#), 88 | // remove all redundant ////s 89 | (r#"///foo////bar/////tail//////"#, r#"foo-bar-tail"#), 90 | // escape leading dot 91 | (r#"."#, r#"\x2e"#), 92 | (r#"/."#, r#"\x2e"#), 93 | (r#"/////////.///////////////"#, r#"\x2e"#), 94 | (r#"....."#, r#"\x2e...."#), 95 | (r#"/.foo/.bar"#, r#"\x2efoo-.bar"#), 96 | (r#".foo/.bar"#, r#"\x2efoo-.bar"#), 97 | // escape disallowed 98 | (r#"///..\-!#??///"#, r#"\x2e.\x5c\x2d\x21\x23\x3f\x3f"#), 99 | ]; 100 | 101 | for t in cases { 102 | let res = escape_path(t.0); 103 | assert_eq!(res, t.1.to_string()); 104 | } 105 | } 106 | 107 | quickcheck! { 108 | fn test_path_escape_nonempty(xs: String) -> bool { 109 | let out = escape_path(&xs); 110 | !out.is_empty() 111 | } 112 | } 113 | 114 | quickcheck! { 115 | fn test_path_escape_no_slash(xs: String) -> bool { 116 | let out = escape_path(&xs); 117 | !out.contains('/') 118 | } 119 | } 120 | 121 | quickcheck! { 122 | fn test_path_escape_no_dash_runs(xs: String) -> bool { 123 | let out = escape_path(&xs); 124 | !out.contains("--") 125 | } 126 | } 127 | 128 | quickcheck! { 129 | fn test_path_escape_no_leading_dot(xs: String) -> bool { 130 | let out = escape_path(&xs); 131 | !out.starts_with('.') 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/connected_to_journal.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings, clippy::all)] 2 | 3 | use std::collections::HashMap; 4 | use std::env::VarError; 5 | use std::process::Command; 6 | 7 | use pretty_assertions::assert_eq; 8 | use rand::distr::Alphanumeric; 9 | use rand::Rng; 10 | 11 | use libsystemd::logging::*; 12 | 13 | fn random_target(prefix: &str) -> String { 14 | format!( 15 | "{}_{}", 16 | prefix, 17 | rand::rng() 18 | .sample_iter(&Alphanumeric) 19 | .take(10) 20 | .map(char::from) 21 | .collect::() 22 | ) 23 | } 24 | 25 | #[derive(Debug, Copy, Clone)] 26 | enum Journal { 27 | User, 28 | System, 29 | } 30 | 31 | fn read_from_journal(journal: Journal, target: &str) -> Vec> { 32 | let stdout = String::from_utf8( 33 | Command::new("journalctl") 34 | .arg(match journal { 35 | Journal::User => "--user", 36 | Journal::System => "--system", 37 | }) 38 | .arg("--output=json") 39 | .arg(format!("TARGET={}", target)) 40 | .output() 41 | .unwrap() 42 | .stdout, 43 | ) 44 | .unwrap(); 45 | 46 | stdout 47 | .lines() 48 | .map(|l| serde_json::from_str(l).unwrap()) 49 | .collect() 50 | } 51 | 52 | fn main() { 53 | let env_name = "_TEST_LOG_TARGET"; 54 | 55 | // On Github Actions use the system instance for this test, because there's 56 | // no user instance running apparently. 57 | let journal_instance = if std::env::var_os("GITHUB_ACTIONS").is_some() { 58 | Journal::System 59 | } else { 60 | Journal::User 61 | }; 62 | 63 | match std::env::var(env_name) { 64 | Ok(target) => { 65 | journal_send( 66 | Priority::Info, 67 | &format!("connected_to_journal() -> {}", connected_to_journal()), 68 | vec![("TARGET", &target)].into_iter(), 69 | ) 70 | .unwrap(); 71 | } 72 | Err(VarError::NotUnicode(value)) => { 73 | panic!("Value of ${} not unicode: {:?}", env_name, value); 74 | } 75 | Err(VarError::NotPresent) => { 76 | // Restart this binary under systemd-run and then check the journal for the test result 77 | let exe = std::env::current_exe().unwrap(); 78 | let target = random_target("connected_to_journal"); 79 | let status = match journal_instance { 80 | Journal::User => { 81 | let mut cmd = Command::new("systemd-run"); 82 | cmd.arg("--user"); 83 | cmd 84 | } 85 | Journal::System => { 86 | let mut cmd = Command::new("sudo"); 87 | cmd.arg("systemd-run"); 88 | cmd 89 | } 90 | } 91 | .arg("--description=systemd-journal-logger integration test: journal_stream") 92 | .arg(format!("--setenv={}={}", env_name, target)) 93 | // Wait until the process exited and unload the entire unit afterwards to 94 | // leave no state behind 95 | .arg("--wait") 96 | .arg("--collect") 97 | .arg(exe) 98 | .status() 99 | .unwrap(); 100 | 101 | assert!(status.success()); 102 | 103 | let entries = read_from_journal(journal_instance, &target); 104 | assert_eq!(entries.len(), 1); 105 | 106 | assert_eq!(entries[0]["TARGET"], target); 107 | assert_eq!(entries[0]["MESSAGE"], "connected_to_journal() -> true"); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/journal.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use libsystemd::logging::Priority; 4 | use rand::distr::Alphanumeric; 5 | use rand::Rng; 6 | use std::collections::HashMap; 7 | use std::time::Duration; 8 | 9 | fn random_name(prefix: &str) -> String { 10 | format!( 11 | "{}_{}", 12 | prefix, 13 | rand::rng() 14 | .sample_iter(&Alphanumeric) 15 | .take(10) 16 | .map(char::from) 17 | .collect::() 18 | ) 19 | } 20 | 21 | /// Retry `f` 10 times 100ms apart. 22 | /// 23 | /// When `f` returns an error wait 100ms and try it again, up to ten times. 24 | /// If the last attempt failed return the error returned by that attempt. 25 | /// 26 | /// If `f` returns Ok immediately return the result. 27 | fn retry(f: impl Fn() -> Result) -> Result { 28 | let attempts = 10; 29 | let interval = Duration::from_millis(100); 30 | for attempt in (0..attempts).rev() { 31 | match f() { 32 | Ok(result) => return Ok(result), 33 | Err(e) if attempt == 0 => return Err(e), 34 | Err(_) => std::thread::sleep(interval), 35 | } 36 | } 37 | unreachable!() 38 | } 39 | 40 | /// Read from journal with `journalctl`. 41 | /// 42 | /// `test_name` is the randomized name of the test being run, and gets 43 | /// added as `TEST_NAME` match to the `journalctl` call, to make sure to 44 | /// only select journal entries originating from and relevant to the 45 | /// current test. 46 | fn read_from_journal(test_name: &str) -> Vec> { 47 | let stdout = String::from_utf8( 48 | Command::new("journalctl") 49 | .args(["--user", "--output=json"]) 50 | // Filter by the PID of the current test process 51 | .arg(format!("_PID={}", std::process::id())) 52 | .arg(format!("TEST_NAME={}", test_name)) 53 | .output() 54 | .unwrap() 55 | .stdout, 56 | ) 57 | .unwrap(); 58 | 59 | stdout 60 | .lines() 61 | .map(|l| serde_json::from_str(l).unwrap()) 62 | .collect() 63 | } 64 | 65 | /// Read exactly one line from journal for the given test name. 66 | /// 67 | /// Try to read lines for `testname` from journal, and `retry()` if the wasn't 68 | /// _exactly_ one matching line. 69 | fn retry_read_one_line_from_journal(testname: &str) -> HashMap { 70 | retry(|| { 71 | let mut messages = read_from_journal(testname); 72 | if messages.len() == 1 { 73 | Ok(messages.pop().unwrap()) 74 | } else { 75 | Err(format!( 76 | "one messages expected, got {} messages", 77 | messages.len() 78 | )) 79 | } 80 | }) 81 | .unwrap() 82 | } 83 | 84 | #[test] 85 | fn simple_message() { 86 | let test_name = random_name("simple_message"); 87 | libsystemd::logging::journal_send( 88 | Priority::Info, 89 | "Hello World", 90 | vec![ 91 | ("TEST_NAME", test_name.as_str()), 92 | ("FOO", "another piece of data"), 93 | ] 94 | .into_iter(), 95 | ) 96 | .unwrap(); 97 | 98 | let message = retry_read_one_line_from_journal(&test_name); 99 | assert_eq!(message["MESSAGE"], "Hello World"); 100 | assert_eq!(message["TEST_NAME"], test_name); 101 | assert_eq!(message["PRIORITY"], "6"); 102 | assert_eq!(message["FOO"], "another piece of data"); 103 | } 104 | 105 | #[test] 106 | fn multiline_message() { 107 | let test_name = random_name("multiline_message"); 108 | libsystemd::logging::journal_send( 109 | Priority::Info, 110 | "Hello\nMultiline\nWorld", 111 | vec![("TEST_NAME", test_name.as_str())].into_iter(), 112 | ) 113 | .unwrap(); 114 | 115 | let message = retry_read_one_line_from_journal(&test_name); 116 | assert_eq!(message["MESSAGE"], "Hello\nMultiline\nWorld"); 117 | assert_eq!(message["TEST_NAME"], test_name); 118 | assert_eq!(message["PRIORITY"], "6"); 119 | } 120 | 121 | #[test] 122 | fn multiline_message_trailing_newline() { 123 | let test_name = random_name("multiline_message_trailing_newline"); 124 | libsystemd::logging::journal_send( 125 | Priority::Info, 126 | "A trailing newline\n", 127 | vec![("TEST_NAME", test_name.as_str())].into_iter(), 128 | ) 129 | .unwrap(); 130 | 131 | let message = retry_read_one_line_from_journal(&test_name); 132 | assert_eq!(message["MESSAGE"], "A trailing newline\n"); 133 | assert_eq!(message["TEST_NAME"], test_name); 134 | assert_eq!(message["PRIORITY"], "6"); 135 | } 136 | -------------------------------------------------------------------------------- /tests/persistent_state.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::{self, File, OpenOptions}; 3 | use std::io::{self, ErrorKind, Read, Seek, Write}; 4 | use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd}; 5 | use std::process::Command; 6 | use std::result::Result; 7 | 8 | use libsystemd::activation; 9 | use libsystemd::daemon::{self, NotifyState}; 10 | 11 | const PERSISTENT_STATE: &[u8] = "STATE".as_bytes(); 12 | 13 | /// Create a memory backed file as state and store it in systemd. 14 | fn create_and_store_persistent_state() -> Result> { 15 | let path = format!("/dev/shm/persistent_state-{}", std::process::id()); 16 | let mut f = OpenOptions::new() 17 | .read(true) 18 | .write(true) 19 | .create(true) 20 | .truncate(true) 21 | .open(&path)?; 22 | fs::remove_file(&path)?; 23 | 24 | let nss = [ 25 | NotifyState::Fdname("persistent-state".to_owned()), 26 | NotifyState::Fdstore, 27 | ]; 28 | 29 | daemon::notify_with_fds(false, &nss, &[f.as_raw_fd()])?; 30 | f.write_all(PERSISTENT_STATE)?; 31 | f.rewind()?; 32 | Ok(f) 33 | } 34 | 35 | fn run() -> Result> { 36 | if !daemon::booted() { 37 | println!("Not running systemd, early exit."); 38 | return Ok(1); 39 | }; 40 | 41 | let mut descriptors = 42 | activation::receive_descriptors_with_names(false).unwrap_or_else(|_| Vec::new()); 43 | 44 | let mut persistent_state = if let Some((fd, name)) = descriptors.pop() { 45 | println!("Fetched persistent state from systemd"); 46 | if name == "persistent-state" { 47 | unsafe { File::from_raw_fd(fd.into_raw_fd()) } 48 | } else { 49 | let err = io::Error::new(ErrorKind::Other, "Got the wrong file descriptor."); 50 | return Err(Box::new(err)); 51 | } 52 | } else { 53 | println!("Got nothing from systemd, create new persistent state"); 54 | create_and_store_persistent_state()?; 55 | // Systemd should have been configured to restart this process on exit status 2. 56 | return Ok(2); 57 | }; 58 | 59 | // Read and increment state 60 | let mut stored_state = [0x0u8; PERSISTENT_STATE.len()]; 61 | persistent_state.read_exact(&mut stored_state)?; 62 | 63 | assert_eq!(stored_state, PERSISTENT_STATE); 64 | 65 | let nss = [ 66 | NotifyState::Fdname("persistent-state".to_owned()), 67 | NotifyState::FdstoreRemove, 68 | ]; 69 | 70 | daemon::notify(false, &nss)?; 71 | println!("Exiting with success"); 72 | 73 | Ok(0) 74 | } 75 | enum Journal { 76 | User, 77 | System, 78 | } 79 | 80 | fn main() -> Result<(), Box> { 81 | // Run the example if we are reexecuted by the test. 82 | // Read on to understand. 83 | if std::env::var_os("RUN_EXAMPLE").is_some() { 84 | std::process::exit(run()?); 85 | } 86 | 87 | // On Github Actions use the system instance for this test, because there's 88 | // no user instance running apparently. 89 | let journal_instance = if std::env::var_os("GITHUB_ACTIONS").is_some() { 90 | Journal::System 91 | } else { 92 | Journal::User 93 | }; 94 | 95 | // Restart this binary under systemd-run and then check the exit status. 96 | let exe = std::env::current_exe().unwrap(); 97 | let status = match journal_instance { 98 | Journal::User => { 99 | let mut cmd = Command::new("systemd-run"); 100 | cmd.arg("--user"); 101 | cmd 102 | } 103 | Journal::System => { 104 | let mut cmd = Command::new("sudo"); 105 | cmd.arg("systemd-run"); 106 | cmd 107 | } 108 | } 109 | // Set environment so that the example will run instead. 110 | .arg("--setenv=RUN_EXAMPLE=1") 111 | // Make sure the example can store a filedescriptor and can be purposefully restarted. 112 | .arg("-pFileDescriptorStoreMax=1") 113 | .arg("-pRestartForceExitStatus=2") 114 | .arg("-pSuccessExitStatus=2") 115 | // Wait until the process exited and unload the entire unit afterwards to 116 | // leave no state behind 117 | .arg("--wait") 118 | .arg("--collect") 119 | .arg(exe) 120 | .status()?; 121 | 122 | assert!(status.success()); 123 | Ok(()) 124 | } 125 | --------------------------------------------------------------------------------