ever-works-node 0.0.0-stage → 0.2.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/LICENSE +661 -0
- package/README.md +385 -2
- package/cli.js +21859 -0
- package/package.json +47 -4
- package/packaging/README.md +276 -0
- package/packaging/systemd/ever-works-node@.service +64 -0
- package/packaging/systemd/install.sh +68 -0
- package/packaging/windows/install-service.logic.ps1 +1082 -0
- package/packaging/windows/install-service.ps1 +943 -0
- package/packaging/windows/test-install-service-logic.ps1 +722 -0
- package/packaging/windows/uninstall-service.ps1 +67 -0
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ever-works-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Ever Works Node - headless execution node (CLI + long-running service): fleet enrollment, heartbeat and capability reporting",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Ever Co. LTD",
|
|
7
|
+
"email": "ever@ever.co",
|
|
8
|
+
"url": "https://ever.co"
|
|
9
|
+
},
|
|
10
|
+
"license": "AGPL-3.0",
|
|
11
|
+
"homepage": "https://ever.works",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/ever-works/ever-works.git",
|
|
15
|
+
"directory": "apps/node"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ever-works/ever-works/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ever-works",
|
|
22
|
+
"fleet",
|
|
23
|
+
"agent",
|
|
24
|
+
"node",
|
|
25
|
+
"runner",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"codex"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"ever-works-node": "cli.js"
|
|
31
|
+
},
|
|
32
|
+
"main": "./cli.js",
|
|
33
|
+
"files": [
|
|
34
|
+
"cli.js",
|
|
35
|
+
"packaging",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.0.0"
|
|
41
|
+
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"@napi-rs/keyring": "^1.1.0",
|
|
44
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Running the Ever Works node unattended
|
|
2
|
+
|
|
3
|
+
`ever-works-node start` is a foreground process. Everything in this
|
|
4
|
+
directory exists so it does not have to be — so an enrolled machine
|
|
5
|
+
survives a reboot, restarts after a crash, and can be drained without
|
|
6
|
+
throwing away work it is half-way through.
|
|
7
|
+
|
|
8
|
+
Three shapes, one binary:
|
|
9
|
+
|
|
10
|
+
| Host | Mechanism | Where |
|
|
11
|
+
| --------------- | ------------------------------------------------ | ----------------------------------------- |
|
|
12
|
+
| Linux (systemd) | template unit `ever-works-node@<user>.service` | `systemd/` |
|
|
13
|
+
| Windows | Windows service (NSSM) or Scheduled Task at boot | `windows/` |
|
|
14
|
+
| Container / k8s | image built from the node's Dockerfile | `../../../.deploy/docker/node/Dockerfile` |
|
|
15
|
+
|
|
16
|
+
Enrollment is **never** part of installation. It consumes a one-time
|
|
17
|
+
token from the platform's Fleet settings page and is an explicit,
|
|
18
|
+
interactive act:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
ever-works-node enroll --api-url https://api.ever.works --token <token>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install the service afterwards.
|
|
25
|
+
|
|
26
|
+
The scripts below live in this directory of the source tree **and** inside
|
|
27
|
+
the published npm package: after `npm install -g ever-works-node` they are
|
|
28
|
+
under `$(npm root -g)/ever-works-node/packaging/`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Linux — systemd
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sudo apps/node/packaging/systemd/install.sh alice
|
|
36
|
+
sudo -u alice ever-works-node enroll --api-url https://api.ever.works --token <token>
|
|
37
|
+
sudo systemctl enable --now ever-works-node@alice
|
|
38
|
+
journalctl -fu ever-works-node@alice
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The unit is a **template** whose instance name is the user the node runs
|
|
42
|
+
as. A fleet node executes that user's commands, so it runs as that user
|
|
43
|
+
rather than as root or a shared service account.
|
|
44
|
+
|
|
45
|
+
`TimeoutStopSec=900` is load-bearing: stopping the node stops the
|
|
46
|
+
heartbeat and then **waits** for in-flight jobs to report their verdicts.
|
|
47
|
+
If systemd `SIGKILL`s the drain half-way, those claims lapse and the
|
|
48
|
+
platform re-runs the same work on another machine. Raise it above your
|
|
49
|
+
longest expected job.
|
|
50
|
+
|
|
51
|
+
`systemctl reload` is wired to `ever-works-node pause` — a drain, not a
|
|
52
|
+
restart.
|
|
53
|
+
|
|
54
|
+
## Windows
|
|
55
|
+
|
|
56
|
+
Enroll first, **as the account the node will run as**, then install from an
|
|
57
|
+
**elevated** PowerShell session:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
# 1. as the fleet user (NOT elevated) - this consumes a one-time token
|
|
61
|
+
ever-works-node enroll --api-url https://api.ever.works --token <token>
|
|
62
|
+
|
|
63
|
+
# 2. as an administrator: rehearse, then install
|
|
64
|
+
$password = Read-Host -AsSecureString 'Password for the node account'
|
|
65
|
+
.\apps\node\packaging\windows\install-service.ps1 -Work -ServicePassword $password -DryRun
|
|
66
|
+
.\apps\node\packaging\windows\install-service.ps1 -Work -ServicePassword $password
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Who the node runs as
|
|
70
|
+
|
|
71
|
+
This is the load-bearing decision, and it is why `-ServicePassword` is not
|
|
72
|
+
optional. A fleet node executes the enrolling user's work with that user's
|
|
73
|
+
credentials:
|
|
74
|
+
|
|
75
|
+
- its config and heartbeat credential live in that user's
|
|
76
|
+
`%APPDATA%\ever-works-node`, written with inheritance stripped and exactly
|
|
77
|
+
one ACE for that user;
|
|
78
|
+
- its git credentials live in that user's Windows Credential Manager vault,
|
|
79
|
+
which the credential helper reads per-user;
|
|
80
|
+
- the Claude Code and Codex logins live in that user's profile.
|
|
81
|
+
|
|
82
|
+
`nssm install` calls `CreateService` with a NULL `lpServiceStartName`, which
|
|
83
|
+
the Win32 API defines as **LocalSystem** — an account that shares none of
|
|
84
|
+
those. A node installed that way starts, finds no config, reports "not
|
|
85
|
+
enrolled", and is restarted every 10 seconds forever.
|
|
86
|
+
|
|
87
|
+
So `-ServiceAccount` defaults to the account running the installer and is
|
|
88
|
+
applied on **every** run, including a re-run to change a pin. It accepts
|
|
89
|
+
`DOMAIN\User`, `MACHINE\User`, `.\User`, a bare local name, a UPN, and a gMSA
|
|
90
|
+
(`DOMAIN\name$`, which takes no password). The built-in service accounts are
|
|
91
|
+
refused, with the reason.
|
|
92
|
+
|
|
93
|
+
A gMSA is accepted but is an odd fit, and the preflight will say so: it is
|
|
94
|
+
provisioned rather than signed in to, so it has no profile, no `%APPDATA%`,
|
|
95
|
+
and nowhere for `ever-works-node enroll` to have put a credential. A fleet
|
|
96
|
+
node's whole job is to run the **enrolling user's** work with that user's git
|
|
97
|
+
and model-CLI logins.
|
|
98
|
+
|
|
99
|
+
### How the password is handled
|
|
100
|
+
|
|
101
|
+
`-ServicePassword` is a **SecureString**, so the plaintext never reaches your
|
|
102
|
+
shell history or `powershell.exe`'s own command line — passing a literal is a
|
|
103
|
+
parameter-binding error, not a history entry. It is never echoed, never
|
|
104
|
+
written to a log file, and never rendered into a progress line or an
|
|
105
|
+
exception.
|
|
106
|
+
|
|
107
|
+
On the **service** path it never reaches a command line either. The obvious
|
|
108
|
+
implementation, `nssm set <name> ObjectName <account> <password>`, puts it on
|
|
109
|
+
a child process's command line, and that is written to persistent telemetry:
|
|
110
|
+
Security event 4688 wherever command-line auditing is enabled (the Microsoft
|
|
111
|
+
and CIS baselines enable it), and any EDR agent that captures
|
|
112
|
+
`ProcessCommandLine` — Defender for Endpoint uploads that to its cloud and
|
|
113
|
+
keeps it queryable. So the installer unwraps the SecureString into unmanaged
|
|
114
|
+
memory and hands `ChangeServiceConfigW` a pointer, zeroing the buffer
|
|
115
|
+
afterwards. No child process, no cmdlet parameter, and no managed `String`
|
|
116
|
+
(which is immutable and could not be zeroed).
|
|
117
|
+
|
|
118
|
+
That means the installer also has to do the half of the job NSSM was quietly
|
|
119
|
+
doing for us: `ChangeServiceConfig` does **not** grant the "Log on as a
|
|
120
|
+
service" right, and without it a correctly configured service fails to start
|
|
121
|
+
with error 1069. The installer grants it with `LsaAddAccountRights`.
|
|
122
|
+
|
|
123
|
+
On the **scheduled-task** path there is no such route: `Register-ScheduledTask`
|
|
124
|
+
takes the password as an ordinary cmdlet parameter and the Task Scheduler API
|
|
125
|
+
takes a `BSTR`, so a managed copy exists for the duration of the call and
|
|
126
|
+
PowerShell module logging (event 4103) would record the bound value where that
|
|
127
|
+
policy is enabled. One more reason the service is the recommended mechanism.
|
|
128
|
+
|
|
129
|
+
### Preflight
|
|
130
|
+
|
|
131
|
+
The installer refuses, before registering anything, when the chosen account
|
|
132
|
+
could not actually run a node: no such account, no profile, no config at that
|
|
133
|
+
account's `%APPDATA%` path, a config whose ACL does not grant it, a
|
|
134
|
+
`-ClaudePath`/`-CodexPath` pin that does not resolve or is not launchable, or
|
|
135
|
+
a workspace root it cannot write (or that would land inside the system
|
|
136
|
+
profile). It warns — but proceeds — for the survivable cases, including a
|
|
137
|
+
pinned CLI with no login: the node would advertise `claude-code` and then fail
|
|
138
|
+
every job it was sent, which is worse than not advertising it at all.
|
|
139
|
+
|
|
140
|
+
It also warns when the account cannot write `%ProgramData%\ever-works-node` or
|
|
141
|
+
a log file already there, and then **fixes** it: files created in that folder
|
|
142
|
+
by a privileged identity inherit only `BUILTIN\Users: ReadAndExecute`, so a
|
|
143
|
+
`node.log` written by a LocalSystem-era install is read-only to the account
|
|
144
|
+
the fixed installer picks. Left alone, the service could not open its
|
|
145
|
+
`AppStdout` and the scheduled task would die inside `cmd.exe` before the node
|
|
146
|
+
ran — with nothing in either log. The installer grants the account `Modify` on
|
|
147
|
+
the directory and on any log left behind.
|
|
148
|
+
|
|
149
|
+
### If a real run fails part-way
|
|
150
|
+
|
|
151
|
+
A fresh install that fails after `nssm install` — most likely at the identity
|
|
152
|
+
step, which is where a wrong account or password surfaces — is **undone**. The
|
|
153
|
+
service is removed rather than left registered under the LocalSystem default
|
|
154
|
+
this whole mechanism exists to avoid. A re-run over a service that already
|
|
155
|
+
existed is not removed; the message says so, and names `uninstall-service.ps1`.
|
|
156
|
+
|
|
157
|
+
### Dry run
|
|
158
|
+
|
|
159
|
+
`-DryRun` (or the standard `-WhatIf`) runs the whole preflight and prints
|
|
160
|
+
every command that would run, changing nothing. It does not require elevation,
|
|
161
|
+
so it is the way to check five remote machines before touching any of them.
|
|
162
|
+
|
|
163
|
+
### Other flags
|
|
164
|
+
|
|
165
|
+
`-ClaudePath` / `-CodexPath` pin the model CLIs for the service (a service
|
|
166
|
+
inherits the **machine** `PATH`, which normally lacks the per-user npm
|
|
167
|
+
directory) and `-WorkspaceRoot` sets the absolute, drive- or UNC-rooted
|
|
168
|
+
directory the per-Task worktrees live under; each becomes the matching `start`
|
|
169
|
+
flag. `-CliPath` points the service at a specific `cli.js` (default: the
|
|
170
|
+
globally installed package; from a source checkout use `apps\node\dist\cli.js`).
|
|
171
|
+
|
|
172
|
+
### Mechanism
|
|
173
|
+
|
|
174
|
+
The script registers `node.exe` running that `cli.js` directly. npm's global
|
|
175
|
+
install also puts `ever-works-node.ps1` / `.cmd` shims on `PATH`, but a service
|
|
176
|
+
manager cannot launch the `.ps1`, and the `.cmd` puts `cmd.exe` between the
|
|
177
|
+
service and the node, where the console stop signal used for draining gets
|
|
178
|
+
swallowed. The command line is quoted once, by the script, and handed to NSSM
|
|
179
|
+
verbatim — Windows PowerShell 5.1 would otherwise re-quote pinned paths
|
|
180
|
+
containing spaces without escaping them.
|
|
181
|
+
|
|
182
|
+
The script prefers a real Windows service via [NSSM](https://nssm.cc).
|
|
183
|
+
This matters: Node.js cannot answer Service Control Manager messages, so
|
|
184
|
+
`sc.exe create` pointed straight at `node.exe` yields a service Windows
|
|
185
|
+
reports as "did not respond in a timely fashion" (error 1053). A wrapper
|
|
186
|
+
is what makes it a genuine service _and_ what gives the node a graceful
|
|
187
|
+
stop long enough to drain.
|
|
188
|
+
|
|
189
|
+
Without NSSM the script registers a **Scheduled Task** at boot and says so.
|
|
190
|
+
That runs unattended and survives reboots, but it will not appear in
|
|
191
|
+
`services.msc`. Both branches write `%ProgramData%\ever-works-node\node.log`
|
|
192
|
+
and `node.err.log`; the task branch reaches them through `cmd.exe`, which is
|
|
193
|
+
acceptable there only because `Stop-ScheduledTask` terminates the task
|
|
194
|
+
outright and delivers no console event for `cmd.exe` to swallow — that branch
|
|
195
|
+
never drained.
|
|
196
|
+
|
|
197
|
+
Without `-ServicePassword` the task falls back to an **S4U** logon, which is
|
|
198
|
+
minted without the account's password and therefore never unlocks its DPAPI
|
|
199
|
+
master key: Windows Credential Manager reads fail there exactly as they do
|
|
200
|
+
under LocalSystem. The preflight says so. (A gMSA is not S4U either — its
|
|
201
|
+
password is held by the domain controller, so it registers as a `Password`
|
|
202
|
+
logon with no value.)
|
|
203
|
+
|
|
204
|
+
Remove with `uninstall-service.ps1`. That leaves the node enrolled — use
|
|
205
|
+
`ever-works-node unenroll` to retire it on the platform too.
|
|
206
|
+
|
|
207
|
+
### Tests
|
|
208
|
+
|
|
209
|
+
`test-install-service-logic.ps1` covers the decision logic (account
|
|
210
|
+
normalisation, the service configuration plan, the scheduled-task plan, the
|
|
211
|
+
ACL plan, the access-verdict and pin rules, the preflight verdicts) without
|
|
212
|
+
registering anything. It also exercises the `ChangeServiceConfigW` and
|
|
213
|
+
`LsaAddAccountRights` signatures against a deliberately invalid handle, which
|
|
214
|
+
proves the marshalling without touching a service or the local security
|
|
215
|
+
policy. `.github/workflows/node-packaging-windows.yml` runs it on a
|
|
216
|
+
`windows-2022` runner under Windows PowerShell 5.1 — the shell the fleet
|
|
217
|
+
workstations actually have — and exercises the installer's `-DryRun` and
|
|
218
|
+
`-WhatIf` paths. `apps/node/src/core/packaging/windows-service-contract.internal.spec.ts`
|
|
219
|
+
pins the same invariants from the Linux CI, so they hold even if that
|
|
220
|
+
workflow's path filter is edited.
|
|
221
|
+
|
|
222
|
+
## Container
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
docker build -f .deploy/docker/node/Dockerfile -t ever-works-node .
|
|
226
|
+
docker run -d --name ever-works-node \
|
|
227
|
+
-v ever-works-node-config:/var/lib/ever-works-node \
|
|
228
|
+
ever-works-node start --work
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Enroll into the same volume first:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
docker run --rm -it \
|
|
235
|
+
-v ever-works-node-config:/var/lib/ever-works-node \
|
|
236
|
+
ever-works-node enroll --api-url https://api.ever.works --token <token>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Build with `--build-arg WITH_BROWSER=true` to install Chromium, so the
|
|
240
|
+
image can honour the `browser` capability it would otherwise not
|
|
241
|
+
advertise. It roughly triples the image size, which is why it is opt-in.
|
|
242
|
+
|
|
243
|
+
`tini` is PID 1 on purpose: PID 1 does not get default signal handling,
|
|
244
|
+
and without it the node's SIGTERM drain would never run.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Credential storage
|
|
249
|
+
|
|
250
|
+
The heartbeat secret goes into the OS keychain (macOS Keychain, Windows
|
|
251
|
+
Credential Manager, Linux Secret Service) when one is available, and the
|
|
252
|
+
config file holds only a pointer. Where no keychain exists — headless
|
|
253
|
+
servers, containers — it falls back into the config file and **says so**,
|
|
254
|
+
loudly, on every load. The file is then restricted to its owner: `0600`
|
|
255
|
+
on POSIX, and an inheritance-stripped owner-only ACL on Windows.
|
|
256
|
+
|
|
257
|
+
Two environment variables change this:
|
|
258
|
+
|
|
259
|
+
| Variable | Effect |
|
|
260
|
+
| ------------------------------------ | ------------------------------------------------------------ |
|
|
261
|
+
| `EVER_WORKS_NODE_DISABLE_KEYCHAIN=1` | Force the file fallback (set in the container image). |
|
|
262
|
+
| `EVER_WORKS_NODE_CONFIG` | Absolute path of the config file, overriding the OS default. |
|
|
263
|
+
|
|
264
|
+
## Draining
|
|
265
|
+
|
|
266
|
+
`pause` is a drain, not a kill:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
ever-works-node pause # stop leasing; in-flight jobs finish and report
|
|
270
|
+
ever-works-node resume # take work again
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
It tells the platform (so the scheduler stops offering work) _and_
|
|
274
|
+
records the intent locally (so a restart comes back paused). A paused
|
|
275
|
+
node keeps heartbeating: a drained machine that vanishes from Fleet is
|
|
276
|
+
indistinguishable from a dead one.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Template unit: the instance name IS the user the node runs as.
|
|
2
|
+
# systemctl enable --now ever-works-node@alice
|
|
3
|
+
# A fleet node executes the OWNER'S commands, so it runs as that owner
|
|
4
|
+
# rather than as root or as a shared service account.
|
|
5
|
+
[Unit]
|
|
6
|
+
Description=Ever Works execution node (running as %i)
|
|
7
|
+
Documentation=https://docs.ever.works
|
|
8
|
+
# Enrollment and heartbeat are outbound HTTPS; starting before the
|
|
9
|
+
# network is up just burns the first few backoff intervals.
|
|
10
|
+
After=network-online.target
|
|
11
|
+
Wants=network-online.target
|
|
12
|
+
|
|
13
|
+
[Service]
|
|
14
|
+
Type=simple
|
|
15
|
+
User=%i
|
|
16
|
+
Group=%i
|
|
17
|
+
|
|
18
|
+
# `--work` is what makes this machine capacity rather than inventory.
|
|
19
|
+
# Drop it to enroll a node for visibility only.
|
|
20
|
+
ExecStart=/usr/bin/env ever-works-node start --work
|
|
21
|
+
# `pause` drains: leasing stops immediately, in-flight jobs finish and
|
|
22
|
+
# report. Wired to ExecReload so `systemctl reload` is a drain, not a cut.
|
|
23
|
+
ExecReload=/usr/bin/env ever-works-node pause
|
|
24
|
+
|
|
25
|
+
# The node's own SIGTERM handler stops the heartbeat and then WAITS for
|
|
26
|
+
# in-flight jobs to report their verdicts. TimeoutStopSec must therefore
|
|
27
|
+
# be longer than the longest job you expect, or systemd SIGKILLs the
|
|
28
|
+
# drain half-way through and the platform re-runs the work elsewhere.
|
|
29
|
+
KillSignal=SIGTERM
|
|
30
|
+
TimeoutStopSec=900
|
|
31
|
+
|
|
32
|
+
Restart=on-failure
|
|
33
|
+
RestartSec=10s
|
|
34
|
+
# A node that crash-loops on a bad config should stop and be visible,
|
|
35
|
+
# not hammer the API forever.
|
|
36
|
+
StartLimitIntervalSec=300
|
|
37
|
+
StartLimitBurst=5
|
|
38
|
+
|
|
39
|
+
# Hardening. The node executes the OWNER'S commands by design, so this is
|
|
40
|
+
# a blast-radius limit, not a sandbox: it keeps a check from touching the
|
|
41
|
+
# rest of the system, it does not make an untrusted check safe.
|
|
42
|
+
NoNewPrivileges=true
|
|
43
|
+
PrivateTmp=true
|
|
44
|
+
ProtectSystem=full
|
|
45
|
+
ProtectHome=read-only
|
|
46
|
+
ProtectKernelTunables=true
|
|
47
|
+
ProtectControlGroups=true
|
|
48
|
+
RestrictSUIDSGID=true
|
|
49
|
+
# Config (credential) and workspaces are the only writable paths.
|
|
50
|
+
StateDirectory=ever-works-node
|
|
51
|
+
ReadWritePaths=/var/lib/ever-works-node
|
|
52
|
+
|
|
53
|
+
Environment=NODE_ENV=production
|
|
54
|
+
Environment=EVER_WORKS_NODE_CONFIG=/var/lib/ever-works-node/node-config.json
|
|
55
|
+
# Drop-in overrides go in
|
|
56
|
+
# /etc/systemd/system/ever-works-node@.service.d/override.conf
|
|
57
|
+
EnvironmentFile=-/etc/default/ever-works-node
|
|
58
|
+
|
|
59
|
+
StandardOutput=journal
|
|
60
|
+
StandardError=journal
|
|
61
|
+
SyslogIdentifier=ever-works-node
|
|
62
|
+
|
|
63
|
+
[Install]
|
|
64
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Install the Ever Works node as a systemd service.
|
|
4
|
+
#
|
|
5
|
+
# sudo ./install.sh <user>
|
|
6
|
+
#
|
|
7
|
+
# Idempotent: re-running refreshes the unit and reloads systemd. It does
|
|
8
|
+
# NOT enroll the node — enrollment consumes a one-time token and is an
|
|
9
|
+
# explicit, interactive act:
|
|
10
|
+
#
|
|
11
|
+
# ever-works-node enroll --api-url https://api.ever.works --token <token>
|
|
12
|
+
#
|
|
13
|
+
# Run that as <user> first, then start the service.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
UNIT_NAME="ever-works-node@.service"
|
|
18
|
+
UNIT_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/${UNIT_NAME}"
|
|
19
|
+
UNIT_DEST="/etc/systemd/system/${UNIT_NAME}"
|
|
20
|
+
STATE_DIR="/var/lib/ever-works-node"
|
|
21
|
+
|
|
22
|
+
RUN_AS="${1:-}"
|
|
23
|
+
if [ -z "${RUN_AS}" ]; then
|
|
24
|
+
echo "usage: $0 <user>" >&2
|
|
25
|
+
echo " the node runs as that user and executes ITS commands" >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
30
|
+
echo "error: must run as root (installing a system unit)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if ! id "${RUN_AS}" >/dev/null 2>&1; then
|
|
35
|
+
echo "error: user '${RUN_AS}' does not exist" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if ! command -v systemctl >/dev/null 2>&1; then
|
|
40
|
+
echo "error: systemd is not available on this host" >&2
|
|
41
|
+
echo " use the container image (.deploy/docker/node/Dockerfile) instead" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if ! command -v ever-works-node >/dev/null 2>&1; then
|
|
46
|
+
# Refuse rather than install a unit that will crash-loop on start.
|
|
47
|
+
echo "error: 'ever-works-node' is not on PATH" >&2
|
|
48
|
+
echo " install it first, e.g.: npm install -g ever-works-node" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
install -d -m 0700 -o "${RUN_AS}" -g "${RUN_AS}" "${STATE_DIR}"
|
|
53
|
+
install -m 0644 "${UNIT_SRC}" "${UNIT_DEST}"
|
|
54
|
+
|
|
55
|
+
systemctl daemon-reload
|
|
56
|
+
|
|
57
|
+
echo "Installed ${UNIT_DEST}"
|
|
58
|
+
echo
|
|
59
|
+
echo "Next:"
|
|
60
|
+
echo " 1. enroll (as ${RUN_AS}):"
|
|
61
|
+
echo " sudo -u ${RUN_AS} ever-works-node enroll --api-url <url> --token <token>"
|
|
62
|
+
echo " 2. start:"
|
|
63
|
+
echo " systemctl enable --now ever-works-node@${RUN_AS}"
|
|
64
|
+
echo
|
|
65
|
+
echo "Operating:"
|
|
66
|
+
echo " drain (finish in-flight work, take no more): ever-works-node pause"
|
|
67
|
+
echo " take work again: ever-works-node resume"
|
|
68
|
+
echo " follow logs: journalctl -fu ever-works-node@${RUN_AS}"
|