omakit 0.5.1 → 0.6.1
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.
- package/README.md +38 -45
- package/blocks/history.json +68 -0
- package/blocks/run/NOTICE +12 -0
- package/blocks/run/Run.qml +242 -0
- package/blocks/run/run-supervisor.py +522 -0
- package/blocks/store/NOTICE +12 -0
- package/blocks/store/Store.qml +157 -0
- package/blocks/store/store-helper.py +431 -0
- package/package.json +12 -5
- package/skills/omarchy-plugin-audit/SKILL.md +11 -5
- package/skills/omarchy-plugin-build/SKILL.md +164 -0
- package/skills/omarchy-plugin-check/SKILL.md +6 -3
- package/skills/omarchy-plugin-submit/SKILL.md +4 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
- package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
- package/tests/fixtures/weigh/clean/Widget.qml +19 -0
- package/tests/fixtures/weigh/clean/manifest.json +9 -0
- package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
- package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
- package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
- package/tests/fixtures/weigh/poller/Service.qml +50 -0
- package/tests/fixtures/weigh/poller/manifest.json +9 -0
- package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
- package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
- package/tests/lab/run/harness/scenarios/controls.sh +6 -0
- package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
- package/tests/lab/run/harness/scenarios/forge.sh +11 -0
- package/tests/lab/run/harness/scenarios/holder.sh +5 -0
- package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
- package/tests/lab/run/harness/scenarios/stall.sh +5 -0
- package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
- package/tests/lab/run/harness/scenarios/tree.sh +7 -0
- package/tests/lab/run/harness/shell.qml +84 -0
- package/tests/lab/run/report.py +217 -0
- package/tests/lab/run/suite.sh +106 -0
- package/tests/lab/store/harness/shell.qml +73 -0
- package/tests/lab/store/report.py +133 -0
- package/tests/lab/store/suite.sh +109 -0
- package/tests/parity/corpus.mjs +8 -3
- package/tests/parity/run.mjs +4 -4
- package/tools/audit/audit.mjs +17 -6
- package/tools/audit/git.mjs +3 -3
- package/tools/audit/report.mjs +31 -5
- package/tools/blocks/add.mjs +138 -0
- package/tools/blocks/commit.json +5 -0
- package/tools/blocks/record-commit.mjs +77 -0
- package/tools/blocks/registry.mjs +191 -0
- package/tools/blocks/stamp.mjs +61 -0
- package/tools/inspect/contract.mjs +36 -5
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +68 -3
- package/tools/inspect/patterns.mjs +18 -3
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +18 -2
- package/tools/inspect/writes.mjs +22 -4
- package/tools/lab/guest.mjs +155 -0
- package/tools/lab/harness.sh +119 -0
- package/tools/lab/host.mjs +177 -0
- package/tools/lab/inspect.mjs +240 -0
- package/tools/lab/omarchy.gpg +13 -0
- package/tools/lab/patches/omarchy-iso-test.patch +351 -0
- package/tools/lab/paths.mjs +173 -0
- package/tools/lab/pin.json +42 -0
- package/tools/lab/pin.mjs +64 -0
- package/tools/lab/prune.mjs +68 -0
- package/tools/lab/qemu.mjs +153 -0
- package/tools/lab/qmp-cli.mjs +21 -0
- package/tools/lab/report.mjs +183 -0
- package/tools/lab/run.mjs +344 -0
- package/tools/lab/setup.mjs +430 -0
- package/tools/lab/suites/run.sh +35 -0
- package/tools/lab/suites/store.sh +41 -0
- package/tools/lab/suites/weigh.sh +196 -0
- package/tools/lab/suites.mjs +142 -0
- package/tools/lab/verify.mjs +134 -0
- package/tools/marketplace/README.md +38 -1
- package/tools/marketplace/banner.mjs +23 -2
- package/tools/marketplace/cli.mjs +449 -146
- package/tools/marketplace/completion-check.mjs +27 -1
- package/tools/marketplace/completion.mjs +32 -4
- package/tools/marketplace/doctor.mjs +47 -9
- package/tools/marketplace/github.mjs +52 -6
- package/tools/marketplace/local-transport.mjs +1 -1
- package/tools/marketplace/options.mjs +16 -5
- package/tools/marketplace/outcome.mjs +244 -0
- package/tools/marketplace/pin.mjs +178 -33
- package/tools/marketplace/setup.mjs +16 -15
- package/tools/marketplace/tree.mjs +1 -1
- package/tools/marketplace/upgrade.mjs +5 -5
- package/tools/marketplace/usage.mjs +116 -72
- package/tools/subject/resolve.mjs +19 -6
- package/tools/weigh/audit.mjs +47 -16
- package/tools/weigh/config.mjs +105 -24
- package/tools/weigh/list.mjs +10 -1
package/README.md
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/banner.gif" alt="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/banner.gif" alt="Animated Omakit wordmark above the line tested plumbing for plugins" width="512">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Tested plumbing, the marketplace's own checks, and a disposable Omarchy to test in.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The three parts are the Run and Store blocks a plugin copies into its own tree, the marketplace's own checks run locally, and repository suites run in a disposable guest. Behind the first are M13's 1,001 security blocker comments from one measured week: 587 raise a Run line, 527 a Store line, and runner plus store handle 777 of 1,001 when read as an upper bound, not as a result for any plugin ([M13](docs/MEASUREMENTS.md#m13-what-the-review-blocks-on-over-one-week-of-comments-and-which-of-it-a-block-can-own)). No reviewer has seen a ported plugin yet.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
| Command | What you get | Read more |
|
|
12
|
-
| --- | --- | --- |
|
|
13
|
-
| `omakit submit <plugin-repo>` | Run the marketplace's own checks before you open the issue, and get the issue text ready to paste. Nothing is posted for you. | [submit](docs/SUBMIT.md) |
|
|
14
|
-
| `omakit inspect <plugin-dir>` | See what needs attention in your plugin before a reviewer does: the longest functions, and the things reviewers flag most often, each with the file and line. | [inspect](docs/INSPECT.md) |
|
|
15
|
-
| `omakit watch <issue-url>` | Know whether the commit the marketplace checked is still the one you are shipping, and what to do when it is not. | [watch](docs/VALIDATION_WATCH.md) |
|
|
16
|
-
| `omakit audit` | Find installed plugins that are running code the marketplace never checked. | [audit](docs/AUDIT.md) |
|
|
17
|
-
| `omakit weigh <plugin>` | Find out what a plugin costs the shell in memory and CPU. | [weigh](docs/WEIGH.md) |
|
|
18
|
-
| `omakit verify <plugin-repo>` | Get the marketplace's security result for your commit, exactly as it would see it. | [commands](docs/COMMANDS.md) |
|
|
19
|
-
| `omakit doctor`, `omakit setup` | Check what is installed and pinned, or set everything up once, with tab completion. | [install](docs/INSTALL.md) |
|
|
20
|
-
|
|
21
|
-
Every number a command prints has a measured origin in [MEASUREMENTS.md](docs/MEASUREMENTS.md); nothing is a guess and nothing is a grade.
|
|
9
|
+
[](https://github.com/tcballard/omarchy-badges) [](https://www.npmjs.com/package/omakit) [](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [](https://socket.dev/npm/package/omakit)
|
|
22
10
|
|
|
23
11
|
## Install
|
|
24
12
|
|
|
@@ -27,54 +15,59 @@ npm i -g omakit && omakit setup
|
|
|
27
15
|
npx skills add mtolhuys/omakit
|
|
28
16
|
```
|
|
29
17
|
|
|
30
|
-
|
|
18
|
+
Omakit needs [Node 22+](package.json) and Git. `omakit setup` prepares the pinned marketplace checkout and shell completion. The blocks also need `/usr/bin/python3`. Audit and weigh need a running Omarchy shell. The lab needs KVM, QEMU and OVMF; `omakit doctor` reports what is missing and installs nothing.
|
|
31
19
|
|
|
32
|
-
|
|
20
|
+
## Build
|
|
33
21
|
|
|
34
|
-
|
|
22
|
+
`omakit add run <plugin-dir>` and `omakit add store <plugin-dir>` copy versioned process and private-state plumbing into the plugin's `omakit/` directory; the plugin owns those files, and an edited copy is never overwritten. Commit the copied files before checking, because check reads committed `HEAD` unless you pass `--allow-dirty`.
|
|
35
23
|
|
|
36
|
-

|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
## Check
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
`omakit inspect <plugin-dir>` shows what the tree does, `omakit verify <plugin-repo>` runs the marketplace's baseline, and `omakit submit <plugin-repo>` runs every submission check and prints a title and body without posting them.
|
|
41
29
|
|
|
42
|
-

|
|
43
31
|
|
|
44
|
-
|
|
32
|
+

|
|
45
33
|
|
|
46
|
-
##
|
|
34
|
+
## Track
|
|
47
35
|
|
|
48
|
-
|
|
36
|
+
`omakit watch --all` compares open submissions with the commits the marketplace validated, while `omakit audit` compares installed third-party plugins with their validated commits; both are read-only.
|
|
49
37
|
|
|
50
|
-
|
|
38
|
+

|
|
51
39
|
|
|
52
|
-
|
|
40
|
+

|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
## Prove
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
After one `omakit lab setup`, `omakit lab prove <suite>` boots a fresh overlay from the verified base, prints the guest identity, runs Run, Store or weigh in that guest, records the result, removes the overlay and checks the base unchanged.
|
|
57
45
|
|
|
58
|
-
|
|
46
|
+

|
|
59
47
|
|
|
60
|
-
|
|
48
|
+
`omakit weigh <plugin>` is the supporting desktop measurement: after explicit consent it restarts the shell without and with the plugin, reports CPU and child processes against the baseline's own spread, and restores `shell.json`.
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+

|
|
63
51
|
|
|
64
|
-
## Evidence
|
|
52
|
+
## Evidence
|
|
65
53
|
|
|
66
|
-
|
|
|
54
|
+
| What was measured | Record |
|
|
67
55
|
| --- | --- |
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
56
|
+
| Review plumbing | 1,001 blocker comments; Run 587, Store 527, either 777, [M13 record](docs/evidence/blocks/2026-09-17-review-blockers.json) |
|
|
57
|
+
| Run | 19 of 19 on the desktop and 19 of 19 on the stock 4.0.3 guest at Run 0.2.1, [desktop](docs/evidence/blocks/2026-09-18-run-lab-desktop.json), [fresh guest](docs/evidence/lab/20260919-143209-run/runlab.json) |
|
|
58
|
+
| Store | 15 of 15 on the desktop and stock guest, including the simulated foreign owner, [desktop](docs/evidence/blocks/2026-09-18-store-lab-desktop.json), [guest](docs/evidence/lab/20260918-161230-store/storelab.json) |
|
|
59
|
+
| The disposable guest | guest `omarchy 4.0.3-1`, skew false, 2m 42.2s, 440,209,408 B overlay removed, base unchanged, [fresh run](docs/evidence/lab/20260919-143209-run/run.json) |
|
|
60
|
+
| The two ports | [Theme Manager through Run](docs/evidence/blocks/2026-09-17-theme-manager-port.json), [Sidecar through Store](docs/evidence/blocks/2026-09-17-sidecar-port.json); neither submitted, no reviewer has seen either |
|
|
61
|
+
| Validation drift | 326 of 519 readable open-submission comparisons stale, 64 of 583 unknown, [dated record](docs/evidence/staleness/2026-09-15.json) |
|
|
62
|
+
| README captures | eight GIFs made from committed captures and scenes, with commands, hashes, dimensions and timings in the [M10 record](docs/evidence/readme/2026-09-19-positioning.json) |
|
|
63
|
+
| First-reader path | packaged install, blocks, checks, tracking and a 19-scenario guest run exercised from `/tmp`, [dated record](docs/evidence/ux/2026-09-19-readme-test.json) |
|
|
64
|
+
| Package boundary | 0 runtime and 0 development dependencies in [package.json](package.json); 0 marketplace writes enforced by the read-only tests |
|
|
74
65
|
|
|
75
66
|
## Documentation
|
|
76
67
|
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
68
|
+
- [Blocks](docs/BLOCKS.md) and [their design spike](docs/BLOCKS_SPIKE.md)
|
|
69
|
+
- [Commands](docs/COMMANDS.md), [inspect](docs/INSPECT.md), [submit](docs/SUBMIT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md) and [weigh](docs/WEIGH.md)
|
|
70
|
+
- [The disposable lab](docs/LAB.md) and [every measurement](docs/MEASUREMENTS.md)
|
|
71
|
+
- [Installation](docs/INSTALL.md), [media recipes](docs/media/README.md) and [repository rules](AGENTS.md)
|
|
72
|
+
|
|
73
|
+
Licence: [MIT](LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"block": "run",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"file": "Run.qml",
|
|
6
|
+
"sha256": "7055007d082b81f328b8c3e7f41e648ed27c111ef546220277f0fb7f18332e28"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"block": "run",
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"file": "run-supervisor.py",
|
|
12
|
+
"sha256": "3a1c8e18c56592e046e1ac1dfe35df5745c39cc60405dfb6d408ee4eb287ef5f"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"block": "store",
|
|
16
|
+
"version": "0.1.0",
|
|
17
|
+
"file": "Store.qml",
|
|
18
|
+
"sha256": "c71b5646e0b6ccc4b45e5c57a8c7ed08a089319f4ee7d3ce74ef5e28d8af3801"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"block": "store",
|
|
22
|
+
"version": "0.1.0",
|
|
23
|
+
"file": "store-helper.py",
|
|
24
|
+
"sha256": "28576b8189c9d2547c11b273243731a6c954644b7135186e57257addc3c99dd9"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"block": "store",
|
|
28
|
+
"version": "0.1.0",
|
|
29
|
+
"file": "store-helper.py",
|
|
30
|
+
"sha256": "3ef0c3f5df90ef93d3ff7cdd979159cce590a4a61b3351f42ea5123fa49f0720"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"block": "store",
|
|
34
|
+
"version": "0.1.0",
|
|
35
|
+
"file": "store-helper.py",
|
|
36
|
+
"sha256": "cec3fcc9573ce7c12f1365e106cc65a936e213553bfe4e3128e8a87eea511dc5"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"block": "run",
|
|
40
|
+
"version": "0.2.0",
|
|
41
|
+
"file": "Run.qml",
|
|
42
|
+
"sha256": "0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"block": "run",
|
|
46
|
+
"version": "0.2.0",
|
|
47
|
+
"file": "run-supervisor.py",
|
|
48
|
+
"sha256": "07c3d5fa103fcbfc40eb081206fdd8571c6e3852a188389d79bbac12db00170d"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"block": "store",
|
|
52
|
+
"version": "0.2.0",
|
|
53
|
+
"file": "Store.qml",
|
|
54
|
+
"sha256": "6fafd2ec98b4d4fe18ead2ae1f1221386e9714eb9a33ef0f1b9cf48520f2c743"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"block": "store",
|
|
58
|
+
"version": "0.2.0",
|
|
59
|
+
"file": "store-helper.py",
|
|
60
|
+
"sha256": "2dd4d6ad308bbed473936c29844793411d8849415812b96ca5e7a6688097e37c"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"block": "run",
|
|
64
|
+
"version": "0.2.1",
|
|
65
|
+
"file": "run-supervisor.py",
|
|
66
|
+
"sha256": "d872a5fc91f648ba4f9b9654d5cb19bba17a8afa4abb618502a04f47f66a6be1"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
omakit blocks in this directory
|
|
2
|
+
|
|
3
|
+
Each file below was copied from omakit (https://github.com/mtolhuys/omakit),
|
|
4
|
+
MIT licence, Copyright (c) 2026 Maarten Tolhuijs, by `omakit add`. The body
|
|
5
|
+
sha256 is over everything after the file's header line
|
|
6
|
+
`end of omakit block header`; `omakit inspect` reports a file whose body differs as
|
|
7
|
+
modified, and `omakit add --update` refuses to overwrite one. Modifications
|
|
8
|
+
are to be listed here by the plugin's author.
|
|
9
|
+
|
|
10
|
+
block run 0.2.1, from omakit commit unstamped
|
|
11
|
+
Run.qml sha256 0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419
|
|
12
|
+
run-supervisor.py sha256 d872a5fc91f648ba4f9b9654d5cb19bba17a8afa4abb618502a04f47f66a6be1
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// omakit block: run 0.2.1
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2026 Maarten Tolhuijs
|
|
4
|
+
// Source: omakit blocks/run/Run.qml, commit unstamped
|
|
5
|
+
// Body sha256: 0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419
|
|
6
|
+
// end of omakit block header
|
|
7
|
+
//
|
|
8
|
+
// Run: starts one program for a plugin and always ends it. The program is
|
|
9
|
+
// started by run-supervisor.py, next to this file, through
|
|
10
|
+
// /usr/bin/python3 -I -S -B: absolute path, argv only, a closed environment,
|
|
11
|
+
// a hard deadline, byte and line caps while reading, TERM then grace then
|
|
12
|
+
// KILL to the whole process group, the leader reaped last, cancel on
|
|
13
|
+
// destruction and on supersession, one result object. The protocol between
|
|
14
|
+
// this file and the supervisor is a per-run token this file writes to the
|
|
15
|
+
// supervisor's stdin: every line the supervisor reports carries it, and a
|
|
16
|
+
// line without it, which is what the program can write into the same pipe,
|
|
17
|
+
// changes nothing. docs/BLOCKS.md is the contract and says which review
|
|
18
|
+
// comments each line answers.
|
|
19
|
+
//
|
|
20
|
+
// Run {
|
|
21
|
+
// id: catalog
|
|
22
|
+
// command: [Quickshell.shellDir + "/catalog.sh", "--refresh"]
|
|
23
|
+
// deadlineMs: 30000
|
|
24
|
+
// onFinished: result => { if (result.state === "ok") parse(result.stdout) }
|
|
25
|
+
// }
|
|
26
|
+
// catalog.start()
|
|
27
|
+
import QtQuick
|
|
28
|
+
import Quickshell
|
|
29
|
+
import Quickshell.Io
|
|
30
|
+
|
|
31
|
+
QtObject {
|
|
32
|
+
id: run
|
|
33
|
+
|
|
34
|
+
// argv; command[0] an absolute path. A shell string (an interpreter with
|
|
35
|
+
// its string flag, through a wrapper or not) is refused as spawn-failed
|
|
36
|
+
// unless allowShellString; the forms it knows are in docs/BLOCKS.md.
|
|
37
|
+
property list<string> command: []
|
|
38
|
+
property bool allowShellString: false
|
|
39
|
+
// Added to the base environment, never replacing it. The base is
|
|
40
|
+
// PATH=/usr/bin, HOME, LANG=C.UTF-8 and XDG_RUNTIME_DIR; nothing else
|
|
41
|
+
// of the shell's environment reaches the program.
|
|
42
|
+
property var environment: ({})
|
|
43
|
+
property int deadlineMs: 10000
|
|
44
|
+
property int graceMs: 1000
|
|
45
|
+
// Per stream, counted while reading; over either the run ends as overflow.
|
|
46
|
+
property int maxBytes: 1048576
|
|
47
|
+
property int maxLines: 10000
|
|
48
|
+
// Per stream, what the result carries; the rest is counted and dropped.
|
|
49
|
+
property int keepBytes: 65536
|
|
50
|
+
|
|
51
|
+
readonly property bool running: _state === "running"
|
|
52
|
+
// One object: state is one of ok, exit, timeout, overflow, cancelled,
|
|
53
|
+
// spawn-failed, supervisor-lost, python-missing; stdout and stderr are
|
|
54
|
+
// plain text of at most keepBytes each, control characters removed,
|
|
55
|
+
// meant for Text.PlainText; outBytes, outLines, errBytes, errLines as
|
|
56
|
+
// counted; exitCode (null when signalled), termSignal, ms, pgid,
|
|
57
|
+
// survivors, and reason for the three failure states.
|
|
58
|
+
signal finished(var result)
|
|
59
|
+
|
|
60
|
+
readonly property string supervisor: decodeURIComponent(Qt.resolvedUrl("run-supervisor.py").toString().replace(/^file:\/\//, ""))
|
|
61
|
+
readonly property var states: ["ok", "exit", "timeout", "overflow", "cancelled", "spawn-failed", "supervisor-lost", "python-missing"]
|
|
62
|
+
property string _state: "idle"
|
|
63
|
+
property bool _started: false
|
|
64
|
+
property bool _pending: false
|
|
65
|
+
property int _pgid: 0
|
|
66
|
+
property double _t0: 0
|
|
67
|
+
property var _result: null
|
|
68
|
+
property string _supervisorErr: ""
|
|
69
|
+
property string _token: ""
|
|
70
|
+
property string _buffer: ""
|
|
71
|
+
|
|
72
|
+
readonly property var _baseEnvironment: ({
|
|
73
|
+
PATH: "/usr/bin",
|
|
74
|
+
HOME: Quickshell.env("HOME"),
|
|
75
|
+
LANG: "C.UTF-8",
|
|
76
|
+
XDG_RUNTIME_DIR: Quickshell.env("XDG_RUNTIME_DIR")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
function _now() { return Date.now() - _t0 }
|
|
80
|
+
|
|
81
|
+
/** Start the program. A run already live is cancelled first, reports cancelled, and this start follows it. */
|
|
82
|
+
function start() {
|
|
83
|
+
if (_state === "running") { _pending = true; cancel(); return }
|
|
84
|
+
if (_proc.running) { _pending = true; return } // the last supervisor is still being reaped (a backstop); it follows on runningChanged
|
|
85
|
+
_t0 = Date.now()
|
|
86
|
+
_state = "running"
|
|
87
|
+
_started = false
|
|
88
|
+
_result = null
|
|
89
|
+
_supervisorErr = ""
|
|
90
|
+
_buffer = ""
|
|
91
|
+
_pgid = 0
|
|
92
|
+
_token = _newToken()
|
|
93
|
+
_proc.command = _argv()
|
|
94
|
+
_proc.environment = Object.assign({}, _baseEnvironment, environment)
|
|
95
|
+
_proc.stdinEnabled = true
|
|
96
|
+
_backstop.interval = deadlineMs + graceMs + 3000
|
|
97
|
+
_backstop.start()
|
|
98
|
+
_proc.running = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** End the run now: TERM to the group, the grace, KILL; the result comes back as cancelled. */
|
|
102
|
+
function cancel() {
|
|
103
|
+
if (_state === "running") _proc.signal(15)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 128 bits from the engine's securely seeded generator; the program never
|
|
107
|
+
// sees a value of it (stdin is consumed by the supervisor before the fork).
|
|
108
|
+
function _newToken() {
|
|
109
|
+
let token = ""
|
|
110
|
+
for (let i = 0; i < 8; i += 1) token += ("000" + Math.floor(Math.random() * 65536).toString(16)).slice(-4)
|
|
111
|
+
return token
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _argv() {
|
|
115
|
+
const options = ["--deadline-ms", String(deadlineMs), "--grace-ms", String(graceMs),
|
|
116
|
+
"--max-bytes", String(maxBytes), "--max-lines", String(maxLines), "--keep-bytes", String(keepBytes)]
|
|
117
|
+
if (allowShellString) options.push("--allow-shell-string")
|
|
118
|
+
return ["/usr/bin/python3", "-I", "-S", "-B", supervisor].concat(options, ["--"], command)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The supervisor's stdout, delivered as it arrives: lines are cut here,
|
|
122
|
+
// and the unfinished line is bounded, so a stream without a newline
|
|
123
|
+
// cannot grow inside the shell process.
|
|
124
|
+
function _chunk(text) {
|
|
125
|
+
_buffer += text
|
|
126
|
+
let end = _buffer.indexOf("\n")
|
|
127
|
+
while (end >= 0) {
|
|
128
|
+
_line(_buffer.slice(0, end))
|
|
129
|
+
_buffer = _buffer.slice(end + 1)
|
|
130
|
+
end = _buffer.indexOf("\n")
|
|
131
|
+
}
|
|
132
|
+
const bound = 6 * keepBytes + 65536
|
|
133
|
+
if (_buffer.length > bound) _buffer = _buffer.slice(-bound)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A protocol line carries the token; anything else on the pipe is the
|
|
137
|
+
// program's and is dropped. The first leader and the first result count.
|
|
138
|
+
function _line(line) {
|
|
139
|
+
const at = _token ? line.indexOf(_token + " ") : -1
|
|
140
|
+
if (at < 0) return
|
|
141
|
+
let event
|
|
142
|
+
try { event = JSON.parse(line.slice(at + _token.length + 1)) } catch (error) { return }
|
|
143
|
+
if (event.ev === "leader") _leader(event)
|
|
144
|
+
else if (event.ev === "result" && !_result) _result = _checked(event)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The first leader line names the group; the acknowledgement releases the
|
|
148
|
+
// supervisor's gate, and stdin closes behind it.
|
|
149
|
+
function _leader(event) {
|
|
150
|
+
if (_pgid !== 0 || _int(event.pgid) === 0) return
|
|
151
|
+
_pgid = _int(event.pgid)
|
|
152
|
+
_proc.write("go\n")
|
|
153
|
+
_proc.stdinEnabled = false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _int(value) { return Number.isInteger(value) && value >= 0 ? value : 0 }
|
|
157
|
+
function _intOrNull(value) { return Number.isInteger(value) ? value : null }
|
|
158
|
+
function _text(value, max) { return _plain(String(value == null ? "" : value)).slice(0, max) }
|
|
159
|
+
|
|
160
|
+
/** The result as this file reports it: the closed set of states, the numbers as integers, the text stripped here as well. */
|
|
161
|
+
function _checked(event) {
|
|
162
|
+
if (!states.includes(event.state)) return null
|
|
163
|
+
return {
|
|
164
|
+
state: event.state, exitCode: _intOrNull(event.exitCode), termSignal: _intOrNull(event.termSignal),
|
|
165
|
+
outBytes: _int(event.outBytes), outLines: _int(event.outLines), errBytes: _int(event.errBytes), errLines: _int(event.errLines),
|
|
166
|
+
survivors: _int(event.survivors), stdout: _text(event.stdout, keepBytes), stderr: _text(event.stderr, keepBytes),
|
|
167
|
+
reason: event.reason == null ? null : _text(event.reason, 4096),
|
|
168
|
+
signals: Array.isArray(event.signals) ? event.signals.filter(s => s && typeof s.sig === "string").map(s => ({ sig: s.sig.slice(0, 8), atMs: _int(s.atMs), esrch: s.esrch === true })) : []
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _plain(text) {
|
|
173
|
+
return String(text).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function _lost(reason) {
|
|
177
|
+
return { state: "supervisor-lost", reason: reason, stderr: _plain(_supervisorErr).slice(0, 4096) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The supervisor is gone without a valid result: whatever it started is
|
|
181
|
+
// ended by the detached reaper, and the result says so.
|
|
182
|
+
function _reap(reason) {
|
|
183
|
+
if (_pgid !== 0) {
|
|
184
|
+
Quickshell.execDetached(["/usr/bin/python3", "-I", "-S", "-B", supervisor, "--kill-group", String(_pgid), "--grace-ms", String(graceMs)])
|
|
185
|
+
reason += "; the group " + _pgid + " was sent TERM and, after the grace, KILL by a detached reaper"
|
|
186
|
+
}
|
|
187
|
+
_finish(_lost(reason))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _onExited(code, status) {
|
|
191
|
+
if (_state !== "running") return
|
|
192
|
+
_backstop.stop()
|
|
193
|
+
if (_result) { _finish(_result); return }
|
|
194
|
+
if (code === 2 && /can.t open file/.test(_supervisorErr)) { _reap("run-supervisor.py is not readable at " + supervisor); return }
|
|
195
|
+
_reap("the supervisor " + (status === 1 ? "died on signal " : "exited ") + code + " without a result")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _onRunningChanged() {
|
|
199
|
+
// Never started: the interpreter itself could not be run (stock
|
|
200
|
+
// Omarchy has it; docs/BLOCKS.md says why it is checked anyway).
|
|
201
|
+
if (!_proc.running && _state === "running" && !_started) {
|
|
202
|
+
_backstop.stop()
|
|
203
|
+
_finish({ state: "python-missing", reason: "/usr/bin/python3 could not be started" })
|
|
204
|
+
} else if (!_proc.running && _pending) { _pending = false; start() }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function _finish(result) {
|
|
208
|
+
_state = "done"
|
|
209
|
+
result.pgid = _pgid
|
|
210
|
+
result.ms = _now()
|
|
211
|
+
finished(result)
|
|
212
|
+
if (_pending && !_proc.running) { _pending = false; start() }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
property Timer _backstop: Timer {
|
|
216
|
+
onTriggered: {
|
|
217
|
+
if (run._pgid !== 0) Quickshell.execDetached(["/usr/bin/kill", "-s", "KILL", "--", "-" + run._pgid])
|
|
218
|
+
run._proc.signal(9)
|
|
219
|
+
run._finish(run._lost("no result " + run._backstop.interval + " ms after start; the group was sent KILL"))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
property Process _proc: Process {
|
|
224
|
+
clearEnvironment: true
|
|
225
|
+
stdinEnabled: true
|
|
226
|
+
stdout: SplitParser { splitMarker: ""; onRead: text => run._chunk(text) }
|
|
227
|
+
stderr: SplitParser { splitMarker: ""; onRead: text => { if (run._supervisorErr.length < 4096) run._supervisorErr += String(text).slice(0, 4096) } }
|
|
228
|
+
onStarted: { run._started = true; run._proc.write(run._token + "\n") }
|
|
229
|
+
onExited: (code, status) => run._onExited(code, status)
|
|
230
|
+
onRunningChanged: run._onRunningChanged()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Component.onDestruction: {
|
|
234
|
+
// The Process destructor SIGKILLs the supervisor right after this, so
|
|
235
|
+
// the group is ended by a detached reaper: TERM, the grace, KILL. A
|
|
236
|
+
// run whose leader this file never learned is still behind the
|
|
237
|
+
// supervisor's gate and exits unrun when the supervisor dies.
|
|
238
|
+
if (_state === "running" && _pgid !== 0) {
|
|
239
|
+
Quickshell.execDetached(["/usr/bin/python3", "-I", "-S", "-B", supervisor, "--kill-group", String(_pgid), "--grace-ms", String(graceMs)])
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|