webhelm 1.0.3 → 1.0.5
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/package.json +1 -1
- package/runtime/chrome.mjs +30 -6
- package/runtime/cli.mjs +4 -2
- package/runtime/login-guard.mjs +47 -0
- package/runtime/managed-chrome-guard.mjs +21 -0
- package/runtime/node-daemon.mjs +39 -2
package/package.json
CHANGED
package/runtime/chrome.mjs
CHANGED
|
@@ -23,10 +23,19 @@ function alive(pid) {
|
|
|
23
23
|
try { if(!pid) return false;process.kill(Number(pid), 0);return true; }
|
|
24
24
|
catch {return false;}
|
|
25
25
|
}
|
|
26
|
+
function matchesManagedChromeProcess(pid,port) {
|
|
27
|
+
if(process.platform==='win32')return alive(pid); // Windows command-line discovery needs a separate provider.
|
|
28
|
+
try {
|
|
29
|
+
const command=execFileSync('ps',['-p',String(pid),'-o','command='],{encoding:'utf8',timeout:2500});
|
|
30
|
+
return command.includes(`--user-data-dir=${PROFILE}`) &&
|
|
31
|
+
command.includes(`--remote-debugging-port=${port}`) &&
|
|
32
|
+
!command.includes('--type=');
|
|
33
|
+
} catch{return false;}
|
|
34
|
+
}
|
|
26
35
|
async function fromState() {
|
|
27
36
|
let state;
|
|
28
37
|
try { state=JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return null; }
|
|
29
|
-
if (state?.pid && Number.isInteger(state.port) &&
|
|
38
|
+
if (state?.pid && Number.isInteger(state.port) && matchesManagedChromeProcess(state.pid,state.port) && await endpointLive(state.port)) return state;
|
|
30
39
|
return null;
|
|
31
40
|
}
|
|
32
41
|
async function discover() {
|
|
@@ -39,7 +48,7 @@ async function discover() {
|
|
|
39
48
|
const match=line.match(/^\s*(\d+)\s+.*?--remote-debugging-port=(\d+)/);
|
|
40
49
|
if (!match) continue;
|
|
41
50
|
const [pid,port]=match.slice(1).map(Number);
|
|
42
|
-
if (!
|
|
51
|
+
if (!matchesManagedChromeProcess(pid,port) || !await endpointLive(port)) continue;
|
|
43
52
|
const state={pid,port,profile_dir:PROFILE,chrome_path:line.slice(line.search(/\S/)).replace(/^\d+\s*/, '').split(' --user-data-dir=')[0]};
|
|
44
53
|
fs.writeFileSync(STATE,JSON.stringify(state,null,2));
|
|
45
54
|
return state;
|
|
@@ -91,6 +100,24 @@ async function lockOwnership(fn) {
|
|
|
91
100
|
try {return await fn();}
|
|
92
101
|
finally {fs.closeSync(fd);try {fs.unlinkSync(LOCK);}catch{}}
|
|
93
102
|
}
|
|
103
|
+
export function chromeMajorFromVersion(versionText){
|
|
104
|
+
const match=String(versionText).match(/(?:Chrome|Chromium)\s+(?:for Testing\s+)?(\d+)\./);
|
|
105
|
+
const major=match?Number(match[1]):NaN;
|
|
106
|
+
if(!Number.isSafeInteger(major)||major<50)throw Error(`Cannot determine managed Chrome version: ${versionText}`);
|
|
107
|
+
return major;
|
|
108
|
+
}
|
|
109
|
+
function chromeMajor(chromePath){
|
|
110
|
+
return chromeMajorFromVersion(execFileSync(chromePath,['--version'],{encoding:'utf8',timeout:5000}).trim());
|
|
111
|
+
}
|
|
112
|
+
export function chromeLaunchArgs(profile,port,extensionPath,major){
|
|
113
|
+
const args=[`--user-data-dir=${profile}`,`--remote-debugging-port=${port}`];
|
|
114
|
+
// Chrome 138 can load unpacked extensions with a compatibility flag.
|
|
115
|
+
// Chrome 139+ must rely on Extensions.loadUnpacked through CDP instead:
|
|
116
|
+
// passing --load-extension can leave a stale, non-running extension entry.
|
|
117
|
+
if(major<=138)args.push('--disable-features=DisableLoadExtensionCommandLineSwitch',`--load-extension=${extensionPath}`);
|
|
118
|
+
args.push('--no-first-run','--no-default-browser-check','about:blank');
|
|
119
|
+
return args;
|
|
120
|
+
}
|
|
94
121
|
export async function ensureManagedChrome(){
|
|
95
122
|
// Explicit CDP endpoints also support hermetic mock-browser tests.
|
|
96
123
|
const specified=process.env.WEBHELM_CDP_URL;
|
|
@@ -107,10 +134,7 @@ export async function ensureManagedChrome(){
|
|
|
107
134
|
const extension=stageExtension();
|
|
108
135
|
const log=fs.openSync(path.join(ROOT,'managed-chrome.log'),'a');
|
|
109
136
|
fs.mkdirSync(PROFILE,{recursive:true});
|
|
110
|
-
const child=spawn(chrome,
|
|
111
|
-
'--disable-features=DisableLoadExtensionCommandLineSwitch',
|
|
112
|
-
`--load-extension=${extension.path}`,
|
|
113
|
-
'--no-first-run','--no-default-browser-check','about:blank'],
|
|
137
|
+
const child=spawn(chrome,chromeLaunchArgs(PROFILE,port,extension.path,chromeMajor(chrome)),
|
|
114
138
|
{detached:true,stdio:['ignore',log,log]});
|
|
115
139
|
child.unref();
|
|
116
140
|
fs.closeSync(log);
|
package/runtime/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureDaemon, request, handle, SOCKET } from './node-daemon.mjs';
|
|
|
8
8
|
import { PLATFORMS, listPlatforms, platformProfile, actionPath, validateConfig } from './platforms.mjs';
|
|
9
9
|
import { ACTIONS_DIR, ensureActions, syncActions, publishActions } from './actions-repo.mjs';
|
|
10
10
|
import { migrateConfig, migrateChromeProfile } from './migrate.mjs';
|
|
11
|
+
import { installLoginGuard } from './login-guard.mjs';
|
|
11
12
|
|
|
12
13
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const emit = value => console.log(JSON.stringify(value));
|
|
@@ -108,14 +109,15 @@ async function main(args = process.argv.slice(2)) {
|
|
|
108
109
|
if (group === 'setup') {
|
|
109
110
|
const repo=await ensureActions();
|
|
110
111
|
const chrome=await ensureDaemon();
|
|
111
|
-
|
|
112
|
+
const login_guard=installLoginGuard();
|
|
113
|
+
return emit({status:'ready',...repo,daemon_pid:chrome.pid,managed_chrome:chrome.managed_chrome,login_guard});
|
|
112
114
|
}
|
|
113
115
|
if (group === 'status') {
|
|
114
116
|
const daemon = await ensureDaemon();
|
|
115
117
|
return emit({version:daemon.version, platform_dir:PLATFORMS, root:ROOT,
|
|
116
118
|
cli:fileURLToPath(import.meta.url),
|
|
117
119
|
admin_helper:process.env.WEBHELM_ADMIN_HELPER || (fs.existsSync(path.join(os.homedir(),'.agents/skills/webhelm/scripts/admin.mjs')) ? path.join(os.homedir(),'.agents/skills/webhelm/scripts/admin.mjs') : path.join(ROOT,'admin.mjs')),
|
|
118
|
-
managed_chrome:daemon.managed_chrome,
|
|
120
|
+
managed_chrome:daemon.managed_chrome, chrome_guard:daemon.chrome_guard,
|
|
119
121
|
daemon:{running:true, pid:daemon.pid, session_count:daemon.session_count, queue_paused:daemon.queue_paused, socket:SOCKET}});
|
|
120
122
|
}
|
|
121
123
|
if (group === 'capacity') {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {execFileSync} from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const LABEL='com.lalalic.webhelm.keepalive';
|
|
7
|
+
const escapeXml=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
8
|
+
export function loginGuardDefinition(home=os.homedir()) {
|
|
9
|
+
const cli=path.join(home,'.local/bin/webhelm');
|
|
10
|
+
const environment=[path.join(home,'.local/bin'),path.join(home,'.local/opt/node22/bin'),
|
|
11
|
+
'/opt/homebrew/bin','/usr/local/bin','/usr/bin','/bin'].join(':');
|
|
12
|
+
// The short-lived status check starts a missing daemon, and is a no-op if
|
|
13
|
+
// it already owns the IPC socket. Node daemon independently repairs Chrome.
|
|
14
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${LABEL}</string>\n<key>ProgramArguments</key><array><string>${escapeXml(cli)}</string><string>status</string></array>\n<key>EnvironmentVariables</key><dict><key>PATH</key><string>${escapeXml(environment)}</string><key>HOME</key><string>${escapeXml(home)}</string></dict>\n<key>RunAtLoad</key><true/>\n<key>StartInterval</key><integer>300</integer>\n<key>StandardOutPath</key><string>/dev/null</string>\n<key>StandardErrorPath</key><string>/dev/null</string>\n</dict></plist>\n`;
|
|
15
|
+
}
|
|
16
|
+
export function installLoginGuard({home=os.homedir(), platform=process.platform, apply=true}={}){
|
|
17
|
+
if(platform!=='darwin')return {status:'unsupported',label:LABEL};
|
|
18
|
+
const cli=path.join(home,'.local/bin/webhelm');
|
|
19
|
+
if(!fs.existsSync(cli))return {status:'skipped',reason:'webhelm Skill launcher missing',label:LABEL};
|
|
20
|
+
const dest=path.join(home,'Library/LaunchAgents',`${LABEL}.plist`);
|
|
21
|
+
const content=loginGuardDefinition(home);
|
|
22
|
+
fs.mkdirSync(path.dirname(dest),{recursive:true});
|
|
23
|
+
if(fs.existsSync(dest)){
|
|
24
|
+
const old=fs.readFileSync(dest,'utf8');
|
|
25
|
+
if(!old.includes(`<string>${LABEL}</string>`))throw Error(`Refusing to replace unrelated LaunchAgent: ${dest}`);
|
|
26
|
+
}
|
|
27
|
+
const updated=!fs.existsSync(dest)||fs.readFileSync(dest,'utf8')!==content;
|
|
28
|
+
if(updated){
|
|
29
|
+
const staging=dest+`.${process.pid}.tmp`;
|
|
30
|
+
fs.writeFileSync(staging,content,{mode:0o600});fs.renameSync(staging,dest);
|
|
31
|
+
}
|
|
32
|
+
const service=`gui/${process.getuid()}/${LABEL}`;
|
|
33
|
+
if(!apply)return {status:'staged',path:dest,updated,label:LABEL};
|
|
34
|
+
try {
|
|
35
|
+
execFileSync('launchctl',['print',service],{timeout:5000,stdio:'ignore'});
|
|
36
|
+
if(updated){
|
|
37
|
+
execFileSync('launchctl',['bootout',service],{timeout:5000,stdio:'ignore'});
|
|
38
|
+
execFileSync('launchctl',['bootstrap',`gui/${process.getuid()}`,dest],{timeout:5000,stdio:'ignore'});
|
|
39
|
+
}
|
|
40
|
+
return {status:'active',path:dest,updated,label:LABEL};
|
|
41
|
+
} catch(error){
|
|
42
|
+
try{
|
|
43
|
+
execFileSync('launchctl',['bootstrap',`gui/${process.getuid()}`,dest],{timeout:5000,stdio:'ignore'});
|
|
44
|
+
return {status:'active',path:dest,updated,label:LABEL};
|
|
45
|
+
}catch(e){return {status:'staged_not_loaded',path:dest,label:LABEL,reason:e.message};}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// A daemon-owned, non-overlapping health loop. Do not use a separate worker or
|
|
2
|
+
// close a user's Chrome windows to recover the managed browser.
|
|
3
|
+
export function createManagedChromeGuard(check, {intervalMs=15000, onError=()=>{}}={}) {
|
|
4
|
+
if(typeof check!=='function')throw TypeError('health check must be a function');
|
|
5
|
+
let timer=null, running=false, stopped=false;
|
|
6
|
+
let last={status:'not_checked'};
|
|
7
|
+
const tick=async()=>{
|
|
8
|
+
if(running||stopped)return last;
|
|
9
|
+
running=true;
|
|
10
|
+
try{last={status:'healthy',...(await check())};}
|
|
11
|
+
catch(error){last={status:'degraded',error:String(error?.message||error)};onError(error);}
|
|
12
|
+
finally{running=false;}
|
|
13
|
+
return last;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
tick,
|
|
17
|
+
start(){if(timer)return;stopped=false;timer=setInterval(()=>{void tick()},intervalMs);timer.unref?.();void tick()},
|
|
18
|
+
stop(){stopped=true;if(timer)clearInterval(timer);timer=null},
|
|
19
|
+
status(){return {...last}}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/runtime/node-daemon.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { ChatGPTQueue } from './chatgpt/chatgpt-queue.mjs';
|
|
|
11
11
|
import { idleUrlFromIdentity, sameIdleUrl, staleSession } from './session-lifecycle.mjs';
|
|
12
12
|
import { ensureManagedChrome } from './chrome.mjs';
|
|
13
13
|
import { ensureExtensionLoaded } from './extension-manager.mjs';
|
|
14
|
+
import { createManagedChromeGuard } from './managed-chrome-guard.mjs';
|
|
14
15
|
import { PROFILE_DIR, RUNTIME_DIR, requireMigratedConfig } from './config-paths.mjs';
|
|
15
16
|
|
|
16
17
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -121,6 +122,42 @@ async function connectBrowser() {
|
|
|
121
122
|
finally{connectingBrowser=null;}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// The daemon owns both the managed Chrome process and the default Harness
|
|
126
|
+
// group. Chrome auto-updates can terminate the browser while the Node daemon
|
|
127
|
+
// survives; an RPC-only health check cannot recover that loss.
|
|
128
|
+
async function guardManagedChrome(){
|
|
129
|
+
if(shuttingDown || process.env.WEBHELM_CDP_URL || process.env.WEBHELM_NO_PROFILE_EXTENSION_PROBE==='1')
|
|
130
|
+
return {skipped:true};
|
|
131
|
+
const actual=await ensureManagedChrome();
|
|
132
|
+
if(!browserWs || browserWs.closed || managedChrome?.pid!==actual.pid || managedChrome?.endpoint!==actual.endpoint){
|
|
133
|
+
const old=browserWs;
|
|
134
|
+
browserWs=null;connected=false;debuggerSession=null;
|
|
135
|
+
if(old && !old.closed)old.ws?.close();
|
|
136
|
+
await connectBrowser();
|
|
137
|
+
}
|
|
138
|
+
managedChrome=actual;
|
|
139
|
+
// Session leasing also creates missing workspaces. Serialize this short
|
|
140
|
+
// identity transaction with session start so one group is created, not two.
|
|
141
|
+
const state=await leaseMutex('Harness').run(async()=>{
|
|
142
|
+
let current;
|
|
143
|
+
try {current=await extensionCall(null,'workspace.status',{name:'Harness'});}
|
|
144
|
+
catch(error){
|
|
145
|
+
if(sessions.size)throw error; // Never hot reload an Extension used by active Sessions.
|
|
146
|
+
const browser=await connectBrowser();
|
|
147
|
+
await ensureExtensionLoaded(browser);
|
|
148
|
+
current=await extensionCall(null,'workspace.status',{name:'Harness'});
|
|
149
|
+
}
|
|
150
|
+
if(!current?.initialized || !Number.isInteger(current.physicalTabs) || current.physicalTabs<1)
|
|
151
|
+
current=await extensionCall(null,'workspace.ensure',{name:'Harness'});
|
|
152
|
+
return current;
|
|
153
|
+
});
|
|
154
|
+
return {managed_chrome_pid:actual.pid,group_id:state.groupId,physical_tabs:state.physicalTabs};
|
|
155
|
+
}
|
|
156
|
+
const managedChromeGuard=createManagedChromeGuard(guardManagedChrome,{
|
|
157
|
+
intervalMs:Number(process.env.WEBHELM_CHROME_GUARD_INTERVAL_MS||15000),
|
|
158
|
+
onError:error=>console.warn(`WebHelm managed Chrome health check: ${error.message}`),
|
|
159
|
+
});
|
|
160
|
+
|
|
124
161
|
function adoptPopup(info){if(info?.type!=='page'||!info.openerId)return;const owner=targets.get(info.openerId);if(owner){owner.targets.add(info.targetId);targets.set(info.targetId,owner)}}
|
|
125
162
|
async function attach(s,targetId,b=null){b ||= await connectBrowser();const {sessionId}=await b.send('Target.attachToTarget',{targetId,flatten:true});s.targets.add(targetId);s.attachments.set(targetId,sessionId);targets.set(targetId,s);for(const d of ['Page','DOM','Runtime','Network'])await b.send(`${d}.enable`,{},sessionId);for(const dispose of s.eventDisposers?.get(targetId)||[])dispose();const removers=[];for(const method of ['Page.loadEventFired','Page.domContentEventFired','Page.javascriptDialogOpening','Page.javascriptDialogClosed','Runtime.exceptionThrown','Network.requestWillBeSent','Network.responseReceived'])removers.push(b.on(method,(params,owner)=>{if(owner===sessionId){if(s.events.length>=500)s.events.shift();s.events.push({method,params,session_id:sessionId,target_id:targetId})}}));s.eventDisposers?.set(targetId,removers);return sessionId}
|
|
126
163
|
function sidFor(s,targetId=s.targetId){const sid=s.attachments.get(targetId);if(!sid)throw new Error(`target ${targetId} is not attached to this session`);return sid}
|
|
@@ -538,7 +575,7 @@ async function runPlatform(req) {
|
|
|
538
575
|
self_heal_capable:!auto,result};
|
|
539
576
|
}
|
|
540
577
|
|
|
541
|
-
export async function handle(req){switch(req.op){case'ping':return {ok:true,pid:process.pid,version:PACKAGE_VERSION,queue_paused:process.env.WEBHELM_CHATGPT_QUEUE_PAUSED==='1',session_count:sessions.size,managed_chrome:managedChrome||{profile_dir:PROFILE_DIR, endpoint:process.env.WEBHELM_CDP_URL||'http://127.0.0.1:9222',reused:true}};case'heartbeat':return heartbeatSession(req.session_id);case'list':return {session_count:sessions.size,sessions:[...sessions.values()].map(s=>({session_id:s.session_id,workspace:s.workspace,target_id:s.targetId,last_used_at:s.last_used_at}))};case'start':return createSession(req);case'exec':return execute(req);case'stop':return stopSession(req.session_id);case'stop-all':{const stopped=[];for(const id of [...sessions.keys()])stopped.push(await stopSession(id));return {stopped_count:stopped.length,error_count:0,stopped,errors:[]}}case'capacity':return workspaceCapacity(req);case'workspace-create':case'workspace-delete':return workspaceOp(req);case'chatgpt-new-turn-enqueue':return getChatGPTQueue().enqueue(req.config);case'platform-run':return runPlatform(req);default:throw Error(`unknown op: ${req.op}`)}}
|
|
578
|
+
export async function handle(req){switch(req.op){case'ping':return {ok:true,pid:process.pid,version:PACKAGE_VERSION,queue_paused:process.env.WEBHELM_CHATGPT_QUEUE_PAUSED==='1',session_count:sessions.size,managed_chrome:managedChrome||{profile_dir:PROFILE_DIR, endpoint:process.env.WEBHELM_CDP_URL||'http://127.0.0.1:9222',reused:true},chrome_guard:managedChromeGuard.status()};case'heartbeat':return heartbeatSession(req.session_id);case'list':return {session_count:sessions.size,sessions:[...sessions.values()].map(s=>({session_id:s.session_id,workspace:s.workspace,target_id:s.targetId,last_used_at:s.last_used_at}))};case'start':return createSession(req);case'exec':return execute(req);case'stop':return stopSession(req.session_id);case'stop-all':{const stopped=[];for(const id of [...sessions.keys()])stopped.push(await stopSession(id));return {stopped_count:stopped.length,error_count:0,stopped,errors:[]}}case'capacity':return workspaceCapacity(req);case'workspace-create':case'workspace-delete':return workspaceOp(req);case'chatgpt-new-turn-enqueue':return getChatGPTQueue().enqueue(req.config);case'platform-run':return runPlatform(req);default:throw Error(`unknown op: ${req.op}`)}}
|
|
542
579
|
function serve(conn) {
|
|
543
580
|
let buffer = '';
|
|
544
581
|
conn.setEncoding('utf8');
|
|
@@ -556,7 +593,7 @@ function serve(conn) {
|
|
|
556
593
|
});
|
|
557
594
|
}
|
|
558
595
|
|
|
559
|
-
async function main(){fs.mkdirSync(RUNTIME,{recursive:true,mode:0o700});let lockfd;try{lockfd=fs.openSync(LOCK,'wx',0o600);fs.writeFileSync(lockfd,String(process.pid))}catch{throw Error(`Node pilot lock exists: ${LOCK}`)}if(fs.existsSync(SOCKET)){try{const c=net.createConnection(SOCKET);await new Promise((res,rej)=>{c.once('connect',res);c.once('error',rej)});c.destroy();fs.closeSync(lockfd);fs.unlinkSync(LOCK);throw Error(`Node daemon socket already active: ${SOCKET}`)}catch(e){if(e.message.includes('already active'))throw e;fs.unlinkSync(SOCKET)}}socket=net.createServer(serve);await new Promise((res,rej)=>socket.listen(SOCKET,res).once('error',rej));fs.chmodSync(SOCKET,0o600);connectBrowser().catch(()=>{});getChatGPTQueue();const cleanup=async()=>{if(shuttingDown)return;shuttingDown=true;if(chatgptQueue)await chatgptQueue.shutdown().catch(()=>{});await handle({op:'stop-all'}).catch(()=>{});socket?.close();try{if(fs.existsSync(SOCKET))fs.unlinkSync(SOCKET)}catch{}try{fs.closeSync(lockfd);fs.unlinkSync(LOCK)}catch{}process.exit(0)};process.on('SIGTERM',cleanup);process.on('SIGINT',cleanup)}
|
|
596
|
+
async function main(){fs.mkdirSync(RUNTIME,{recursive:true,mode:0o700});let lockfd;try{lockfd=fs.openSync(LOCK,'wx',0o600);fs.writeFileSync(lockfd,String(process.pid))}catch{throw Error(`Node pilot lock exists: ${LOCK}`)}if(fs.existsSync(SOCKET)){try{const c=net.createConnection(SOCKET);await new Promise((res,rej)=>{c.once('connect',res);c.once('error',rej)});c.destroy();fs.closeSync(lockfd);fs.unlinkSync(LOCK);throw Error(`Node daemon socket already active: ${SOCKET}`)}catch(e){if(e.message.includes('already active'))throw e;fs.unlinkSync(SOCKET)}}socket=net.createServer(serve);await new Promise((res,rej)=>socket.listen(SOCKET,res).once('error',rej));fs.chmodSync(SOCKET,0o600);connectBrowser().catch(()=>{});managedChromeGuard.start();getChatGPTQueue();const cleanup=async()=>{if(shuttingDown)return;shuttingDown=true;managedChromeGuard.stop();if(chatgptQueue)await chatgptQueue.shutdown().catch(()=>{});await handle({op:'stop-all'}).catch(()=>{});socket?.close();try{if(fs.existsSync(SOCKET))fs.unlinkSync(SOCKET)}catch{}try{fs.closeSync(lockfd);fs.unlinkSync(LOCK)}catch{}process.exit(0)};process.on('SIGTERM',cleanup);process.on('SIGINT',cleanup)}
|
|
560
597
|
async function request(payload,ensure=true){if(ensure)await ensureDaemon();return new Promise((resolve,reject)=>{const c=net.createConnection(SOCKET);let data='';c.setEncoding('utf8');c.on('connect',()=>c.write(JSON.stringify(payload)+'\n'));c.on('data',x=>{data+=x;if(!data.includes('\n'))return;c.end();try{const r=JSON.parse(data);r.error?reject(Error(r.error)):resolve(r)}catch(e){reject(e)}});c.on('error',reject)})}
|
|
561
598
|
export async function ensureDaemon(){try{const p=await request({op:'ping'},false);if(p.ok)return p}catch{}if(!process.env.WEBHELM_CDP_URL)requireMigratedConfig();fs.mkdirSync(RUNTIME,{recursive:true,mode:0o700});if(fs.existsSync(LOCK)){let owner=0;try{owner=Number(fs.readFileSync(LOCK,'utf8').trim())}catch{};let alive=false;if(owner>0)try{process.kill(owner,0);alive=true}catch{};if(alive)throw Error(`daemon owner lock exists but socket is unreachable; refusing duplicate startup (${LOCK})`);fs.unlinkSync(LOCK)}const child=await import('node:child_process');const log=fs.openSync(path.join(RUNTIME,'session-node.log'),'a');child.spawn(process.execPath,[fileURLToPath(import.meta.url),'daemon'],{detached:true,stdio:['ignore',log,log],env:process.env}).unref();for(let i=0;i<100;i++){await delay(100);try{const p=await request({op:'ping'},false);if(p.ok)return p}catch{}}throw Error(`WebHelm Node daemon did not start; see ${path.join(RUNTIME,'session-node.log')}`)}
|
|
562
599
|
if(process.argv[2]==='daemon')main().catch(e=>{console.error(e);process.exit(1)});
|