├── .github ├── actions-rs │ └── grcov.yml ├── codecov.yml └── workflows │ ├── audit.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── TODO ├── examples ├── hws-complete.rs ├── hws.rs └── hws.toml ├── rustfmt.toml ├── spirit-cfg-helpers ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src │ └── lib.rs ├── spirit-daemonize ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── go_background.rs └── src │ └── lib.rs ├── spirit-dipstick ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── metrics.rs └── src │ └── lib.rs ├── spirit-hyper ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── hws-hyper.rs └── src │ └── lib.rs ├── spirit-log ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── background.rs │ └── logging.rs └── src │ ├── background.rs │ └── lib.rs ├── spirit-reqwest ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── make_request_async.rs │ └── make_request_blocking.rs └── src │ └── lib.rs ├── spirit-tokio ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── hws-tokio.rs │ └── msg_print.rs └── src │ ├── either.rs │ ├── handlers.rs │ ├── installer.rs │ ├── lib.rs │ ├── net │ ├── limits.rs │ ├── mod.rs │ └── unix.rs │ └── runtime.rs ├── src ├── app.rs ├── bodies.rs ├── cfg_loader.rs ├── empty.rs ├── error.rs ├── extension.rs ├── fragment │ ├── driver.rs │ ├── mod.rs │ └── pipeline.rs ├── guide │ ├── configuration.rs │ ├── daemonization.rs │ ├── extend.rs │ ├── fragments.rs │ ├── levels.rs │ ├── mod.rs │ ├── principles.rs │ ├── testing.rs │ └── tutorial.rs ├── lib.rs ├── macro_support.rs ├── spirit.rs ├── terminate_guard.rs ├── utils.rs └── validation.rs └── tests └── data ├── cfg1.yaml └── cfg2.toml /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: true 2 | ignore-not-existing: true 3 | llvm: true 4 | filter: covered 5 | output-type: lcov 6 | output-path: ./lcov.info 7 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "diff, flags, files" 3 | require_changes: true 4 | 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | # Don't fail on coverage, only show 10 | informational: true 11 | -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | - cron: '0 0 * * 0' 9 | 10 | jobs: 11 | security_audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/audit-check@35b7b53b1e25b55642157ac01b4adceb5b9ebef3 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | RUST_BACKTRACE: full 9 | 10 | jobs: 11 | test: 12 | name: Build & test 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | rust: 20 | - stable 21 | - beta 22 | - nightly 23 | 24 | runs-on: ${{ matrix.os }} 25 | 26 | steps: 27 | - name: checkout 28 | uses: actions/checkout@v2 29 | 30 | - name: Install Rust 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: ${{ matrix.rust }} 34 | default: true 35 | components: ${{ matrix.components }} 36 | 37 | - name: Restore cache 38 | uses: Swatinem/rust-cache@v1 39 | 40 | - name: Build & test 41 | run: cargo test --all && cargo test --all --all-features 42 | 43 | rustfmt: 44 | name: Check formatting 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: checkout 48 | uses: actions/checkout@v2 49 | 50 | - name: Install Rust 51 | uses: actions-rs/toolchain@v1 52 | with: 53 | profile: minimal 54 | toolchain: stable 55 | default: true 56 | components: rustfmt 57 | 58 | - run: cargo fmt --all -- --check 59 | 60 | # This somehow generates a lot of false positives - links to things that are not mentioned in files at all :-( 61 | # links: 62 | # name: Check documentation links 63 | # runs-on: ubuntu-latest 64 | # steps: 65 | # - name: checkout 66 | # uses: actions/checkout@v2 67 | # 68 | # - name: Install Rust 69 | # uses: actions-rs/toolchain@v1 70 | # with: 71 | # toolchain: stable 72 | # default: true 73 | # 74 | # - name: Restore cache 75 | # uses: Swatinem/rust-cache@v1 76 | # 77 | # - name: Install cargo-deadlinks 78 | # uses: actions-rs/install@v0.1 79 | # with: 80 | # crate: cargo-deadlinks 81 | # use-tool-cache: true 82 | # 83 | # - name: Check links 84 | # run: | 85 | # for package in $(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | .name'); do 86 | # cargo rustdoc -p "$package" --all-features -- -D warnings 87 | # dname=$(echo "$package" | tr '-' '_') 88 | # cargo deadlinks --dir "target/doc/$dname" --check-intra-doc-links --ignore-fragments 89 | # done 90 | 91 | clippy: 92 | name: Clippy lints 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout repository 96 | uses: actions/checkout@v2 97 | 98 | - name: Install Rust 99 | uses: actions-rs/toolchain@v1 100 | with: 101 | toolchain: stable 102 | profile: minimal 103 | default: true 104 | components: clippy 105 | 106 | - name: Restore cache 107 | uses: Swatinem/rust-cache@v1 108 | 109 | - name: Run clippy linter 110 | run: cargo clippy --all --all-features --tests -- -D clippy::all -D warnings 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | tags 4 | .idea 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit" 3 | version = "0.4.21" 4 | authors = ["Michal 'vorner' Vaner "] 5 | description = "Helper to create well behaved daemons with runtime-reconfiguration support" 6 | documentation = "https://docs.rs/spirit" 7 | repository = "https://github.com/vorner/spirit" 8 | readme = "README.md" 9 | categories = ["command-line-interface", "config"] 10 | keywords = ["unix", "daemon", "service", "configuration", "spirit"] 11 | license = "Apache-2.0 OR MIT" 12 | edition = "2018" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [workspace] 20 | members = [ 21 | "./", 22 | "spirit-cfg-helpers", 23 | "spirit-daemonize", 24 | "spirit-dipstick", 25 | "spirit-hyper", 26 | "spirit-log", 27 | "spirit-reqwest", 28 | "spirit-tokio", 29 | ] 30 | 31 | [badges] 32 | travis-ci = { repository = "vorner/spirit" } 33 | maintenance = { status = "passively-maintained" } 34 | 35 | [features] 36 | default = ["cfg-help", "json", "yaml", "suggestions", "color"] 37 | ini = ["config/ini"] 38 | json = ["config/json"] 39 | hjson = ["config/hjson"] 40 | yaml = ["config/yaml"] 41 | cfg-help = ["structdoc"] 42 | suggestions = ["structopt/suggestions"] 43 | color = ["structopt/color"] 44 | 45 | [dependencies] 46 | arc-swap = "~1" 47 | config = { version = "~0.11", default-features = false, features = ["toml"] } 48 | either = "~1" 49 | err-context = "~0.1" 50 | fallible-iterator = "~0.2" 51 | humantime = "~2" 52 | libc = "~0.2" 53 | log = "~0.4" 54 | serde = { version = "~1", features = ["derive"] } 55 | serde_ignored = { version = "~0.1.0" } 56 | serde_path_to_error = "~0.1" 57 | signal-hook = { version = "^0.3.3", features = ["extended-siginfo"] } 58 | structdoc = { version = "~0.1.3", optional = true } 59 | # Due to a hack, we are using part of API that doesn't have the proper API guarantees. So make sure cargo update doesn't break stuff. 60 | # We should solve it eventually somehow. See the StructOptInternal. 61 | structopt = { version = "~0.3.12", default-features = false } 62 | toml = "~0.5" 63 | 64 | [dev-dependencies] 65 | hyper = "~0.14" 66 | once_cell = "~1" 67 | maplit = "~1" 68 | spirit-cfg-helpers = { version = "~0.4", path = "spirit-cfg-helpers" } 69 | spirit-daemonize = { version = "~0.5", path = "spirit-daemonize" } 70 | spirit-hyper = { version = "~0.9", path = "spirit-hyper" } 71 | spirit-log = { version = "~0.4", path = "spirit-log", features = ["background"] } 72 | spirit-tokio = { version = "~0.9", path = "spirit-tokio", features = ["rt-from-cfg"] } 73 | structdoc = "~0.1.3" 74 | tokio = { version = "1", features = ["sync"] } 75 | 76 | # Tests and building is faster with debug turned off and nobody really run a debugger on the 77 | # produced binaries here ever. If it is needed, enable temporarily. 78 | [profile.dev] 79 | debug = false 80 | 81 | [profile.test] 82 | debug = false 83 | 84 | [package.metadata.docs.rs] 85 | all-features = true 86 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 spirit developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spirit 2 | 3 | [![Actions Status](https://github.com/vorner/spirit/workflows/test/badge.svg)](https://github.com/vorner/spirit/actions) 4 | [![codecov](https://codecov.io/gh/vorner/spirit/branch/master/graph/badge.svg?token=3KA3R2D9fV)](https://codecov.io/gh/vorner/spirit) 5 | [![docs](https://docs.rs/spirit/badge.svg)](https://docs.rs/spirit) 6 | 7 | ## Looking for contributors and maintainers 8 | 9 | This library is too big project for me do in my spare time and I don't have a 10 | direct use case for it now. See [this blog post](https://vorner.github.io/2020/03/01/spring-cleanup.html). 11 | 12 | If you want this library to thrive, roll up your sleeves and help. Send PRs, 13 | help reviewing, etc. I'll still be willing fixing serious bugs, doing reviews 14 | and consulting. 15 | 16 | Alternatively, I may accept payment to do some development on it. 17 | 18 | ## The library 19 | 20 | Spirit aims to help with combining configuration and command line parsing, 21 | reloading it at runtime and provides various ready-made helpers for common parts 22 | of configuration like logging. 23 | 24 | In other words, it cuts down on glue code between many different libraries and 25 | makes writing unix daemons easier. 26 | 27 | ## License 28 | 29 | Licensed under either of 30 | 31 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 32 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 33 | 34 | at your option. 35 | 36 | ### Contribution 37 | 38 | Contributions are welcome. If you want to add another helper, or just want to 39 | solve some of the issues, feel free. Some mentoring would also available ‒ 40 | contact me either on the rust gitter or over email. 41 | 42 | Unless you explicitly state otherwise, any contribution intentionally 43 | submitted for inclusion in the work by you, as defined in the Apache-2.0 44 | license, shall be dual licensed as above, without any additional terms 45 | or conditions. 46 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Another Opt structure with -v and such. 2 | * Run the backgroud thread as a task in tokio? 3 | * Why C needs to be Debug? 4 | * config_exts_all? 5 | 6 | * unify Order of Opts/Config 7 | 8 | * Reintroduce scaling 9 | * Parts of the core spirit can be split off and useful separately ‒ base traits, config loading. Then, if the user doesn't want the background thread and stuff, it can be used manually. 10 | * Being able to configure the tokio thread pool 11 | - As a fragment 12 | - Have a callback/time when we already have the config loaded byt we can still create the singleton and body wrapper ‒ like, on_build? 13 | * Make some of the traits sealed 14 | * Builder::run should not need the closure to be static. 15 | 16 | * Stackable and optional for references, eg Vec<&Fragment> or Option<&Fragment> 17 | * Use humantime-serde to get rid of our own humantime/serde integration 18 | -------------------------------------------------------------------------------- /examples/hws-complete.rs: -------------------------------------------------------------------------------- 1 | //! A hello world service 2 | //! 3 | //! This version of a hello world service demonstrates a wide range of the possibilities and tools 4 | //! spirit offers. 5 | //! 6 | //! It listens on one or more ports and greets with hello world (or other configured message) over 7 | //! HTTP. It includes logging and daemonization. 8 | //! 9 | //! It allows reconfiguring almost everything at runtime ‒ change the config file(s), send SIGHUP 10 | //! to it and it'll reload it. 11 | 12 | use std::convert::Infallible; 13 | use std::sync::Arc; 14 | 15 | use arc_swap::ArcSwap; 16 | use hyper::server::Builder; 17 | use hyper::service::{make_service_fn, service_fn}; 18 | use hyper::{Body, Request, Response}; 19 | use log::{debug, trace}; 20 | use serde::{Deserialize, Serialize}; 21 | use spirit::fragment::driver::SilentOnceDriver; 22 | use spirit::prelude::*; 23 | use spirit::utils; 24 | use spirit::{Pipeline, Spirit}; 25 | use spirit_cfg_helpers::Opts as CfgOpts; 26 | use spirit_daemonize::{Daemon, Opts as DaemonOpts}; 27 | use spirit_hyper::{BuildServer, HyperServer}; 28 | use spirit_log::background::{Background, OverflowMode}; 29 | use spirit_log::{Cfg as Logging, CfgAndOpts as LogBoth, Opts as LogOpts}; 30 | use spirit_tokio::either::Either; 31 | use spirit_tokio::net::limits::WithLimits; 32 | #[cfg(unix)] 33 | use spirit_tokio::net::unix::UnixListen; 34 | use spirit_tokio::runtime::{Config as TokioCfg, Tokio}; 35 | use spirit_tokio::TcpListen; 36 | use structdoc::StructDoc; 37 | use structopt::StructOpt; 38 | use tokio::sync::oneshot::Receiver; 39 | 40 | // The command line arguments we would like our application to have. 41 | // 42 | // Here we build it from prefabricated fragments provided by the `spirit-*` crates. Of course we 43 | // could also roll our own. 44 | // 45 | // The spirit will add some more options on top of that ‒ it'll be able to accept 46 | // `--config-override` to override one or more config option on the command line and it'll accept 47 | // an optional list of config files and config directories. 48 | // 49 | // Note that this doc comment gets printed as part of the `--help` message, you can include 50 | // authors, etc: 51 | /// A Hello World Service. 52 | /// 53 | /// Will listen on some HTTP sockets and greet every client that comes with a configured message, 54 | /// by default „hello world“. 55 | /// 56 | /// You can play with the options, configuration, runtime-reloading (by SIGHUP), etc. 57 | #[derive(Clone, Debug, StructOpt)] 58 | #[structopt( 59 | version = "1.0.0-example", // Taken from Cargo.toml if not specified 60 | author, 61 | )] 62 | struct Opts { 63 | // Adds the `--daemonize` and `--foreground` options. 64 | #[structopt(flatten)] 65 | daemon: DaemonOpts, 66 | 67 | // Adds the `--log` and `--log-module` options. 68 | #[structopt(flatten)] 69 | logging: LogOpts, 70 | 71 | // Adds the --help-config and --dump-config options 72 | #[structopt(flatten)] 73 | cfg_opts: CfgOpts, 74 | } 75 | 76 | impl Opts { 77 | fn logging(&self) -> LogOpts { 78 | self.logging.clone() 79 | } 80 | fn cfg_opts(&self) -> &CfgOpts { 81 | &self.cfg_opts 82 | } 83 | } 84 | 85 | /// An application specific configuration. 86 | /// 87 | /// For the Hello World Service, we configure just the message to send. 88 | #[derive(Clone, Debug, Default, Deserialize, StructDoc, Serialize)] 89 | struct Ui { 90 | /// The message to send. 91 | msg: String, 92 | } 93 | 94 | /// Similarly, each transport we listen on will carry its own signature. 95 | /// 96 | /// Well, optional signature. It may be missing. 97 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, StructDoc, Serialize)] 98 | struct Signature { 99 | /// A signature appended to the message. 100 | /// 101 | /// May be different on each listening port. 102 | #[serde(skip_serializing_if = "Option::is_none")] 103 | signature: Option, 104 | } 105 | 106 | /// Configuration of a http server. 107 | /// 108 | /// The `HttpServer` could be enough. It would allow configuring the listening port and a whole 109 | /// bunch of other details about listening (how many accepting tasks there should be in parallel, 110 | /// on what interface to listen, TCP keepalive, HTTP keepalive...). 111 | /// 112 | /// But we actually want something even more crazy. We want our users to be able to use on both 113 | /// normal http over TCP on some port as well as on unix domain sockets. Don't say you've never 114 | /// heard of HTTP over unix domain sockets... 115 | /// 116 | /// So when the user puts `port = 1234`, it listens on TCP. If there's `path = 117 | /// "/tmp/path/to/socket"`, it listens on http. 118 | /// 119 | /// We also bundle the optional signature inside of that thing. 120 | #[cfg(unix)] 121 | type ListenSocket = WithLimits, UnixListen>>; 122 | 123 | #[cfg(not(unix))] 124 | type ListenSocket = WithLimits>; 125 | 126 | type Server = HyperServer; 127 | 128 | #[cfg(unix)] 129 | fn extract_signature(listen: &Server) -> &Option { 130 | match &listen.transport.listen { 131 | Either::A(tcp) => &tcp.extra_cfg.signature, 132 | Either::B(unix) => &unix.extra_cfg.signature, 133 | } 134 | } 135 | 136 | #[cfg(not(unix))] 137 | fn extract_signature(listen: &Server) -> &Option { 138 | &listen.transport.listen.extra_cfg.signature 139 | } 140 | 141 | /// Putting the whole configuration together. 142 | /// 143 | /// Note that here too, the doc comments can become part of the user help ‒ the `--help-config` 144 | /// this time. 145 | #[derive(Clone, Debug, Default, Deserialize, StructDoc, Serialize)] 146 | struct Cfg { 147 | /// Deamonization stuff 148 | /// 149 | /// Like the user to switch to, working directory or if it should actually daemonize. 150 | #[serde(default)] 151 | daemon: Daemon, 152 | 153 | /// The logging. 154 | /// 155 | /// This allows multiple logging destinations in parallel, configuring the format, timestamp 156 | /// format, destination. 157 | #[serde(default, skip_serializing_if = "Logging::is_empty")] 158 | logging: Logging, 159 | 160 | /// Where to listen on. 161 | /// 162 | /// This allows multiple listening ports at once, both over ordinary TCP and on unix domain 163 | /// stream sockets. 164 | listen: Vec, 165 | 166 | /// The user interface. 167 | ui: Ui, 168 | 169 | /// The work threadpool. 170 | /// 171 | /// This is for performance tuning. 172 | threadpool: TokioCfg, 173 | } 174 | 175 | impl Cfg { 176 | fn logging(&self) -> Logging { 177 | self.logging.clone() 178 | } 179 | fn listen(&self) -> &Vec { 180 | &self.listen 181 | } 182 | fn threadpool(&self) -> TokioCfg { 183 | self.threadpool.clone() 184 | } 185 | } 186 | 187 | /// Let's bake some configuration in. 188 | /// 189 | /// We wouldn't have to do that, but bundling a piece of configuration inside makes sure we can 190 | /// start without one. 191 | const DEFAULT_CONFIG: &str = r#" 192 | [daemon] 193 | pid-file = "/tmp/hws" 194 | workdir = "/" 195 | 196 | [[logging]] 197 | level = "DEBUG" 198 | type = "stderr" 199 | clock = "UTC" 200 | per-module = { hws_complete = "TRACE", hyper = "INFO", tokio = "INFO" } 201 | format = "extended" 202 | 203 | [[listen]] 204 | port = 5678 205 | host = "127.0.0.1" 206 | http-mode = "http1-only" 207 | backlog = 256 208 | scale = 2 209 | signature = "IPv4" 210 | reuse-addr = true 211 | 212 | [[listen]] 213 | port = 5678 214 | host = "::1" 215 | http-mode = "http1-only" 216 | backlog = 256 217 | scale = 2 218 | only-v6 = true 219 | signature = "IPv6" 220 | max-conn = 20 221 | reuse-addr = true 222 | 223 | [[listen]] 224 | # This one will be rejected on Windows, because it'll turn off the unix domain socket support. 225 | path = "/tmp/hws.socket" 226 | unlink-before = "try-connect" 227 | unlink-after = true 228 | http-mode = "http1-only" 229 | backlog = 256 230 | scale = 2 231 | error-sleep = "100ms" 232 | 233 | [ui] 234 | msg = "Hello world" 235 | 236 | [threadpool] 237 | core-threads = 2 238 | max-threads = 4 239 | "#; 240 | 241 | /// This is the actual workhorse of the application. 242 | /// 243 | /// This thing handles one request. The plumbing behind the scenes give it access to the relevant 244 | /// parts of config. 245 | #[allow(clippy::needless_pass_by_value)] // The server_configured expects this signature 246 | fn hello(global_cfg: &Cfg, cfg: &Arc, req: Request) -> Response { 247 | trace!("Handling request {:?}", req); 248 | // Get some global configuration 249 | let mut msg = format!("{}\n", global_cfg.ui.msg); 250 | // Get some listener-local configuration. 251 | if let Some(ref signature) = extract_signature(cfg) { 252 | msg.push_str(&format!("Brought to you by {signature}\n")); 253 | } 254 | Response::new(Body::from(msg)) 255 | } 256 | 257 | /// Putting it all together and starting. 258 | fn main() { 259 | // Do a forced shutdown on second CTRL+C if the shutdown after the first one takes too 260 | // long. 261 | utils::support_emergency_shutdown().expect("Installing signals isn't supposed to fail"); 262 | let global_cfg = Arc::new(ArcSwap::from_pointee(Cfg::default())); 263 | let build_server = { 264 | let global_cfg = Arc::clone(&global_cfg); 265 | move |builder: Builder<_>, cfg: &Server, _: &'static str, shutdown: Receiver<()>| { 266 | let global_cfg = Arc::clone(&global_cfg); 267 | let cfg = Arc::new(cfg.clone()); 268 | builder 269 | .serve(make_service_fn(move |_conn| { 270 | let global_cfg = Arc::clone(&global_cfg); 271 | let cfg = Arc::clone(&cfg); 272 | async move { 273 | let global_cfg = Arc::clone(&global_cfg); 274 | let cfg = Arc::clone(&cfg); 275 | Ok::<_, Infallible>(service_fn(move |req| { 276 | let global_cfg = Arc::clone(&global_cfg); 277 | let cfg = Arc::clone(&cfg); 278 | async move { Ok::<_, Infallible>(hello(&global_cfg.load(), &cfg, req)) } 279 | })) 280 | } 281 | })) 282 | .with_graceful_shutdown(async move { 283 | let _ = shutdown.await; 284 | }) 285 | } 286 | }; 287 | Spirit::::new() 288 | // The baked in configuration. 289 | .config_defaults(DEFAULT_CONFIG) 290 | // In addition to specifying configuration in files and command line, also allow overriding 291 | // it through an environment variable. This is useful to passing secrets in many 292 | // deployments (like all these docker based clouds). 293 | .config_env("HELLO") 294 | // If passed a directory, look for files with these extensions and load them as 295 | // configurations. 296 | // 297 | // Note that if a file is added or removed at runtime and the application receives SIGHUP, 298 | // the change is reflected. 299 | .config_exts(["toml", "ini", "json"]) 300 | // Put help options early. They may terminate the program and we may want to do it before 301 | // daemonization or other side effects. 302 | .with(CfgOpts::extension(Opts::cfg_opts)) 303 | // Early logging without the background thread. Only once, then it is taken over by the 304 | // pipeline lower. We can't have the background thread before daemonization. 305 | .with( 306 | Pipeline::new("early-logging") 307 | .extract(|opts: &Opts, cfg: &Cfg| LogBoth { 308 | cfg: cfg.logging(), 309 | opts: opts.logging(), 310 | }) 311 | .set_driver(SilentOnceDriver::default()), 312 | ) 313 | // Plug in the daemonization configuration and command line arguments. The library will 314 | // make it alive ‒ it'll do the actual daemonization based on the config, it only needs to 315 | // be told it should do so this way. 316 | // 317 | // Must come very early, before any threads are started. That includes any potential 318 | // logging threads. 319 | .with(unsafe { 320 | spirit_daemonize::extension(|cfg: &Cfg, opts: &Opts| { 321 | (cfg.daemon.clone(), opts.daemon.clone()) 322 | }) 323 | }) 324 | // Now we can do the full logging, with a background thread. 325 | .with( 326 | Pipeline::new("logging") 327 | .extract(|opts: &Opts, cfg: &Cfg| LogBoth { 328 | cfg: cfg.logging(), 329 | opts: opts.logging(), 330 | }) 331 | .transform(Background::new(100, OverflowMode::Block)), 332 | ) 333 | // A custom callback ‒ when a new config is loaded, we want to print it to logs. 334 | .on_config(|cmd_line, new_cfg| { 335 | debug!("Current cmdline: {:?} and config {:?}", cmd_line, new_cfg); 336 | }) 337 | // Alternatively, one could use provided one: 338 | // .with(spirit_cfg_helpers::config_logging(Level::Debug, true)) 339 | // Store a copy of current configuration whenever it changes. It's also accessible through 340 | // the spirit object passed to the run method, but this may be more convenient and is 341 | // available sooner. 342 | .with(spirit_cfg_helpers::cfg_store(Arc::clone(&global_cfg))) 343 | // Configure number of threads & similar. And make sure spirit has a tokio runtime to 344 | // provide it for the installed futures (the http server). 345 | // 346 | // Usually the pipeline would install a default tokio runtime if it is not provided. But if 347 | // we plugged the pipeline in inside the run method, that'd be too late ‒ we need the run 348 | // to be wrapped in it. So we either need to plug the pipeline in before run, or need to 349 | // make sure we add the Tokio runtime manually (even if Tokio::Default). 350 | // 351 | // This one should be installed as singleton. Having two is not a good idea. 352 | .with_singleton(Tokio::from_cfg(Cfg::threadpool)) 353 | // And finally, the server. 354 | .with( 355 | Pipeline::new("listen") 356 | .extract_cfg(Cfg::listen) 357 | .transform(BuildServer(build_server)), 358 | ) 359 | .run(|_| { 360 | // The run is empty, that's OK, because we are running with tokio. We let it keep 361 | // running until we shut down the application. 362 | Ok(()) 363 | }); 364 | } 365 | -------------------------------------------------------------------------------- /examples/hws.rs: -------------------------------------------------------------------------------- 1 | //! A hello-world service 2 | //! 3 | //! This is the simpler of the two hello-world-service examples. 4 | //! 5 | //! The service listens on set of TCP sockets. Everyone who connects is greeted with a message and 6 | //! the connection is terminated. 7 | //! 8 | //! Try it out: connect to it, try different logging options on command line. When you start it 9 | //! with a configuration file passed on a command line, you can modify the file and send SIGHUP ‒ 10 | //! and see the changes to the logging and the message take effect without restarting the 11 | //! application. 12 | 13 | use std::collections::HashSet; 14 | use std::error::Error; 15 | use std::fmt::{Display, Formatter, Result as FmtResult}; 16 | use std::io::Write; 17 | use std::net::{TcpListener, TcpStream}; 18 | use std::sync::mpsc; 19 | use std::thread; 20 | 21 | use arc_swap::ArcSwap; 22 | use log::{debug, error, info, warn}; 23 | use once_cell::sync::Lazy; 24 | use serde::Deserialize; 25 | use spirit::prelude::*; 26 | use spirit::{extension, AnyError}; 27 | use spirit::{Empty, Spirit}; 28 | 29 | #[derive(Copy, Clone, Debug)] 30 | struct NoPorts; 31 | 32 | impl Display for NoPorts { 33 | fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 34 | write!(fmt, "No ports to listen on") 35 | } 36 | } 37 | 38 | impl Error for NoPorts {} 39 | 40 | // In this part, we define how our configuration looks like. Just like with the `config` crate 41 | // (which is actually used internally), the configuration is loaded using the serde's Deserialize. 42 | // 43 | // Of course the actual structure and names of the configuration is up to the application to 44 | // choose. 45 | // 46 | // The spirit library will further enrich the configuration by logging configuration (and possibly 47 | // other things in the future) and use that internally. 48 | 49 | fn default_host() -> String { 50 | "::".to_owned() 51 | } 52 | 53 | /// Description of one listening socket. 54 | #[derive(Clone, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)] 55 | struct Listen { 56 | port: u16, 57 | #[serde(default = "default_host")] 58 | host: String, 59 | } 60 | 61 | /// Description of the user-facing interface. 62 | #[derive(Default, Deserialize)] 63 | struct Ui { 64 | /// The message printed to each visitor. 65 | msg: String, 66 | } 67 | 68 | #[derive(Default, Deserialize)] 69 | struct Config { 70 | listen: HashSet, 71 | ui: Ui, 72 | } 73 | 74 | /// Here we'll have the current config stored at each time. The spirit library will refresh it 75 | /// with a new version on reload (and the old one will get dropped eventually). 76 | static CONFIG: Lazy> = Lazy::new(Default::default); 77 | 78 | /// This is used as the base configuration. 79 | const DEFAULT_CONFIG: &str = r#" 80 | [[listen]] 81 | port = 1234 82 | 83 | [[listen]] 84 | port = 5678 85 | host = "localhost" 86 | 87 | [ui] 88 | msg = "Hello world" 89 | "#; 90 | 91 | /// Handles one connection. 92 | /// 93 | /// As the configuration is globally accessible, it can directly load the message from there. 94 | fn handle_conn(mut conn: TcpStream) { 95 | let addr = conn 96 | .peer_addr() 97 | .map(|addr| addr.to_string()) 98 | // The address is just for logging, so don't hard-fail on that. 99 | .unwrap_or_else(|_| "".to_owned()); 100 | debug!("Handling connection from {}", addr); 101 | let msg = format!("{}\n", CONFIG.load().ui.msg); 102 | if let Err(e) = conn.write_all(msg.as_bytes()) { 103 | error!("Failed to handle connection {}: {}", addr, e); 104 | } 105 | } 106 | 107 | /// Start all the threads, one for each listening socket. 108 | fn start_threads() -> Result<(), AnyError> { 109 | let config = CONFIG.load(); 110 | if config.listen.is_empty() { 111 | return Err(NoPorts.into()); 112 | } 113 | for listen in &config.listen { 114 | info!("Starting thread on {}:{}", listen.host, listen.port); 115 | let listener = TcpListener::bind((&listen.host as &str, listen.port))?; 116 | thread::spawn(move || { 117 | for conn in listener.incoming() { 118 | match conn { 119 | Ok(conn) => handle_conn(conn), 120 | Err(e) => warn!("Error accepting: {}", e), 121 | } 122 | } 123 | }); 124 | } 125 | Ok(()) 126 | } 127 | 128 | fn main() -> Result<(), AnyError> { 129 | let (term_send, term_recv) = mpsc::channel(); 130 | let _spirit = Spirit::::new() 131 | // Keep the current config accessible through a global variable 132 | .with(spirit_cfg_helpers::cfg_store(&*CONFIG)) 133 | // Set the default config values. This is very similar to passing the first file on command 134 | // line, except that nobody can lose this one as it is baked into the application. Any 135 | // files passed by the user can override the values. 136 | .config_defaults(DEFAULT_CONFIG) 137 | // If the user passes a directory path instead of file path, take files with these 138 | // extensions and load config from there. 139 | .config_exts(["toml", "ini", "json"]) 140 | // Config can be read from environment too 141 | .config_env("HWS") 142 | // Perform some more validation of the results. 143 | // 144 | // We are a bit lazy here. Changing the set of ports we listen on at runtime is hard to do. 145 | // Therefore we simply warn about a change that doesn't take an effect. 146 | // 147 | // The hws example in spirit-tokio has a working update of listening ports. 148 | .with(extension::immutable_cfg( 149 | |cfg: &Config| &cfg.listen, 150 | "listen ports", 151 | )) 152 | .on_terminate(move || { 153 | // This unfortunately cuts all the listening threads right away. 154 | term_send.send(()).unwrap(); 155 | }) 156 | .build(true)?; 157 | start_threads()?; 158 | info!("Starting up"); 159 | // And this waits for the ctrl+C or something similar. 160 | term_recv.recv().unwrap(); 161 | info!("Shutting down"); 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /examples/hws.toml: -------------------------------------------------------------------------------- 1 | [[listen]] 2 | port = 1234 3 | scale = 2 4 | 5 | [[listen]] 6 | port = 5678 7 | host = "localhost" 8 | 9 | [ui] 10 | msg = "Hello world" 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vorner/spirit/33bc839cba1d0e16ed075049263374f5a5c786a0/rustfmt.toml -------------------------------------------------------------------------------- /spirit-cfg-helpers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-cfg-helpers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-cfg-helpers" 3 | version = "0.4.0" 4 | authors = ["Michal 'vorner' Vaner "] 5 | edition = "2018" 6 | description = "Helpers for spirit to make interacting with configuration more smooth for the user" 7 | documentation = "https://docs.rs/spirit-cfg-helpers" 8 | repository = "https://github.com/vorner/spirit" 9 | license = "Apache-2.0 OR MIT" 10 | categories = ["command-line-interface", "config"] 11 | keywords = ["configuration", "spirit", "ux"] 12 | readme = "README.md" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = ["cfg-help", "json", "yaml"] 25 | cfg-help = ["spirit/cfg-help", "structdoc"] 26 | json = ["serde_json"] 27 | yaml = ["serde_yaml"] 28 | 29 | [dependencies] 30 | arc-swap = "~1" 31 | log = "~0.4" 32 | serde = "~1" 33 | serde_json = { version = "~1", optional = true } 34 | serde_yaml = { version = "~0.8", optional = true } 35 | spirit = { path = "..", version = "~0.4", default-features = false } 36 | structdoc = { version = "~0.1", default-features = false, optional = true } 37 | structopt = { version = "~0.3", default-features = false } 38 | toml = "~0.5" 39 | 40 | [dev-dependencies] 41 | once_cell = "~1" 42 | serde_derive = "~1" 43 | -------------------------------------------------------------------------------- /spirit-cfg-helpers/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-cfg-helpers/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-cfg-helpers/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-hyper 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | A helper to add the `--help-config` and `--dump-config` options to a 6 | [spirit](https://crates.io/crates/spirit)-based application. Some more useful 7 | helpers around configuration too. 8 | 9 | See the [docs](https://docs.rs/spirit-cfg-helpers). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-daemonize/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-daemonize/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-daemonize" 3 | version = "0.5.1" 4 | authors = ["Michal 'vorner' Vaner "] 5 | description = "Spirit helpers and config fragments to daemonize" 6 | documentation = "https://docs.rs/spirit-daemonize" 7 | edition = "2018" 8 | repository = "https://github.com/vorner/spirit" 9 | readme = "README.md" 10 | categories = ["config", "os::unix-apis"] 11 | keywords = ["unix", "daemon", "service", "spirit"] 12 | license = "Apache-2.0 OR MIT" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = ["cfg-help"] 25 | cfg-help = ["spirit/cfg-help", "structdoc"] 26 | 27 | [dependencies] 28 | err-context = "~0.1" 29 | log = "~0.4" 30 | nix = "~0.23" 31 | privdrop = "~0.5" 32 | serde = { version = "~1", features = ["derive"] } 33 | spirit = { version = "~0.4", path = "..", default-features = false } 34 | # TODO: Proper versions, feature flag 35 | structdoc = { version = "~0.1", optional = true } 36 | structopt = { version = "~0.3", default-features = false } 37 | -------------------------------------------------------------------------------- /spirit-daemonize/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-daemonize/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-daemonize/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-daemonize 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | Helpers and configuration fragments to integrate daemonization into the spirit 6 | configuration framework. 7 | 8 | See the [docs](https://docs.rs/spirit-daemonize) and the 9 | [examples](spirit-daemonize/examples). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-daemonize/examples/go_background.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use serde::Deserialize; 5 | use spirit::prelude::*; 6 | use spirit::Spirit; 7 | use spirit_daemonize::{Daemon, Opts as DaemonOpts}; 8 | use structopt::StructOpt; 9 | 10 | #[derive(Clone, Debug, StructOpt)] 11 | struct Opts { 12 | #[structopt(flatten)] 13 | daemon: DaemonOpts, 14 | } 15 | 16 | #[derive(Clone, Debug, Default, Deserialize)] 17 | struct Ui { 18 | msg: String, 19 | sleep_ms: u64, 20 | } 21 | 22 | #[derive(Clone, Debug, Default, Deserialize)] 23 | struct Cfg { 24 | #[serde(default)] 25 | daemon: Daemon, 26 | ui: Ui, 27 | } 28 | 29 | const DEFAULT_CONFIG: &str = r#" 30 | [daemon] 31 | pid-file = "/tmp/go_background.pid" 32 | workdir = "/" 33 | 34 | [ui] 35 | msg = "Hello world" 36 | sleep_ms = 100 37 | "#; 38 | 39 | fn main() { 40 | Spirit::::new() 41 | .config_defaults(DEFAULT_CONFIG) 42 | .config_exts(["toml", "ini", "json"]) 43 | .with(unsafe { 44 | spirit_daemonize::extension(|c: &Cfg, o: &Opts| (c.daemon.clone(), o.daemon.clone())) 45 | }) 46 | .run(|spirit| { 47 | while !spirit.is_terminated() { 48 | let cfg = spirit.config(); 49 | println!("{}", cfg.ui.msg); 50 | thread::sleep(Duration::from_millis(cfg.ui.sleep_ms)); 51 | } 52 | Ok(()) 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /spirit-dipstick/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-dipstick/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-dipstick" 3 | version = "0.3.0" 4 | authors = ["Michal 'vorner' Vaner "] 5 | edition = "2018" 6 | description = "Automatic configuration of dipstick backends" 7 | documentation = "https://docs.rs/spirit-dipstick" 8 | repository = "https://github.com/vorner/spirit" 9 | license = "Apache-2.0 OR MIT" 10 | categories = ["config", "development-tools::profiling"] 11 | keywords = ["dipstick", "metrics", "configuration", "spirit"] 12 | readme = "README.md" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = ["cfg-help"] 25 | cfg-help = ["spirit/cfg-help", "structdoc"] 26 | 27 | [dependencies] 28 | dipstick = { version = "~0.9", default-features = false } 29 | err-context = "~0.1" 30 | log = "~0.4" 31 | serde = { version = "~1", features = ["derive"] } 32 | spirit = { version = "~0.4.0", path = "..", default-features = false } 33 | structdoc = { version = "~0.1", optional = true } 34 | 35 | [dev-dependencies] 36 | env_logger = "~0.9" 37 | -------------------------------------------------------------------------------- /spirit-dipstick/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-dipstick/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-dipstick/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-hyper 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | A helper to auto-configure [dipstick](https://crates.io/crates/dipstick) 6 | backend. It is part of the [spirit](https://crates.io/crates/spirit) system. 7 | 8 | See the [docs](https://docs.rs/spirit-dipstick) and the 9 | [examples](spirit-dipstick/examples). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-dipstick/examples/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use dipstick::{stats_all, InputScope}; 5 | use log::debug; 6 | use serde::Deserialize; 7 | use spirit::prelude::*; 8 | use spirit::{Empty, Pipeline, Spirit}; 9 | use spirit_dipstick::{Config as MetricsConfig, Monitor}; 10 | 11 | #[derive(Debug, Default, Deserialize)] 12 | struct Cfg { 13 | metrics: Option, 14 | } 15 | 16 | impl Cfg { 17 | fn metrics(&self) -> &Option { 18 | &self.metrics 19 | } 20 | } 21 | 22 | const CFG: &str = r#" 23 | [metrics] 24 | prefix = "example" # If omitted, the name of the application is used 25 | flush-period = "5s" # Dump metric statistics every 5 seconds 26 | backends = [ 27 | { type = "file", filename = "/tmp/metrics.txt" }, 28 | { type = "stdout" }, 29 | ] 30 | "#; 31 | 32 | fn main() { 33 | env_logger::init(); 34 | let root = Monitor::new(); 35 | 36 | Spirit::::new() 37 | .config_defaults(CFG) 38 | .with( 39 | Pipeline::new("metrics") 40 | .extract_cfg(Cfg::metrics) 41 | .install(root.installer(stats_all)), 42 | ) 43 | .run(move |spirit| { 44 | let counter = root.counter("looped"); 45 | while !spirit.is_terminated() { 46 | thread::sleep(Duration::from_millis(100)); 47 | counter.count(1); 48 | debug!("tick"); 49 | } 50 | Ok(()) 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /spirit-hyper/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-hyper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-hyper" 3 | version = "0.9.0" 4 | authors = ["Michal 'vorner' Vaner "] 5 | description = "Hyper helpers for Spirit" 6 | documentation = "https://docs.rs/spirit-hyper" 7 | repository = "https://github.com/vorner/spirit" 8 | categories = ["config", "web-programming"] 9 | keywords = ["http", "hyper", "service", "configuration", "spirit"] 10 | license = "Apache-2.0 OR MIT" 11 | readme = "README.md" 12 | edition = "2018" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = ["cfg-help"] 25 | cfg-help = ["spirit/cfg-help", "spirit-tokio/cfg-help", "structdoc"] 26 | 27 | [dependencies] 28 | err-context = "^0.1" 29 | hyper = { version = "^0.14", features = ["server", "http1", "http2", "runtime"] } 30 | log = "^0.4" 31 | pin-project = "1" 32 | serde = { version = "^1", features = ["derive"] } 33 | spirit = { path = "..", version = "^0.4.8", default-features = false } 34 | spirit-tokio = { path = "../spirit-tokio", version = "^0.9.0", default-features = false, features = ["net"] } 35 | structdoc = { version = "^0.1", optional = true } 36 | structopt = { version = "^0.3", default-features = false } 37 | tokio = { version = "^1", default-features = false, features = ["rt", "sync"] } 38 | 39 | [dev-dependencies] 40 | env_logger = "~0.9" 41 | 42 | [package.metadata.docs.rs] 43 | all-features = true 44 | -------------------------------------------------------------------------------- /spirit-hyper/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-hyper/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-hyper/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-hyper 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | Several helpers to easily integrate hyper with configuration managed by the 6 | [spirit](https://crates.io/crates/spirit) system. 7 | 8 | See the [docs](https://docs.rs/spirit-hyper) and the 9 | [examples](spirit-hyper/examples). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-hyper/examples/hws-hyper.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::convert::Infallible; 3 | use std::sync::Arc; 4 | 5 | use hyper::server::Builder; 6 | use hyper::service::{make_service_fn, service_fn}; 7 | use hyper::{Body, Request, Response}; 8 | use serde::Deserialize; 9 | use spirit::prelude::*; 10 | use spirit::{Empty, Pipeline, Spirit}; 11 | use spirit_hyper::{BuildServer, HttpServer}; 12 | use spirit_tokio::runtime::Tokio; 13 | use tokio::sync::oneshot::Receiver; 14 | 15 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Hash)] 16 | struct Signature { 17 | signature: Option, 18 | } 19 | 20 | #[derive(Default, Deserialize)] 21 | struct Ui { 22 | msg: String, 23 | } 24 | 25 | #[derive(Default, Deserialize)] 26 | struct Config { 27 | /// On which ports (and interfaces) to listen. 28 | /// 29 | /// With some additional configuration about listening, the http server... 30 | /// 31 | /// Also, signature of the given listening port. 32 | listen: HashSet>, 33 | 34 | /// The UI (there's only the message to send). 35 | ui: Ui, 36 | } 37 | 38 | impl Config { 39 | /// A function to extract the HTTP servers configuration 40 | fn listen(&self) -> &HashSet> { 41 | &self.listen 42 | } 43 | } 44 | 45 | const DEFAULT_CONFIG: &str = r#" 46 | [[listen]] 47 | port = 1234 48 | http-mode = "http1-only" 49 | tcp-keepalive = "20s" 50 | backlog = 30 51 | only-v6 = true 52 | 53 | [[listen]] 54 | port = 5678 55 | host = "127.0.0.1" 56 | signature = "local" 57 | 58 | [ui] 59 | msg = "Hello world" 60 | "#; 61 | 62 | async fn hello( 63 | spirit: &Arc>, 64 | cfg: &Arc>, 65 | _req: Request, 66 | ) -> Result, Infallible> { 67 | // Get some global configuration 68 | let mut msg = format!("{}\n", spirit.config().ui.msg); 69 | // Get some listener-local configuration. 70 | if let Some(ref signature) = cfg.transport.listen.extra_cfg.signature { 71 | msg.push_str(&format!("Brought to you by {signature}\n")); 72 | } 73 | Ok(Response::new(Body::from(msg))) 74 | } 75 | 76 | fn main() { 77 | // You could use spirit_log instead to gain better configurability 78 | env_logger::init(); 79 | 80 | Spirit::::new() 81 | .config_defaults(DEFAULT_CONFIG) 82 | .config_exts(["toml", "ini", "json"]) 83 | // Explicitly enabling tokio integration, implicitly it would happen inside run and that's 84 | // too late. 85 | .with_singleton(Tokio::SingleThreaded) // Explicitly enabling tokio 86 | .run(|spirit| { 87 | let spirit_srv = Arc::clone(spirit); 88 | let build_server = move |builder: Builder<_>, 89 | cfg: &HttpServer, 90 | _: &'static str, 91 | shutdown: Receiver<()>| { 92 | let spirit = Arc::clone(&spirit_srv); 93 | let cfg = Arc::new(cfg.clone()); 94 | builder 95 | .serve(make_service_fn(move |_conn| { 96 | let spirit = Arc::clone(&spirit); 97 | let cfg = Arc::clone(&cfg); 98 | async move { 99 | let spirit = Arc::clone(&spirit); 100 | let cfg = Arc::clone(&cfg); 101 | Ok::<_, Infallible>(service_fn(move |req| { 102 | let spirit = Arc::clone(&spirit); 103 | let cfg = Arc::clone(&cfg); 104 | async move { hello(&spirit, &cfg, req).await } 105 | })) 106 | } 107 | })) 108 | .with_graceful_shutdown(async move { 109 | let _ = shutdown.await; // Throw away errors 110 | }) 111 | }; 112 | spirit.with( 113 | Pipeline::new("listen") 114 | .extract_cfg(Config::listen) 115 | .transform(BuildServer(build_server)), 116 | )?; 117 | Ok(()) 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /spirit-log/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-log" 3 | version = "0.4.4" 4 | authors = ["Michal 'vorner' Vaner "] 5 | description = "Spirit helpers and config fragments for logging" 6 | documentation = "https://docs.rs/spirit-log" 7 | repository = "https://github.com/vorner/spirit" 8 | readme = "README.md" 9 | categories = ["config", "development-tools::debugging"] 10 | keywords = ["log", "spirit", "service", "configuration"] 11 | license = "Apache-2.0 OR MIT" 12 | edition = "2018" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | background = ["flume", "either"] 25 | default = ["with-backtrace", "cfg-help"] 26 | with-backtrace = ["log-panics/with-backtrace"] 27 | cfg-help = ["spirit/cfg-help", "structdoc"] 28 | 29 | [dependencies] 30 | chrono = "~0.4" 31 | either = { version = "~1", optional = true } 32 | fern = { version = "~0.6", default-features = false } 33 | flume = { version = "^0.10", optional = true } 34 | itertools = "^0.10" 35 | log = "~0.4" 36 | log-panics = { version = "~2", default-features = false } 37 | log-reroute = "~0.1.2" 38 | serde = { version = "~1", features = ["derive"] } 39 | serde_json = "~1" 40 | spirit = { version = "~0.4.0", path = "..", default-features = false } 41 | structdoc = { version = "~0.1", optional = true } 42 | structopt = { version = "~0.3", default-features = false } 43 | syslog = { version = "~5", optional = true } 44 | 45 | [package.metadata.docs.rs] 46 | all-features = true 47 | -------------------------------------------------------------------------------- /spirit-log/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-log/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-log/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-log 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | Helpers and configuration fragments to integrate logging into the spirit 6 | configuration framework. 7 | 8 | See the [docs](https://docs.rs/spirit-log) and the 9 | [examples](spirit-log/examples). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-log/examples/background.rs: -------------------------------------------------------------------------------- 1 | //! Unlike the logging example, this one moves the actual writing of logs (potentially over network 2 | //! or to a slow HDD) to a separate thread as not to block the main application. 3 | 4 | // A trick, just disable the compilation of the example if the needed feature is not turned on. Not 5 | // actually part of the example itself. 6 | // Is there a better way? 7 | #[cfg(feature = "background")] 8 | mod everything { 9 | use std::thread; 10 | use std::time::Duration; 11 | 12 | use log::info; 13 | use serde::Deserialize; 14 | use spirit::prelude::*; 15 | use spirit::{Pipeline, Spirit}; 16 | use spirit_log::{ 17 | Background, Cfg as LogCfg, CfgAndOpts as LogBoth, FlushGuard, Opts as LogOpts, OverflowMode, 18 | }; 19 | use structopt::StructOpt; 20 | 21 | #[derive(Clone, Debug, StructOpt)] 22 | struct Opts { 23 | #[structopt(flatten)] 24 | logging: LogOpts, 25 | } 26 | 27 | impl Opts { 28 | fn logging(&self) -> LogOpts { 29 | self.logging.clone() 30 | } 31 | } 32 | 33 | #[derive(Clone, Debug, Default, Deserialize)] 34 | struct Ui { 35 | msg: String, 36 | sleep_ms: u64, 37 | } 38 | 39 | #[derive(Clone, Debug, Default, Deserialize)] 40 | struct Cfg { 41 | #[serde(default, skip_serializing_if = "LogCfg::is_empty")] 42 | logging: LogCfg, 43 | ui: Ui, 44 | } 45 | 46 | impl Cfg { 47 | fn logging(&self) -> LogCfg { 48 | self.logging.clone() 49 | } 50 | } 51 | 52 | const DEFAULT_CONFIG: &str = r#" 53 | [[logging]] 54 | level = "INFO" 55 | type = "stderr" 56 | 57 | [[logging]] 58 | level = "DEBUG" 59 | type = "file" 60 | filename = "/tmp/example.log" 61 | clock = "UTC" 62 | 63 | [ui] 64 | msg = "Hello!" 65 | sleep_ms = 100 66 | "#; 67 | 68 | pub(crate) fn run() { 69 | Spirit::::new() 70 | .config_defaults(DEFAULT_CONFIG) 71 | .config_exts(["toml", "ini", "json"]) 72 | .with( 73 | Pipeline::new("logging") 74 | .extract(|opts: &Opts, cfg: &Cfg| LogBoth { 75 | cfg: cfg.logging(), 76 | opts: opts.logging(), 77 | }) 78 | // Transforms the logger to the background one. 79 | .transform(Background::new(100, OverflowMode::Block)), 80 | ) 81 | // This makes sure the logs are flushed on termination. 82 | .with_singleton(FlushGuard) 83 | .run(|spirit| { 84 | while !spirit.is_terminated() { 85 | let cfg = spirit.config(); 86 | info!("{}", cfg.ui.msg); 87 | thread::sleep(Duration::from_millis(cfg.ui.sleep_ms)); 88 | } 89 | Ok(()) 90 | }); 91 | } 92 | } 93 | 94 | fn main() { 95 | #[cfg(feature = "background")] 96 | everything::run(); 97 | } 98 | -------------------------------------------------------------------------------- /spirit-log/examples/logging.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | 4 | use log::info; 5 | use serde::Deserialize; 6 | use spirit::prelude::*; 7 | use spirit::{Pipeline, Spirit}; 8 | use spirit_log::{Cfg as LogCfg, CfgAndOpts as LogBoth, Opts as LogOpts}; 9 | use structopt::StructOpt; 10 | 11 | #[derive(Clone, Debug, StructOpt)] 12 | struct Opts { 13 | #[structopt(flatten)] 14 | logging: LogOpts, 15 | } 16 | 17 | impl Opts { 18 | fn logging(&self) -> LogOpts { 19 | self.logging.clone() 20 | } 21 | } 22 | 23 | #[derive(Clone, Debug, Default, Deserialize)] 24 | struct Ui { 25 | msg: String, 26 | sleep_ms: u64, 27 | } 28 | 29 | #[derive(Clone, Debug, Default, Deserialize)] 30 | struct Cfg { 31 | #[serde(default, skip_serializing_if = "LogCfg::is_empty")] 32 | logging: LogCfg, 33 | ui: Ui, 34 | } 35 | 36 | impl Cfg { 37 | fn logging(&self) -> LogCfg { 38 | self.logging.clone() 39 | } 40 | } 41 | 42 | const DEFAULT_CONFIG: &str = r#" 43 | [[logging]] 44 | level = "INFO" 45 | type = "stderr" 46 | 47 | [[logging]] 48 | level = "DEBUG" 49 | type = "file" 50 | filename = "/tmp/example.log" 51 | clock = "UTC" 52 | 53 | [ui] 54 | msg = "Hello!" 55 | sleep_ms = 100 56 | "#; 57 | 58 | fn main() { 59 | Spirit::::new() 60 | .config_defaults(DEFAULT_CONFIG) 61 | .config_exts(["toml", "ini", "json"]) 62 | .with( 63 | Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| LogBoth { 64 | cfg: cfg.logging(), 65 | opts: opts.logging(), 66 | }), 67 | ) 68 | .run(|spirit| { 69 | while !spirit.is_terminated() { 70 | let cfg = spirit.config(); 71 | info!("{}", cfg.ui.msg); 72 | thread::sleep(Duration::from_millis(cfg.ui.sleep_ms)); 73 | } 74 | Ok(()) 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /spirit-reqwest/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-reqwest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-reqwest" 3 | version = "0.5.1" 4 | authors = ["Michal 'vorner' Vaner "] 5 | edition = "2018" 6 | description = "Reqwest helpers for Spirit" 7 | documentation = "https://docs.rs/spirit-reqwest" 8 | repository = "https://github.com/vorner/spirit" 9 | license = "Apache-2.0 OR MIT" 10 | categories = ["config", "web-programming"] 11 | keywords = ["http", "reqwest", "service", "configuration", "spirit"] 12 | readme = "README.md" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [badges] 20 | travis-ci = { repository = "vorner/spirit" } 21 | maintenance = { status = "actively-developed" } 22 | 23 | [features] 24 | default = ["cfg-help"] 25 | cfg-help = ["spirit/cfg-help", "structdoc"] 26 | 27 | blocking = ["reqwest/blocking"] 28 | brotli = ["reqwest/brotli"] 29 | gzip = ["reqwest/gzip"] 30 | native-tls = ["reqwest/native-tls"] 31 | 32 | [dependencies] 33 | arc-swap = "^1" 34 | err-context = "^0.1" 35 | log = "^0.4" 36 | # Yes, depend on default-features. That means default-tls, we don't want to disable so many parts of the code 37 | reqwest = "^0.11.4" 38 | serde = { version = "^1", features = ["derive"] } 39 | spirit = { version = "^0.4.8", path = "..", default-features = false } 40 | structdoc = { version = "^0.1", optional = true } 41 | url = { version = "^2", features = ["serde"] } 42 | 43 | [dev-dependencies] 44 | env_logger = "^0.9" 45 | tokio = { version = "^1", features = ["full"] } 46 | 47 | [package.metadata.docs.rs] 48 | all-features = true 49 | -------------------------------------------------------------------------------- /spirit-reqwest/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-reqwest/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-reqwest/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-hyper 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | A helper to create configured [reqwest](https://crates.io/crates/reqwest) 6 | client and a helper to keep one around with up to date configuration. 7 | It is part of the [spirit](https://crates.io/crates/spirit) system. 8 | 9 | See the [docs](https://docs.rs/spirit-reqwest) and the 10 | [examples](spirit-reqwest/examples). 11 | 12 | ## License 13 | 14 | Licensed under either of 15 | 16 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 17 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 18 | 19 | at your option. 20 | 21 | ### Contribution 22 | 23 | Unless you explicitly state otherwise, any contribution intentionally 24 | submitted for inclusion in the work by you, as defined in the Apache-2.0 25 | license, shall be dual licensed as above, without any additional terms 26 | or conditions. 27 | -------------------------------------------------------------------------------- /spirit-reqwest/examples/make_request_async.rs: -------------------------------------------------------------------------------- 1 | use err_context::AnyError; 2 | use reqwest::Client; 3 | use serde::Deserialize; 4 | use spirit::prelude::*; 5 | use spirit::{Empty, Pipeline, Spirit}; 6 | use spirit_reqwest::futures::{AtomicClient, IntoClient}; 7 | use spirit_reqwest::ReqwestClient; 8 | use tokio::runtime::Runtime; 9 | 10 | #[derive(Debug, Default, Deserialize)] 11 | struct Cfg { 12 | #[serde(default)] 13 | client: ReqwestClient, 14 | } 15 | 16 | impl Cfg { 17 | fn client(&self) -> &ReqwestClient { 18 | &self.client 19 | } 20 | } 21 | 22 | const DEFAULT_CFG: &str = r#" 23 | [client] 24 | timeout = "5s" 25 | enable-gzip = false 26 | "#; 27 | 28 | async fn make_request(client: &Client) -> Result<(), AnyError> { 29 | let page = client 30 | .get("https://www.rust-lang.org") 31 | .send() 32 | .await? 33 | .error_for_status()? 34 | .text() 35 | .await?; 36 | println!("{page}"); 37 | Ok(()) 38 | } 39 | 40 | fn main() { 41 | env_logger::init(); 42 | // The ::empty client would panic if used before it is configured 43 | let client = AtomicClient::empty(); 44 | Spirit::::new() 45 | .config_defaults(DEFAULT_CFG) 46 | .with( 47 | Pipeline::new("http client") 48 | .extract_cfg(Cfg::client) 49 | .transform(IntoClient) 50 | .install(client.clone()), 51 | ) 52 | .run(move |_| { 53 | let runtime = Runtime::new()?; 54 | // But by now, spirit already stored the configured client in there. Also, if we were 55 | // running for a longer time, it would replace it with a new one every time we change 56 | // the configuration. 57 | let client = client.client(); 58 | runtime.block_on(make_request(&client)) 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /spirit-reqwest/examples/make_request_blocking.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "blocking")] 2 | mod example { 3 | use serde::Deserialize; 4 | use spirit::prelude::*; 5 | use spirit::{Empty, Pipeline, Spirit}; 6 | use spirit_reqwest::blocking::{AtomicClient, IntoClient}; 7 | use spirit_reqwest::ReqwestClient; 8 | 9 | #[derive(Debug, Default, Deserialize)] 10 | struct Cfg { 11 | #[serde(default)] 12 | client: ReqwestClient, 13 | } 14 | 15 | impl Cfg { 16 | fn client(&self) -> &ReqwestClient { 17 | &self.client 18 | } 19 | } 20 | 21 | const DEFAULT_CFG: &str = r#" 22 | [client] 23 | timeout = "5s" 24 | enable-gzip = false 25 | "#; 26 | 27 | pub fn main() { 28 | env_logger::init(); 29 | // The ::empty client would panic if used before it is configured 30 | let client = AtomicClient::empty(); 31 | Spirit::::new() 32 | .config_defaults(DEFAULT_CFG) 33 | .with( 34 | Pipeline::new("http client") 35 | .extract_cfg(Cfg::client) 36 | .transform(IntoClient) 37 | .install(client.clone()), 38 | ) 39 | .run(move |_| { 40 | // But by now, spirit already stored the configured client in there. Also, if we were 41 | // running for a longer time, it would replace it with a new one every time we change 42 | // the configuration. 43 | let page = client 44 | .get("https://www.rust-lang.org") 45 | .send()? 46 | .error_for_status()? 47 | .text()?; 48 | println!("{}", page); 49 | Ok(()) 50 | }); 51 | } 52 | } 53 | 54 | #[cfg(not(feature = "blocking"))] 55 | mod example { 56 | pub fn main() {} 57 | } 58 | 59 | // trick to deal with enabling/disabling the blocking feature 60 | fn main() { 61 | example::main() 62 | } 63 | -------------------------------------------------------------------------------- /spirit-tokio/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /spirit-tokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirit-tokio" 3 | version = "0.9.2" 4 | authors = ["Michal 'vorner' Vaner "] 5 | description = "Tokio helpers for Spirit" 6 | documentation = "https://docs.rs/spirit-tokio" 7 | repository = "https://github.com/vorner/spirit" 8 | license = "Apache-2.0 OR MIT" 9 | readme = "README.md" 10 | categories = ["config", "network-programming"] 11 | keywords = ["async", "tokio", "service", "configuration", "spirit"] 12 | edition = "2018" 13 | include = [ 14 | "Cargo.toml", 15 | "README.md", 16 | "src/**", 17 | ] 18 | 19 | [features] 20 | # TODO: Cut down on the default features a bit 21 | default = ["cfg-help", "rt-from-cfg", "net", "stream"] 22 | 23 | cfg-help = ["spirit/cfg-help", "structdoc"] 24 | multithreaded = ["tokio/rt-multi-thread"] 25 | net = ["humantime", "pin-project", "socket2", "tokio/net", "tokio/time"] 26 | rt-from-cfg = ["multithreaded", "tokio/time", "num_cpus"] 27 | stream = ["tokio-stream"] 28 | 29 | futures = ["futures-util"] 30 | 31 | [badges] 32 | travis-ci = { repository = "vorner/spirit" } 33 | maintenance = { status = "actively-developed" } 34 | 35 | [dependencies] 36 | either = { version = "^1", optional = true } 37 | futures-util = { version = "~0.3", optional = true, default-features = false } 38 | err-context = "^0.1" 39 | log = "^0.4" 40 | humantime = { version = "^2", optional = true } 41 | socket2 = { version = "^0.4", optional = true, features = ["all"] } 42 | num_cpus = { version = "^1", optional = true } 43 | pin-project = { version = "1", optional = true } 44 | serde = { version = "^1", features = ["derive"] } 45 | spirit = { version = "^0.4.6", path = "..", default-features = false } 46 | structdoc = { version = "^0.1", optional = true } 47 | structopt = { version = "^0.3", default-features = false } 48 | tokio = { version = "^1", default-features = false, features = ["macros", "rt", "sync"] } 49 | tokio-stream = { version = "^0.1", optional = true } 50 | 51 | [dev-dependencies] 52 | arc-swap = "^1" 53 | env_logger = "~0.9" 54 | serde_json = "^1" 55 | tokio = { version = "^1", features = ["io-util"] } 56 | 57 | [package.metadata.docs.rs] 58 | all-features = true 59 | -------------------------------------------------------------------------------- /spirit-tokio/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /spirit-tokio/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /spirit-tokio/README.md: -------------------------------------------------------------------------------- 1 | # Spirit-tokio 2 | 3 | [![Travis Build Status](https://api.travis-ci.org/vorner/spirit.png?branch=master)](https://travis-ci.org/vorner/spirit) 4 | 5 | Several helpers to easily integrate tokio with configuration managed by the 6 | [spirit](https://crates.io/crates/spirit) system. 7 | 8 | See the [docs](https://docs.rs/spirit-tokio) and the 9 | [examples](spirit-tokio/examples). 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | ### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally 23 | submitted for inclusion in the work by you, as defined in the Apache-2.0 24 | license, shall be dual licensed as above, without any additional terms 25 | or conditions. 26 | -------------------------------------------------------------------------------- /spirit-tokio/examples/hws-tokio.rs: -------------------------------------------------------------------------------- 1 | //! A tokio-based hello world service. 2 | //! 3 | //! Look at hws.rs in core spirit first, that one is simpler. 4 | //! 5 | //! Unlike that one, it supports reconfiguring of everything ‒ including the ports it listens on. 6 | //! 7 | //! # The configuration helpers 8 | //! 9 | //! The port reconfiguration is done by using a helper. By using the provided struct inside the 10 | //! configuration, the helper is able to spawn and shut down tasks inside tokio as needed. You only 11 | //! need to provide it with a function to extract that bit of configuration, the action to take (in 12 | //! case of TCP, the action is handling one incoming connection) and a name (which is used in 13 | //! logs). 14 | 15 | use std::collections::HashSet; 16 | use std::sync::Arc; 17 | 18 | use arc_swap::ArcSwap; 19 | use log::{debug, warn}; 20 | use serde::Deserialize; 21 | use spirit::prelude::*; 22 | use spirit::{AnyError, Empty, Pipeline, Spirit}; 23 | use spirit_tokio::handlers::PerConnection; 24 | use spirit_tokio::net::limits::Tracked; 25 | use spirit_tokio::net::TcpListenWithLimits; 26 | use spirit_tokio::runtime::Config as TokioCfg; 27 | use spirit_tokio::Tokio; 28 | use tokio::io::AsyncWriteExt; 29 | use tokio::net::TcpStream; 30 | 31 | // Configuration. It has the same shape as the one in spirit's hws.rs. 32 | 33 | #[derive(Default, Deserialize)] 34 | struct Ui { 35 | msg: String, 36 | } 37 | 38 | #[derive(Default, Deserialize)] 39 | struct Config { 40 | /// On which ports (and interfaces) to listen. 41 | listen: HashSet, 42 | 43 | /// The UI (there's only the message to send). 44 | ui: Ui, 45 | 46 | /// Threadpool to do the async work. 47 | #[serde(default)] 48 | threadpool: TokioCfg, 49 | } 50 | 51 | impl Config { 52 | /// A function to extract the tcp ports configuration. 53 | fn listen(&self) -> &HashSet { 54 | &self.listen 55 | } 56 | 57 | /// Extraction of the threadpool configuration 58 | fn threadpool(&self) -> TokioCfg { 59 | self.threadpool.clone() 60 | } 61 | } 62 | 63 | const DEFAULT_CONFIG: &str = r#" 64 | [threadpool] 65 | core-threads = 2 66 | max-threads = 4 67 | 68 | [[listen]] 69 | port = 1234 70 | max-conn = 30 71 | error-sleep = "50ms" 72 | reuse-addr = true 73 | 74 | [[listen]] 75 | port = 5678 76 | host = "127.0.0.1" 77 | 78 | [ui] 79 | msg = "Hello world" 80 | "#; 81 | 82 | async fn handle_connection(conn: &mut Tracked, cfg: &Config) -> Result<(), AnyError> { 83 | let msg = format!("{}\n", cfg.ui.msg); 84 | conn.write_all(msg.as_bytes()).await?; 85 | conn.shutdown().await?; 86 | Ok(()) 87 | } 88 | 89 | pub fn main() { 90 | env_logger::init(); 91 | let cfg = Arc::new(ArcSwap::default()); 92 | let cfg_store = Arc::clone(&cfg); 93 | let conn_handler = move |mut conn: Tracked, _: &_| { 94 | let cfg = cfg.load_full(); 95 | async move { 96 | let addr = conn 97 | .peer_addr() 98 | .map(|a| a.to_string()) 99 | .unwrap_or_else(|_| "".to_owned()); 100 | debug!("Handling connection {}", addr); 101 | if let Err(e) = handle_connection(&mut conn, &cfg).await { 102 | warn!("Failed to handle connection {}: {}", addr, e); 103 | } 104 | } 105 | }; 106 | Spirit::::new() 107 | .on_config(move |_, cfg: &Arc| cfg_store.store(Arc::clone(cfg))) 108 | .config_defaults(DEFAULT_CONFIG) 109 | .config_exts(["toml", "ini", "json"]) 110 | .with_singleton(Tokio::from_cfg(Config::threadpool)) 111 | .with( 112 | Pipeline::new("listen") 113 | .extract_cfg(Config::listen) 114 | .transform(PerConnection(conn_handler)), 115 | ) 116 | .run(|_| Ok(())); 117 | } 118 | -------------------------------------------------------------------------------- /spirit-tokio/examples/msg_print.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::time::Duration; 4 | 5 | use err_context::AnyError; 6 | use serde::{Deserialize, Serialize}; 7 | use spirit::fragment::driver::CacheEq; 8 | use spirit::prelude::*; 9 | use spirit::{Empty, Pipeline, Spirit}; 10 | use spirit_tokio::runtime::Config as TokioCfg; 11 | use spirit_tokio::{FutureInstaller, Tokio}; 12 | use structdoc::StructDoc; 13 | 14 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, StructDoc)] 15 | #[serde(default)] 16 | struct MsgCfg { 17 | /// A message to print now and then. 18 | msg: String, 19 | /// Time between printing the message. 20 | interval: Duration, 21 | } 22 | 23 | impl MsgCfg { 24 | async fn run(self) { 25 | loop { 26 | println!("{}", self.msg); 27 | tokio::time::sleep(self.interval).await; 28 | } 29 | } 30 | } 31 | 32 | impl Default for MsgCfg { 33 | fn default() -> Self { 34 | MsgCfg { 35 | msg: "Hello".to_owned(), 36 | interval: Duration::from_secs(1), 37 | } 38 | } 39 | } 40 | 41 | spirit::simple_fragment! { 42 | impl Fragment for MsgCfg { 43 | type Driver = CacheEq; 44 | type Resource = Pin + Send>>; 45 | type Installer = FutureInstaller; 46 | fn create(&self, _: &'static str) -> Result { 47 | let fut = self.clone().run(); 48 | Ok(Box::pin(fut)) 49 | } 50 | } 51 | } 52 | 53 | /// An application. 54 | #[derive(Default, Deserialize, Serialize, StructDoc)] 55 | struct AppConfig { 56 | #[serde(flatten)] 57 | msg: MsgCfg, 58 | 59 | /// Configuration of the asynchronous tokio runtime. 60 | #[serde(default)] 61 | threadpool: TokioCfg, 62 | } 63 | 64 | impl AppConfig { 65 | fn threadpool(&self) -> TokioCfg { 66 | self.threadpool.clone() 67 | } 68 | 69 | fn msg(&self) -> &MsgCfg { 70 | &self.msg 71 | } 72 | } 73 | 74 | fn main() { 75 | // You'd use spirit-log here instead probably, but we don't want to have cross-dependencies in 76 | // the example. 77 | env_logger::init(); 78 | Spirit::::new() 79 | .with_singleton(Tokio::from_cfg(AppConfig::threadpool)) 80 | .with(Pipeline::new("Msg").extract_cfg(AppConfig::msg)) 81 | .run(|_| Ok(())) 82 | } 83 | -------------------------------------------------------------------------------- /spirit-tokio/src/handlers.rs: -------------------------------------------------------------------------------- 1 | //! Various [`Transformation`]s for working with resources in async contexts. 2 | //! 3 | //! Oftentimes, one needs to transform some resource into a future and spawn it into the runtime. 4 | //! While possible to do manually, the types here might be a bit more comfortable. 5 | 6 | use std::future::Future; 7 | #[cfg(feature = "net")] 8 | use std::pin::Pin; 9 | #[cfg(feature = "net")] 10 | use std::task::{Context, Poll}; 11 | 12 | use err_context::AnyError; 13 | 14 | #[cfg(feature = "net")] 15 | use super::net::Accept; 16 | use super::FutureInstaller; 17 | #[cfg(feature = "net")] 18 | use log::error; 19 | use log::trace; 20 | use pin_project::pin_project; 21 | use spirit::fragment::Transformation; 22 | 23 | /// A [`Transformation`] to take a resource, turn it into a future and install it. 24 | pub struct ToFuture(pub F); 25 | 26 | impl Transformation for ToFuture 27 | where 28 | F: FnMut(R, &SF) -> Result, 29 | Fut: Future + 'static, 30 | { 31 | type OutputResource = Fut; 32 | type OutputInstaller = FutureInstaller; 33 | fn installer(&mut self, _: II, _: &str) -> FutureInstaller { 34 | FutureInstaller 35 | } 36 | fn transform(&mut self, r: R, cfg: &SF, name: &str) -> Result { 37 | trace!("Wrapping {} into a future", name); 38 | (self.0)(r, cfg) 39 | } 40 | } 41 | 42 | /// A [`Transformation`] to take a resource, turn it into a future and install it. 43 | /// 44 | /// Unlike [`ToFuture`], this one doesn't pass the configuration to the closure. 45 | /// 46 | /// # Examples 47 | /// 48 | /// This is mostly the same example as the one at the crate root, but done slightly differently. 49 | /// The future is created later on, during the transformation phase. While a little bit more 50 | /// verbose here, this comes with two advantages: 51 | /// 52 | /// * Works with already provided fragments, like the network primitives in [`net`][crate::net]. In 53 | /// that case you might want to prefer the [`ToFuture`], as it also gives access to the original 54 | /// configuration fragment, including any extra configuration for the future. 55 | /// * The future can be an arbitrary anonymous/unnameable type (eg. `impl Future` or an `async` 56 | /// function), there's no need for boxing. This might have slight positive effect on performance. 57 | /// 58 | /// ```rust 59 | /// use std::time::Duration; 60 | /// 61 | /// use err_context::AnyError; 62 | /// use serde::{Deserialize, Serialize}; 63 | /// use spirit::{Empty, Pipeline, Spirit}; 64 | /// use spirit::prelude::*; 65 | /// use spirit::fragment::driver::CacheEq; 66 | /// use spirit_tokio::handlers::ToFutureUnconfigured; 67 | /// use structdoc::StructDoc; 68 | /// 69 | /// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, StructDoc)] 70 | /// #[serde(default)] 71 | /// struct MsgCfg { 72 | /// /// A message to print now and then. 73 | /// msg: String, 74 | /// /// Time between printing the message. 75 | /// interval: Duration, 76 | /// } 77 | /// 78 | /// impl MsgCfg { 79 | /// async fn run(self) { 80 | /// loop { 81 | /// println!("{}", self.msg); 82 | /// tokio::time::sleep(self.interval).await; 83 | /// } 84 | /// } 85 | /// } 86 | /// 87 | /// impl Default for MsgCfg { 88 | /// fn default() -> Self { 89 | /// MsgCfg { 90 | /// msg: "Hello".to_owned(), 91 | /// interval: Duration::from_secs(1), 92 | /// } 93 | /// } 94 | /// } 95 | /// 96 | /// spirit::simple_fragment! { 97 | /// impl Fragment for MsgCfg { 98 | /// type Driver = CacheEq; 99 | /// // We simply send the configuration forward 100 | /// type Resource = Self; 101 | /// type Installer = (); 102 | /// fn create(&self, _: &'static str) -> Result { 103 | /// Ok(self.clone()) 104 | /// } 105 | /// } 106 | /// } 107 | /// 108 | /// /// An application. 109 | /// #[derive(Default, Deserialize, Serialize, StructDoc)] 110 | /// struct AppConfig { 111 | /// #[serde(flatten)] 112 | /// msg: MsgCfg, 113 | /// } 114 | /// 115 | /// impl AppConfig { 116 | /// fn msg(&self) -> &MsgCfg { 117 | /// &self.msg 118 | /// } 119 | /// } 120 | /// 121 | /// fn main() { 122 | /// Spirit::::new() 123 | /// // Will install and possibly cancel and replace the future if the config changes. 124 | /// .with( 125 | /// Pipeline::new("Msg") 126 | /// .extract_cfg(AppConfig::msg) 127 | /// // This thing turns it into the future and sets how to install it. 128 | /// .transform(ToFutureUnconfigured(MsgCfg::run)) 129 | /// ) 130 | /// // Just an empty body here. 131 | /// .run(|spirit| { 132 | /// // Usually, one would terminate by CTRL+C, but we terminate from here to make sure 133 | /// // the example finishes. 134 | /// spirit.terminate(); 135 | /// Ok(()) 136 | /// }) 137 | /// } 138 | /// ``` 139 | pub struct ToFutureUnconfigured(pub F); 140 | 141 | impl Transformation for ToFutureUnconfigured 142 | where 143 | F: FnMut(R) -> Fut, 144 | Fut: Future + 'static, 145 | { 146 | type OutputResource = Fut; 147 | type OutputInstaller = FutureInstaller; 148 | fn installer(&mut self, _: II, _: &str) -> FutureInstaller { 149 | FutureInstaller 150 | } 151 | fn transform(&mut self, r: R, _: &SF, name: &str) -> Result { 152 | trace!("Wrapping {} into a future", name); 153 | Ok((self.0)(r)) 154 | } 155 | } 156 | 157 | /// A plumbing type for [`PerConnection`]. 158 | #[cfg(feature = "net")] 159 | #[pin_project] 160 | pub struct Acceptor { 161 | #[pin] 162 | accept: A, 163 | f: F, 164 | cfg: C, 165 | name: &'static str, 166 | } 167 | 168 | #[cfg(feature = "net")] 169 | impl Future for Acceptor 170 | where 171 | A: Accept, 172 | F: FnMut(A::Connection, &C) -> Fut, 173 | Fut: Future + Send + 'static, 174 | { 175 | type Output = (); 176 | fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<()> { 177 | let mut me = self.project(); 178 | loop { 179 | match me.accept.as_mut().poll_accept(ctx) { 180 | Poll::Ready(Err(e)) => { 181 | error!("Giving up acceptor {}: {}", me.name, e); 182 | return Poll::Ready(()); 183 | } 184 | Poll::Ready(Ok(conn)) => { 185 | trace!("Got a new connection on {}", me.name); 186 | // Poking the borrow checker around the un-pinning, otherwise it is unhappy 187 | let fut = (me.f)(conn, me.cfg); 188 | tokio::spawn(fut); 189 | } 190 | Poll::Pending => return Poll::Pending, 191 | } 192 | } 193 | } 194 | } 195 | 196 | /// A [`Transformation`] that creates a new future from each accepted connection. 197 | /// 198 | /// For each connection yielded from the acceptor (passed in as the input resource), the function 199 | /// is used to create a new future. That new future is spawned into [`tokio`] runtime. Note that 200 | /// even when this resource gets uninstalled, the spawned futures from the connections are left 201 | /// running. 202 | /// 203 | /// In case the acceptor yields an error, it is dropped and not used any more. Note that some 204 | /// primitives, like [`TcpListenWithLimits`][crate::net::TcpListenWithLimits] handles errors 205 | /// internally and never returns any, therefore it might be a good candidate for long-running 206 | /// servers. 207 | /// 208 | /// # Examples 209 | #[cfg(feature = "net")] 210 | pub struct PerConnection(pub F); 211 | 212 | #[cfg(feature = "net")] 213 | impl Transformation for PerConnection 214 | where 215 | A: Accept, 216 | F: Clone + FnMut(A::Connection, &SF) -> Fut + 'static, 217 | Fut: Future + 'static, 218 | SF: Clone + 'static, 219 | { 220 | type OutputResource = Acceptor; 221 | type OutputInstaller = FutureInstaller; 222 | fn installer(&mut self, _: II, _: &str) -> FutureInstaller { 223 | FutureInstaller 224 | } 225 | fn transform( 226 | &mut self, 227 | accept: A, 228 | cfg: &SF, 229 | name: &'static str, 230 | ) -> Result, AnyError> { 231 | trace!("Creating new acceptor for {}", name); 232 | let f = self.0.clone(); 233 | let cfg = cfg.clone(); 234 | Ok(Acceptor { 235 | accept, 236 | f, 237 | cfg, 238 | name, 239 | }) 240 | } 241 | } 242 | 243 | /// A more flexible (and mind-bending) version of [`PerConnection`]. 244 | /// 245 | /// The [`PerConnection`] applies a closure to each accepted connection. To share the closure 246 | /// between all listeners, the closure is cloned. 247 | /// 248 | /// This version's closure is higher-level closure. It is called once for each listener to produce 249 | /// a per-listener closure to handle its connections. In effect, it allows for custom „cloning“ of 250 | /// the closure. 251 | #[cfg(feature = "net")] 252 | pub struct PerConnectionInit(pub F); 253 | 254 | #[cfg(feature = "net")] 255 | impl Transformation for PerConnectionInit 256 | where 257 | A: Accept, 258 | FA: FnMut(&A, &SF) -> FC + 'static, 259 | FC: FnMut(A::Connection, &SF) -> Fut + 'static, 260 | Fut: Future + 'static, 261 | SF: Clone + 'static, 262 | { 263 | type OutputResource = Acceptor; 264 | type OutputInstaller = FutureInstaller; 265 | fn installer(&mut self, _: II, _: &str) -> FutureInstaller { 266 | FutureInstaller 267 | } 268 | fn transform( 269 | &mut self, 270 | accept: A, 271 | cfg: &SF, 272 | name: &'static str, 273 | ) -> Result, AnyError> { 274 | trace!("Creating new acceptor for {}", name); 275 | let f = (self.0)(&accept, cfg); 276 | let cfg = cfg.clone(); 277 | Ok(Acceptor { 278 | accept, 279 | f, 280 | cfg, 281 | name, 282 | }) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /spirit-tokio/src/installer.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::mutex_atomic)] // Mutex needed for condvar 2 | //! Installer of futures. 3 | //! 4 | //! The [`FutureInstaller`] is an [`Installer`] that allows installing (spawning) futures, but also 5 | //! canceling them when they are no longer required by the configuration. 6 | //! 7 | //! [`FutureInstaller`]: crate::installer::FutureInstaller 8 | //! [`Installer`]: spirit::fragment::Installer 9 | 10 | use std::future::Future; 11 | use std::sync::{Arc, Condvar, Mutex}; 12 | 13 | use err_context::AnyError; 14 | use log::trace; 15 | use serde::de::DeserializeOwned; 16 | use spirit::extension::Extensible; 17 | use spirit::fragment::Installer; 18 | use structopt::StructOpt; 19 | use tokio::select; 20 | use tokio::sync::oneshot::{self, Sender}; 21 | 22 | use crate::runtime::{self, ShutGuard, Tokio}; 23 | 24 | /// Wakeup async -> sync. 25 | #[derive(Default, Debug)] 26 | struct Wakeup { 27 | wakeup: Mutex, 28 | condvar: Condvar, 29 | } 30 | 31 | impl Wakeup { 32 | fn wait(&self) { 33 | trace!("Waiting on wakeup on {:p}/{:?}", self, self); 34 | let g = self.wakeup.lock().unwrap(); 35 | let _g = self.condvar.wait_while(g, |w| !*w).unwrap(); 36 | } 37 | 38 | fn wakeup(&self) { 39 | trace!("Waking up {:p}/{:?}", self, self); 40 | // Expected to be unlocked all the time. 41 | *self.wakeup.lock().unwrap() = true; 42 | self.condvar.notify_all(); 43 | } 44 | } 45 | 46 | /// An [`UninstallHandle`] for the [`FutureInstaller`]. 47 | /// 48 | /// This allows to cancel a future when this handle is dropped and wait for it to happen. It is not 49 | /// publicly creatable. 50 | /// 51 | /// [`UninstallHandle`]: Installer::UninstallHandle 52 | pub struct RemoteDrop { 53 | name: &'static str, 54 | request_drop: Option>, 55 | wakeup: Arc, 56 | // Prevent the tokio runtime from shutting down too soon, as long as the resource is still 57 | // alive. We want to remove it first gracefully. 58 | _shut_guard: Option, 59 | } 60 | 61 | impl Drop for RemoteDrop { 62 | fn drop(&mut self) { 63 | trace!("Requesting remote drop on {}", self.name); 64 | // Ask the other side to drop the thing 65 | let _ = self.request_drop.take().unwrap().send(()); 66 | // And wait for it to actually happen 67 | self.wakeup.wait(); 68 | trace!("Remote drop done on {}", self.name); 69 | } 70 | } 71 | 72 | struct SendOnDrop(Arc); 73 | 74 | impl Drop for SendOnDrop { 75 | fn drop(&mut self) { 76 | self.0.wakeup(); 77 | } 78 | } 79 | 80 | /// An installer able to install (an uninstall) long-running futures. 81 | /// 82 | /// This is an installer that can be used with [`Fragment`] that produce futures. 83 | /// 84 | /// * If the spirit does not contain a [`Tokio`] runtime yet, one (the default one) will be added 85 | /// as a singleton. Note that this has effect on how spirit manages lifetime of the application. 86 | /// * An uninstallation is performed by dropping canceling the future. 87 | /// * This works with both concrete types (implementing `Future + Send`) and boxed 88 | /// ones. Note that the boxed ones are `Pin + Send>>`, created by 89 | /// [`Box::pin`]. 90 | /// 91 | /// See the crate level examples for details how to use (the installer is used only as the 92 | /// associated type in the fragment implementation). 93 | /// 94 | /// [`Fragment`]: spirit::fragment::Fragment 95 | #[derive(Copy, Clone, Debug, Default)] 96 | pub struct FutureInstaller; 97 | 98 | impl Installer for FutureInstaller 99 | where 100 | F: Future + Send + 'static, 101 | { 102 | type UninstallHandle = RemoteDrop; 103 | 104 | fn install(&mut self, fut: F, name: &'static str) -> RemoteDrop { 105 | let (request_send, request_recv) = oneshot::channel(); 106 | let wakeup = Default::default(); 107 | 108 | // Make sure we can terminate the future remotely/uninstall it. 109 | // (Is there a more lightweight version in tokio than bringing in the macros & doing an 110 | // async block? The futures crate has select function, but we don't have futures as dep and 111 | // bringing it just for this feels… heavy) 112 | let guard = SendOnDrop(Arc::clone(&wakeup)); 113 | let cancellable_future = async move { 114 | // Send the confirmation we're done by RAII. This works with panics and shutdown. 115 | let _guard = guard; 116 | 117 | select! { 118 | _ = request_recv => trace!("Future {} requested to terminate", name), 119 | _ = fut => trace!("Future {} terminated on its own", name), 120 | }; 121 | }; 122 | 123 | trace!("Installing future {}", name); 124 | tokio::spawn(cancellable_future); 125 | 126 | RemoteDrop { 127 | name, 128 | request_drop: Some(request_send), 129 | wakeup, 130 | _shut_guard: runtime::shut_guard(), 131 | } 132 | } 133 | 134 | fn init(&mut self, ext: E, _name: &'static str) -> Result 135 | where 136 | E: Extensible, 137 | E::Config: DeserializeOwned + Send + Sync + 'static, 138 | E::Opts: StructOpt + Send + Sync + 'static, 139 | { 140 | #[cfg(feature = "multithreaded")] 141 | { 142 | ext.with_singleton(Tokio::Default) 143 | } 144 | #[cfg(not(feature = "multithreaded"))] 145 | { 146 | ext.with_singleton(Tokio::SingleThreaded) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /spirit-tokio/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(test(attr(deny(warnings))))] 2 | // Our program-long snippets are more readable with main 3 | #![allow(clippy::needless_doctest_main)] 4 | #![forbid(unsafe_code)] 5 | #![warn(missing_docs)] 6 | #![cfg_attr(docsrs, feature(doc_cfg))] 7 | 8 | //! Support for tokio inside spirit 9 | //! 10 | //! This provides configuration of the tokio runtime and installer of futures. 11 | //! 12 | //! It also provides few configuration [`Fragment`]s for configuring network primitives. 13 | //! 14 | //! Note that this enables several features of [`tokio`]. 15 | //! 16 | //! # Features 17 | //! 18 | //! * `rt-from-cfg`: Allows creating runtime from configuration. Enables the [`Tokio::FromCfg`] 19 | //! variant and [`Config`][crate::runtime::Config]. Enabled by default. 20 | //! * `cfg-help`: Support for generating help for the configuration options. 21 | //! * `net`: Network primitive configuration [`Fragment`]s in the [`net`] module. 22 | //! * `stream`: Implementations of [`tokio_stream::Stream`] on several types. 23 | //! * `futures`: Support for converting between [`futures`'s][futures_util::future::Either] and our 24 | //! [`Either`][crate::either::Either]. 25 | //! * `either`: Support for converting between our [`Either`][crate::either::Either] and the one 26 | //! from the [`either`] crate. 27 | //! 28 | //! # Examples 29 | //! 30 | //! ```rust 31 | //! use std::future::Future; 32 | //! use std::pin::Pin; 33 | //! use std::time::Duration; 34 | //! 35 | //! use err_context::AnyError; 36 | //! use serde::{Deserialize, Serialize}; 37 | //! use spirit::{Empty, Pipeline, Spirit}; 38 | //! use spirit::prelude::*; 39 | //! use spirit::fragment::driver::CacheEq; 40 | //! use spirit_tokio::{FutureInstaller, Tokio}; 41 | //! use spirit_tokio::runtime::Config as TokioCfg; 42 | //! use structdoc::StructDoc; 43 | //! 44 | //! #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, StructDoc)] 45 | //! #[serde(default)] 46 | //! struct MsgCfg { 47 | //! /// A message to print now and then. 48 | //! msg: String, 49 | //! /// Time between printing the message. 50 | //! interval: Duration, 51 | //! } 52 | //! 53 | //! impl MsgCfg { 54 | //! async fn run(self) { 55 | //! loop { 56 | //! println!("{}", self.msg); 57 | //! tokio::time::sleep(self.interval).await; 58 | //! } 59 | //! } 60 | //! } 61 | //! 62 | //! impl Default for MsgCfg { 63 | //! fn default() -> Self { 64 | //! MsgCfg { 65 | //! msg: "Hello".to_owned(), 66 | //! interval: Duration::from_secs(1), 67 | //! } 68 | //! } 69 | //! } 70 | //! 71 | //! spirit::simple_fragment! { 72 | //! impl Fragment for MsgCfg { 73 | //! type Driver = CacheEq; 74 | //! type Resource = Pin + Send>>; 75 | //! type Installer = FutureInstaller; 76 | //! fn create(&self, _: &'static str) -> Result { 77 | //! let fut = self.clone().run(); 78 | //! Ok(Box::pin(fut)) 79 | //! } 80 | //! } 81 | //! } 82 | //! 83 | //! /// An application. 84 | //! #[derive(Default, Deserialize, Serialize, StructDoc)] 85 | //! struct AppConfig { 86 | //! #[serde(flatten)] 87 | //! msg: MsgCfg, 88 | //! 89 | //! /// Configuration of the asynchronous tokio runtime. 90 | //! #[serde(default)] 91 | //! threadpool: TokioCfg, 92 | //! } 93 | //! 94 | //! impl AppConfig { 95 | //! fn threadpool(&self) -> TokioCfg { 96 | //! self.threadpool.clone() 97 | //! } 98 | //! 99 | //! fn msg(&self) -> &MsgCfg { 100 | //! &self.msg 101 | //! } 102 | //! } 103 | //! 104 | //! fn main() { 105 | //! Spirit::::new() 106 | //! // Makes sure we have a runtime configured from the config. 107 | //! // If we don't do this, the pipeline below would insert a default Tokio runtime to make 108 | //! // it work. If you want to customize the runtime (like here), make sure to insert it 109 | //! // before any pipelines requiring it (otherwise you get the default one from them). 110 | //! .with_singleton(Tokio::from_cfg(AppConfig::threadpool)) 111 | //! // Will install and possibly cancel and replace the future if the config changes. 112 | //! .with(Pipeline::new("Msg").extract_cfg(AppConfig::msg)) 113 | //! // Just an empty body here. 114 | //! .run(|spirit| { 115 | //! // Usually, one would terminate by CTRL+C, but we terminate from here to make sure 116 | //! // the example finishes. 117 | //! spirit.terminate(); 118 | //! Ok(()) 119 | //! }) 120 | //! } 121 | //! ``` 122 | //! 123 | //! An alternative approach can be seen at [`handlers::ToFutureUnconfigured`]. 124 | //! 125 | //! [`Fragment`]: spirit::fragment::Fragment 126 | 127 | pub mod either; 128 | pub mod handlers; 129 | pub mod installer; 130 | #[cfg(feature = "net")] 131 | pub mod net; 132 | pub mod runtime; 133 | 134 | pub use crate::installer::FutureInstaller; 135 | #[cfg(feature = "net")] 136 | pub use crate::net::{TcpListen, TcpListenWithLimits, UdpListen}; 137 | pub use crate::runtime::Tokio; 138 | -------------------------------------------------------------------------------- /spirit-tokio/src/net/unix.rs: -------------------------------------------------------------------------------- 1 | //! Support for unix domain sockets. 2 | //! 3 | //! This is equivalent to the configuration fragments in the [`net`] module, but for unix domain 4 | //! sockets. This is available only on unix platforms. 5 | //! 6 | //! If you want to accept both normal (IP) sockets and unix domain sockets as part of the 7 | //! configuration, you can use the [`Either`] enum. 8 | //! 9 | //! [`net`]: crate::net 10 | //! [`Either`]: crate::either::Either 11 | 12 | use std::cmp; 13 | use std::ffi::OsStr; 14 | use std::fmt::Debug; 15 | use std::fs; 16 | use std::io::Error as IoError; 17 | use std::os::unix::ffi::OsStrExt; 18 | use std::os::unix::fs::FileTypeExt; 19 | use std::os::unix::net::{UnixDatagram as StdUnixDatagram, UnixListener as StdUnixListener}; 20 | use std::path::{Path, PathBuf}; 21 | use std::pin::Pin; 22 | use std::task::{Context, Poll}; 23 | 24 | use err_context::prelude::*; 25 | use err_context::AnyError; 26 | use log::{info, warn}; 27 | use serde::{Deserialize, Serialize}; 28 | use socket2::{Domain, SockAddr, Socket, Type as SocketType}; 29 | use spirit::fragment::driver::{CacheSimilar, Comparable, Comparison}; 30 | use spirit::fragment::{Fragment, Stackable}; 31 | use spirit::utils::is_default; 32 | use spirit::{log_error, Empty}; 33 | use tokio::net::{UnixDatagram, UnixListener, UnixStream}; 34 | 35 | use super::limits::WithLimits; 36 | use super::{Accept, ConfiguredListener}; 37 | 38 | /// When should an existing file/socket be removed before trying to bind to it. 39 | /// 40 | /// # Warning 41 | /// 42 | /// These settings are racy (they first check the condition and then act on that, but by that time 43 | /// something else might be in place already) and have the possibility to delete unrelated things. 44 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] 45 | #[non_exhaustive] 46 | #[serde(rename_all = "kebab-case")] 47 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 48 | pub enum UnlinkBefore { 49 | /// Try removing it always. 50 | /// 51 | /// This has the risk of removing unrelated files. 52 | Always, 53 | 54 | /// Remove it if it is a socket. 55 | IsSocket, 56 | 57 | /// Try connecting there first and if connecting fails, remove otherwise keep alive. 58 | TryConnect, 59 | } 60 | 61 | fn is_socket(p: &Path) -> Result { 62 | let meta = fs::metadata(p)?; 63 | Ok(meta.file_type().is_socket()) 64 | } 65 | 66 | fn try_connect(addr: &SockAddr, tp: SocketType) -> Result { 67 | let socket = Socket::new(Domain::UNIX, tp, None)?; 68 | Ok(socket.connect(addr).is_err()) 69 | } 70 | 71 | #[doc(hidden)] 72 | pub struct SocketWithPath { 73 | to_delete: Option, 74 | socket: S, 75 | } 76 | 77 | impl Drop for SocketWithPath { 78 | fn drop(&mut self) { 79 | if let Some(path) = &self.to_delete { 80 | if let Err(e) = fs::remove_file(path) { 81 | warn!("Failed to remove {} after use: {}", path.display(), e); 82 | } 83 | } 84 | } 85 | } 86 | 87 | /// Configuration of where to bind a unix domain socket. 88 | /// 89 | /// This is the lower-level configuration fragment that doesn't directly provide much 90 | /// functionality. But it is the basic building block of both [`UnixListen`] and [`UnixDatagram`]. 91 | /// 92 | /// Note that this does provide the [`Default`] trait, but the default value is mostly useless. 93 | /// 94 | /// # Configuration options 95 | /// 96 | /// * `path`: The filesystem path to bind the socket to. 97 | /// * `remove_before`: Under which conditions should the socket be removed before trying to bind 98 | /// it. Beware that this is racy (both because the check is separate from the removal and that 99 | /// something might have been created after we have removed it). No removal by default. 100 | /// * `remove_after`: It an attempt to remove it should be done after we are done using the socket. 101 | /// Default is `false`. 102 | /// * `abstract_path`: If set to `true`, interprets the path as abstract path. 103 | /// 104 | /// # TODO 105 | /// 106 | /// * Setting permissions on the newly bound socket. 107 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] 108 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 109 | #[serde(rename_all = "kebab-case")] 110 | #[non_exhaustive] 111 | pub struct Listen { 112 | /// The path on the FS where to create the unix domain socket. 113 | pub path: PathBuf, 114 | 115 | /// When should an existing file/socket be removed before trying to bind to it. 116 | /// 117 | /// # Warning 118 | /// 119 | /// These settings are racy (they first check the condition and then act on that, but by that time 120 | /// something else might be in place already) and have the possibility to delete unrelated things. 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub unlink_before: Option, 123 | 124 | /// Try removing the file after stopping using it. 125 | /// 126 | /// Removal might fail in case of unclean shutdown and be left behind. 127 | #[serde(default, skip_serializing_if = "is_default")] 128 | pub unlink_after: bool, 129 | 130 | /// Interpret as an abstract path instead of ordinary one. 131 | #[serde(default, rename = "abstract", skip_serializing_if = "is_default")] 132 | pub abstract_path: bool, 133 | 134 | /// The accepting backlog. 135 | /// 136 | /// Has no effect for DGRAM sockets. 137 | /// 138 | /// This specifies how many connections can wait in the kernel before being accepted. If more 139 | /// than this limit are queued, the kernel starts refusing them with connection reset packets. 140 | /// 141 | /// The default is 128. 142 | #[serde(default = "super::default_backlog")] 143 | pub backlog: u32, 144 | // TODO: Permissions 145 | } 146 | 147 | impl Listen { 148 | fn add_path(&self, socket: S) -> SocketWithPath { 149 | let to_delete = if self.unlink_after && !self.abstract_path { 150 | Some(self.path.clone()) 151 | } else { 152 | None 153 | }; 154 | SocketWithPath { to_delete, socket } 155 | } 156 | fn unlink_before(&self, addr: &SockAddr, tp: SocketType) { 157 | use UnlinkBefore::*; 158 | 159 | let unlink = match (self.abstract_path, self.unlink_before) { 160 | (true, _) | (false, None) => false, 161 | (false, Some(Always)) => true, 162 | (false, Some(IsSocket)) => is_socket(&self.path).unwrap_or(false), 163 | (false, Some(TryConnect)) => { 164 | self.path.exists() && try_connect(addr, tp).unwrap_or(false) 165 | } 166 | }; 167 | 168 | if unlink { 169 | if let Err(e) = fs::remove_file(&self.path) { 170 | log_error!( 171 | Warn, 172 | e.context(format!( 173 | "Failed to remove previous socket at {}", 174 | self.path.display() 175 | )) 176 | .into() 177 | ); 178 | } else { 179 | info!("Removed previous socket {}", self.path.display()); 180 | } 181 | } 182 | } 183 | 184 | fn create_any(&self, tp: SocketType) -> Result { 185 | let mut buf = Vec::new(); 186 | let addr: &OsStr = if self.abstract_path { 187 | buf.push(0); 188 | buf.extend(self.path.as_os_str().as_bytes()); 189 | OsStr::from_bytes(&buf) 190 | } else { 191 | self.path.as_ref() 192 | }; 193 | let addr = SockAddr::unix(addr).context("Create sockaddr")?; 194 | let sock = Socket::new(Domain::UNIX, tp, None).context("Create socket")?; 195 | self.unlink_before(&addr, tp); 196 | sock.bind(&addr) 197 | .with_context(|_| format!("Binding socket to {}", self.path.display()))?; 198 | 199 | Ok(sock) 200 | } 201 | 202 | /// Creates a unix listener. 203 | /// 204 | /// This is a low-level function, returning the *blocking* (std) listener. 205 | pub fn create_listener(&self) -> Result { 206 | let sock = self.create_any(SocketType::STREAM)?; 207 | sock.listen(cmp::min(self.backlog, i32::max_value() as u32) as i32) 208 | .context("Listening to Stream socket")?; 209 | Ok(sock.into()) 210 | } 211 | 212 | /// Creates a unix datagram socket. 213 | /// 214 | /// This is a low-level function, returning the *blocking* (std) socket. 215 | pub fn create_datagram(&self) -> Result { 216 | let sock = self.create_any(SocketType::DGRAM)?; 217 | Ok(sock.into()) 218 | } 219 | } 220 | 221 | /// Additional configuration for unix domain stream sockets. 222 | /// 223 | /// *Currently* this is an alias to `Empty`, because there haven't been yet any idea what further 224 | /// to configure on them. However, this can turn into its own type in some future time and it won't 225 | /// be considered semver-incompatible change. 226 | /// 227 | /// If you want to always have no additional configuration, use [`Empty`] explicitly. 228 | pub type UnixConfig = Empty; 229 | 230 | impl Accept for UnixListener { 231 | type Connection = UnixStream; 232 | fn poll_accept( 233 | self: Pin<&mut Self>, 234 | ctx: &mut Context, 235 | ) -> Poll> { 236 | mod inner { 237 | use super::{Context, IoError, Poll, UnixListener, UnixStream}; 238 | // Hide the Accept trait from scope so it doesn't interfere 239 | pub(super) fn poll_accept( 240 | l: &UnixListener, 241 | ctx: &mut Context, 242 | ) -> Poll> { 243 | l.poll_accept(ctx).map_ok(|(s, _)| s) 244 | } 245 | } 246 | inner::poll_accept(&self, ctx) 247 | } 248 | } 249 | 250 | /// A listener for unix domain stream sockets. 251 | /// 252 | /// This is the unix-domain equivalent of [`TcpListen`]. All notes about it apply here with the 253 | /// sole difference that the fields added by it are from [`unix::Listen`] instead of 254 | /// [`net::Listen`]. 255 | /// 256 | /// [`TcpListen`]: crate::net::TcpListen 257 | /// [`unix::Listen`]: Listen 258 | /// [`net::Listen`]: crate::net::Listen 259 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] 260 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 261 | pub struct UnixListen { 262 | #[serde(flatten)] 263 | listen: Listen, 264 | #[serde(flatten)] 265 | unix_config: UnixStreamConfig, 266 | 267 | /// Arbitrary additional application-specific configuration that doesn't influence the socket. 268 | /// 269 | /// But it can be looked into by the [`handlers`][crate::handlers]. 270 | #[serde(flatten)] 271 | pub extra_cfg: ExtraCfg, 272 | } 273 | 274 | impl Stackable for UnixListen {} 275 | 276 | impl Comparable for UnixListen 277 | where 278 | ExtraCfg: PartialEq, 279 | UnixStreamConfig: PartialEq, 280 | { 281 | fn compare(&self, other: &Self) -> Comparison { 282 | if self.listen != other.listen { 283 | Comparison::Dissimilar 284 | } else if self != other { 285 | Comparison::Similar 286 | } else { 287 | Comparison::Same 288 | } 289 | } 290 | } 291 | 292 | impl Fragment for UnixListen 293 | where 294 | ExtraCfg: Clone + Debug + PartialEq, 295 | UnixStreamConfig: Clone + Debug + PartialEq, 296 | { 297 | type Driver = CacheSimilar; 298 | type Installer = (); 299 | type Seed = SocketWithPath; 300 | type Resource = ConfiguredListener; 301 | fn make_seed(&self, name: &str) -> Result { 302 | self.listen 303 | .create_listener() 304 | .with_context(|_| format!("Failed to create a unix stream socket {name}/{self:?}")) 305 | .map_err(AnyError::from) 306 | .map(|s| self.listen.add_path(s)) 307 | } 308 | fn make_resource(&self, seed: &mut Self::Seed, name: &str) -> Result { 309 | let config = self.unix_config.clone(); 310 | seed.socket 311 | .try_clone() // Another copy of the listener 312 | // std → tokio socket conversion 313 | .and_then(|sock| -> Result { 314 | sock.set_nonblocking(true)?; 315 | UnixListener::from_std(sock) 316 | }) 317 | .with_context(|_| { 318 | format!("Failed to make unix streamsocket {name}/{self:?} asynchronous") 319 | }) 320 | .map_err(AnyError::from) 321 | .map(|listener| ConfiguredListener::new(listener, config)) 322 | } 323 | } 324 | 325 | /// Type alias for [`UnixListen`] without any unnecessary configuration options. 326 | pub type MinimalUnixListen = UnixListen; 327 | 328 | /// Wrapped [`UnixListen`] that limits the number of concurrent connections and handles some 329 | /// recoverable errors. 330 | pub type UnixListenWithLimits = 331 | WithLimits>; 332 | 333 | /// A [`Fragment`] for unix datagram sockets 334 | /// 335 | /// This is an unix domain equivalent to the [`UdpListen`] configuration fragment. All its notes 336 | /// apply here except that the base configuration fields are taken from [`unix::Listen`] instead of 337 | /// [`net::Listen`]. 338 | /// 339 | /// [`UdpListen`]: crate::net::UdpListen 340 | /// [`unix::Listen`]: Listen 341 | /// [`net::Listen`]: crate::net::Listen 342 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] 343 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 344 | #[non_exhaustive] 345 | pub struct DatagramListen { 346 | /// The listening address. 347 | #[serde(flatten)] 348 | pub listen: Listen, 349 | 350 | /// Arbitrary application-specific configuration that doesn't influence the socket itself. 351 | /// 352 | /// But it can be examined from within the [`handlers`][crate::handlers]. 353 | #[serde(flatten)] 354 | pub extra_cfg: ExtraCfg, 355 | } 356 | 357 | impl Stackable for DatagramListen {} 358 | 359 | impl Comparable for DatagramListen { 360 | fn compare(&self, other: &Self) -> Comparison { 361 | if self.listen != other.listen { 362 | Comparison::Dissimilar 363 | } else if self != other { 364 | Comparison::Similar 365 | } else { 366 | Comparison::Same 367 | } 368 | } 369 | } 370 | 371 | impl Fragment for DatagramListen 372 | where 373 | ExtraCfg: Clone + Debug + PartialEq, 374 | { 375 | type Driver = CacheSimilar; 376 | type Installer = (); 377 | type Seed = SocketWithPath; 378 | type Resource = UnixDatagram; 379 | fn make_seed(&self, name: &str) -> Result { 380 | self.listen 381 | .create_datagram() 382 | .with_context(|_| format!("Failed to create unix datagram socket {name}/{self:?}")) 383 | .map_err(AnyError::from) 384 | .map(|s| self.listen.add_path(s)) 385 | } 386 | fn make_resource(&self, seed: &mut Self::Seed, name: &str) -> Result { 387 | seed.socket 388 | .try_clone() // Another copy of the socket 389 | // std → tokio socket conversion 390 | .and_then(|sock| -> Result { 391 | sock.set_nonblocking(true)?; 392 | UnixDatagram::from_std(sock) 393 | }) 394 | .with_context(|_| { 395 | format!("Failed to make unix datagram socket {name}/{self:?} asynchronous") 396 | }) 397 | .map_err(AnyError::from) 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /spirit-tokio/src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! An extension to start the tokio runtime at the appropriate time. 2 | 3 | use std::cell::Cell; 4 | use std::sync::{Arc, Mutex, PoisonError}; 5 | 6 | use err_context::prelude::*; 7 | use err_context::AnyError; 8 | use log::{debug, trace, warn}; 9 | use serde::de::DeserializeOwned; 10 | #[cfg(feature = "rt-from-cfg")] 11 | use serde::{Deserialize, Serialize}; 12 | use spirit::extension::{Extensible, Extension}; 13 | use spirit::validation::Action; 14 | #[cfg(all(feature = "cfg-help", feature = "rt-from-cfg"))] 15 | use structdoc::StructDoc; 16 | use structopt::StructOpt; 17 | use tokio::runtime::{Builder, Runtime}; 18 | use tokio::sync::oneshot::{self, Sender}; 19 | 20 | /// A guard preventing the tokio runtime from shutting down just yet. 21 | /// 22 | /// One can keep the runtime alive by holding onto an instance of this. 23 | /// 24 | /// Note that a forced shutdown is still possible. In case the `spirit`s `run` returns an error, 25 | /// the runtime is terminated right away. 26 | #[derive(Clone, Debug)] 27 | pub struct ShutGuard(Arc>); 28 | 29 | thread_local! { 30 | static SHUT_GUARD: Cell> = Cell::new(None); 31 | } 32 | 33 | fn with_shut_guard R>(g: Option, f: F) -> R { 34 | SHUT_GUARD.with(|c| { 35 | // Install the ShutGuard to thread local storage so others can have a look at it. 36 | assert!(c.replace(g).is_none()); 37 | struct Guard<'a>(&'a Cell>); 38 | impl Drop for Guard<'_> { 39 | fn drop(&mut self) { 40 | self.0.take(); 41 | } 42 | } 43 | // Make sure it is removed after we are done no matter what. 44 | let _g = Guard(c); 45 | 46 | f() 47 | }) 48 | } 49 | 50 | /// Provides a shut guard. 51 | /// 52 | /// Note that the guard is available only from within the hooks and main thread of run of `spirit` 53 | /// and only after a [`Tokio`] has been plugged into it and before termination. 54 | /// 55 | /// It is generally possible to pair a created resource (eg. a future) with an instance of this to 56 | /// allow it to shut down gracefully. 57 | /// 58 | /// Futures installed by [`FutureInstaller`][crate::FutureInstaller] already have this protection. 59 | pub fn shut_guard() -> Option { 60 | SHUT_GUARD.with(|c| { 61 | let g = c.take(); 62 | c.set(g.clone()); 63 | g 64 | }) 65 | } 66 | 67 | // From tokio documentation 68 | #[cfg(feature = "rt-from-cfg")] 69 | const DEFAULT_MAX_THREADS: usize = 512; 70 | #[cfg(feature = "rt-from-cfg")] 71 | const THREAD_NAME: &str = "tokio-runtime-worker"; 72 | 73 | /// Configuration for building a threaded runtime. 74 | #[cfg(feature = "rt-from-cfg")] 75 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash, Serialize)] 76 | #[serde(rename_all = "kebab-case", default)] 77 | #[cfg_attr(feature = "cfg-help", derive(StructDoc))] 78 | #[non_exhaustive] 79 | pub struct Config { 80 | /// Number of threads used for asynchronous processing. 81 | /// 82 | /// Defaults to number of available CPUs in the system if left unconfigured. 83 | #[serde(skip_serializing_if = "Option::is_none")] 84 | pub core_threads: Option, 85 | 86 | /// Total maximum number of threads, including ones for blocking (non-async) operations. 87 | /// 88 | /// Must be at least `core-threads` 89 | #[serde(skip_serializing_if = "Option::is_none")] 90 | pub max_threads: Option, 91 | 92 | /// The thread name to use for the worker threads. 93 | pub thread_name: String, 94 | } 95 | 96 | #[cfg(feature = "rt-from-cfg")] 97 | impl Default for Config { 98 | fn default() -> Self { 99 | Config { 100 | core_threads: None, 101 | max_threads: None, 102 | thread_name: THREAD_NAME.to_owned(), 103 | } 104 | } 105 | } 106 | 107 | #[cfg(feature = "rt-from-cfg")] 108 | impl From for Builder { 109 | fn from(cfg: Config) -> Builder { 110 | let mut builder = Builder::new_multi_thread(); 111 | let threads = cfg.core_threads.unwrap_or_else(num_cpus::get); 112 | builder.worker_threads(threads); 113 | let max = match cfg.max_threads { 114 | None if threads >= DEFAULT_MAX_THREADS => { 115 | warn!( 116 | "Increasing max threads from implicit {} to {} to match core threads", 117 | DEFAULT_MAX_THREADS, threads 118 | ); 119 | threads 120 | } 121 | None => DEFAULT_MAX_THREADS, 122 | Some(max_threads) if max_threads <= threads => { 123 | warn!( 124 | "Incrementing max threads from configured {} to {} to match core threads", 125 | max_threads, 126 | threads + 1 127 | ); 128 | threads + 1 129 | } 130 | Some(max) => max, 131 | }; 132 | builder.max_blocking_threads(max); 133 | builder.thread_name(cfg.thread_name); 134 | builder 135 | } 136 | } 137 | 138 | /// A [`spirit`] [`Extension`] to inject a [`tokio`] runtime. 139 | /// 140 | /// This, when inserted into spirit [`Builder`][spirit::Builder] with the 141 | /// [`with_singleton`][Extensible::with_singleton] will provide the application with a tokio 142 | /// runtime. 143 | /// 144 | /// This will: 145 | /// 146 | /// * Run the application body inside the runtime (to allow spawning tasks and creating tokio 147 | /// resources). 148 | /// * Run the configuration/termination/signal hooks inside the async context of the runtime (for 149 | /// similar reasons). 150 | /// * Keep the runtime running until [`terminate`][spirit::Spirit::terminate] is invoked (either 151 | /// explicitly or by CTRL+C or similar). 152 | /// 153 | /// A default instance ([`Tokio::Default`]) is inserted by pipelines containing the 154 | /// [`FutureInstaller`][crate::FutureInstaller]. 155 | #[non_exhaustive] 156 | #[allow(clippy::type_complexity)] // While complex, the types are probably more readable inline 157 | #[derive(Default)] 158 | pub enum Tokio { 159 | /// Provides the equivalent of [`Runtime::new`]. 160 | #[cfg(feature = "multithreaded")] 161 | #[default] 162 | Default, 163 | 164 | /// A singlethreaded runtime. 165 | SingleThreaded, 166 | 167 | /// Allows the caller to provide arbitrary constructor for the [`Runtime`]. 168 | /// 169 | /// This variant also allows creating the basic (non-threaded) scheduler if needed. Note that 170 | /// some operations with such runtime are prone to cause deadlocks. 171 | Custom(Box) -> Result + Send>), 172 | 173 | /// Uses configuration for constructing the [`Runtime`]. 174 | /// 175 | /// This'll use the extractor (the first closure) to get a [`Config`], create a [`Builder`] 176 | /// based on that. It'll explicitly enable all the drivers and enable threaded runtime. Then it 177 | /// calls the postprocessor (the second closure) to turn it into the [`Runtime`]. 178 | /// 179 | /// This is the more general form. If you're fine with just basing it on the configuration 180 | /// without much tweaking, you can use [`Tokio::from_cfg`] (which will create this variant with 181 | /// reasonable value of preprocessor). 182 | /// 183 | /// This is available only with the [`rt-from-cfg`] feature enabled. 184 | #[cfg(feature = "rt-from-cfg")] 185 | FromCfg( 186 | Box Config + Send>, 187 | Box Result + Send>, 188 | ), 189 | } 190 | 191 | impl Tokio { 192 | /// Simplified construction from configuration. 193 | /// 194 | /// This is similar to [`Tokio::FromCfg`]. However, the extractor takes only the configuration 195 | /// structure, not the command line options. Furthermore, post-processing is simply calling 196 | /// [`Builder::build`], without a chance to tweak. 197 | #[cfg(feature = "rt-from-cfg")] 198 | pub fn from_cfg(mut extractor: E) -> Self 199 | where 200 | E: FnMut(&C) -> Config + Send + 'static, 201 | { 202 | let extractor = move |_opts: &O, cfg: &C| extractor(cfg); 203 | let finish = |mut builder: Builder| -> Result { Ok(builder.build()?) }; 204 | Tokio::FromCfg(Box::new(extractor), Box::new(finish)) 205 | } 206 | 207 | /// Method to create the runtime. 208 | /// 209 | /// This can be used when not taking advantage of the spirit auto-management features. 210 | pub fn create(&mut self, opts: &O, cfg: &Arc) -> Result { 211 | match self { 212 | #[cfg(feature = "multithreaded")] 213 | Tokio::Default => Runtime::new().map_err(AnyError::from), 214 | Tokio::SingleThreaded => Builder::new_current_thread() 215 | .enable_all() 216 | .build() 217 | .map_err(AnyError::from), 218 | Tokio::Custom(ctor) => ctor(opts, cfg), 219 | #[cfg(feature = "rt-from-cfg")] 220 | Tokio::FromCfg(extractor, postprocess) => { 221 | let cfg = extractor(opts, cfg); 222 | let mut builder: Builder = cfg.into(); 223 | builder.enable_all(); 224 | postprocess(builder) 225 | } 226 | } 227 | } 228 | } 229 | 230 | #[cfg(feature = "multithreaded")] 231 | 232 | impl Extension for Tokio 233 | where 234 | E: Extensible, 235 | E::Config: DeserializeOwned + Send + Sync + 'static, 236 | E::Opts: StructOpt + Send + Sync + 'static, 237 | { 238 | fn apply(mut self, ext: E) -> Result { 239 | trace!("Wrapping in tokio runtime"); 240 | 241 | // The local hackonomicon: 242 | // 243 | // * We need to create a runtime and wrap both the application body in it and the 244 | // callbacks/hooks of spirit, as they might want to create some futures/resources that 245 | // need access to tokio. 246 | // * But that will be ready only after we did the configuration. By that time we no longer 247 | // have access to anything that would allow us this kind of injection. 248 | // * Furthermore, we need even the following on_config callbacks/whatever else to be 249 | // wrapped in the handle. 250 | // 251 | // Therefore we install the wrappers in right away and provide them with the data only once 252 | // it is ready. The mutexes will always be unlocked, all these things will in fact be 253 | // called from the same thread, it's just not possible to explain to Rust right now. 254 | 255 | let runtime = Arc::new(Mutex::new(None)); 256 | let handle = Arc::new(Mutex::new(None)); 257 | 258 | let init = { 259 | let mut initialized = false; 260 | let runtime = Arc::clone(&runtime); 261 | let handle = Arc::clone(&handle); 262 | #[cfg(feature = "rt-from-cfg")] 263 | let mut prev_cfg = None; 264 | 265 | // This runs as config validator to be sooner in the chain. 266 | move |_: &_, cfg: &Arc<_>, opts: &_| -> Result { 267 | if initialized { 268 | #[cfg(feature = "rt-from-cfg")] 269 | if let Tokio::FromCfg(extract, _) = &mut self { 270 | let prev = prev_cfg 271 | .as_ref() 272 | .expect("Should have stored config on init"); 273 | let new = extract(opts, cfg); 274 | if prev != &new { 275 | warn!("Tokio configuration differs, but can't be reloaded at run time"); 276 | } 277 | } 278 | } else { 279 | debug!("Creating the tokio runtime"); 280 | let new_runtime = self.create(opts, cfg).context("Tokio runtime creation")?; 281 | let new_handle = new_runtime.handle().clone(); 282 | // We do so *right now* so the following config validators have some chance of 283 | // having the runtime. It's one-shot anyway, so we are not *replacing* anything 284 | // previous. 285 | *runtime.lock().unwrap() = Some(new_runtime); 286 | *handle.lock().unwrap() = Some(new_handle); 287 | initialized = true; 288 | #[cfg(feature = "rt-from-cfg")] 289 | if let Tokio::FromCfg(extract, _) = &mut self { 290 | prev_cfg = Some(extract(opts, cfg)); 291 | } 292 | } 293 | Ok(Action::new()) 294 | } 295 | }; 296 | 297 | let (terminate_send, terminate_recv) = oneshot::channel::<()>(); 298 | let shutdown_guard = Arc::new(Mutex::new(Some(ShutGuard(Arc::new(terminate_send))))); 299 | let shutdown_guard_main = Arc::clone(&shutdown_guard); 300 | let shutdown_guard_drain = Arc::clone(&shutdown_guard); 301 | 302 | // Ugly hack: seems like Rust is not exactly ready to deal with our crazy type in here and 303 | // deduce it in just the function call :-(. Force it by an explicit type. 304 | #[allow(clippy::type_complexity)] // We are glad we managed to make it compile at all 305 | let around_hooks: Box FnMut(Box) + Send> = 306 | Box::new(move |inner| { 307 | let locked = handle 308 | .lock() 309 | // The inner may panic and we are OK with that, we just want to be able to run 310 | // next time again. 311 | .unwrap_or_else(PoisonError::into_inner); 312 | 313 | let shut_guard = shutdown_guard.lock().unwrap().clone(); 314 | with_shut_guard(shut_guard, || { 315 | if let Some(handle) = locked.as_ref() { 316 | trace!("Wrapping hooks into tokio handle"); 317 | let _guard = handle.enter(); 318 | inner(); 319 | trace!("Leaving tokio handle"); 320 | } else { 321 | // During startup, the handle/runtime is not *yet* ready. This is OK and 322 | // expected, but we must run without it. And we also need to unlock that 323 | // mutex, because the inner might actually contain the above init and want 324 | // to provide the handle, so we don't want a deadlock. 325 | drop(locked); 326 | trace!("Running hooks without tokio handle, not available yet"); 327 | inner(); 328 | } 329 | }) 330 | }); 331 | 332 | ext 333 | .config_validator(init) 334 | .around_hooks(around_hooks) 335 | .on_terminate(move || { 336 | trace!("Removing global shutdown guard (others might be present in resources)"); 337 | shutdown_guard_drain.lock().unwrap().take(); 338 | }) 339 | .run_around(move |spirit, inner| -> Result<(), AnyError> { 340 | // No need to worry about poisons here, we are going to be called just once 341 | let runtime = runtime.lock().unwrap().take().expect("Run even before config"); 342 | debug!("Running with tokio runtime"); 343 | let _guard = runtime.enter(); 344 | let shut_guard = shutdown_guard_main.lock().unwrap().clone(); 345 | let result = with_shut_guard(shut_guard, inner); 346 | debug!("Inner bodies ended"); 347 | if result.is_ok() { 348 | debug!("Waiting for spirit to terminate"); 349 | // It's OK to terminate with error here. We actually release it by dropping the 350 | // last Arc to it. 351 | let _ = runtime.block_on(terminate_recv); 352 | debug!("Spirit signalled termination to runtime"); 353 | drop(runtime); 354 | } else { 355 | warn!("Tokio runtime initialization body returned an error, trying to shut everything down"); 356 | runtime.shutdown_background(); 357 | spirit.terminate(); 358 | } 359 | result 360 | }) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | //! The running application part. 2 | //! 3 | //! The convenient way to manage the application runtime is through 4 | //! [`Builder::run`][crate::SpiritBuilder::run]. If more flexibility is needed, the 5 | //! [`Builder::build`][crate::SpiritBuilder::build] can be used instead. That method returns the 6 | //! [`App`][crate::app::App] object, representing the application runner. The application can then 7 | //! be run at any later time, as convenient. 8 | 9 | use std::process; 10 | use std::sync::Arc; 11 | use std::thread; 12 | 13 | use log::debug; 14 | use serde::de::DeserializeOwned; 15 | use structopt::StructOpt; 16 | 17 | use crate::bodies::{InnerBody, WrapBody}; 18 | use crate::error; 19 | use crate::spirit::Spirit; 20 | use crate::terminate_guard::TerminateGuard; 21 | use crate::utils::FlushGuard; 22 | use crate::AnyError; 23 | 24 | /// The running application part. 25 | /// 26 | /// This is returned by [`Builder::build`][crate::SpiritBuilder::build] and represents the rest of 27 | /// the application runtime except the actual application body. It can be used to run at any later 28 | /// time, after the spirit has been created. 29 | /// 30 | /// This carries all the [around-bodies][crate::Extensible::run_around] and 31 | /// [before-bodies][crate::Extensible::run_before]. If you run the application body directly, not 32 | /// through this, some of the pipelines or extensions might not work as expected. 33 | /// 34 | /// The [`Builder::run`][crate::SpiritBuilder::run] is just a convenient wrapper around this. Note 35 | /// that that one handles and logs errors from the application startup as well as from its runtime. 36 | /// Here it is up to the caller to handle the startup errors. 37 | /// 38 | /// # Examples 39 | /// 40 | /// ```rust 41 | /// use spirit::{AnyError, Empty, Spirit}; 42 | /// use spirit::prelude::*; 43 | /// 44 | /// # fn main() -> Result<(), AnyError> { 45 | /// Spirit::::new() 46 | /// .build(true)? 47 | /// .run_term(|| { 48 | /// println!("Hello world"); 49 | /// Ok(()) 50 | /// }); 51 | /// # Ok(()) 52 | /// # } 53 | /// ``` 54 | pub struct App { 55 | spirit: Arc>, 56 | inner: InnerBody, 57 | wrapper: WrapBody, 58 | } 59 | 60 | impl App 61 | where 62 | O: StructOpt + Send + Sync + 'static, 63 | C: DeserializeOwned + Send + Sync + 'static, 64 | { 65 | pub(crate) fn new(spirit: Arc>, inner: InnerBody, wrapper: WrapBody) -> Self { 66 | Self { 67 | spirit, 68 | inner, 69 | wrapper, 70 | } 71 | } 72 | 73 | /// Access to the built spirit object. 74 | /// 75 | /// The object can be used to manipulate the runtime of the application, access the current 76 | /// configuration and register further callbacks (and extensions and pipelines). 77 | /// 78 | /// Depending on your needs, you may pass it to the closure started with [`run`][App::run] or 79 | /// even placed into some kind of global storage. 80 | pub fn spirit(&self) -> &Arc> { 81 | &self.spirit 82 | } 83 | 84 | /// Run the application with provided body. 85 | /// 86 | /// This will run the provided body. However, it'll wrap it in all the 87 | /// [around-bodies][crate::Extensible::run_around] and precede it with all the 88 | /// [before-bodies][crate::Extensible::run_before]. If any of these fail, or if the `body` 89 | /// fails, the error is propagated (and further bodies are not started). 90 | /// 91 | /// Furthermore, depending on the [`autojoin_bg_thread`][crate::Extensible::autojoin_bg_thread] 92 | /// configuration, termination and joining of the background thread may be performed. If the 93 | /// body errors, termination is done unconditionally (which may be needed in some corner cases 94 | /// to not deadlock on error). 95 | /// 96 | /// In other words, unless you have very special needs, this is how you actually invoke the 97 | /// application itself. 98 | /// 99 | /// Any errors are simply returned and it is up to the caller to handle them somehow. 100 | pub fn run(self, body: B) -> Result<(), AnyError> 101 | where 102 | B: FnOnce() -> Result<(), AnyError> + Send + 'static, 103 | { 104 | debug!("Running bodies"); 105 | let _flush = FlushGuard; 106 | struct ScopeGuard(Option); 107 | impl Drop for ScopeGuard { 108 | fn drop(&mut self) { 109 | self.0.take().expect("Drop called twice")(); 110 | } 111 | } 112 | let spirit = &self.spirit; 113 | let _thread = ScopeGuard(Some(|| { 114 | if thread::panicking() { 115 | spirit.terminate(); 116 | } 117 | spirit.maybe_autojoin_bg_thread(); 118 | })); 119 | let inner = self.inner; 120 | let inner = move || inner().and_then(|()| body()); 121 | let result = (self.wrapper)(Box::new(inner)); 122 | if result.is_err() { 123 | self.spirit.terminate(); 124 | } 125 | result 126 | } 127 | 128 | /// Similar to [`run`][App::run], but with error handling. 129 | /// 130 | /// This calls the [`run`][App::run]. However, if there are any errors, they are logged and the 131 | /// application terminates with non-zero exit code. 132 | pub fn run_term(self, body: B) 133 | where 134 | B: FnOnce() -> Result<(), AnyError> + Send + 'static, 135 | { 136 | let flush = FlushGuard; 137 | if error::log_errors("top-level", || self.run(body)).is_err() { 138 | drop(flush); 139 | process::exit(1); 140 | } 141 | } 142 | 143 | /// Run the application in a background thread for testing purposes. 144 | /// 145 | /// This'll run the application and return an RAII guard. That guard can be used to access the 146 | /// [Spirit] and manipulate it. It also terminates the application and background thread when 147 | /// dropped. 148 | /// 149 | /// This is for testing purposes (it panics if there are errors). See the [testing guide]. 150 | /// 151 | /// testing guide: crate::guide::testing 152 | pub fn run_test(self, body: B) -> TerminateGuard 153 | where 154 | B: FnOnce() -> Result<(), AnyError> + Send + 'static, 155 | { 156 | let spirit = Arc::clone(self.spirit()); 157 | let bg_thread = thread::spawn(move || self.run(body)); 158 | TerminateGuard::new(spirit, bg_thread) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/bodies.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{AnyError, Spirit}; 4 | 5 | pub(crate) type InnerBody = Box Result<(), AnyError> + Send>; 6 | pub(crate) type WrapBody = Box Result<(), AnyError> + Send>; 7 | pub(crate) type Wrapper = 8 | Box>, InnerBody) -> Result<(), AnyError> + Send>; 9 | pub(crate) type SpiritBody = 10 | Box>) -> Result<(), AnyError> + Send>; 11 | pub(crate) type HookBody<'a> = Box; 12 | pub trait HookWrapper: for<'a> FnMut(HookBody<'a>) + Send + 'static {} 13 | 14 | impl HookWrapper for F where F: for<'a> FnMut(HookBody<'a>) + Send + 'static {} 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use super::*; 19 | 20 | fn _accept_hook_wrapper(_: Box) {} 21 | 22 | // Not really run, we check this compiles 23 | fn _hook_wrapper_works() { 24 | _accept_hook_wrapper(Box::new(|inner| inner())); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/empty.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use structopt::StructOpt; 3 | 4 | /// A struct that may be used when either configuration or command line options are not needed. 5 | /// 6 | /// When the application doesn't need the configuration (in excess of the automatic part provided 7 | /// by this library) or it doesn't need any command line options of its own, this struct can be 8 | /// used to plug the type parameter. 9 | /// 10 | /// Other places (eg. around extensions) may use this to plug a type parameter that isn't needed, 11 | /// do nothing or something like that. 12 | #[derive( 13 | Copy, 14 | Clone, 15 | Debug, 16 | Default, 17 | Deserialize, 18 | Eq, 19 | PartialEq, 20 | Hash, 21 | Ord, 22 | PartialOrd, 23 | StructOpt, 24 | Serialize, 25 | )] 26 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 27 | pub struct Empty {} 28 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error handling utilities. 2 | 3 | use std::error::Error; 4 | 5 | use err_context::prelude::*; 6 | use log::{log, Level}; 7 | 8 | /// A wrapper type for any error. 9 | /// 10 | /// This is just a type alias for boxed standard error. Any errors go and this is guaranteed to be 11 | /// fully compatible. 12 | pub type AnyError = Box; 13 | 14 | /// How to format errors in logs. 15 | /// 16 | /// The enum is non-exhaustive ‒ more variants may be added in the future and it won't be 17 | /// considered an API breaking change. 18 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 19 | #[non_exhaustive] 20 | pub enum ErrorLogFormat { 21 | /// Multi-cause error will span multiple log messages. 22 | MultiLine, 23 | 24 | /// The error is formatted on a single line. 25 | /// 26 | /// The causes are separated by semicolons. 27 | SingleLine, 28 | } 29 | 30 | /// Log one error on given log level. 31 | /// 32 | /// It is printed to the log with all the causes and optionally a backtrace (if it is available and 33 | /// debug logging is enabled). 34 | /// 35 | /// This is the low-level version with full customization. You might also be interested in 36 | /// [`log_errors`][crate::error::log_error] or one of the convenience macro ([`log_error`][macro@log_error]). 37 | pub fn log_error(level: Level, target: &str, e: &AnyError, format: ErrorLogFormat) { 38 | match format { 39 | ErrorLogFormat::MultiLine => { 40 | for cause in e.chain() { 41 | log!(target: target, level, "{}", cause); 42 | } 43 | } 44 | ErrorLogFormat::SingleLine => { 45 | log!(target: target, level, "{}", e.display("; ")); 46 | } 47 | } 48 | } 49 | 50 | /// A convenience macro to log an [`AnyError`]. 51 | /// 52 | /// This logs an [`AnyError`] on given log level as a single line without backtrace. Removes some 53 | /// boilerplate from the [`log_error`] function. 54 | /// 55 | /// # Examples 56 | /// 57 | /// ```rust 58 | /// use std::error::Error; 59 | /// use std::fmt::{Display, Formatter, Result as FmtResult}; 60 | /// use spirit::log_error; 61 | /// 62 | /// #[derive(Debug)] 63 | /// struct Broken; 64 | /// 65 | /// impl Display for Broken { 66 | /// fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 67 | /// write!(fmt, "Something is broken") 68 | /// } 69 | /// } 70 | /// 71 | /// impl Error for Broken {} 72 | /// 73 | /// log_error!(Warn, Broken.into()); 74 | /// ``` 75 | /// 76 | /// [`log_error`]: fn@crate::error::log_error 77 | #[macro_export] 78 | macro_rules! log_error { 79 | ($level: ident, $descr: expr => $err: expr) => { 80 | $crate::log_error!(@SingleLine, $level, $err.context($descr).into()); 81 | }; 82 | ($level: ident, $err: expr) => { 83 | $crate::log_error!(@SingleLine, $level, $err); 84 | }; 85 | (multi $level: ident, $descr: expr => $err: expr) => { 86 | $crate::log_error!(@MultiLine, $level, $err.context($descr).into()); 87 | }; 88 | (multi $level: ident, $err: expr) => { 89 | $crate::log_error!(@MultiLine, $level, $err); 90 | }; 91 | (@$format: ident, $level: ident, $err: expr) => { 92 | $crate::error::log_error( 93 | $crate::macro_support::Level::$level, 94 | module_path!(), 95 | &$err, 96 | $crate::error::ErrorLogFormat::$format, 97 | ); 98 | }; 99 | } 100 | 101 | /// A wrapper around a fallible function, logging any returned errors. 102 | /// 103 | /// The errors will be logged in the provided target. You may want to provide `module_path!` as the 104 | /// target. 105 | /// 106 | /// If the error has multiple levels (causes), they are printed in multi-line fashion, as multiple 107 | /// separate log messages. 108 | /// 109 | /// # Examples 110 | /// 111 | /// ```rust 112 | /// use err_context::prelude::*; 113 | /// use spirit::AnyError; 114 | /// use spirit::error; 115 | /// # fn try_to_do_stuff() -> Result<(), AnyError> { Ok(()) } 116 | /// 117 | /// let result = error::log_errors(module_path!(), || { 118 | /// try_to_do_stuff().context("Didn't manage to do stuff")?; 119 | /// Ok(()) 120 | /// }); 121 | /// # let _result = result; 122 | /// ``` 123 | pub fn log_errors(target: &str, f: F) -> Result 124 | where 125 | F: FnOnce() -> Result, 126 | { 127 | let result = f(); 128 | if let Err(ref e) = result { 129 | log_error(Level::Error, target, e, ErrorLogFormat::MultiLine); 130 | } 131 | result 132 | } 133 | 134 | #[cfg(test)] 135 | mod tests { 136 | use std::fmt::{Display, Formatter, Result as FmtResult}; 137 | 138 | use super::*; 139 | 140 | #[derive(Copy, Clone, Debug)] 141 | struct Dummy; 142 | 143 | impl Display for Dummy { 144 | fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 145 | write!(fmt, "Dummy error") 146 | } 147 | } 148 | 149 | impl Error for Dummy {} 150 | 151 | #[test] 152 | fn log_error_macro() { 153 | let err = Dummy; 154 | log_error!(Debug, err.into()); 155 | log_error!(Debug, &err.into()); 156 | log_error!(Debug, err.context("Another level").into()); 157 | log_error!(Debug, "Another level" => err); 158 | let multi_err = err.context("Another level").into(); 159 | log_error!(multi Info, multi_err); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/guide/configuration.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Loading and handling configuration 3 | 4 | Apart from the fact that one doesn't have to use the whole [`Spirit`][crate::Spirit] to load 5 | configuration, you can use the [`Loader`][crate::cfg_loader::Loader] if you don't need the rest of 6 | the stuff, there's nothing interesting about it. Or, is there? Well, there's a lot of little 7 | details you might want to use. 8 | 9 | # Sources of configuration 10 | 11 | The configuration comes from different sources and is merged together. The last one to set a value 12 | wins. 13 | 14 | * A configuration embedded inside the program through 15 | [config_defaults][crate::cfg_loader::ConfigBuilder::config_defaults]. 16 | * Configuration loaded from files. They are loaded in the order they appear on the command line. If 17 | it specifies a directory, all config files in there are loaded, in the order of sorting (the list 18 | is made anew at each reload). If no path is set on the command line, the value set through 19 | [config_default_paths][crate::cfg_loader::ConfigBuilder::config_default_paths]. 20 | * Things loaded from environment variables, if a [prefix is 21 | configured][crate::cfg_loader::ConfigBuilder::config_env]. 22 | * Last, the values set on command line through `--config-override`, are used. 23 | 24 | # Configuration phases 25 | 26 | Every time the configuration is loaded, it goes through few phases. If any of them fails, the whole 27 | loading fails. If it's the first time, the application aborts. On any future reload, if it fails, 28 | the error is logged, but the application continues with previous configuration. 29 | 30 | * First, the configuration is composed together and merged. 31 | * It is deserialized with [`serde`] and the configuration is filled into the provided structure. 32 | * The [validators][crate::extension::Extensible::config_validator] are run. They can schedule an 33 | action to run on commit, when everything succeeds, or on abort, if it fails. It allows to either 34 | postpone actually activating the configuration, or rolling back an attempt. Only once all of them 35 | succeed, the new configuration is activated. 36 | * Then the [`on_config`][crate::extension::Extensible::on_config] hooks are run. 37 | 38 | # Using the configuration 39 | 40 | One can use the configuration in multiple ways: 41 | 42 | * From the validator, try it out and schedule using it or throwing it out. This is useful if the 43 | configuration may still be invalid even if it is the right type. 44 | * With the `on_config` hook, if it can't possibly fail. 45 | * Look into it each time it is needed. There is the [`config`][crate::Spirit::config] method. 46 | 47 | A variation of the last one is propagating it through parts of program through 48 | [`arc-swap`](https://lib.rs/crates/arc-swap). It also have the [`access`][arc_swap::access] 49 | module, to „slice“ the shared configuration and provide 50 | only part of it to different parts of program. 51 | 52 | ```rust 53 | # use std::sync::Arc; 54 | # 55 | # use arc_swap::ArcSwap; 56 | # use serde::Deserialize; 57 | # use spirit::{Empty, Spirit}; 58 | # use spirit::prelude::*; 59 | # 60 | # fn some_part_of_app(_: T) {} 61 | #[derive(Clone, Debug, Default, Deserialize)] 62 | struct Cfg { 63 | // Something goes in here 64 | } 65 | 66 | let cfg_store = Arc::new(ArcSwap::from_pointee(Cfg::default())); 67 | 68 | Spirit::::new() 69 | .on_config({ 70 | let cfg_store = Arc::clone(&cfg_store); 71 | move |_, cfg| cfg_store.store(cfg.clone()) 72 | }) 73 | .run(|_| { 74 | some_part_of_app(cfg_store); 75 | Ok(()) 76 | }); 77 | ``` 78 | */ 79 | -------------------------------------------------------------------------------- /src/guide/daemonization.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Proper daemonization and early startup 3 | 4 | ## What is daemonization 5 | 6 | Traditionally, unix services are called daemons. When they start, they go into background instead 7 | of staying in the terminal they were started from. That makes the terminal available for further use 8 | and they don't get killed once the user terminates the session. 9 | 10 | While there are alternatives nowdays (`systemd` keeps the services it starts in "foreground", at 11 | least from the service's point of view, there's an external `daemonize` program, ...), it is still 12 | expected "proper" services are able to go into background on their own and have this ability 13 | configurable. 14 | 15 | Daemonization, among other things, entails: 16 | * Switching to either the `/` directory or to the daemon's home, to leave the place it was started 17 | from. 18 | * Possibly switching the user it runs under and dropping privileges. 19 | * Closing `stdin`, `stdout` and `stderr`. 20 | * Detaching from the terminal, by performing `fork` twice. 21 | 22 | ## The problem 23 | 24 | There are some subtleties about the order things are done (some of the problems are general, some 25 | are made even harder by the desire to go into background). 26 | 27 | * Printing help (and similar things, like printing configuration) and terminating should happen 28 | before the program attempts to actually do something. This lower the chance that something is 29 | undesirable and also the chance it would fail and would not get around to the help. 30 | * Configuring logging, panic handlers and similar should happen as early as possible, to preserve 31 | the information if the startup fails. Specifically, the program should be logging to a file by 32 | the time the program tries to go into background. 33 | * Daemonization uses `fork` internally and that one is **not safe** to be performed in a 34 | multi-threaded program. Therefore, daemonization needs to happen early enough ‒ if any kind of 35 | initialization starts additional threads, this may be performed only after daemonization is 36 | complete. Things known to start threads are `tokio` (or other worker threadpools and schedulers) 37 | and background logging. In particular, it is not possible to perform daemonization in an 38 | application created with the `#[tokio::main]` annotation and it is not possible to start 39 | background logging before daemonization. 40 | * Registering signal handlers is recommended before additional threads are started (though failing 41 | to do that is not inherently UB as with the daemonization, it only contains a tiny race condition 42 | when chaining multiple signal-handling libraries together). 43 | 44 | ## The correct order 45 | 46 | While multiple solutions are probably possible, here we present one observed to work. 47 | 48 | * Install the emergency shutdown signals handling (spirit handles signals and initiates a graceful 49 | shutdown, but if it gets stuck, we want second termination signal to have an immediate effect). 50 | * Handle the `--help`, `--help-config` and similar early, before any possible side effects or 51 | fallible operations (unfortunately, the handlers need to parse configuration before 52 | `--dump-config`, which might come a bit later). 53 | * Configure first-stage logging. This one must happen without background logging (logging in a 54 | background thread, which might be often more efficient, but we can't afford to do it just yet). 55 | * Perform daemonization. 56 | * Switch to full-featured logging, optionally with the background thread. 57 | * Start all the other things, including ones that may need threads. 58 | 59 | Beware that the pipelines and extensions are run in the order of registration (inside each relevant 60 | phase ‒ before-config, config validation, confirmation of thereof, on-config), but these are still 61 | delayed until the start of the `run` method. If you start any threads manually, do so within the 62 | `run`. 63 | 64 | ## Example 65 | 66 | ``` 67 | use serde::{Deserialize, Serialize}; 68 | use spirit::{utils, Pipeline, Spirit}; 69 | use spirit::fragment::driver::SilentOnceDriver; 70 | use spirit::prelude::*; 71 | use spirit_cfg_helpers::{Opts as CfgOpts}; 72 | use spirit_daemonize::{Daemon, Opts as DaemonOpts}; 73 | use spirit_log::{Cfg as Logging, Opts as LogOpts, CfgAndOpts as LogBoth}; 74 | use spirit_log::background::{Background, OverflowMode}; 75 | use structdoc::StructDoc; 76 | use structopt::StructOpt; 77 | 78 | #[derive(Clone, Debug, StructOpt)] 79 | struct Opts { 80 | #[structopt(flatten)] 81 | daemon: DaemonOpts, 82 | 83 | #[structopt(flatten)] 84 | logging: LogOpts, 85 | 86 | #[structopt(flatten)] 87 | cfg_opts: CfgOpts, 88 | 89 | // Other stuff goes in here 90 | } 91 | 92 | impl Opts { 93 | fn logging(&self) -> LogOpts { 94 | self.logging.clone() 95 | } 96 | fn cfg_opts(&self) -> &CfgOpts { 97 | &self.cfg_opts 98 | } 99 | } 100 | 101 | #[derive(Clone, Debug, Default, Deserialize, StructDoc, Serialize)] 102 | struct Cfg { 103 | #[serde(default)] 104 | daemon: Daemon, 105 | 106 | #[serde(default, skip_serializing_if = "Logging::is_empty")] 107 | logging: Logging, 108 | 109 | // Other stuff 110 | } 111 | 112 | impl Cfg { 113 | fn logging(&self) -> Logging { 114 | self.logging.clone() 115 | } 116 | } 117 | 118 | fn main() { 119 | // Set the emergency signal shutdown 120 | utils::support_emergency_shutdown().expect("Installing signals isn't supposed to fail"); 121 | 122 | Spirit::::new() 123 | // Some extra config setup, like `.config_defaults() or `.config_env` 124 | 125 | // Put help options early. They may terminate the program and we may want to do it before 126 | // daemonization or other side effects. 127 | .with(CfgOpts::extension(Opts::cfg_opts)) 128 | // Early logging without the background thread. Only once, then it is taken over by the 129 | // pipeline lower. We can't have the background thread before daemonization. 130 | .with( 131 | Pipeline::new("early-logging") 132 | .extract(|opts: &Opts, cfg: &Cfg| LogBoth { 133 | cfg: cfg.logging(), 134 | opts: opts.logging(), 135 | }) 136 | // Make sure this is run only once, at the very beginning. 137 | .set_driver(SilentOnceDriver::default()), 138 | ) 139 | // Plug in the daemonization configuration and command line arguments. The library will 140 | // make it alive ‒ it'll do the actual daemonization based on the config, it only needs to 141 | // be told it should do so this way. 142 | // 143 | // Must come very early, before any threads are started. That includes any potential 144 | // logging threads. 145 | .with(unsafe { 146 | spirit_daemonize::extension(|cfg: &Cfg, opts: &Opts| { 147 | (cfg.daemon.clone(), opts.daemon.clone()) 148 | }) 149 | }) 150 | // Now we can do the full logging, with a background thread. 151 | .with( 152 | Pipeline::new("logging") 153 | .extract(|opts: &Opts, cfg: &Cfg| LogBoth { 154 | cfg: cfg.logging(), 155 | opts: opts.logging(), 156 | }) 157 | .transform(Background::new(100, OverflowMode::Block)), 158 | ) 159 | // More things and pipelines can go in here, including ones that start threads. 160 | .run(|_| { 161 | // And we can start more threads here manually. 162 | Ok(()) 163 | }); 164 | } 165 | ``` 166 | */ 167 | -------------------------------------------------------------------------------- /src/guide/extend.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Extending Spirit 3 | 4 | Eventually, you may want to create your own [`Extension`]s and [`Fragment`]s, for more convenient 5 | reusability. 6 | 7 | # Extensions 8 | 9 | [`Extension`]s are just glorified functions. They get a [`Builder`], modify it and return it back. 10 | They, however, can be plugged in conveniently by the 11 | [`with`][crate::extension::Extensible::with]. You can create an extension to plug in 12 | configuration to a part of application, or a reusable 13 | library. 14 | 15 | ```rust 16 | use spirit::{Builder, Empty, Spirit}; 17 | use spirit::prelude::*; 18 | 19 | fn ext(builder: Builder) -> Builder { 20 | builder.on_terminate(|| println!("I'm done here")) 21 | } 22 | 23 | Spirit::::new().with(ext).run(|_| Ok(())); 24 | ``` 25 | 26 | # Fragments 27 | 28 | A fragment is a bit of configuration that can create *something*. They are to be used through 29 | [`Pipeline`]s and that pipeline will become an extension, so it can be registered just like one. 30 | 31 | That happens in two phases (a `Seed` and a `Resource`). This allows for creating resources that 32 | actually allocate something unique in the OS (maybe a socket listening on a port) and connect it 33 | with some further configuration. If only the additional configuration is changed, only the second 34 | phase is run. This works even if the reconfiguration fails ‒ it doesn't require relinquishing the 35 | original in the attempt. 36 | 37 | Nevertheless, most of the times one doesn't need both instances. It is enough to specify the driver 38 | (see below), a function to create that something and provide a type that puts it to use (an 39 | [`Installer`]). For that, you can use the [`simple_fragment`] macro. 40 | 41 | # Drivers 42 | 43 | Most of the time, if configuration is reloaded, most of it stays the same. But replacing everything 44 | inside the application may be a bit expensive and therefore wasteful. So pipelines can decide when 45 | it makes sense to re-run either both or just the second phase or if whatever they manage shall stay 46 | the same. 47 | 48 | Drivers are what manages this context and makes the decision. There's a selection of them in the 49 | [`drivers`][crate::fragment::driver]. The fragment chooses the default driver for it, but the user 50 | can override it. 51 | 52 | Note that composite fragments (eg. `Vec`) also compose their drivers, to make the 53 | composite types work ‒ the outer (the driver for the `Vec`) keeps track and is able to add and 54 | remove instances as needed. 55 | 56 | Note that for a fragment to participate in this composition, one need to implement the relevant 57 | marker traits ([`Stackable`][crate::fragment::Stackable], [`Optional`][crate::fragment::Optional]. 58 | 59 | # Installers 60 | 61 | At the end of the pipeline, there needs to be a way to put the resource to use or to withdraw it if 62 | it no longer should exist according to the documentation. That's done by the [`Installer`]. It can 63 | install it into a global place (for example the logger is installed into a global place, and is 64 | *not* `Stackable` by the way). Some others install into a specific place (and therefore need to be 65 | provided by the user). 66 | 67 | The installer returns a handle to the installed resource. By dropping the handle, the pipeline 68 | signals that the resource should be removed. 69 | 70 | [`Extension`]: crate::extension::Extension 71 | [`Fragment`]: crate::fragment::Fragment 72 | [`Builder`]: crate::Builder 73 | [`Installer`]: crate::fragment::Installer 74 | [`simple_fragment`]: crate::simple_fragment 75 | [`Pipeline`]: crate::fragment::pipeline::Pipeline 76 | */ 77 | -------------------------------------------------------------------------------- /src/guide/fragments.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Using Fragments and Pipelines 3 | 4 | It is possible to integrate with [`Spirit`] by registering the callbacks directly. But if one needs 5 | to track state with that ‒ replacing an old instance of something if the configuration changed, but 6 | not bothering if it is the same and putting it to use in the application ‒ that might be a bit of 7 | work to do every time. This pattern, when something is created form a bit of configuration and then 8 | put to use, is abstracted by the [`Pipeline`]. 9 | 10 | The pipeline works by going through several phases. 11 | 12 | * First, the fragment is extracted from configuration (and/or the command line options). 13 | * The fragment is optionally checked for equality from last time. 14 | * If it is different (or configured to get replaced every time), a Resource is created ‒ the thing 15 | one is interested in. 16 | * It might go through a series of transformations. 17 | * Eventually, it is installed. 18 | 19 | The idea is, this allows building a customized resources from bits of configuration. The 20 | [`Fragment`] usually comes with enough setup, so it is enough to provide the function to extract 21 | it, unless something unusual is needed. 22 | 23 | Note that pipelines have names. They are used in logging. 24 | 25 | # Composing things 26 | 27 | Apart from tracking changes to stuff, pipelines can do one more thing. One can use an `Option` or 28 | `Vec` of the configuration fragment (or one of few other containers). Then as many instances are 29 | created and installed, and they are removed or added as needed. 30 | 31 | # Customization 32 | 33 | * The strategy of when and how to replace the instances is done through the 34 | [set_driver][crate::fragment::pipeline::Pipeline::set_driver]. 35 | * One can modify the instance of the resource through the 36 | [transform][crate::fragment::pipeline::Pipeline::transform], 37 | [map][crate::fragment::pipeline::Pipeline::map] or 38 | [and_then][crate::fragment::pipeline::Pipeline::and_then]. 39 | * How one installs is set up through [install][crate::fragment::pipeline::Pipeline::install]. 40 | 41 | # Quirks of pipelines 42 | 43 | Pipelines use generics heavily. Furthermore, the „check“ if all the types align in them (and if 44 | they implement the right traits) is performed at the very end. That makes it possible for the types 45 | *not* to align on the way and enables creating of [`Fragment`]s that are not complete (for 46 | example, they *need* some post-processing from the caller, but already want to set the installer, 47 | which doesn't match at the beginning). The downside is, there's often too much flexibility in the 48 | solutions for `rustc` to give reasonable hints in the error message. 49 | 50 | The [`check`][crate::fragment::pipeline::Pipeline::check] method does nothing (it doesn't even 51 | modify the pipeline), except that it forces the type checking at the point. Placing it at strategic 52 | place (even at the end) might help `rustc` produce a better hint. 53 | 54 | [`Spirit`]: crate::Spirit 55 | [`Pipeline`]: crate::fragment::pipeline::Pipeline 56 | [`Fragment`]: crate::fragment::Fragment 57 | */ 58 | -------------------------------------------------------------------------------- /src/guide/levels.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Low and high level APIs 3 | 4 | The way most of the documentation describes `spirit`, it is meant as batteries included manager of 5 | your application. While that might speed up development and take care of everything, sometimes you 6 | might need to do things your way, not the way the library wants to. So this section describes the 7 | various levels of the API, so you can take advantage of parts of what is offered while doing the 8 | rest in a custom way. 9 | 10 | Let's start with the lowest tier first. 11 | 12 | # Loading of configuration 13 | 14 | The first tier helps with configuration loading. You specify the structures that should be 15 | filled with the configuration or command line options. `Spirit` reads the command line, finds the 16 | relevant configuration files (depending on both compiled-in values and values on the command 17 | line), scans configuration directories and hands the configuration to you. 18 | 19 | You can also ask it to reload the configuration later on, to see if it changed (but you do so 20 | manually). 21 | 22 | This basic configuration loading lives in the [`cfg_loader`][crate::cfg_loader] module. 23 | 24 | ```rust 25 | use serde::Deserialize; 26 | use spirit::{AnyError, ConfigBuilder, Empty}; 27 | use spirit::cfg_loader::Builder; 28 | 29 | #[derive(Debug, Default, Deserialize)] 30 | struct Cfg { 31 | message: String, 32 | } 33 | 34 | static DEFAULT_CFG: &str = r#" 35 | message = "hello" 36 | "#; 37 | 38 | fn main() -> Result<(), AnyError> { 39 | // Don't care about command line options - there are none in addition to specifying the 40 | // configuration. If we wanted some more config options, we would use a StructOpt 41 | // structure instead of Empty. 42 | // 43 | // If the user specifies invalid options, a help is printed and the application exits. 44 | let (Empty {}, mut loader) = Builder::new() 45 | .config_defaults(DEFAULT_CFG) 46 | .build(); 47 | 48 | // This can be done as many times as needed, to load fresh configuration. 49 | let cfg: Cfg = loader.load()?; 50 | 51 | // The interesting stuff of your application. 52 | println!("{}", cfg.message); 53 | Ok(()) 54 | } 55 | ``` 56 | 57 | # Prefabricated fragments of configuration 58 | 59 | *Having* your configuration is not enough. You need to *do* something with the configuration. 60 | And if it is something specific to your service, then there's nothing much `spirit` can do. But 61 | usually, there's a lot of the common functionality ‒ you want to configure logging, ports your 62 | service listens on, etc. 63 | 64 | For that reason, there are additional crates that each bring some little fragment you can reuse 65 | in your configuration. That fragment provides the configuration options that'll appear inside 66 | the configuration. But it also comes with functionality to create whatever it is being 67 | configured with just a method call. 68 | 69 | Most of them are described by the [`Fragment`][crate::fragment::Fragment] trait which allows it 70 | to participate in some further tiers. 71 | 72 | Currently, there are these crates with fragments: 73 | 74 | * [`spirit-daemonize`](https://crates.rs/crates/spirit-daemonize): Configuration and routines to go 75 | into background and be a nice daemon. 76 | * [`spirit-dipstick`](https://crates.rs/crates/spirit-dipstick): Configuration of the dipstick 77 | metrics library. 78 | * [`spirit-log`](https://crates.rs/crates/spirit-log): Configuration of logging. 79 | * [`spirit-tokio`](https://crates.rs/crates/spirit-tokio): Integrates basic tokio primitives ‒ 80 | auto-reconfiguration for TCP and UDP sockets and starting the runtime. 81 | * [`spirit-hyper`](https://crates.rs/crates/spirit-hyper): Integrates the hyper web server. 82 | * [`spirit-reqwest`](https://crates.rs/crates/spirit-reqwest): Configuration for the reqwest HTTP 83 | [`Client`](https://docs.rs/reqwest/0.10.10/reqwest/struct.Client.html). 84 | 85 | Also, while this is not outright a configuration fragment, it comes close. When you build your 86 | configuration from the fragments, there's a lot of options. The 87 | [`spirit-cfg-helpers`](https://crates.rs/crates/spirit-cfg-helpers) crate brings the 88 | `--help-config` and `--dump-config` command line options, that describe what options can be 89 | configured and what values would be used after combining all configuration sources together. 90 | 91 | You can create your own fragments and, if it's something others could use, share them. 92 | 93 | ```rust 94 | use log::info; 95 | use serde::Deserialize; 96 | use spirit::AnyError; 97 | use spirit::cfg_loader::{Builder, ConfigBuilder}; 98 | use spirit::fragment::Fragment; 99 | use spirit_log::{Cfg as LogCfg, CfgAndOpts as Logging, Opts as LogOpts}; 100 | use structopt::StructOpt; 101 | 102 | #[derive(Debug, Default, Deserialize)] 103 | struct Cfg { 104 | message: String, 105 | // Some configuration options to configure logging. 106 | #[serde(default, skip_serializing_if = "LogCfg::is_empty")] 107 | logging: LogCfg, 108 | } 109 | 110 | #[derive(Clone, Debug, StructOpt)] 111 | struct Opts { 112 | // And some command line switches to also interact with logging. 113 | #[structopt(flatten)] 114 | logging: LogOpts, 115 | } 116 | 117 | static DEFAULT_CFG: &str = r#" 118 | message = "hello" 119 | "#; 120 | 121 | fn main() -> Result<(), AnyError> { 122 | // Here we added 123 | let (opts, mut loader): (Opts, _) = Builder::new() 124 | .config_defaults(DEFAULT_CFG) 125 | .build(); 126 | let cfg: Cfg = loader.load()?; 127 | 128 | // We put them together (speciality of the logging fragments ‒ some other fragments come 129 | // only in the configuration). 130 | let logging = Logging { 131 | cfg: cfg.logging, 132 | opts: opts.logging, 133 | }; 134 | // And here we get ready-made top level logger we can use. 135 | // (the "logging" string helps to identify fragments in logs ‒ when stuff gets complex, 136 | // naming things helps). 137 | // 138 | // This can configure multiple loggers at once (STDOUT, files, network…). 139 | let logger = logging.create("logging")?; 140 | // This apply is from the fern crate. It's one-time initialization. If you want to update 141 | // logging at runtime, see the next section. 142 | logger.apply()?; 143 | 144 | // The interesting stuff of your application. 145 | info!("{}", cfg.message); 146 | Ok(()) 147 | } 148 | ``` 149 | 150 | # Application lifetime management 151 | 152 | There's the [`Spirit`] object (and its [`Builder`]) you can use. 153 | It'll start by loading the configuration. It'll also wait for signals (like `SIGHUP` or 154 | `SIGTERM`) and reload configuration as needed, terminate the application, provide access to the 155 | currently loaded configuration, etc. This is done in a background thread which registers the 156 | signals using [`signal_hook`]. 157 | 158 | You can attach callbacks to it that'll get called at appropriate times ‒ when the configuration 159 | is being loaded (and you can refuse the configuration as invalid) or when the application 160 | should terminate. 161 | 162 | The callbacks can be added both to the [`Builder`] and to already started [`Spirit`]. 163 | 164 | You also can have your main body of the application wrapped in the 165 | [`Spirit::run`][crate::SpiritBuilder::run] method. That way any errors returned are properly 166 | logged and the application terminates with non-zero exit status. 167 | 168 | Note that the functionality of these is provided through several traits. It is recommended to 169 | import the [`spirit::prelude::*`][crate::prelude] to get all the relevant traits. 170 | 171 | ```rust 172 | use std::time::Duration; 173 | use std::thread; 174 | 175 | use log::{debug, info}; 176 | use serde::Deserialize; 177 | use spirit::Spirit; 178 | use spirit::prelude::*; 179 | use spirit::validation::Action; 180 | use spirit_log::{Cfg as LogCfg, CfgAndOpts as Logging, Opts as LogOpts}; 181 | use structopt::StructOpt; 182 | 183 | #[derive(Debug, Default, Deserialize)] 184 | struct Cfg { 185 | message: String, 186 | sleep: u64, 187 | #[serde(default, skip_serializing_if = "LogCfg::is_empty")] 188 | logging: LogCfg, 189 | } 190 | 191 | #[derive(Clone, Debug, StructOpt)] 192 | struct Opts { 193 | // And some command line switches to also interact with logging. 194 | #[structopt(flatten)] 195 | logging: LogOpts, 196 | } 197 | 198 | static DEFAULT_CFG: &str = r#" 199 | message = "hello" 200 | sleep = 2 201 | "#; 202 | 203 | fn main() { 204 | // Sets up spirit_log ‒ it will register panic handler to log panics. It will also prepare 205 | // the global logger so the actual logger can be replaced multiple times, using the 206 | // spirit_log::install 207 | spirit_log::init(); 208 | Spirit::::new() 209 | // Provide default values for the configuration 210 | .config_defaults(DEFAULT_CFG) 211 | // If the program is passed a directory, load files with these extensions from there 212 | .config_exts(&["toml", "ini", "json"]) 213 | .on_terminate(|| debug!("Asked to terminate")) 214 | .config_validator(|_old_cfg, cfg, opts| { 215 | let logging = Logging { 216 | opts: opts.logging.clone(), 217 | cfg: cfg.logging.clone(), 218 | }; 219 | // Whenever there's a new configuration, create new logging 220 | let logger = logging.create("logging")?; 221 | // But postpone the installation until the whole config has been validated and 222 | // accepted. 223 | Ok(Action::new().on_success(|| spirit_log::install(logger))) 224 | }) 225 | // Run the closure, logging the error nicely if it happens (note: no error happens 226 | // here) 227 | .run(|spirit: &_| { 228 | while !spirit.is_terminated() { 229 | let cfg = spirit.config(); // Get a new version of config every round 230 | thread::sleep(Duration::from_secs(cfg.sleep)); 231 | info!("{}", cfg.message); 232 | # spirit.terminate(); // Just to make sure the doc-test terminates 233 | } 234 | Ok(()) 235 | }); 236 | } 237 | ``` 238 | 239 | # Extensions and pipelines 240 | 241 | The crates with fragments actually allow their functionality to happen almost automatically. 242 | Instead of manually registering a callback when eg. the config is reloaded, each fragment can 243 | be either directly registered into the [`Spirit`] (or [`Builder`]) to do its thing whenever 244 | appropriate, or allows building a [`Pipeline`][crate::Pipeline] that handles loading and 245 | reloading the bit of configuration. 246 | 247 | As an example, if the application shall listen on a HTTP endpoint, instead of registering an 248 | [`on_config`][crate::Extensible::on_config] callback and creating the server based on the new 249 | configuration (and shutting down the previous one as needed), you build a pipeline. You provide 250 | a function that extracts the HTTP endpoint configuration from the whole configuration, you 251 | provide a closure that attaches the actual service to the server and register the pipeline. The 252 | pipeline then takes care of creating the server (or servers, if the configuration contains eg. 253 | a `Vec` of them), removing stale ones, rolling back the configuration in case something in it 254 | is broken, etc. 255 | 256 | Note that some pipelines and extensions are better registered right away, into the [`Builder`] 257 | (daemonization, logging), you might want to register others only when you are ready for them ‒ 258 | you may want to start listening on HTTP only once you've loaded all data. In that case you'd 259 | register it into the [`Spirit`] inside the [`run`][crate::SpiritBuilder::run] method. 260 | 261 | ```rust 262 | use std::time::Duration; 263 | use std::thread; 264 | 265 | use log::{debug, info}; 266 | use serde::{Serialize, Deserialize}; 267 | use spirit::{Pipeline, Spirit}; 268 | use spirit::prelude::*; 269 | use spirit_cfg_helpers::Opts as CfgOpts; 270 | use spirit_daemonize::{Daemon, Opts as DaemonOpts}; 271 | use spirit_log::{Cfg as LogCfg, CfgAndOpts as Logging, Opts as LogOpts}; 272 | use structdoc::StructDoc; 273 | use structopt::StructOpt; 274 | 275 | #[derive(Debug, Default, Deserialize, Serialize, StructDoc)] 276 | struct Cfg { 277 | /// The message to print every now and then. 278 | message: String, 279 | 280 | /// How long to wait in between messages, in seconds. 281 | sleep: u64, 282 | 283 | /// How and where to log. 284 | #[serde(default, skip_serializing_if = "LogCfg::is_empty")] 285 | logging: LogCfg, 286 | 287 | /// How to switch into the background. 288 | #[serde(default)] 289 | daemon: Daemon, 290 | } 291 | 292 | #[derive(Clone, Debug, StructOpt)] 293 | struct Opts { 294 | #[structopt(flatten)] 295 | logging: LogOpts, 296 | #[structopt(flatten)] 297 | daemon: DaemonOpts, 298 | #[structopt(flatten)] 299 | cfg_opts: CfgOpts, 300 | } 301 | 302 | static DEFAULT_CFG: &str = r#" 303 | message = "hello" 304 | sleep = 2 305 | "#; 306 | 307 | fn main() { 308 | Spirit::::new() 309 | // Provide default values for the configuration 310 | .config_defaults(DEFAULT_CFG) 311 | // If the program is passed a directory, load files with these extensions from there 312 | .config_exts(&["toml", "ini", "json"]) 313 | .on_terminate(|| debug!("Asked to terminate")) 314 | // Daemonization must go early, before any threads are started. 315 | .with(unsafe { 316 | spirit_daemonize::extension(|cfg: &Cfg, opts: &Opts| { 317 | (cfg.daemon.clone(), opts.daemon.clone()) 318 | }) 319 | }) 320 | // All the validation, etc, is done for us behind the scene here. 321 | // Even the spirit_log::init is not needed, the pipeline handles that. 322 | .with(Pipeline::new("logging").extract(|opts: &Opts, cfg: &Cfg| Logging { 323 | cfg: cfg.logging.clone(), 324 | opts: opts.logging.clone(), 325 | })) 326 | // Let's provide some --config-help and --config-dump options. These get the 327 | // information from the documentation strings we provided inside the structures. It 328 | // also uses the `Serialize` trait to provide the dump. 329 | .with(CfgOpts::extension(|opts: &Opts| &opts.cfg_opts)) 330 | // And some help 331 | // Run the closure, logging the error nicely if it happens (note: no error happens 332 | // here) 333 | .run(|spirit: &_| { 334 | while !spirit.is_terminated() { 335 | let cfg = spirit.config(); // Get a new version of config every round 336 | thread::sleep(Duration::from_secs(cfg.sleep)); 337 | info!("{}", cfg.message); 338 | # spirit.terminate(); // Just to make sure the doc-test terminates 339 | } 340 | Ok(()) 341 | }); 342 | } 343 | ``` 344 | 345 | [`Builder`]: crate::Builder 346 | [`Spirit`]: crate::Spirit 347 | */ 348 | -------------------------------------------------------------------------------- /src/guide/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The user guide 3 | 4 | *This module doesn't contain any APIs or code. It contains only the documentation.* 5 | 6 | In short, when writing a daemon or a service, we have the *"muscle"* of the 7 | application ‒ whatever we write the daemon for. And we have a whole lot of 8 | infrastructure around that: logging, command line parsing, configuration, to 9 | name a few. While there are Rust libraries for all that, it requires a 10 | non-trivial amount of boilerplate code to bridge all this together. 11 | 12 | Spirit aims to be this bridge. 13 | 14 | It takes care of things like 15 | 16 | - signal handling and the application lifecycle 17 | - combining multiple pieces of configuration together with command line and environment variables 18 | overrides 19 | - it allows for reloading the configuration at runtime 20 | - application metrics 21 | 22 | It leverages the existing crate ecosystem and provides the plumbing to connect all the necessary 23 | pieces together. Nevertheless, it specifically aims *not* to be a framework. While there is a 24 | certain way the library is designed to work, it isn't *required* to be used that way and should 25 | gracefully get out of your way at places where you want to do something in your own way, or to 26 | perform only part of the management. 27 | 28 | You'll find here: 29 | 30 | * [Description of basic principles][self::principles] 31 | * [Lower and higher levels of API][self::levels] 32 | * [A tutorial][self::tutorial] 33 | * Common tasks 34 | - [Loading & handling configuration][self::configuration] 35 | * Advanced topis: 36 | - [Using fragments and pipelines][self::fragments] 37 | - [Extending with own fragments][self::extend] 38 | - [Proper daemonization and early startup][self::daemonization] 39 | - [Testing][self::testing] 40 | */ 41 | 42 | pub mod configuration; 43 | pub mod daemonization; 44 | pub mod extend; 45 | pub mod fragments; 46 | pub mod levels; 47 | pub mod principles; 48 | pub mod testing; 49 | pub mod tutorial; 50 | -------------------------------------------------------------------------------- /src/guide/principles.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The basic principles 3 | 4 | At the core, the [`Spirit`] is the top level manager of things. It is parametrized by two 5 | structures, one to hold parsed command line (implementing [`StructOpt`]), the other for holding the 6 | parsed configuration options (implementing [`serde`'s `Derive`][`Deserialize`]). It parses the 7 | command line, enriched with some options of its own. It uses these options to find the 8 | configuration files and loads the configuration for the first time. 9 | 10 | To manage the life time of the application, it is possible to register hooks into the manager. They 11 | are called on certain events ‒ when new configuration is loaded (both the first time and when it is 12 | reloaded), when the application is about to be terminated, on registered signal, etc. 13 | 14 | It then runs the „body“ of the whole application and leaves it to run. The manager does its job in 15 | its own background thread. 16 | 17 | Note that many things here can be turned off (see further chapters about that) if they are not 18 | doing what you want. 19 | 20 | # Extensions and pipelines 21 | 22 | Having a place to put callbacks might be handy a bit, but certainly nothing to make any fuss about 23 | (or write tutorials about). The added value of the library is the system of 24 | [extensions][crate::extension::Extension], [fragments][crate::fragment::Fragment] and 25 | [pipelines][crate::fragment::pipeline::Pipeline]. There are other, related crates (eg. 26 | `spirit-log`) that provide these and allow easy and reusable plugging of functionality in. 27 | 28 | An extension simply modifies the [builder][crate::Builder], usually by registering some callbacks. 29 | The fragments and pipelines are ways to build extensions from already existing parts. They are 30 | explained in detail in [their own chapter][super::fragments]. 31 | 32 | # Error handling conventions 33 | 34 | In a library like this, quite a lot of things can go wrong. Some of the errors might come from the 35 | library, but many would come from user-provided code in callbacks, extensions, etc. Error are 36 | passed around as a boxed trait objects (the [`AnyError`][crate::AnyError] is just a boxed trait 37 | object). Other option would be something like the [`anyhow`](https://lib.rs/anyhow), but that would 38 | force the users of the library into a specific one. Future versions might go that way if there's a 39 | clear winner between these crates, but until then we stay with only what [`std`] provides. 40 | 41 | It is expected that most of these errors can't be automatically handled by the application, 42 | therefore distinguishing types of the errors isn't really a concern most of the time, though it 43 | is possible to get away with downcasting. In practice, most of the errors will end up somewhere 44 | in logs or other places where users can read them. 45 | 46 | To make the errors more informative, the library constructs layered errors (or error chains). 47 | The outer layer is the high level problem, while the inner ones describe the causes of the 48 | problem. It is expected all the layers are presented to the user. When the errors are handled 49 | by the library (either in termination error or with unsuccessful configuration reload), the 50 | library prints all the layers. To replicate similar behaviour in user code, it is possible to 51 | use the [`log_error`][macro@crate::log_error] macro or [`log_error`][fn@crate::error::log_error] 52 | function. 53 | 54 | Internally, the library uses the [`err-context`] crate to construct and handle such errors. In 55 | addition to constructing such errors, the crate also allows some limited examination of error 56 | chains. However, users are not forced to use that crate as the chains constructed are based 57 | directly on the [`std::error::Error`] trait and are therefore compatible with errors constructed in 58 | any other way. 59 | 60 | [`Spirit`]: crate::Spirit 61 | [`StructOpt`]: structopt::StructOpt 62 | [`Deserialize`]: serde::Deserialize 63 | [`err-context`]: https://lib.rs/crates/err-context 64 | */ 65 | -------------------------------------------------------------------------------- /src/guide/testing.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Testing 3 | 4 | Sometimes, during tests, one might want to run an almost-full application. It can be done by 5 | executing the compiled binary externally, but such thing is quite heavy-weight to orchestrate 6 | (making sure it is already compiled, providing configuration, waiting for it to become ready, 7 | shutting it down properly). 8 | 9 | Alternatively, it is possible to get close to full application setup inside a local test function 10 | with spirit. 11 | 12 | First, make sure the application setup is a separate function that can be applied to the spirit 13 | [Builder]. That'll allow the tests [Spirit] to use the same core setup as the application. The only 14 | missing part is passing the right configuration and arguments to it (because it should *not* use 15 | the arguments passed to the test process). To do so, the [config Builder] can be used. 16 | 17 | We also disable the background management thread. First, we want to make sure we don't touch signal 18 | handlers so things work as expected. Second, this allows having multiple parallel instances of the 19 | "application" (rust tests run in parallel). 20 | 21 | [Builder]: crate::Builder 22 | [Spirit]: crate::Spirit 23 | [config Builder]: crate::cfg_loader::Builder 24 | 25 | ```rust 26 | // This goes to some tests/something.rs or similar place. 27 | use std::sync::{mpsc, Arc}; 28 | 29 | use spirit::prelude::*; 30 | use spirit::{Empty, Spirit}; 31 | 32 | // Custom configuration "file" 33 | let TEST_CFG: &str = r#" 34 | [section] 35 | option = true 36 | "#; 37 | 38 | // Note: Some "real" config and opts structures are likely used here. 39 | let app = Spirit::::new() 40 | // Inject the "config file" in it. 41 | .config_defaults(TEST_CFG) 42 | // Provide already parsed command line argument structure here. Ours is `Empty` here, but it's 43 | // whatever you use in the app. 44 | // 45 | // This also ignores the real command line arguments, so it's important to call in the test 46 | // even if you pass `Default::default()`. We want to ignore the arguments passed to the test 47 | // binary. 48 | .preparsed_opts(Empty::default()) 49 | // False -> we don't have the background management thread that listens to signals. Lifetime is 50 | // managed in a different way here in the test. 51 | .build(false) 52 | .expect("Failed to create the test spirit app"); 53 | 54 | // We want to wait for the background thread to fully start up as necessary so the test doesn't do 55 | // its stuff too early. We'll let the application to signal us. 56 | let (ready_send, ready_recv) = mpsc::channel::<()>(); 57 | 58 | // A spirit copy to go inside the closure below. 59 | let spirit = Arc::clone(app.spirit()); 60 | 61 | // The running is a RAII guard. Once it's dropped, it will terminate spirit and check it terminated 62 | // correctly. Runs the application in another thread. 63 | let running = app.run_test(move || { 64 | // Once everything is started up enough for the test to start doing stuff, we'll tell it to 65 | // proceed (ignore errors in case the test somehow died). 66 | let _ = ready_send.send(()); 67 | // The inside of the application. You should have a function that goes inside `run` and 68 | // call it both here and from the real `main`'s `run`. 69 | while !spirit.is_terminated() { 70 | // Do stuff! 71 | } 72 | Ok(()) 73 | }); 74 | 75 | // Wait for the background thread to be ready (or dead, which would error out and the test would 76 | // fail). 77 | let _ = ready_recv.recv(); 78 | 79 | // Now, we can do our testing here. After we are done, the above will just shut down all the stuff. 80 | assert!(!running.spirit().is_terminated()); 81 | ``` 82 | */ 83 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(test(attr(deny(warnings))))] 2 | #![allow( 3 | unknown_lints, 4 | renamed_and_removed_lints, 5 | clippy::unknown_clippy_lints, 6 | clippy::type_complexity, 7 | clippy::needless_doctest_main 8 | )] 9 | #![forbid(unsafe_code)] 10 | #![warn(missing_docs)] 11 | #![cfg_attr(docsrs, feature(doc_cfg))] 12 | 13 | //! Helpers to cut down on boilerplate when writing services. 14 | //! 15 | //! When writing a service (in the unix terminology, a daemon), there are two parts of the job. One 16 | //! is the actual functionality of the service, the part that makes it different than all the other 17 | //! services out there ‒ in other words, the interesting part. And then there's the very boring 18 | //! part of turning the prototype implementation into a well-behaved service with configuration, 19 | //! logging, metrics, signal handling and whatever else one considers to be part of the deal. 20 | //! 21 | //! This crate is supposed to help with the latter. Surely, there's still something left to do but 22 | //! the aim is to provide reusable building blocks to get the boring stuff done as fast as minimal 23 | //! fuss as possible. 24 | //! 25 | //! # Foreword 26 | //! 27 | //! Before using this crate (or, family of crates), you should know few things: 28 | //! 29 | //! * While there has been some experimentation how the API should look like and it is being used 30 | //! in production software, the API is probably not final. One one hand that means upgrading to 31 | //! next version might need some work. On the other hand, if it doesn't fit your needs or use 32 | //! case, this is a great time to discuss it now, it might be possible to amend it and make it do 33 | //! what you need in the next version. 34 | //! * Also, there's a lot to be done still ‒ both in terms of documentation, tutorials, examples 35 | //! but missing functionality as well (eg. fragments for configuring more things). Help in that 36 | //! direction is welcome ‒ if you find yourself in the need to configure something and have to 37 | //! roll your own implementation, consider sharing it with others. 38 | //! * It is being tested on unix, with unix-style daemons. Supporting Windows should be possible in 39 | //! theory, but I don't have it at hand. If you use Windows, consider trying it out and adding 40 | //! support. 41 | //! * The crate is on the heavier spectrum when it comes to dependencies, with aim for 42 | //! functionality and ease of use. Think more about server-side or desktop services. While it is 43 | //! possible to cut down on them somewhat by tweaking the feature flags, you probably don't want 44 | //! to use this in embedded scenarios. 45 | //! * The crate doesn't come with much original functionality. Mostly, it is a lot of other great 46 | //! crates glued together to create a cohesive whole. That means you can do most of the stuff 47 | //! without spirit (though the crates come with few little utilities or tiny workarounds for 48 | //! problems you would face if you started to glue the things together). 49 | //! 50 | //! # The user guide 51 | //! 52 | //! You will find the API documentation here, but we also have the [user guide][crate::guide], 53 | //! including a [tutorial][crate::guide::tutorial]. It might help you to get up to speed to read 54 | //! through it, to know about the principles of how it works. 55 | //! 56 | //! # Features 57 | //! 58 | //! There are several features that can tweak functionality. Currently, the `json`, `yaml` and 59 | //! `cfg-help` are on by default. All the other spirit crates depend only on the bare minimum of 60 | //! features they need (and may have their own features). 61 | //! 62 | //! * `ini`, `json`, `hjson`, `yaml`: support for given configuration formats. 63 | //! * `cfg-help`: support for adding documentation to the configuration fragmtents that can be used 64 | //! by the [`spirit-cfg-helpers`] crate to add the `--help-config` command line option. It is 65 | //! implemented by the [`structdoc`] crate behind the scenes. On by default. This feature flag is 66 | //! actually available in all the other sub-crates too. 67 | //! * `color`: support for colored command line help (on by default). 68 | //! * `suggestions`: support for command line suggestions on errors (on by default). 69 | //! 70 | //! [`Spirit`]: crate::Spirit 71 | //! [`Builder`]: crate::Builder 72 | //! [`AnyError`]: crate::AnyError 73 | //! [`log_error`]: macro@crate::log_error 74 | //! [`serde`]: https://lib.rs/crates/serde 75 | //! [`Deserialize`]: https://docs.rs/serde/*/serde/trait.Deserialize.html 76 | //! [`config`]: https://lib.rs/crates/config 77 | //! [`signal-hook`]: https://lib.rs/crates/signal-hook 78 | //! [`spirit-cfg-helpers`]: https://lib.rs/crates/spirit-cfg-helpers 79 | //! [`spirit-daemonize`]: https://lib.rs/crates/spirit-daemonize 80 | //! [`spirit-log`]: https://lib.rs/crates/spirit-log 81 | //! [`spirit-tokio`]: https://lib.rs/crates/spirit-tokio 82 | //! [`spirit-reqwest`]: https://lib.rs/crates/spirit-reqwest 83 | //! [`spirit-hyper`]: https://lib.rs/crates/spirit-hyper 84 | //! [`spirit-dipstick`]: https://lib.rs/crates/spirit-dipstick 85 | //! [reqwest-client]: https://docs.rs/reqwest/~0.9.5/reqwest/struct.Client.html 86 | //! [repository]: https://github.com/vorner/spirit 87 | //! [tutorial]: https://vorner.github.io/2018/12/09/Spirit-Tutorial.html 88 | //! [`err-context`]: https://lib.rs/crates/err-context 89 | //! [`failure`]: https://lib.rs/crates/failure 90 | //! [`err-context`]: https://lib.rs/crates/err-context 91 | //! [`err-derive`]: https://lib.rs/crates/err-derive 92 | 93 | pub mod app; 94 | mod bodies; 95 | pub mod cfg_loader; 96 | mod empty; 97 | pub mod error; 98 | pub mod extension; 99 | pub mod fragment; 100 | pub mod guide; 101 | #[doc(hidden)] 102 | pub mod macro_support; 103 | mod spirit; 104 | pub mod terminate_guard; 105 | pub mod utils; 106 | pub mod validation; 107 | 108 | pub use crate::cfg_loader::ConfigBuilder; 109 | pub use crate::empty::Empty; 110 | pub use crate::error::AnyError; 111 | pub use crate::extension::Extensible; 112 | pub use crate::fragment::pipeline::Pipeline; 113 | pub use crate::fragment::Fragment; 114 | pub use crate::spirit::{Builder, Spirit, SpiritBuilder}; 115 | 116 | /// The prelude. 117 | /// 118 | /// To use the spirit libraries effectively, a lot of traits need to be imported. Instead 119 | /// of importing them one by one manually, the [`prelude`][crate::prelude] contains the most 120 | /// commonly used imports that are used around application runtime management. This imports only 121 | /// traits and only in anonymous mode (eg. `pub use spirit::SpiritBuilder as _`). 122 | /// 123 | /// This can be imported as `use spirit::prelude::*`. 124 | pub mod prelude { 125 | pub use super::{ConfigBuilder as _, Extensible as _, Fragment as _, SpiritBuilder as _}; 126 | } 127 | -------------------------------------------------------------------------------- /src/macro_support.rs: -------------------------------------------------------------------------------- 1 | //! Reexports and support for our macros. 2 | //! 3 | //! This module contains reexports of some foreign types so macros can access them through the 4 | //! $crate path. However, the module is not considered part of public API for the sake of semver 5 | //! guarantees and its contents is not to be used directly in user code. 6 | 7 | pub use log::Level; 8 | -------------------------------------------------------------------------------- /src/terminate_guard.rs: -------------------------------------------------------------------------------- 1 | //! A termination RAII guard for test purposes. 2 | //! 3 | //! See the [testing guide] for details. 4 | //! 5 | //! testing guide: crate::guide::testing 6 | 7 | use std::sync::Arc; 8 | use std::thread::{self, JoinHandle}; 9 | 10 | use serde::de::DeserializeOwned; 11 | use structopt::StructOpt; 12 | 13 | use crate::{AnyError, Spirit}; 14 | 15 | /// The termination RAII guard for test purposes. 16 | /// 17 | /// See the [testing guide] for details of use. Created by 18 | /// [App::run_test][run_test]. 19 | /// 20 | /// Note that this will shut down (call `terminate`) when dropped and wait for the termination to 21 | /// happen. It'll then check that everything went successfully and panic if not. This is meant for 22 | /// tests, so it's desired behaviour. 23 | /// 24 | /// # Panics 25 | /// 26 | /// The **destructor** may panic if the contained spirit app fails during termination. See above. 27 | /// 28 | /// [testing guide]: crate::guide::testing 29 | /// [run_test]: crate::app::App::run_test 30 | pub struct TerminateGuard 31 | where 32 | C: DeserializeOwned + Send + Sync, 33 | O: StructOpt, 34 | { 35 | spirit: Arc>, 36 | bg: Option>>, 37 | } 38 | 39 | impl TerminateGuard 40 | where 41 | C: DeserializeOwned + Send + Sync, 42 | O: StructOpt, 43 | { 44 | pub(crate) fn new(spirit: Arc>, bg: JoinHandle>) -> Self { 45 | Self { 46 | spirit, 47 | bg: Some(bg), 48 | } 49 | } 50 | 51 | /// Access to the managed [Spirit] instance. 52 | pub fn spirit(&self) -> &Arc> { 53 | &self.spirit 54 | } 55 | } 56 | 57 | impl Drop for TerminateGuard 58 | where 59 | C: DeserializeOwned + Send + Sync, 60 | O: StructOpt, 61 | { 62 | fn drop(&mut self) { 63 | self.spirit.terminate(); 64 | let result = self 65 | .bg 66 | .take() 67 | .expect("Drop called multiple times?!? Missing the join handle") 68 | .join(); 69 | if !thread::panicking() { 70 | result 71 | .expect("Spirit test thread panicked") 72 | .expect("Test spirit terminated with an error"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Various utilities. 2 | //! 3 | //! All the little things that are useful through the spirit's or user's code, and don't really fit 4 | //! anywhere else. 5 | 6 | use std::env; 7 | use std::error::Error; 8 | use std::ffi::OsStr; 9 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 10 | use std::ops::{Deref, DerefMut}; 11 | use std::path::PathBuf; 12 | use std::str::FromStr; 13 | use std::sync::atomic::AtomicBool; 14 | use std::sync::Arc; 15 | use std::time::Duration; 16 | 17 | use err_context::prelude::*; 18 | use libc::c_int; 19 | use log::{debug, error, warn}; 20 | use serde::de::{Deserializer, Error as DeError, Unexpected}; 21 | use serde::ser::Serializer; 22 | use serde::{Deserialize, Serialize}; 23 | 24 | use crate::AnyError; 25 | 26 | /// Tries to read an absolute path from the given OS string. 27 | /// 28 | /// This converts the path to PathBuf. Then it tries to make it absolute and canonical, so changing 29 | /// current directory later on doesn't make it invalid. 30 | /// 31 | /// The function never fails. However, the substeps (finding current directory to make it absolute 32 | /// and canonization) might fail. In such case, the failing step is skipped. 33 | /// 34 | /// The motivation is parsing command line arguments using the 35 | /// [`structopt`](https://lib.rs/crates/structopt) crate. Users are used 36 | /// to passing relative paths to command line (as opposed to configuration files). However, if the 37 | /// daemon changes the current directory (for example during daemonization), the relative paths now 38 | /// point somewhere else. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ```rust 43 | /// use std::path::PathBuf; 44 | /// 45 | /// use structopt::StructOpt; 46 | /// 47 | /// # #[allow(dead_code)] 48 | /// #[derive(Debug, StructOpt)] 49 | /// struct MyOpts { 50 | /// #[structopt(short = "p", parse(from_os_str = spirit::utils::absolute_from_os_str))] 51 | /// path: PathBuf, 52 | /// } 53 | /// 54 | /// # fn main() { } 55 | /// ``` 56 | pub fn absolute_from_os_str(path: &OsStr) -> PathBuf { 57 | let mut current = env::current_dir().unwrap_or_else(|e| { 58 | warn!( 59 | "Some paths may not be turned to absolute. Couldn't read current dir: {}", 60 | e, 61 | ); 62 | PathBuf::new() 63 | }); 64 | current.push(path); 65 | if let Ok(canonicized) = current.canonicalize() { 66 | canonicized 67 | } else { 68 | current 69 | } 70 | } 71 | 72 | /// An error returned when the user passes a key-value option without the equal sign. 73 | /// 74 | /// Some internal options take a key-value pairs on the command line. If such option is expected, 75 | /// but it doesn't contain the equal sign, this is the used error. 76 | #[derive(Copy, Clone, Debug)] 77 | pub struct MissingEquals; 78 | 79 | impl Display for MissingEquals { 80 | fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 81 | write!(fmt, "Missing = in map option") 82 | } 83 | } 84 | 85 | impl Error for MissingEquals {} 86 | 87 | /// A helper for deserializing map-like command line arguments. 88 | /// 89 | /// # Examples 90 | /// 91 | /// ```rust 92 | /// # use structopt::StructOpt; 93 | /// # #[allow(dead_code)] 94 | /// #[derive(Debug, StructOpt)] 95 | /// struct MyOpts { 96 | /// #[structopt( 97 | /// short = "D", 98 | /// long = "define", 99 | /// parse(try_from_str = spirit::utils::key_val), 100 | /// number_of_values(1), 101 | /// )] 102 | /// defines: Vec<(String, String)>, 103 | /// } 104 | /// 105 | /// # fn main() {} 106 | /// ``` 107 | pub fn key_val(opt: &str) -> Result<(K, V), AnyError> 108 | where 109 | K: FromStr, 110 | K::Err: Error + Send + Sync + 'static, 111 | V: FromStr, 112 | V::Err: Error + Send + Sync + 'static, 113 | { 114 | let pos = opt.find('=').ok_or(MissingEquals)?; 115 | Ok((opt[..pos].parse()?, opt[pos + 1..].parse()?)) 116 | } 117 | 118 | /// A wrapper to hide a configuration field from logs. 119 | /// 120 | /// This acts in as much transparent way as possible towards the field inside. It only replaces the 121 | /// [`Debug`] and [`Serialize`] implementations with returning `"******"`. 122 | /// 123 | /// The idea is if the configuration contains passwords, they shouldn't leak into the logs. 124 | /// Therefore, wrap them in this, eg: 125 | /// 126 | /// ```rust 127 | /// use std::io::Write; 128 | /// use std::str; 129 | /// 130 | /// use spirit::utils::Hidden; 131 | /// 132 | /// # #[allow(dead_code)] 133 | /// #[derive(Debug)] 134 | /// struct Cfg { 135 | /// username: String, 136 | /// password: Hidden, 137 | /// } 138 | /// 139 | /// # fn main() -> Result<(), Box> { 140 | /// let cfg = Cfg { 141 | /// username: "me".to_owned(), 142 | /// password: "secret".to_owned().into(), 143 | /// }; 144 | /// 145 | /// let mut buffer: Vec = Vec::new(); 146 | /// write!(&mut buffer, "{:?}", cfg)?; 147 | /// assert_eq!(r#"Cfg { username: "me", password: "******" }"#, str::from_utf8(&buffer)?); 148 | /// # Ok(()) 149 | /// # } 150 | /// ``` 151 | #[derive(Clone, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash)] 152 | #[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))] 153 | #[repr(transparent)] 154 | #[serde(transparent)] 155 | pub struct Hidden(pub T); 156 | 157 | impl From for Hidden { 158 | fn from(val: T) -> Self { 159 | Hidden(val) 160 | } 161 | } 162 | 163 | impl Deref for Hidden { 164 | type Target = T; 165 | fn deref(&self) -> &T { 166 | &self.0 167 | } 168 | } 169 | 170 | impl DerefMut for Hidden { 171 | fn deref_mut(&mut self) -> &mut T { 172 | &mut self.0 173 | } 174 | } 175 | 176 | impl Debug for Hidden { 177 | fn fmt(&self, fmt: &mut Formatter) -> FmtResult { 178 | write!(fmt, "\"******\"") 179 | } 180 | } 181 | 182 | impl Serialize for Hidden { 183 | fn serialize(&self, s: S) -> Result { 184 | s.serialize_str("******") 185 | } 186 | } 187 | 188 | /// Serialize a duration. 189 | /// 190 | /// This can be used in configuration structures containing durations. See [`deserialize_duration`] 191 | /// for the counterpart. 192 | /// 193 | /// The default serialization produces human unreadable values, this is more suitable for dumping 194 | /// configuration users will read. 195 | /// 196 | /// # Examples 197 | /// 198 | /// ```rust 199 | /// use std::time::Duration; 200 | /// 201 | /// use serde::{Deserialize, Serialize}; 202 | /// 203 | /// #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] 204 | /// struct Cfg { 205 | /// #[serde( 206 | /// serialize_with = "spirit::utils::serialize_duration", 207 | /// deserialize_with = "spirit::utils::deserialize_duration", 208 | /// )] 209 | /// how_long: Duration, 210 | /// } 211 | /// ``` 212 | pub fn serialize_duration(dur: &Duration, s: S) -> Result { 213 | s.serialize_str(&humantime::format_duration(*dur).to_string()) 214 | } 215 | 216 | /// Deserialize a human-readable duration. 217 | /// 218 | /// # Examples 219 | /// 220 | /// ```rust 221 | /// use std::time::Duration; 222 | /// 223 | /// use serde::{Deserialize, Serialize}; 224 | /// 225 | /// #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] 226 | /// struct Cfg { 227 | /// #[serde( 228 | /// serialize_with = "spirit::utils::serialize_duration", 229 | /// deserialize_with = "spirit::utils::deserialize_duration", 230 | /// )] 231 | /// how_long: Duration, 232 | /// } 233 | /// ``` 234 | pub fn deserialize_duration<'de, D: Deserializer<'de>>(d: D) -> Result { 235 | let s = String::deserialize(d)?; 236 | 237 | humantime::parse_duration(&s) 238 | .map_err(|_| DeError::invalid_value(Unexpected::Str(&s), &"Human readable duration")) 239 | } 240 | 241 | /// Deserialize an `Option` using the [`humantime`](https://lib.rs/crates/humantime) crate. 242 | /// 243 | /// This allows reading human-friendly representations of time, like `30s` or `5days`. It should be 244 | /// paired with [`serialize_opt_duration`]. Also, to act like [`Option`] does when deserializing by 245 | /// default, the `#[serde(default)]` is recommended. 246 | /// 247 | /// # Examples 248 | /// 249 | /// ```rust 250 | /// use std::time::Duration; 251 | /// 252 | /// use serde::{Deserialize, Serialize}; 253 | /// 254 | /// #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] 255 | /// struct Cfg { 256 | /// #[serde( 257 | /// serialize_with = "spirit::utils::serialize_opt_duration", 258 | /// deserialize_with = "spirit::utils::deserialize_opt_duration", 259 | /// default, 260 | /// )] 261 | /// how_long: Option, 262 | /// } 263 | /// ``` 264 | pub fn deserialize_opt_duration<'de, D: Deserializer<'de>>( 265 | d: D, 266 | ) -> Result, D::Error> { 267 | if let Some(dur) = Option::::deserialize(d)? { 268 | humantime::parse_duration(&dur) 269 | .map_err(|_| DeError::invalid_value(Unexpected::Str(&dur), &"Human readable duration")) 270 | .map(Some) 271 | } else { 272 | Ok(None) 273 | } 274 | } 275 | 276 | /// Serialize an `Option` in a human friendly form. 277 | /// 278 | /// See the [`deserialize_opt_duration`] for more details and an example. 279 | pub fn serialize_opt_duration( 280 | dur: &Option, 281 | s: S, 282 | ) -> Result { 283 | match dur { 284 | Some(d) => serialize_duration(d, s), 285 | None => s.serialize_none(), 286 | } 287 | } 288 | 289 | #[deprecated(note = "Abstraction at the wrong place. Use support_emergency_shutdown instead.")] 290 | #[doc(hidden)] 291 | pub fn cleanup_signals() { 292 | debug!("Resetting termination signal handlers to defaults"); 293 | // Originally, this was done by removing all signals and resetting to defaults. We now install 294 | // default-handler emulation instead. That's a little bit problematic, if it's the signal 295 | // handlers that get stuck, but folks are recommended to use the support_emergency_shutdown 296 | // instead anyway. 297 | for sig in signal_hook::consts::TERM_SIGNALS { 298 | let registered = 299 | signal_hook::flag::register_conditional_default(*sig, Arc::new(AtomicBool::new(true))); 300 | if let Err(e) = registered { 301 | let name = signal_hook::low_level::signal_name(*sig).unwrap_or_default(); 302 | error!( 303 | "Failed to register forced shutdown signal {}/{}: {}", 304 | name, sig, e 305 | ); 306 | } 307 | } 308 | } 309 | 310 | /// Installs a stage-shutdown handling. 311 | /// 312 | /// If CTRL+C (or some other similar signal) is received for the first time, a graceful shutdown is 313 | /// initiated and a flag is set to true. If it is received for a second time, the application is 314 | /// terminated abruptly. 315 | /// 316 | /// The flag handle is returned to the caller, so the graceful shutdown and second stage kill can 317 | /// be aborted. 318 | /// 319 | /// Note that this API doesn't allow for removing the staged shutdown (due to the needed API 320 | /// clutter). If that is needed, you can use [`signal_hook`] directly. 321 | /// 322 | /// # Usage 323 | /// 324 | /// This is supposed to be called early in the program (usually as the first thing in `main`). This 325 | /// is for two reasons: 326 | /// 327 | /// * One usually wants this kind of emergency handling even during startup ‒ if something gets 328 | /// stuck during the initialization. 329 | /// * Installing signal handlers once there are multiple threads is inherently racy, therefore it 330 | /// is better to be done before any additional threads are started. 331 | /// 332 | /// # Examples 333 | /// 334 | /// ```rust 335 | /// use spirit::prelude::*; 336 | /// use spirit::{utils, Empty, Spirit}; 337 | /// 338 | /// fn main() { 339 | /// // Do this first, so double CTRL+C works from the very beginning. 340 | /// utils::support_emergency_shutdown().expect("This doesn't fail on healthy systems"); 341 | /// // Proceed to doing something useful. 342 | /// Spirit::::new() 343 | /// .run(|_spirit| { 344 | /// println!("Hello world"); 345 | /// Ok(()) 346 | /// }); 347 | /// } 348 | /// ``` 349 | /// 350 | /// # Errors 351 | /// 352 | /// This manipulates low-level signal handlers internally, so in theory this can fail. But this is 353 | /// not expected to fail in practice (not on a system that isn't severely broken anyway). As such, 354 | /// it is probably reasonable to unwrap here. 355 | pub fn support_emergency_shutdown() -> Result, AnyError> { 356 | let flag = Arc::new(AtomicBool::new(false)); 357 | 358 | let install = |sig: c_int| -> Result<(), AnyError> { 359 | signal_hook::flag::register_conditional_shutdown(sig, 2, Arc::clone(&flag))?; 360 | signal_hook::flag::register(sig, Arc::clone(&flag))?; 361 | Ok(()) 362 | }; 363 | 364 | for sig in signal_hook::consts::TERM_SIGNALS { 365 | let name = signal_hook::low_level::signal_name(*sig).unwrap_or_default(); 366 | debug!("Installing emergency shutdown support for {}/{}", name, sig); 367 | install(*sig).with_context(|_| { 368 | format!("Failed to install staged shutdown handler for {name}/{sig}") 369 | })? 370 | } 371 | 372 | Ok(flag) 373 | } 374 | 375 | /// Checks if value is default. 376 | /// 377 | /// Useful in `#[serde(skip_serializing_if = "is_default")]` 378 | pub fn is_default(v: &T) -> bool { 379 | v == &T::default() 380 | } 381 | 382 | /// Checks if value is set to true. 383 | /// 384 | /// Useful in `#[serde(skip_serializing_if = "is_true")]` 385 | pub fn is_true(v: &bool) -> bool { 386 | *v 387 | } 388 | 389 | pub(crate) struct FlushGuard; 390 | 391 | impl Drop for FlushGuard { 392 | fn drop(&mut self) { 393 | log::logger().flush(); 394 | } 395 | } 396 | 397 | #[cfg(test)] 398 | mod tests { 399 | use std::ffi::OsString; 400 | use std::net::{AddrParseError, IpAddr}; 401 | use std::num::ParseIntError; 402 | 403 | use super::*; 404 | 405 | #[test] 406 | fn abs() { 407 | let current = env::current_dir().unwrap(); 408 | let parent = absolute_from_os_str(&OsString::from("..")); 409 | assert!(parent.is_absolute()); 410 | assert!(current.starts_with(parent)); 411 | 412 | let child = absolute_from_os_str(&OsString::from("this-likely-doesn't-exist")); 413 | assert!(child.is_absolute()); 414 | assert!(child.starts_with(current)); 415 | } 416 | 417 | /// Valid inputs for the key-value parser 418 | #[test] 419 | fn key_val_success() { 420 | assert_eq!( 421 | ("hello".to_owned(), "world".to_owned()), 422 | key_val("hello=world").unwrap() 423 | ); 424 | let ip: IpAddr = "192.0.2.1".parse().unwrap(); 425 | assert_eq!(("ip".to_owned(), ip), key_val("ip=192.0.2.1").unwrap()); 426 | assert_eq!(("count".to_owned(), 4), key_val("count=4").unwrap()); 427 | } 428 | 429 | /// The extra equals sign go into the value part. 430 | #[test] 431 | fn key_val_extra_equals() { 432 | assert_eq!( 433 | ("greeting".to_owned(), "hello=world".to_owned()), 434 | key_val("greeting=hello=world").unwrap(), 435 | ); 436 | } 437 | 438 | /// Test when the key or value doesn't get parsed. 439 | #[test] 440 | fn key_val_parse_fail() { 441 | key_val::("hello=192.0.2.1.0") 442 | .unwrap_err() 443 | .downcast_ref::() 444 | .expect("Different error returned"); 445 | key_val::("hello=world") 446 | .unwrap_err() 447 | .downcast_ref::() 448 | .expect("Different error returned"); 449 | } 450 | 451 | #[test] 452 | fn key_val_missing_eq() { 453 | key_val::("no equal sign") 454 | .unwrap_err() 455 | .downcast_ref::() 456 | .expect("Different error returned"); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/validation.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for configuration validation. 2 | //! 3 | //! See [`config_validator`][crate::Extensible::config_validator]. 4 | 5 | /// A validation action. 6 | /// 7 | /// The validator (see [`config_validator`][crate::Extensible::config_validator]) is 8 | /// supposed to either return an error or an action to be taken once validation completes. 9 | /// 10 | /// By default, the [`Action`] is empty, but an [`on_success`][Action::on_success] and 11 | /// [`on_abort`][Action::on_abort] callbacks can be attached to it. These'll execute once the 12 | /// validation completes (only one of them will be called, depending on the result of validation). 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```rust 17 | /// use spirit::{Empty, Spirit}; 18 | /// use spirit::prelude::*; 19 | /// use spirit::validation::Action; 20 | /// # fn create_something(_cfg: T) -> Result { Ok(Empty {}) } 21 | /// # fn install_something(_empty: Empty) {} 22 | /// # let _ = 23 | /// Spirit::::new() 24 | /// .config_validator(|_old_cfg, new_cfg, _opts| { 25 | /// let something = create_something(new_cfg)?; 26 | /// Ok(Action::new().on_success(move || install_something(something))) 27 | /// }); 28 | /// ``` 29 | /// 30 | /// Or, if you want to only check the configuration: 31 | /// 32 | /// ```rust 33 | /// use spirit::{Empty, Spirit}; 34 | /// use spirit::prelude::*; 35 | /// use spirit::validation::Action; 36 | /// 37 | /// # fn looks_good(_cfg: T) -> Result<(), spirit::AnyError> { Ok(()) } 38 | /// # let _ = 39 | /// Spirit::::new() 40 | /// .config_validator(|_old_cfg, new_cfg, _opts| { 41 | /// looks_good(new_cfg)?; 42 | /// Ok(Action::new()) 43 | /// }); 44 | /// ``` 45 | #[derive(Default)] 46 | pub struct Action { 47 | pub(crate) on_abort: Option>, 48 | pub(crate) on_success: Option>, 49 | } 50 | 51 | impl Action { 52 | /// Creates actions wit both hooks empty. 53 | pub fn new() -> Self { 54 | Self::default() 55 | } 56 | 57 | /// Attaches (replaces) the success action. 58 | pub fn on_success(self, f: F) -> Self { 59 | let mut f = Some(f); 60 | let wrapper = move || (f.take().unwrap())(); 61 | Self { 62 | on_success: Some(Box::new(wrapper)), 63 | ..self 64 | } 65 | } 66 | 67 | /// Attaches (replaces) the failure action. 68 | pub fn on_abort(self, f: F) -> Self { 69 | let mut f = Some(f); 70 | let wrapper = move || (f.take().unwrap())(); 71 | Self { 72 | on_abort: Some(Box::new(wrapper)), 73 | ..self 74 | } 75 | } 76 | 77 | pub(crate) fn run(self, success: bool) { 78 | let selected = if success { 79 | self.on_success 80 | } else { 81 | self.on_abort 82 | }; 83 | if let Some(mut cback) = selected { 84 | cback(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/data/cfg1.yaml: -------------------------------------------------------------------------------- 1 | value: 12 2 | -------------------------------------------------------------------------------- /tests/data/cfg2.toml: -------------------------------------------------------------------------------- 1 | option = true 2 | --------------------------------------------------------------------------------