| 210 | // Best-effort: maps a host port to the running Docker container(s) publishing it, so |
| 211 | // a conflict can name the offender. Returns an empty map if Docker isn't available. |
| 212 | async function getDockerPublishedPortOwners(): Promise<Map<number, string[]>> { |
| 213 | return new Promise<Map<number, string[]>>((resolve) => { |
| 214 | const child = spawn('docker', ['ps', '--format', '{{.Names}}\t{{.Ports}}'], { |
| 215 | stdio: ['ignore', 'pipe', 'ignore'], |
| 216 | }); |
| 217 | let out = ''; |
| 218 | child.stdout?.on('data', (chunk: Buffer) => { |
| 219 | out += chunk.toString(); |
| 220 | }); |
| 221 | child.on('exit', (code) => { |
| 222 | const map = new Map<number, string[]>(); |
| 223 | if (code !== 0) { |
| 224 | resolve(map); |
| 225 | return; |
| 226 | } |
| 227 | for (const line of out.split('\n')) { |
| 228 | const [name, portsStr] = line.split('\t'); |
| 229 | if (!name || !portsStr) { |
| 230 | continue; |
| 231 | } |
| 232 | // e.g. "0.0.0.0:5432->5432/tcp, :::5432->5432/tcp" — the number before |
| 233 | // each "->" is the published host port. |
| 234 | for (const m of portsStr.matchAll(/(\d+)->/g)) { |
| 235 | const port = Number(m[1]); |
| 236 | const list = map.get(port) ?? []; |
| 237 | if (!list.includes(name)) { |
| 238 | list.push(name); |
| 239 | } |
| 240 | map.set(port, list); |
| 241 | } |
| 242 | } |
| 243 | resolve(map); |
| 244 | }); |
| 245 | child.on('error', () => resolve(new Map<number, string[]>())); |
| 246 | }); |
| 247 | } |
| 248 | |
| 249 | // Stops the given containers (by name). Returns true only if all stopped cleanly. |
| 250 | async function stopDockerContainers(names: string[]): Promise<boolean> { |