11// Coordinates gateway restart requests across supported supervisors.
22import { spawnSync } from "node:child_process" ;
3- import fs from "node:fs" ;
43import os from "node:os" ;
54import path from "node:path" ;
65import { getRuntimeConfig } from "../config/config.js" ;
7- import { resolveStateDir } from "../config/paths.js" ;
86import {
97 resolveGatewayLaunchAgentLabel ,
108 resolveGatewaySystemdServiceName ,
119} from "../daemon/constants.js" ;
1210import { createSubsystemLogger } from "../logging/subsystem.js" ;
1311import { resolveTimerTimeoutMs } from "../shared/number-coercion.js" ;
14- import { replaceFileAtomicSync } from "./replace-file.js" ;
12+ import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js" ;
13+ import {
14+ openOpenClawStateDatabase ,
15+ runOpenClawStateWriteTransaction ,
16+ } from "../state/openclaw-state-db.js" ;
17+ import {
18+ executeSqliteQuerySync ,
19+ executeSqliteQueryTakeFirstSync ,
20+ getNodeSqliteKysely ,
21+ } from "./kysely-sync.js" ;
1522import { cleanStaleGatewayProcessesSync , findGatewayPidsOnPortSync } from "./restart-stale-pids.js" ;
1623import type { RestartAttempt } from "./restart.types.js" ;
1724import { relaunchGatewayScheduledTask } from "./windows-task-restart.js" ;
@@ -25,11 +32,11 @@ const DEFAULT_DEFERRAL_STILL_PENDING_WARN_MS = 30_000;
2532export const DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS = 300_000 ;
2633const RESTART_COOLDOWN_MS = 30_000 ;
2734const LAUNCHCTL_ALREADY_LOADED_EXIT_CODE = 37 ;
28- const GATEWAY_RESTART_INTENT_FILENAME = "gateway-restart-intent.json " ;
35+ const GATEWAY_RESTART_INTENT_KEY = "gateway-restart" ;
2936const GATEWAY_RESTART_INTENT_TTL_MS = 60_000 ;
30- const GATEWAY_RESTART_INTENT_MAX_BYTES = 1024 ;
3137
3238const restartLog = createSubsystemLogger ( "restart" ) ;
39+ type GatewayRestartIntentDatabase = Pick < OpenClawStateKyselyDatabase , "gateway_restart_intent" > ;
3340
3441export { findGatewayPidsOnPortSync } ;
3542
@@ -137,23 +144,6 @@ export type GatewayRestartIntent = {
137144 waitMs ?: number ;
138145} ;
139146
140- function resolveGatewayRestartIntentPath ( env : NodeJS . ProcessEnv = process . env ) : string {
141- return path . join ( resolveStateDir ( env ) , GATEWAY_RESTART_INTENT_FILENAME ) ;
142- }
143-
144- function unlinkGatewayRestartIntentFileSync ( intentPath : string ) : boolean {
145- try {
146- const stat = fs . lstatSync ( intentPath ) ;
147- if ( ! stat . isFile ( ) || stat . nlink > 1 ) {
148- return false ;
149- }
150- fs . unlinkSync ( intentPath ) ;
151- return true ;
152- } catch {
153- return false ;
154- }
155- }
156-
157147function normalizeRestartIntentPid ( pid : number | undefined ) : number | null {
158148 return typeof pid === "number" && Number . isSafeInteger ( pid ) && pid > 0 ? pid : null ;
159149}
@@ -170,26 +160,46 @@ export function writeGatewayRestartIntentSync(opts: {
170160 }
171161 const env = opts . env ?? process . env ;
172162 try {
173- const intentPath = resolveGatewayRestartIntentPath ( env ) ;
174163 const reason = normalizeRestartIntentReason ( opts . reason ?? opts . intent ?. reason ) ;
175- const payload : GatewayRestartIntentPayload = {
176- kind : "gateway-restart" ,
177- pid : targetPid ,
178- createdAt : Date . now ( ) ,
179- ...( reason ? { reason } : { } ) ,
180- ...( opts . intent ?. force ? { force : true } : { } ) ,
181- ...( typeof opts . intent ?. waitMs === "number" &&
164+ const waitMs =
165+ typeof opts . intent ?. waitMs === "number" &&
182166 Number . isFinite ( opts . intent . waitMs ) &&
183167 opts . intent . waitMs >= 0
184- ? { waitMs : Math . floor ( opts . intent . waitMs ) }
185- : { } ) ,
186- } ;
187- replaceFileAtomicSync ( {
188- filePath : intentPath ,
189- content : `${ JSON . stringify ( payload ) } \n` ,
190- mode : 0o600 ,
191- tempPrefix : ".gateway-restart-intent" ,
192- } ) ;
168+ ? Math . floor ( opts . intent . waitMs )
169+ : null ;
170+ const createdAt = Date . now ( ) ;
171+ runOpenClawStateWriteTransaction (
172+ ( { db } ) => {
173+ const stateDb = getNodeSqliteKysely < GatewayRestartIntentDatabase > ( db ) ;
174+ executeSqliteQuerySync (
175+ db ,
176+ stateDb
177+ . insertInto ( "gateway_restart_intent" )
178+ . values ( {
179+ intent_key : GATEWAY_RESTART_INTENT_KEY ,
180+ kind : "gateway-restart" ,
181+ pid : targetPid ,
182+ created_at : createdAt ,
183+ reason : reason ?? null ,
184+ force : opts . intent ?. force ? 1 : null ,
185+ wait_ms : waitMs ,
186+ updated_at_ms : createdAt ,
187+ } )
188+ . onConflict ( ( conflict ) =>
189+ conflict . column ( "intent_key" ) . doUpdateSet ( {
190+ kind : ( eb ) => eb . ref ( "excluded.kind" ) ,
191+ pid : ( eb ) => eb . ref ( "excluded.pid" ) ,
192+ created_at : ( eb ) => eb . ref ( "excluded.created_at" ) ,
193+ reason : ( eb ) => eb . ref ( "excluded.reason" ) ,
194+ force : ( eb ) => eb . ref ( "excluded.force" ) ,
195+ wait_ms : ( eb ) => eb . ref ( "excluded.wait_ms" ) ,
196+ updated_at_ms : ( eb ) => eb . ref ( "excluded.updated_at_ms" ) ,
197+ } ) ,
198+ ) ,
199+ ) ;
200+ } ,
201+ { env } ,
202+ ) ;
193203 return true ;
194204 } catch ( err ) {
195205 restartLog . warn ( `failed to write gateway restart intent: ${ String ( err ) } ` ) ;
@@ -198,31 +208,57 @@ export function writeGatewayRestartIntentSync(opts: {
198208}
199209
200210export function clearGatewayRestartIntentSync ( env : NodeJS . ProcessEnv = process . env ) : void {
201- unlinkGatewayRestartIntentFileSync ( resolveGatewayRestartIntentPath ( env ) ) ;
211+ try {
212+ runOpenClawStateWriteTransaction (
213+ ( { db } ) => {
214+ const stateDb = getNodeSqliteKysely < GatewayRestartIntentDatabase > ( db ) ;
215+ executeSqliteQuerySync (
216+ db ,
217+ stateDb
218+ . deleteFrom ( "gateway_restart_intent" )
219+ . where ( "intent_key" , "=" , GATEWAY_RESTART_INTENT_KEY ) ,
220+ ) ;
221+ } ,
222+ { env } ,
223+ ) ;
224+ } catch { }
202225}
203226
204- function parseGatewayRestartIntent ( raw : string ) : GatewayRestartIntentPayload | null {
227+ function readGatewayRestartIntentPayloadSync (
228+ env : NodeJS . ProcessEnv ,
229+ ) : GatewayRestartIntentPayload | null {
205230 try {
206- const parsed = JSON . parse ( raw ) as Partial < GatewayRestartIntentPayload > ;
231+ const { db } = openOpenClawStateDatabase ( { env } ) ;
232+ const stateDb = getNodeSqliteKysely < GatewayRestartIntentDatabase > ( db ) ;
233+ const parsed = executeSqliteQueryTakeFirstSync (
234+ db ,
235+ stateDb
236+ . selectFrom ( "gateway_restart_intent" )
237+ . select ( [ "kind" , "pid" , "created_at" , "reason" , "force" , "wait_ms" ] )
238+ . where ( "intent_key" , "=" , GATEWAY_RESTART_INTENT_KEY ) ,
239+ ) ;
207240 if (
208- parsed . kind === "gateway-restart" &&
241+ parsed ? .kind === "gateway-restart" &&
209242 typeof parsed . pid === "number" &&
210243 Number . isFinite ( parsed . pid ) &&
211- typeof parsed . createdAt === "number" &&
212- Number . isFinite ( parsed . createdAt ) &&
213- ( parsed . reason === undefined || typeof parsed . reason === "string" ) &&
214- ( parsed . force === undefined || typeof parsed . force === "boolean" ) &&
215- ( parsed . waitMs === undefined ||
216- ( typeof parsed . waitMs === "number" && Number . isFinite ( parsed . waitMs ) && parsed . waitMs >= 0 ) )
244+ typeof parsed . created_at === "number" &&
245+ Number . isFinite ( parsed . created_at ) &&
246+ ( parsed . reason === null || typeof parsed . reason === "string" ) &&
247+ ( parsed . force === null ||
248+ ( typeof parsed . force === "number" && Number . isFinite ( parsed . force ) ) ) &&
249+ ( parsed . wait_ms === null ||
250+ ( typeof parsed . wait_ms === "number" &&
251+ Number . isFinite ( parsed . wait_ms ) &&
252+ parsed . wait_ms >= 0 ) )
217253 ) {
218- const reason = normalizeRestartIntentReason ( parsed . reason ) ;
254+ const reason = normalizeRestartIntentReason ( parsed . reason ?? undefined ) ;
219255 return {
220256 kind : "gateway-restart" ,
221257 pid : parsed . pid ,
222- createdAt : parsed . createdAt ,
258+ createdAt : parsed . created_at ,
223259 ...( reason ? { reason } : { } ) ,
224260 ...( parsed . force ? { force : true } : { } ) ,
225- ...( typeof parsed . waitMs === "number" ? { waitMs : Math . floor ( parsed . waitMs ) } : { } ) ,
261+ ...( typeof parsed . wait_ms === "number" ? { waitMs : Math . floor ( parsed . wait_ms ) } : { } ) ,
226262 } ;
227263 }
228264 } catch {
@@ -240,20 +276,8 @@ export function consumeGatewayRestartIntentPayloadSync(
240276 env : NodeJS . ProcessEnv = process . env ,
241277 now = Date . now ( ) ,
242278) : GatewayRestartIntent | null {
243- const intentPath = resolveGatewayRestartIntentPath ( env ) ;
244- let raw : string ;
245- try {
246- const stat = fs . lstatSync ( intentPath ) ;
247- if ( ! stat . isFile ( ) || stat . size > GATEWAY_RESTART_INTENT_MAX_BYTES ) {
248- return null ;
249- }
250- raw = fs . readFileSync ( intentPath , "utf8" ) ;
251- } catch {
252- return null ;
253- } finally {
254- clearGatewayRestartIntentSync ( env ) ;
255- }
256- const payload = parseGatewayRestartIntent ( raw ) ;
279+ const payload = readGatewayRestartIntentPayloadSync ( env ) ;
280+ clearGatewayRestartIntentSync ( env ) ;
257281 if ( ! payload ) {
258282 return null ;
259283 }
0 commit comments