@volter/supercode-teams 0.2.94 → 0.2.96
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 +52 -0
- package/bin/workspace.mjs +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// An upgrade replaces the code a long-running connector runs (its native binary, this package) under it. Its readers
|
|
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; 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';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const CODE_WATCH_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
const stamp = (file) => {
|
|
12
|
+
try { const s = statSync(file); return `${s.ino}:${s.size}:${s.mtimeMs}`; } catch { return null; }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Call `onChange(file)` once, the first time one of `files` is replaced; returns a function that stops watching. */
|
|
16
|
+
export function watchCodeChange(files, onChange) {
|
|
17
|
+
const watched = files.filter(Boolean).map((file) => (file instanceof URL ? fileURLToPath(file) : String(file)));
|
|
18
|
+
const first = new Map(watched.map((file) => [file, stamp(file)]));
|
|
19
|
+
const timer = setInterval(() => {
|
|
20
|
+
// A file that is missing for a moment (an installer between removing and writing it) is not yet a change.
|
|
21
|
+
const changed = watched.find((file) => { const now = stamp(file); return now !== null && now !== first.get(file); });
|
|
22
|
+
if (!changed) return;
|
|
23
|
+
clearInterval(timer);
|
|
24
|
+
onChange(changed);
|
|
25
|
+
}, CODE_WATCH_MS);
|
|
26
|
+
timer.unref?.();
|
|
27
|
+
return () => clearInterval(timer);
|
|
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,6 +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 { retireLinklessDaemon, watchCodeChange } from './workspace/upgrade-watch.mjs';
|
|
21
22
|
import { volterDeviceSignIn } from './workspace/volter-device.mjs';
|
|
22
23
|
import { loginSummary, signInToTeams } from './workspace/volter-login.mjs';
|
|
23
24
|
import { readSharedSignIn, signOut } from '@volter/identity';
|
|
@@ -735,8 +736,29 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
735
736
|
// The connector is the service's daemon and keeps serving; a daemon that took its socket path is the one to stop.
|
|
736
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`));
|
|
737
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);
|
|
738
743
|
try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
|
|
739
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). On Windows the service runs a per-version copy, so what an upgrade replaces is the
|
|
748
|
+
// `supercode` on PATH, and the move is the install that re-pins the service (it stops this connector and starts
|
|
749
|
+
// the new copy). Elsewhere it stops and exits non-zero, which every service manager here restarts.
|
|
750
|
+
if (saved.service) {
|
|
751
|
+
const shim = process.platform === 'win32' ? (() => { try { return execFileSync('where', ['supercode'], { encoding: 'utf8', windowsHide: true }).split(/\r?\n/).find(Boolean); } catch { return null; } })() : null;
|
|
752
|
+
watchCodeChange([process.env.SUPERCODE_BIN, new URL('../package.json', import.meta.url), shim], (changed) => {
|
|
753
|
+
process.stderr.write(`supercode teams: ${changed} changed under this connector (an upgrade); moving to the new code\n`);
|
|
754
|
+
if (process.platform === 'win32') {
|
|
755
|
+
spawn('supercode', ['teams', 'connect', '--install', '--context', contextName, '--cwd', path], { shell: true, detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
process.exitCode = 75;
|
|
759
|
+
void stopConnector?.();
|
|
760
|
+
});
|
|
761
|
+
}
|
|
740
762
|
await new Promise(resolveStop => { let stopped = false; stopConnector = async () => { if (stopped) return; stopped = true; await stopAll(); resolveStop(); }; for (const signal of ['SIGINT', 'SIGTERM', ...(process.platform === 'win32' ? ['SIGHUP', 'SIGBREAK'] : [])]) process.once(signal, stopConnector); });
|
|
741
763
|
return;
|
|
742
764
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.96",
|
|
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",
|