@bitsocial/bitsocial-cli 0.19.69 → 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 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.69/src/cli/commands/challenge/install.ts)_
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.69/src/cli/commands/challenge/list.ts)_
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.69/src/cli/commands/challenge/remove.ts)_
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.69/src/cli/commands/community/create.ts)_
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.69/src/cli/commands/community/delete.ts)_
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.69/src/cli/commands/community/edit.ts)_
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.69/src/cli/commands/community/export.ts)_
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.69/src/cli/commands/community/get.ts)_
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.69/src/cli/commands/community/list.ts)_
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.69/src/cli/commands/community/start.ts)_
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.69/src/cli/commands/community/stop.ts)_
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.69/src/cli/commands/daemon.ts)_
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.69/src/cli/commands/logs.ts)_
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.69/src/cli/commands/update/check.ts)_
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.69/src/cli/commands/update/install.ts)_
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.69/src/cli/commands/update/versions.ts)_
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
- private _restartDaemons;
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 { getAliveDaemonStates, DAEMON_SHUTDOWN_TIMEOUT_MS } from "../../../common-utils/daemon-state.js";
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
- // Check for running daemons via state files
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 all running daemons
44
- for (const d of aliveDaemons) {
45
- this.log(`Stopping daemon (PID ${d.pid})...`);
46
- try {
47
- process.kill(d.pid, "SIGINT");
48
- }
49
- catch (e) {
50
- if (e.code === "ESRCH") {
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
- if (aliveDaemons.length > 0 && flags["restart-daemons"]) {
90
- // We stopped daemons but don't need to update restart them
91
- await this._restartDaemons(aliveDaemons);
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 this._restartDaemons(aliveDaemons);
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
- async _restartDaemons(daemons) {
151
- this.log(`Restarting ${daemons.length} daemon(s)...`);
152
- for (const d of daemons) {
153
- const argStr = d.argv.length > 0 ? d.argv.join(" ") : "(defaults)";
154
- this.log(` Starting daemon with args: ${argStr}`);
155
- const child = spawn("bitsocial", ["daemon", ...d.argv], {
156
- detached: true,
157
- stdio: "ignore"
158
- });
159
- child.unref();
160
- if (!child.pid) {
161
- this.warn(`Failed to spawn daemon for args: ${argStr}`);
162
- continue;
163
- }
164
- // Wait briefly for the daemon's RPC port to come up
165
- const url = new URL(d.pkcRpcUrl);
166
- const port = Number(url.port);
167
- const started = await tcpPortUsed.waitUntilUsed(port, 500, 30000).then(() => true).catch(() => false);
168
- if (started) {
169
- this.log(` Daemon started (port ${port}).`);
170
- }
171
- else {
172
- this.warn(` Daemon may not have started — port ${port} not responding after 30s. Check logs with: bitsocial logs`);
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
+ }
@@ -871,5 +871,5 @@
871
871
  ]
872
872
  }
873
873
  },
874
- "version": "0.19.69"
874
+ "version": "0.19.71"
875
875
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.69",
3
+ "version": "0.19.71",
4
4
  "description": "Command line interface to Bitsocial API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "homepage": "https://github.com/bitsocialnet/bitsocial-cli",
@@ -119,7 +119,7 @@
119
119
  "@oclif/plugin-help": "6.2.36",
120
120
  "@oclif/plugin-not-found": "3.2.73",
121
121
  "@oclif/table": "0.5.1",
122
- "@pkcprotocol/pkc-js": "0.0.41",
122
+ "@pkcprotocol/pkc-js": "0.0.45",
123
123
  "dataobject-parser": "1.2.22",
124
124
  "decompress": "4.2.1",
125
125
  "env-paths": "2.2.1",