@bitsocial/bitsocial-cli 0.19.70 → 0.19.71
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 +16 -16
- package/dist/cli/commands/daemon.js +7 -3
- package/dist/cli/commands/update/install.d.ts +4 -1
- package/dist/cli/commands/update/install.js +88 -60
- package/dist/common-utils/daemon-state.d.ts +36 -0
- package/dist/common-utils/daemon-state.js +52 -0
- package/dist/update/restart-orchestration.d.ts +34 -0
- package/dist/update/restart-orchestration.js +40 -0
- package/dist/update/systemctl.d.ts +6 -0
- package/dist/update/systemctl.js +10 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -421,7 +421,7 @@ EXAMPLES
|
|
|
421
421
|
$ bitsocial challenge install ./my-local-challenge
|
|
422
422
|
```
|
|
423
423
|
|
|
424
|
-
_See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
424
|
+
_See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/challenge/install.ts)_
|
|
425
425
|
|
|
426
426
|
## `bitsocial challenge list`
|
|
427
427
|
|
|
@@ -447,7 +447,7 @@ EXAMPLES
|
|
|
447
447
|
$ bitsocial challenge list -q
|
|
448
448
|
```
|
|
449
449
|
|
|
450
|
-
_See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
450
|
+
_See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/challenge/list.ts)_
|
|
451
451
|
|
|
452
452
|
## `bitsocial challenge ls`
|
|
453
453
|
|
|
@@ -501,7 +501,7 @@ EXAMPLES
|
|
|
501
501
|
$ bitsocial challenge remove @scope/my-challenge
|
|
502
502
|
```
|
|
503
503
|
|
|
504
|
-
_See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
504
|
+
_See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/challenge/remove.ts)_
|
|
505
505
|
|
|
506
506
|
## `bitsocial challenge rm NAME`
|
|
507
507
|
|
|
@@ -615,7 +615,7 @@ EXAMPLES
|
|
|
615
615
|
$ bitsocial community create --jsonFile ./create-options.json
|
|
616
616
|
```
|
|
617
617
|
|
|
618
|
-
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
618
|
+
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/create.ts)_
|
|
619
619
|
|
|
620
620
|
## `bitsocial community delete ADDRESSES`
|
|
621
621
|
|
|
@@ -640,7 +640,7 @@ EXAMPLES
|
|
|
640
640
|
$ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
641
641
|
```
|
|
642
642
|
|
|
643
|
-
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
643
|
+
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/delete.ts)_
|
|
644
644
|
|
|
645
645
|
## `bitsocial community edit ADDRESS`
|
|
646
646
|
|
|
@@ -710,7 +710,7 @@ EXAMPLES
|
|
|
710
710
|
$ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
|
|
711
711
|
```
|
|
712
712
|
|
|
713
|
-
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
713
|
+
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/edit.ts)_
|
|
714
714
|
|
|
715
715
|
## `bitsocial community export [ADDRESS]`
|
|
716
716
|
|
|
@@ -751,7 +751,7 @@ EXAMPLES
|
|
|
751
751
|
$ bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
752
752
|
```
|
|
753
753
|
|
|
754
|
-
_See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
754
|
+
_See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/export.ts)_
|
|
755
755
|
|
|
756
756
|
## `bitsocial community get [ADDRESS]`
|
|
757
757
|
|
|
@@ -782,7 +782,7 @@ EXAMPLES
|
|
|
782
782
|
$ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
783
783
|
```
|
|
784
784
|
|
|
785
|
-
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
785
|
+
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/get.ts)_
|
|
786
786
|
|
|
787
787
|
## `bitsocial community list`
|
|
788
788
|
|
|
@@ -805,7 +805,7 @@ EXAMPLES
|
|
|
805
805
|
$ bitsocial community list
|
|
806
806
|
```
|
|
807
807
|
|
|
808
|
-
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
808
|
+
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/list.ts)_
|
|
809
809
|
|
|
810
810
|
## `bitsocial community start ADDRESSES`
|
|
811
811
|
|
|
@@ -839,7 +839,7 @@ EXAMPLES
|
|
|
839
839
|
$ bitsocial community start $(bitsocial community list -q) --concurrency 1
|
|
840
840
|
```
|
|
841
841
|
|
|
842
|
-
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
842
|
+
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/start.ts)_
|
|
843
843
|
|
|
844
844
|
## `bitsocial community stop ADDRESSES`
|
|
845
845
|
|
|
@@ -864,7 +864,7 @@ EXAMPLES
|
|
|
864
864
|
$ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
|
|
865
865
|
```
|
|
866
866
|
|
|
867
|
-
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
867
|
+
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/community/stop.ts)_
|
|
868
868
|
|
|
869
869
|
## `bitsocial daemon`
|
|
870
870
|
|
|
@@ -911,7 +911,7 @@ EXAMPLES
|
|
|
911
911
|
$ bitsocial daemon --no-allowPrivateKeyExport
|
|
912
912
|
```
|
|
913
913
|
|
|
914
|
-
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
914
|
+
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/daemon.ts)_
|
|
915
915
|
|
|
916
916
|
## `bitsocial help [COMMAND]`
|
|
917
917
|
|
|
@@ -977,7 +977,7 @@ EXAMPLES
|
|
|
977
977
|
$ bitsocial logs --stdout -f
|
|
978
978
|
```
|
|
979
979
|
|
|
980
|
-
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
980
|
+
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/logs.ts)_
|
|
981
981
|
|
|
982
982
|
## `bitsocial update check`
|
|
983
983
|
|
|
@@ -994,7 +994,7 @@ EXAMPLES
|
|
|
994
994
|
$ bitsocial update check
|
|
995
995
|
```
|
|
996
996
|
|
|
997
|
-
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
997
|
+
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/update/check.ts)_
|
|
998
998
|
|
|
999
999
|
## `bitsocial update install [VERSION]`
|
|
1000
1000
|
|
|
@@ -1026,7 +1026,7 @@ EXAMPLES
|
|
|
1026
1026
|
$ bitsocial update install --no-restart-daemons
|
|
1027
1027
|
```
|
|
1028
1028
|
|
|
1029
|
-
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
1029
|
+
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/update/install.ts)_
|
|
1030
1030
|
|
|
1031
1031
|
## `bitsocial update versions`
|
|
1032
1032
|
|
|
@@ -1048,7 +1048,7 @@ EXAMPLES
|
|
|
1048
1048
|
$ bitsocial update versions --limit 5
|
|
1049
1049
|
```
|
|
1050
1050
|
|
|
1051
|
-
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
1051
|
+
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.71/src/cli/commands/update/versions.ts)_
|
|
1052
1052
|
<!-- commandsstop -->
|
|
1053
1053
|
|
|
1054
1054
|
## Contribution
|
|
@@ -9,7 +9,7 @@ import { printBanner } from "../ascii-banner.js";
|
|
|
9
9
|
import { loadChallengesIntoPKC, formatChallengeNameVersion } from "../../challenge-packages/challenge-utils.js";
|
|
10
10
|
import { migrateDataDirectory } from "../../common-utils/data-migration.js";
|
|
11
11
|
import { createBsoResolvers, DEFAULT_PROVIDERS } from "../../common-utils/resolvers.js";
|
|
12
|
-
import { pruneStaleStates, writeDaemonState, deleteDaemonState, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../common-utils/daemon-state.js";
|
|
12
|
+
import { pruneStaleStates, writeDaemonState, deleteDaemonState, detectSelfSupervisor, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../common-utils/daemon-state.js";
|
|
13
13
|
import { createDaemonFileLogger } from "../../common-utils/daemon-file-logger.js";
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import fsPromise from "fs/promises";
|
|
@@ -260,13 +260,17 @@ export default class Daemon extends Command {
|
|
|
260
260
|
migrateDataDirectory(mergedPkcOptions.dataPath);
|
|
261
261
|
// Prune stale daemon state files (dead PIDs from crashed daemons)
|
|
262
262
|
await pruneStaleStates();
|
|
263
|
-
// Persist this daemon's PID and startup args so `bitsocial update install --restart-daemons` can stop and restart it
|
|
263
|
+
// Persist this daemon's PID and startup args so `bitsocial update install --restart-daemons` can stop and restart it.
|
|
264
|
+
// Also record the supervisor (e.g. systemd) so the updater restarts via the supervisor instead of spawning a
|
|
265
|
+
// detached daemon that would compete with it for the RPC port (issue #82).
|
|
264
266
|
const daemonArgv = process.argv.slice(process.argv.indexOf("daemon") + 1);
|
|
267
|
+
const supervisor = await detectSelfSupervisor();
|
|
265
268
|
await writeDaemonState({
|
|
266
269
|
pid: process.pid,
|
|
267
270
|
startedAt: new Date().toISOString(),
|
|
268
271
|
argv: daemonArgv,
|
|
269
|
-
pkcRpcUrl: pkcRpcUrl.toString()
|
|
272
|
+
pkcRpcUrl: pkcRpcUrl.toString(),
|
|
273
|
+
...(supervisor ? { supervisor } : {})
|
|
270
274
|
});
|
|
271
275
|
// Create BSO name resolvers for .bso/.eth domain resolution
|
|
272
276
|
const bsoResolvers = createBsoResolvers(flags.chainProviderUrls);
|
|
@@ -16,5 +16,8 @@ export default class Install extends Command {
|
|
|
16
16
|
* but owned by another user, so we keep waiting.
|
|
17
17
|
*/
|
|
18
18
|
private _waitForProcessExit;
|
|
19
|
-
|
|
19
|
+
/** Build the side effects that the restart orchestration drives (split out so the routing is testable). */
|
|
20
|
+
private _daemonLifecycle;
|
|
21
|
+
/** Restart a supervised daemon onto the new binary by asking its supervisor (e.g. systemd). */
|
|
22
|
+
private _restartViaSupervisor;
|
|
20
23
|
}
|
|
@@ -4,7 +4,9 @@ import tcpPortUsed from "tcp-port-used";
|
|
|
4
4
|
import { fetchLatestVersion, installGlobal } from "../../../update/npm-registry.js";
|
|
5
5
|
import { fastInstallGlobal } from "../../../update/fast-update.js";
|
|
6
6
|
import { compareVersions } from "../../../update/semver.js";
|
|
7
|
-
import {
|
|
7
|
+
import { systemctlRestart } from "../../../update/systemctl.js";
|
|
8
|
+
import { getAliveDaemonStates, resolveDaemonSupervisor, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../../common-utils/daemon-state.js";
|
|
9
|
+
import { planDaemonRestarts, stopUnmanagedDaemons, startUnmanagedDaemons, restartManagedDaemons } from "../../../update/restart-orchestration.js";
|
|
8
10
|
export default class Install extends Command {
|
|
9
11
|
static description = "Install a specific version of bitsocial from npm";
|
|
10
12
|
static args = {
|
|
@@ -34,40 +36,26 @@ export default class Install extends Command {
|
|
|
34
36
|
];
|
|
35
37
|
async run() {
|
|
36
38
|
const { args, flags } = await this.parse(Install);
|
|
37
|
-
//
|
|
39
|
+
// Discover running daemons and split them into supervisor-managed vs. updater-managed (issue #82).
|
|
40
|
+
// Supervised daemons (e.g. systemd) are restarted through their supervisor; spawning a detached
|
|
41
|
+
// replacement ourselves would create a process the supervisor doesn't own that competes with it
|
|
42
|
+
// for the RPC port and triggers a restart loop.
|
|
38
43
|
const aliveDaemons = await getAliveDaemonStates();
|
|
44
|
+
const plan = await planDaemonRestarts(aliveDaemons, (d) => resolveDaemonSupervisor(d));
|
|
45
|
+
const lifecycle = this._daemonLifecycle();
|
|
39
46
|
if (aliveDaemons.length > 0) {
|
|
40
47
|
if (!flags["restart-daemons"]) {
|
|
41
48
|
this.error(`${aliveDaemons.length} daemon(s) running. Stop them first, then retry.`, { exit: 1 });
|
|
42
49
|
}
|
|
43
|
-
// Stop
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.log(` PID ${d.pid} already exited.`);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
throw e;
|
|
55
|
-
}
|
|
50
|
+
// Stop only the unsupervised daemons before the binary swap. Supervised daemons keep running
|
|
51
|
+
// and are restarted by their supervisor afterwards (see _restartViaSupervisor).
|
|
52
|
+
await stopUnmanagedDaemons(plan, lifecycle);
|
|
53
|
+
if (plan.unmanaged.length > 0)
|
|
54
|
+
this.log("All unsupervised daemons stopped.");
|
|
55
|
+
for (const { daemon, supervisor } of plan.managed) {
|
|
56
|
+
this.log(`Daemon (PID ${daemon.pid}) is managed by ${supervisor.type} (${supervisor.unit}); ` +
|
|
57
|
+
`it will be restarted by its supervisor.`);
|
|
56
58
|
}
|
|
57
|
-
// Wait for each daemon process to fully exit — NOT just for its RPC port to free.
|
|
58
|
-
// The daemon releases its RPC port (daemonServer.destroy()) before it finishes killing
|
|
59
|
-
// its kubo child, so a port-only wait lets us restart while the old kubo still holds the
|
|
60
|
-
// IPFS API port; the new daemon then dies on startup with "IPFS API port already in use"
|
|
61
|
-
// (issue #70). The daemon's exit hook kills kubo before the process exits, so waiting for
|
|
62
|
-
// the PID to disappear guarantees the kubo port is free before we restart.
|
|
63
|
-
for (const d of aliveDaemons) {
|
|
64
|
-
this.log(`Waiting for daemon (PID ${d.pid}) to exit...`);
|
|
65
|
-
const exited = await this._waitForProcessExit(d.pid, DAEMON_SHUTDOWN_TIMEOUT_MS);
|
|
66
|
-
if (!exited) {
|
|
67
|
-
this.error(`Daemon (PID ${d.pid}) did not shut down within ${DAEMON_SHUTDOWN_TIMEOUT_MS / 1000} seconds.`, { exit: 1 });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
this.log("All daemons stopped.");
|
|
71
59
|
}
|
|
72
60
|
// Resolve the target version
|
|
73
61
|
let targetVersion;
|
|
@@ -86,10 +74,10 @@ export default class Install extends Command {
|
|
|
86
74
|
// Skip if already on this version (unless --force)
|
|
87
75
|
if (compareVersions(current, targetVersion) === 0 && !flags.force) {
|
|
88
76
|
this.log(`Already on v${current}. Use --force to reinstall.`);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
// We stopped the unsupervised daemons but aren't updating — bring them back. Supervised daemons
|
|
78
|
+
// were never stopped, so leave them running (no unnecessary service bounce).
|
|
79
|
+
if (flags["restart-daemons"])
|
|
80
|
+
await startUnmanagedDaemons(plan, lifecycle);
|
|
93
81
|
return;
|
|
94
82
|
}
|
|
95
83
|
this.log(`Installing bitsocial-cli@${targetVersion}...`);
|
|
@@ -114,10 +102,13 @@ export default class Install extends Command {
|
|
|
114
102
|
}
|
|
115
103
|
}
|
|
116
104
|
this.log(`Installed bitsocial v${targetVersion} (was v${current}).`);
|
|
117
|
-
// Restart daemons with the new binary
|
|
105
|
+
// Restart daemons with the new binary: re-spawn the unsupervised ones we stopped, and ask each
|
|
106
|
+
// supervisor to restart its daemon onto the new binary.
|
|
118
107
|
if (aliveDaemons.length > 0 && flags["restart-daemons"]) {
|
|
119
|
-
await
|
|
108
|
+
await startUnmanagedDaemons(plan, lifecycle);
|
|
109
|
+
await restartManagedDaemons(plan, lifecycle);
|
|
120
110
|
this.log("To see the daemon logs run `bitsocial logs --stdout`");
|
|
111
|
+
this.log("Check community status with: bitsocial community list");
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
114
|
/**
|
|
@@ -147,32 +138,69 @@ export default class Install extends Command {
|
|
|
147
138
|
}
|
|
148
139
|
return false;
|
|
149
140
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
141
|
+
/** Build the side effects that the restart orchestration drives (split out so the routing is testable). */
|
|
142
|
+
_daemonLifecycle() {
|
|
143
|
+
return {
|
|
144
|
+
stopUnmanaged: async (daemon) => {
|
|
145
|
+
this.log(`Stopping daemon (PID ${daemon.pid})...`);
|
|
146
|
+
try {
|
|
147
|
+
process.kill(daemon.pid, "SIGINT");
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
if (e.code === "ESRCH") {
|
|
151
|
+
this.log(` PID ${daemon.pid} already exited.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
// Wait for the process to fully exit — NOT just for its RPC port to free. The daemon
|
|
157
|
+
// releases its RPC port (daemonServer.destroy()) before it finishes killing its kubo
|
|
158
|
+
// child, so a port-only wait lets us restart while the old kubo still holds the IPFS API
|
|
159
|
+
// port; the new daemon then dies on "IPFS API port already in use" (issue #70). The
|
|
160
|
+
// daemon's exit hook kills kubo before exiting, so "PID gone" guarantees kubo is free.
|
|
161
|
+
this.log(`Waiting for daemon (PID ${daemon.pid}) to exit...`);
|
|
162
|
+
const exited = await this._waitForProcessExit(daemon.pid, DAEMON_SHUTDOWN_TIMEOUT_MS);
|
|
163
|
+
if (!exited) {
|
|
164
|
+
this.error(`Daemon (PID ${daemon.pid}) did not shut down within ${DAEMON_SHUTDOWN_TIMEOUT_MS / 1000} seconds.`, { exit: 1 });
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
startUnmanaged: async (daemon) => {
|
|
168
|
+
const argStr = daemon.argv.length > 0 ? daemon.argv.join(" ") : "(defaults)";
|
|
169
|
+
this.log(`Restarting daemon with args: ${argStr}`);
|
|
170
|
+
const child = spawn("bitsocial", ["daemon", ...daemon.argv], {
|
|
171
|
+
detached: true,
|
|
172
|
+
stdio: "ignore"
|
|
173
|
+
});
|
|
174
|
+
child.unref();
|
|
175
|
+
if (!child.pid) {
|
|
176
|
+
this.warn(`Failed to spawn daemon for args: ${argStr}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Wait briefly for the daemon's RPC port to come up
|
|
180
|
+
const port = Number(new URL(daemon.pkcRpcUrl).port);
|
|
181
|
+
const started = await tcpPortUsed.waitUntilUsed(port, 500, 30000).then(() => true).catch(() => false);
|
|
182
|
+
if (started) {
|
|
183
|
+
this.log(` Daemon started (port ${port}).`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.warn(` Daemon may not have started — port ${port} not responding after 30s. Check logs with: bitsocial logs`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
restartManaged: async (supervisor) => {
|
|
190
|
+
await this._restartViaSupervisor(supervisor);
|
|
173
191
|
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Restart a supervised daemon onto the new binary by asking its supervisor (e.g. systemd). */
|
|
195
|
+
async _restartViaSupervisor(supervisor) {
|
|
196
|
+
this.log(`Restarting ${supervisor.type} unit ${supervisor.unit} so it picks up the new binary...`);
|
|
197
|
+
try {
|
|
198
|
+
await systemctlRestart(supervisor.unit);
|
|
199
|
+
this.log(` ${supervisor.unit} restarted.`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
this.error(`Updated the binary but failed to restart ${supervisor.type} unit ${supervisor.unit}: ${err.message}. ` +
|
|
203
|
+
`Restart it manually, e.g. 'sudo systemctl restart ${supervisor.unit}'.`, { exit: 1 });
|
|
174
204
|
}
|
|
175
|
-
this.log("Check community status with: bitsocial community list");
|
|
176
|
-
this.log("Check logs with: bitsocial logs");
|
|
177
205
|
}
|
|
178
206
|
}
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
* a slow-but-valid shutdown (within the daemon's own contract) aborts the update midway.
|
|
6
6
|
*/
|
|
7
7
|
export declare const DAEMON_SHUTDOWN_TIMEOUT_MS = 120000;
|
|
8
|
+
/**
|
|
9
|
+
* How a daemon's lifecycle is managed by an external supervisor. Recorded at startup so that
|
|
10
|
+
* `update install` restarts the daemon through its supervisor instead of spawning a detached
|
|
11
|
+
* replacement that would compete with the supervisor for the RPC port (issue #82).
|
|
12
|
+
*/
|
|
13
|
+
export interface DaemonSupervisor {
|
|
14
|
+
/** The supervisor managing this daemon. Only systemd is detected today. */
|
|
15
|
+
type: "systemd";
|
|
16
|
+
/** The unit that owns the daemon, e.g. "bitsocial.service". */
|
|
17
|
+
unit: string;
|
|
18
|
+
}
|
|
8
19
|
export interface DaemonState {
|
|
9
20
|
pid: number;
|
|
10
21
|
startedAt: string;
|
|
@@ -12,7 +23,32 @@ export interface DaemonState {
|
|
|
12
23
|
pkcRpcUrl: string;
|
|
13
24
|
/** OS-reported process start time, used to detect PID reuse. Absent in legacy state files. */
|
|
14
25
|
procStartTime?: string;
|
|
26
|
+
/** External supervisor managing this daemon, if any. Absent for standalone or legacy daemons. */
|
|
27
|
+
supervisor?: DaemonSupervisor;
|
|
15
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse the systemd service unit a process belongs to out of its cgroup contents, or undefined.
|
|
31
|
+
* cgroup v2: a single line `0::/system.slice/bitsocial.service`
|
|
32
|
+
* cgroup v1: many `id:controller:/system.slice/bitsocial.service` lines (all point at the same unit)
|
|
33
|
+
* The unit is the leaf of the cgroup path when it ends in `.service`. A user session has a `.scope`
|
|
34
|
+
* leaf (e.g. `…/session-36.scope`) — not a service — so it returns undefined (that daemon is not
|
|
35
|
+
* systemd-supervised even if it happens to live under system.slice somewhere up the tree).
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseSystemdUnitFromCgroup(content: string): string | undefined;
|
|
38
|
+
/** Read the systemd unit owning `pid` (or the current process when "self") from /proc, or undefined. */
|
|
39
|
+
export declare function readSystemdUnit(pid: number | "self"): Promise<string | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Detect whether THIS process was started by systemd, and under which unit. systemd sets
|
|
42
|
+
* $INVOCATION_ID for every service it spawns; the unit name comes from this process's own cgroup.
|
|
43
|
+
* `env`/`readUnit` are injectable for testing. Returns undefined when not systemd-supervised.
|
|
44
|
+
*/
|
|
45
|
+
export declare function detectSelfSupervisor(env?: NodeJS.ProcessEnv, readUnit?: (pid: number | "self") => Promise<string | undefined>): Promise<DaemonSupervisor | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the supervisor for a daemon described by `state`. Prefers the `supervisor` it recorded
|
|
48
|
+
* at startup; for legacy daemons that predate that field, falls back to inferring the unit from the
|
|
49
|
+
* live process's cgroup. `readUnit` is injectable for testing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveDaemonSupervisor(state: DaemonState, readUnit?: (pid: number | "self") => Promise<string | undefined>): Promise<DaemonSupervisor | undefined>;
|
|
16
52
|
/** Write a daemon state file atomically (write to .tmp then rename). */
|
|
17
53
|
export declare function writeDaemonState(state: DaemonState): Promise<void>;
|
|
18
54
|
/** Read all state files from the daemon states directory. */
|
|
@@ -12,6 +12,58 @@ const DAEMON_STATES_DIR = path.join(defaults.PKC_DATA_PATH, ".daemon_states");
|
|
|
12
12
|
* a slow-but-valid shutdown (within the daemon's own contract) aborts the update midway.
|
|
13
13
|
*/
|
|
14
14
|
export const DAEMON_SHUTDOWN_TIMEOUT_MS = 120000;
|
|
15
|
+
/**
|
|
16
|
+
* Parse the systemd service unit a process belongs to out of its cgroup contents, or undefined.
|
|
17
|
+
* cgroup v2: a single line `0::/system.slice/bitsocial.service`
|
|
18
|
+
* cgroup v1: many `id:controller:/system.slice/bitsocial.service` lines (all point at the same unit)
|
|
19
|
+
* The unit is the leaf of the cgroup path when it ends in `.service`. A user session has a `.scope`
|
|
20
|
+
* leaf (e.g. `…/session-36.scope`) — not a service — so it returns undefined (that daemon is not
|
|
21
|
+
* systemd-supervised even if it happens to live under system.slice somewhere up the tree).
|
|
22
|
+
*/
|
|
23
|
+
export function parseSystemdUnitFromCgroup(content) {
|
|
24
|
+
for (const line of content.split("\n")) {
|
|
25
|
+
if (!line.trim())
|
|
26
|
+
continue;
|
|
27
|
+
// hierarchy-id:controller-list:cgroup-path — the path is the last colon-separated field
|
|
28
|
+
const cgroupPath = line.slice(line.lastIndexOf(":") + 1);
|
|
29
|
+
const leaf = cgroupPath.slice(cgroupPath.lastIndexOf("/") + 1);
|
|
30
|
+
if (leaf.endsWith(".service"))
|
|
31
|
+
return leaf;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Read the systemd unit owning `pid` (or the current process when "self") from /proc, or undefined. */
|
|
36
|
+
export async function readSystemdUnit(pid) {
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(`/proc/${pid}/cgroup`, "utf-8");
|
|
39
|
+
return parseSystemdUnitFromCgroup(content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined; // no /proc (non-Linux) or unreadable — treat as unsupervised
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect whether THIS process was started by systemd, and under which unit. systemd sets
|
|
47
|
+
* $INVOCATION_ID for every service it spawns; the unit name comes from this process's own cgroup.
|
|
48
|
+
* `env`/`readUnit` are injectable for testing. Returns undefined when not systemd-supervised.
|
|
49
|
+
*/
|
|
50
|
+
export async function detectSelfSupervisor(env = process.env, readUnit = readSystemdUnit) {
|
|
51
|
+
if (!env.INVOCATION_ID)
|
|
52
|
+
return undefined;
|
|
53
|
+
const unit = await readUnit("self");
|
|
54
|
+
return unit ? { type: "systemd", unit } : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the supervisor for a daemon described by `state`. Prefers the `supervisor` it recorded
|
|
58
|
+
* at startup; for legacy daemons that predate that field, falls back to inferring the unit from the
|
|
59
|
+
* live process's cgroup. `readUnit` is injectable for testing.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveDaemonSupervisor(state, readUnit = readSystemdUnit) {
|
|
62
|
+
if (state.supervisor)
|
|
63
|
+
return state.supervisor;
|
|
64
|
+
const unit = await readUnit(state.pid);
|
|
65
|
+
return unit ? { type: "systemd", unit } : undefined;
|
|
66
|
+
}
|
|
15
67
|
function stateFilePath(pid) {
|
|
16
68
|
return path.join(DAEMON_STATES_DIR, `${pid}-daemon.state`);
|
|
17
69
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DaemonState, DaemonSupervisor } from "../common-utils/daemon-state.js";
|
|
2
|
+
export interface ManagedDaemon {
|
|
3
|
+
daemon: DaemonState;
|
|
4
|
+
supervisor: DaemonSupervisor;
|
|
5
|
+
}
|
|
6
|
+
export interface DaemonPlan {
|
|
7
|
+
/** Daemons restarted through their supervisor (left running across the binary swap). */
|
|
8
|
+
managed: ManagedDaemon[];
|
|
9
|
+
/** Daemons the updater stops and re-spawns itself. */
|
|
10
|
+
unmanaged: DaemonState[];
|
|
11
|
+
}
|
|
12
|
+
/** The side effects of restarting daemons; injected so the orchestration is testable. */
|
|
13
|
+
export interface DaemonLifecycle {
|
|
14
|
+
/** Take an unsupervised daemon down (SIGINT) and wait for it to fully exit, before the binary swap. */
|
|
15
|
+
stopUnmanaged(daemon: DaemonState): Promise<void>;
|
|
16
|
+
/** Re-spawn an unsupervised daemon as a detached process with its original args, after the binary swap. */
|
|
17
|
+
startUnmanaged(daemon: DaemonState): Promise<void>;
|
|
18
|
+
/** Ask a supervisor to restart its daemon onto the new binary (e.g. `systemctl restart <unit>`). */
|
|
19
|
+
restartManaged(supervisor: DaemonSupervisor): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Partition the alive daemons into supervised vs. updater-managed. */
|
|
22
|
+
export declare function planDaemonRestarts(daemons: DaemonState[], resolve: (daemon: DaemonState) => Promise<DaemonSupervisor | undefined>): Promise<DaemonPlan>;
|
|
23
|
+
/**
|
|
24
|
+
* Stop the daemons that must come down before the binary swap: only the unsupervised ones.
|
|
25
|
+
* Supervised daemons keep running — their supervisor restarts them after the swap.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stopUnmanagedDaemons(plan: DaemonPlan, lifecycle: DaemonLifecycle): Promise<void>;
|
|
28
|
+
/** Re-spawn the unsupervised daemons that were stopped. Safe to call even after a no-op update. */
|
|
29
|
+
export declare function startUnmanagedDaemons(plan: DaemonPlan, lifecycle: DaemonLifecycle): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Restart supervised daemons onto the new binary, deduplicated per unit. Call only after a real
|
|
32
|
+
* install — a no-op update shouldn't bounce a supervised service, since it was never stopped.
|
|
33
|
+
*/
|
|
34
|
+
export declare function restartManagedDaemons(plan: DaemonPlan, lifecycle: DaemonLifecycle): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Partition the alive daemons into supervised vs. updater-managed. */
|
|
2
|
+
export async function planDaemonRestarts(daemons, resolve) {
|
|
3
|
+
const managed = [];
|
|
4
|
+
const unmanaged = [];
|
|
5
|
+
for (const daemon of daemons) {
|
|
6
|
+
const supervisor = await resolve(daemon);
|
|
7
|
+
if (supervisor)
|
|
8
|
+
managed.push({ daemon, supervisor });
|
|
9
|
+
else
|
|
10
|
+
unmanaged.push(daemon);
|
|
11
|
+
}
|
|
12
|
+
return { managed, unmanaged };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Stop the daemons that must come down before the binary swap: only the unsupervised ones.
|
|
16
|
+
* Supervised daemons keep running — their supervisor restarts them after the swap.
|
|
17
|
+
*/
|
|
18
|
+
export async function stopUnmanagedDaemons(plan, lifecycle) {
|
|
19
|
+
for (const daemon of plan.unmanaged)
|
|
20
|
+
await lifecycle.stopUnmanaged(daemon);
|
|
21
|
+
}
|
|
22
|
+
/** Re-spawn the unsupervised daemons that were stopped. Safe to call even after a no-op update. */
|
|
23
|
+
export async function startUnmanagedDaemons(plan, lifecycle) {
|
|
24
|
+
for (const daemon of plan.unmanaged)
|
|
25
|
+
await lifecycle.startUnmanaged(daemon);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Restart supervised daemons onto the new binary, deduplicated per unit. Call only after a real
|
|
29
|
+
* install — a no-op update shouldn't bounce a supervised service, since it was never stopped.
|
|
30
|
+
*/
|
|
31
|
+
export async function restartManagedDaemons(plan, lifecycle) {
|
|
32
|
+
const restarted = new Set();
|
|
33
|
+
for (const { supervisor } of plan.managed) {
|
|
34
|
+
const key = `${supervisor.type}:${supervisor.unit}`;
|
|
35
|
+
if (restarted.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
restarted.add(key);
|
|
38
|
+
await lifecycle.restartManaged(supervisor);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Exec = (cmd: string, args: string[]) => Promise<unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* Restart a systemd unit so it picks up a freshly installed binary. Rejects (propagating the
|
|
4
|
+
* underlying error) if systemctl is missing or the restart fails. `exec` is injectable for testing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function systemctlRestart(unit: string, exec?: Exec): Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Restart a systemd unit so it picks up a freshly installed binary. Rejects (propagating the
|
|
6
|
+
* underlying error) if systemctl is missing or the restart fails. `exec` is injectable for testing.
|
|
7
|
+
*/
|
|
8
|
+
export async function systemctlRestart(unit, exec = execFileAsync) {
|
|
9
|
+
await exec("systemctl", ["restart", unit]);
|
|
10
|
+
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED