@@ -7,7 +7,7 @@ import { dispatchCommand, type CommandFlags } from './core/dispatch.ts';
77import { isCommandSupportedOnDevice } from './core/capabilities.ts' ;
88import { asAppError , AppError } from './utils/errors.ts' ;
99import { readVersion } from './utils/version.ts' ;
10- import { stopIosRunnerSession } from './platforms/ios/runner-client.ts' ;
10+ import { stopAllIosRunnerSessions } from './platforms/ios/runner-client.ts' ;
1111import type { DaemonRequest , DaemonResponse } from './daemon/types.ts' ;
1212import { SessionStore } from './daemon/session-store.ts' ;
1313import { contextFromFlags as contextFromFlagsWithLog , type DaemonCommandContext } from './daemon/context.ts' ;
@@ -18,16 +18,30 @@ import { handleRecordTraceCommands } from './daemon/handlers/record-trace.ts';
1818import { handleInteractionCommands } from './daemon/handlers/interaction.ts' ;
1919import { assertSessionSelectorMatches } from './daemon/session-selector.ts' ;
2020import { resolveEffectiveSessionName } from './daemon/session-routing.ts' ;
21+ import {
22+ isAgentDeviceDaemonProcess ,
23+ readProcessStartTime ,
24+ } from './utils/process-identity.ts' ;
2125
2226const baseDir = path . join ( os . homedir ( ) , '.agent-device' ) ;
2327const infoPath = path . join ( baseDir , 'daemon.json' ) ;
28+ const lockPath = path . join ( baseDir , 'daemon.lock' ) ;
2429const logPath = path . join ( baseDir , 'daemon.log' ) ;
2530const sessionsDir = path . join ( baseDir , 'sessions' ) ;
2631const sessionStore = new SessionStore ( sessionsDir ) ;
2732const version = readVersion ( ) ;
2833const token = crypto . randomBytes ( 24 ) . toString ( 'hex' ) ;
2934const selectorValidationExemptCommands = new Set ( [ 'session_list' , 'devices' ] ) ;
3035
36+ type DaemonLockInfo = {
37+ pid : number ;
38+ version : string ;
39+ startedAt : number ;
40+ processStartTime ?: string ;
41+ } ;
42+
43+ const daemonProcessStartTime = readProcessStartTime ( process . pid ) ?? undefined ;
44+
3145function contextFromFlags (
3246 flags : CommandFlags | undefined ,
3347 appBundleId ?: string ,
@@ -122,7 +136,7 @@ function writeInfo(port: number): void {
122136 fs . writeFileSync ( logPath , '' ) ;
123137 fs . writeFileSync (
124138 infoPath ,
125- JSON . stringify ( { port, token, pid : process . pid , version } , null , 2 ) ,
139+ JSON . stringify ( { port, token, pid : process . pid , version, processStartTime : daemonProcessStartTime } , null , 2 ) ,
126140 {
127141 mode : 0o600 ,
128142 } ,
@@ -133,7 +147,71 @@ function removeInfo(): void {
133147 if ( fs . existsSync ( infoPath ) ) fs . unlinkSync ( infoPath ) ;
134148}
135149
150+ function readLockInfo ( ) : DaemonLockInfo | null {
151+ if ( ! fs . existsSync ( lockPath ) ) return null ;
152+ try {
153+ const parsed = JSON . parse ( fs . readFileSync ( lockPath , 'utf8' ) ) as DaemonLockInfo ;
154+ if ( ! Number . isInteger ( parsed . pid ) || parsed . pid <= 0 ) return null ;
155+ return parsed ;
156+ } catch {
157+ return null ;
158+ }
159+ }
160+
161+ function acquireDaemonLock ( ) : boolean {
162+ if ( ! fs . existsSync ( baseDir ) ) fs . mkdirSync ( baseDir , { recursive : true } ) ;
163+ const lockData : DaemonLockInfo = {
164+ pid : process . pid ,
165+ version,
166+ startedAt : Date . now ( ) ,
167+ processStartTime : daemonProcessStartTime ,
168+ } ;
169+ const payload = JSON . stringify ( lockData , null , 2 ) ;
170+
171+ const tryWriteLock = ( ) : boolean => {
172+ try {
173+ fs . writeFileSync ( lockPath , payload , { flag : 'wx' , mode : 0o600 } ) ;
174+ return true ;
175+ } catch ( err ) {
176+ if ( ( err as NodeJS . ErrnoException ) . code === 'EEXIST' ) return false ;
177+ throw err ;
178+ }
179+ } ;
180+
181+ if ( tryWriteLock ( ) ) return true ;
182+ const existing = readLockInfo ( ) ;
183+ if (
184+ existing ?. pid
185+ && existing . pid !== process . pid
186+ && isAgentDeviceDaemonProcess ( existing . pid , existing . processStartTime )
187+ ) {
188+ return false ;
189+ }
190+ try {
191+ fs . unlinkSync ( lockPath ) ;
192+ } catch {
193+ // ignore
194+ }
195+ return tryWriteLock ( ) ;
196+ }
197+
198+ function releaseDaemonLock ( ) : void {
199+ const existing = readLockInfo ( ) ;
200+ if ( existing && existing . pid !== process . pid ) return ;
201+ try {
202+ if ( fs . existsSync ( lockPath ) ) fs . unlinkSync ( lockPath ) ;
203+ } catch {
204+ // ignore
205+ }
206+ }
207+
136208function start ( ) : void {
209+ if ( ! acquireDaemonLock ( ) ) {
210+ process . stderr . write ( 'Daemon lock is held by another process; exiting.\n' ) ;
211+ process . exit ( 0 ) ;
212+ return ;
213+ }
214+
137215 const server = net . createServer ( ( socket ) => {
138216 let buffer = '' ;
139217 socket . setEncoding ( 'utf8' ) ;
@@ -172,16 +250,18 @@ function start(): void {
172250 }
173251 } ) ;
174252
253+ let shuttingDown = false ;
175254 const shutdown = async ( ) => {
255+ if ( shuttingDown ) return ;
256+ shuttingDown = true ;
176257 const sessionsToStop = sessionStore . toArray ( ) ;
177258 for ( const session of sessionsToStop ) {
178- if ( session . device . platform === 'ios' ) {
179- await stopIosRunnerSession ( session . device . id ) ;
180- }
181259 sessionStore . writeSessionLog ( session ) ;
182260 }
261+ await stopAllIosRunnerSessions ( ) ;
183262 server . close ( ( ) => {
184263 removeInfo ( ) ;
264+ releaseDaemonLock ( ) ;
185265 process . exit ( 0 ) ;
186266 } ) ;
187267 } ;
0 commit comments