From 0a4b1329a6d63660fe5c6d6207f43f4bee387bdc Mon Sep 17 00:00:00 2001 From: Ryan Cote Date: Wed, 17 Dec 2025 02:50:50 +0000 Subject: [PATCH 1/2] Fix Cypress Chainlit shutdown and entrypoint checks --- cypress.config.ts | 52 ++++++++++++++------------ cypress/support/run.ts | 83 +++++++++++++++++++++++++++++------------- 2 files changed, 86 insertions(+), 49 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 87e26ec521..cbe64083c9 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,25 +1,36 @@ import { defineConfig } from 'cypress'; import fkill from 'fkill'; -import { runChainlit } from './cypress/support/run'; +import { runChainlit, stopChainlit } from './cypress/support/run'; export const CHAINLIT_APP_PORT = 8000; +const signals = ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGBREAK'] as const; +type ExitSignal = (typeof signals)[number]; + +const signalMap: Record = { + SIGTERM: 15, + SIGINT: 2, + SIGHUP: 1, + SIGBREAK: 21 +}; + async function killChainlit() { - await fkill(`:${CHAINLIT_APP_PORT}`, { - force: true, - silent: true - }); -} + const stoppedTracked = await stopChainlit(); -['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGBREAK'].forEach((signal) => { - process.on(signal, () => { - (async () => { - await killChainlit(); // Ensure Chainlit is killed on exit + if (!stoppedTracked) { + try { + await fkill(`:${CHAINLIT_APP_PORT}`, { force: true, silent: true }); + } catch { + // best-effort cleanup only + } + } +} - const signalMap = { SIGTERM: 15, SIGINT: 2, SIGHUP: 1, SIGBREAK: 21 }; - process.exit(128 + (signalMap[signal] || 0)); - })(); +signals.forEach((signal) => { + process.on(signal, async () => { + await killChainlit(); // Ensure Chainlit is killed on exit + process.exit(128 + signalMap[signal]); }); }); @@ -36,7 +47,7 @@ export default defineConfig({ experimentalInteractiveRunEvents: true, async setupNodeEvents(on, config) { await killChainlit(); // Fallback to ensure no previous instance is running - await runChainlit(); // Start Chainlit before running tests as Cypress require + await runChainlit(); // Start Chainlit before running tests as Cypress requires on('before:spec', async (spec) => { await killChainlit(); @@ -57,15 +68,10 @@ export default defineConfig({ return null; }, restartChainlit(spec: Cypress.Spec) { - return new Promise((resolve) => { - killChainlit().then(() => { - runChainlit(spec).then(() => { - setTimeout(() => { - resolve(null); - }, 1000); - }); - }); - }); + return killChainlit() + .then(() => runChainlit(spec)) + .then(() => new Promise((r) => setTimeout(r, 1000))) + .then(() => null); } }); diff --git a/cypress/support/run.ts b/cypress/support/run.ts index d05bea079b..e20c38b231 100644 --- a/cypress/support/run.ts +++ b/cypress/support/run.ts @@ -1,10 +1,38 @@ import { - ChildProcessWithoutNullStreams, - SpawnOptionsWithoutStdio, + type ChildProcessWithoutNullStreams, + type SpawnOptionsWithoutStdio, spawn -} from 'child_process'; -import { access } from 'fs/promises'; -import { dirname, join } from 'path'; +} from 'node:child_process'; +import { access } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +let currentChainlit: ChildProcessWithoutNullStreams | null = null; + +export async function stopChainlit(): Promise { + const proc = currentChainlit; + if (!proc?.pid) return false; + + const pid = proc.pid; + const killTarget = process.platform === 'win32' ? pid : -pid; + + // Kill the entire process group (requires detached: true when spawned) + try { + process.kill(killTarget, 'SIGTERM'); + } catch { + // ignore + } + + await new Promise((r) => setTimeout(r, 1500)); + + try { + process.kill(killTarget, 'SIGKILL'); + } catch { + // ignore + } + + currentChainlit = null; + return true; +} export const runChainlit = async ( spec: Cypress.Spec | null = null @@ -12,24 +40,24 @@ export const runChainlit = async ( const CHAILIT_DIR = join(process.cwd(), 'backend', 'chainlit'); const SAMPLE_DIR = join(CHAILIT_DIR, 'sample'); - return new Promise((resolve, reject) => { - const testDir = spec ? dirname(spec.absolute) : SAMPLE_DIR; - const entryPointFileName = spec - ? spec.name.startsWith('async') - ? 'main_async.py' - : spec.name.startsWith('sync') - ? 'main_sync.py' - : 'main.py' - : 'hello.py'; - - const entryPointPath = join(testDir, entryPointFileName); - - if (!access(entryPointPath)) { - return reject( - new Error(`Entry point file does not exist: ${entryPointPath}`) - ); - } + const testDir = spec ? dirname(spec.absolute) : SAMPLE_DIR; + const entryPointFileName = spec + ? spec.name.startsWith('async') + ? 'main_async.py' + : spec.name.startsWith('sync') + ? 'main_sync.py' + : 'main.py' + : 'hello.py'; + + const entryPointPath = join(testDir, entryPointFileName); + + try { + await access(entryPointPath); + } catch { + throw new Error(`Entry point file does not exist: ${entryPointPath}`); + } + return new Promise((resolve, reject) => { const command = 'uv'; const args = [ @@ -47,11 +75,14 @@ export const runChainlit = async ( env: { ...process.env, CHAINLIT_APP_ROOT: testDir - } + }, + detached: true }; const chainlit = spawn(command, args, options); + currentChainlit = chainlit; + chainlit.stdout.on('data', (data) => { const output = data.toString(); if (output.includes('Your app is available at')) { @@ -64,11 +95,11 @@ export const runChainlit = async ( }); chainlit.on('error', (error) => { - reject(error.message); + reject(error); }); - chainlit.on('exit', function (code) { - reject('Chainlit process exited with code ' + code); + chainlit.on('exit', (code) => { + reject(new Error('Chainlit process exited with code ' + code)); }); }); }; From 9ef23db39b217723ccde0aa9961a7234f0f7cc78 Mon Sep 17 00:00:00 2001 From: Ryan Cote Date: Wed, 17 Dec 2025 05:40:00 +0000 Subject: [PATCH 2/2] Fix Chainlit process cleanup on Windows --- cypress/support/run.ts | 55 +++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/cypress/support/run.ts b/cypress/support/run.ts index e20c38b231..9d9e3385dc 100644 --- a/cypress/support/run.ts +++ b/cypress/support/run.ts @@ -8,29 +8,56 @@ import { dirname, join } from 'node:path'; let currentChainlit: ChildProcessWithoutNullStreams | null = null; +/** + * Kill a process tree on Windows using taskkill /T /F + */ +async function taskkillTree(pid: number): Promise { + await new Promise((resolve) => { + const p = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { + windowsHide: true, + stdio: 'ignore' + }); + + p.on('exit', () => resolve()); + p.on('error', () => resolve()); // best-effort + }); + + // Give Windows time to release the port + await sleep(750); +} + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + export async function stopChainlit(): Promise { const proc = currentChainlit; if (!proc?.pid) return false; const pid = proc.pid; - const killTarget = process.platform === 'win32' ? pid : -pid; - // Kill the entire process group (requires detached: true when spawned) - try { - process.kill(killTarget, 'SIGTERM'); - } catch { - // ignore - } - - await new Promise((r) => setTimeout(r, 1500)); - - try { - process.kill(killTarget, 'SIGKILL'); - } catch { - // ignore + if (process.platform === 'win32') { + // Windows: kill entire process tree + await taskkillTree(pid); + } else { + // POSIX: kill process group (requires detached: true) + try { + process.kill(-pid, 'SIGTERM'); + } catch { + // ignore + } + + await sleep(1500); + + try { + process.kill(-pid, 'SIGKILL'); + } catch { + // ignore + } } currentChainlit = null; + return true; }