@vibelord/lanes 1.0.1 → 1.0.3
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/package.json +1 -1
- package/payload/lanes/README.md +37 -7
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +664 -50
- package/payload/lanes/client/ghostty/common.conf +3 -0
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
- package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
- package/payload/lanes/client/ghostty/omarchy.conf +3 -2
- package/payload/lanes/client/lanes-launch +18 -3
- package/payload/lanes/client/lanes-window +17 -1
- package/payload/lanes/container/env.sh +1 -1
- package/payload/lanes/container/hooks/claude-settings.json +10 -0
- package/payload/lanes/container/hooks/codex-hooks.json +22 -0
- package/payload/lanes/container/lane +310 -26
- package/payload/lanes/container/lane-signal +173 -0
- package/payload/lanes/docs/ARCHITECTURE.md +87 -13
- package/payload/lanes/docs/DESIGN.md +182 -4
- package/payload/lanes/docs/INSTALL.md +37 -2
- package/payload/lanes/docs/RUNBOOK.md +458 -39
- package/payload/lanes/docs/SHARING.md +240 -5
- package/payload/lanes/holders/exec.sh +2 -5
- package/payload/lanes/holders.lock +13 -0
- package/payload/lanes/install/enable-studio-host.sh +14 -0
- package/payload/lanes/install/host-install.sh +15 -3
- package/payload/lanes/install/services.py +128 -4
- package/payload/lanes/install.sh +108 -3
- package/payload/lanes/libexec/agent_state.py +32 -0
- package/payload/lanes/libexec/cursor_sync.py +10 -2
- package/payload/lanes/libexec/economy_poll.py +685 -0
- package/payload/lanes/libexec/guard.py +27 -1
- package/payload/lanes/libexec/lanes_lib.py +285 -38
- package/payload/lanes/libexec/notify.py +317 -0
- package/payload/lanes/libexec/registrations.py +1 -1
- package/payload/lanes/libexec/session_actions.py +7 -8
- package/payload/lanes/libexec/sidebar.py +911 -218
- package/payload/lanes/libexec/statusd.py +258 -74
- package/payload/lanes/libexec/studio_host.py +1608 -0
- package/payload/lanes/libexec/tokens.py +38 -7
- package/payload/lanes/libexec/view.sh +13 -3
- package/payload/lanes/systemd/lanes-economy.service +20 -0
- package/payload/lanes/systemd/lanes-notify.service +19 -0
- package/payload/lanes/systemd/lanes-studio-host-enrol.service +11 -0
- package/payload/lanes/systemd/lanes-studio-host-enrol.timer +10 -0
- package/payload/lanes/systemd/lanes-studio-host.service +37 -0
- package/payload/lanes/systemd/lanes-sync.service +3 -0
- package/payload/lanes/web/README.md +83 -4
- package/payload/lanes/web/index.html +2 -1
- package/payload/lanes/web/mobile.css +53 -198
- package/payload/lanes/web/mobile.js +87 -55
- package/payload/lanes/zellij/config.kdl +67 -16
- package/payload/manifest.json +5 -3
- package/payload/lanes/libexec/legacy_sessions.py +0 -130
|
@@ -30,9 +30,10 @@ An existing in-container Studio Host reads `~/.lz/registry.json` by default.
|
|
|
30
30
|
That is the registered-lane projection written by `lanes sync`. No additional
|
|
31
31
|
configuration is needed for this source.
|
|
32
32
|
|
|
33
|
-
For a Lanes server hosting several workspace containers,
|
|
34
|
-
|
|
35
|
-
`~/.config/throttle-rc/studio-host.toml
|
|
33
|
+
For a Lanes server hosting several workspace containers, prefer the installer-owned
|
|
34
|
+
[co-located Studio Host](#co-located-studio-host-one-host-per-lanes-server) below. Hand-run, the server's
|
|
35
|
+
`~/.config/throttle-rc/studio-host.toml` reads (add `mode = "lanes-server"` under `[inventory]` for the
|
|
36
|
+
interactive adapter; without it the Host serves the read-only sharing capture only):
|
|
36
37
|
|
|
37
38
|
```toml
|
|
38
39
|
server = "https://throttle.example.com"
|
|
@@ -72,6 +73,236 @@ eight-hour Host session. If you deployed that candidate, use the
|
|
|
72
73
|
before deploying current builds. The normal migrator correctly refuses that
|
|
73
74
|
journal; do not edit its digest or reset the database.
|
|
74
75
|
|
|
76
|
+
## Co-located Studio Host (one Host per lanes server)
|
|
77
|
+
|
|
78
|
+
On a lanes server — the account that runs `lanes-statusd` and reaches every workspace container's tmux sockets
|
|
79
|
+
through `/proc/<container-pid>/root/tmp/tmux-<uid>/` — ONE Studio Host serves every board, installed and supervised
|
|
80
|
+
by the lanes installer as `lanes-studio-host.service` (design: `docs/design/throttle-studio.md` §8.10). It replaces
|
|
81
|
+
one Host per workspace container and the hand-built `lanes-sharing-host.service`. Laptops keep the in-container Host
|
|
82
|
+
and `throttle-rc studio service`; never run both on one machine (the enable step refuses).
|
|
83
|
+
|
|
84
|
+
**Status of the pieces.** Assembled on the integrated tree (2026-10-04): the installer below
|
|
85
|
+
(`tests/unit/test_studio_host.py`), the Host's interactive `lanes-server` adapter, `studio publish-key` /
|
|
86
|
+
`adopt-receipt` and the platform's `server studio-adopt-host`. Only a `throttle-rc` built before 2026-10-04 refuses
|
|
87
|
+
those verbs `not_assembled`, and the enable step then stops with exit 1 before enabling anything. A live lanes
|
|
88
|
+
server and a live deploy are operator-gated; see "Deploy-provisioned enrolment" for the handoff's precondition.
|
|
89
|
+
|
|
90
|
+
### 1. Install the binary
|
|
91
|
+
|
|
92
|
+
No `node/<sha12>` release that carries the co-location CLI is pinned yet, so install a locally built binary. It is
|
|
93
|
+
hashed, copied to `~/.local/share/lanes/bin/throttle-rc-local-<sha12>`, re-hashed, and selected through the link
|
|
94
|
+
`~/.local/share/lanes/bin/throttle-rc` that the unit runs; `throttle-rc.pin.json` beside it records the digest:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
cargo build --release --locked -p throttle-rc # on a build machine; copy the file to the server
|
|
98
|
+
lanes studio-host install --from ./throttle-rc --sha256 <the sha256 you computed where you built it>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Once a release is tagged, its line goes into `holders.lock` as an `opt-in` pin (the file's comment has the
|
|
102
|
+
template) and `lanes studio-host install` with no `--from` downloads it — `gh release download` for the private
|
|
103
|
+
repository, else `curl` — and refuses unless BOTH the tarball and the binary inside it match their digests.
|
|
104
|
+
`./install.sh --server` never downloads an opt-in pin. Nothing is restarted by `install`.
|
|
105
|
+
|
|
106
|
+
### 2. Enable
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com --dry-run # the plan; changes nothing
|
|
110
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
In order, and refusing (exit 3) before anything changes when another Host unit is enabled or running
|
|
114
|
+
(`another_host_unit`), the hand-built unit exists (`hand_built_unit_present`, below), no binary is installed
|
|
115
|
+
(`no_binary`) or an existing config is not in lanes-server mode (`config_mode`, fixed by `--set-mode`):
|
|
116
|
+
|
|
117
|
+
1. writes `~/.config/throttle-rc/studio-host.toml` (0600) in lanes-server mode if there is none —
|
|
118
|
+
`[inventory] mode = "lanes-server"`, `interactive = true`, `sharing_state_dir = $LANES_STATE`,
|
|
119
|
+
`sharing_container_uid = 1000` (`--container-uid`), `machine_id` from `--machine-id` or `<hostname>-lanes`.
|
|
120
|
+
An existing file is never rewritten except, with `--set-mode`, its one `mode` key (a `.bak-<time>` is kept);
|
|
121
|
+
2. runs `throttle-rc studio publish-key --out ~/.local/state/lanes/studio-host/public.jwk` — the machine key is
|
|
122
|
+
created HERE if absent and only its PUBLIC half (0644) is written for the deploy;
|
|
123
|
+
3. enrols, if it can: a deploy receipt at `~/.local/state/lanes/studio-host/receipt.json` (`--receipt PATH`, which is
|
|
124
|
+
remembered in the enrolment record so the enrol timer and `status` watch that path too) is installed
|
|
125
|
+
with `throttle-rc studio adopt-receipt --config <the config>`, which refuses a receipt issued for another key
|
|
126
|
+
(`receipt_key_mismatch`) or by another server URL (`receipt_server_mismatch`); else a registration key
|
|
127
|
+
file at `~/.config/throttle-rc/registration.key` (`--registration-key-file`, 0600, remembered like `--receipt`) is
|
|
128
|
+
handed to `throttle-rc studio enroll <server> --registration-key-file` — the script names the file and never opens
|
|
129
|
+
it (its readability is access(2) and the timer's memory is the file's stat identity, never a digest of the secret;
|
|
130
|
+
review round 5). A refused or failed receipt/key does not stop the next step: the units are still rendered and enabled, and the
|
|
131
|
+
run exits with the refusal (3, or 1 for an operational failure) saying the unit is waiting;
|
|
132
|
+
4. renders the lanes units (`install/services.py`), enables `lanes-studio-host.service` and
|
|
133
|
+
`lanes-studio-host-enrol.timer`;
|
|
134
|
+
5. enrolled: runs `throttle-rc studio host --check`, then starts the Host (restarts it when it was running, so a new
|
|
135
|
+
config or binary is in force) and exits 0 only when it is still running `--settle` seconds (default 5) later.
|
|
136
|
+
Not enrolled (and nothing refused): exits 0 **waiting for enrolment** — the unit is enabled and exits 3 (a clean
|
|
137
|
+
stop, never a restart loop) until a receipt or key arrives.
|
|
138
|
+
|
|
139
|
+
`lanes-studio-host-enrol.timer` runs `enable-studio-host.sh --retry` every 5 minutes. It acts only on something
|
|
140
|
+
new — a receipt it has not adopted and has not refused (typed, exit 3) within the last hour under the same config, a
|
|
141
|
+
key file not tried in the last hour (a chmod/chown of it counts as new), a machine key that is gone (it publishes a
|
|
142
|
+
new one — over an existing `credential.json` only as described below), an enrolment the Host has not been started with — so a Host that stopped on a revoked enrolment is not restarted into the same refusal. The
|
|
143
|
+
hour applies to the timer only: a manual run of the script always tries the registration key and always tries a
|
|
144
|
+
refused receipt — enrolled or not (review round 4: an enrolled machine used to leave a refused NEW receipt alone for
|
|
145
|
+
ever). A receipt refused with `receipt_server_mismatch` is tried again by the timer as soon as `studio-host.toml`
|
|
146
|
+
changes (the deploy re-emits the SAME bytes, so fixing the Host's `server` is enough); an operational failure of a
|
|
147
|
+
RECEIPT (exit 1/2: a read-only disk, an unloadable config, a timeout) is never remembered. A registration key is
|
|
148
|
+
remembered for the hour after ANY attempt (each try may spend one of its uses), with what `studio enroll` answered:
|
|
149
|
+
`status` says `key-refused` (code, exit, time, next retry) instead of `waiting-for-enrolment`, and a refused key
|
|
150
|
+
SOURCE (`registration_key_source_refused`, the binary's exit 2) is the installer's typed exit 3. A new receipt for
|
|
151
|
+
THIS machine's key is adopted over an existing `credential.json` for the same key (a relabel, a new host row after a
|
|
152
|
+
platform reset); `adopt-receipt` refuses one that would replace a receipt issued for ANOTHER key
|
|
153
|
+
(`receipt_replaces_enrolment`) unless `--new-key` says so, and refuses a receipt for an enrolment this machine
|
|
154
|
+
MEASURED revoked (`receipt_for_revoked_key`). `status` says `receipt-refused` for a refused receipt on disk whether
|
|
155
|
+
or not an older credential exists. A `credential.json` whose machine key is gone, or is not the key this machine
|
|
156
|
+
published, is an enrolment only when it binds the published key (`status --json` `key_binding`). The installer
|
|
157
|
+
re-keys over it — publishes a new key, then `adopt-receipt --new-key` / `studio enroll --new-key` — ONLY when this
|
|
158
|
+
machine MEASURED that host row revoked (the Host's `state.json`); otherwise it refuses `credential_for_gone_key`,
|
|
159
|
+
publishes nothing, and `status` says `credential-for-gone-key`: revoke the row (console, or `throttle-rc studio revoke
|
|
160
|
+
--id <host>`), then `lanes studio-host re-key --yes` — the explicit decision to enrol this machine under a new key
|
|
161
|
+
(review round 6: the installer used to decide that itself). A receipt, key or config this account cannot read is a typed refusal
|
|
162
|
+
(`receipt_unreadable`/`key_unreadable`/`config_unreadable`, exit 3) naming the file's owner and mode — never a
|
|
163
|
+
traceback; `status` says `receipt-unreadable`/`key-unreadable`/`config-unreadable`. Both studio units ALWAYS pin `XDG_CONFIG_HOME`/`XDG_DATA_HOME` — the roots this enable step ran with
|
|
164
|
+
(recorded in `~/.local/state/lanes/studio-host/xdg-roots.json`; a later `./install.sh --server` renders the studio
|
|
165
|
+
units from that record, never from its own shell, and warns when they differ), else `%h/.config` and
|
|
166
|
+
`%h/.local/share` — and every `throttle-rc` child the installer runs gets the same two, so neither the user manager's
|
|
167
|
+
environment nor a reinstall from another shell points the supervised Host at another `credential.json`. A manual run
|
|
168
|
+
on an enrolled machine whose `studio host --check` is red ONLY on the refusal the Host's own last run recorded (the
|
|
169
|
+
owner's tunnel cap, a build mismatch fixed by `lanes studio-host install`, three displacements) starts the Host
|
|
170
|
+
anyway — its first phase replaces that record, and the settle window reports a refusal that still holds. The unit files are
|
|
171
|
+
written where the user MANAGER reads them: `$XDG_CONFIG_HOME/systemd/user` when `systemctl --user show-environment`
|
|
172
|
+
carries it, else `~/.config/systemd/user`. The adopt refusal's code is read from `throttle-rc studio adopt-receipt`'s
|
|
173
|
+
own line (`throttle-rc studio adopt-receipt: refused <code>: …`); a line with no code is remembered as `unknown`.
|
|
174
|
+
|
|
175
|
+
### Deploy-provisioned enrolment (no human step)
|
|
176
|
+
|
|
177
|
+
The platform deploy binds the published PUBLIC key to its owner (`throttle-rc server studio-adopt-host`, design
|
|
178
|
+
§8.10 C4) and writes the receipt where this server finds it:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
THROTTLE_RC_STUDIO_ADOPT_JWK=/home/dev/.local/state/lanes/studio-host/public.jwk
|
|
182
|
+
THROTTLE_RC_STUDIO_ADOPT_OWNER=owner@example.com
|
|
183
|
+
THROTTLE_RC_STUDIO_ADOPT_RECEIPT=/home/dev/.local/state/lanes/studio-host/receipt.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The step is `deploy/studio-adopt-host.sh`, sourced by `deploy/deploy-ceobox-rc.sh`; a receipt path whose directory is
|
|
187
|
+
absent or not writable is refused `receipt_dir_unusable` (exit 2) before the platform is asked.
|
|
188
|
+
|
|
189
|
+
Run the deploy **as the lanes account**: `~/.local/state/lanes/studio-host/` is kept at 0700 and the deploy writes
|
|
190
|
+
the receipt 0600 as whoever ran it, so a deploy run by another account cannot read `public.jwk` and leaves a receipt
|
|
191
|
+
the Host cannot read (DEF-1429 in `docs/defect-ledger.md`; a group-readable handoff directory is the unbuilt
|
|
192
|
+
alternative). `--receipt PATH` points the installer at another path the lanes account can read; it is remembered,
|
|
193
|
+
so the enrol timer adopts a receipt that appears there later and `status` reports it (naming the default path again
|
|
194
|
+
forgets it). The receipt carries no secret.
|
|
195
|
+
|
|
196
|
+
**Precondition — one filesystem (DEF-1441).** The handoff is a file exchange, so the deploy must run where both
|
|
197
|
+
paths above exist: on the docker host, as the lanes account, from a checkout of this repository (the compose build
|
|
198
|
+
context), with an env file that account can read named by `THROTTLE_RC_ENV_FILE` — the script's default is the
|
|
199
|
+
literal `/home/coder/.config/throttle-rc/ceobox.env`, which a lanes account named anything else does not have:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
THROTTLE_RC_ENV_FILE=~/.config/throttle-rc/ceobox.env bash deploy/deploy-ceobox-rc.sh # as the lanes account
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Copying that env file (it holds the database password) to the lanes account is a secret-custody step the operator
|
|
206
|
+
owns. On ceobox the deploy is normally run from the Coder workspace container, which has neither `/home/dev` nor the
|
|
207
|
+
lanes account, so the zero-step path above does NOT execute there as-is (`[INFERRED-NOT-EXECUTABLE]`, design §8.10
|
|
208
|
+
C4 "Topology precondition"); until one of those two is arranged, use the registration-key path. An owner who has not signed in yet makes the adopt exit 3 and the deploy
|
|
209
|
+
retries next time; the timer picks the receipt up within 5 minutes of it appearing. `lanes studio-host status` then
|
|
210
|
+
says `enrolment: via deploy`.
|
|
211
|
+
|
|
212
|
+
### After the owner revokes this machine
|
|
213
|
+
|
|
214
|
+
A revoked key is never adopted again: the Host stops (exit 3) and later deploys WARN `host_revoked` and finish. Give
|
|
215
|
+
the machine a new key:
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
lanes studio-host re-key # the plan; changes nothing (exit 2)
|
|
219
|
+
lanes studio-host re-key --yes # stop the Host; remove the machine key, credential.json, public.jwk, receipt.json
|
|
220
|
+
# and the Host's refused state; publish a NEW public key
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then deploy again: the deploy adopts the new key, and the timer installs the receipt and starts the Host. A new key
|
|
224
|
+
is a new machine in the console (design §8.7 "What a new key costs"); any `throttle-rc` login session on this
|
|
225
|
+
account was bound to the old key and must log in again.
|
|
226
|
+
|
|
227
|
+
### Check it
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
lanes studio-host status # state: running | running-unenrolled | waiting-for-enrolment | receipt-refused | key-refused | credential-for-gone-key | receipt-unreadable | key-unreadable | config-unreadable | not-enabled | stopped | failed | conflict | not-installed | unknown
|
|
231
|
+
lanes studio-host status --check # also runs `throttle-rc studio host --check`; its exit binds (below)
|
|
232
|
+
journalctl --user -u lanes-studio-host.service -n 30
|
|
233
|
+
journalctl --user -u lanes-studio-host-enrol.service -n 20 # the enrol timer's one line per tick
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`waiting-for-enrolment` is said only while the Host unit is enabled AND the enrol timer is active; otherwise the
|
|
237
|
+
machine is `not-enabled` (nothing retries: re-run the enable script). `receipt-refused` names a receipt on disk the
|
|
238
|
+
installer recorded as refused (its code, when, and when the timer tries it again — an hour, or at once after
|
|
239
|
+
`studio-host.toml` changes); its remedy is the code's (`receipt_server_mismatch`: the config's `server`;
|
|
240
|
+
`receipt_key_mismatch`: redeploy with this machine's public key), then `sh install/enable-studio-host.sh` to try now.
|
|
241
|
+
`key-refused` is the registration key's twin (`studio enroll`'s code and exit; fix the file, its mode or the key, then
|
|
242
|
+
re-run the script — the timer also retries a changed file on its next tick). `credential-for-gone-key` is a receipt
|
|
243
|
+
on disk for a key this machine no longer holds, for a row it has not measured revoked: revoke it, then
|
|
244
|
+
`lanes studio-host re-key --yes`.
|
|
245
|
+
|
|
246
|
+
`status` re-hashes the selected binary against its record, names the config's mode, the enrolment's `via`, both
|
|
247
|
+
units and any other Host unit, and says `unknown` — never a guess — when the user manager does not answer. It exits
|
|
248
|
+
0 only when the Host runs enrolled (an active unit with no enrolment is `running-unenrolled`, exit 1; with
|
|
249
|
+
`--check`, an active unit whose `studio host --check` is non-zero is `running-check-failed`, exit 1). A caller without
|
|
250
|
+
`XDG_RUNTIME_DIR` (a `sudo -u` or cron shell) reaches the manager at `/run/user/<uid>` when that exists, and
|
|
251
|
+
`re-key` refuses `manager_unreachable` rather than remove the key under a Host whose state the manager did not
|
|
252
|
+
answer, and `another_host_running` (exit 3, nothing changed) while `lanes-sharing-host.service` or
|
|
253
|
+
`throttle-rc-studio-host.service` is active: every Host unit of this account signs with the same machine key, so
|
|
254
|
+
re-keying under the hand-built Host would end its sessions. Migrate (or stop) that unit first. It refuses
|
|
255
|
+
`rekey_path_unremovable` (exit 3, nothing changed) when a file it must remove — e.g. a remembered `--receipt` in the
|
|
256
|
+
deploying account's directory — sits in a directory this account cannot write, and it removes the machine key LAST.
|
|
257
|
+
`status` also reports an installed unit that has lost a load-bearing line
|
|
258
|
+
(`SuccessExitStatus=3`, `KillSignal=SIGINT`, `KillMode=process`, the slice, the statusd ordering).
|
|
259
|
+
|
|
260
|
+
### Migrating the hand-built `lanes-sharing-host.service`
|
|
261
|
+
|
|
262
|
+
ceobox's first Host was a hand-written unit running `~/.local/bin/throttle-rc-lanes studio host` with a sharing-only
|
|
263
|
+
config (no `mode`, so in-container). That Host predates enrolment receipts: it authenticated with a stored human
|
|
264
|
+
login SESSION beside its machine key, and holds NO `credential.json` (docs/handoffs/2026-09-22-lanes-sharing.md).
|
|
265
|
+
The lanes unit never uses a session, so such a machine is enrolled as part of the migration. Read the unit before
|
|
266
|
+
replacing it:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
systemctl --user cat lanes-sharing-host.service
|
|
270
|
+
lanes studio-host install --from ./throttle-rc --sha256 <digest>
|
|
271
|
+
sh install/enable-studio-host.sh --migrate-hand-built --dry-run
|
|
272
|
+
sh install/enable-studio-host.sh --migrate-hand-built
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`--migrate-hand-built` refuses a unit that does not run `studio host`, or that names a `--config` other than
|
|
276
|
+
`~/.config/throttle-rc/studio-host.toml` (`%h`, `${HOME}`, `$HOME` and `~` are resolved to the account's home before
|
|
277
|
+
the comparison). Otherwise it sets `mode = "lanes-server"` in the existing config (keeping a `.bak-<time>`), runs the
|
|
278
|
+
replacement's own `throttle-rc studio host --check` on that config (an enrolled machine), and only when it passes
|
|
279
|
+
saves the unit's text to `~/.local/state/lanes/studio-host/migrated/`, runs
|
|
280
|
+
`systemctl --user disable --now lanes-sharing-host.service`, and then starts the new unit. A refusing check
|
|
281
|
+
(`host_check_failed`; e.g. a version-1 credential whose key binding is `unchecked`) leaves the hand-built unit
|
|
282
|
+
running and enabled, restores the config from the `.bak`, and names both — as does ANY failure before the
|
|
283
|
+
hand-built unit is stopped (a failed `publish-key`, an unreadable receipt or key, a failed `disable --now`; review
|
|
284
|
+
round 5: its older binary refuses the `mode` key at its next start). A RUNNING hand-built unit on a machine with no
|
|
285
|
+
`credential.json` (ceobox's shape) is enrolled first, and the **published public key is the first artifact**:
|
|
286
|
+
|
|
287
|
+
1. `sh install/enable-studio-host.sh --migrate-hand-built` with nothing to enrol from installs the binary, publishes
|
|
288
|
+
this machine's PUBLIC key to `~/.local/state/lanes/studio-host/public.jwk` (the existing machine key is loaded,
|
|
289
|
+
never rewritten; the hand-built Host, its unit and its config are untouched) and refuses
|
|
290
|
+
`hand_built_host_unenrolled` (exit 3), naming the `THROTTLE_RC_STUDIO_ADOPT_*` values for the deploy;
|
|
291
|
+
2. the deploy adopts that key and writes the receipt to the receipt path (see the runbook's one-filesystem
|
|
292
|
+
precondition; a deploy that runs before step 1 defers — "not published yet" — and finishes) — or put the owner's
|
|
293
|
+
registration key in `~/.config/throttle-rc/registration.key` (0600) instead;
|
|
294
|
+
3. re-run `sh install/enable-studio-host.sh --migrate-hand-built`: it installs the receipt (or enrols) FIRST, with the
|
|
295
|
+
same machine key, while the hand-built Host still runs, and only then checks, stops and replaces it (a refused
|
|
296
|
+
enrolment leaves it running with its config restored). The machine key (`~/.local/share/throttle-rc/machine/key.json`)
|
|
297
|
+
is never read or moved, and an existing enrolment receipt is kept: the new Host serves as the same machine (the same
|
|
298
|
+
key, so the same thumbprint and machine id once enrolled). The old unit file and the old binary are left for
|
|
299
|
+
you to delete. A later run treats the disabled, stopped unit as migrated while the enrolment record under
|
|
300
|
+
`~/.local/state/lanes/studio-host/` survives; if that record is lost, the enrol timer still never refuses on the
|
|
301
|
+
stopped, disabled unit, and a manual run asks for `--migrate-hand-built` again (idempotent: it saves the text again).
|
|
302
|
+
|
|
303
|
+
**Rollback:** `systemctl --user disable --now lanes-studio-host.service lanes-studio-host-enrol.timer`, restore the
|
|
304
|
+
config's `.bak-<time>`, then `systemctl --user enable --now lanes-sharing-host.service`.
|
|
305
|
+
|
|
75
306
|
## Enable publishers, then expose workspaces
|
|
76
307
|
|
|
77
308
|
1. Sign in to Throttle and open **Shared lanes → Manage sharing**.
|
|
@@ -91,7 +322,7 @@ clears on its next refresh. Previously read output cannot be recalled.
|
|
|
91
322
|
## What viewers can see
|
|
92
323
|
|
|
93
324
|
- The selected workspace's current and future **registered lanes**. Unregistered
|
|
94
|
-
sessions,
|
|
325
|
+
sessions, tool tabs and unrelated workspaces are excluded.
|
|
95
326
|
- Lane names and observed activity; “active” describes recent terminal output,
|
|
96
327
|
not a build verdict.
|
|
97
328
|
- For tmux lanes, a selectable pane and its latest 2,000 lines, capped at
|
|
@@ -112,7 +343,11 @@ interactive `/phone` endpoint.
|
|
|
112
343
|
|
|
113
344
|
Focused regression coverage lives in the platform's `pg_http_studio` sharing
|
|
114
345
|
tests, `pg_studio_schema`, the Host's `studio_host_sharing` real-tmux test, and
|
|
115
|
-
`views/shared-lanes/view.test.mjs`. The
|
|
346
|
+
`views/shared-lanes/view.test.mjs`. The co-located Host's installer pieces (the
|
|
347
|
+
rendered unit against `tests/unit/golden/lanes-studio-host.service`, the opt-in
|
|
348
|
+
pin, `lanes studio-host`, `install/enable-studio-host.sh` and the hand-built
|
|
349
|
+
migration) are `tests/unit/test_studio_host.py`, run with the other lanes unit
|
|
350
|
+
tests: `python3 -m unittest discover -s lanes/tests/unit`. The Node tests run without an npm install:
|
|
116
351
|
|
|
117
352
|
```sh
|
|
118
353
|
node --test crates/throttle-platform/src/http/console/views/shared-lanes/view.test.mjs
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# View-owned displays; interactive shells are persistent tmux sessions created by New +.
|
|
3
|
-
# usage: exec.sh <container> monitor
|
|
3
|
+
# usage: exec.sh <container> monitor [cwd] (the `legacy` view of pre-persistence Docker shells was retired by
|
|
4
|
+
# plan Task 19: a leftover `exec … legacy` resolve file now ends as an unknown helper, and `lanes gc` removes it)
|
|
4
5
|
set -u
|
|
5
6
|
container=${1:-} kind=${2:-} cwd=${3:-/home/coder}
|
|
6
7
|
case "$container" in ''|-*|*[!A-Za-z0-9_.-]*) echo 'invalid container' >&2; exit 64 ;; esac
|
|
7
|
-
if [ "$kind" = legacy ]; then
|
|
8
|
-
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
9
|
-
exec python3 -S -E "$repo/libexec/legacy_sessions.py" view "$container" "$cwd"
|
|
10
|
-
fi
|
|
11
8
|
case "$cwd" in /*) ;; *) cwd=/home/coder ;; esac
|
|
12
9
|
case "$cwd" in *[!A-Za-z0-9_./-]*) cwd=/home/coder ;; esac
|
|
13
10
|
common="-it -u coder -e TERM=xterm-256color -e COLORTERM=truecolor -e LANG=C.UTF-8"
|
|
@@ -3,3 +3,16 @@
|
|
|
3
3
|
# zellij 0.45.1: tarball digest matches GitHub's asset digest; inner binary d006c521…19dd matches the published .sha256sum.
|
|
4
4
|
zellij 0.45.1 40bcc2e03f5d5ae8e054e39f676081fe12ab70871506996ba595834c3718eefc https://github.com/zellij-org/zellij/releases/download/v0.45.1/zellij-x86_64-unknown-linux-musl.tar.gz
|
|
5
5
|
zmx 0.8.1 dfd75720b942466f28870731cc86dbc07afa72fb8f3bd5eeb4ff707e4eecebe8 https://github.com/neurosnap/zmx/releases/download/v0.8.1/zmx-0.8.1-linux-x86_64.tar.gz
|
|
6
|
+
# glow 3.0.0 (markdown preview, `lanes preview`): tarball digest matches GitHub's asset digest and the release's checksums.txt;
|
|
7
|
+
# the binary sits in glow_3.0.0_Linux_x86_64/. Installed only when no glow is on PATH; without either, the preview uses less -R.
|
|
8
|
+
glow 3.0.0 13e05e4b2acc18d2aee44291aefe6325b077ec321b631a0cfa780e8e3bc33f78 https://github.com/charmbracelet/glow/releases/download/v3.0.0/glow_3.0.0_Linux_x86_64.tar.gz
|
|
9
|
+
#
|
|
10
|
+
# OPT-IN pins carry three more fields and are skipped by host-install.sh; the step that needs one installs it:
|
|
11
|
+
# name version sha256(tarball) url opt-in member-inside-the-tarball sha256(member)
|
|
12
|
+
# throttle-rc is the co-located Studio Host (docs/SHARING.md "Co-located Studio Host"; `lanes studio-host install`
|
|
13
|
+
# downloads, checks BOTH digests and selects it). It comes from a `node/<sha12>` GitHub prerelease
|
|
14
|
+
# (docs/runbooks/node-release-asset.md): version = node-<sha12>, the asset sidecars give both digests. No
|
|
15
|
+
# node/<sha12> release containing the co-location CLI exists yet, so no line is active: until one is tagged, install
|
|
16
|
+
# a locally built binary with `lanes studio-host install --from PATH [--sha256 HEX]` (also sha256-verified).
|
|
17
|
+
# Template (fill from the release's two .sha256 sidecars, then uncomment):
|
|
18
|
+
# throttle-rc node-<sha12> <tarball-sha256> https://github.com/bwllaming/bw-tools/releases/download/node/<sha12>/throttle-rc-x86_64-unknown-linux-gnu.tar.gz opt-in throttle-rc-x86_64-unknown-linux-gnu/throttle-rc <binary-sha256>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Opt-in: run the Throttle Studio Host co-located on this lanes server (docs/SHARING.md "Co-located Studio Host").
|
|
3
|
+
# As the workspace account. Writes the Host config in lanes-server mode if absent, publishes this machine's PUBLIC
|
|
4
|
+
# key for the deploy, adopts a deploy receipt or enrols with a registration key file, and enables
|
|
5
|
+
# lanes-studio-host.service — or leaves it enabled and WAITING for enrolment (exit 0; exit 3 when a receipt or key
|
|
6
|
+
# was refused, the units still enabled so lanes-studio-host-enrol.timer retries).
|
|
7
|
+
# Never prints a key. --dry-run changes nothing. --help lists the options.
|
|
8
|
+
set -eu
|
|
9
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
10
|
+
case "$(uname -s)" in Linux) ;; *) echo 'The co-located Studio Host runs on a Linux lanes server.' >&2; exit 1 ;; esac
|
|
11
|
+
[ "$(id -u)" != 0 ] || { echo 'Run as the workspace account, not root.' >&2; exit 1; }
|
|
12
|
+
command -v python3 >/dev/null 2>&1 || { echo 'Missing prerequisite: python3 (see docs/INSTALL.md)' >&2; exit 1; }
|
|
13
|
+
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
|
14
|
+
exec python3 "$repo/libexec/studio_host.py" enable "$@"
|
|
@@ -22,7 +22,7 @@ for command in python3 curl tar sha256sum infocmp tic install systemctl docker t
|
|
|
22
22
|
command -v "$command" >/dev/null 2>&1 || { echo "Missing prerequisite: $command (see docs/INSTALL.md)" >&2; exit 1; }
|
|
23
23
|
done
|
|
24
24
|
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else "Python 3.9+ is required")'
|
|
25
|
-
export XDG_RUNTIME_DIR
|
|
25
|
+
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
|
26
26
|
if [ "$start" = 1 ]; then
|
|
27
27
|
systemctl --user show-environment >/dev/null
|
|
28
28
|
python3 -c 'import subprocess; subprocess.run(["docker", "info", "--format", "{{.ServerVersion}}"], check=True, timeout=15)'
|
|
@@ -34,10 +34,18 @@ fi
|
|
|
34
34
|
share=${LANES_SHARE:-$HOME/.local/share/lanes}
|
|
35
35
|
state=${LANES_STATE:-$HOME/.local/state/lanes}
|
|
36
36
|
mkdir -p "$share/bin" "$share/dl" "$state/flags"
|
|
37
|
-
|
|
37
|
+
# The view server's PATH (bin/lanes clean_env): the Run pane that renders `lanes preview` sees this, never the
|
|
38
|
+
# installer's PATH, so a glow in ~/.local/bin or ~/go/bin does not count as installed.
|
|
39
|
+
pane_path=$share/bin:$repo/bin:/usr/local/bin:/usr/bin:/bin
|
|
40
|
+
grep -v '^#' "$repo/holders.lock" | while read -r name version sha url flag _rest; do
|
|
38
41
|
[ -n "$name" ] || continue
|
|
42
|
+
# An `opt-in` pin (the co-located Studio Host's throttle-rc) is installed by the opt-in step that needs it
|
|
43
|
+
# (install/enable-studio-host.sh, `lanes studio-host install`), never by every server install.
|
|
44
|
+
[ "$flag" != opt-in ] || continue
|
|
39
45
|
target=$share/bin/$name-$version
|
|
40
46
|
if [ -x "$target" ]; then echo "ok $name $version"; continue; fi
|
|
47
|
+
# The markdown preview's renderer is optional: a glow the pane can find serves `lanes preview` as well.
|
|
48
|
+
if [ "$name" = glow ] && system_glow=$(PATH=$pane_path; command -v glow); then echo "ok glow (system: $system_glow)"; continue; fi
|
|
41
49
|
archive=$share/dl/$(basename "$url")
|
|
42
50
|
curl -fsSL --retry 3 -o "$archive" "$url"
|
|
43
51
|
actual=$(sha256sum "$archive" | cut -d' ' -f1)
|
|
@@ -45,7 +53,11 @@ grep -v '^#' "$repo/holders.lock" | while read -r name version sha url; do
|
|
|
45
53
|
temporary=$(mktemp -d)
|
|
46
54
|
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
|
|
47
55
|
tar -xzf "$archive" -C "$temporary"
|
|
48
|
-
|
|
56
|
+
binary=$temporary/$name
|
|
57
|
+
# zellij and zmx ship the binary at the top of the tarball; glow nests it in glow_<version>_Linux_x86_64/.
|
|
58
|
+
[ -f "$binary" ] || binary=$(find "$temporary" -type f -name "$name" | head -n 1)
|
|
59
|
+
[ -n "$binary" ] && [ -f "$binary" ] || { echo "no $name binary in $archive" >&2; exit 1; }
|
|
60
|
+
install -m 0755 "$binary" "$target"
|
|
49
61
|
rm -rf "$temporary"
|
|
50
62
|
trap - EXIT HUP INT TERM
|
|
51
63
|
echo "installed $target"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Register this checkout's user services without starting or restarting them."""
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
5
8
|
from pathlib import Path
|
|
6
9
|
import tempfile
|
|
7
10
|
|
|
@@ -14,8 +17,116 @@ def unit_value(value, command=False):
|
|
|
14
17
|
return value.replace('$', '$$') if command else value
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
# The opt-in economy poller's settings (libexec/economy_poll.py). Rendered into lanes-economy.service only, and only
|
|
21
|
+
# when set in the installer's environment; the unit itself is never enabled by an install (operator gate).
|
|
22
|
+
ECONOMY_SETTINGS = ('LANES_THROTTLE_CONTROL', 'LANES_THROTTLE_TOKEN_FILE', 'LANES_THROTTLE_LOG_DIR',
|
|
23
|
+
'LANES_THROTTLE_CONTAINER')
|
|
24
|
+
ECONOMY_UNITS = ('lanes-economy.service',)
|
|
25
|
+
# The opt-in push notifier's config path (libexec/notify.py), rendered into lanes-notify.service only and only when
|
|
26
|
+
# set in the installer's environment (a config FILE path; the ntfy token stays in the file its token_file names).
|
|
27
|
+
NOTIFY_SETTINGS = ('LANES_NOTIFY_CONF',)
|
|
28
|
+
NOTIFY_UNITS = ('lanes-notify.service',)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def economy_settings(environ):
|
|
32
|
+
"""The non-empty LANES_THROTTLE_* settings of `environ` (a token FILE path, never a token)."""
|
|
33
|
+
return {name: environ[name] for name in ECONOMY_SETTINGS if environ.get(name)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def notify_settings(environ):
|
|
37
|
+
"""The non-empty LANES_NOTIFY_* settings of `environ`."""
|
|
38
|
+
return {name: environ[name] for name in NOTIFY_SETTINGS if environ.get(name)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def throttle_rc_path(share):
|
|
42
|
+
"""The co-located Studio Host's binary: a stable link the opt-in installer points at a sha256-verified file
|
|
43
|
+
(`lanes studio-host install`, libexec/studio_host.py). Rendered whether or not it exists yet, like @CADDY@."""
|
|
44
|
+
return Path(share) / 'bin' / 'throttle-rc'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Units that run throttle-rc (or the installer that resolves its paths): they must see the SAME XDG roots the
|
|
48
|
+
# installer used, or the config and credential it wrote are not where the supervised Host looks (review round 1).
|
|
49
|
+
# Review round 3: BOTH roots are pinned ALWAYS — the installer's own, else the defaults as systemd specifiers — never
|
|
50
|
+
# left to the user manager's environment block, which can carry roots the installing shell did not.
|
|
51
|
+
STUDIO_HOST_UNITS = ('lanes-studio-host.service', 'lanes-studio-host-enrol.service')
|
|
52
|
+
XDG_ROOTS = ('XDG_CONFIG_HOME', 'XDG_DATA_HOME')
|
|
53
|
+
XDG_DEFAULTS = {'XDG_CONFIG_HOME': '%h/.config', 'XDG_DATA_HOME': '%h/.local/share'}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Review round 4: the roots the opt-in enable step (install/enable-studio-host.sh) rendered the studio units with,
|
|
57
|
+
# recorded beside its enrolment record. A later server install (re-run for every update, possibly from a shell that
|
|
58
|
+
# does not carry them, e.g. `sudo -u <account> sh` without -i) renders the studio units from THIS record, never from
|
|
59
|
+
# its own environment, so it cannot move the supervised Host away from the config and credential enable wrote.
|
|
60
|
+
XDG_RECORD = Path('studio-host') / 'xdg-roots.json'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def recorded_xdg(state):
|
|
64
|
+
"""The roots enable recorded under $LANES_STATE, or None when it never ran (or the record is unreadable)."""
|
|
65
|
+
try:
|
|
66
|
+
doc = json.loads((Path(state) / XDG_RECORD).read_text())
|
|
67
|
+
except (OSError, ValueError):
|
|
68
|
+
return None
|
|
69
|
+
if not isinstance(doc, dict) or not isinstance(doc.get('roots'), dict):
|
|
70
|
+
return None
|
|
71
|
+
return {name: value for name, value in doc['roots'].items()
|
|
72
|
+
if name in XDG_ROOTS and isinstance(value, str) and value.startswith('/')}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def record_xdg(state, xdg):
|
|
76
|
+
"""Record the roots enable renders the studio units with (an empty dict = the defaults)."""
|
|
77
|
+
path = Path(state) / XDG_RECORD
|
|
78
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
fd, temporary = tempfile.mkstemp(prefix='.xdg-roots-', dir=path.parent)
|
|
80
|
+
try:
|
|
81
|
+
with os.fdopen(fd, 'w') as handle:
|
|
82
|
+
json.dump({'schema': 'lanes.studio-host-xdg-roots', 'version': 1,
|
|
83
|
+
'roots': {name: str(Path(xdg[name]).resolve()) for name in XDG_ROOTS if xdg.get(name)}},
|
|
84
|
+
handle, sort_keys=True)
|
|
85
|
+
handle.write('\n')
|
|
86
|
+
os.chmod(temporary, 0o600)
|
|
87
|
+
os.replace(temporary, path)
|
|
88
|
+
finally:
|
|
89
|
+
if os.path.exists(temporary):
|
|
90
|
+
os.unlink(temporary)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def studio_host_config(home, xdg):
|
|
94
|
+
"""The Host's config path as libexec/studio_host.py's Layout resolves it: $XDG_CONFIG_HOME, else %h/.config
|
|
95
|
+
(left as the systemd specifier so the unit stays the golden when no XDG root is set)."""
|
|
96
|
+
if xdg.get('XDG_CONFIG_HOME'):
|
|
97
|
+
return unit_value(Path(xdg['XDG_CONFIG_HOME']) / 'throttle-rc/studio-host.toml', command=True)
|
|
98
|
+
return '%h/.config/throttle-rc/studio-host.toml'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def manager_unit_dir(home):
|
|
102
|
+
"""Where the user MANAGER reads unit files (systemd.unit(5)): $XDG_CONFIG_HOME/systemd/user when the manager's own
|
|
103
|
+
environment sets it, else ~/.config/systemd/user. Asked of the manager, never of this shell (review round 2);
|
|
104
|
+
no answer keeps the default. libexec/studio_host.py's manager_units_dir is the same rule."""
|
|
105
|
+
default = Path(home) / '.config/systemd/user'
|
|
106
|
+
try:
|
|
107
|
+
result = subprocess.run(['systemctl', '--user', 'show-environment'], capture_output=True, text=True,
|
|
108
|
+
timeout=10, stdin=subprocess.DEVNULL)
|
|
109
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
110
|
+
return default
|
|
111
|
+
if result.returncode == 0:
|
|
112
|
+
for line in result.stdout.splitlines():
|
|
113
|
+
if line.startswith('XDG_CONFIG_HOME=') and line.split('=', 1)[1].startswith('/'):
|
|
114
|
+
return Path(line.split('=', 1)[1]) / 'systemd/user'
|
|
115
|
+
return default
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register(repo, home, share, state, workspaces=None, economy=None, notify=None, xdg=None, units_dir=None):
|
|
18
119
|
repo, home, share, state = (Path(p).resolve() for p in (repo, home, share, state))
|
|
120
|
+
if xdg is None:
|
|
121
|
+
ambient = {name: os.environ[name] for name in XDG_ROOTS if os.environ.get(name)}
|
|
122
|
+
xdg = recorded_xdg(state)
|
|
123
|
+
if xdg is None:
|
|
124
|
+
xdg = ambient
|
|
125
|
+
elif {k: str(Path(v).resolve()) for k, v in ambient.items()} != {k: str(Path(v).resolve()) for k, v in xdg.items()}:
|
|
126
|
+
print(f'services.py: the studio units keep the XDG roots install/enable-studio-host.sh recorded in '
|
|
127
|
+
f'{state / XDG_RECORD} ({xdg or "the defaults"}), not this shell\'s ({ambient or "none"}); re-run '
|
|
128
|
+
'install/enable-studio-host.sh from the shell whose roots should hold', file=sys.stderr)
|
|
129
|
+
xdg = {name: str(Path(value).resolve()) for name, value in xdg.items() if name in XDG_ROOTS and value}
|
|
19
130
|
current = home / '.local/share/lanes/current'
|
|
20
131
|
links = ((current, repo), (home / '.local/bin/lanes', repo / 'bin/lanes'))
|
|
21
132
|
for link, _ in links:
|
|
@@ -25,6 +136,13 @@ def register(repo, home, share, state, workspaces=None):
|
|
|
25
136
|
if workspaces:
|
|
26
137
|
settings.append(('LANES_WORKSPACES', Path(workspaces).resolve()))
|
|
27
138
|
environment = ''.join(f'Environment="{name}={unit_value(value)}"\n' for name, value in settings)
|
|
139
|
+
economy_environment = ''.join(f'Environment="{name}={unit_value(value)}"\n'
|
|
140
|
+
for name, value in sorted((economy or {}).items()) if name in ECONOMY_SETTINGS)
|
|
141
|
+
notify_environment = ''.join(f'Environment="{name}={unit_value(value)}"\n'
|
|
142
|
+
for name, value in sorted((notify or {}).items()) if name in NOTIFY_SETTINGS)
|
|
143
|
+
studio_environment = environment + ''.join(
|
|
144
|
+
f'Environment="{name}={unit_value(xdg[name]) if name in xdg else XDG_DEFAULTS[name]}"\n'
|
|
145
|
+
for name in XDG_ROOTS)
|
|
28
146
|
rendered = {}
|
|
29
147
|
for source in sorted((repo / 'systemd').iterdir()):
|
|
30
148
|
if source.name.startswith('.') or source.suffix not in ('.service', '.timer', '.slice') or not source.is_file():
|
|
@@ -32,10 +150,15 @@ def register(repo, home, share, state, workspaces=None):
|
|
|
32
150
|
content = source.read_text().replace('@REPO@', unit_value(repo, command=True))
|
|
33
151
|
content = content.replace('@SHARE@', unit_value(share, command=True))
|
|
34
152
|
content = content.replace('@CADDY@', unit_value(shutil.which('caddy') or '/usr/bin/caddy', command=True))
|
|
153
|
+
content = content.replace('@THROTTLE_RC@', unit_value(throttle_rc_path(share), command=True))
|
|
154
|
+
content = content.replace('@STUDIO_HOST_CONFIG@', studio_host_config(home, xdg))
|
|
35
155
|
if '[Service]\n' in content:
|
|
36
|
-
|
|
156
|
+
lines = studio_environment if source.name in STUDIO_HOST_UNITS else environment
|
|
157
|
+
extra = (economy_environment if source.name in ECONOMY_UNITS else
|
|
158
|
+
notify_environment if source.name in NOTIFY_UNITS else '')
|
|
159
|
+
content = content.replace('[Service]\n', '[Service]\n' + lines + extra, 1)
|
|
37
160
|
rendered[source.name] = '# Generated by lanes/install/services.py; re-run the server installer to update.\n' + content
|
|
38
|
-
units = home / '.config/systemd/user'
|
|
161
|
+
units = Path(units_dir) if units_dir else home / '.config/systemd/user'
|
|
39
162
|
units.mkdir(parents=True, exist_ok=True)
|
|
40
163
|
for name, content in rendered.items():
|
|
41
164
|
fd, temporary = tempfile.mkstemp(prefix='.lanes-', dir=units)
|
|
@@ -58,4 +181,5 @@ if __name__ == '__main__':
|
|
|
58
181
|
register(Path(__file__).resolve().parents[1], Path.home(),
|
|
59
182
|
os.environ.get('LANES_SHARE', Path.home() / '.local/share/lanes'),
|
|
60
183
|
os.environ.get('LANES_STATE', Path.home() / '.local/state/lanes'),
|
|
61
|
-
os.environ.get('LANES_WORKSPACES'))
|
|
184
|
+
os.environ.get('LANES_WORKSPACES'), economy_settings(os.environ), notify_settings(os.environ),
|
|
185
|
+
units_dir=manager_unit_dir(Path.home()))
|