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.
- uniservice-1.4.0/PKG-INFO +346 -0
- uniservice-1.4.0/README.md +317 -0
- uniservice-1.4.0/pyproject.toml +69 -0
- uniservice-1.4.0/setup.cfg +4 -0
- uniservice-1.4.0/tests/test_cli.py +399 -0
- uniservice-1.4.0/tests/test_entrypoint.py +125 -0
- uniservice-1.4.0/tests/test_install_script.py +422 -0
- uniservice-1.4.0/tests/test_installation.py +166 -0
- uniservice-1.4.0/tests/test_linux_backend.py +474 -0
- uniservice-1.4.0/tests/test_live_integration.py +116 -0
- uniservice-1.4.0/tests/test_macos_backend.py +580 -0
- uniservice-1.4.0/tests/test_naming.py +82 -0
- uniservice-1.4.0/tests/test_packaging.py +121 -0
- uniservice-1.4.0/tests/test_platform_utils.py +91 -0
- uniservice-1.4.0/tests/test_process.py +74 -0
- uniservice-1.4.0/tests/test_scope.py +39 -0
- uniservice-1.4.0/tests/test_windows_backend.py +511 -0
- uniservice-1.4.0/uniservice.egg-info/PKG-INFO +346 -0
- uniservice-1.4.0/uniservice.egg-info/SOURCES.txt +35 -0
- uniservice-1.4.0/uniservice.egg-info/dependency_links.txt +1 -0
- uniservice-1.4.0/uniservice.egg-info/entry_points.txt +2 -0
- uniservice-1.4.0/uniservice.egg-info/requires.txt +8 -0
- uniservice-1.4.0/uniservice.egg-info/top_level.txt +1 -0
- uniservice-1.4.0/uniservice_lib/__init__.py +24 -0
- uniservice-1.4.0/uniservice_lib/backends/__init__.py +30 -0
- uniservice-1.4.0/uniservice_lib/backends/base.py +111 -0
- uniservice-1.4.0/uniservice_lib/backends/linux.py +270 -0
- uniservice-1.4.0/uniservice_lib/backends/macos.py +472 -0
- uniservice-1.4.0/uniservice_lib/backends/windows.py +426 -0
- uniservice-1.4.0/uniservice_lib/cli.py +382 -0
- uniservice-1.4.0/uniservice_lib/errors.py +36 -0
- uniservice-1.4.0/uniservice_lib/installation.py +268 -0
- uniservice-1.4.0/uniservice_lib/logging_utils.py +69 -0
- uniservice-1.4.0/uniservice_lib/naming.py +113 -0
- uniservice-1.4.0/uniservice_lib/platform_utils.py +96 -0
- uniservice-1.4.0/uniservice_lib/process.py +81 -0
- 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) · [](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) · [](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
|
+
```
|