Skip to content

Commit 61da4b4

Browse files
committed
feat: implement automatic container cleanup to prevent port conflicts
- Add cleanupStaleContainers() method to ContainerPool that runs before initialization - Use Docker API to find and remove stale zemu containers automatically - Enhanced global test setup with comprehensive process exit handlers - Added handlers for SIGINT, SIGTERM, beforeExit, uncaughtException, and unhandledRejection - Prevents port allocation conflicts by cleaning up leftover containers from previous runs - No more manual container cleanup required - the library manages this automatically This resolves port conflicts like 'Bind for 0.0.0.0:15000 failed: port is already allocated' that occurred when containers weren't properly cleaned up from interrupted test runs.
1 parent 84cbfdd commit 61da4b4

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

src/containerPool.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import axios from 'axios'
1818
import axiosRetry from 'axios-retry'
19+
import Docker from 'dockerode'
1920
import rndstr from 'randomstring'
2021
import { BASE_NAME, DEFAULT_EMU_IMG, DEFAULT_HOST, DEFAULT_START_DELAY } from './constants'
2122
import EmuContainer from './emulator'
@@ -47,6 +48,7 @@ export class ContainerPool {
4748
private portRanges: Map<TModel, { transportStart: number; speculosStart: number }> = new Map()
4849
private readonly host: string = DEFAULT_HOST
4950
private readonly emuImage: string = DEFAULT_EMU_IMG
51+
private readonly docker: Docker = new Docker()
5052

5153
private constructor() {
5254
this.initializePortRanges()
@@ -69,6 +71,9 @@ export class ContainerPool {
6971
}
7072

7173
async initialize(config: IPoolConfig): Promise<void> {
74+
// Clean up any stale containers before initializing new ones
75+
await this.cleanupStaleContainers()
76+
7277
const initPromises: Promise<void>[] = []
7378

7479
for (const [model, count] of Object.entries(config)) {
@@ -307,6 +312,41 @@ export class ContainerPool {
307312
}
308313
}
309314

315+
private async cleanupStaleContainers(): Promise<void> {
316+
try {
317+
// List all containers that match our naming pattern
318+
const containers = await this.docker.listContainers({
319+
all: true,
320+
filters: {
321+
name: [`${BASE_NAME}-pool-`, `${BASE_NAME}-`]
322+
}
323+
})
324+
325+
if (containers.length === 0) {
326+
return
327+
}
328+
329+
console.warn(`Found ${containers.length} stale zemu containers, cleaning up...`)
330+
331+
// Remove all found containers in parallel
332+
const cleanupPromises = containers.map(async (containerInfo) => {
333+
try {
334+
const container = this.docker.getContainer(containerInfo.Id)
335+
await container.remove({ force: true })
336+
} catch (error) {
337+
// Ignore errors when removing containers (they might already be removed)
338+
console.warn(`Failed to remove stale container ${containerInfo.Names?.[0]}:`, error)
339+
}
340+
})
341+
342+
await Promise.all(cleanupPromises)
343+
console.warn(`Cleaned up ${containers.length} stale containers`)
344+
} catch (error) {
345+
console.warn('Failed to cleanup stale containers:', error)
346+
// Don't throw - we want initialization to continue even if cleanup fails
347+
}
348+
}
349+
310350
async cleanup(): Promise<void> {
311351
const cleanupPromises: Promise<void>[] = []
312352

tests/globalsetup.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
import Zemu from '../src'
22

33
const catchExit = () => {
4-
process.on('SIGINT', () => {
4+
const cleanup = () => {
55
console.log('Stopping dangling containers')
66
Zemu.stopAllEmuContainers()
7+
}
8+
9+
// Handle various exit signals
10+
process.on('SIGINT', cleanup)
11+
process.on('SIGTERM', cleanup)
12+
process.on('beforeExit', cleanup)
13+
14+
// Handle uncaught exceptions
15+
process.on('uncaughtException', (error) => {
16+
console.error('Uncaught exception, cleaning up containers:', error)
17+
cleanup()
18+
process.exit(1)
19+
})
20+
21+
process.on('unhandledRejection', (reason, promise) => {
22+
console.error('Unhandled rejection, cleaning up containers:', reason)
23+
cleanup()
24+
process.exit(1)
725
})
826
}
927

0 commit comments

Comments
 (0)