|
5 | 5 | */ |
6 | 6 |
|
7 | 7 | import assert from 'node:assert'; |
8 | | -import {spawn, type ChildProcess} from 'node:child_process'; |
| 8 | +import {execFile, spawn, type ChildProcess} from 'node:child_process'; |
9 | 9 | import fs from 'node:fs'; |
10 | 10 | import os from 'node:os'; |
11 | 11 | import path from 'node:path'; |
12 | | -import {describe, it} from 'node:test'; |
| 12 | +import {describe, it, type TestContext as NodeTestContext} from 'node:test'; |
13 | 13 |
|
14 | 14 | const SERVER_PATH = path.resolve('build/src/main.js'); |
15 | 15 | const WATCHDOG_START_PATTERN = /Watchdog started[\s\S]*?"pid":\s*(\d+)/; |
@@ -145,6 +145,126 @@ describe('Telemetry E2E', () => { |
145 | 145 | } |
146 | 146 | } |
147 | 147 |
|
| 148 | + async function runWindowsTaskkillTest(t: NodeTestContext): Promise<void> { |
| 149 | + if (process.platform !== 'win32') { |
| 150 | + t.skip('Windows only test'); |
| 151 | + return; |
| 152 | + } |
| 153 | + const ctx: TestContext = { |
| 154 | + logFile: createLogFilePath('taskkill'), |
| 155 | + }; |
| 156 | + |
| 157 | + try { |
| 158 | + ctx.process = spawn( |
| 159 | + process.execPath, |
| 160 | + [ |
| 161 | + SERVER_PATH, |
| 162 | + `--log-file=${ctx.logFile}`, |
| 163 | + '--usage-statistics', |
| 164 | + '--headless', |
| 165 | + ], |
| 166 | + {stdio: ['pipe', 'pipe', 'pipe']}, |
| 167 | + ); |
| 168 | + |
| 169 | + const match = await waitForLogPattern( |
| 170 | + ctx.logFile, |
| 171 | + WATCHDOG_START_PATTERN, |
| 172 | + ); |
| 173 | + assert.ok(match, 'Watchdog start log not found'); |
| 174 | + ctx.watchdogPid = parseInt(match[1], 10); |
| 175 | + assert.ok(ctx.watchdogPid > 0, 'Invalid watchdog PID'); |
| 176 | + |
| 177 | + await new Promise<void>((resolve, reject) => { |
| 178 | + execFile( |
| 179 | + 'taskkill', |
| 180 | + ['/PID', ctx.process!.pid!.toString(), '/T', '/F'], |
| 181 | + (error: unknown) => { |
| 182 | + if (error) { |
| 183 | + reject(error); |
| 184 | + } else { |
| 185 | + resolve(); |
| 186 | + } |
| 187 | + }, |
| 188 | + ); |
| 189 | + }); |
| 190 | + |
| 191 | + await waitForProcessExit(ctx.watchdogPid); |
| 192 | + |
| 193 | + const shutdownMatch = await waitForLogPattern( |
| 194 | + ctx.logFile, |
| 195 | + SHUTDOWN_PATTERN, |
| 196 | + 2000, |
| 197 | + ); |
| 198 | + assert.ok(shutdownMatch, 'server_shutdown not logged'); |
| 199 | + |
| 200 | + const deathMatch = await waitForLogPattern( |
| 201 | + ctx.logFile, |
| 202 | + PARENT_DEATH_PATTERN, |
| 203 | + 2000, |
| 204 | + ); |
| 205 | + assert.ok(deathMatch, 'Parent death not detected'); |
| 206 | + } finally { |
| 207 | + cleanupTest(ctx); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + async function runPosixGroupKillTest(t: NodeTestContext): Promise<void> { |
| 212 | + if (process.platform === 'win32') { |
| 213 | + t.skip('POSIX only test'); |
| 214 | + return; |
| 215 | + } |
| 216 | + const ctx: TestContext = { |
| 217 | + logFile: createLogFilePath('sigterm-group'), |
| 218 | + }; |
| 219 | + |
| 220 | + try { |
| 221 | + // Spawn detached to create a new process group |
| 222 | + ctx.process = spawn( |
| 223 | + process.execPath, |
| 224 | + [ |
| 225 | + SERVER_PATH, |
| 226 | + `--log-file=${ctx.logFile}`, |
| 227 | + '--usage-statistics', |
| 228 | + '--headless', |
| 229 | + ], |
| 230 | + { |
| 231 | + stdio: ['pipe', 'pipe', 'pipe'], |
| 232 | + detached: true, |
| 233 | + }, |
| 234 | + ); |
| 235 | + |
| 236 | + const match = await waitForLogPattern( |
| 237 | + ctx.logFile, |
| 238 | + WATCHDOG_START_PATTERN, |
| 239 | + ); |
| 240 | + assert.ok(match, 'Watchdog start log not found'); |
| 241 | + ctx.watchdogPid = parseInt(match[1], 10); |
| 242 | + assert.ok(ctx.watchdogPid > 0, 'Invalid watchdog PID'); |
| 243 | + |
| 244 | + // Kill the process group |
| 245 | + process.kill(-ctx.process.pid!, 'SIGTERM'); |
| 246 | + await waitForProcessExit(ctx.watchdogPid); |
| 247 | + |
| 248 | + const shutdownMatch = await waitForLogPattern( |
| 249 | + ctx.logFile, |
| 250 | + SHUTDOWN_PATTERN, |
| 251 | + 2000, |
| 252 | + ); |
| 253 | + assert.ok(shutdownMatch, 'server_shutdown not logged'); |
| 254 | + |
| 255 | + const deathMatch = await waitForLogPattern( |
| 256 | + ctx.logFile, |
| 257 | + PARENT_DEATH_PATTERN, |
| 258 | + 2000, |
| 259 | + ); |
| 260 | + assert.ok(deathMatch, 'Parent death not detected'); |
| 261 | + } finally { |
| 262 | + cleanupTest(ctx); |
| 263 | + } |
| 264 | + } |
| 265 | + |
148 | 266 | it('handles SIGKILL', () => runSignalTest('SIGKILL')); |
149 | 267 | it('handles SIGTERM', () => runSignalTest('SIGTERM')); |
| 268 | + it('handles Windows taskkill /T /F', t => runWindowsTaskkillTest(t)); |
| 269 | + it('handles POSIX process group SIGTERM', t => runPosixGroupKillTest(t)); |
150 | 270 | }); |
0 commit comments