From 887834eaa0fc272a60437aace9a46d07332f0fc0 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 11:23:06 +0100 Subject: [PATCH 01/12] fix: improve orphaned process cleanup to handle child processes safely - Add port-based cleanup for LSP (2087) and Jupyter (8888) servers - Add isDeepnoteRelatedProcess() to verify processes are from deepnote-venvs - Expand process detection to include pylsp and jupyter child processes - Prevent killing external Jupyter/LSP servers not managed by extension - Fix 'Address already in use' errors from orphaned LSP servers The cleanup now uses a two-stage safety check: 1. Verify process is deepnote-related (from deepnote-venvs directory) 2. Verify process is orphaned (parent no longer exists) This prevents interference with user's manual Jupyter servers or other tools while still cleaning up stuck processes from crashed VSCode sessions. --- .../deepnote/deepnoteServerStarter.node.ts | 151 ++++++++++++++++-- 1 file changed, 140 insertions(+), 11 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index 53c892cb92..d7c39bb188 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -589,6 +589,118 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension return false; } + /** + * Check if a process is a deepnote-toolkit related process by examining its command line. + */ + private async isDeepnoteRelatedProcess(pid: number): Promise { + try { + const processService = await this.processServiceFactory.create(undefined); + + if (process.platform === 'win32') { + // Windows: use wmic to get command line + const result = await processService.exec( + 'wmic', + ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine'], + { throwOnStdErr: false } + ); + if (result.stdout) { + const cmdLine = result.stdout.toLowerCase(); + // Check if it's running from our deepnote-venvs directory or is deepnote_toolkit + return cmdLine.includes('deepnote-venvs') || cmdLine.includes('deepnote_toolkit'); + } + } else { + // Unix-like: use ps to get command line + const result = await processService.exec('ps', ['-p', pid.toString(), '-o', 'command='], { + throwOnStdErr: false + }); + if (result.stdout) { + const cmdLine = result.stdout.toLowerCase(); + // Check if it's running from our deepnote-venvs directory or is deepnote_toolkit + return cmdLine.includes('deepnote-venvs') || cmdLine.includes('deepnote_toolkit'); + } + } + } catch (ex) { + logger.debug(`Failed to check if process ${pid} is deepnote-related: ${ex}`); + } + return false; + } + + /** + * Find and kill orphaned deepnote-toolkit processes using specific ports. + * This is useful for cleaning up LSP servers and Jupyter servers that may be stuck. + * Only kills processes that are both orphaned AND deepnote-related. + */ + private async cleanupProcessesByPort(port: number): Promise { + try { + const processService = await this.processServiceFactory.create(undefined); + + if (process.platform === 'win32') { + // Windows: use netstat to find process using port + const result = await processService.exec('netstat', ['-ano'], { throwOnStdErr: false }); + if (result.stdout) { + const lines = result.stdout.split('\n'); + for (const line of lines) { + if (line.includes(`:${port}`) && line.includes('LISTENING')) { + const parts = line.trim().split(/\s+/); + const pid = parseInt(parts[parts.length - 1], 10); + if (!isNaN(pid) && pid > 0) { + // Check if it's deepnote-related first + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); + if (!isDeepnoteRelated) { + logger.debug(`Process ${pid} on port ${port} is not deepnote-related, skipping`); + continue; + } + + const isOrphaned = await this.isProcessOrphaned(pid); + if (isOrphaned) { + logger.info( + `Found orphaned deepnote-related process ${pid} using port ${port}, killing...` + ); + await processService.exec('taskkill', ['/F', '/PID', pid.toString()], { + throwOnStdErr: false + }); + } + } + } + } + } + } else { + // Unix-like: use lsof to find process using port + const result = await processService.exec('lsof', ['-i', `:${port}`, '-t'], { throwOnStdErr: false }); + if (result.stdout) { + const pids = result.stdout + .trim() + .split('\n') + .map((p) => parseInt(p.trim(), 10)) + .filter((p) => !isNaN(p) && p > 0); + + for (const pid of pids) { + // Check if it's deepnote-related first + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); + if (!isDeepnoteRelated) { + logger.debug(`Process ${pid} on port ${port} is not deepnote-related, skipping`); + continue; + } + + const isOrphaned = await this.isProcessOrphaned(pid); + if (isOrphaned) { + logger.info( + `Found orphaned deepnote-related process ${pid} using port ${port}, killing...` + ); + await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); + } else { + logger.info( + `Deepnote-related process ${pid} using port ${port} has active parent, skipping` + ); + } + } + } + } + } catch (ex) { + logger.debug(`Failed to cleanup processes on port ${port}: ${ex}`); + } + } + /** * Cleans up any orphaned deepnote-toolkit processes from previous VS Code sessions. * This prevents port conflicts when starting new servers. @@ -596,9 +708,15 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension private async cleanupOrphanedProcesses(): Promise { try { logger.info('Checking for orphaned deepnote-toolkit processes...'); + + // First, clean up any orphaned processes using known ports + // This catches LSP servers (2087) and Jupyter servers (8888+) that may be stuck + await this.cleanupProcessesByPort(2087); // Python LSP server + await this.cleanupProcessesByPort(8888); // Default Jupyter port + const processService = await this.processServiceFactory.create(undefined); - // Find all deepnote-toolkit server processes + // Find all deepnote-toolkit server processes and related child processes let command: string; let args: string[]; @@ -619,8 +737,17 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const candidatePids: number[] = []; for (const line of lines) { - // Look for processes running deepnote_toolkit server - if (line.includes('deepnote_toolkit') && line.includes('server')) { + // Look for processes running deepnote_toolkit server or related child processes + // This includes: + // - deepnote_toolkit server (main server process) + // - pylsp (Python LSP server child process) + // - jupyter (Jupyter server child process) + const isDeepnoteRelated = + (line.includes('deepnote_toolkit') && line.includes('server')) || + (line.includes('pylsp') && line.includes('2087')) || // LSP server on port 2087 + (line.includes('jupyter') && line.includes('deepnote')); + + if (isDeepnoteRelated) { // Extract PID based on platform let pid: number | undefined; @@ -646,7 +773,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension if (candidatePids.length > 0) { logger.info( - `Found ${candidatePids.length} deepnote-toolkit server process(es): ${candidatePids.join(', ')}` + `Found ${candidatePids.length} deepnote-related process(es): ${candidatePids.join(', ')}` ); const pidsToKill: number[] = []; @@ -654,11 +781,12 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // Check each process to determine if it should be killed for (const pid of candidatePids) { - // Check if there's a lock file for this PID + // Check if there's a lock file for this PID (only main server processes have lock files) const lockData = await this.readLockFile(pid); if (lockData) { - // Lock file exists - check if it belongs to a different session + // Lock file exists - this is a main server process + // Check if it belongs to a different session if (lockData.sessionId !== this.sessionId) { // Different session - check if the process is actually orphaned const isOrphaned = await this.isProcessOrphaned(pid); @@ -678,7 +806,8 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension pidsToSkip.push({ pid, reason: 'belongs to current session' }); } } else { - // No lock file - check if orphaned before killing + // No lock file - could be a child process (LSP, Jupyter) or orphaned main process + // Check if orphaned before killing const isOrphaned = await this.isProcessOrphaned(pid); if (isOrphaned) { logger.info(`PID ${pid} has no lock file and is orphaned - will kill`); @@ -700,7 +829,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension if (pidsToKill.length > 0) { logger.info(`Killing ${pidsToKill.length} orphaned process(es): ${pidsToKill.join(', ')}`); this.outputChannel.appendLine( - `Cleaning up ${pidsToKill.length} orphaned deepnote-toolkit process(es)...` + `Cleaning up ${pidsToKill.length} orphaned deepnote-related process(es)...` ); for (const pid of pidsToKill) { @@ -714,7 +843,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension } logger.info(`Killed orphaned process ${pid}`); - // Clean up the lock file after killing + // Clean up the lock file after killing (if it exists) await this.deleteLockFile(pid); } catch (ex) { logger.warn(`Failed to kill process ${pid}: ${ex}`); @@ -723,10 +852,10 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension this.outputChannel.appendLine('✓ Cleanup complete'); } else { - logger.info('No orphaned deepnote-toolkit processes found (all processes are active)'); + logger.info('No orphaned deepnote-related processes found (all processes are active)'); } } else { - logger.info('No deepnote-toolkit server processes found'); + logger.info('No deepnote-related processes found'); } } } catch (ex) { From cbcbca80c2d3a6c7de3e14e1716abee5da4b777c Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 11:36:25 +0100 Subject: [PATCH 02/12] refactor: improve process detection robustness - Use PowerShell Get-CimInstance on Windows (WMIC is deprecated) - Add WMIC fallback for older Windows systems - Add -ww flag to ps on Unix to prevent command line truncation - Use regex path matching for deepnote-venvs detection - Prevent false positives with proper path boundary matching --- .../deepnote/deepnoteServerStarter.node.ts | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index d7c39bb188..d0eee6430d 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -597,26 +597,45 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const processService = await this.processServiceFactory.create(undefined); if (process.platform === 'win32') { - // Windows: use wmic to get command line - const result = await processService.exec( - 'wmic', - ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine'], - { throwOnStdErr: false } - ); - if (result.stdout) { - const cmdLine = result.stdout.toLowerCase(); - // Check if it's running from our deepnote-venvs directory or is deepnote_toolkit - return cmdLine.includes('deepnote-venvs') || cmdLine.includes('deepnote_toolkit'); + // Windows: prefer PowerShell CIM, fallback to WMIC + let cmdLine = ''; + try { + const ps = await processService.exec( + 'powershell.exe', + [ + '-NoProfile', + '-Command', + `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine` + ], + { throwOnStdErr: false } + ); + cmdLine = (ps.stdout || '').toLowerCase(); + } catch { + // Ignore PowerShell errors, will fallback to WMIC + } + if (!cmdLine) { + const result = await processService.exec( + 'wmic', + ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine'], + { throwOnStdErr: false } + ); + cmdLine = (result.stdout || '').toLowerCase(); + } + if (cmdLine) { + // Use regex to match path separators for more robust detection + const inVenv = /[\\/](deepnote-venvs)[\\/]/i.test(cmdLine); + return inVenv || cmdLine.includes('deepnote_toolkit'); } } else { - // Unix-like: use ps to get command line - const result = await processService.exec('ps', ['-p', pid.toString(), '-o', 'command='], { + // Unix-like: use ps with -ww to avoid truncation of long command lines + const result = await processService.exec('ps', ['-ww', '-p', pid.toString(), '-o', 'command='], { throwOnStdErr: false }); if (result.stdout) { const cmdLine = result.stdout.toLowerCase(); - // Check if it's running from our deepnote-venvs directory or is deepnote_toolkit - return cmdLine.includes('deepnote-venvs') || cmdLine.includes('deepnote_toolkit'); + // Use regex to match path separators for more robust detection + const inVenv = /[\\/](deepnote-venvs)[\\/]/i.test(cmdLine); + return inVenv || cmdLine.includes('deepnote_toolkit'); } } } catch (ex) { From a977208ffa0b4e3dafe7674546771f604449b4be Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 11:38:53 +0100 Subject: [PATCH 03/12] refactor: improve port cleanup with graceful termination and fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isProcessAlive() helper to check process status cross-platform - Implement graceful kill on Unix (SIGTERM → SIGKILL escalation) - Implement graceful kill on Windows (taskkill → taskkill /F escalation) - Deduplicate PIDs before processing to avoid double-kills - Add ss fallback when lsof is unavailable on Unix - Filter for LISTEN sockets only using lsof -sTCP:LISTEN - Wait 1 second after SIGTERM before escalating to SIGKILL - Improve error handling and logging throughout cleanup process --- .../deepnote/deepnoteServerStarter.node.ts | 179 ++++++++++++++---- 1 file changed, 146 insertions(+), 33 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index d0eee6430d..60f4e26466 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -644,6 +644,60 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension return false; } + /** + * Check if a process is still alive. + */ + private async isProcessAlive(pid: number): Promise { + try { + const processService = await this.processServiceFactory.create(undefined); + if (process.platform === 'win32') { + const result = await processService.exec('tasklist', ['/FI', `PID eq ${pid}`, '/NH'], { + throwOnStdErr: false + }); + return result.stdout.includes(pid.toString()); + } else { + // Use kill -0 to check if process exists (doesn't actually kill) + // If it succeeds, process exists; if it fails, process doesn't exist + try { + await processService.exec('kill', ['-0', pid.toString()], { throwOnStdErr: false }); + return true; + } catch { + return false; + } + } + } catch { + return false; + } + } + + /** + * Attempt graceful kill (SIGTERM) then escalate to SIGKILL if needed. + */ + private async killProcessGracefully( + pid: number, + processService: import('../../platform/common/process/types.node').IProcessService + ): Promise { + try { + // Try graceful termination first (SIGTERM) + logger.debug(`Attempting graceful termination of process ${pid} (SIGTERM)...`); + await processService.exec('kill', [pid.toString()], { throwOnStdErr: false }); + + // Wait a bit for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Check if still alive + const stillAlive = await this.isProcessAlive(pid); + if (stillAlive) { + logger.debug(`Process ${pid} did not terminate gracefully, escalating to SIGKILL...`); + await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); + } else { + logger.debug(`Process ${pid} terminated gracefully`); + } + } catch (ex) { + logger.debug(`Error during graceful kill of process ${pid}: ${ex}`); + } + } + /** * Find and kill orphaned deepnote-toolkit processes using specific ports. * This is useful for cleaning up LSP servers and Jupyter servers that may be stuck. @@ -654,46 +708,25 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const processService = await this.processServiceFactory.create(undefined); if (process.platform === 'win32') { - // Windows: use netstat to find process using port + // Windows: use netstat to find LISTENING processes on port const result = await processService.exec('netstat', ['-ano'], { throwOnStdErr: false }); if (result.stdout) { const lines = result.stdout.split('\n'); + const uniquePids = new Set(); + + // Parse and deduplicate PIDs for (const line of lines) { if (line.includes(`:${port}`) && line.includes('LISTENING')) { const parts = line.trim().split(/\s+/); const pid = parseInt(parts[parts.length - 1], 10); if (!isNaN(pid) && pid > 0) { - // Check if it's deepnote-related first - const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); - if (!isDeepnoteRelated) { - logger.debug(`Process ${pid} on port ${port} is not deepnote-related, skipping`); - continue; - } - - const isOrphaned = await this.isProcessOrphaned(pid); - if (isOrphaned) { - logger.info( - `Found orphaned deepnote-related process ${pid} using port ${port}, killing...` - ); - await processService.exec('taskkill', ['/F', '/PID', pid.toString()], { - throwOnStdErr: false - }); - } + uniquePids.add(pid); } } } - } - } else { - // Unix-like: use lsof to find process using port - const result = await processService.exec('lsof', ['-i', `:${port}`, '-t'], { throwOnStdErr: false }); - if (result.stdout) { - const pids = result.stdout - .trim() - .split('\n') - .map((p) => parseInt(p.trim(), 10)) - .filter((p) => !isNaN(p) && p > 0); - for (const pid of pids) { + // Process each unique PID + for (const pid of uniquePids) { // Check if it's deepnote-related first const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); if (!isDeepnoteRelated) { @@ -704,16 +737,96 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const isOrphaned = await this.isProcessOrphaned(pid); if (isOrphaned) { logger.info( - `Found orphaned deepnote-related process ${pid} using port ${port}, killing...` + `Found orphaned deepnote-related process ${pid} using port ${port}, killing process tree...` ); - await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); + // Try without /F first (graceful) + try { + await processService.exec('taskkill', ['/T', '/PID', pid.toString()], { + throwOnStdErr: false + }); + logger.debug(`Gracefully killed process ${pid}`); + } catch (gracefulError) { + // If graceful kill failed, use /F (force) + logger.debug(`Graceful kill failed for ${pid}, using /F flag...`); + try { + await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { + throwOnStdErr: false + }); + } catch (forceError) { + logger.debug(`Force kill also failed for ${pid}: ${forceError}`); + } + } } else { - logger.info( - `Deepnote-related process ${pid} using port ${port} has active parent, skipping` - ); + logger.debug(`Deepnote-related process ${pid} on port ${port} has active parent, skipping`); } } } + } else { + // Unix-like: try lsof first, fallback to ss + let uniquePids = new Set(); + + // Try lsof with LISTEN filter + try { + const lsofResult = await processService.exec('lsof', ['-sTCP:LISTEN', '-i', `:${port}`, '-t'], { + throwOnStdErr: false + }); + if (lsofResult.stdout) { + const pids = lsofResult.stdout + .trim() + .split('\n') + .map((p) => parseInt(p.trim(), 10)) + .filter((p) => !isNaN(p) && p > 0); + pids.forEach((pid) => uniquePids.add(pid)); + } + } catch (lsofError) { + logger.debug(`lsof failed or unavailable, trying ss: ${lsofError}`); + } + + // Fallback to ss if lsof didn't find anything or failed + if (uniquePids.size === 0) { + try { + const ssResult = await processService.exec('ss', ['-tlnp', `sport = :${port}`], { + throwOnStdErr: false + }); + if (ssResult.stdout) { + // Parse ss output: look for pid= + const pidMatches = ssResult.stdout.matchAll(/pid=(\d+)/g); + for (const match of pidMatches) { + const pid = parseInt(match[1], 10); + if (!isNaN(pid) && pid > 0) { + uniquePids.add(pid); + } + } + } + } catch (ssError) { + logger.debug(`ss also failed: ${ssError}`); + } + } + + if (uniquePids.size === 0) { + logger.debug(`No processes found listening on port ${port}`); + return; + } + + // Process each unique PID + for (const pid of uniquePids) { + // Check if it's deepnote-related first + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); + if (!isDeepnoteRelated) { + logger.debug(`Process ${pid} on port ${port} is not deepnote-related, skipping`); + continue; + } + + const isOrphaned = await this.isProcessOrphaned(pid); + if (isOrphaned) { + logger.info( + `Found orphaned deepnote-related process ${pid} using port ${port}, attempting graceful kill...` + ); + await this.killProcessGracefully(pid, processService); + } else { + logger.debug(`Deepnote-related process ${pid} on port ${port} has active parent, skipping`); + } + } } } catch (ex) { logger.debug(`Failed to cleanup processes on port ${port}: ${ex}`); From 0b22d4d842c02584265a1a2d6eeb873f43023984 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 11:43:06 +0100 Subject: [PATCH 04/12] refactor: scan Jupyter port range (8888-8895) during cleanup - Change from single port 8888 to range 8888-8895 - Jupyter increments port when 8888 is busy, so we need to check multiple ports - Add timing measurement for port-based cleanup phase - Log cleanup duration for performance monitoring The loop adds minimal overhead as each port check is fast when no process is listening. Timing is logged at debug level for monitoring. --- src/kernels/deepnote/deepnoteServerStarter.node.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index 60f4e26466..ffd73f8263 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -839,12 +839,21 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension */ private async cleanupOrphanedProcesses(): Promise { try { + const startTime = Date.now(); logger.info('Checking for orphaned deepnote-toolkit processes...'); // First, clean up any orphaned processes using known ports - // This catches LSP servers (2087) and Jupyter servers (8888+) that may be stuck + // This catches LSP servers (2087) and Jupyter servers (8888-8895) that may be stuck await this.cleanupProcessesByPort(2087); // Python LSP server - await this.cleanupProcessesByPort(8888); // Default Jupyter port + + // Scan common Jupyter port range (8888-8895) + // Jupyter typically uses 8888 but will increment if that port is busy + for (let port = 8888; port <= 8895; port++) { + await this.cleanupProcessesByPort(port); + } + + const portCleanupTime = Date.now() - startTime; + logger.debug(`Port-based cleanup completed in ${portCleanupTime}ms`); const processService = await this.processServiceFactory.create(undefined); From 38f3a8ef3f3701f07406e802c3393b072e834487 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 11:47:42 +0100 Subject: [PATCH 05/12] fix: Windows process detection now resolves command lines properly - Windows tasklist CSV output never includes command lines - Changed to two-step approach: collect all python.exe/pythonw.exe PIDs first - Then resolve each PID's command line using isDeepnoteRelatedProcess() - Only verified deepnote-related processes are added to candidate list - Kill logic remains gated by isDeepnoteRelatedProcess + orphan checks - Fixes false negatives where deepnote processes were missed on Windows --- .../deepnote/deepnoteServerStarter.node.ts | 250 ++++++++++-------- 1 file changed, 142 insertions(+), 108 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index ffd73f8263..8b61ec44be 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -858,146 +858,180 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const processService = await this.processServiceFactory.create(undefined); // Find all deepnote-toolkit server processes and related child processes - let command: string; - let args: string[]; + const candidatePids: number[] = []; if (process.platform === 'win32') { - // Windows: use tasklist and findstr - command = 'tasklist'; - args = ['/FI', 'IMAGENAME eq python.exe', '/FO', 'CSV', '/NH']; - } else { - // Unix-like: use ps and grep - command = 'ps'; - args = ['aux']; - } + // Windows: tasklist CSV doesn't include command lines, so we need a two-step approach: + // 1. Get all python.exe and pythonw.exe PIDs + // 2. Check each PID's command line to see if it's deepnote-related - const result = await processService.exec(command, args, { throwOnStdErr: false }); + // Step 1: Get all Python process PIDs + const pythonPids: number[] = []; - if (result.stdout) { - const lines = result.stdout.split('\n'); - const candidatePids: number[] = []; + // Check python.exe + const pythonResult = await processService.exec( + 'tasklist', + ['/FI', 'IMAGENAME eq python.exe', '/FO', 'CSV', '/NH'], + { throwOnStdErr: false } + ); + if (pythonResult.stdout) { + const lines = pythonResult.stdout.split('\n'); + for (const line of lines) { + // Windows CSV format: "python.exe","12345",... + const match = line.match(/"python\.exe","(\d+)"/); + if (match) { + const pid = parseInt(match[1], 10); + if (!isNaN(pid)) { + pythonPids.push(pid); + } + } + } + } + + // Check pythonw.exe + const pythonwResult = await processService.exec( + 'tasklist', + ['/FI', 'IMAGENAME eq pythonw.exe', '/FO', 'CSV', '/NH'], + { throwOnStdErr: false } + ); + if (pythonwResult.stdout) { + const lines = pythonwResult.stdout.split('\n'); + for (const line of lines) { + // Windows CSV format: "pythonw.exe","12345",... + const match = line.match(/"pythonw\.exe","(\d+)"/); + if (match) { + const pid = parseInt(match[1], 10); + if (!isNaN(pid)) { + pythonPids.push(pid); + } + } + } + } - for (const line of lines) { - // Look for processes running deepnote_toolkit server or related child processes - // This includes: - // - deepnote_toolkit server (main server process) - // - pylsp (Python LSP server child process) - // - jupyter (Jupyter server child process) - const isDeepnoteRelated = - (line.includes('deepnote_toolkit') && line.includes('server')) || - (line.includes('pylsp') && line.includes('2087')) || // LSP server on port 2087 - (line.includes('jupyter') && line.includes('deepnote')); + logger.debug(`Found ${pythonPids.length} Python process(es) on Windows`); + // Step 2: Check each Python PID to see if it's deepnote-related + for (const pid of pythonPids) { + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); if (isDeepnoteRelated) { - // Extract PID based on platform - let pid: number | undefined; - - if (process.platform === 'win32') { - // Windows CSV format: "python.exe","12345",... - const match = line.match(/"python\.exe","(\d+)"/); - if (match) { - pid = parseInt(match[1], 10); - } - } else { + candidatePids.push(pid); + } + } + } else { + // Unix-like: use ps with full command line + const result = await processService.exec('ps', ['aux'], { throwOnStdErr: false }); + + if (result.stdout) { + const lines = result.stdout.split('\n'); + + for (const line of lines) { + // Look for processes running deepnote_toolkit server or related child processes + // This includes: + // - deepnote_toolkit server (main server process) + // - pylsp (Python LSP server child process) + // - jupyter (Jupyter server child process) + const isDeepnoteRelated = + (line.includes('deepnote_toolkit') && line.includes('server')) || + (line.includes('pylsp') && line.includes('2087')) || // LSP server on port 2087 + (line.includes('jupyter') && line.includes('deepnote')); + + if (isDeepnoteRelated) { // Unix format: user PID ... const parts = line.trim().split(/\s+/); if (parts.length > 1) { - pid = parseInt(parts[1], 10); + const pid = parseInt(parts[1], 10); + if (!isNaN(pid)) { + candidatePids.push(pid); + } } } - - if (pid && !isNaN(pid)) { - candidatePids.push(pid); - } } } + } - if (candidatePids.length > 0) { - logger.info( - `Found ${candidatePids.length} deepnote-related process(es): ${candidatePids.join(', ')}` - ); + if (candidatePids.length > 0) { + logger.info(`Found ${candidatePids.length} deepnote-related process(es): ${candidatePids.join(', ')}`); - const pidsToKill: number[] = []; - const pidsToSkip: Array<{ pid: number; reason: string }> = []; - - // Check each process to determine if it should be killed - for (const pid of candidatePids) { - // Check if there's a lock file for this PID (only main server processes have lock files) - const lockData = await this.readLockFile(pid); - - if (lockData) { - // Lock file exists - this is a main server process - // Check if it belongs to a different session - if (lockData.sessionId !== this.sessionId) { - // Different session - check if the process is actually orphaned - const isOrphaned = await this.isProcessOrphaned(pid); - if (isOrphaned) { - logger.info( - `PID ${pid} belongs to session ${lockData.sessionId} and is orphaned - will kill` - ); - pidsToKill.push(pid); - } else { - pidsToSkip.push({ - pid, - reason: `belongs to active session ${lockData.sessionId.substring(0, 8)}...` - }); - } - } else { - // Same session - this shouldn't happen during startup, but skip it - pidsToSkip.push({ pid, reason: 'belongs to current session' }); - } - } else { - // No lock file - could be a child process (LSP, Jupyter) or orphaned main process - // Check if orphaned before killing + const pidsToKill: number[] = []; + const pidsToSkip: Array<{ pid: number; reason: string }> = []; + + // Check each process to determine if it should be killed + for (const pid of candidatePids) { + // Check if there's a lock file for this PID (only main server processes have lock files) + const lockData = await this.readLockFile(pid); + + if (lockData) { + // Lock file exists - this is a main server process + // Check if it belongs to a different session + if (lockData.sessionId !== this.sessionId) { + // Different session - check if the process is actually orphaned const isOrphaned = await this.isProcessOrphaned(pid); if (isOrphaned) { - logger.info(`PID ${pid} has no lock file and is orphaned - will kill`); + logger.info( + `PID ${pid} belongs to session ${lockData.sessionId} and is orphaned - will kill` + ); pidsToKill.push(pid); } else { - pidsToSkip.push({ pid, reason: 'no lock file but has active parent process' }); + pidsToSkip.push({ + pid, + reason: `belongs to active session ${lockData.sessionId.substring(0, 8)}...` + }); } + } else { + // Same session - this shouldn't happen during startup, but skip it + pidsToSkip.push({ pid, reason: 'belongs to current session' }); } - } - - // Log skipped processes - if (pidsToSkip.length > 0) { - for (const { pid, reason } of pidsToSkip) { - logger.info(`Skipping PID ${pid}: ${reason}`); + } else { + // No lock file - could be a child process (LSP, Jupyter) or orphaned main process + // Check if orphaned before killing + const isOrphaned = await this.isProcessOrphaned(pid); + if (isOrphaned) { + logger.info(`PID ${pid} has no lock file and is orphaned - will kill`); + pidsToKill.push(pid); + } else { + pidsToSkip.push({ pid, reason: 'no lock file but has active parent process' }); } } + } - // Kill orphaned processes - if (pidsToKill.length > 0) { - logger.info(`Killing ${pidsToKill.length} orphaned process(es): ${pidsToKill.join(', ')}`); - this.outputChannel.appendLine( - `Cleaning up ${pidsToKill.length} orphaned deepnote-related process(es)...` - ); + // Log skipped processes + if (pidsToSkip.length > 0) { + for (const { pid, reason } of pidsToSkip) { + logger.info(`Skipping PID ${pid}: ${reason}`); + } + } - for (const pid of pidsToKill) { - try { - if (process.platform === 'win32') { - await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { - throwOnStdErr: false - }); - } else { - await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); - } - logger.info(`Killed orphaned process ${pid}`); + // Kill orphaned processes + if (pidsToKill.length > 0) { + logger.info(`Killing ${pidsToKill.length} orphaned process(es): ${pidsToKill.join(', ')}`); + this.outputChannel.appendLine( + `Cleaning up ${pidsToKill.length} orphaned deepnote-related process(es)...` + ); - // Clean up the lock file after killing (if it exists) - await this.deleteLockFile(pid); - } catch (ex) { - logger.warn(`Failed to kill process ${pid}: ${ex}`); + for (const pid of pidsToKill) { + try { + if (process.platform === 'win32') { + await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { + throwOnStdErr: false + }); + } else { + await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); } - } + logger.info(`Killed orphaned process ${pid}`); - this.outputChannel.appendLine('✓ Cleanup complete'); - } else { - logger.info('No orphaned deepnote-related processes found (all processes are active)'); + // Clean up the lock file after killing (if it exists) + await this.deleteLockFile(pid); + } catch (ex) { + logger.warn(`Failed to kill process ${pid}: ${ex}`); + } } + + this.outputChannel.appendLine('✓ Cleanup complete'); } else { - logger.info('No deepnote-related processes found'); + logger.info('No orphaned deepnote-related processes found (all processes are active)'); } + } else { + logger.info('No deepnote-related processes found'); } } catch (ex) { // Don't fail startup if cleanup fails From 375326c45ec63ff03b303028652903485ade9a6c Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:22:40 +0100 Subject: [PATCH 06/12] fix: improve Windows process liveness check with CSV parsing - Windows tasklist substring match was brittle and unreliable - Changed to use /FO CSV option for structured output - Parse CSV rows to extract PID from second column - Use exact numeric equality comparison instead of substring match - Skip INFO: messages and empty lines in output - Handle parse errors safely with try-catch per line - Return false on any failure while preserving throwOnStdErr: false - More robust and accurate process existence detection on Windows --- .../deepnote/deepnoteServerStarter.node.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index 8b61ec44be..4e79117ea2 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -651,10 +651,36 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension try { const processService = await this.processServiceFactory.create(undefined); if (process.platform === 'win32') { - const result = await processService.exec('tasklist', ['/FI', `PID eq ${pid}`, '/NH'], { + // Use CSV format for reliable parsing + const result = await processService.exec('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], { throwOnStdErr: false }); - return result.stdout.includes(pid.toString()); + + // Parse CSV output to find matching PID + const lines = result.stdout.split('\n'); + for (const line of lines) { + // Skip INFO: messages and empty lines + if (line.trim().startsWith('INFO:') || line.trim() === '') { + continue; + } + + try { + // CSV format: "ImageName","PID","SessionName","Session#","MemUsage" + // Example: "python.exe","12345","Console","1","50,000 K" + // PID is in the second column (index 1) + const match = line.match(/"[^"]*","(\d+)"/); + if (match) { + const linePid = parseInt(match[1], 10); + if (!isNaN(linePid) && linePid === pid) { + return true; + } + } + } catch { + // Ignore parse errors for individual lines + continue; + } + } + return false; } else { // Use kill -0 to check if process exists (doesn't actually kill) // If it succeeds, process exists; if it fails, process doesn't exist From 2345dfc7c55575dc41ef9f5957acdb8befca6fa6 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:24:49 +0100 Subject: [PATCH 07/12] perf: add -nP flag to lsof and cleanup lock files after port-based kills - Add -nP flag to lsof invocations to avoid slow DNS/service lookups - Clean up lock files after successful process kills in port cleanup logic - Windows: cleanup lock files after both graceful and force kills - Unix: cleanup lock files after graceful kill completes - Uses existing deleteLockFile() helper which safely checks existence - Prevents stale lock files from accumulating after cleanup operations - Improves performance by avoiding unnecessary DNS reverse lookups --- .../deepnote/deepnoteServerStarter.node.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index 4e79117ea2..4c6e1c5b20 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -771,6 +771,9 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension throwOnStdErr: false }); logger.debug(`Gracefully killed process ${pid}`); + + // Clean up lock file after successful kill + await this.deleteLockFile(pid); } catch (gracefulError) { // If graceful kill failed, use /F (force) logger.debug(`Graceful kill failed for ${pid}, using /F flag...`); @@ -778,6 +781,9 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { throwOnStdErr: false }); + + // Clean up lock file after successful force kill + await this.deleteLockFile(pid); } catch (forceError) { logger.debug(`Force kill also failed for ${pid}: ${forceError}`); } @@ -791,11 +797,15 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // Unix-like: try lsof first, fallback to ss let uniquePids = new Set(); - // Try lsof with LISTEN filter + // Try lsof with LISTEN filter and -nP to avoid slow DNS/service lookups try { - const lsofResult = await processService.exec('lsof', ['-sTCP:LISTEN', '-i', `:${port}`, '-t'], { - throwOnStdErr: false - }); + const lsofResult = await processService.exec( + 'lsof', + ['-sTCP:LISTEN', '-i', `:${port}`, '-t', '-nP'], + { + throwOnStdErr: false + } + ); if (lsofResult.stdout) { const pids = lsofResult.stdout .trim() @@ -849,6 +859,9 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension `Found orphaned deepnote-related process ${pid} using port ${port}, attempting graceful kill...` ); await this.killProcessGracefully(pid, processService); + + // Clean up lock file after successful kill + await this.deleteLockFile(pid); } else { logger.debug(`Deepnote-related process ${pid} on port ${port} has active parent, skipping`); } From 096d858527f37140015fadcb93cc76a0558578fd Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:27:04 +0100 Subject: [PATCH 08/12] fix: enforce two-stage verification for Unix process candidates - Unix branch was using only string matching to add PIDs to candidates - Now validates each PID with isDeepnoteRelatedProcess() before adding - Added re-verification in kill/lock loop before any action - Matches Windows two-stage verification pattern - Prevents false positives from ps output string matching - Ensures only verified deepnote-related processes are considered - Adds safety check in case process changes between detection and action --- .../deepnote/deepnoteServerStarter.node.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index 4c6e1c5b20..e95c84b25a 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -969,18 +969,22 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // - deepnote_toolkit server (main server process) // - pylsp (Python LSP server child process) // - jupyter (Jupyter server child process) - const isDeepnoteRelated = + const matchesPattern = (line.includes('deepnote_toolkit') && line.includes('server')) || (line.includes('pylsp') && line.includes('2087')) || // LSP server on port 2087 (line.includes('jupyter') && line.includes('deepnote')); - if (isDeepnoteRelated) { + if (matchesPattern) { // Unix format: user PID ... const parts = line.trim().split(/\s+/); if (parts.length > 1) { const pid = parseInt(parts[1], 10); if (!isNaN(pid)) { - candidatePids.push(pid); + // Validate with isDeepnoteRelatedProcess before adding to candidates + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); + if (isDeepnoteRelated) { + candidatePids.push(pid); + } } } } @@ -996,6 +1000,14 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // Check each process to determine if it should be killed for (const pid of candidatePids) { + // Re-verify it's deepnote-related before any kill/lock logic + const isDeepnoteRelated = await this.isDeepnoteRelatedProcess(pid); + if (!isDeepnoteRelated) { + logger.debug(`PID ${pid} is no longer deepnote-related, skipping`); + pidsToSkip.push({ pid, reason: 'not deepnote-related' }); + continue; + } + // Check if there's a lock file for this PID (only main server processes have lock files) const lockData = await this.readLockFile(pid); From 770fc528cd7da50cb568c10c345b508b27acfcf4 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:28:14 +0100 Subject: [PATCH 09/12] refactor: use graceful termination in process name-based cleanup - Changed from immediate force kill to graceful termination first - Windows: try taskkill without /F first, escalate to /F on failure - Unix: use existing killProcessGracefully helper (SIGTERM then SIGKILL) - Matches the graceful termination pattern from port-based cleanup - Preserves throwOnStdErr: false behavior throughout - Gives processes a chance to clean up before force termination - More consistent behavior across all cleanup code paths --- .../deepnote/deepnoteServerStarter.node.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index e95c84b25a..b5d42fa300 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -1062,11 +1062,23 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension for (const pid of pidsToKill) { try { if (process.platform === 'win32') { - await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { - throwOnStdErr: false - }); + // Windows: Try graceful kill first (without /F), then force kill if needed + logger.debug(`Attempting graceful termination of process ${pid}...`); + try { + await processService.exec('taskkill', ['/T', '/PID', pid.toString()], { + throwOnStdErr: false + }); + logger.debug(`Gracefully killed process ${pid}`); + } catch (gracefulError) { + // If graceful kill failed, use /F (force) + logger.debug(`Graceful kill failed for ${pid}, using /F flag...`); + await processService.exec('taskkill', ['/F', '/T', '/PID', pid.toString()], { + throwOnStdErr: false + }); + } } else { - await processService.exec('kill', ['-9', pid.toString()], { throwOnStdErr: false }); + // Unix: Use helper that tries SIGTERM then SIGKILL + await this.killProcessGracefully(pid, processService); } logger.info(`Killed orphaned process ${pid}`); From 51e0704a33424bef0fdca193d77a5e6cdaa9c179 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:29:33 +0100 Subject: [PATCH 10/12] chore: add pylsp, pythonw, and tlnp to cspell dictionary - pylsp: Python Language Server Protocol (LSP server) - pythonw: Windows Python executable without console window - tlnp: ss command flag for TCP listening numeric ports --- cspell.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cspell.json b/cspell.json index 33845480f2..624314dc93 100644 --- a/cspell.json +++ b/cspell.json @@ -46,9 +46,12 @@ "pgsql", "pids", "Pids", + "pylsp", "PYTHONHOME", + "pythonw", "Reselecting", "taskkill", + "tlnp", "unconfigured", "Unconfigured", "unittests", From 6bff35b4776bb27b47a0029aa1bbb53b5e245463 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:49:03 +0100 Subject: [PATCH 11/12] fix: use exact port matching in Windows netstat parsing - Changed from substring match to regex-based port extraction - Prevents false positives (e.g., port 8888 matching 88880) - Parses LocalAddress column and extracts port number explicitly - Handles both IPv4 (0.0.0.0:8888) and IPv6 ([::]:8888) formats - Uses exact numeric equality comparison instead of string contains - More robust and accurate port-based process detection --- src/kernels/deepnote/deepnoteServerStarter.node.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index b5d42fa300..c88e2aad55 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -742,12 +742,15 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // Parse and deduplicate PIDs for (const line of lines) { - if (line.includes(`:${port}`) && line.includes('LISTENING')) { - const parts = line.trim().split(/\s+/); + if (!line.includes('LISTENING')) continue; + const parts = line.trim().split(/\s+/); + // Windows netstat columns: Proto LocalAddress ForeignAddress State PID + const local = parts[1] || ''; + const m = local.match(/:(\d+)]?$/); // handles IPv4 and [::]:port + const localPort = m ? parseInt(m[1], 10) : NaN; + if (localPort === port) { const pid = parseInt(parts[parts.length - 1], 10); - if (!isNaN(pid) && pid > 0) { - uniquePids.add(pid); - } + if (!isNaN(pid) && pid > 0) uniquePids.add(pid); } } From 99021aedf5e87429fa6b809cc43f7a044d40f358 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 13:50:31 +0100 Subject: [PATCH 12/12] fix: add fallback for ss builds that reject sport filter - Some ss builds don't support 'sport = :port' filter syntax - Detect filter failure (empty stdout, stderr mentions 'filter') - Fallback to unfiltered 'ss -tlnp' and parse full output - Use regex to match exact port with word boundary (e.g., :8888\b) - Extract pid=NUMBER from matching lines only - Prevents false positives (port 8888 won't match 88880) - Maintains Set for automatic PID deduplication - Preserves throwOnStdErr: false throughout - Handles null/empty output safely --- .../deepnote/deepnoteServerStarter.node.ts | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index c88e2aad55..116c952692 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -824,11 +824,43 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension // Fallback to ss if lsof didn't find anything or failed if (uniquePids.size === 0) { try { + // Try ss with filter first const ssResult = await processService.exec('ss', ['-tlnp', `sport = :${port}`], { throwOnStdErr: false }); - if (ssResult.stdout) { - // Parse ss output: look for pid= + + // Check if filtered call succeeded (some ss builds reject the filter) + const filterFailed = + !ssResult.stdout || + ssResult.stdout.trim().length === 0 || + (ssResult.stderr && ssResult.stderr.includes('filter')); + + if (filterFailed) { + logger.debug(`ss filter not supported, trying without filter...`); + // Run ss without filter and parse full output + const ssUnfilteredResult = await processService.exec('ss', ['-tlnp'], { + throwOnStdErr: false + }); + + if (ssUnfilteredResult.stdout) { + const lines = ssUnfilteredResult.stdout.split('\n'); + for (const line of lines) { + // Match lines containing the port (e.g., ":8888" or ":8888 ") + const portPattern = new RegExp(`:${port}\\b`); + if (portPattern.test(line)) { + // Extract pid= from matching lines + const pidMatches = line.matchAll(/pid=(\d+)/g); + for (const match of pidMatches) { + const pid = parseInt(match[1], 10); + if (!isNaN(pid) && pid > 0) { + uniquePids.add(pid); + } + } + } + } + } + } else if (ssResult.stdout) { + // Filtered call succeeded, parse its output const pidMatches = ssResult.stdout.matchAll(/pid=(\d+)/g); for (const match of pidMatches) { const pid = parseInt(match[1], 10);