1- import { readFileSync , statSync } from "node:fs" ;
1+ import { readFileSync } from "node:fs" ;
22import { join , resolve } from "node:path" ;
33
44import {
@@ -18,39 +18,42 @@ import {
1818
1919import type { ToolPackConfig } from "./config.js" ;
2020
21+ export type ToolPackControlMode = "desktop" | "headless" ;
22+
23+ const TOOL_PACK_SERVICE_STOPS = [
24+ { service : APP_KEYS . DESKTOP , options : { graceMs : 15_000 } } ,
25+ { service : APP_KEYS . WEB } ,
26+ { service : APP_KEYS . DAEMON } ,
27+ ] as const ;
28+
2129export function createToolPackControl (
2230 config : ToolPackConfig ,
31+ mode : ToolPackControlMode ,
2332) : SidecarControlAccess {
2433 const fallbackChannel = releaseChannelFromVersion ( config . appVersion )
2534 ?? releaseChannelFromNamespace ( config . namespace , "default" )
2635 ?? "local" ;
2736 const namespaceRoot = resolve ( config . roots . runtime . namespaceRoot ) ;
2837 let scope : SidecarControlScope = { channel : fallbackChannel , generation : 0 , namespace : config . namespace } ;
29- let newestIdentityMtime = Number . NEGATIVE_INFINITY ;
30- for ( const name of [ "desktop-root.json" , "headless-root.json" ] ) {
31- const identityPath = join ( namespaceRoot , "runtime" , name ) ;
32- try {
33- const identity = JSON . parse ( readFileSync ( identityPath , "utf8" ) ) as {
34- runtime ?: { channel ?: unknown ; generation ?: unknown ; namespace ?: unknown } ;
38+ const identityPath = join ( namespaceRoot , "runtime" , `${ mode } -root.json` ) ;
39+ try {
40+ const identity = JSON . parse ( readFileSync ( identityPath , "utf8" ) ) as {
41+ runtime ?: { channel ?: unknown ; generation ?: unknown ; namespace ?: unknown } ;
42+ } ;
43+ if (
44+ typeof identity . runtime ?. channel === "string"
45+ && Number . isSafeInteger ( identity . runtime . generation )
46+ && ( identity . runtime . generation as number ) >= 0
47+ && identity . runtime . namespace === config . namespace
48+ ) {
49+ scope = {
50+ channel : identity . runtime . channel ,
51+ generation : identity . runtime . generation as number ,
52+ namespace : config . namespace ,
3553 } ;
36- const mtime = statSync ( identityPath ) . mtimeMs ;
37- if (
38- mtime > newestIdentityMtime
39- && typeof identity . runtime ?. channel === "string"
40- && Number . isSafeInteger ( identity . runtime . generation )
41- && ( identity . runtime . generation as number ) >= 0
42- && identity . runtime . namespace === config . namespace
43- ) {
44- newestIdentityMtime = mtime ;
45- scope = {
46- channel : identity . runtime . channel ,
47- generation : identity . runtime . generation as number ,
48- namespace : config . namespace ,
49- } ;
50- }
51- } catch {
52- // Missing or invalid identities do not define a live control scope.
5354 }
55+ } catch {
56+ // A missing or invalid mode-owned identity does not define a live scope.
5457 }
5558 return accessControlPlane ( {
5659 runtimeRoot : join ( namespaceRoot , "runtime" ) ,
@@ -61,11 +64,7 @@ export function createToolPackControl(
6164export async function stopToolPackServices (
6265 control : SidecarControlAccess ,
6366) : Promise < SidecarConvergeResult [ ] > {
64- const attempts = await stopSidecarServices ( control , [
65- { service : APP_KEYS . DESKTOP , options : { graceMs : 15_000 } } ,
66- { service : APP_KEYS . WEB } ,
67- { service : APP_KEYS . DAEMON } ,
68- ] ) ;
67+ const attempts = await stopSidecarServices ( control , TOOL_PACK_SERVICE_STOPS ) ;
6968 const failures = attempts . flatMap ( ( attempt ) =>
7069 attempt . status === "rejected"
7170 ? [ new Error ( `failed to stop ${ attempt . service } ` , { cause : attempt . error } ) ]
@@ -79,3 +78,21 @@ export async function stopToolPackServices(
7978 return attempt . result ;
8079 } ) ;
8180}
81+
82+ export async function convergeToolPackServices (
83+ control : SidecarControlAccess ,
84+ ) : Promise < SidecarConvergeResult [ ] > {
85+ const results = await stopToolPackServices ( control ) ;
86+ const failures = results . flatMap ( ( result , index ) =>
87+ result . stopped
88+ ? [ ]
89+ : [ new Error (
90+ `could not prove ${ TOOL_PACK_SERVICE_STOPS [ index ] ! . service } stopped`
91+ + ( result . pid == null ? "" : ` (pid ${ result . pid } )` ) ,
92+ ) ] ,
93+ ) ;
94+ if ( failures . length > 0 ) {
95+ throw new AggregateError ( failures , "failed to converge one or more packaged services" ) ;
96+ }
97+ return results ;
98+ }
0 commit comments