├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | [](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 |
--------------------------------------------------------------------------------