├── .github ├── FUNDING.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── windows.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── deny.toml ├── dependencies.json ├── docs ├── cli │ ├── log.md │ ├── rebuild.md │ ├── reload.md │ ├── reset.md │ ├── restart.md │ ├── start.md │ ├── state.md │ ├── status.md │ ├── stop.md │ └── units.md ├── example.md ├── index.md ├── installation.md └── tutorial.md ├── examples ├── json │ ├── desktop.json │ ├── kanata.json │ ├── komokana.json │ ├── komorebi-bar.json │ ├── komorebi.json │ ├── mousemaster.json │ └── whkd.json └── toml │ ├── desktop.toml │ ├── kanata.toml │ ├── komokana.toml │ ├── komorebi-bar.toml │ ├── komorebi.toml │ ├── mousemaster.toml │ └── whkd.toml ├── justfile ├── mkdocs.yml ├── rust-toolchain.toml ├── rustfmt.toml ├── schema.unit.json ├── shell.nix ├── wix ├── License.rtf └── main.wxs ├── wpm ├── Cargo.toml └── src │ ├── communication.rs │ ├── generators.rs │ ├── lib.rs │ ├── process_manager.rs │ ├── process_manager_status.rs │ ├── unit.rs │ └── unit_status.rs ├── wpmctl ├── Cargo.toml ├── build.rs └── src │ └── main.rs └── wpmd ├── Cargo.toml ├── build.rs └── src └── main.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: LGUG2Z 2 | ko_fi: lgug2z 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | assignees: 8 | - "LGUG2Z" 9 | commit-message: 10 | prefix: chore 11 | include: scope 12 | 13 | - package-ecosystem: "cargo" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | assignees: 18 | - "LGUG2Z" 19 | commit-message: 20 | prefix: chore 21 | include: scope 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/workflows/windows.yaml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*" 7 | push: 8 | branches: 9 | - master 10 | - feature/* 11 | - hotfix/* 12 | tags: 13 | - v* 14 | schedule: 15 | - cron: "30 0 * * 0" # Every day at 00:30 UTC 16 | workflow_dispatch: 17 | 18 | jobs: 19 | cargo-deny: 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - uses: EmbarkStudios/cargo-deny-action@v2 26 | 27 | build: 28 | strategy: 29 | fail-fast: true 30 | matrix: 31 | platform: 32 | - os-name: Windows-x86_64 33 | runs-on: windows-latest 34 | target: x86_64-pc-windows-msvc 35 | - os-name: Windows-aarch64 36 | runs-on: windows-latest 37 | target: aarch64-pc-windows-msvc 38 | runs-on: ${{ matrix.platform.runs-on }} 39 | permissions: write-all 40 | env: 41 | RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings 42 | GH_TOKEN: ${{ github.token }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | fetch-depth: 0 47 | - run: rustup toolchain install stable --profile minimal 48 | - run: rustup toolchain install nightly --allow-downgrade -c rustfmt 49 | - uses: Swatinem/rust-cache@v2 50 | with: 51 | cache-on-failure: "true" 52 | cache-all-crates: "true" 53 | key: ${{ matrix.platform.target }} 54 | - run: cargo +nightly fmt --check 55 | - run: cargo clippy 56 | - uses: houseabsolute/actions-rust-cross@v1 57 | with: 58 | command: "build" 59 | target: ${{ matrix.platform.target }} 60 | args: "--locked --release" 61 | - run: | 62 | cargo install cargo-wix 63 | cargo wix --no-build -p wpm --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }} 64 | - uses: actions/upload-artifact@v4 65 | with: 66 | name: wpm-${{ matrix.platform.target }}-${{ github.sha }} 67 | path: | 68 | target/${{ matrix.platform.target }}/release/*.exe 69 | target/${{ matrix.platform.target }}/release/*.pdb 70 | target/wix/wpm-*.msi 71 | retention-days: 14 72 | 73 | release-dry-run: 74 | needs: build 75 | runs-on: windows-latest 76 | permissions: write-all 77 | if: ${{ github.ref == 'refs/heads/master' }} 78 | steps: 79 | - uses: actions/checkout@v4 80 | with: 81 | fetch-depth: 0 82 | - shell: bash 83 | run: | 84 | TAG=${{ github.ref_name }} 85 | echo "VERSION=${TAG#v}" >> $GITHUB_ENV 86 | - uses: actions/download-artifact@v4 87 | - shell: bash 88 | run: ls -R 89 | - run: | 90 | Compress-Archive -Force ./wpm-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip 91 | Copy-Item ./wpm-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./wpm-$Env:VERSION-x86_64.msi 92 | echo "$((Get-FileHash wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt 93 | 94 | Compress-Archive -Force ./wpm-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip 95 | Copy-Item ./wpm-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./wpm-$Env:VERSION-aarch64.msi 96 | echo "$((Get-FileHash wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt 97 | - uses: Swatinem/rust-cache@v2 98 | with: 99 | cache-on-failure: "true" 100 | cache-all-crates: "true" 101 | - shell: bash 102 | run: | 103 | if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi 104 | git tag -d nightly || true 105 | kokai release --no-emoji --add-links github:commits,issues --ref "${{ github.ref_name }}" >"CHANGELOG.md" 106 | - uses: softprops/action-gh-release@v2 107 | with: 108 | body_path: "CHANGELOG.md" 109 | draft: true 110 | files: | 111 | checksums.txt 112 | *.zip 113 | *.msi 114 | 115 | release: 116 | needs: build 117 | runs-on: windows-latest 118 | permissions: write-all 119 | if: startsWith(github.ref, 'refs/tags/v') 120 | env: 121 | GH_TOKEN: ${{ github.token }} 122 | steps: 123 | - uses: actions/checkout@v4 124 | with: 125 | fetch-depth: 0 126 | - shell: bash 127 | run: | 128 | TAG=${{ github.ref_name }} 129 | echo "VERSION=${TAG#v}" >> $GITHUB_ENV 130 | - uses: actions/download-artifact@v4 131 | - run: | 132 | Compress-Archive -Force ./wpm-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip 133 | Copy-Item ./wpm-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./wpm-$Env:VERSION-x86_64.msi 134 | echo "$((Get-FileHash wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) wpm-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt 135 | 136 | Compress-Archive -Force ./wpm-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip 137 | Copy-Item ./wpm-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./wpm-$Env:VERSION-aarch64.msi 138 | echo "$((Get-FileHash wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) wpm-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt 139 | - uses: Swatinem/rust-cache@v2 140 | with: 141 | cache-on-failure: "true" 142 | cache-all-crates: "true" 143 | - shell: bash 144 | run: | 145 | if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi 146 | git tag -d nightly || true 147 | kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md" 148 | - uses: softprops/action-gh-release@v2 149 | with: 150 | body_path: "CHANGELOG.md" 151 | files: | 152 | checksums.txt 153 | *.zip 154 | *.msi 155 | 156 | # - if: startsWith(github.ref, 'refs/tags/v') 157 | # uses: vedantmgoyal2009/winget-releaser@main 158 | # with: 159 | # identifier: LGUG2Z.wpm 160 | # token: ${{ secrets.WINGET_TOKEN }} 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | *.log 4 | /units -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | resolver = "2" 4 | members = [ 5 | "wpm", 6 | "wpmctl", 7 | "wpmd" 8 | ] 9 | 10 | [workspace.dependencies] 11 | chrono = { version = "0.4", features = ["serde"] } 12 | clap = { version = "4", features = ["derive"] } 13 | color-eyre = "0.6" 14 | dirs = "6" 15 | interprocess = { version = "2" } 16 | parking_lot = "0.12" 17 | serde = { version = "1", features = ["derive"]} 18 | serde_json = "1" 19 | sysinfo = { git = "https://github.com/GuillaumeGomez/sysinfo", rev = "6ddf3fe1f6b1f60aaaabf15a3b5608ba6dc5278b"} 20 | thiserror = "2" 21 | tracing = "0.1" 22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 23 | shadow-rs = "1" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Komorebi License 2 | 3 | Version 2.0.0 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the software 14 | to do everything you might do with the software that would 15 | otherwise infringe the licensor's copyright in it for any 16 | permitted purpose. However, you may only distribute the source 17 | code of the software according to the [Distribution License]( 18 | #distribution-license), you may only make changes according 19 | to the [Changes License](#changes-license), and you may not 20 | otherwise distribute the software or new works based on the 21 | software. 22 | 23 | ## Distribution License 24 | 25 | The licensor grants you an additional copyright license to 26 | distribute copies of the source code of the software. Your 27 | license to distribute covers distributing the source code of 28 | the software with changes permitted by the [Changes License]( 29 | #changes-license). 30 | 31 | ## Changes License 32 | 33 | The licensor grants you an additional copyright license to 34 | make changes for any permitted purpose. 35 | 36 | ## Patent License 37 | 38 | The licensor grants you a patent license for the software that 39 | covers patent claims the licensor can license, or becomes able 40 | to license, that you would infringe by using the software. 41 | 42 | ## Personal Uses 43 | 44 | Personal use for research, experiment, and testing for 45 | the benefit of public knowledge, personal study, private 46 | entertainment, hobby projects, amateur pursuits, or religious 47 | observance, without any anticipated commercial application, 48 | is use for a permitted purpose. 49 | 50 | ## Fair Use 51 | 52 | You may have "fair use" rights for the software under the 53 | law. These terms do not limit them. 54 | 55 | ## No Other Rights 56 | 57 | These terms do not allow you to sublicense or transfer any of 58 | your licenses to anyone else, or prevent the licensor from 59 | granting licenses to anyone else. These terms do not imply 60 | any other licenses. 61 | 62 | ## Patent Defense 63 | 64 | If you make any written claim that the software infringes or 65 | contributes to infringement of any patent, your patent license 66 | for the software granted under these terms ends immediately. If 67 | your company makes such a claim, your patent license ends 68 | immediately for work on behalf of your company. 69 | 70 | ## Violations 71 | 72 | The first time you are notified in writing that you have 73 | violated any of these terms, or done anything with the software 74 | not covered by your licenses, your licenses can nonetheless 75 | continue if you come into full compliance with these terms, 76 | and take practical steps to correct past violations, within 77 | 32 days of receiving notice. Otherwise, all your licenses 78 | end immediately. 79 | 80 | ## No Liability 81 | 82 | ***As far as the law allows, the software comes as is, without 83 | any warranty or condition, and the licensor will not be liable 84 | to you for any damages arising out of these terms or the use 85 | or nature of the software, under any kind of legal claim.*** 86 | 87 | ## Definitions 88 | 89 | The **licensor** is the individual or entity offering these 90 | terms, and the **software** is the software the licensor makes 91 | available under these terms. 92 | 93 | **You** refers to the individual or entity agreeing to these 94 | terms. 95 | 96 | **Your company** is any legal entity, sole proprietorship, 97 | or other kind of organization that you work for, plus all 98 | organizations that have control over, are under the control of, 99 | or are under common control with that organization. **Control** 100 | means ownership of substantially all the assets of an entity, 101 | or the power to direct its management and policies by vote, 102 | contract, or otherwise. Control can be direct or indirect. 103 | 104 | **Your licenses** are all the licenses granted to you for the 105 | software under these terms. 106 | 107 | **Use** means anything you do with the software requiring one 108 | of your licenses. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wpm 2 | 3 | Simple user process management for Windows. 4 | 5 |

6 | 7 | Tech for Palestine 8 | 9 | GitHub Workflow Status 10 | GitHub all releases 11 | GitHub commits since latest release (by date) for a branch 12 | 13 | Discord 14 | 15 | 16 | GitHub Sponsors 17 | 18 | 19 | Ko-fi 20 | 21 | 22 | Notado Feed 23 | 24 | 25 | YouTube 26 | 27 |

28 | 29 | [![Watch the introduction video](https://img.youtube.com/vi/uY8OwE4XXHs/hqdefault.jpg)](https://www.youtube.com/watch?v=uY8OwE4XXHs) 30 | 31 | _wpm_ is a simple user process manager for Microsoft Windows 11 and above. 32 | 33 | _wpm_ allows you to start, stop and manage user level background processes as defined in unit files. 34 | 35 | _wpm_ allows you to codify availability and dependency relationships between different units. 36 | 37 | _wpm_ allows you to configure healthchecks for different units, with custom retry and back-off strategies. 38 | 39 | _wpm_ allows you to pin executables in your unit files to specific versions via remote package manifests. 40 | 41 | _wpm_ provides a comprehensive collection of lifecycle hooks which can be set for each unit. 42 | 43 | _wpm_ is not an open source project, but an educational source project which is free for personal use, and one that 44 | encourages you to make charitable donations if you find the software to be useful and have the financial means. 45 | 46 | I encourage you to make a charitable donation to 47 | the [Palestine Children's Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or contributing to 48 | a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring me on GitHub. 49 | 50 | [GitHub Sponsors is enabled for this project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have anything 51 | specific to offer besides my gratitude and shout outs at the end of _komorebi_ live development videos and tutorials. 52 | 53 | If you would like to tip or sponsor the project but are unable to use GitHub Sponsors, you may also sponsor 54 | through [Ko-fi](https://ko-fi.com/lgug2z). 55 | 56 | # Installation 57 | 58 | While this project is in a pre-release state, you can install `wpmd` and `wpmctl` using `cargo`: 59 | 60 | ```shell 61 | cargo install --git https://github.com/LGUG2Z/wpm wpmd 62 | cargo install --git https://github.com/LGUG2Z/wpm wpmctl 63 | ``` 64 | 65 | # Usage 66 | 67 | - Create unit files in `~/.config/wpm` - take a look at the [examples](./examples) 68 | - The full schema can be found [here](./schema.unit.json) and is likely to change during this early development phase 69 | - `$USERPROFILE` is a specially handled string in both `arguments` and `environment` which will be replaced with your home dir 70 | - Run `wpmd` to start the daemon, this will load all unit files in `~/.config/wpm` 71 | - Run `wpmctl start ` (or whatever your unit name is) to start the process 72 | - Run `wpmctl stop ` (or whatever your unit name is) to stop the process 73 | - Run `wpmctl reload` to reload all unit definitions (useful if you're making changes) 74 | - Run `wpmctl rebuild` to install manifests for remote executable sources 75 | 76 | ## Process Monitoring 77 | 78 | - Run `wpmctl log ` (or whatever your unit name is) to log the output of the process 79 | 80 | ``` 81 | ❯ wpmctl log whkd 82 | komorebic focus right 83 | komorebic focus left 84 | komorebic focus left 85 | komorebic focus right 86 | ``` 87 | 88 | - Run `wpmctl state` to inspect the state of the process manager 89 | 90 | ``` 91 | ❯ wpmctl state 92 | +--------------+---------+-----------+-------+--------------------------------------+ 93 | | name | kind | state | pid | timestamp | 94 | +--------------+---------+-----------+-------+--------------------------------------+ 95 | | komorebi | Simple | Running | 34304 | | 96 | +--------------+---------+-----------+-------+--------------------------------------+ 97 | | whkd | Simple | Running | 6460 | | 98 | +--------------+---------+-----------+-------+--------------------------------------+ 99 | | komorebi-bar | Simple | Running | 37400 | | 100 | +--------------+---------+-----------+-------+--------------------------------------+ 101 | | kanata | Simple | Running | 34204 | | 102 | +--------------+---------+-----------+-------+--------------------------------------+ 103 | | masir | Simple | Stopped | | | 104 | +--------------+---------+-----------+-------+--------------------------------------+ 105 | | desktop | Oneshot | Completed | | 2024-12-15 20:12:36.446380800 -08:00 | 106 | +--------------+---------+-----------+-------+--------------------------------------+ 107 | ``` 108 | 109 | - Run `wpmctl status ` to inspect the status of a unit 110 | 111 | ``` 112 | ❯ wpmctl status kanata 113 | ● Status of kanata: 114 | Kind: Simple 115 | State: Running since 2024-12-16 17:20:01.796661100 -08:00 116 | PID: 41704 117 | Log file: C:\Users\LGUG2Z\.config\wpm\logs\kanata.log 118 | Command: C:\Users\LGUG2Z\.cargo\bin\kanata.exe -c C:\Users\LGUG2Z\minimal.kbd --port 9999 119 | Healthcheck: Liveness check after 1s 120 | 121 | Recent logs: 122 | 15:46:38.0790 [INFO] Sleeping for 2s. Please release all keys and don't press additional ones. Run kanata with --help to see how understand more and how to disable this sleep. 123 | 15:46:40.0807 [INFO] entering the processing loop 124 | 15:46:40.0808 [INFO] listening for event notifications to relay to connected clients 125 | 15:46:40.0808 [INFO] Init: catching only releases and sending immediately 126 | 15:46:40.6899 [INFO] Starting kanata proper 127 | 15:46:40.6900 [INFO] You may forcefully exit kanata by pressing lctl+spc+esc at any time. These keys refer to defsrc input, meaning BEFORE kanata remaps keys. 128 | ``` 129 | 130 | # Contribution Guidelines 131 | 132 | If you would like to contribute to `wpm` please take the time to carefully read the guidelines below. 133 | 134 | ## Commit hygiene 135 | 136 | - Flatten all `use` statements 137 | - Run `cargo +stable clippy` and ensure that all lints and suggestions have been addressed before committing 138 | - Run `cargo +nightly fmt --all` to ensure consistent formatting before committing 139 | - Use `git cz` with 140 | the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare 141 | commit messages 142 | - Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the 143 | changes being committed 144 | 145 | ## License 146 | 147 | `wpm` is licensed under the [Komorebi 2.0.0 license](./LICENSE.md), which 148 | is a fork of the [PolyForm Strict 1.0.0 149 | license](https://polyformproject.org/licenses/strict/1.0.0). On a high level 150 | this means that you are free to do whatever you want with `wpm` for 151 | personal use other than redistribution, or distribution of new works (i.e. 152 | hard-forks) based on the software. 153 | 154 | Anyone is free to make their own fork of `wpm` with changes intended 155 | either for personal use or for integration back upstream via pull requests. 156 | 157 | _The [Komorebi 2.0.0 License](./LICENSE.md) does not permit any kind of 158 | commercial use._ 159 | 160 | ### Contribution licensing 161 | 162 | Contributions are accepted with the following understanding: 163 | 164 | - Contributed content is licensed under the terms of the 0-BSD license 165 | - Contributors accept the terms of the project license at the time of contribution 166 | 167 | By making a contribution, you accept both the current project license terms, and that all contributions that you have 168 | made are provided under the terms of the 0-BSD license. 169 | 170 | #### Zero-Clause BSD 171 | 172 | ``` 173 | Permission to use, copy, modify, and/or distribute this software for 174 | any purpose with or without fee is hereby granted. 175 | 176 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 177 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 178 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 179 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 180 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 181 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 182 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 183 | ``` 184 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | "x86_64-pc-windows-msvc", 4 | "i686-pc-windows-msvc", 5 | "aarch64-pc-windows-msvc", 6 | ] 7 | all-features = false 8 | no-default-features = false 9 | 10 | [output] 11 | feature-depth = 1 12 | 13 | [advisories] 14 | ignore = [] 15 | 16 | [licenses] 17 | allow = [ 18 | "0BSD", 19 | "Apache-2.0", 20 | "BSD-3-Clause", 21 | "BSL-1.0", 22 | "ISC", 23 | "MIT", 24 | "MIT-0", 25 | "MPL-2.0", 26 | "Unicode-3.0", 27 | "Zlib", 28 | "LicenseRef-Komorebi-1.0", 29 | ] 30 | confidence-threshold = 0.8 31 | 32 | [[licenses.clarify]] 33 | crate = "wpm" 34 | expression = "LicenseRef-Komorebi-1.0" 35 | license-files = [] 36 | 37 | [[licenses.clarify]] 38 | crate = "wpmd" 39 | expression = "LicenseRef-Komorebi-1.0" 40 | license-files = [] 41 | 42 | [[licenses.clarify]] 43 | crate = "wpmctl" 44 | expression = "LicenseRef-Komorebi-1.0" 45 | license-files = [] 46 | 47 | [bans] 48 | multiple-versions = "allow" 49 | wildcards = "allow" 50 | highlight = "all" 51 | workspace-default-features = "allow" 52 | external-default-features = "allow" 53 | 54 | [sources] 55 | unknown-registry = "deny" 56 | unknown-git = "deny" 57 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 58 | allow-git = ["https://github.com/GuillaumeGomez/sysinfo"] 59 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "licenses": [ 3 | [ 4 | "0BSD", 5 | [ 6 | "doctest-file 1.0.0 registry+https://github.com/rust-lang/crates.io-index", 7 | "recvmsg 1.0.0 registry+https://github.com/rust-lang/crates.io-index" 8 | ] 9 | ], 10 | [ 11 | "Apache-2.0", 12 | [ 13 | "ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index", 14 | "allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index", 15 | "anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index", 16 | "anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index", 17 | "anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index", 18 | "anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index", 19 | "anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index", 20 | "async-trait 0.1.87 registry+https://github.com/rust-lang/crates.io-index", 21 | "atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index", 22 | "autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index", 23 | "backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index", 24 | "base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index", 25 | "bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index", 26 | "block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index", 27 | "bytecount 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 28 | "cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index", 29 | "cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index", 30 | "chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index", 31 | "clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index", 32 | "clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index", 33 | "clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index", 34 | "clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index", 35 | "color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index", 36 | "color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index", 37 | "colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index", 38 | "cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index", 39 | "crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index", 40 | "crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index", 41 | "crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index", 42 | "crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index", 43 | "crypto-common 0.1.6 registry+https://github.com/rust-lang/crates.io-index", 44 | "ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index", 45 | "deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index", 46 | "digest 0.10.7 registry+https://github.com/rust-lang/crates.io-index", 47 | "dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index", 48 | "dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index", 49 | "displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index", 50 | "dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index", 51 | "either 1.14.0 registry+https://github.com/rust-lang/crates.io-index", 52 | "encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index", 53 | "equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index", 54 | "eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index", 55 | "fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index", 56 | "form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index", 57 | "futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 58 | "futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 59 | "futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 60 | "futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 61 | "futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 62 | "futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 63 | "git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index", 64 | "hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index", 65 | "hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index", 66 | "heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index", 67 | "hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index", 68 | "http 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 69 | "httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index", 70 | "hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index", 71 | "iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index", 72 | "idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index", 73 | "idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 74 | "indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index", 75 | "indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index", 76 | "interprocess 2.2.2 registry+https://github.com/rust-lang/crates.io-index", 77 | "ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index", 78 | "is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index", 79 | "is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index", 80 | "itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index", 81 | "jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index", 82 | "lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 83 | "libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index", 84 | "libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index", 85 | "libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index", 86 | "lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index", 87 | "log 0.4.26 registry+https://github.com/rust-lang/crates.io-index", 88 | "mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index", 89 | "native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index", 90 | "ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index", 91 | "num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 92 | "num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index", 93 | "once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index", 94 | "parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index", 95 | "parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index", 96 | "percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index", 97 | "pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index", 98 | "pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 99 | "pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index", 100 | "powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 101 | "proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index", 102 | "proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index", 103 | "proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index", 104 | "quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index", 105 | "rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index", 106 | "rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index", 107 | "regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index", 108 | "regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index", 109 | "regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index", 110 | "regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index", 111 | "reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index", 112 | "rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index", 113 | "rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index", 114 | "rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index", 115 | "ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index", 116 | "scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 117 | "serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index", 118 | "serde-envfile 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 119 | "serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index", 120 | "serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index", 121 | "serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index", 122 | "serde_spanned 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 123 | "serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index", 124 | "sha2 0.10.8 registry+https://github.com/rust-lang/crates.io-index", 125 | "sha256 1.6.0 registry+https://github.com/rust-lang/crates.io-index", 126 | "shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index", 127 | "shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index", 128 | "smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index", 129 | "socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index", 130 | "stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 131 | "syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index", 132 | "sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index", 133 | "thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index", 134 | "thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index", 135 | "thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index", 136 | "thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index", 137 | "thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index", 138 | "time 0.3.37 registry+https://github.com/rust-lang/crates.io-index", 139 | "time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index", 140 | "toml 0.8.20 registry+https://github.com/rust-lang/crates.io-index", 141 | "toml_datetime 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 142 | "toml_edit 0.22.24 registry+https://github.com/rust-lang/crates.io-index", 143 | "typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index", 144 | "tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index", 145 | "tzdb 0.7.2 registry+https://github.com/rust-lang/crates.io-index", 146 | "unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index", 147 | "unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 148 | "unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index", 149 | "url 2.5.4 registry+https://github.com/rust-lang/crates.io-index", 150 | "utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index", 151 | "utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index", 152 | "utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index", 153 | "vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index", 154 | "version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index", 155 | "widestring 1.1.0 registry+https://github.com/rust-lang/crates.io-index", 156 | "winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index", 157 | "windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 158 | "windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index", 159 | "windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 160 | "windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 161 | "windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 162 | "windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 163 | "windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 164 | "windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index", 165 | "windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 166 | "windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 167 | "windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index", 168 | "windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index", 169 | "windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 170 | "windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 171 | "windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 172 | "windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 173 | "write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index", 174 | "zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index" 175 | ] 176 | ], 177 | [ 178 | "BSD-2-Clause", 179 | ["zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index"] 180 | ], 181 | [ 182 | "BSD-3-Clause", 183 | [ 184 | "encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index" 185 | ] 186 | ], 187 | [ 188 | "BSL-1.0", 189 | ["ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index"] 190 | ], 191 | [ 192 | "EUPL-1.2", 193 | [ 194 | "serde-envfile 0.1.0 registry+https://github.com/rust-lang/crates.io-index" 195 | ] 196 | ], 197 | [ 198 | "ISC", 199 | [ 200 | "rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index" 201 | ] 202 | ], 203 | [ 204 | "MIT", 205 | [ 206 | "ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index", 207 | "allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index", 208 | "anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index", 209 | "anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index", 210 | "anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index", 211 | "anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index", 212 | "anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index", 213 | "async-trait 0.1.87 registry+https://github.com/rust-lang/crates.io-index", 214 | "atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index", 215 | "autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index", 216 | "backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index", 217 | "base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index", 218 | "bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index", 219 | "block-buffer 0.10.4 registry+https://github.com/rust-lang/crates.io-index", 220 | "bytecount 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 221 | "bytes 1.10.0 registry+https://github.com/rust-lang/crates.io-index", 222 | "cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index", 223 | "cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index", 224 | "chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index", 225 | "clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index", 226 | "clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index", 227 | "clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index", 228 | "clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index", 229 | "color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index", 230 | "color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index", 231 | "colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index", 232 | "cpufeatures 0.2.17 registry+https://github.com/rust-lang/crates.io-index", 233 | "crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index", 234 | "crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index", 235 | "crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index", 236 | "crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index", 237 | "crypto-common 0.1.6 registry+https://github.com/rust-lang/crates.io-index", 238 | "ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index", 239 | "deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index", 240 | "digest 0.10.7 registry+https://github.com/rust-lang/crates.io-index", 241 | "dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index", 242 | "dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index", 243 | "displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index", 244 | "dotenvy 0.15.7 registry+https://github.com/rust-lang/crates.io-index", 245 | "dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index", 246 | "either 1.14.0 registry+https://github.com/rust-lang/crates.io-index", 247 | "encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index", 248 | "envy 0.4.2 registry+https://github.com/rust-lang/crates.io-index", 249 | "equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index", 250 | "eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index", 251 | "fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index", 252 | "form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index", 253 | "fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index", 254 | "futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 255 | "futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 256 | "futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 257 | "futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 258 | "futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 259 | "futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index", 260 | "generic-array 0.14.7 registry+https://github.com/rust-lang/crates.io-index", 261 | "git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index", 262 | "h2 0.4.8 registry+https://github.com/rust-lang/crates.io-index", 263 | "hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index", 264 | "hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index", 265 | "heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index", 266 | "hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index", 267 | "http 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 268 | "http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index", 269 | "http-body-util 0.1.2 registry+https://github.com/rust-lang/crates.io-index", 270 | "httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index", 271 | "hyper 1.6.0 registry+https://github.com/rust-lang/crates.io-index", 272 | "hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index", 273 | "hyper-util 0.1.10 registry+https://github.com/rust-lang/crates.io-index", 274 | "iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index", 275 | "idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index", 276 | "idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 277 | "indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index", 278 | "indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index", 279 | "interprocess 2.2.2 registry+https://github.com/rust-lang/crates.io-index", 280 | "ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index", 281 | "is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index", 282 | "is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index", 283 | "itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index", 284 | "jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index", 285 | "lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 286 | "libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index", 287 | "libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index", 288 | "libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index", 289 | "lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index", 290 | "log 0.4.26 registry+https://github.com/rust-lang/crates.io-index", 291 | "matchers 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 292 | "memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index", 293 | "mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index", 294 | "mio 1.0.3 registry+https://github.com/rust-lang/crates.io-index", 295 | "native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index", 296 | "ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index", 297 | "nu-ansi-term 0.46.0 registry+https://github.com/rust-lang/crates.io-index", 298 | "num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 299 | "num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index", 300 | "once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index", 301 | "overload 0.1.1 registry+https://github.com/rust-lang/crates.io-index", 302 | "owo-colors 3.5.0 registry+https://github.com/rust-lang/crates.io-index", 303 | "papergrid 0.14.0 registry+https://github.com/rust-lang/crates.io-index", 304 | "parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index", 305 | "parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index", 306 | "percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index", 307 | "pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index", 308 | "pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 309 | "pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index", 310 | "powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 311 | "proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index", 312 | "proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index", 313 | "proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index", 314 | "quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index", 315 | "rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index", 316 | "rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index", 317 | "regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index", 318 | "regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index", 319 | "regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index", 320 | "regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index", 321 | "regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index", 322 | "reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index", 323 | "rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index", 324 | "rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index", 325 | "rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index", 326 | "schannel 0.1.27 registry+https://github.com/rust-lang/crates.io-index", 327 | "schemars 0.8.22 registry+https://github.com/rust-lang/crates.io-index", 328 | "schemars_derive 0.8.22 registry+https://github.com/rust-lang/crates.io-index", 329 | "scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 330 | "serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index", 331 | "serde-envfile 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 332 | "serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index", 333 | "serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index", 334 | "serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index", 335 | "serde_spanned 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 336 | "serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index", 337 | "sha2 0.10.8 registry+https://github.com/rust-lang/crates.io-index", 338 | "sha256 1.6.0 registry+https://github.com/rust-lang/crates.io-index", 339 | "shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index", 340 | "sharded-slab 0.1.7 registry+https://github.com/rust-lang/crates.io-index", 341 | "shared_child 1.0.1 registry+https://github.com/rust-lang/crates.io-index", 342 | "shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index", 343 | "slab 0.4.9 registry+https://github.com/rust-lang/crates.io-index", 344 | "smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index", 345 | "socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index", 346 | "stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index", 347 | "strsim 0.11.1 registry+https://github.com/rust-lang/crates.io-index", 348 | "syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index", 349 | "synstructure 0.13.1 registry+https://github.com/rust-lang/crates.io-index", 350 | "sysinfo 0.33.0 git+https://github.com/GuillaumeGomez/sysinfo?rev=6ddf3fe1f6b1f60aaaabf15a3b5608ba6dc5278b", 351 | "tabled 0.18.0 registry+https://github.com/rust-lang/crates.io-index", 352 | "tabled_derive 0.10.0 registry+https://github.com/rust-lang/crates.io-index", 353 | "thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index", 354 | "thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index", 355 | "thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index", 356 | "thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index", 357 | "thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index", 358 | "time 0.3.37 registry+https://github.com/rust-lang/crates.io-index", 359 | "time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index", 360 | "tokio 1.43.0 registry+https://github.com/rust-lang/crates.io-index", 361 | "tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index", 362 | "tokio-util 0.7.13 registry+https://github.com/rust-lang/crates.io-index", 363 | "toml 0.8.20 registry+https://github.com/rust-lang/crates.io-index", 364 | "toml_datetime 0.6.8 registry+https://github.com/rust-lang/crates.io-index", 365 | "toml_edit 0.22.24 registry+https://github.com/rust-lang/crates.io-index", 366 | "tower 0.5.2 registry+https://github.com/rust-lang/crates.io-index", 367 | "tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index", 368 | "tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index", 369 | "tracing 0.1.41 registry+https://github.com/rust-lang/crates.io-index", 370 | "tracing-appender 0.2.3 registry+https://github.com/rust-lang/crates.io-index", 371 | "tracing-attributes 0.1.28 registry+https://github.com/rust-lang/crates.io-index", 372 | "tracing-core 0.1.33 registry+https://github.com/rust-lang/crates.io-index", 373 | "tracing-error 0.2.1 registry+https://github.com/rust-lang/crates.io-index", 374 | "tracing-log 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 375 | "tracing-subscriber 0.3.19 registry+https://github.com/rust-lang/crates.io-index", 376 | "try-lock 0.2.5 registry+https://github.com/rust-lang/crates.io-index", 377 | "typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index", 378 | "tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index", 379 | "unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index", 380 | "unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 381 | "unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index", 382 | "url 2.5.4 registry+https://github.com/rust-lang/crates.io-index", 383 | "utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index", 384 | "utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index", 385 | "utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index", 386 | "vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index", 387 | "version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index", 388 | "want 0.3.1 registry+https://github.com/rust-lang/crates.io-index", 389 | "widestring 1.1.0 registry+https://github.com/rust-lang/crates.io-index", 390 | "winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index", 391 | "windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 392 | "windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index", 393 | "windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 394 | "windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 395 | "windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index", 396 | "windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 397 | "windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 398 | "windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index", 399 | "windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index", 400 | "windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index", 401 | "windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index", 402 | "windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index", 403 | "windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 404 | "windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 405 | "windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 406 | "windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index", 407 | "winnow 0.7.3 registry+https://github.com/rust-lang/crates.io-index", 408 | "write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index", 409 | "zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index" 410 | ] 411 | ], 412 | [ 413 | "MIT-0", 414 | ["tzdb_data 0.2.1 registry+https://github.com/rust-lang/crates.io-index"] 415 | ], 416 | [ 417 | "MPL-2.0", 418 | ["option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index"] 419 | ], 420 | [ 421 | "Unicode-3.0", 422 | [ 423 | "icu_collections 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 424 | "icu_locid 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 425 | "icu_locid_transform 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 426 | "icu_locid_transform_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 427 | "icu_normalizer 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 428 | "icu_normalizer_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 429 | "icu_properties 1.5.1 registry+https://github.com/rust-lang/crates.io-index", 430 | "icu_properties_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 431 | "icu_provider 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 432 | "icu_provider_macros 1.5.0 registry+https://github.com/rust-lang/crates.io-index", 433 | "litemap 0.7.5 registry+https://github.com/rust-lang/crates.io-index", 434 | "tinystr 0.7.6 registry+https://github.com/rust-lang/crates.io-index", 435 | "unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index", 436 | "writeable 0.5.5 registry+https://github.com/rust-lang/crates.io-index", 437 | "yoke 0.7.5 registry+https://github.com/rust-lang/crates.io-index", 438 | "yoke-derive 0.7.5 registry+https://github.com/rust-lang/crates.io-index", 439 | "zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index", 440 | "zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index", 441 | "zerovec 0.10.4 registry+https://github.com/rust-lang/crates.io-index", 442 | "zerovec-derive 0.10.3 registry+https://github.com/rust-lang/crates.io-index" 443 | ] 444 | ], 445 | [ 446 | "Unlicense", 447 | [ 448 | "memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index", 449 | "regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index" 450 | ] 451 | ], 452 | [ 453 | "Zlib", 454 | [ 455 | "const_format 0.2.34 registry+https://github.com/rust-lang/crates.io-index", 456 | "const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index" 457 | ] 458 | ] 459 | ] 460 | } 461 | -------------------------------------------------------------------------------- /docs/cli/log.md: -------------------------------------------------------------------------------- 1 | # log 2 | 3 | ``` 4 | Tail the logs of a unit or of the process manager 5 | 6 | Usage: wpmctl.exe log [UNIT] 7 | 8 | Arguments: 9 | [UNIT] 10 | Target unit 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/rebuild.md: -------------------------------------------------------------------------------- 1 | # rebuild 2 | 3 | ``` 4 | Ensure all remote dependencies are downloaded and built 5 | 6 | Usage: wpmctl.exe rebuild [PATH] 7 | 8 | Arguments: 9 | [PATH] 10 | Target path 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/reload.md: -------------------------------------------------------------------------------- 1 | # reload 2 | 3 | ``` 4 | Reload all unit definitions 5 | 6 | Usage: wpmctl.exe reload [PATH] 7 | 8 | Arguments: 9 | [PATH] 10 | Target path 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/reset.md: -------------------------------------------------------------------------------- 1 | # reset 2 | 3 | ``` 4 | Reset units 5 | 6 | Usage: wpmctl.exe reset [UNITS]... 7 | 8 | Arguments: 9 | [UNITS]... 10 | Target units 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/restart.md: -------------------------------------------------------------------------------- 1 | # restart 2 | 3 | ``` 4 | Restart units 5 | 6 | Usage: wpmctl.exe restart [OPTIONS] [UNITS]... 7 | 8 | Arguments: 9 | [UNITS]... 10 | Target units 11 | 12 | Options: 13 | -d, --with-dependents 14 | Restart dependents of target units 15 | 16 | -h, --help 17 | Print help 18 | 19 | ``` -------------------------------------------------------------------------------- /docs/cli/start.md: -------------------------------------------------------------------------------- 1 | # start 2 | 3 | ``` 4 | Start units 5 | 6 | Usage: wpmctl.exe start [UNITS]... 7 | 8 | Arguments: 9 | [UNITS]... 10 | Target units 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/state.md: -------------------------------------------------------------------------------- 1 | # state 2 | 3 | ``` 4 | Show the state of the process manager 5 | 6 | Usage: wpmctl.exe state 7 | 8 | Options: 9 | -h, --help 10 | Print help 11 | 12 | ``` -------------------------------------------------------------------------------- /docs/cli/status.md: -------------------------------------------------------------------------------- 1 | # status 2 | 3 | ``` 4 | Show status of a unit 5 | 6 | Usage: wpmctl.exe status 7 | 8 | Arguments: 9 | 10 | Target unit 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/stop.md: -------------------------------------------------------------------------------- 1 | # stop 2 | 3 | ``` 4 | Stop units 5 | 6 | Usage: wpmctl.exe stop [UNITS]... 7 | 8 | Arguments: 9 | [UNITS]... 10 | Target units 11 | 12 | Options: 13 | -h, --help 14 | Print help 15 | 16 | ``` -------------------------------------------------------------------------------- /docs/cli/units.md: -------------------------------------------------------------------------------- 1 | # units 2 | 3 | ``` 4 | Print the path to the wpm global unit definition directory 5 | 6 | Usage: wpmctl.exe units 7 | 8 | Options: 9 | -h, --help 10 | Print help 11 | 12 | ``` -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | # Units 2 | 3 | Unit files define everything that `wpm` needs to be able to manage a process. 4 | Below is a non-trivial unit file for 5 | [komorebi](https://github.com/LGUG2Z/komorebi) which has been annotated to help 6 | explain the various configuration options available. 7 | 8 | 1. [`Requires`](https://wpm.lgug2z.com/schema#Unit_Requires) is used to specify 9 | dependency relationships between different processes. In this example, 10 | `komorebi` depends on `whkd` and `kanata`, which means that if you attempt 11 | to `wpmctl start komorebi`, `wpm` will check if `whkd` and `kanata` are both 12 | healthy and running before it attempts to start `komorebi`. 13 | 1. [`Resources`](https://wpm.lgug2z.com/schema#Resources) is used to provide 14 | URLs to additional resources that the unit requires in order to run, such as 15 | configuration files. The key given to each URL here can be used to reference 16 | the cached location of the downloaded file on disk, for example, when 17 | passing a configuration file as an argument or an environment variable. 18 | 1. [`Kind`](https://wpm.lgug2z.com/schema#Service_Kind) is used to tell `wpm` 19 | if this process continues running when launched (`Simple`), runs and then 20 | exits (`OneShot`), or runs and exits after forking a new process 21 | (`Forking`). 22 | 1. Every unit has a complete set of lifecycle hooks available 23 | ([`ExecStartPre`](https://wpm.lgug2z.com/schema#Service_ExecStartPre), 24 | [`ExecStartPost`](https://wpm.lgug2z.com/schema#Service_ExecStartPost), 25 | [`ExecStop`](https://wpm.lgug2z.com/schema#Service_ExecStop), 26 | [`ExecStopPost`](https://wpm.lgug2z.com/schema#Service_ExecStopPost)) to 27 | specify any preflight or cleanup tasks a process might require. 28 | 1. [`Executable`](https://wpm.lgug2z.com/schema#Service_ExecStart_Executable) 29 | can reference either an binary in the system `$PATH`, a remote URL and 30 | checksum hash, or a [Scoop](https://scoop.sh) package manifest. Remote 31 | binaries will be cached in a local store for future use, as will Scoop 32 | packages. The latter two approaches can be used to pin binary dependencies 33 | to exact versions (ie. enforcing service dependency consistency across a 34 | team) 35 | 1. Keys declared in [`Resources`](https://wpm.lgug2z.com/schema#Resources) can 36 | be referenced as arguments using the `Resources.KEY` syntax inside of double 37 | curly braces. 38 | 1. `$USERPROFILE` will resolve to `C:\Users\` when used in 39 | `Arguments` and `Environment` 40 | 1. [`Healthcheck`](https://wpm.lgug2z.com/schema#Service_Healthcheck) is used 41 | to tell `wpm` how to validate the health of a process. This can be done by 42 | invoking a command until it returns with a successful exit code, or by 43 | checking the liveness of a process after a fixed period of time. 44 | 45 | {% raw %} 46 | 47 | ```json 48 | { 49 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 50 | "Unit": { 51 | "Name": "komorebi", 52 | "Description": "Tiling window management for Windows", 53 | // [1] 54 | "Requires": ["whkd", "kanata"] 55 | }, 56 | // [2] 57 | "Resources": { 58 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.example.json" 59 | }, 60 | "Service": { 61 | // [3] 62 | "Kind": "Simple", 63 | // [4] 64 | "ExecStartPre": [ 65 | { 66 | "Executable": "komorebic.exe", 67 | "Arguments": ["fetch-asc"] 68 | } 69 | ], 70 | "ExecStart": { 71 | // [5] 72 | "Executable": { 73 | "Package": "komorebi", 74 | "Version": "0.1.35", 75 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json", 76 | "Target": "komorebi.exe" 77 | }, 78 | "Arguments": [ 79 | "--config", 80 | // [6] 81 | "{{ Resources.CONFIGURATION_FILE }}" 82 | ], 83 | "Environment": [ 84 | [ 85 | "KOMOREBI_CONFIG_HOME", 86 | // [7] 87 | "$USERPROFILE/.config/komorebi" 88 | ] 89 | ] 90 | }, 91 | // [4] 92 | "ExecStop": [ 93 | { 94 | "Executable": "komorebic.exe", 95 | "Arguments": ["stop"] 96 | } 97 | ], 98 | // [4] 99 | "ExecStopPost": [ 100 | { 101 | "Executable": "komorebic.exe", 102 | "Arguments": ["restore-windows"] 103 | } 104 | ], 105 | // [8] 106 | "Healthcheck": { 107 | "Command": { 108 | "Executable": "komorebic.exe", 109 | "Arguments": ["state"], 110 | "DelaySec": 1 111 | } 112 | }, 113 | "Restart": "Never" 114 | } 115 | } 116 | ``` 117 | 118 | {% endraw %} 119 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # wpm 2 | 3 | ## Overview 4 | 5 | `wpm` is a simple user process manager for Microsoft Windows 11 and above. 6 | 7 | `wpm` allows you to start, stop and manage user-level background processes as 8 | defined in unit files. Unit files allow you to codify availability and 9 | dependency relationships between other processes, and allow you to configure 10 | process healthchecks with custom retry and back-off strategies. 11 | 12 | `wpm` allows you to pin executables in your unit files to specific versions via 13 | remote package manifests, and provides a comprehensive collection of lifecycle 14 | hooks to customize behaviour on process start and shutdown. 15 | 16 | ## Community 17 | 18 | There is a [Discord server](https://discord.gg/mGkn66PHkx) available for 19 | `wpm`-related discussion, help, troubleshooting etc. 20 | 21 | There is a [YouTube 22 | channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post 23 | `wpm` development videos, feature previews and release overviews. Subscribing 24 | to the channel (which is monetized as part of the YouTube Partner Program) and 25 | watching videos is a really simple and passive way to contribute financially to 26 | the development and maintenance of `wpm`. 27 | 28 | ## Licensing for Personal Use 29 | 30 | `wpm` is licensed under the [Komorebi 2.0.0 31 | license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the 32 | [PolyForm Strict 1.0.0 33 | license](https://polyformproject.org/licenses/strict/1.0.0). On a high level 34 | this means that you are free to do whatever you want with `wpm` for personal 35 | use other than redistribution, or distribution of new works (i.e. hard-forks) 36 | based on the software. 37 | 38 | Anyone is free to make their own fork of `wpm` with changes intended either for 39 | personal use or for integration back upstream via pull requests. 40 | 41 | The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does 42 | not permit any kind of commercial use ( i.e. using `wpm` at work). 43 | 44 | ## Sponsorship for Personal Use 45 | 46 | `wpm` is a free and educational source project, and one that encourages you 47 | to make charitable donations if you find the software to be useful and have the 48 | financial means. 49 | 50 | I encourage you to make a charitable donation to the [Palestine Children's 51 | Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute 52 | to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring 53 | me on GitHub. 54 | 55 | [GitHub Sponsors is enabled for this 56 | project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on 57 | the Discord server, get shout-outs at the end of _wpm_-related videos on 58 | YouTube, and gain the ability to submit feature requests on the issue tracker. 59 | 60 | If you would like to tip or sponsor the project but are unable to use GitHub 61 | Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or 62 | make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`. 63 | 64 | ## Licensing for Commercial Use 65 | 66 | A dedicated Individual Commercial Use License is available for those who want to 67 | use `wpm` at work. 68 | 69 | The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use” 70 | for the licensed individual only, for the duration of a valid paid license 71 | subscription only. All provisions and restrictions enumerated in the [Komorebi 72 | License](https://github.com/LGUG2Z/komorebi-license) continue to apply. 73 | 74 | More information, pricing and purchase links for Individual Commercial Use 75 | Licenses [can be found here](https://lgug2z.com/software/wpm). 76 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | `wpm` is a tiling window manager for Windows that is comprised of two main 4 | binaries, `wpmd.exe`, which contains the process management daemon itself, and 5 | `wpmctl.exe`, which is the main way to send commands to the process management 6 | daemon. 7 | 8 | ## Installation 9 | 10 | `wpm` is available pre-built to install via 11 | [Scoop](https://scoop.sh/#/apps?q=wpm) and 12 | [WinGet](https://winget.run/pkg/LGUG2Z/wpm), and you may also build 13 | it from [source](https://github.com/LGUG2Z/wpm) if you would prefer. 14 | 15 | - [Scoop](#scoop) 16 | - [WinGet](#winget) 17 | - [Building from source](#building-from-source) 18 | - [Offline](#offline) 19 | 20 | ## Long path support 21 | 22 | It is highly recommended that you enable support for long paths in Windows by 23 | running the following command in an Administrator Terminal before installing 24 | `wpm`. 25 | 26 | ```powershell 27 | Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 28 | ``` 29 | 30 | ## Scoop 31 | 32 | Make sure you have installed [`scoop`](https://scoop.sh) and verified that 33 | installed binaries are available in your `$PATH` before proceeding. 34 | 35 | Issues with `wpm` and related commands not being recognized in the 36 | terminal ultimately come down to the `$PATH` environment variable not being 37 | correctly configured by your package manager and **should not** be raised as 38 | bugs or issues either on the `wpm` GitHub repository or Discord server. 39 | 40 | ### Install wpm 41 | 42 | First add the extras bucket 43 | 44 | ```powershell 45 | scoop bucket add extras 46 | ``` 47 | 48 | Then install the `wpm` package using `scoop install` 49 | 50 | ```powershell 51 | scoop install wpm 52 | ``` 53 | 54 | ## WinGet 55 | 56 | Make sure you have installed the latest version of 57 | [`winget`](https://learn.microsoft.com/en-us/windows/package-manager/winget/) 58 | and verified that installed binaries are available in your `$PATH` before 59 | proceeding. 60 | 61 | Issues with `wpmd` and related commands not being recognized in the 62 | terminal ultimately come down to the `$PATH` environment variable not being 63 | correctly configured by your package manager and **should not** be raised as 64 | bugs or issues either on the `wpm` GitHub repository or Discord server. 65 | 66 | ### Install wpm 67 | 68 | Install the `wpm` packages using `winget install` 69 | 70 | ```powershell 71 | winget install LGUG2Z.wpm 72 | ``` 73 | 74 | ## Building from source 75 | 76 | Make sure you have installed [`rustup`](https://rustup.rs), a stable `rust` 77 | compiler toolchain, and the Visual Studio [Visual Studio 78 | prerequisites](https://rust-lang.github.io/rustup/installation/windows-msvc.html). 79 | 80 | Clone the git repository, enter the directory, and build the following binaries: 81 | 82 | ```powershell 83 | cargo +stable install --path wpm --locked 84 | cargo +stable install --path wpmd --locked 85 | ``` 86 | 87 | If the binaries have been built and added to your `$PATH` correctly, you should 88 | see some output when running `wpmd --help` and `wpmctl --help` 89 | 90 | ### Offline 91 | 92 | Download the latest [wpm](https://github.com/LGUG2Z/wpm/releases) 93 | MSI installer on an internet-connected computer, then copy it to 94 | an offline machine to install. 95 | 96 | ## Upgrades 97 | 98 | Before upgrading, make sure that `wpmd` is stopped. This is to ensure that all 99 | the current `wpm`-related exe files can be replaced without issue. 100 | 101 | Then, depending on whether you installed via `scoop` or `winget`, you can run 102 | the appropriate command: 103 | 104 | ```powershell 105 | # for winget 106 | winget upgrade LGUG2Z.wpm 107 | ``` 108 | 109 | ```powershell 110 | # for scoop 111 | scoop update wpm 112 | ``` 113 | 114 | ## Uninstallation 115 | 116 | Before uninstalling, first ensure that `wpmd` is stopped. 117 | 118 | Then, depending on whether you installed with Scoop or WinGet, run `scoop 119 | uninstall wpm` or `winget uninstall LGUG2Z.wpm`. 120 | 121 | Finally, you can run the following commands in a PowerShell prompt to clean up 122 | files created by the `quickstart` command and any other runtime files: 123 | 124 | ```powershell 125 | rm -r -Force $Env:USERPROFILE\.config\wpm 126 | rm -r -Force $Env:LOCALAPPDATA\wpm 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | For the tutorial, we will use `wpm` to set up a keyboard-focused desktop 4 | environment on a brand new virtual machine which uses `kanata` to enable 5 | QMK-style keyboard layers, `whkd` to enable programmable hotkeys, `komorebi` 6 | to enable tiling window management, and `komorebi-bar` as a status bar. 7 | 8 | One you have completed the tutorial, you should have a good idea of how `wpm` 9 | can be used to model and enforce constraints in use cases from customized 10 | desktops to complex local development environments and more. 11 | 12 | ## Create a new Virtual Machine 13 | 14 | * Open Hyper-V Manager 15 | * Select "Quick Create" 16 | * Select "Windows 11 dev environment" 17 | * Select "Create Virtual Machine" 18 | 19 | ## Install scoop 20 | 21 | ```powershell 22 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 23 | Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression 24 | ``` 25 | 26 | ## Install wpm 27 | 28 | Install `scoop` and then install `wpm` 29 | 30 | ```powershell 31 | scoop install git # need this to be able to add the extras bucket 32 | 33 | scoop bucket add extras 34 | scoop install wpm 35 | ``` 36 | 37 | ## Generate example units 38 | 39 | Generate some example unit files in `~/.config/wpm` 40 | 41 | ```powershell 42 | wpmctl examplegen $(wpm units) 43 | ``` 44 | 45 | You can `ls` the directory to make sure they have been generated 46 | 47 | ```powershell 48 | PS C:\Users\User> ls $(wpmctl units) 49 | 50 | 51 | Directory: C:\Users\User\.config\wpm 52 | 53 | 54 | Mode LastWriteTime Length Name 55 | ---- ------------- ------ ---- 56 | -a---- 4/6/2025 3:02 PM 503 desktop.json 57 | -a---- 4/6/2025 3:02 PM 883 kanata.json 58 | -a---- 4/6/2025 3:02 PM 1032 komokana.json 59 | -a---- 4/6/2025 3:02 PM 1026 komorebi-bar.json 60 | -a---- 4/6/2025 3:02 PM 1417 komorebi.json 61 | -a---- 4/6/2025 3:02 PM 971 mousemaster.json 62 | -a---- 4/6/2025 3:02 PM 835 whkd.json 63 | ``` 64 | 65 | ## Start `wpmd` 66 | 67 | Run `wpmd` in terminal to start the process manager - this will automatically 68 | download all required packages and configuration files before starting to 69 | listen for commands 70 | 71 | ```text 72 | PS C:\Users\User> wpmd 73 | 2025-04-06T22:07:42.124299Z INFO wpm::process_manager: desktop: registered unit 74 | 2025-04-06T22:07:42.126354Z INFO wpm::unit: kanata: adding resource C:\Users\User\AppData\Local\wpm\store\gist.githubusercontent.com_LGUG2Z_bbafc51ddde2bd1462151cfcc3f7f489_raw_28e24c4a493166fa866ae24ebc4ed8df7f164bd1\minimal.clj to store 75 | 2025-04-06T22:07:42.207182Z INFO wpm::unit: installing scoop manifest https://raw.githubusercontent.com/ScoopInstaller/Extras/8a6d8ff0f3963611ae61fd9f45ff36e3c321c8b5/bucket/kanata.json 76 | Installing 'kanata' (1.8.1) [64bit] from 'https://raw.githubusercontent.com/ScoopInstaller/Extras/8a6d8ff0f3963611ae61fd9f45ff36e3c321c8b5/bucket/kanata.json' 77 | Loading kanata.exe from cache 78 | Checking hash of kanata.exe ... ok. 79 | Linking ~\scoop\apps\kanata\current => ~\scoop\apps\kanata\1.8.1 80 | Creating shim for 'kanata'. 81 | 'kanata' (1.8.1) was installed successfully! 82 | Notes 83 | ----- 84 | Configuration Guide: https://github.com/jtroo/kanata/blob/main/docs/config.adoc 85 | 86 | 2025-04-06T22:07:44.513651Z INFO wpm::process_manager: kanata: registered unit 87 | 2025-04-06T22:07:44.515218Z INFO wpm::unit: komokana: adding resource C:\Users\User\AppData\Local\wpm\store\raw.githubusercontent.com_LGUG2Z_komokana_refs_tags_v0.1.5\komokana.example.yaml to store 88 | 2025-04-06T22:07:44.592705Z INFO wpm::unit: installing scoop manifest https://raw.githubusercontent.com/ScoopInstaller/Extras/e633292b4e1101273caac59ffcb4a7ce7ee7a2e8/bucket/komokana.json 89 | Installing 'komokana' (0.1.5) [64bit] from 'https://raw.githubusercontent.com/ScoopInstaller/Extras/e633292b4e1101273caac59ffcb4a7ce7ee7a2e8/bucket/komokana.json' 90 | Loading komokana-0.1.5-x86_64-pc-windows-msvc.zip from cache 91 | Checking hash of komokana-0.1.5-x86_64-pc-windows-msvc.zip ... ok. 92 | Extracting komokana-0.1.5-x86_64-pc-windows-msvc.zip ... done. 93 | Linking ~\scoop\apps\komokana\current => ~\scoop\apps\komokana\0.1.5 94 | Creating shim for 'komokana'. 95 | 'komokana' (0.1.5) was installed successfully! 96 | 'komokana' suggests installing 'extras/komorebi'. 97 | 98 | 2025-04-06T22:07:46.792651Z INFO wpm::process_manager: komokana: registered unit 99 | 2025-04-06T22:07:46.793421Z INFO wpm::unit: komorebi-bar: adding resource C:\Users\User\AppData\Local\wpm\store\raw.githubusercontent.com_LGUG2Z_komorebi_refs_tags_v0.1.35_docs\komorebi.bar.example.json to store 100 | 2025-04-06T22:07:46.820347Z INFO wpm::unit: installing scoop manifest https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json 101 | Installing 'komorebi' (0.1.35) [64bit] from 'https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json' 102 | Loading komorebi-0.1.35-x86_64-pc-windows-msvc.zip from cache 103 | Checking hash of komorebi-0.1.35-x86_64-pc-windows-msvc.zip ... ok. 104 | Extracting komorebi-0.1.35-x86_64-pc-windows-msvc.zip ... done. 105 | Linking ~\scoop\apps\komorebi\current => ~\scoop\apps\komorebi\0.1.35 106 | Creating shim for 'komorebi'. 107 | Creating shim for 'komorebic'. 108 | Creating shim for 'komorebic-no-console'. 109 | Making C:\Users\User\scoop\shims\komorebic-no-console.exe a GUI binary. 110 | Creating shim for 'komorebi-gui'. 111 | Creating shim for 'komorebi-bar'. 112 | 'komorebi' (0.1.35) was installed successfully! 113 | Notes 114 | ----- 115 | Check out the quickstart guide on https://lgug2z.github.io/komorebi 116 | 'komorebi' suggests installing 'extras/autohotkey'. 117 | 'komorebi' suggests installing 'extras/whkd'. 118 | 119 | 2025-04-06T22:07:49.420615Z INFO wpm::process_manager: komorebi-bar: registered unit 120 | 2025-04-06T22:07:49.421177Z INFO wpm::unit: komorebi: adding resource C:\Users\User\AppData\Local\wpm\store\raw.githubusercontent.com_LGUG2Z_komorebi_refs_tags_v0.1.35_docs\komorebi.example.json to store 121 | 2025-04-06T22:07:49.442994Z INFO wpm::process_manager: komorebi: registered unit 122 | 2025-04-06T22:07:49.443741Z INFO wpm::unit: whkd: adding resource C:\Users\User\AppData\Local\wpm\store\raw.githubusercontent.com_LGUG2Z_komorebi_refs_tags_v0.1.35_docs\whkdrc.sample to store 123 | 2025-04-06T22:07:49.470394Z INFO wpm::unit: installing scoop manifest https://raw.githubusercontent.com/ScoopInstaller/Extras/112fd691392878f8c4e9e9703dde3d1d182941e3/bucket/whkd.json 124 | Installing 'whkd' (0.2.7) [64bit] from 'https://raw.githubusercontent.com/ScoopInstaller/Extras/112fd691392878f8c4e9e9703dde3d1d182941e3/bucket/whkd.json' 125 | Loading whkd-0.2.7-x86_64-pc-windows-msvc.zip from cache 126 | Checking hash of whkd-0.2.7-x86_64-pc-windows-msvc.zip ... ok. 127 | Extracting whkd-0.2.7-x86_64-pc-windows-msvc.zip ... done. 128 | Linking ~\scoop\apps\whkd\current => ~\scoop\apps\whkd\0.2.7 129 | Creating shim for 'whkd'. 130 | 'whkd' (0.2.7) was installed successfully! 131 | 132 | 2025-04-06T22:07:51.580342Z INFO wpm::process_manager: whkd: registered unit 133 | 2025-04-06T22:07:51.580712Z INFO wpmd: listening on wpmd.sock 134 | ``` 135 | 136 | ## Start the units 137 | 138 | The dependency graph of our example units looks like this 139 | 140 | * `komorebi-bar` depends on `komorebi` 141 | * `komorebi` depends on `whkd` and `kanata` 142 | 143 | So we can run `wpmctl start komorebi-bar` to ensure that `whkd`, `kanata`, 144 | `komorebi` and `komorebi-bar` are all started and passing their healthchecks. 145 | 146 | 147 | ```text 148 | 2025-04-06T22:12:26.419780Z INFO wpmd: received socket message: Start(["komorebi-bar"]) 149 | 2025-04-06T22:12:26.420163Z INFO wpmd: successfully queued socket message 150 | 2025-04-06T22:12:26.420204Z INFO wpm::process_manager: komorebi-bar: requires komorebi 151 | 2025-04-06T22:12:26.420689Z INFO wpm::process_manager: komorebi: requires whkd 152 | 2025-04-06T22:12:26.421003Z INFO wpm::unit: whkd: starting unit 153 | 2025-04-06T22:12:26.424052Z INFO wpm::unit: whkd: running pid 11716 liveness healthcheck (1s) 154 | 2025-04-06T22:12:27.441812Z INFO wpm::unit: whkd: passed healthcheck 155 | 2025-04-06T22:12:27.442303Z INFO wpm::process_manager: komorebi: requires kanata 156 | 2025-04-06T22:12:27.442572Z INFO wpm::unit: kanata: starting unit 157 | 2025-04-06T22:12:27.446427Z INFO wpm::unit: kanata: running pid 9520 liveness healthcheck (1s) 158 | 2025-04-06T22:12:28.466568Z INFO wpm::unit: kanata: passed healthcheck 159 | 2025-04-06T22:12:28.466976Z INFO wpm::unit: komorebi: starting unit 160 | 2025-04-06T22:12:28.471147Z INFO wpm::unit: komorebi: running command healthcheck - C:\Users\User\scoop\shims\komorebic.exe state (1s) 161 | 2025-04-06T22:12:29.503620Z INFO wpm::unit: komorebi: passed healthcheck 162 | 2025-04-06T22:12:29.503983Z INFO wpm::unit: komorebi-bar: starting unit 163 | 2025-04-06T22:12:29.507663Z INFO wpm::unit: komorebi-bar: running pid 9860 liveness healthcheck (1s) 164 | 2025-04-06T22:12:30.529935Z INFO wpm::unit: komorebi-bar: passed healthcheck 165 | ``` 166 | 167 | ## Shutdown 168 | 169 | You can press `ctrl-c` on the terminal window running `wpmd` to trigger a shutdown, 170 | which will ensure that all processes started in the previous steps are shutdown 171 | cleanly with their shutdown hooks respected. 172 | 173 | ```text 174 | 2025-04-06T22:20:02.303289Z INFO wpm::process_manager: wpmd: shutting down process manager 175 | 2025-04-06T22:20:02.303496Z INFO wpm::process_manager: whkd: stopping unit 176 | 2025-04-06T22:20:02.303622Z INFO wpm::process_manager: whkd: sending kill signal to 70000 177 | 2025-04-06T22:20:02.306467Z INFO wpm::process_manager: whkd: process 70000 successfully terminated 178 | 2025-04-06T22:20:02.306587Z INFO wpm::process_manager: komorebi-bar: stopping unit 179 | 2025-04-06T22:20:02.306714Z INFO wpm::process_manager: komorebi-bar: sending kill signal to 40192 180 | 2025-04-06T22:20:02.352968Z INFO wpm::process_manager: komorebi-bar: process 40192 successfully terminated 181 | 2025-04-06T22:20:02.356650Z INFO wpm::process_manager: komorebi: stopping unit 182 | 2025-04-06T22:20:02.356777Z INFO wpm::process_manager: komorebi: executing shutdown command - C:\Users\User\scoop\shims\komorebic.exe stop 183 | 2025-04-06T22:20:02.453174Z INFO wpm::unit: komorebi: executing cleanup command - C:\Users\User\scoop\shims\komorebic.exe restore-windows 184 | 2025-04-06T22:20:02.792875Z INFO wpm::process_manager: komorebi: sending kill signal to 17448 185 | 2025-04-06T22:20:02.793018Z INFO wpm::process_manager: komorebi: process 17448 successfully terminated 186 | 2025-04-06T22:20:02.793137Z INFO wpm::process_manager: komorebi: executing cleanup command - C:\Users\User\scoop\shims\komorebic.exe restore-windows 187 | 2025-04-06T22:20:02.811672Z INFO wpm::process_manager: kanata: stopping unit 188 | 2025-04-06T22:20:02.811799Z INFO wpm::process_manager: kanata: sending kill signal to 70136 189 | 2025-04-06T22:20:02.814617Z INFO wpm::process_manager: kanata: process 70136 successfully terminated 190 | ``` 191 | -------------------------------------------------------------------------------- /examples/json/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "desktop", 5 | "Description": "Everything I need to work on Windows", 6 | "Requires": [ 7 | "komorebi", 8 | "komorebi-bar", 9 | "mousemaster" 10 | ] 11 | }, 12 | "Resources": null, 13 | "Service": { 14 | "Kind": "Oneshot", 15 | "ExecStart": { 16 | "Executable": "msg.exe", 17 | "Arguments": [ 18 | "*", 19 | "Desktop recipe completed!" 20 | ] 21 | }, 22 | "Restart": "Never" 23 | } 24 | } -------------------------------------------------------------------------------- /examples/json/kanata.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "kanata", 5 | "Description": "Software keyboard remapper" 6 | }, 7 | "Resources": { 8 | "CONFIGURATION_FILE": "https://gist.githubusercontent.com/LGUG2Z/bbafc51ddde2bd1462151cfcc3f7f489/raw/28e24c4a493166fa866ae24ebc4ed8df7f164bd1/minimal.clj" 9 | }, 10 | "Service": { 11 | "Kind": "Simple", 12 | "ExecStart": { 13 | "Executable": { 14 | "Package": "kanata", 15 | "Version": "1.8.1", 16 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/8a6d8ff0f3963611ae61fd9f45ff36e3c321c8b5/bucket/kanata.json" 17 | }, 18 | "Arguments": [ 19 | "-c", 20 | "{{ Resources.CONFIGURATION_FILE }}", 21 | "--port", 22 | "9999" 23 | ] 24 | }, 25 | "Healthcheck": { 26 | "Process": { 27 | "DelaySec": 1 28 | } 29 | }, 30 | "Restart": "Never" 31 | } 32 | } -------------------------------------------------------------------------------- /examples/json/komokana.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "komokana", 5 | "Description": "Automatic application-aware keyboard layer switching for Windows", 6 | "Requires": [ 7 | "komorebi", 8 | "kanata" 9 | ] 10 | }, 11 | "Resources": { 12 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/LGUG2Z/komokana/refs/tags/v0.1.5/komokana.example.yaml" 13 | }, 14 | "Service": { 15 | "Kind": "Simple", 16 | "ExecStart": { 17 | "Executable": { 18 | "Package": "komokana", 19 | "Version": "0.1.5", 20 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/e633292b4e1101273caac59ffcb4a7ce7ee7a2e8/bucket/komokana.json" 21 | }, 22 | "Arguments": [ 23 | "--kanata-port", 24 | "9999", 25 | "--configuration", 26 | "{{ Resources.CONFIGURATION_FILE }}", 27 | "--default-layer", 28 | "qwerty" 29 | ] 30 | }, 31 | "Healthcheck": { 32 | "Process": { 33 | "DelaySec": 1 34 | } 35 | }, 36 | "Restart": "OnFailure", 37 | "RestartSec": 2 38 | } 39 | } -------------------------------------------------------------------------------- /examples/json/komorebi-bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "komorebi-bar", 5 | "Description": "Status bar for komorebi", 6 | "Requires": [ 7 | "komorebi" 8 | ] 9 | }, 10 | "Resources": { 11 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.bar.example.json" 12 | }, 13 | "Service": { 14 | "Kind": "Simple", 15 | "ExecStart": { 16 | "Executable": { 17 | "Package": "komorebi", 18 | "Version": "0.1.35", 19 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json", 20 | "Target": "komorebi-bar.exe" 21 | }, 22 | "Arguments": [ 23 | "--config", 24 | "{{ Resources.CONFIGURATION_FILE }}" 25 | ] 26 | }, 27 | "Environment": [ 28 | [ 29 | "KOMOREBI_CONFIG_HOME", 30 | "$USERPROFILE/.config/komorebi" 31 | ] 32 | ], 33 | "Healthcheck": { 34 | "Process": { 35 | "DelaySec": 1 36 | } 37 | }, 38 | "Restart": "Never" 39 | } 40 | } -------------------------------------------------------------------------------- /examples/json/komorebi.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "komorebi", 5 | "Description": "Tiling window management for Windows", 6 | "Requires": [ 7 | "whkd", 8 | "kanata" 9 | ] 10 | }, 11 | "Resources": { 12 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.example.json" 13 | }, 14 | "Service": { 15 | "Kind": "Simple", 16 | "ExecStartPre": [ 17 | { 18 | "Executable": "komorebic.exe", 19 | "Arguments": [ 20 | "fetch-asc" 21 | ] 22 | } 23 | ], 24 | "ExecStart": { 25 | "Executable": { 26 | "Package": "komorebi", 27 | "Version": "0.1.35", 28 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json", 29 | "Target": "komorebi.exe" 30 | }, 31 | "Arguments": [ 32 | "--config", 33 | "{{ Resources.CONFIGURATION_FILE }}" 34 | ], 35 | "Environment": [ 36 | [ 37 | "KOMOREBI_CONFIG_HOME", 38 | "$USERPROFILE/.config/komorebi" 39 | ] 40 | ] 41 | }, 42 | "ExecStop": [ 43 | { 44 | "Executable": "komorebic.exe", 45 | "Arguments": [ 46 | "stop" 47 | ] 48 | } 49 | ], 50 | "ExecStopPost": [ 51 | { 52 | "Executable": "komorebic.exe", 53 | "Arguments": [ 54 | "restore-windows" 55 | ] 56 | } 57 | ], 58 | "Healthcheck": { 59 | "Command": { 60 | "Executable": "komorebic.exe", 61 | "Arguments": [ 62 | "state" 63 | ], 64 | "DelaySec": 1 65 | } 66 | }, 67 | "Restart": "Never" 68 | } 69 | } -------------------------------------------------------------------------------- /examples/json/mousemaster.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "mousemaster", 5 | "Description": "A keyboard driven interface for mouseless mouse manipulation", 6 | "Requires": [ 7 | "whkd", 8 | "kanata" 9 | ] 10 | }, 11 | "Resources": { 12 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/petoncle/mousemaster/refs/tags/73/configuration/neo-mousekeys-ijkl.properties" 13 | }, 14 | "Service": { 15 | "Kind": "Simple", 16 | "ExecStart": { 17 | "Executable": { 18 | "Url": "https://github.com/petoncle/mousemaster/releases/download/73/mousemaster.exe", 19 | "Hash": "7b696461e128aec9cc50d187d8656123a6e7a4e6b1d9ec1dbe504ad2de3cad25" 20 | }, 21 | "Arguments": [ 22 | "--configuration-file={{ Resources.CONFIGURATION_FILE }}", 23 | "--pause-on-error=false" 24 | ] 25 | }, 26 | "Healthcheck": { 27 | "Process": { 28 | "DelaySec": 2 29 | } 30 | }, 31 | "Restart": "OnFailure", 32 | "RestartSec": 2 33 | } 34 | } -------------------------------------------------------------------------------- /examples/json/whkd.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json", 3 | "Unit": { 4 | "Name": "whkd", 5 | "Description": "Simple hotkey daemon for Windows" 6 | }, 7 | "Resources": { 8 | "CONFIGURATION_FILE": "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/whkdrc.sample" 9 | }, 10 | "Service": { 11 | "Kind": "Simple", 12 | "ExecStart": { 13 | "Executable": { 14 | "Package": "whkd", 15 | "Version": "0.2.7", 16 | "Manifest": "https://raw.githubusercontent.com/ScoopInstaller/Extras/112fd691392878f8c4e9e9703dde3d1d182941e3/bucket/whkd.json" 17 | }, 18 | "Arguments": [ 19 | "--config", 20 | "{{ Resources.CONFIGURATION_FILE }}" 21 | ] 22 | }, 23 | "Healthcheck": { 24 | "Process": { 25 | "DelaySec": 1 26 | } 27 | }, 28 | "Restart": "OnFailure", 29 | "RestartSec": 2 30 | } 31 | } -------------------------------------------------------------------------------- /examples/toml/desktop.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "desktop" 3 | Description = "Everything I need to work on Windows" 4 | Requires = [ 5 | "komorebi", 6 | "komorebi-bar", 7 | "mousemaster", 8 | ] 9 | 10 | [Service] 11 | Kind = "Oneshot" 12 | Restart = "Never" 13 | 14 | [Service.ExecStart] 15 | Executable = "msg.exe" 16 | Arguments = [ 17 | "*", 18 | "Desktop recipe completed!", 19 | ] 20 | -------------------------------------------------------------------------------- /examples/toml/kanata.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "kanata" 3 | Description = "Software keyboard remapper" 4 | 5 | [Resources] 6 | CONFIGURATION_FILE = "https://gist.githubusercontent.com/LGUG2Z/bbafc51ddde2bd1462151cfcc3f7f489/raw/28e24c4a493166fa866ae24ebc4ed8df7f164bd1/minimal.clj" 7 | 8 | [Service] 9 | Kind = "Simple" 10 | Restart = "Never" 11 | 12 | [Service.ExecStart] 13 | Arguments = [ 14 | "-c", 15 | "{{ Resources.CONFIGURATION_FILE }}", 16 | "--port", 17 | "9999", 18 | ] 19 | 20 | [Service.ExecStart.Executable] 21 | Package = "kanata" 22 | Version = "1.8.1" 23 | Manifest = "https://raw.githubusercontent.com/ScoopInstaller/Extras/8a6d8ff0f3963611ae61fd9f45ff36e3c321c8b5/bucket/kanata.json" 24 | 25 | [Service.Healthcheck.Process] 26 | DelaySec = 1 27 | -------------------------------------------------------------------------------- /examples/toml/komokana.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "komokana" 3 | Description = "Automatic application-aware keyboard layer switching for Windows" 4 | Requires = [ 5 | "komorebi", 6 | "kanata", 7 | ] 8 | 9 | [Resources] 10 | CONFIGURATION_FILE = "https://raw.githubusercontent.com/LGUG2Z/komokana/refs/tags/v0.1.5/komokana.example.yaml" 11 | 12 | [Service] 13 | Kind = "Simple" 14 | Restart = "OnFailure" 15 | RestartSec = 2 16 | 17 | [Service.ExecStart] 18 | Arguments = [ 19 | "--kanata-port", 20 | "9999", 21 | "--configuration", 22 | "{{ Resources.CONFIGURATION_FILE }}", 23 | "--default-layer", 24 | "qwerty", 25 | ] 26 | 27 | [Service.ExecStart.Executable] 28 | Package = "komokana" 29 | Version = "0.1.5" 30 | Manifest = "https://raw.githubusercontent.com/ScoopInstaller/Extras/e633292b4e1101273caac59ffcb4a7ce7ee7a2e8/bucket/komokana.json" 31 | 32 | [Service.Healthcheck.Process] 33 | DelaySec = 1 34 | -------------------------------------------------------------------------------- /examples/toml/komorebi-bar.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "komorebi-bar" 3 | Description = "Status bar for komorebi" 4 | Requires = ["komorebi"] 5 | 6 | [Resources] 7 | CONFIGURATION_FILE = "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.bar.example.json" 8 | 9 | [Service] 10 | Kind = "Simple" 11 | Environment = [[ 12 | "KOMOREBI_CONFIG_HOME", 13 | "$USERPROFILE/.config/komorebi", 14 | ]] 15 | Restart = "Never" 16 | 17 | [Service.ExecStart] 18 | Arguments = [ 19 | "--config", 20 | "{{ Resources.CONFIGURATION_FILE }}", 21 | ] 22 | 23 | [Service.ExecStart.Executable] 24 | Package = "komorebi" 25 | Version = "0.1.35" 26 | Manifest = "https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json" 27 | Target = "komorebi-bar.exe" 28 | 29 | [Service.Healthcheck.Process] 30 | DelaySec = 1 31 | -------------------------------------------------------------------------------- /examples/toml/komorebi.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "komorebi" 3 | Description = "Tiling window management for Windows" 4 | Requires = [ 5 | "whkd", 6 | "kanata", 7 | ] 8 | 9 | [Resources] 10 | CONFIGURATION_FILE = "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.example.json" 11 | 12 | [Service] 13 | Kind = "Simple" 14 | Restart = "Never" 15 | 16 | [[Service.ExecStartPre]] 17 | Executable = "komorebic.exe" 18 | Arguments = ["fetch-asc"] 19 | 20 | [Service.ExecStart] 21 | Arguments = [ 22 | "--config", 23 | "{{ Resources.CONFIGURATION_FILE }}", 24 | ] 25 | Environment = [[ 26 | "KOMOREBI_CONFIG_HOME", 27 | "$USERPROFILE/.config/komorebi", 28 | ]] 29 | 30 | [Service.ExecStart.Executable] 31 | Package = "komorebi" 32 | Version = "0.1.35" 33 | Manifest = "https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json" 34 | Target = "komorebi.exe" 35 | 36 | [[Service.ExecStop]] 37 | Executable = "komorebic.exe" 38 | Arguments = ["stop"] 39 | 40 | [[Service.ExecStopPost]] 41 | Executable = "komorebic.exe" 42 | Arguments = ["restore-windows"] 43 | 44 | [Service.Healthcheck.Command] 45 | Executable = "komorebic.exe" 46 | Arguments = ["state"] 47 | DelaySec = 1 48 | -------------------------------------------------------------------------------- /examples/toml/mousemaster.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "mousemaster" 3 | Description = "A keyboard driven interface for mouseless mouse manipulation" 4 | Requires = [ 5 | "whkd", 6 | "kanata", 7 | ] 8 | 9 | [Resources] 10 | CONFIGURATION_FILE = "https://raw.githubusercontent.com/petoncle/mousemaster/refs/tags/73/configuration/neo-mousekeys-ijkl.properties" 11 | 12 | [Service] 13 | Kind = "Simple" 14 | Restart = "OnFailure" 15 | RestartSec = 2 16 | 17 | [Service.ExecStart] 18 | Arguments = [ 19 | "--configuration-file={{ Resources.CONFIGURATION_FILE }}", 20 | "--pause-on-error=false", 21 | ] 22 | 23 | [Service.ExecStart.Executable] 24 | Url = "https://github.com/petoncle/mousemaster/releases/download/73/mousemaster.exe" 25 | Hash = "7b696461e128aec9cc50d187d8656123a6e7a4e6b1d9ec1dbe504ad2de3cad25" 26 | 27 | [Service.Healthcheck.Process] 28 | DelaySec = 2 29 | -------------------------------------------------------------------------------- /examples/toml/whkd.toml: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Name = "whkd" 3 | Description = "Simple hotkey daemon for Windows" 4 | 5 | [Resources] 6 | CONFIGURATION_FILE = "https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/whkdrc.sample" 7 | 8 | [Service] 9 | Kind = "Simple" 10 | Restart = "OnFailure" 11 | RestartSec = 2 12 | 13 | [Service.ExecStart] 14 | Arguments = [ 15 | "--config", 16 | "{{ Resources.CONFIGURATION_FILE }}", 17 | ] 18 | 19 | [Service.ExecStart.Executable] 20 | Package = "whkd" 21 | Version = "0.2.7" 22 | Manifest = "https://raw.githubusercontent.com/ScoopInstaller/Extras/112fd691392878f8c4e9e9703dde3d1d182941e3/bucket/whkd.json" 23 | 24 | [Service.Healthcheck.Process] 25 | DelaySec = 1 26 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] 2 | 3 | export RUST_BACKTRACE := "full" 4 | 5 | clean: 6 | cargo clean 7 | 8 | fmt: 9 | cargo +nightly fmt 10 | cargo +stable clippy 11 | prettier -w README.md 12 | prettier -w .github 13 | 14 | install-targets *targets: 15 | "{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ } 16 | 17 | install-target target: 18 | cargo +stable install --path {{ target }} --locked 19 | 20 | install: 21 | just install-targets wpmd wpmctl 22 | 23 | run target: 24 | cargo +stable run --bin {{ target }} --locked 25 | 26 | warn target $RUST_LOG="warn": 27 | just run {{ target }} 28 | 29 | info target $RUST_LOG="info": 30 | just run {{ target }} 31 | 32 | debug target $RUST_LOG="debug": 33 | just run {{ target }} 34 | 35 | trace target $RUST_LOG="trace": 36 | just run {{ target }} 37 | 38 | jsonschema: 39 | cargo +stable run --bin wpmctl --locked -- schemagen >schema.unit.json 40 | 41 | # this part is run in a nix shell because python is a nightmare 42 | schemagen: 43 | rm -rf schema-docs 44 | mkdir -p schema-docs 45 | generate-schema-doc ./schema.unit.json --config template_name=js_offline --config minify=false ./schema-docs/ 46 | mv ./schema-docs/schema.unit.html ./schema-docs/schema.html 47 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | copyright: Copyright © 2024-Present LGUG2Z 2 | use_directory_urls: false 3 | site_name: wpm 4 | site_description: Windows Process Manager 5 | repo_url: https://github.com/LGUG2Z/wpm 6 | repo_name: LGUG2Z/wpm 7 | docs_dir: docs 8 | theme: 9 | name: material 10 | palette: 11 | - media: "(prefers-color-scheme: light)" 12 | scheme: default 13 | toggle: 14 | icon: material/weather-sunny 15 | name: Switch to dark mode 16 | 17 | # Palette toggle for dark mode 18 | - media: "(prefers-color-scheme: dark)" 19 | scheme: slate 20 | toggle: 21 | icon: material/weather-night 22 | name: Switch to light mode 23 | features: 24 | - content.action.edit 25 | - content.action.view 26 | - content.code.copy 27 | - content.tabs.link 28 | - navigation.footer 29 | - navigation.indexes 30 | - navigation.sections 31 | # - navigation.tabs 32 | - navigation.top 33 | - navigation.tracking 34 | - search.highlight 35 | - search.share 36 | - search.suggest 37 | - toc.follow 38 | markdown_extensions: 39 | - admonition 40 | - pymdownx.highlight 41 | - pymdownx.superfences 42 | plugins: 43 | - macros 44 | - search 45 | 46 | nav: 47 | - About: 48 | - wpm: index.md 49 | - Getting started: 50 | - Installation: installation.md 51 | - Tutorial: tutorial.md 52 | - Units: 53 | - Example: example.md 54 | - Schema: https://wpm.lgug2z.com/schema 55 | - CLI reference: 56 | - cli/start.md 57 | - cli/stop.md 58 | - cli/restart.md 59 | - cli/reset.md 60 | - cli/state.md 61 | - cli/status.md 62 | - cli/reload.md 63 | - cli/log.md 64 | - cli/rebuild.md 65 | - cli/units.md 66 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Item" -------------------------------------------------------------------------------- /schema.unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Definition", 4 | "description": "A wpm definition", 5 | "type": "object", 6 | "required": [ 7 | "Service", 8 | "Unit" 9 | ], 10 | "properties": { 11 | "Resources": { 12 | "description": "Remote resources used by this definition", 13 | "type": [ 14 | "object", 15 | "null" 16 | ], 17 | "additionalProperties": { 18 | "type": "string", 19 | "format": "uri" 20 | } 21 | }, 22 | "Schema": { 23 | "description": "JSON Schema definition for auto completions", 24 | "type": [ 25 | "string", 26 | "null" 27 | ] 28 | }, 29 | "Service": { 30 | "description": "Information about what this definition executes", 31 | "allOf": [ 32 | { 33 | "$ref": "#/definitions/Service" 34 | } 35 | ] 36 | }, 37 | "Unit": { 38 | "description": "Information about this definition and its dependencies", 39 | "allOf": [ 40 | { 41 | "$ref": "#/definitions/Unit" 42 | } 43 | ] 44 | } 45 | }, 46 | "definitions": { 47 | "CommandHealthcheck": { 48 | "description": "A service liveness healthcheck based on the successful exit code of a command", 49 | "type": "object", 50 | "required": [ 51 | "DelaySec", 52 | "Executable" 53 | ], 54 | "properties": { 55 | "Arguments": { 56 | "description": "Arguments passed to the executable", 57 | "type": [ 58 | "array", 59 | "null" 60 | ], 61 | "items": { 62 | "type": "string" 63 | } 64 | }, 65 | "DelaySec": { 66 | "description": "The number of seconds to delay before checking for liveness", 67 | "type": "integer", 68 | "format": "uint64", 69 | "minimum": 0.0 70 | }, 71 | "Environment": { 72 | "description": "Environment variables for this command", 73 | "type": [ 74 | "array", 75 | "null" 76 | ], 77 | "items": { 78 | "type": "array", 79 | "items": [ 80 | { 81 | "type": "string" 82 | }, 83 | { 84 | "type": "string" 85 | } 86 | ], 87 | "maxItems": 2, 88 | "minItems": 2 89 | } 90 | }, 91 | "Executable": { 92 | "description": "Executable name or absolute path to an executable", 93 | "type": "string" 94 | }, 95 | "RetryLimit": { 96 | "description": "The maximum number of retries (default: 5)", 97 | "type": [ 98 | "integer", 99 | "null" 100 | ], 101 | "format": "uint8", 102 | "minimum": 0.0 103 | } 104 | } 105 | }, 106 | "Executable": { 107 | "anyOf": [ 108 | { 109 | "description": "A remote executable file verified using a SHA256 hash", 110 | "allOf": [ 111 | { 112 | "$ref": "#/definitions/RemoteExecutable" 113 | } 114 | ] 115 | }, 116 | { 117 | "description": "A local executable file", 118 | "type": "string" 119 | }, 120 | { 121 | "description": "An executable file with a Scoop package dependency", 122 | "allOf": [ 123 | { 124 | "$ref": "#/definitions/ScoopExecutable" 125 | } 126 | ] 127 | } 128 | ] 129 | }, 130 | "Healthcheck": { 131 | "oneOf": [ 132 | { 133 | "type": "object", 134 | "required": [ 135 | "Command" 136 | ], 137 | "properties": { 138 | "Command": { 139 | "$ref": "#/definitions/CommandHealthcheck" 140 | } 141 | }, 142 | "additionalProperties": false 143 | }, 144 | { 145 | "type": "object", 146 | "required": [ 147 | "Process" 148 | ], 149 | "properties": { 150 | "Process": { 151 | "$ref": "#/definitions/ProcessHealthcheck" 152 | } 153 | }, 154 | "additionalProperties": false 155 | } 156 | ] 157 | }, 158 | "ProcessHealthcheck": { 159 | "description": "A process liveness healthcheck either based on an automatic PID or an optional binary", 160 | "type": "object", 161 | "required": [ 162 | "DelaySec" 163 | ], 164 | "properties": { 165 | "DelaySec": { 166 | "description": "The number of seconds to delay before checking for liveness", 167 | "type": "integer", 168 | "format": "uint64", 169 | "minimum": 0.0 170 | }, 171 | "Target": { 172 | "description": "An optional binary with which to check process liveness", 173 | "type": [ 174 | "string", 175 | "null" 176 | ] 177 | } 178 | } 179 | }, 180 | "RemoteExecutable": { 181 | "type": "object", 182 | "required": [ 183 | "Hash", 184 | "Url" 185 | ], 186 | "properties": { 187 | "Hash": { 188 | "description": "Sha256 hash of the remote executable at", 189 | "type": "string" 190 | }, 191 | "Url": { 192 | "description": "Url to a remote executable", 193 | "type": "string", 194 | "format": "uri" 195 | } 196 | } 197 | }, 198 | "RestartStrategy": { 199 | "description": "Information about a wpm definition's restart strategy", 200 | "type": "string", 201 | "enum": [ 202 | "Never", 203 | "Always", 204 | "OnFailure" 205 | ] 206 | }, 207 | "ScoopBucket": { 208 | "type": "string", 209 | "enum": [ 210 | "Main", 211 | "Extras" 212 | ] 213 | }, 214 | "ScoopExecutable": { 215 | "anyOf": [ 216 | { 217 | "$ref": "#/definitions/ScoopPackage" 218 | }, 219 | { 220 | "description": "A Scoop package identified using a raw manifest", 221 | "allOf": [ 222 | { 223 | "$ref": "#/definitions/ScoopManifest" 224 | } 225 | ] 226 | } 227 | ] 228 | }, 229 | "ScoopManifest": { 230 | "type": "object", 231 | "required": [ 232 | "Manifest", 233 | "Package", 234 | "Version" 235 | ], 236 | "properties": { 237 | "Manifest": { 238 | "description": "Url to a Scoop manifest", 239 | "type": "string", 240 | "format": "uri" 241 | }, 242 | "Package": { 243 | "description": "Name of the package", 244 | "type": "string" 245 | }, 246 | "Target": { 247 | "description": "Target executable in the package", 248 | "type": [ 249 | "string", 250 | "null" 251 | ] 252 | }, 253 | "Version": { 254 | "description": "Version of the package", 255 | "type": "string" 256 | } 257 | } 258 | }, 259 | "ScoopPackage": { 260 | "type": "object", 261 | "required": [ 262 | "Bucket", 263 | "Package", 264 | "Version" 265 | ], 266 | "properties": { 267 | "Bucket": { 268 | "description": "Bucket that the package is found in", 269 | "allOf": [ 270 | { 271 | "$ref": "#/definitions/ScoopBucket" 272 | } 273 | ] 274 | }, 275 | "Package": { 276 | "description": "Name of the package", 277 | "type": "string" 278 | }, 279 | "Target": { 280 | "description": "Target executable in the package", 281 | "type": [ 282 | "string", 283 | "null" 284 | ] 285 | }, 286 | "Version": { 287 | "description": "Version of the package", 288 | "type": "string" 289 | } 290 | } 291 | }, 292 | "Service": { 293 | "description": "Information about what a wpm definition executes", 294 | "type": "object", 295 | "required": [ 296 | "ExecStart" 297 | ], 298 | "properties": { 299 | "Autostart": { 300 | "description": "Autostart this definition with wpmd", 301 | "type": "boolean" 302 | }, 303 | "Environment": { 304 | "description": "Environment variables inherited by all commands in this service definition", 305 | "type": [ 306 | "array", 307 | "null" 308 | ], 309 | "items": { 310 | "type": "array", 311 | "items": [ 312 | { 313 | "type": "string" 314 | }, 315 | { 316 | "type": "string" 317 | } 318 | ], 319 | "maxItems": 2, 320 | "minItems": 2 321 | } 322 | }, 323 | "EnvironmentFile": { 324 | "description": "Path to an environment file, containing environment variables inherited by all commands in this service definition", 325 | "type": [ 326 | "string", 327 | "null" 328 | ] 329 | }, 330 | "ExecStart": { 331 | "description": "Command executed by this service definition", 332 | "allOf": [ 333 | { 334 | "$ref": "#/definitions/ServiceCommand" 335 | } 336 | ] 337 | }, 338 | "ExecStartPost": { 339 | "description": "Commands executed after ExecStart in this service definition", 340 | "type": [ 341 | "array", 342 | "null" 343 | ], 344 | "items": { 345 | "$ref": "#/definitions/ServiceCommand" 346 | } 347 | }, 348 | "ExecStartPre": { 349 | "description": "Commands executed before ExecStart in this service definition", 350 | "type": [ 351 | "array", 352 | "null" 353 | ], 354 | "items": { 355 | "$ref": "#/definitions/ServiceCommand" 356 | } 357 | }, 358 | "ExecStop": { 359 | "description": "Shutdown commands for this service definition", 360 | "type": [ 361 | "array", 362 | "null" 363 | ], 364 | "items": { 365 | "$ref": "#/definitions/ServiceCommand" 366 | } 367 | }, 368 | "ExecStopPost": { 369 | "description": "Post-shutdown cleanup commands for this service definition", 370 | "type": [ 371 | "array", 372 | "null" 373 | ], 374 | "items": { 375 | "$ref": "#/definitions/ServiceCommand" 376 | } 377 | }, 378 | "Healthcheck": { 379 | "description": "Healthcheck for this service definition", 380 | "anyOf": [ 381 | { 382 | "$ref": "#/definitions/Healthcheck" 383 | }, 384 | { 385 | "type": "null" 386 | } 387 | ] 388 | }, 389 | "Kind": { 390 | "description": "Type of service definition", 391 | "default": "Simple", 392 | "allOf": [ 393 | { 394 | "$ref": "#/definitions/ServiceKind" 395 | } 396 | ] 397 | }, 398 | "Restart": { 399 | "description": "Restart strategy for this service definition", 400 | "default": "Never", 401 | "allOf": [ 402 | { 403 | "$ref": "#/definitions/RestartStrategy" 404 | } 405 | ] 406 | }, 407 | "RestartSec": { 408 | "description": "Time to sleep in seconds before attempting to restart service (default: 1s)", 409 | "type": [ 410 | "integer", 411 | "null" 412 | ], 413 | "format": "uint64", 414 | "minimum": 0.0 415 | }, 416 | "WorkingDirectory": { 417 | "description": "Working directory for this service definition", 418 | "type": [ 419 | "string", 420 | "null" 421 | ] 422 | } 423 | } 424 | }, 425 | "ServiceCommand": { 426 | "description": "A wpm definition command", 427 | "type": "object", 428 | "required": [ 429 | "Executable" 430 | ], 431 | "properties": { 432 | "Arguments": { 433 | "description": "Arguments passed to the executable", 434 | "type": [ 435 | "array", 436 | "null" 437 | ], 438 | "items": { 439 | "type": "string" 440 | } 441 | }, 442 | "Environment": { 443 | "description": "Environment variables for this command", 444 | "type": [ 445 | "array", 446 | "null" 447 | ], 448 | "items": { 449 | "type": "array", 450 | "items": [ 451 | { 452 | "type": "string" 453 | }, 454 | { 455 | "type": "string" 456 | } 457 | ], 458 | "maxItems": 2, 459 | "minItems": 2 460 | } 461 | }, 462 | "EnvironmentFile": { 463 | "description": "Path to an environment file, containing environment variables for this command", 464 | "type": [ 465 | "string", 466 | "null" 467 | ] 468 | }, 469 | "Executable": { 470 | "description": "Executable (local file, remote file, or Scoop package)", 471 | "allOf": [ 472 | { 473 | "$ref": "#/definitions/Executable" 474 | } 475 | ] 476 | }, 477 | "RetryLimit": { 478 | "description": "The maximum number of retries for ExecStart (default: 5)", 479 | "type": [ 480 | "integer", 481 | "null" 482 | ], 483 | "format": "uint8", 484 | "minimum": 0.0 485 | } 486 | } 487 | }, 488 | "ServiceKind": { 489 | "type": "string", 490 | "enum": [ 491 | "Simple", 492 | "Oneshot", 493 | "Forking" 494 | ] 495 | }, 496 | "Unit": { 497 | "description": "Information about a wpm definition and its dependencies", 498 | "type": "object", 499 | "required": [ 500 | "Name" 501 | ], 502 | "properties": { 503 | "Description": { 504 | "description": "Description of this definition", 505 | "type": [ 506 | "string", 507 | "null" 508 | ] 509 | }, 510 | "Name": { 511 | "description": "Name of this definition, must be unique", 512 | "type": "string" 513 | }, 514 | "Requires": { 515 | "description": "Dependencies of this definition, validated at runtime", 516 | "type": [ 517 | "array", 518 | "null" 519 | ], 520 | "items": { 521 | "type": "string" 522 | } 523 | } 524 | } 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import {}}: 2 | with pkgs; 3 | mkShell { 4 | name = "wpm"; 5 | 6 | buildInputs = [ 7 | python311Packages.mkdocs-material 8 | python311Packages.mkdocs-macros 9 | python311Packages.setuptools 10 | python311Packages.json-schema-for-humans 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /wix/License.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033\deflangfe1033{\fonttbl{\f0\fswiss\fprq2\fcharset0 Arial;}} 2 | {\*\generator Riched20 10.0.22621}{\*\mmathPr\mdispDef1\mwrapIndent1440 }\viewkind4\uc1 3 | \pard\sa200\sl276\slmult1\f0\fs24\lang9 # Komorebi License\par 4 | \par 5 | Version 2.0.0\par 6 | \par 7 | ## Acceptance\par 8 | \par 9 | In order to get any license under these terms, you must agree\par 10 | to them as both strict obligations and conditions to all\par 11 | your licenses.\par 12 | \par 13 | ## Copyright License\par 14 | \par 15 | The licensor grants you a copyright license for the software\par 16 | to do everything you might do with the software that would\par 17 | otherwise infringe the licensor's copyright in it for any\par 18 | permitted purpose. However, you may only distribute the source\par 19 | code of the software according to the [Distribution License](\par 20 | #distribution-license), you may only make changes according\par 21 | to the [Changes License](#changes-license), and you may not\par 22 | otherwise distribute the software or new works based on the\par 23 | software.\par 24 | \par 25 | ## Distribution License\par 26 | \par 27 | The licensor grants you an additional copyright license to\par 28 | distribute copies of the source code of the software. Your\par 29 | license to distribute covers distributing the source code of\par 30 | the software with changes permitted by the [Changes License](\par 31 | #changes-license).\par 32 | \par 33 | ## Changes License\par 34 | \par 35 | The licensor grants you an additional copyright license to\par 36 | make changes for any permitted purpose.\par 37 | \par 38 | ## Patent License\par 39 | \par 40 | The licensor grants you a patent license for the software that\par 41 | covers patent claims the licensor can license, or becomes able\par 42 | to license, that you would infringe by using the software.\par 43 | \par 44 | ## Personal Uses\par 45 | \par 46 | Personal use for research, experiment, and testing for\par 47 | the benefit of public knowledge, personal study, private\par 48 | entertainment, hobby projects, amateur pursuits, or religious\par 49 | observance, without any anticipated commercial application,\par 50 | is use for a permitted purpose.\par 51 | \par 52 | ## Fair Use\par 53 | \par 54 | You may have "fair use" rights for the software under the\par 55 | law. These terms do not limit them.\par 56 | \par 57 | ## No Other Rights\par 58 | \par 59 | These terms do not allow you to sublicense or transfer any of\par 60 | your licenses to anyone else, or prevent the licensor from\par 61 | granting licenses to anyone else. These terms do not imply\par 62 | any other licenses.\par 63 | \par 64 | ## Patent Defense\par 65 | \par 66 | If you make any written claim that the software infringes or\par 67 | contributes to infringement of any patent, your patent license\par 68 | for the software granted under these terms ends immediately. If\par 69 | your company makes such a claim, your patent license ends\par 70 | immediately for work on behalf of your company.\par 71 | \par 72 | ## Violations\par 73 | \par 74 | The first time you are notified in writing that you have\par 75 | violated any of these terms, or done anything with the software\par 76 | not covered by your licenses, your licenses can nonetheless\par 77 | continue if you come into full compliance with these terms,\par 78 | and take practical steps to correct past violations, within\par 79 | 32 days of receiving notice. Otherwise, all your licenses\par 80 | end immediately.\par 81 | \par 82 | ## No Liability\par 83 | \par 84 | ***As far as the law allows, the software comes as is, without\par 85 | any warranty or condition, and the licensor will not be liable\par 86 | to you for any damages arising out of these terms or the use\par 87 | or nature of the software, under any kind of legal claim.***\par 88 | \par 89 | ## Definitions\par 90 | \par 91 | The **licensor** is the individual or entity offering these\par 92 | terms, and the **software** is the software the licensor makes\par 93 | available under these terms.\par 94 | \par 95 | **You** refers to the individual or entity agreeing to these\par 96 | terms.\par 97 | \par 98 | **Your company** is any legal entity, sole proprietorship,\par 99 | or other kind of organization that you work for, plus all\par 100 | organizations that have control over, are under the control of,\par 101 | or are under common control with that organization. **Control**\par 102 | means ownership of substantially all the assets of an entity,\par 103 | or the power to direct its management and policies by vote,\par 104 | contract, or otherwise. Control can be direct or indirect.\par 105 | \par 106 | **Your licenses** are all the licenses granted to you for the\par 107 | software under these terms.\par 108 | \par 109 | **Use** means anything you do with the software requiring one\par 110 | of your licenses.\par 111 | } 112 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | 115 | 116 | 117 | 123 | 124 | 125 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 146 | 150 | 151 | 152 | 153 | 154 | 155 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 189 | 190 | 191 | 192 | 193 | 194 | 198 | 199 | 200 | 201 | 209 | 210 | 211 | 212 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /wpm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wpm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | chrono = { workspace = true } 8 | dirs = { workspace = true } 9 | interprocess = { workspace = true } 10 | parking_lot = { workspace = true } 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | serde-envfile = "0.2" 14 | shared_child = "1" 15 | sysinfo = { workspace = true } 16 | thiserror = { workspace = true } 17 | tracing = { workspace = true } 18 | regex = "1" 19 | reqwest = { version = "0.12", features = ["blocking"] } 20 | url = { version = "2", features = ["serde"] } 21 | sha256 = "1" 22 | 23 | schemars = { version = "0.8", features = ["url"] } 24 | tabled = { version = "0.18", features = ["derive"] } 25 | toml = "0.8" 26 | -------------------------------------------------------------------------------- /wpm/src/communication.rs: -------------------------------------------------------------------------------- 1 | use crate::SocketMessage; 2 | use interprocess::local_socket::traits::Stream as StreamExt; 3 | use interprocess::local_socket::GenericNamespaced; 4 | use interprocess::local_socket::Stream; 5 | use interprocess::local_socket::ToNsName; 6 | use std::io::Write; 7 | 8 | pub fn send_message(to: &str, message: SocketMessage) -> Result<(), std::io::Error> { 9 | let json = serde_json::to_string(&message)?; 10 | let name = to.to_ns_name::()?; 11 | let connection = Stream::connect(name)?; 12 | let (_, mut sender) = connection.split(); 13 | sender.write_all(json.as_bytes())?; 14 | 15 | Ok(()) 16 | } 17 | 18 | pub fn send_str(to: &str, message: &str) -> Result<(), std::io::Error> { 19 | let name = to.to_ns_name::()?; 20 | let connection = Stream::connect(name)?; 21 | let (_, mut sender) = connection.split(); 22 | sender.write_all(message.as_bytes())?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /wpm/src/generators.rs: -------------------------------------------------------------------------------- 1 | use crate::unit::CommandHealthcheck; 2 | use crate::unit::Definition; 3 | use crate::unit::Executable; 4 | use crate::unit::Healthcheck; 5 | use crate::unit::ProcessHealthcheck; 6 | use crate::unit::RemoteExecutable; 7 | use crate::unit::RestartStrategy; 8 | use crate::unit::ScoopExecutable; 9 | use crate::unit::ScoopManifest; 10 | use crate::unit::Service; 11 | use crate::unit::ServiceCommand; 12 | use crate::unit::ServiceKind; 13 | use crate::unit::Unit; 14 | use schemars::schema_for; 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | use std::str::FromStr; 18 | use url::Url; 19 | 20 | impl Definition { 21 | pub fn schemagen() -> String { 22 | let schema = schema_for!(Self); 23 | serde_json::to_string_pretty(&schema).unwrap() 24 | } 25 | 26 | pub fn examplegen(path: Option) { 27 | let examples = vec![ 28 | Self { 29 | schema: None, 30 | unit: Unit { 31 | name: "kanata".to_string(), 32 | description: Some("Software keyboard remapper".to_string()), 33 | requires: None, 34 | }, 35 | resources: Some( 36 | [( 37 | String::from("CONFIGURATION_FILE"), 38 | Url::from_str("https://gist.githubusercontent.com/LGUG2Z/bbafc51ddde2bd1462151cfcc3f7f489/raw/28e24c4a493166fa866ae24ebc4ed8df7f164bd1/minimal.clj").unwrap() 39 | )] 40 | .into_iter() 41 | .collect() 42 | ), 43 | service: Service { 44 | kind: ServiceKind::Simple, 45 | exec_start: ServiceCommand { 46 | executable: Executable::Scoop(ScoopExecutable::Manifest(ScoopManifest { 47 | manifest: Url::from_str("https://raw.githubusercontent.com/ScoopInstaller/Extras/8a6d8ff0f3963611ae61fd9f45ff36e3c321c8b5/bucket/kanata.json").unwrap(), 48 | package: "kanata".to_string(), 49 | version: "1.8.1".to_string(), 50 | target: None 51 | })), 52 | arguments: Some(vec![ 53 | "-c".to_string(), 54 | "{{ Resources.CONFIGURATION_FILE }}".to_string(), 55 | "--port".to_string(), 56 | "9999".to_string(), 57 | ]), 58 | environment: None, 59 | environment_file: None, 60 | retry_limit: None, 61 | }, 62 | environment: None, 63 | environment_file: None, 64 | working_directory: None, 65 | healthcheck: Some(Healthcheck::default()), 66 | restart: Default::default(), 67 | restart_sec: None, 68 | exec_stop: None, 69 | exec_stop_post: None, 70 | autostart: false, 71 | exec_start_pre: None, 72 | exec_start_post: None, 73 | }, 74 | }, 75 | Self { 76 | schema: None, 77 | unit: Unit { 78 | name: "komorebi-bar".to_string(), 79 | description: Some("Status bar for komorebi".to_string()), 80 | requires: Some(vec!["komorebi".to_string()]), 81 | }, 82 | resources: Some( 83 | [( 84 | String::from("CONFIGURATION_FILE"), 85 | Url::from_str("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.bar.example.json").unwrap() 86 | )] 87 | .into_iter() 88 | .collect() 89 | ), 90 | service: Service { 91 | kind: ServiceKind::Simple, 92 | environment: Some(vec![( 93 | "KOMOREBI_CONFIG_HOME".to_string(), 94 | "$USERPROFILE/.config/komorebi".to_string(), 95 | )]), 96 | exec_start: ServiceCommand { 97 | executable: Executable::Scoop(ScoopExecutable::Manifest(ScoopManifest { 98 | manifest: Url::from_str("https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json").unwrap(), 99 | package: "komorebi".to_string(), 100 | version: "0.1.35".to_string(), 101 | target: Some("komorebi-bar.exe".to_string()) 102 | })), 103 | arguments: Some(vec![ 104 | "--config".to_string(), 105 | "{{ Resources.CONFIGURATION_FILE }}".to_string(), 106 | ]), 107 | environment: None, 108 | environment_file: None, 109 | retry_limit: None, 110 | }, 111 | working_directory: None, 112 | healthcheck: Some(Healthcheck::default()), 113 | restart: Default::default(), 114 | restart_sec: None, 115 | exec_stop: None, 116 | exec_stop_post: None, 117 | autostart: false, 118 | exec_start_pre: None, 119 | exec_start_post: None, 120 | environment_file: None, 121 | }, 122 | }, 123 | Self { 124 | schema: None, 125 | unit: Unit { 126 | name: "komorebi".to_string(), 127 | description: Some("Tiling window management for Windows".to_string()), 128 | requires: Some(vec!["whkd".to_string(), "kanata".to_string()]), 129 | }, 130 | resources: Some( 131 | [( 132 | String::from("CONFIGURATION_FILE"), 133 | Url::from_str("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/komorebi.example.json").unwrap() 134 | )] 135 | .into_iter() 136 | .collect() 137 | ), 138 | service: Service { 139 | kind: ServiceKind::Simple, 140 | exec_start: ServiceCommand { 141 | executable: Executable::Scoop(ScoopExecutable::Manifest(ScoopManifest { 142 | manifest: Url::from_str("https://raw.githubusercontent.com/ScoopInstaller/Extras/8e21dc2cd902b865d153e64a078d97d3cd0593f7/bucket/komorebi.json").unwrap(), 143 | package: "komorebi".to_string(), 144 | version: "0.1.35".to_string(), 145 | target: Some("komorebi.exe".to_string()) 146 | })), 147 | arguments: Some(vec![ 148 | "--config".to_string(), 149 | "{{ Resources.CONFIGURATION_FILE }}".to_string(), 150 | ]), 151 | environment: Some(vec![( 152 | "KOMOREBI_CONFIG_HOME".to_string(), 153 | "$USERPROFILE/.config/komorebi".to_string(), 154 | )]), 155 | environment_file: None, 156 | retry_limit: None, 157 | }, 158 | environment: None, 159 | environment_file: None, 160 | working_directory: None, 161 | healthcheck: Some(Healthcheck::Command(CommandHealthcheck { 162 | executable: PathBuf::from("komorebic.exe"), 163 | arguments: Some(vec!["state".to_string()]), 164 | environment: None, 165 | delay_sec: 1, 166 | retry_limit: None, 167 | })), 168 | restart: Default::default(), 169 | restart_sec: None, 170 | exec_stop: Some(vec![ServiceCommand { 171 | executable: Executable::Local(PathBuf::from("komorebic.exe")), 172 | arguments: Some(vec!["stop".to_string()]), 173 | environment: None, 174 | environment_file: None, 175 | retry_limit: None, 176 | }]), 177 | exec_stop_post: Some(vec![ServiceCommand { 178 | executable: Executable::Local(PathBuf::from("komorebic.exe")), 179 | arguments: Some(vec!["restore-windows".to_string()]), 180 | environment: None, 181 | environment_file: None, 182 | retry_limit: None, 183 | }]), 184 | autostart: false, 185 | exec_start_pre: Some(vec![ServiceCommand { 186 | executable: Executable::Local(PathBuf::from("komorebic.exe")), 187 | arguments: Some(vec!["fetch-asc".to_string()]), 188 | environment: None, 189 | environment_file: None, 190 | retry_limit: None, 191 | }]), 192 | exec_start_post: None, 193 | }, 194 | }, 195 | Self { 196 | schema: None, 197 | unit: Unit { 198 | name: "whkd".to_string(), 199 | description: Some("Simple hotkey daemon for Windows".to_string()), 200 | requires: None, 201 | }, 202 | resources: Some( 203 | [( 204 | String::from("CONFIGURATION_FILE"), 205 | Url::from_str("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v0.1.35/docs/whkdrc.sample").unwrap() 206 | )] 207 | .into_iter() 208 | .collect() 209 | ), 210 | service: Service { 211 | kind: ServiceKind::Simple, 212 | exec_start: ServiceCommand { 213 | executable: Executable::Scoop(ScoopExecutable::Manifest(ScoopManifest { 214 | manifest: Url::from_str("https://raw.githubusercontent.com/ScoopInstaller/Extras/112fd691392878f8c4e9e9703dde3d1d182941e3/bucket/whkd.json").unwrap(), 215 | package: "whkd".to_string(), 216 | version: "0.2.7".to_string(), 217 | target: None, 218 | })), 219 | arguments: Some(vec![ 220 | "--config".to_string(), 221 | "{{ Resources.CONFIGURATION_FILE }}".to_string(), 222 | ]), 223 | environment: None, 224 | environment_file: None, 225 | retry_limit: None, 226 | }, 227 | environment: None, 228 | environment_file: None, 229 | working_directory: None, 230 | healthcheck: Some(Healthcheck::default()), 231 | restart: RestartStrategy::OnFailure, 232 | restart_sec: Some(2), 233 | exec_stop: None, 234 | exec_stop_post: None, 235 | autostart: false, 236 | exec_start_pre: None, 237 | exec_start_post: None, 238 | }, 239 | }, 240 | Self { 241 | schema: None, 242 | unit: Unit { 243 | name: "mousemaster".to_string(), 244 | description: Some("A keyboard driven interface for mouseless mouse manipulation".to_string()), 245 | requires: Some(vec!["whkd".to_string(), "kanata".to_string()]), 246 | }, 247 | resources: Some( 248 | [( 249 | String::from("CONFIGURATION_FILE"), 250 | Url::from_str("https://raw.githubusercontent.com/petoncle/mousemaster/refs/tags/73/configuration/neo-mousekeys-ijkl.properties").unwrap() 251 | )] 252 | .into_iter() 253 | .collect() 254 | ), 255 | service: Service { 256 | kind: ServiceKind::Simple, 257 | exec_start: ServiceCommand { 258 | executable: Executable::Remote(RemoteExecutable { 259 | url: Url::from_str("https://github.com/petoncle/mousemaster/releases/download/73/mousemaster.exe").unwrap(), 260 | hash: "55009596854109e0e7fb6ded3f5a1098e4ab211bed8e3d975d81c4bd8a849aa5".to_string() 261 | }), 262 | arguments: Some(vec![ 263 | "--configuration-file={{ Resources.CONFIGURATION_FILE }}".to_string(), 264 | "--pause-on-error=false".to_string(), 265 | ]), 266 | environment: None, 267 | environment_file: None, 268 | retry_limit: None, 269 | }, 270 | environment: None, 271 | environment_file: None, 272 | working_directory: None, 273 | healthcheck: Some(Healthcheck::Process(ProcessHealthcheck { 274 | target: None, 275 | delay_sec: 2, 276 | })), 277 | restart: RestartStrategy::OnFailure, 278 | restart_sec: Some(2), 279 | exec_stop: None, 280 | exec_stop_post: None, 281 | autostart: false, 282 | exec_start_pre: None, 283 | exec_start_post: None, 284 | }, 285 | }, 286 | Self { 287 | schema: None, 288 | unit: Unit { 289 | name: "komokana".to_string(), 290 | description: Some("Automatic application-aware keyboard layer switching for Windows".to_string()), 291 | requires: Some(vec!["komorebi".to_string(), "kanata".to_string()]), 292 | }, 293 | resources: Some( 294 | [( 295 | String::from("CONFIGURATION_FILE"), 296 | Url::from_str("https://raw.githubusercontent.com/LGUG2Z/komokana/refs/tags/v0.1.5/komokana.example.yaml").unwrap() 297 | )] 298 | .into_iter() 299 | .collect() 300 | ), 301 | service: Service { 302 | kind: ServiceKind::Simple, 303 | exec_start: ServiceCommand { 304 | executable: Executable::Scoop(ScoopExecutable::Manifest(ScoopManifest { 305 | manifest: Url::from_str("https://raw.githubusercontent.com/ScoopInstaller/Extras/e633292b4e1101273caac59ffcb4a7ce7ee7a2e8/bucket/komokana.json").unwrap(), 306 | package: "komokana".to_string(), 307 | version: "0.1.5".to_string(), 308 | target: None, 309 | })), 310 | arguments: Some(vec![ 311 | "--kanata-port".to_string(), 312 | "9999".to_string(), 313 | "--configuration".to_string(), 314 | "{{ Resources.CONFIGURATION_FILE }}".to_string(), 315 | "--default-layer".to_string(), 316 | "qwerty".to_string() 317 | ]), 318 | environment: None, 319 | environment_file: None, 320 | retry_limit: None, 321 | }, 322 | environment: None, 323 | environment_file: None, 324 | working_directory: None, 325 | healthcheck: Some(Healthcheck::default()), 326 | restart: RestartStrategy::OnFailure, 327 | restart_sec: Some(2), 328 | exec_stop: None, 329 | exec_stop_post: None, 330 | autostart: false, 331 | exec_start_pre: None, 332 | exec_start_post: None, 333 | }, 334 | }, 335 | Self { 336 | schema: None, 337 | unit: Unit { 338 | name: "desktop".to_string(), 339 | description: Some("Everything I need to work on Windows".to_string()), 340 | requires: Some(vec!["komorebi".to_string(), "komorebi-bar".to_string(), "mousemaster".to_string()]), 341 | }, 342 | resources: None, 343 | service: Service { 344 | kind: ServiceKind::Oneshot, 345 | exec_start: ServiceCommand { 346 | executable: Executable::Local(PathBuf::from("msg.exe")), 347 | arguments: Some(vec![ 348 | "*".to_string(), 349 | "Desktop recipe completed!".to_string(), 350 | ]), 351 | environment: None, 352 | environment_file: None, 353 | retry_limit: None, 354 | }, 355 | environment: None, 356 | environment_file: None, 357 | working_directory: None, 358 | healthcheck: None, 359 | restart: Default::default(), 360 | restart_sec: None, 361 | exec_stop: None, 362 | exec_stop_post: None, 363 | autostart: false, 364 | exec_start_pre: None, 365 | exec_start_post: None, 366 | }, 367 | }, 368 | ]; 369 | 370 | let formats = if path.is_some() { 371 | vec!["json"] 372 | } else { 373 | vec!["json", "toml"] 374 | }; 375 | 376 | for format in formats { 377 | let parent = if let Some(path) = &path { 378 | path 379 | } else { 380 | &Path::new("examples").join(format) 381 | }; 382 | 383 | std::fs::create_dir_all(parent).unwrap(); 384 | 385 | for example in &examples { 386 | match format { 387 | "json" => { 388 | let mut example = example.clone(); 389 | example.schema = Some("https://raw.githubusercontent.com/LGUG2Z/wpm/refs/heads/master/schema.unit.json".to_string()); 390 | 391 | std::fs::write( 392 | parent.join(format!("{}.json", example.unit.name)), 393 | serde_json::to_string_pretty(&example).unwrap(), 394 | ) 395 | .unwrap(); 396 | } 397 | "toml" => { 398 | std::fs::write( 399 | parent.join(format!("{}.toml", example.unit.name)), 400 | toml::to_string_pretty(&example).unwrap(), 401 | ) 402 | .unwrap(); 403 | } 404 | _ => {} 405 | } 406 | } 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /wpm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use regex::Regex; 4 | use serde::Deserialize; 5 | use serde::Serialize; 6 | use std::path::PathBuf; 7 | use std::sync::OnceLock; 8 | use tracing::warn; 9 | 10 | pub mod communication; 11 | mod generators; 12 | pub mod process_manager; 13 | mod process_manager_status; 14 | pub mod unit; 15 | pub mod unit_status; 16 | 17 | static DATA_DIR: OnceLock = OnceLock::new(); 18 | static REQWEST_CLIENT: OnceLock = OnceLock::new(); 19 | 20 | static RESOURCE_REGEX: OnceLock = OnceLock::new(); 21 | 22 | pub fn resource_regex<'regex>() -> &'regex Regex { 23 | RESOURCE_REGEX.get_or_init(|| Regex::new(r"\{\{\s*Resources\.([A-Za-z0-9_]+)\s*\}\}").unwrap()) 24 | } 25 | pub fn reqwest_client() -> reqwest::blocking::Client { 26 | REQWEST_CLIENT 27 | .get_or_init(|| { 28 | let builder = reqwest::blocking::Client::builder(); 29 | builder.user_agent("wpm").build().unwrap() 30 | }) 31 | .clone() 32 | } 33 | 34 | pub fn wpm_data_dir() -> PathBuf { 35 | DATA_DIR 36 | .get_or_init(|| { 37 | let wpm_dir = dirs::data_local_dir() 38 | .expect("could not find the system's local data dir") 39 | .join("wpm"); 40 | 41 | std::fs::create_dir_all(&wpm_dir) 42 | .expect("could not ensure creation of the wpm local data dir"); 43 | 44 | let log_dir = wpm_dir.join("logs"); 45 | 46 | std::fs::create_dir_all(&log_dir) 47 | .expect("could not ensure creation of the wpm logs local data dir"); 48 | 49 | let store_dir = wpm_dir.join("store"); 50 | 51 | std::fs::create_dir_all(&store_dir) 52 | .expect("could not ensure creation of the wpm store local data dir"); 53 | 54 | wpm_dir 55 | }) 56 | .clone() 57 | } 58 | 59 | pub fn wpm_store_dir() -> PathBuf { 60 | wpm_data_dir().join("store") 61 | } 62 | 63 | pub fn wpm_log_dir() -> PathBuf { 64 | wpm_data_dir().join("logs") 65 | } 66 | 67 | pub fn wpm_units_dir() -> PathBuf { 68 | dirs::home_dir() 69 | .expect("could not find home dir") 70 | .join(".config") 71 | .join("wpm") 72 | } 73 | 74 | #[derive(Debug, Serialize, Deserialize)] 75 | pub enum SocketMessage { 76 | Start(Vec), 77 | Stop(Vec), 78 | Status(String), 79 | State, 80 | Reload(Option), 81 | Reset(Vec), 82 | Restart(Vec), 83 | RestartWithDependents(Vec), 84 | } 85 | -------------------------------------------------------------------------------- /wpm/src/process_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::communication::send_message; 2 | use crate::process_manager_status::ProcessManagerStatus; 3 | use crate::unit::Definition; 4 | use crate::unit::Executable; 5 | use crate::unit::Healthcheck; 6 | use crate::unit::RestartStrategy; 7 | use crate::unit::ServiceKind; 8 | use crate::unit_status::DisplayedOption; 9 | use crate::unit_status::UnitState; 10 | use crate::unit_status::UnitStatus; 11 | use crate::SocketMessage; 12 | use chrono::DateTime; 13 | use chrono::Local; 14 | use chrono::Utc; 15 | use parking_lot::Mutex; 16 | use shared_child::SharedChild; 17 | use std::collections::HashMap; 18 | use std::ffi::OsStr; 19 | use std::path::Path; 20 | use std::path::PathBuf; 21 | use std::process::ExitStatus; 22 | use std::sync::Arc; 23 | use std::time::Duration; 24 | use sysinfo::Pid; 25 | use sysinfo::ProcessRefreshKind; 26 | use sysinfo::ProcessesToUpdate; 27 | use sysinfo::System; 28 | use thiserror::Error; 29 | 30 | #[derive(Error, Debug)] 31 | pub enum ProcessManagerError { 32 | #[error("{0} is not a registered unit")] 33 | UnregisteredUnit(String), 34 | #[error("{0} is already running")] 35 | RunningUnit(String), 36 | #[error("{0} is marked as completed; reset unit before trying again")] 37 | CompletedUnit(String), 38 | #[error("{0} failed its healthcheck; reset unit before trying again")] 39 | FailedHealthcheck(String), 40 | #[error("{0} is not running")] 41 | NotRunning(String), 42 | #[error(transparent)] 43 | Io(#[from] std::io::Error), 44 | #[error(transparent)] 45 | Toml(#[from] toml::de::Error), 46 | #[error(transparent)] 47 | Json(#[from] serde_json::Error), 48 | #[error(transparent)] 49 | Reqwest(#[from] reqwest::Error), 50 | #[error("{0} did not spawn a process with a handle")] 51 | NoHandle(String), 52 | #[error("a forking service must have a process healthcheck target defined")] 53 | InvalidForkingService, 54 | #[error("a simple service cannot have a separate process healthcheck target")] 55 | InvalidSimpleService, 56 | #[error("hash mismatch (expected {expected}, actual {actual})")] 57 | HashMismatch { expected: String, actual: String }, 58 | } 59 | 60 | #[derive(Clone)] 61 | pub enum Child { 62 | Shared(Arc), 63 | Pid(u32), 64 | } 65 | 66 | impl Child { 67 | pub fn id(&self) -> u32 { 68 | match self { 69 | Child::Shared(shared) => shared.id(), 70 | Child::Pid(id) => *id, 71 | } 72 | } 73 | 74 | pub fn kill(&self) -> std::io::Result<()> { 75 | match self { 76 | Child::Shared(shared) => shared.kill(), 77 | Child::Pid(id) => { 78 | let pid = *id; 79 | let s = System::new_all(); 80 | if let Some(process) = s.process(Pid::from_u32(pid)) { 81 | process.kill(); 82 | } 83 | 84 | Ok(()) 85 | } 86 | } 87 | } 88 | 89 | pub fn wait(&self) -> std::io::Result { 90 | match self { 91 | Self::Shared(child) => child.wait(), 92 | Self::Pid(pid) => { 93 | let mut system = System::new_all(); 94 | let pid = Pid::from_u32(*pid); 95 | system.refresh_processes_specifics( 96 | ProcessesToUpdate::Some(&[pid]), 97 | true, 98 | ProcessRefreshKind::everything(), 99 | ); 100 | 101 | if let Some(process) = system.process(pid) { 102 | process.wait().ok_or_else(|| { 103 | std::io::Error::new(std::io::ErrorKind::Other, "Process wait returned None") 104 | }) 105 | } else { 106 | Err(std::io::Error::new( 107 | std::io::ErrorKind::NotFound, 108 | "Process not found", 109 | )) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | #[derive(Clone)] 117 | pub struct ProcessState { 118 | pub child: Child, 119 | pub timestamp: DateTime, 120 | } 121 | 122 | pub struct ProcessManager { 123 | definitions: HashMap, 124 | running: Arc>>, 125 | completed: Arc>>>, 126 | failed: Arc>>>, 127 | terminated: Arc>>>, 128 | } 129 | 130 | impl ProcessManager { 131 | pub fn unit_directory() -> PathBuf { 132 | let home = dirs::home_dir().expect("could not find home dir"); 133 | let dir = home.join(".config").join("wpm"); 134 | 135 | if !dir.is_dir() { 136 | std::fs::create_dir_all(&dir).expect("could not create ~/.config/wpm"); 137 | } 138 | 139 | dir 140 | } 141 | 142 | pub(crate) fn find_exe(exe_name: &Path) -> Option { 143 | let mut name = exe_name.to_path_buf(); 144 | if name.extension().is_none() { 145 | name = exe_name.with_extension("exe"); 146 | } 147 | 148 | if let Ok(paths) = std::env::var("PATH") { 149 | for path in std::env::split_paths(&paths) { 150 | let full_path = path.join(&name); 151 | if full_path.is_file() { 152 | return Some(full_path); 153 | } 154 | } 155 | } 156 | None 157 | } 158 | 159 | pub fn init(path: Option) -> Result { 160 | let mut pm = ProcessManager { 161 | definitions: Default::default(), 162 | running: Arc::new(Default::default()), 163 | completed: Arc::new(Default::default()), 164 | failed: Arc::new(Default::default()), 165 | terminated: Arc::new(Default::default()), 166 | }; 167 | 168 | pm.load_units(path)?; 169 | pm.autostart(); 170 | 171 | Ok(pm) 172 | } 173 | 174 | pub fn autostart(&mut self) { 175 | let mut autostart = vec![]; 176 | 177 | for (name, def) in &self.definitions { 178 | if def.service.autostart { 179 | autostart.push(name.clone()); 180 | } 181 | } 182 | 183 | for name in &autostart { 184 | tracing::info!("{name}: autostarting"); 185 | if let Err(error) = self.start(name) { 186 | tracing::error!("{error}"); 187 | } 188 | } 189 | } 190 | 191 | pub fn retrieve_units(path: Option) -> Result, ProcessManagerError> { 192 | let unit_dir = if let Some(path) = path { 193 | path 194 | } else { 195 | Self::unit_directory() 196 | }; 197 | 198 | let read_dir = std::fs::read_dir(unit_dir)?; 199 | 200 | let mut paths = vec![]; 201 | 202 | for entry in read_dir.flatten() { 203 | let path = entry.path(); 204 | if path.is_file() { 205 | #[allow(clippy::if_same_then_else)] 206 | if path.extension() == Some(OsStr::new("json")) { 207 | paths.push(path); 208 | } else if path.extension() == Some(OsStr::new("toml")) 209 | && path.file_name() != Some(OsStr::new("taplo.toml")) 210 | && path.file_name() != Some(OsStr::new(".taplo.toml")) 211 | { 212 | paths.push(path); 213 | } 214 | } 215 | } 216 | 217 | let mut units = vec![]; 218 | 219 | for path in paths { 220 | let definition: Definition = match path.extension() { 221 | Some(extension) => match extension.to_string_lossy().to_string().as_str() { 222 | "json" => serde_json::from_str(&std::fs::read_to_string(path)?)?, 223 | "toml" => toml::from_str(&std::fs::read_to_string(path)?)?, 224 | _ => continue, 225 | }, 226 | None => continue, 227 | }; 228 | 229 | units.push(definition); 230 | } 231 | 232 | Ok(units) 233 | } 234 | 235 | pub fn load_units(&mut self, path: Option) -> Result<(), ProcessManagerError> { 236 | let unit_dir = if let Some(path) = path { 237 | path 238 | } else { 239 | Self::unit_directory() 240 | }; 241 | 242 | let read_dir = std::fs::read_dir(unit_dir)?; 243 | 244 | let mut units = vec![]; 245 | 246 | for entry in read_dir.flatten() { 247 | let path = entry.path(); 248 | if path.is_file() { 249 | #[allow(clippy::if_same_then_else)] 250 | if path.extension() == Some(OsStr::new("json")) { 251 | units.push(path); 252 | } else if path.extension() == Some(OsStr::new("toml")) 253 | && path.file_name() != Some(OsStr::new("taplo.toml")) 254 | && path.file_name() != Some(OsStr::new(".taplo.toml")) 255 | { 256 | units.push(path); 257 | } 258 | } 259 | } 260 | 261 | for path in units { 262 | let mut definition: Definition = match path.extension() { 263 | Some(extension) => match extension.to_string_lossy().to_string().as_str() { 264 | "json" => serde_json::from_str(&std::fs::read_to_string(path)?)?, 265 | "toml" => toml::from_str(&std::fs::read_to_string(path)?)?, 266 | _ => continue, 267 | }, 268 | None => continue, 269 | }; 270 | 271 | definition.resolve_resources()?; 272 | 273 | if matches!(definition.service.kind, ServiceKind::Forking) { 274 | let mut is_valid_forking_service = false; 275 | if let Some(Healthcheck::Process(proc)) = &definition.service.healthcheck { 276 | if proc.target.is_some() { 277 | is_valid_forking_service = true; 278 | } 279 | } 280 | 281 | if !is_valid_forking_service { 282 | return Err(ProcessManagerError::InvalidForkingService); 283 | } 284 | } 285 | 286 | if matches!(definition.service.kind, ServiceKind::Simple) { 287 | let mut is_invalid_simple_service = false; 288 | if let Some(Healthcheck::Process(proc)) = &definition.service.healthcheck { 289 | if proc.target.is_some() { 290 | is_invalid_simple_service = true; 291 | } 292 | } 293 | 294 | if is_invalid_simple_service { 295 | return Err(ProcessManagerError::InvalidSimpleService); 296 | } 297 | } 298 | 299 | let home_dir = dirs::home_dir() 300 | .expect("could not find home dir") 301 | .to_str() 302 | .unwrap() 303 | .to_string(); 304 | 305 | if let Some(working_directory) = definition.service.working_directory.as_mut() { 306 | let stringified = working_directory.to_string_lossy(); 307 | let stringified = stringified.replace("$USERPROFILE", &home_dir); 308 | let directory = PathBuf::from(stringified); 309 | 310 | *working_directory = directory; 311 | } 312 | 313 | if let Some(environment_file) = &definition.service.environment_file { 314 | let stringified = environment_file.to_string_lossy(); 315 | let stringified = stringified.replace("$USERPROFILE", &home_dir); 316 | let environment_file = PathBuf::from(stringified); 317 | 318 | if let Ok(environment) = 319 | serde_envfile::from_file::(&environment_file) 320 | { 321 | for (k, v) in environment.iter() { 322 | match &mut definition.service.environment { 323 | None => { 324 | definition.service.environment = Some(vec![(k.clone(), v.clone())]) 325 | } 326 | Some(e) => { 327 | e.push((k.clone(), v.clone())); 328 | } 329 | } 330 | } 331 | } 332 | } 333 | 334 | for (_, value) in definition.service.environment.iter_mut().flatten() { 335 | *value = value.replace("$USERPROFILE", &home_dir); 336 | } 337 | 338 | for cmd in definition.service.exec_start_pre.iter_mut().flatten() { 339 | cmd.resolve_user_profile(); 340 | } 341 | 342 | definition.service.exec_start.resolve_user_profile(); 343 | 344 | for cmd in definition.service.exec_start_post.iter_mut().flatten() { 345 | cmd.resolve_user_profile(); 346 | } 347 | 348 | for cmd in definition.service.exec_stop.iter_mut().flatten() { 349 | cmd.resolve_user_profile(); 350 | } 351 | 352 | for cmd in definition.service.exec_stop_post.iter_mut().flatten() { 353 | cmd.resolve_user_profile(); 354 | } 355 | 356 | if definition 357 | .service 358 | .exec_start 359 | .executable 360 | .pathbuf()? 361 | .canonicalize() 362 | .is_err() 363 | { 364 | match Self::find_exe(&definition.service.exec_start.executable.pathbuf()?) { 365 | Some(path) => { 366 | definition.service.exec_start.executable = Executable::Local(path) 367 | } 368 | None => { 369 | tracing::warn!( 370 | "{}: could not find executable in $PATH, skipping unit", 371 | definition.unit.name 372 | ); 373 | continue; 374 | } 375 | } 376 | } 377 | 378 | for command in definition.service.exec_start_pre.iter_mut().flatten() { 379 | if command.executable.pathbuf()?.canonicalize().is_err() { 380 | match Self::find_exe(&command.executable.pathbuf()?) { 381 | Some(path) => command.executable = Executable::Local(path), 382 | None => { 383 | tracing::warn!( 384 | "{}: could not find pre-start command executable in $PATH, skipping unit", 385 | definition.unit.name 386 | ); 387 | continue; 388 | } 389 | } 390 | } 391 | } 392 | 393 | for command in definition.service.exec_start_post.iter_mut().flatten() { 394 | if command.executable.pathbuf()?.canonicalize().is_err() { 395 | match Self::find_exe(&command.executable.pathbuf()?) { 396 | Some(path) => command.executable = Executable::Local(path), 397 | None => { 398 | tracing::warn!( 399 | "{}: could not find post-start command executable in $PATH, skipping unit", 400 | definition.unit.name 401 | ); 402 | continue; 403 | } 404 | } 405 | } 406 | } 407 | 408 | for command in definition.service.exec_stop.iter_mut().flatten() { 409 | if command.executable.pathbuf()?.canonicalize().is_err() { 410 | match Self::find_exe(&command.executable.pathbuf()?) { 411 | Some(path) => command.executable = Executable::Local(path), 412 | None => { 413 | tracing::warn!( 414 | "{}: could not find shutdown command executable in $PATH, skipping unit", 415 | definition.unit.name 416 | ); 417 | continue; 418 | } 419 | } 420 | } 421 | } 422 | 423 | for command in definition.service.exec_stop_post.iter_mut().flatten() { 424 | if command.executable.pathbuf()?.canonicalize().is_err() { 425 | match Self::find_exe(&command.executable.pathbuf()?) { 426 | Some(path) => command.executable = Executable::Local(path), 427 | None => { 428 | tracing::warn!( 429 | "{}: could not find cleanup command executable in $PATH, skipping unit", 430 | definition.unit.name 431 | ); 432 | continue; 433 | } 434 | } 435 | } 436 | } 437 | 438 | if matches!(definition.service.kind, ServiceKind::Simple) 439 | && definition.service.healthcheck.is_none() 440 | { 441 | definition.service.healthcheck = Some(Healthcheck::default()); 442 | } 443 | 444 | if matches!(definition.service.kind, ServiceKind::Oneshot) 445 | && definition.service.healthcheck.is_some() 446 | { 447 | definition.service.healthcheck = None; 448 | } 449 | 450 | if let Some(Healthcheck::Command(command)) = &mut definition.service.healthcheck { 451 | command.resolve_user_profile(); 452 | 453 | if command.executable.canonicalize().is_err() { 454 | match Self::find_exe(&command.executable) { 455 | Some(path) => command.executable = path, 456 | None => { 457 | tracing::warn!( 458 | "{}: could not find healthcheck command executable in $PATH, skipping unit", 459 | definition.unit.name 460 | ); 461 | continue; 462 | } 463 | } 464 | } 465 | } 466 | 467 | self.register(definition); 468 | } 469 | 470 | Ok(()) 471 | } 472 | 473 | pub fn register(&mut self, definition: Definition) { 474 | let name = definition.unit.name.clone(); 475 | self.definitions 476 | .insert(definition.unit.name.clone(), definition); 477 | tracing::info!("{name}: registered unit"); 478 | } 479 | 480 | pub fn start(&mut self, name: &str) -> Result, ProcessManagerError> { 481 | let definition = self 482 | .definitions 483 | .get(name) 484 | .cloned() 485 | .ok_or(ProcessManagerError::UnregisteredUnit(name.to_string()))?; 486 | 487 | if self.running.lock().contains_key(name) { 488 | return Err(ProcessManagerError::RunningUnit(name.to_string())); 489 | } 490 | 491 | if self.completed.lock().contains_key(name) { 492 | return Err(ProcessManagerError::CompletedUnit(name.to_string())); 493 | } 494 | 495 | self.failed.lock().remove(name); 496 | self.terminated.lock().remove(name); 497 | 498 | for dep in definition.unit.requires.iter().flatten() { 499 | tracing::info!("{name}: requires {dep}"); 500 | let dependency = self 501 | .definitions 502 | .get(dep) 503 | .cloned() 504 | .ok_or(ProcessManagerError::UnregisteredUnit(dep.to_string()))?; 505 | 506 | if !self.running.lock().contains_key(&dependency.unit.name) { 507 | self.start(&dependency.unit.name)?; 508 | } 509 | } 510 | 511 | let mut retry_limit = definition.service.exec_start.retry_limit.unwrap_or(5); 512 | let mut process_id = None; 513 | while retry_limit > 0 { 514 | let id = definition.execute( 515 | self.running.clone(), 516 | self.completed.clone(), 517 | self.terminated.clone(), 518 | )?; 519 | 520 | match definition.healthcheck( 521 | id.clone(), 522 | self.running.clone(), 523 | self.failed.clone(), 524 | self.terminated.clone(), 525 | ) { 526 | Ok(_) => { 527 | process_id = Some(id); 528 | break; 529 | } 530 | Err(error) => { 531 | retry_limit -= 1; 532 | if retry_limit == 0 { 533 | return Err(error); 534 | } 535 | } 536 | } 537 | } 538 | 539 | #[allow(clippy::unwrap_used)] 540 | Ok(process_id.unwrap()) 541 | } 542 | 543 | pub fn stop(&mut self, name: &str) -> Result<(), ProcessManagerError> { 544 | let unit = self 545 | .definitions 546 | .get(name) 547 | .cloned() 548 | .ok_or(ProcessManagerError::UnregisteredUnit(name.to_string()))?; 549 | 550 | let mut running = self.running.lock(); 551 | 552 | let proc_state = running 553 | .get(name) 554 | .cloned() 555 | .ok_or(ProcessManagerError::NotRunning(name.to_string()))?; 556 | 557 | let id = proc_state.child.id(); 558 | 559 | tracing::info!("{name}: stopping unit"); 560 | 561 | if let Some(shutdown_commands) = unit.service.exec_stop { 562 | for command in shutdown_commands { 563 | let stringified = if let Some(args) = &command.arguments { 564 | format!("{} {}", command.executable, args.join(" ")) 565 | } else { 566 | command.executable.to_string() 567 | }; 568 | 569 | tracing::info!("{name}: executing shutdown command - {stringified}"); 570 | let mut command = command.to_silent_command(unit.service.environment.clone()); 571 | command.output()?; 572 | } 573 | } 574 | 575 | tracing::info!("{name}: sending kill signal to {id}"); 576 | 577 | // remove first to avoid race condition with the other child.wait() 578 | // call spawned in a thread by Unit.execute() 579 | let tmp_proc_state = running.remove(name).unwrap(); 580 | 581 | if let Err(error) = proc_state.child.kill() { 582 | // If there are any errors in killing the process, it's still considered to be running 583 | // so we reinsert before returning the errors 584 | running.insert(name.to_string(), tmp_proc_state); 585 | return Err(error.into()); 586 | } 587 | 588 | if let Err(error) = proc_state.child.wait() { 589 | if matches!(error.kind(), std::io::ErrorKind::NotFound) { 590 | tracing::warn!("{name}: process {id} not found; assuming successful termination"); 591 | } else { 592 | running.insert(name.to_string(), tmp_proc_state); 593 | return Err(error.into()); 594 | } 595 | } 596 | 597 | tracing::info!("{name}: process {id} successfully terminated"); 598 | 599 | if let Some(cleanup_commands) = unit.service.exec_stop_post { 600 | for command in cleanup_commands { 601 | let stringified = if let Some(args) = &command.arguments { 602 | format!("{} {}", command.executable, args.join(" ")) 603 | } else { 604 | command.executable.to_string() 605 | }; 606 | 607 | tracing::info!("{name}: executing cleanup command - {stringified}"); 608 | let mut command = command.to_silent_command(unit.service.environment.clone()); 609 | command.output()?; 610 | } 611 | } 612 | 613 | let thread_name = name.to_string(); 614 | if matches!(unit.service.restart, RestartStrategy::Always) { 615 | std::thread::spawn(move || { 616 | let restart_sec = unit.service.restart_sec.unwrap_or(1); 617 | tracing::info!("{thread_name}: restarting terminated process in {restart_sec}s"); 618 | std::thread::sleep(Duration::from_secs(restart_sec)); 619 | 620 | if let Err(error) = 621 | send_message("wpmd.sock", SocketMessage::Start(vec![thread_name.clone()])) 622 | { 623 | tracing::error!("{thread_name}: {error}"); 624 | } 625 | }); 626 | } 627 | 628 | Ok(()) 629 | } 630 | 631 | pub fn reset(&mut self, name: &str) { 632 | tracing::info!("{name}: resetting unit"); 633 | self.completed.lock().remove(name); 634 | self.failed.lock().remove(name); 635 | self.terminated.lock().remove(name); 636 | } 637 | 638 | pub fn shutdown(&mut self) -> Result<(), ProcessManagerError> { 639 | tracing::info!("wpmd: shutting down process manager"); 640 | 641 | let mut units = vec![]; 642 | for unit in self.running.lock().keys() { 643 | units.push(unit.clone()); 644 | } 645 | 646 | for unit in units { 647 | self.stop(&unit)?; 648 | } 649 | 650 | Ok(()) 651 | } 652 | 653 | pub fn unit(&self, name: &str) -> Option { 654 | self.definitions.get(name).cloned() 655 | } 656 | 657 | pub fn dependents(&self, name: &str) -> Vec { 658 | let mut dependents = vec![]; 659 | for (def_name, def) in &self.definitions { 660 | if let Some(dependencies) = &def.unit.requires { 661 | if dependencies.contains(&name.to_string()) { 662 | dependents.push(def_name.to_string()); 663 | } 664 | } 665 | } 666 | 667 | dependents 668 | } 669 | 670 | pub fn state(&self) -> ProcessManagerStatus { 671 | let mut units = vec![]; 672 | let running = self.running.lock(); 673 | let completed = self.completed.lock(); 674 | let failed = self.failed.lock(); 675 | let terminated = self.terminated.lock(); 676 | 677 | for (name, def) in &self.definitions { 678 | if let Some(proc_state) = running.get(name) { 679 | let local: DateTime = DateTime::from(proc_state.timestamp); 680 | 681 | units.push(( 682 | def.clone(), 683 | UnitStatus { 684 | name: name.clone(), 685 | kind: def.service.kind, 686 | state: UnitState::Running, 687 | pid: DisplayedOption(Some(proc_state.child.id())), 688 | timestamp: DisplayedOption(Some(local.to_string())), 689 | }, 690 | )) 691 | } else if let Some(timestamp) = completed.get(name) { 692 | let local: DateTime = DateTime::from(*timestamp); 693 | 694 | units.push(( 695 | def.clone(), 696 | UnitStatus { 697 | name: name.clone(), 698 | kind: def.service.kind, 699 | state: UnitState::Completed, 700 | pid: DisplayedOption(None), 701 | timestamp: DisplayedOption(Some(local.to_string())), 702 | }, 703 | )) 704 | } else if let Some(timestamp) = failed.get(name) { 705 | let local: DateTime = DateTime::from(*timestamp); 706 | 707 | units.push(( 708 | def.clone(), 709 | UnitStatus { 710 | name: name.clone(), 711 | kind: def.service.kind, 712 | state: UnitState::Failed, 713 | pid: DisplayedOption(None), 714 | timestamp: DisplayedOption(Some(local.to_string())), 715 | }, 716 | )) 717 | } else if let Some(timestamp) = terminated.get(name) { 718 | let local: DateTime = DateTime::from(*timestamp); 719 | 720 | units.push(( 721 | def.clone(), 722 | UnitStatus { 723 | name: name.clone(), 724 | kind: def.service.kind, 725 | state: UnitState::Terminated, 726 | pid: DisplayedOption(None), 727 | timestamp: DisplayedOption(Some(local.to_string())), 728 | }, 729 | )) 730 | } else { 731 | units.push(( 732 | def.clone(), 733 | UnitStatus { 734 | name: name.clone(), 735 | kind: def.service.kind, 736 | state: UnitState::Stopped, 737 | pid: DisplayedOption(None), 738 | timestamp: DisplayedOption(None), 739 | }, 740 | )) 741 | } 742 | } 743 | 744 | ProcessManagerStatus(units) 745 | } 746 | } 747 | -------------------------------------------------------------------------------- /wpm/src/process_manager_status.rs: -------------------------------------------------------------------------------- 1 | use crate::process_manager::ProcessManagerError; 2 | use crate::unit::Definition; 3 | use crate::unit::Healthcheck; 4 | use crate::unit_status::UnitState; 5 | use crate::unit_status::UnitStatus; 6 | use tabled::Table; 7 | 8 | pub struct ProcessManagerStatus(pub Vec<(Definition, UnitStatus)>); 9 | 10 | impl ProcessManagerStatus { 11 | pub fn as_table(&self) -> String { 12 | Table::new(self.0.iter().map(|(_, status)| status).collect::>()).to_string() 13 | } 14 | 15 | pub fn unit_status(&self, name: &str) -> Result { 16 | match self.0.iter().find(|(def, _status)| def.unit.name == name) { 17 | None => Ok(format!("Unregistered unit: {name}")), 18 | Some((definition, status)) => { 19 | let log_path = definition.log_path(); 20 | let mut output = Vec::new(); 21 | 22 | match status.state { 23 | UnitState::Running => { 24 | output.append(&mut vec![ 25 | format!("● Status of {name}:"), 26 | format!(" Kind: {}", definition.service.kind), 27 | format!(" State: Running since {}", status.timestamp.to_string()), 28 | format!(" PID: {}", status.pid), 29 | format!(" Log: {}", log_path.to_string_lossy()), 30 | ]); 31 | } 32 | UnitState::Stopped => { 33 | output.append(&mut vec![ 34 | format!("● Status of {name}:"), 35 | format!(" Kind: {}", definition.service.kind), 36 | " State: Stopped".to_string(), 37 | format!(" Log file: {}", log_path.to_string_lossy()), 38 | ]); 39 | } 40 | UnitState::Completed => { 41 | output.append(&mut vec![ 42 | format!("● Status of {name}:"), 43 | format!(" Kind: {}", definition.service.kind), 44 | format!(" State: Completed at {}", status.timestamp), 45 | format!(" Log file: {}", log_path.to_string_lossy()), 46 | ]); 47 | } 48 | UnitState::Failed => { 49 | output.append(&mut vec![ 50 | format!("● Status of {name}:"), 51 | format!(" Kind: {}", definition.service.kind), 52 | format!(" State: Failed at {}", status.timestamp), 53 | format!(" Log file: {}", log_path.to_string_lossy()), 54 | ]); 55 | } 56 | UnitState::Terminated => { 57 | output.append(&mut vec![ 58 | format!("● Status of {name}:"), 59 | format!(" Kind: {}", definition.service.kind), 60 | format!(" State: Terminated at {}", status.timestamp), 61 | format!(" Log file: {}", log_path.to_string_lossy()), 62 | ]); 63 | } 64 | } 65 | 66 | if let Some(args) = &definition.service.exec_start.arguments { 67 | let arguments = args.join(" "); 68 | let arguments = arguments.replace("/", "\\"); 69 | output.push(format!( 70 | " ExecStart: {} {arguments}", 71 | definition 72 | .service 73 | .exec_start 74 | .executable 75 | .to_string() 76 | .replace("/", "\\") 77 | )); 78 | } else { 79 | output.push(format!( 80 | " ExecStart: {}", 81 | definition 82 | .service 83 | .exec_start 84 | .executable 85 | .to_string() 86 | .replace("/", "\\") 87 | )); 88 | } 89 | 90 | match &definition.service.healthcheck { 91 | Some(Healthcheck::Command(command)) => { 92 | if let Some(args) = &command.arguments { 93 | let arguments = args.join(" "); 94 | let arguments = arguments.replace("/", "\\"); 95 | output.push(format!( 96 | " Healthcheck: {} {arguments}", 97 | command.executable.to_string_lossy() 98 | )); 99 | } else { 100 | output.push(format!( 101 | " Healthcheck: {}", 102 | command.executable.to_string_lossy() 103 | )); 104 | } 105 | } 106 | Some(Healthcheck::Process(healthcheck)) => { 107 | let seconds = healthcheck.delay_sec; 108 | match &healthcheck.target { 109 | None => { 110 | output.push(format!( 111 | " Healthcheck: Liveness check after {seconds}s", 112 | )); 113 | } 114 | Some(target) => { 115 | output.push(format!( 116 | " Healthcheck: Liveness check for {} after {seconds}s", 117 | target.display() 118 | )); 119 | } 120 | } 121 | } 122 | None => {} 123 | } 124 | 125 | if let Some(shutdowns) = &definition.service.exec_stop { 126 | output.push(" ExecStop:".to_string()); 127 | for command in shutdowns { 128 | if let Some(args) = &command.arguments { 129 | let arguments = args.join(" "); 130 | let arguments = arguments.replace("/", "\\"); 131 | output.push(format!(" {} {arguments}", command.executable)); 132 | } else { 133 | output.push(format!(" {}", command.executable)); 134 | } 135 | } 136 | } 137 | 138 | if let Some(shutdowns) = &definition.service.exec_stop_post { 139 | output.push(" ExecStopPost:".to_string()); 140 | for command in shutdowns { 141 | if let Some(args) = &command.arguments { 142 | let arguments = args.join(" "); 143 | let arguments = arguments.replace("/", "\\"); 144 | output.push(format!(" {} {arguments}", command.executable,)); 145 | } else { 146 | output.push(format!(" {}", command.executable)); 147 | } 148 | } 149 | } 150 | 151 | if let Some(environment) = &definition.service.environment { 152 | let vars = environment 153 | .iter() 154 | .map(|(a, b)| format!("{a}={b}")) 155 | .collect::>(); 156 | 157 | output.push(" Environment (Service):".to_string()); 158 | for var in vars { 159 | let var = var.replace("/", "\\"); 160 | output.push(format!(" {var}")); 161 | } 162 | } 163 | 164 | if let Some(environment) = &definition.service.exec_start.environment { 165 | let vars = environment 166 | .iter() 167 | .map(|(a, b)| format!("{a}={b}")) 168 | .collect::>(); 169 | 170 | output.push(" Environment (ExecStart):".to_string()); 171 | for var in vars { 172 | let var = var.replace("/", "\\"); 173 | output.push(format!(" {var}")); 174 | } 175 | } 176 | 177 | if let Some(requires) = &definition.unit.requires { 178 | let requires = requires.join(" "); 179 | output.push(format!(" Requires: {requires}",)); 180 | } 181 | 182 | let log_contents = std::fs::read_to_string(log_path)?; 183 | let lines = log_contents 184 | .lines() 185 | .filter(|line| !line.is_empty()) 186 | .collect::>(); 187 | let last_ten_lines = lines.iter().rev().take(10).rev().collect::>(); 188 | 189 | if !last_ten_lines.is_empty() { 190 | output.push("\nRecent logs:".to_string()); 191 | for line in last_ten_lines { 192 | output.push(format!(" {line}")); 193 | } 194 | } 195 | 196 | Ok(output.join("\n")) 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /wpm/src/unit_status.rs: -------------------------------------------------------------------------------- 1 | use crate::unit::ServiceKind; 2 | use std::fmt::Display; 3 | use std::fmt::Formatter; 4 | use tabled::Tabled; 5 | 6 | #[derive(Tabled)] 7 | pub struct UnitStatus { 8 | pub name: String, 9 | pub kind: ServiceKind, 10 | pub state: UnitState, 11 | pub pid: DisplayedOption, 12 | pub timestamp: DisplayedOption, 13 | } 14 | 15 | #[derive(Tabled)] 16 | pub enum UnitState { 17 | Running, 18 | Stopped, 19 | Completed, 20 | Failed, 21 | Terminated, 22 | } 23 | 24 | impl Display for UnitState { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | UnitState::Running => write!(f, "Running"), 28 | UnitState::Stopped => write!(f, "Stopped"), 29 | UnitState::Completed => write!(f, "Completed"), 30 | UnitState::Failed => write!(f, "Failed"), 31 | UnitState::Terminated => write!(f, "Terminated"), 32 | } 33 | } 34 | } 35 | 36 | pub struct DisplayedOption(pub Option); 37 | 38 | impl Display for DisplayedOption { 39 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 40 | match &self.0 { 41 | None => write!(f, ""), 42 | Some(inner) => write!(f, "{inner}"), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /wpmctl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wpmctl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wpm = { path = "../wpm" } 8 | 9 | chrono = { workspace = true } 10 | clap = { workspace = true } 11 | fs-tail = "0.1" 12 | interprocess = { workspace = true } 13 | shadow-rs = { workspace = true } 14 | 15 | [build-dependencies] 16 | shadow-rs = { workspace = true } 17 | -------------------------------------------------------------------------------- /wpmctl/build.rs: -------------------------------------------------------------------------------- 1 | use shadow_rs::ShadowBuilder; 2 | fn main() { 3 | ShadowBuilder::builder().build().unwrap(); 4 | } 5 | -------------------------------------------------------------------------------- /wpmctl/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use chrono::Utc; 4 | use clap::CommandFactory; 5 | use clap::Parser; 6 | use fs_tail::TailedFile; 7 | use interprocess::local_socket::traits::Listener; 8 | use interprocess::local_socket::GenericNamespaced; 9 | use interprocess::local_socket::ListenerOptions; 10 | use interprocess::local_socket::ToNsName; 11 | use std::fs::File; 12 | use std::io::BufRead; 13 | use std::io::BufReader; 14 | use std::io::Read; 15 | use std::path::PathBuf; 16 | use wpm::communication::send_message; 17 | use wpm::process_manager::ProcessManager; 18 | use wpm::unit::Definition; 19 | use wpm::unit::Executable; 20 | use wpm::unit::ScoopExecutable; 21 | use wpm::wpm_data_dir; 22 | use wpm::wpm_units_dir; 23 | use wpm::SocketMessage; 24 | 25 | shadow_rs::shadow!(build); 26 | 27 | #[derive(Parser)] 28 | #[clap(author, about, version = build::CLAP_LONG_VERSION)] 29 | struct Opts { 30 | #[clap(subcommand)] 31 | subcmd: SubCommand, 32 | } 33 | 34 | macro_rules! gen_unit_subcommands { 35 | // SubCommand Pattern 36 | ( $( $name:ident ),+ $(,)? ) => { 37 | $( 38 | #[derive(clap::Parser)] 39 | pub struct $name { 40 | /// Target units 41 | units: Vec, 42 | } 43 | )+ 44 | }; 45 | } 46 | 47 | gen_unit_subcommands! { 48 | Start, 49 | Stop, 50 | Reset, 51 | } 52 | 53 | #[derive(clap::Parser)] 54 | pub struct Restart { 55 | /// Target units 56 | units: Vec, 57 | #[clap(long, short = 'd', action)] 58 | /// Restart dependents of target units 59 | with_dependents: bool, 60 | } 61 | 62 | #[derive(Parser)] 63 | struct Status { 64 | /// Target unit 65 | unit: String, 66 | } 67 | 68 | #[derive(Parser)] 69 | struct Log { 70 | /// Target unit 71 | unit: Option, 72 | } 73 | 74 | #[derive(Parser)] 75 | struct Examplegen { 76 | /// Target path 77 | path: Option, 78 | } 79 | 80 | #[derive(Parser)] 81 | struct Reload { 82 | /// Target path 83 | path: Option, 84 | } 85 | 86 | #[derive(Parser)] 87 | struct Rebuild { 88 | /// Target path 89 | path: Option, 90 | } 91 | 92 | #[derive(Parser)] 93 | enum SubCommand { 94 | /// Generate a CLI command documentation 95 | #[clap(hide = true)] 96 | Docgen, 97 | /// Generate a JSON schema for wpm units 98 | #[clap(hide = true)] 99 | Schemagen, 100 | /// Generate some example wpm units 101 | #[clap(hide = true)] 102 | Examplegen(Examplegen), 103 | /// Start units 104 | #[clap(arg_required_else_help = true)] 105 | Start(Start), 106 | /// Stop units 107 | #[clap(arg_required_else_help = true)] 108 | Stop(Stop), 109 | /// Restart units 110 | #[clap(arg_required_else_help = true)] 111 | Restart(Restart), 112 | /// Reset units 113 | #[clap(arg_required_else_help = true)] 114 | Reset(Reset), 115 | /// Show the state of the process manager 116 | State, 117 | /// Show status of a unit 118 | #[clap(arg_required_else_help = true)] 119 | Status(Status), 120 | /// Reload all unit definitions 121 | Reload(Reload), 122 | /// Tail the logs of a unit or of the process manager 123 | Log(Log), 124 | /// Ensure all remote dependencies are downloaded and built 125 | Rebuild(Rebuild), 126 | /// Print the path to the wpm global unit definition directory 127 | Units, 128 | } 129 | 130 | fn listen_for_response() -> Result> { 131 | let name = "wpmctl.sock".to_ns_name::()?; 132 | let opts = ListenerOptions::new().name(name); 133 | 134 | let listener = match opts.create_sync() { 135 | Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => { 136 | println!("{error}"); 137 | return Err(error.into()); 138 | } 139 | x => x?, 140 | }; 141 | 142 | let mut buf = String::new(); 143 | let stream = match listener.accept() { 144 | Ok(connection) => connection, 145 | Err(error) => { 146 | println!("{error}"); 147 | return Err(error.into()); 148 | } 149 | }; 150 | 151 | let mut receiver = BufReader::new(&stream); 152 | 153 | receiver.read_to_string(&mut buf)?; 154 | 155 | Ok(buf) 156 | } 157 | 158 | fn main() -> Result<(), Box> { 159 | let opts: Opts = Opts::parse(); 160 | match opts.subcmd { 161 | SubCommand::Docgen => { 162 | let mut cli = Opts::command(); 163 | let subcommands = cli.get_subcommands_mut(); 164 | std::fs::create_dir_all("docs/cli")?; 165 | 166 | let ignore = ["docgen", "schemagen", "examplegen"]; 167 | 168 | for cmd in subcommands { 169 | let name = cmd.get_name().to_string(); 170 | if !ignore.contains(&name.as_str()) { 171 | let help_text = cmd.render_long_help().to_string(); 172 | let help_text = help_text.replace("Usage: ", "Usage: wpmctl.exe "); 173 | let outpath = format!("docs/cli/{name}.md"); 174 | let markdown = format!("# {name}\n\n```\n{help_text}\n```"); 175 | std::fs::write(outpath, markdown)?; 176 | println!(" - cli/{name}.md"); 177 | } 178 | } 179 | } 180 | SubCommand::Schemagen => { 181 | println!("{}", Definition::schemagen()); 182 | } 183 | SubCommand::Examplegen(args) => { 184 | Definition::examplegen(args.path); 185 | } 186 | SubCommand::Start(args) => { 187 | send_message("wpmd.sock", SocketMessage::Start(args.units))?; 188 | } 189 | SubCommand::Stop(args) => { 190 | send_message("wpmd.sock", SocketMessage::Stop(args.units))?; 191 | } 192 | SubCommand::Restart(args) => { 193 | if args.with_dependents { 194 | send_message( 195 | "wpmd.sock", 196 | SocketMessage::RestartWithDependents(args.units), 197 | )?; 198 | } else { 199 | send_message("wpmd.sock", SocketMessage::Restart(args.units))?; 200 | } 201 | } 202 | SubCommand::Reset(args) => { 203 | send_message("wpmd.sock", SocketMessage::Reset(args.units))?; 204 | } 205 | SubCommand::Status(args) => { 206 | send_message("wpmd.sock", SocketMessage::Status(args.unit.clone()))?; 207 | let response = listen_for_response()?; 208 | println!("{}", response); 209 | } 210 | SubCommand::State => { 211 | send_message("wpmd.sock", SocketMessage::State)?; 212 | println!("{}", listen_for_response()?); 213 | } 214 | SubCommand::Reload(args) => { 215 | send_message("wpmd.sock", SocketMessage::Reload(args.path))?; 216 | } 217 | SubCommand::Log(args) => match args.unit { 218 | None => { 219 | let timestamp = Utc::now().format("%Y-%m-%d").to_string(); 220 | let color_log = std::env::temp_dir().join(format!("wpmd.log.{timestamp}")); 221 | let file = TailedFile::new(File::open(color_log)?); 222 | let locked = file.lock(); 223 | #[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)] 224 | for line in locked.lines().flatten() { 225 | println!("{line}"); 226 | } 227 | } 228 | Some(unit) => { 229 | let dir = wpm_data_dir().join("logs"); 230 | let file = File::open(dir.join(format!("{}.log", unit))).unwrap(); 231 | 232 | let file = TailedFile::new(file); 233 | let locked = file.lock(); 234 | #[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)] 235 | for line in locked.lines().flatten() { 236 | println!("{line}"); 237 | } 238 | } 239 | }, 240 | SubCommand::Rebuild(args) => { 241 | let mut units = ProcessManager::retrieve_units(args.path)?; 242 | for definition in &mut units { 243 | let name = &definition.unit.name; 244 | let executable = &definition.service.exec_start.executable; 245 | let url = match executable { 246 | Executable::Remote(remote) => remote.url.to_string(), 247 | Executable::Scoop(scoop) => match scoop { 248 | ScoopExecutable::Package(_) => continue, 249 | ScoopExecutable::Manifest(manifest) => manifest.manifest.to_string(), 250 | }, 251 | _ => continue, 252 | }; 253 | 254 | let path = executable.cached_executable_path()?; 255 | if !path.is_file() { 256 | println!("[{name}]: Downloading from {url}"); 257 | executable.download_remote_executable()?; 258 | } else { 259 | println!("[{name}]: Already exists at {}", path.display()); 260 | } 261 | 262 | if definition.resources.is_some() { 263 | println!("[{name}]: Resolving remote resources"); 264 | definition.resolve_resources()?; 265 | } 266 | } 267 | } 268 | SubCommand::Units => { 269 | println!("{}", wpm_units_dir().display()); 270 | } 271 | } 272 | 273 | Ok(()) 274 | } 275 | -------------------------------------------------------------------------------- /wpmd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wpmd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wpm = { path = "../wpm" } 8 | 9 | clap = { workspace = true } 10 | color-eyre = { workspace = true } 11 | ctrlc = "3" 12 | interprocess = { workspace = true } 13 | parking_lot = { workspace = true } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | shadow-rs = { workspace = true } 17 | sysinfo = { workspace = true } 18 | thiserror = { workspace = true } 19 | tracing = { workspace = true } 20 | tracing-appender = "0.2" 21 | tracing-subscriber = { workspace = true } 22 | 23 | [build-dependencies] 24 | shadow-rs = { workspace = true } 25 | -------------------------------------------------------------------------------- /wpmd/build.rs: -------------------------------------------------------------------------------- 1 | use shadow_rs::ShadowBuilder; 2 | fn main() { 3 | ShadowBuilder::builder().build().unwrap(); 4 | } 5 | -------------------------------------------------------------------------------- /wpmd/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | 3 | use clap::Parser; 4 | use interprocess::local_socket::traits::Listener; 5 | use interprocess::local_socket::GenericNamespaced; 6 | use interprocess::local_socket::ListenerOptions; 7 | use interprocess::local_socket::Stream; 8 | use interprocess::local_socket::ToNsName; 9 | use parking_lot::Mutex; 10 | use std::io::BufRead; 11 | use std::io::BufReader; 12 | use std::path::PathBuf; 13 | use std::process::exit; 14 | use std::sync::mpsc; 15 | use std::sync::Arc; 16 | use sysinfo::Process; 17 | use sysinfo::ProcessesToUpdate; 18 | use sysinfo::System; 19 | use thiserror::Error; 20 | use tracing_subscriber::layer::SubscriberExt; 21 | use tracing_subscriber::EnvFilter; 22 | use wpm::communication::send_str; 23 | use wpm::process_manager::ProcessManager; 24 | use wpm::process_manager::ProcessManagerError; 25 | use wpm::unit_status::UnitState; 26 | use wpm::SocketMessage; 27 | 28 | shadow_rs::shadow!(build); 29 | 30 | static SOCKET_NAME: &str = "wpmd.sock"; 31 | 32 | #[derive(Parser)] 33 | #[clap(author, about, version = build::CLAP_LONG_VERSION)] 34 | struct Args { 35 | /// Path to unit files (default: $Env:USERPROFILE/.config/wpm) 36 | path: Option, 37 | } 38 | 39 | #[derive(Error, Debug)] 40 | pub enum WpmdError { 41 | #[error(transparent)] 42 | ProcessManager(#[from] ProcessManagerError), 43 | #[error(transparent)] 44 | SerdeJson(#[from] serde_json::Error), 45 | #[error(transparent)] 46 | Io(#[from] std::io::Error), 47 | } 48 | 49 | fn main() -> Result<(), Box> { 50 | let args: Args = Args::parse(); 51 | 52 | if std::env::var("RUST_LIB_BACKTRACE").is_err() { 53 | std::env::set_var("RUST_LIB_BACKTRACE", "1"); 54 | } 55 | 56 | color_eyre::install()?; 57 | 58 | if std::env::var("RUST_LOG").is_err() { 59 | std::env::set_var("RUST_LOG", "info"); 60 | } 61 | 62 | println!("Thank you for using wpm!\n"); 63 | println!("# Commercial Use License"); 64 | println!("* View licensing options https://lgug2z.com/software/wpm - A commercial use license is required to use wpm at work"); 65 | println!("\n# Personal Use Sponsorship"); 66 | println!( 67 | "* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference" 68 | ); 69 | println!("* Leave a tip https://ko-fi.com/lgug2z - An alternative to GitHub Sponsors"); 70 | println!("\n# Community"); 71 | println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, get help"); 72 | println!( 73 | "* Subscribe to https://youtube.com/@LGUG2Z - Development videos, feature previews and release overviews" 74 | ); 75 | println!("\n# Documentation"); 76 | println!("* Read the docs https://lgug2z.github.io/wpm\n"); 77 | 78 | let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "wpmd_plaintext.log"); 79 | let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "wpmd.log"); 80 | let (non_blocking, _guard) = tracing_appender::non_blocking(appender); 81 | let (color_non_blocking, _color_guard) = tracing_appender::non_blocking(color_appender); 82 | 83 | tracing::subscriber::set_global_default( 84 | tracing_subscriber::fmt::Subscriber::builder() 85 | .with_env_filter(EnvFilter::from_default_env()) 86 | .finish() 87 | .with( 88 | tracing_subscriber::fmt::Layer::default() 89 | .with_writer(non_blocking) 90 | .with_ansi(false), 91 | ) 92 | .with( 93 | tracing_subscriber::fmt::Layer::default() 94 | .with_writer(color_non_blocking) 95 | .with_ansi(true), 96 | ), 97 | )?; 98 | 99 | if args.path.is_none() { 100 | let mut system = System::new_all(); 101 | system.refresh_processes(ProcessesToUpdate::All, true); 102 | let matched_procs: Vec<&Process> = system.processes_by_name("wpmd.exe".as_ref()).collect(); 103 | if matched_procs.len() > 1 { 104 | let mut len = matched_procs.len(); 105 | for proc in matched_procs { 106 | if let Some(executable_path) = proc.exe() { 107 | if executable_path.to_string_lossy().contains("shims") { 108 | len -= 1; 109 | } 110 | } 111 | } 112 | 113 | if len > 1 { 114 | tracing::error!("wpmd.exe is already running, please exit the existing process before starting a new one"); 115 | exit(1); 116 | } 117 | } 118 | } 119 | 120 | let process_manager = ProcessManager::init(args.path)?; 121 | 122 | let process_manager_arc = Arc::new(Mutex::new(process_manager)); 123 | let loop_arc = process_manager_arc.clone(); 124 | let ctrlc_arc = process_manager_arc.clone(); 125 | 126 | let name = SOCKET_NAME.to_ns_name::()?; 127 | let opts = ListenerOptions::new().name(name.clone()); 128 | 129 | let (tx, rx) = mpsc::channel::(); 130 | 131 | let listener = match opts.create_sync() { 132 | Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => { 133 | tracing::error!("failed to create listener: {error}"); 134 | return Err(error.into()); 135 | } 136 | x => x?, 137 | }; 138 | 139 | tracing::info!("listening on {SOCKET_NAME}"); 140 | 141 | std::thread::spawn(move || loop { 142 | let conn = match listener.accept() { 143 | Ok(connection) => connection, 144 | Err(error) => { 145 | tracing::error!("failed to accept incoming socket connection: {error}"); 146 | continue; 147 | } 148 | }; 149 | 150 | if let Ok(socket_message) = extract_socket_message(conn) { 151 | match tx.send(socket_message) { 152 | Ok(_) => { 153 | tracing::info!("successfully queued socket message"); 154 | } 155 | Err(_) => { 156 | tracing::warn!("failed to queue socket message"); 157 | } 158 | } 159 | } 160 | }); 161 | 162 | std::thread::spawn(move || { 163 | while let Ok(message) = rx.recv() { 164 | let pm = loop_arc.clone(); 165 | if let Err(error) = handle_socket_message(pm, message) { 166 | tracing::error!("{error}") 167 | } 168 | } 169 | }); 170 | 171 | let (ctrlc_sender, ctrlc_receiver) = mpsc::channel(); 172 | ctrlc::set_handler(move || { 173 | ctrlc_sender 174 | .send(()) 175 | .expect("could not send signal on ctrl-c channel"); 176 | })?; 177 | 178 | ctrlc_receiver 179 | .recv() 180 | .expect("could not receive signal on ctrl-c channel"); 181 | 182 | ctrlc_arc.lock().shutdown()?; 183 | 184 | Ok(()) 185 | } 186 | 187 | fn extract_socket_message(conn: Stream) -> Result { 188 | let mut conn = BufReader::new(&conn); 189 | let mut buf = String::new(); 190 | conn.read_line(&mut buf)?; 191 | 192 | match serde_json::from_str::(&buf) { 193 | Err(error) => { 194 | tracing::error!("failed to deserialize socket message: {error}"); 195 | Err(WpmdError::SerdeJson(error)) 196 | } 197 | Ok(socket_message) => { 198 | tracing::info!("received socket message: {socket_message:?}"); 199 | Ok(socket_message) 200 | } 201 | } 202 | } 203 | 204 | fn handle_socket_message( 205 | pm: Arc>, 206 | socket_message: SocketMessage, 207 | ) -> Result<(), WpmdError> { 208 | let mut pm = pm.lock(); 209 | 210 | match socket_message { 211 | SocketMessage::Start(arg) => { 212 | for name in arg { 213 | pm.start(&name)?; 214 | } 215 | } 216 | SocketMessage::Stop(arg) => { 217 | for name in arg { 218 | pm.stop(&name)?; 219 | } 220 | } 221 | SocketMessage::Restart(arg) => { 222 | for name in arg { 223 | if let Err(error) = pm.stop(&name) { 224 | tracing::warn!("{error}"); 225 | } 226 | 227 | pm.start(&name)?; 228 | } 229 | } 230 | SocketMessage::RestartWithDependents(arg) => { 231 | for name in arg { 232 | if let Err(error) = pm.stop(&name) { 233 | tracing::warn!("{error}"); 234 | } 235 | 236 | pm.start(&name)?; 237 | 238 | for dependent in pm.dependents(&name) { 239 | for (definition, status) in pm.state().0 { 240 | if definition.unit.name.eq(&dependent) 241 | && matches!(status.state, UnitState::Running) 242 | { 243 | tracing::info!("{dependent}: restarting as a dependent of {name}"); 244 | if let Err(error) = pm.stop(&dependent) { 245 | tracing::warn!("{error}"); 246 | } 247 | 248 | pm.start(&dependent)?; 249 | } 250 | } 251 | } 252 | } 253 | } 254 | SocketMessage::Status(arg) => { 255 | let status_message = pm.state().unit_status(&arg)?; 256 | send_str("wpmctl.sock", &status_message)?; 257 | } 258 | SocketMessage::State => { 259 | let table = format!("{}\n", pm.state().as_table()); 260 | send_str("wpmctl.sock", &table)?; 261 | } 262 | SocketMessage::Reload(arg) => { 263 | pm.load_units(arg)?; 264 | } 265 | SocketMessage::Reset(arg) => { 266 | for name in arg { 267 | pm.reset(&name); 268 | } 269 | } 270 | } 271 | 272 | Ok(()) 273 | } 274 | --------------------------------------------------------------------------------