@@ -77,7 +77,7 @@ export default class Zemu {
7777 private readonly desiredSpeculosApiPort ?: number
7878
7979 private readonly emuContainer : EmuContainer
80- public readonly containerName : string
80+ public containerName : string
8181 private lastTransportError : Error | null = null
8282
8383 public readonly elfPath : string
@@ -128,14 +128,17 @@ export default class Zemu {
128128 await new Promise < void > ( ( resolve ) => setTimeout ( resolve , timeInMs ) )
129129 }
130130
131- /** Force-removes every zemu container, giving up after KILL_TIMEOUT. */
132- static async stopAllEmuContainers ( ) : Promise < void > {
131+ /**
132+ * Force-removes every zemu container, giving up after KILL_TIMEOUT.
133+ * Pass `createdAfter` (unix seconds) to leave containers of other sessions alone.
134+ */
135+ static async stopAllEmuContainers ( createdAfter ?: number ) : Promise < void > {
133136 let timer : NodeJS . Timeout | undefined
134137 const timeout = new Promise < never > ( ( _ , reject ) => {
135138 timer = setTimeout ( ( ) => reject ( new Error ( `Could not kill all containers within ${ KILL_TIMEOUT } ms` ) ) , KILL_TIMEOUT )
136139 } )
137140 try {
138- await Promise . race ( [ EmuContainer . killContainerByName ( BASE_NAME ) , timeout ] )
141+ await Promise . race ( [ EmuContainer . killContainerByName ( BASE_NAME , createdAfter ) , timeout ] )
139142 } finally {
140143 clearTimeout ( timer )
141144 }
@@ -189,20 +192,17 @@ export default class Zemu {
189192 this . log ( 'Checking ELF' )
190193 Zemu . checkElf ( this . startOptions . model , this . elfPath )
191194
192- try {
193- await this . runContainerWithFreePorts ( )
195+ await this . runContainerWithFreePorts ( )
194196
197+ // From here on a running container exists: never leave it behind on failure
198+ try {
195199 this . log ( 'Connecting to container' )
196- await this . connect ( ) . catch ( async ( error ) => {
197- this . log ( `${ error } ` )
198- await this . close ( )
199- throw error
200- } )
201-
200+ await this . connect ( )
202201 await this . finalizeStart ( )
203- } catch ( e ) {
204- this . log ( `[ZEMU] ${ e } ` )
205- throw e
202+ } catch ( error ) {
203+ this . log ( `[ZEMU] ${ error } ` )
204+ await this . close ( ) . catch ( ( closeError ) => this . log ( `[ZEMU] Cleanup after failed start: ${ closeError } ` ) )
205+ throw error
206206 }
207207 }
208208
@@ -296,12 +296,15 @@ export default class Zemu {
296296 } )
297297 return
298298 } catch ( error ) {
299+ this . log ( `[ZEMU] ${ error } ` )
300+ // Docker may have created the container without starting it; drop it so the
301+ // name is free again and nothing is left behind
302+ await this . emuContainer . stop ( ) . catch ( ( stopError ) => this . log ( `[ZEMU] Cleanup after failed container start: ${ stopError } ` ) )
303+
299304 const portConflict = / p o r t i s a l r e a d y a l l o c a t e d | a d d r e s s a l r e a d y i n u s e / i. test ( String ( error ) )
300305 if ( ! portConflict || attempt >= MAX_ATTEMPTS ) throw error
301306
302- this . log ( `Port conflict, retrying with new ports: ${ error } ` )
303- // Docker created the container but could not start it; drop it before retrying
304- await this . emuContainer . stop ( ) . catch ( ( stopError ) => this . log ( `Cleanup after port conflict failed: ${ stopError } ` ) )
307+ this . log ( 'Port conflict, retrying with new ports' )
305308 this . transportPort = undefined as unknown as number
306309 this . speculosApiPort = undefined as unknown as number
307310 }
@@ -315,10 +318,17 @@ export default class Zemu {
315318 }
316319 }
317320
318- /** Resolves once the gRPC server is listening. Rejects if the address cannot be bound. */
319- startGRPCServer ( ip : string , port : number ) : Promise < void > {
320- this . grpcManager = new GRPCRouter ( ip , port , this . transport )
321- return this . grpcManager . startServer ( )
321+ /**
322+ * Starts a gRPC server that forwards Exchange calls to the device transport.
323+ * Resolves with the bound port (useful when `port` is 0). Rejects if the address cannot be bound.
324+ * A previously started server is shut down first.
325+ */
326+ async startGRPCServer ( ip : string , port : number ) : Promise < number > {
327+ this . stopGRPCServer ( )
328+ const router = new GRPCRouter ( ip , port , this . transport )
329+ const boundPort = await router . startServer ( )
330+ this . grpcManager = router
331+ return boundPort
322332 }
323333
324334 stopGRPCServer ( ) : void {
0 commit comments