uniservice 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. uniservice-1.4.0/PKG-INFO +346 -0
  2. uniservice-1.4.0/README.md +317 -0
  3. uniservice-1.4.0/pyproject.toml +69 -0
  4. uniservice-1.4.0/setup.cfg +4 -0
  5. uniservice-1.4.0/tests/test_cli.py +399 -0
  6. uniservice-1.4.0/tests/test_entrypoint.py +125 -0
  7. uniservice-1.4.0/tests/test_install_script.py +422 -0
  8. uniservice-1.4.0/tests/test_installation.py +166 -0
  9. uniservice-1.4.0/tests/test_linux_backend.py +474 -0
  10. uniservice-1.4.0/tests/test_live_integration.py +116 -0
  11. uniservice-1.4.0/tests/test_macos_backend.py +580 -0
  12. uniservice-1.4.0/tests/test_naming.py +82 -0
  13. uniservice-1.4.0/tests/test_packaging.py +121 -0
  14. uniservice-1.4.0/tests/test_platform_utils.py +91 -0
  15. uniservice-1.4.0/tests/test_process.py +74 -0
  16. uniservice-1.4.0/tests/test_scope.py +39 -0
  17. uniservice-1.4.0/tests/test_windows_backend.py +511 -0
  18. uniservice-1.4.0/uniservice.egg-info/PKG-INFO +346 -0
  19. uniservice-1.4.0/uniservice.egg-info/SOURCES.txt +35 -0
  20. uniservice-1.4.0/uniservice.egg-info/dependency_links.txt +1 -0
  21. uniservice-1.4.0/uniservice.egg-info/entry_points.txt +2 -0
  22. uniservice-1.4.0/uniservice.egg-info/requires.txt +8 -0
  23. uniservice-1.4.0/uniservice.egg-info/top_level.txt +1 -0
  24. uniservice-1.4.0/uniservice_lib/__init__.py +24 -0
  25. uniservice-1.4.0/uniservice_lib/backends/__init__.py +30 -0
  26. uniservice-1.4.0/uniservice_lib/backends/base.py +111 -0
  27. uniservice-1.4.0/uniservice_lib/backends/linux.py +270 -0
  28. uniservice-1.4.0/uniservice_lib/backends/macos.py +472 -0
  29. uniservice-1.4.0/uniservice_lib/backends/windows.py +426 -0
  30. uniservice-1.4.0/uniservice_lib/cli.py +382 -0
  31. uniservice-1.4.0/uniservice_lib/errors.py +36 -0
  32. uniservice-1.4.0/uniservice_lib/installation.py +268 -0
  33. uniservice-1.4.0/uniservice_lib/logging_utils.py +69 -0
  34. uniservice-1.4.0/uniservice_lib/naming.py +113 -0
  35. uniservice-1.4.0/uniservice_lib/platform_utils.py +96 -0
  36. uniservice-1.4.0/uniservice_lib/process.py +81 -0
  37. uniservice-1.4.0/uniservice_lib/scope.py +55 -0
@@ -0,0 +1,346 @@
1
+ Metadata-Version: 2.4
2
+ Name: uniservice
3
+ Version: 1.4.0
4
+ Summary: Cross-platform service manager built on systemd, launchd and Scheduled Tasks
5
+ Author-email: Kevin Huang <kevinwong.yiming@outlook.com>
6
+ Project-URL: Homepage, https://github.com/kevinhuang001/uniservice
7
+ Project-URL: Issues, https://github.com/kevinhuang001/uniservice/issues
8
+ Keywords: systemd,launchd,scheduled-tasks,service-manager,daemon
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.4; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
26
+ Requires-Dist: ruff>=0.5; extra == "dev"
27
+ Provides-Extra: build
28
+ Requires-Dist: pyinstaller>=6.0; extra == "build"
29
+
30
+ # uniservice
31
+
32
+ [中文说明](README.zh.md) · [![CI](https://github.com/kevinhuang001/uniservice/actions/workflows/ci.yml/badge.svg)](https://github.com/kevinhuang001/uniservice/actions/workflows/ci.yml)
33
+
34
+ Cross-platform service manager that delegates long-running processes, autostart and supervision to native OS mechanisms:
35
+
36
+ - Linux: systemd (user/system)
37
+ - macOS: launchd (LaunchAgents/LaunchDaemons)
38
+ - Windows: Scheduled Tasks (admin required)
39
+
40
+ More platform details: [details.md](details.md) ([中文](details.zh.md))
41
+
42
+ Requires **Python 3.10+**. No third-party runtime dependencies.
43
+
44
+ ## Install
45
+
46
+ Every release publishes **two artifacts**, and the installer lets you pick one:
47
+
48
+ | | Artifact | Size | Requires |
49
+ | --- | --- | --- | --- |
50
+ | **default** | portable zipapp | ~30 KB | Python 3.10+ on the machine |
51
+ | `--binary` | standalone binary | ~24 MB | nothing (bundles its own CPython) |
52
+
53
+ **The zipapp is the recommended default**: one small file that is byte-identical on every
54
+ platform, and it runs on the Python you already have. The standalone binary exists for machines
55
+ with no Python environment; because PyInstaller cannot cross-compile, it is built and published
56
+ separately for each OS and CPU architecture.
57
+
58
+ Either way the command lands in `/usr/local/bin/uniservice` and is recorded in
59
+ `/usr/local/lib/uniservice/manifest`. `/usr/local/bin` is already on `PATH` for every account, so
60
+ the installer never edits a shell profile, and one installation serves both scopes:
61
+ `uniservice ...` for per-user services and `sudo uniservice ...` for system services.
62
+
63
+ Writing to `/usr/local` needs root, and the installer does not silently fall back somewhere else:
64
+ **run it with `sudo` or it stops with an error**. No `~/.local` install, no PATH edits.
65
+
66
+ ### One-liner
67
+
68
+ ```bash
69
+ # macOS / Linux — the recommended portable zipapp (needs Python 3.10+)
70
+ curl -fsSL https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install.sh | sudo bash
71
+
72
+ # ... or the standalone binary, which needs no Python at all
73
+ curl -fsSL https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install.sh | sudo bash -s -- --binary
74
+ ```
75
+
76
+ ```powershell
77
+ # Windows — the recommended portable zipapp (needs Python 3.10+)
78
+ iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 | iex
79
+
80
+ # ... or the standalone .exe, which needs no Python at all
81
+ $env:UNISERVICE_BINARY = 1; iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 | iex
82
+ ```
83
+
84
+ Piping into `iex` leaves no file on disk, so the artifact is selected with the
85
+ `UNISERVICE_BINARY` environment variable. If you would rather use the switch, download the script
86
+ first:
87
+
88
+ ```powershell
89
+ iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 -OutFile install-windows.ps1
90
+ ./install-windows.ps1 -Binary
91
+ ```
92
+
93
+ The same `UNISERVICE_BINARY=1` environment variable works for `install.sh`.
94
+
95
+ ### Installer options
96
+
97
+ ```
98
+ --binary install the standalone binary instead of the zipapp
99
+ --prefix DIR install under DIR instead of /usr/local
100
+ (for packaging and tests; needs no elevated privileges)
101
+ --version TAG install a specific release, e.g. --version v1.2.0
102
+ --sha256 HEX verify the artifact against this digest
103
+ (by default the digest published in SHA256SUMS is used)
104
+ --from FILE install a local zipapp or binary instead of downloading
105
+ --uninstall remove a previous installation, using its manifest
106
+ ```
107
+
108
+ ```bash
109
+ sudo ./install.sh # recommended: the zipapp
110
+ sudo ./install.sh --binary # no Python needed
111
+ sudo ./install.sh --version v1.2.0 # pin a release
112
+ ./install.sh --prefix /tmp/uniservice-test # unprivileged, for packaging/tests
113
+ sudo ./install.sh --uninstall
114
+ ```
115
+
116
+ The zipapp build is byte-for-byte reproducible, so pinning a `--sha256` gives a verifiable
117
+ install. When a release has no asset for your platform the installer says so and points at the
118
+ zipapp alternative.
119
+
120
+ ### Release assets
121
+
122
+ | Asset | Notes |
123
+ | --- | --- |
124
+ | `uniservice` | the portable zipapp (recommended) |
125
+ | `uniservice-linux-x86_64`, `uniservice-linux-aarch64` | standalone binaries |
126
+ | `uniservice-macos-arm64`, `uniservice-macos-x86_64` | standalone binaries |
127
+ | `uniservice-windows-x86_64.exe` | standalone binary |
128
+ | `uniservice-<version>-py3-none-any.whl`, `.tar.gz` | the same code as a Python package |
129
+ | `SHA256SUMS` | digests for everything above |
130
+
131
+ ### Verify the checksum yourself
132
+
133
+ ```bash
134
+ base=https://github.com/kevinhuang001/uniservice/releases/latest/download
135
+ curl -fsSLO "$base/uniservice" && curl -fsSLO "$base/SHA256SUMS"
136
+ grep ' uniservice$' SHA256SUMS | sha256sum -c -
137
+ sudo install -m 0755 uniservice /usr/local/bin/uniservice
138
+ ```
139
+
140
+ ### Windows notes
141
+
142
+ The default (zipapp) layout installs `uniservice.pyz` plus a `uniservice.cmd` shim into
143
+ `%LOCALAPPDATA%\uniservice\bin` and adds it to your user `PATH` and PowerShell profile.
144
+ `-Binary` installs `uniservice.exe` there instead, with no shim and no Python requirement.
145
+ `-Uninstall` reverses either.
146
+
147
+ Reopen the terminal, then:
148
+
149
+ ```bash
150
+ uniservice --help
151
+ ```
152
+
153
+ ## Usage
154
+
155
+ ### Scope (macOS/Linux)
156
+
157
+ The scope is derived from your privileges, not from a flag:
158
+
159
+ | How you run it | Scope | Definitions live in |
160
+ | --- | --- | --- |
161
+ | `uniservice ...` | user | `~/.config/systemd/user/`, `~/Library/LaunchAgents/` |
162
+ | `sudo uniservice ...` | system | `/etc/systemd/system/`, `/Library/LaunchDaemons/` |
163
+
164
+ Because the installer puts the command in `/usr/local/bin` (on every account's `PATH`), both
165
+ forms work with no further setup.
166
+
167
+ Two things change under `sudo`:
168
+
169
+ - the command after `--` is resolved with **root's** `PATH`, so `python3` may
170
+ resolve to `/usr/bin/python3` instead of your conda/venv copy — pass an
171
+ absolute path when that matters;
172
+ - the definition and its logs belong to root (`/root/.uniservice/logs/`).
173
+
174
+ On Windows there is no `sudo`: `uniservice list` works in any shell, every other
175
+ command needs an **Administrator** PowerShell/CMD.
176
+
177
+ ### Add
178
+
179
+ ```bash
180
+ uniservice add demo --workdir /tmp -- python3 -m http.server 8000
181
+ ```
182
+
183
+ - If the executable is not an absolute path, uniservice will try to resolve it from PATH and print a WARNING.
184
+ - If `--workdir` is not provided, the command runs in the current directory.
185
+ - If a service with the same name already exists, uniservice will ask whether to overwrite it.
186
+ - `add` performs `enable` + `start`.
187
+
188
+ Service names must not contain `/`, `\`, control characters or NUL, must not be `.`/`..`, and must not start with `-`
189
+ (those characters would escape the definition directory or break the native definition format).
190
+
191
+ ### List
192
+
193
+ ```bash
194
+ uniservice list
195
+ ```
196
+
197
+ Output is TSV (tab-separated), sorted by name:
198
+
199
+ | Column | Meaning |
200
+ | --- | --- |
201
+ | `NAME` | service name |
202
+ | `ENABLED` | starts automatically: `yes` / `no` / `?` |
203
+ | `RUNNING` | currently running: `yes` / `no` / `?` |
204
+
205
+ `?` means the platform did not give a definitive answer (for example `systemctl` is missing, the launchd override
206
+ query failed, or a unit is in a transitional state). It is never a guess. Names containing control characters are
207
+ sanitised and duplicate rows are collapsed, so the output always stays valid TSV.
208
+
209
+ ### Control
210
+
211
+ ```bash
212
+ uniservice enable demo
213
+ uniservice start demo
214
+ uniservice stop demo
215
+ uniservice disable demo
216
+ ```
217
+
218
+ ### Status and logs
219
+
220
+ ```bash
221
+ uniservice status demo
222
+ uniservice logs demo --lines 200
223
+ uniservice logs demo --follow # or -f
224
+ ```
225
+
226
+ ### Remove
227
+
228
+ ```bash
229
+ uniservice remove demo
230
+ ```
231
+
232
+ `remove` performs `stop` + `disable` before deleting the definition.
233
+
234
+ ### Cat
235
+
236
+ ```bash
237
+ uniservice cat demo
238
+ ```
239
+
240
+ Prints an equivalent `uniservice add ...` command for the service.
241
+
242
+ ## Logging
243
+
244
+ - Console: WARNING and above
245
+ - File: DEBUG and above
246
+ - Log file:
247
+ - macOS/Linux: `~/.uniservice/logs/uniservice.log`
248
+ - Windows: `%LOCALAPPDATA%\uniservice\logs\uniservice.log`
249
+
250
+ ## Project layout
251
+
252
+ ```
253
+ uniservice executable entry point (thin launcher, and what the zipapp runs)
254
+ uniservice_lib/
255
+ cli.py argument parsing and command dispatch
256
+ scope.py user vs. system scope
257
+ naming.py service-name validation and native definition names
258
+ process.py subprocess helpers
259
+ platform_utils.py platform and privilege detection
260
+ logging_utils.py console + file logging
261
+ errors.py exception hierarchy
262
+ backends/
263
+ __init__.py backend selection
264
+ base.py Backend interface, ServiceInfo, state parsing
265
+ linux.py systemd units
266
+ macos.py launchd jobs
267
+ windows.py Scheduled Tasks
268
+ scripts/build_zipapp.py builds the portable zipapp (the recommended artifact)
269
+ scripts/build_binary.py builds the standalone binary (PyInstaller, per platform)
270
+ packaging/entrypoint.py PyInstaller entry point
271
+ install.sh macOS/Linux installer (artifact choice, manifest, --uninstall)
272
+ install-windows.ps1 Windows installer (zipapp by default, -Binary for the .exe)
273
+ tests/ pytest suite (unit, end-to-end, installer, opt-in integration)
274
+ .github/workflows/ CI (3 platforms + 5 binary targets) and the release workflow
275
+ ```
276
+
277
+ Each backend implements the same `Backend` interface, so adding a platform means adding one module and registering it
278
+ in `uniservice_lib/backends/__init__.py`.
279
+
280
+ ## Development
281
+
282
+ ```bash
283
+ python -m pip install -e ".[dev]"
284
+
285
+ ruff check . # lint
286
+ ruff format --check . # formatting
287
+ python -m pytest # unit + end-to-end tests
288
+ ```
289
+
290
+ Build both artifacts and try the installer against a throwaway prefix:
291
+
292
+ ```bash
293
+ python scripts/build_zipapp.py --output dist/uniservice # ~30 KB, needs Python
294
+ python -m pip install -e ".[build]"
295
+ python scripts/build_binary.py --output-dir dist # ~24 MB, self-contained
296
+
297
+ ./install.sh --prefix /tmp/uniservice-test --from dist/uniservice
298
+ /tmp/uniservice-test/bin/uniservice --version
299
+ ./install.sh --uninstall --prefix /tmp/uniservice-test
300
+ ```
301
+
302
+ The zipapp build is deterministic, so `pytest tests/test_packaging.py tests/test_install_script.py` can compare a
303
+ locally built artifact against the digest the installer computes. The installer tests serve a fake release over
304
+ `file://`, so they exercise the download, the asset selection and the checksum verification without the network.
305
+
306
+ The default suite mocks the native tools, so it runs on every platform. Opt-in integration tests drive the real
307
+ service manager of the host:
308
+
309
+ ```bash
310
+ UNISERVICE_RUN_INTEGRATION=1 python -m pytest -m integration -v
311
+ ```
312
+
313
+ CI runs lint, shellcheck/PowerShell checks, the reproducible zipapp build, the full test suite on Ubuntu, macOS and
314
+ Windows, and builds the standalone binary for five targets. Pushing a `v*` tag runs
315
+ `.github/workflows/release.yml`, which publishes the wheel, the sdist, the zipapp, every platform binary and
316
+ `SHA256SUMS` as a GitHub release.
317
+
318
+ ## Uninstall
319
+
320
+ `uniservice` removes itself:
321
+
322
+ ```bash
323
+ sudo uniservice uninstall # or: uniservice uninstall, for a --prefix install
324
+ uniservice uninstall --dry-run # show what would be removed first
325
+ ```
326
+
327
+ It reads the manifest its installer wrote (`/usr/local/lib/uniservice/manifest`), deletes exactly
328
+ those files and prunes the directories that become empty. It never added a `PATH` line, so there is
329
+ nothing else to clean up. On Windows the running `.exe` cannot delete itself, so the command hands
330
+ that last file to a short-lived helper and it disappears a moment after the command returns.
331
+
332
+ **Your services are untouched** - only the command is removed. Delete the services you no longer
333
+ want first, while the command still exists:
334
+
335
+ ```bash
336
+ uniservice list
337
+ uniservice remove <name>
338
+ ```
339
+
340
+ If the command is already broken or gone, the installer can still clean up after it:
341
+
342
+ ```bash
343
+ sudo ./install.sh --uninstall # macOS/Linux, same manifest
344
+ ./install.sh --uninstall --prefix DIR # if you installed to a custom prefix
345
+ ./install-windows.ps1 -Uninstall # Windows
346
+ ```
@@ -0,0 +1,317 @@
1
+ # uniservice
2
+
3
+ [中文说明](README.zh.md) · [![CI](https://github.com/kevinhuang001/uniservice/actions/workflows/ci.yml/badge.svg)](https://github.com/kevinhuang001/uniservice/actions/workflows/ci.yml)
4
+
5
+ Cross-platform service manager that delegates long-running processes, autostart and supervision to native OS mechanisms:
6
+
7
+ - Linux: systemd (user/system)
8
+ - macOS: launchd (LaunchAgents/LaunchDaemons)
9
+ - Windows: Scheduled Tasks (admin required)
10
+
11
+ More platform details: [details.md](details.md) ([中文](details.zh.md))
12
+
13
+ Requires **Python 3.10+**. No third-party runtime dependencies.
14
+
15
+ ## Install
16
+
17
+ Every release publishes **two artifacts**, and the installer lets you pick one:
18
+
19
+ | | Artifact | Size | Requires |
20
+ | --- | --- | --- | --- |
21
+ | **default** | portable zipapp | ~30 KB | Python 3.10+ on the machine |
22
+ | `--binary` | standalone binary | ~24 MB | nothing (bundles its own CPython) |
23
+
24
+ **The zipapp is the recommended default**: one small file that is byte-identical on every
25
+ platform, and it runs on the Python you already have. The standalone binary exists for machines
26
+ with no Python environment; because PyInstaller cannot cross-compile, it is built and published
27
+ separately for each OS and CPU architecture.
28
+
29
+ Either way the command lands in `/usr/local/bin/uniservice` and is recorded in
30
+ `/usr/local/lib/uniservice/manifest`. `/usr/local/bin` is already on `PATH` for every account, so
31
+ the installer never edits a shell profile, and one installation serves both scopes:
32
+ `uniservice ...` for per-user services and `sudo uniservice ...` for system services.
33
+
34
+ Writing to `/usr/local` needs root, and the installer does not silently fall back somewhere else:
35
+ **run it with `sudo` or it stops with an error**. No `~/.local` install, no PATH edits.
36
+
37
+ ### One-liner
38
+
39
+ ```bash
40
+ # macOS / Linux — the recommended portable zipapp (needs Python 3.10+)
41
+ curl -fsSL https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install.sh | sudo bash
42
+
43
+ # ... or the standalone binary, which needs no Python at all
44
+ curl -fsSL https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install.sh | sudo bash -s -- --binary
45
+ ```
46
+
47
+ ```powershell
48
+ # Windows — the recommended portable zipapp (needs Python 3.10+)
49
+ iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 | iex
50
+
51
+ # ... or the standalone .exe, which needs no Python at all
52
+ $env:UNISERVICE_BINARY = 1; iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 | iex
53
+ ```
54
+
55
+ Piping into `iex` leaves no file on disk, so the artifact is selected with the
56
+ `UNISERVICE_BINARY` environment variable. If you would rather use the switch, download the script
57
+ first:
58
+
59
+ ```powershell
60
+ iwr -useb https://raw.githubusercontent.com/kevinhuang001/uniservice/main/install-windows.ps1 -OutFile install-windows.ps1
61
+ ./install-windows.ps1 -Binary
62
+ ```
63
+
64
+ The same `UNISERVICE_BINARY=1` environment variable works for `install.sh`.
65
+
66
+ ### Installer options
67
+
68
+ ```
69
+ --binary install the standalone binary instead of the zipapp
70
+ --prefix DIR install under DIR instead of /usr/local
71
+ (for packaging and tests; needs no elevated privileges)
72
+ --version TAG install a specific release, e.g. --version v1.2.0
73
+ --sha256 HEX verify the artifact against this digest
74
+ (by default the digest published in SHA256SUMS is used)
75
+ --from FILE install a local zipapp or binary instead of downloading
76
+ --uninstall remove a previous installation, using its manifest
77
+ ```
78
+
79
+ ```bash
80
+ sudo ./install.sh # recommended: the zipapp
81
+ sudo ./install.sh --binary # no Python needed
82
+ sudo ./install.sh --version v1.2.0 # pin a release
83
+ ./install.sh --prefix /tmp/uniservice-test # unprivileged, for packaging/tests
84
+ sudo ./install.sh --uninstall
85
+ ```
86
+
87
+ The zipapp build is byte-for-byte reproducible, so pinning a `--sha256` gives a verifiable
88
+ install. When a release has no asset for your platform the installer says so and points at the
89
+ zipapp alternative.
90
+
91
+ ### Release assets
92
+
93
+ | Asset | Notes |
94
+ | --- | --- |
95
+ | `uniservice` | the portable zipapp (recommended) |
96
+ | `uniservice-linux-x86_64`, `uniservice-linux-aarch64` | standalone binaries |
97
+ | `uniservice-macos-arm64`, `uniservice-macos-x86_64` | standalone binaries |
98
+ | `uniservice-windows-x86_64.exe` | standalone binary |
99
+ | `uniservice-<version>-py3-none-any.whl`, `.tar.gz` | the same code as a Python package |
100
+ | `SHA256SUMS` | digests for everything above |
101
+
102
+ ### Verify the checksum yourself
103
+
104
+ ```bash
105
+ base=https://github.com/kevinhuang001/uniservice/releases/latest/download
106
+ curl -fsSLO "$base/uniservice" && curl -fsSLO "$base/SHA256SUMS"
107
+ grep ' uniservice$' SHA256SUMS | sha256sum -c -
108
+ sudo install -m 0755 uniservice /usr/local/bin/uniservice
109
+ ```
110
+
111
+ ### Windows notes
112
+
113
+ The default (zipapp) layout installs `uniservice.pyz` plus a `uniservice.cmd` shim into
114
+ `%LOCALAPPDATA%\uniservice\bin` and adds it to your user `PATH` and PowerShell profile.
115
+ `-Binary` installs `uniservice.exe` there instead, with no shim and no Python requirement.
116
+ `-Uninstall` reverses either.
117
+
118
+ Reopen the terminal, then:
119
+
120
+ ```bash
121
+ uniservice --help
122
+ ```
123
+
124
+ ## Usage
125
+
126
+ ### Scope (macOS/Linux)
127
+
128
+ The scope is derived from your privileges, not from a flag:
129
+
130
+ | How you run it | Scope | Definitions live in |
131
+ | --- | --- | --- |
132
+ | `uniservice ...` | user | `~/.config/systemd/user/`, `~/Library/LaunchAgents/` |
133
+ | `sudo uniservice ...` | system | `/etc/systemd/system/`, `/Library/LaunchDaemons/` |
134
+
135
+ Because the installer puts the command in `/usr/local/bin` (on every account's `PATH`), both
136
+ forms work with no further setup.
137
+
138
+ Two things change under `sudo`:
139
+
140
+ - the command after `--` is resolved with **root's** `PATH`, so `python3` may
141
+ resolve to `/usr/bin/python3` instead of your conda/venv copy — pass an
142
+ absolute path when that matters;
143
+ - the definition and its logs belong to root (`/root/.uniservice/logs/`).
144
+
145
+ On Windows there is no `sudo`: `uniservice list` works in any shell, every other
146
+ command needs an **Administrator** PowerShell/CMD.
147
+
148
+ ### Add
149
+
150
+ ```bash
151
+ uniservice add demo --workdir /tmp -- python3 -m http.server 8000
152
+ ```
153
+
154
+ - If the executable is not an absolute path, uniservice will try to resolve it from PATH and print a WARNING.
155
+ - If `--workdir` is not provided, the command runs in the current directory.
156
+ - If a service with the same name already exists, uniservice will ask whether to overwrite it.
157
+ - `add` performs `enable` + `start`.
158
+
159
+ Service names must not contain `/`, `\`, control characters or NUL, must not be `.`/`..`, and must not start with `-`
160
+ (those characters would escape the definition directory or break the native definition format).
161
+
162
+ ### List
163
+
164
+ ```bash
165
+ uniservice list
166
+ ```
167
+
168
+ Output is TSV (tab-separated), sorted by name:
169
+
170
+ | Column | Meaning |
171
+ | --- | --- |
172
+ | `NAME` | service name |
173
+ | `ENABLED` | starts automatically: `yes` / `no` / `?` |
174
+ | `RUNNING` | currently running: `yes` / `no` / `?` |
175
+
176
+ `?` means the platform did not give a definitive answer (for example `systemctl` is missing, the launchd override
177
+ query failed, or a unit is in a transitional state). It is never a guess. Names containing control characters are
178
+ sanitised and duplicate rows are collapsed, so the output always stays valid TSV.
179
+
180
+ ### Control
181
+
182
+ ```bash
183
+ uniservice enable demo
184
+ uniservice start demo
185
+ uniservice stop demo
186
+ uniservice disable demo
187
+ ```
188
+
189
+ ### Status and logs
190
+
191
+ ```bash
192
+ uniservice status demo
193
+ uniservice logs demo --lines 200
194
+ uniservice logs demo --follow # or -f
195
+ ```
196
+
197
+ ### Remove
198
+
199
+ ```bash
200
+ uniservice remove demo
201
+ ```
202
+
203
+ `remove` performs `stop` + `disable` before deleting the definition.
204
+
205
+ ### Cat
206
+
207
+ ```bash
208
+ uniservice cat demo
209
+ ```
210
+
211
+ Prints an equivalent `uniservice add ...` command for the service.
212
+
213
+ ## Logging
214
+
215
+ - Console: WARNING and above
216
+ - File: DEBUG and above
217
+ - Log file:
218
+ - macOS/Linux: `~/.uniservice/logs/uniservice.log`
219
+ - Windows: `%LOCALAPPDATA%\uniservice\logs\uniservice.log`
220
+
221
+ ## Project layout
222
+
223
+ ```
224
+ uniservice executable entry point (thin launcher, and what the zipapp runs)
225
+ uniservice_lib/
226
+ cli.py argument parsing and command dispatch
227
+ scope.py user vs. system scope
228
+ naming.py service-name validation and native definition names
229
+ process.py subprocess helpers
230
+ platform_utils.py platform and privilege detection
231
+ logging_utils.py console + file logging
232
+ errors.py exception hierarchy
233
+ backends/
234
+ __init__.py backend selection
235
+ base.py Backend interface, ServiceInfo, state parsing
236
+ linux.py systemd units
237
+ macos.py launchd jobs
238
+ windows.py Scheduled Tasks
239
+ scripts/build_zipapp.py builds the portable zipapp (the recommended artifact)
240
+ scripts/build_binary.py builds the standalone binary (PyInstaller, per platform)
241
+ packaging/entrypoint.py PyInstaller entry point
242
+ install.sh macOS/Linux installer (artifact choice, manifest, --uninstall)
243
+ install-windows.ps1 Windows installer (zipapp by default, -Binary for the .exe)
244
+ tests/ pytest suite (unit, end-to-end, installer, opt-in integration)
245
+ .github/workflows/ CI (3 platforms + 5 binary targets) and the release workflow
246
+ ```
247
+
248
+ Each backend implements the same `Backend` interface, so adding a platform means adding one module and registering it
249
+ in `uniservice_lib/backends/__init__.py`.
250
+
251
+ ## Development
252
+
253
+ ```bash
254
+ python -m pip install -e ".[dev]"
255
+
256
+ ruff check . # lint
257
+ ruff format --check . # formatting
258
+ python -m pytest # unit + end-to-end tests
259
+ ```
260
+
261
+ Build both artifacts and try the installer against a throwaway prefix:
262
+
263
+ ```bash
264
+ python scripts/build_zipapp.py --output dist/uniservice # ~30 KB, needs Python
265
+ python -m pip install -e ".[build]"
266
+ python scripts/build_binary.py --output-dir dist # ~24 MB, self-contained
267
+
268
+ ./install.sh --prefix /tmp/uniservice-test --from dist/uniservice
269
+ /tmp/uniservice-test/bin/uniservice --version
270
+ ./install.sh --uninstall --prefix /tmp/uniservice-test
271
+ ```
272
+
273
+ The zipapp build is deterministic, so `pytest tests/test_packaging.py tests/test_install_script.py` can compare a
274
+ locally built artifact against the digest the installer computes. The installer tests serve a fake release over
275
+ `file://`, so they exercise the download, the asset selection and the checksum verification without the network.
276
+
277
+ The default suite mocks the native tools, so it runs on every platform. Opt-in integration tests drive the real
278
+ service manager of the host:
279
+
280
+ ```bash
281
+ UNISERVICE_RUN_INTEGRATION=1 python -m pytest -m integration -v
282
+ ```
283
+
284
+ CI runs lint, shellcheck/PowerShell checks, the reproducible zipapp build, the full test suite on Ubuntu, macOS and
285
+ Windows, and builds the standalone binary for five targets. Pushing a `v*` tag runs
286
+ `.github/workflows/release.yml`, which publishes the wheel, the sdist, the zipapp, every platform binary and
287
+ `SHA256SUMS` as a GitHub release.
288
+
289
+ ## Uninstall
290
+
291
+ `uniservice` removes itself:
292
+
293
+ ```bash
294
+ sudo uniservice uninstall # or: uniservice uninstall, for a --prefix install
295
+ uniservice uninstall --dry-run # show what would be removed first
296
+ ```
297
+
298
+ It reads the manifest its installer wrote (`/usr/local/lib/uniservice/manifest`), deletes exactly
299
+ those files and prunes the directories that become empty. It never added a `PATH` line, so there is
300
+ nothing else to clean up. On Windows the running `.exe` cannot delete itself, so the command hands
301
+ that last file to a short-lived helper and it disappears a moment after the command returns.
302
+
303
+ **Your services are untouched** - only the command is removed. Delete the services you no longer
304
+ want first, while the command still exists:
305
+
306
+ ```bash
307
+ uniservice list
308
+ uniservice remove <name>
309
+ ```
310
+
311
+ If the command is already broken or gone, the installer can still clean up after it:
312
+
313
+ ```bash
314
+ sudo ./install.sh --uninstall # macOS/Linux, same manifest
315
+ ./install.sh --uninstall --prefix DIR # if you installed to a custom prefix
316
+ ./install-windows.ps1 -Uninstall # Windows
317
+ ```