@volter/supercode-teams 0.2.95 → 0.2.97
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/bin/workspace/upgrade-watch.mjs +28 -2
- package/bin/workspace.mjs +29 -1
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// An upgrade replaces the code a long-running connector runs (its native binary, this package) under it. Its readers
|
|
2
2
|
// go on answering from the code they loaded, so every reading it serves is the old version's until something
|
|
3
|
-
// restarts it. This watches the files that code came from and says, once, when one of them changed
|
|
4
|
-
|
|
3
|
+
// restarts it. This watches the files that code came from and says, once, when one of them changed; and it lets the
|
|
4
|
+
// service's connector take back the socket from a linkless daemon started while it was between restarts.
|
|
5
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { connect } from 'node:net';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
6
8
|
|
|
7
9
|
const CODE_WATCH_MS = 60_000;
|
|
@@ -24,3 +26,27 @@ export function watchCodeChange(files, onChange) {
|
|
|
24
26
|
timer.unref?.();
|
|
25
27
|
return () => clearInterval(timer);
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stop the daemon `record` (its machine.json) names when it has no server link (`server: null`, a standalone
|
|
34
|
+
* `machine start`) and is not this process, and wait up to 15 s for it to go. A linked daemon is left alone.
|
|
35
|
+
*/
|
|
36
|
+
export async function retireLinklessDaemon(record) {
|
|
37
|
+
let daemon;
|
|
38
|
+
try { daemon = JSON.parse(readFileSync(record, 'utf8')); } catch { return; }
|
|
39
|
+
const pid = Number(daemon?.pid);
|
|
40
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || daemon.server || !alive(pid)) return;
|
|
41
|
+
// The record outlives a crash and its pid can be reused: only a daemon that answers on the socket is stopped.
|
|
42
|
+
const answers = await new Promise((resolve) => {
|
|
43
|
+
if (typeof daemon.socket !== 'string') return resolve(false);
|
|
44
|
+
const probe = connect(daemon.socket);
|
|
45
|
+
probe.once('connect', () => { probe.destroy(); resolve(true); });
|
|
46
|
+
probe.once('error', () => resolve(false));
|
|
47
|
+
});
|
|
48
|
+
if (!answers) return;
|
|
49
|
+
process.stderr.write(`supercode teams: a machine daemon with no server link (pid ${pid}) holds this machine's socket; asking it to stop\n`);
|
|
50
|
+
try { process.kill(pid, 'SIGTERM'); } catch { return; }
|
|
51
|
+
for (let waited = 0; waited < 15_000 && alive(pid); waited += 250) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
52
|
+
}
|
package/bin/workspace.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { loadOrCreateKeyPair, publicKeyId } from '@volter/teams/identity';
|
|
|
18
18
|
import { duration, parseArgs, positiveInteger, requireArgs, usage } from './workspace/args.mjs';
|
|
19
19
|
import { reserveArtifactDirectory, reserveProtected, writeProtected } from './workspace/files.mjs';
|
|
20
20
|
import { acquireLifecycle, activeSocket, lifecycleHeld, requestStop } from './workspace/lifecycle.mjs';
|
|
21
|
-
import { watchCodeChange } from './workspace/upgrade-watch.mjs';
|
|
21
|
+
import { retireLinklessDaemon, watchCodeChange } from './workspace/upgrade-watch.mjs';
|
|
22
22
|
import { volterDeviceSignIn } from './workspace/volter-device.mjs';
|
|
23
23
|
import { loginSummary, signInToTeams } from './workspace/volter-login.mjs';
|
|
24
24
|
import { readSharedSignIn, signOut } from '@volter/identity';
|
|
@@ -736,6 +736,34 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
736
736
|
// The connector is the service's daemon and keeps serving; a daemon that took its socket path is the one to stop.
|
|
737
737
|
machine.on('superseded', (path) => process.stderr.write(`supercode teams: another machine daemon replaced this connector's socket at ${path}; stop it (supercode teams status)\n`));
|
|
738
738
|
const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await liveCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
739
|
+
// A daemon with no server link holding the socket is a session's auto-start while this service was between
|
|
740
|
+
// restarts (from a supercode old enough not to wait for the service): it yields to the service's connector, which
|
|
741
|
+
// otherwise failed on EADDRINUSE at every restart and left this machine unlinked.
|
|
742
|
+
if (saved.service) await retireLinklessDaemon(paths(daemonHome).machine);
|
|
743
|
+
try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
|
|
744
|
+
output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
|
|
745
|
+
// Run by a service, the connector moves to an upgrade on its own: its readers would otherwise answer from the code
|
|
746
|
+
// they loaded until something restarted it (after 0.5.51 this Mac's went on reading a Codex turn waiting on an
|
|
747
|
+
// approval as busy). The move is the install, run by the upgraded CLI, because an upgrade can change the service
|
|
748
|
+
// unit too (a launchd job without ProcessType ran at background priority and missed discovery's bound): it rewrites
|
|
749
|
+
// a changed unit and restarts this connector under it. It runs in a session of its own, so a service manager
|
|
750
|
+
// stopping this connector cannot stop it half done. On Windows the service runs a per-version copy, so the upgraded
|
|
751
|
+
// CLI is the `supercode` on PATH (whose install stops this connector; the task's repeat starts the new copy).
|
|
752
|
+
// An unchanged unit leaves this connector running, and it then restarts itself: every service manager here
|
|
753
|
+
// restarts a connector that exits non-zero.
|
|
754
|
+
if (saved.service) {
|
|
755
|
+
const shim = process.platform === 'win32' ? (() => { try { return execFileSync('where', ['supercode'], { encoding: 'utf8', windowsHide: true }).split(/\r?\n/).find(Boolean); } catch { return null; } })() : null;
|
|
756
|
+
watchCodeChange([process.env.SUPERCODE_BIN, new URL('../package.json', import.meta.url), shim], (changed) => {
|
|
757
|
+
process.stderr.write(`supercode teams: ${changed} changed under this connector (an upgrade); moving to the new code\n`);
|
|
758
|
+
const cli = process.platform === 'win32' ? 'supercode' : process.env.SUPERCODE_BIN;
|
|
759
|
+
spawn(cli, ['teams', 'connect', '--install', '--context', contextName, '--cwd', path], { shell: process.platform === 'win32', detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
760
|
+
if (process.platform !== 'win32') setTimeout(() => { process.exitCode = 75; void stopConnector?.(); }, 60_000);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
// A daemon with no server link holding the socket is a session's auto-start while this service was between
|
|
764
|
+
// restarts (from a supercode old enough not to wait for the service): it yields to the service's connector, which
|
|
765
|
+
// otherwise failed on EADDRINUSE at every restart and left this machine unlinked.
|
|
766
|
+
if (saved.service) await retireLinklessDaemon(paths(daemonHome).machine);
|
|
739
767
|
try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
|
|
740
768
|
output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
|
|
741
769
|
// Run by a service, the connector moves to an upgrade on its own: its readers would otherwise answer from the code
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.97",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|