Skip to content

Commit 1df838b

Browse files
authored
fix(deepnote-server): ensure consecutive port allocation skips bound ports (#189)
1 parent 5bdf938 commit 1df838b

File tree

3 files changed

+606
-39
lines changed

3 files changed

+606
-39
lines changed

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"jupyter",
4646
"jupyterlab",
4747
"JVSC",
48+
"loopbacks",
4849
"matplotlib",
4950
"millis",
5051
"mindsdb",
@@ -67,9 +68,9 @@
6768
"Unconfigured",
6869
"unittests",
6970
"vegalite",
70-
"venv's",
7171
"venv",
7272
"Venv",
73+
"venv's",
7374
"venvs",
7475
"vscode",
7576
"xanchor",

src/kernels/deepnote/deepnoteServerStarter.node.ts

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
import * as fs from 'fs-extra';
5-
import getPort from 'get-port';
65
import { inject, injectable, named, optional } from 'inversify';
76
import * as os from 'os';
87
import { CancellationToken, l10n, Uri } from 'vscode';
@@ -19,6 +18,7 @@ import { ISqlIntegrationEnvVarsProvider } from '../../platform/notebooks/deepnot
1918
import { PythonEnvironment } from '../../platform/pythonEnvironments/info';
2019
import * as path from '../../platform/vscode-path/path';
2120
import { DEEPNOTE_DEFAULT_PORT, DeepnoteServerInfo, IDeepnoteServerStarter, IDeepnoteToolkitInstaller } from './types';
21+
import tcpPortUsed from 'tcp-port-used';
2222

2323
/**
2424
* Lock file data structure for tracking server ownership
@@ -499,14 +499,16 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
499499
* @returns Object with jupyterPort and lspPort
500500
*/
501501
private async allocatePorts(key: string): Promise<{ jupyterPort: number; lspPort: number }> {
502-
// Wait for any ongoing port allocation to complete
503-
await this.portAllocationLock;
504-
505-
// Create new allocation promise and update the lock
502+
// Chain onto the existing lock promise to serialize allocations even when multiple calls start concurrently
503+
const previousLock = this.portAllocationLock;
506504
let releaseLock: () => void;
507-
this.portAllocationLock = new Promise((resolve) => {
505+
const currentLock = new Promise<void>((resolve) => {
508506
releaseLock = resolve;
509507
});
508+
this.portAllocationLock = previousLock.then(() => currentLock);
509+
510+
// Wait until all prior allocations have completed before proceeding
511+
await previousLock;
510512

511513
try {
512514
// Get all ports currently in use by our managed servers
@@ -520,13 +522,11 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
520522
}
521523
}
522524

523-
// Allocate Jupyter port first
524-
const jupyterPort = await this.findAvailablePort(DEEPNOTE_DEFAULT_PORT, portsInUse);
525-
portsInUse.add(jupyterPort); // Reserve it immediately
526-
527-
// Allocate LSP port (starting from jupyterPort + 1 to avoid conflicts)
528-
const lspPort = await this.findAvailablePort(jupyterPort + 1, portsInUse);
529-
portsInUse.add(lspPort); // Reserve it immediately
525+
// Find a pair of consecutive available ports
526+
const { jupyterPort, lspPort } = await this.findConsecutiveAvailablePorts(
527+
DEEPNOTE_DEFAULT_PORT,
528+
portsInUse
529+
);
530530

531531
// Reserve both ports by adding to serverInfos
532532
// This prevents other concurrent allocations from getting the same ports
@@ -538,7 +538,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
538538
this.serverInfos.set(key, serverInfo);
539539

540540
logger.info(
541-
`Allocated ports for ${key}: jupyter=${jupyterPort}, lsp=${lspPort} (excluded: ${
541+
`Allocated consecutive ports for ${key}: jupyter=${jupyterPort}, lsp=${lspPort} (excluded: ${
542542
portsInUse.size > 2
543543
? Array.from(portsInUse)
544544
.filter((p) => p !== jupyterPort && p !== lspPort)
@@ -549,14 +549,89 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
549549

550550
return { jupyterPort, lspPort };
551551
} finally {
552-
// Release the lock to allow next allocation
552+
// Release the lock to allow next allocation in the chain to proceed
553553
releaseLock!();
554554
}
555555
}
556556

557+
/**
558+
* Find a pair of consecutive available ports (port and port+1).
559+
* This is critical for the deepnote-toolkit server which expects consecutive ports.
560+
*
561+
* @param startPort The port number to start searching from
562+
* @param portsInUse Set of ports already allocated to other servers
563+
* @returns A pair of consecutive ports { jupyterPort, lspPort } where lspPort = jupyterPort + 1
564+
* @throws DeepnoteServerStartupError if no consecutive ports can be found after maxAttempts
565+
*/
566+
private async findConsecutiveAvailablePorts(
567+
startPort: number,
568+
portsInUse: Set<number>
569+
): Promise<{ jupyterPort: number; lspPort: number }> {
570+
const maxAttempts = 100;
571+
572+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
573+
// Try to find an available Jupyter port
574+
const candidatePort = await this.findAvailablePort(
575+
attempt === 0 ? startPort : startPort + attempt,
576+
portsInUse
577+
);
578+
579+
const nextPort = candidatePort + 1;
580+
581+
// Check if the consecutive port (candidatePort + 1) is also available
582+
const isNextPortInUse = portsInUse.has(nextPort);
583+
const isNextPortAvailable = !isNextPortInUse && (await this.isPortAvailable(nextPort));
584+
logger.info(
585+
`Consecutive port check for base ${candidatePort}: next=${nextPort}, inUseSet=${isNextPortInUse}, available=${isNextPortAvailable}`
586+
);
587+
588+
if (isNextPortAvailable) {
589+
// Found a consecutive pair!
590+
return { jupyterPort: candidatePort, lspPort: nextPort };
591+
}
592+
593+
// Consecutive port not available - mark both as unavailable and try next
594+
portsInUse.add(candidatePort);
595+
portsInUse.add(nextPort);
596+
}
597+
598+
// Failed to find consecutive ports after max attempts
599+
throw new DeepnoteServerStartupError(
600+
'python',
601+
startPort,
602+
'process_failed',
603+
'',
604+
l10n.t(
605+
'Failed to find consecutive available ports after {0} attempts starting from port {1}. Please close some applications using network ports and try again.',
606+
maxAttempts,
607+
startPort
608+
)
609+
);
610+
}
611+
612+
/**
613+
* Check if a specific port is available on the system by actually trying to bind to it.
614+
* This is more reliable than get-port which doesn't test the exact port.
615+
*/
616+
private async isPortAvailable(port: number): Promise<boolean> {
617+
try {
618+
const inUse = await tcpPortUsed.check(port, '127.0.0.1');
619+
if (inUse) {
620+
return false;
621+
}
622+
623+
// Also check IPv6 loopback to be safe
624+
const inUseIpv6 = await tcpPortUsed.check(port, '::1');
625+
return !inUseIpv6;
626+
} catch (error) {
627+
logger.warn(`Failed to check port availability for ${port}:`, error);
628+
return false;
629+
}
630+
}
631+
557632
/**
558633
* Find an available port starting from the given port number.
559-
* Checks both our internal portsInUse set and system availability.
634+
* Checks both our internal portsInUse set and system availability by actually binding to test.
560635
*/
561636
private async findAvailablePort(startPort: number, portsInUse: Set<number>): Promise<number> {
562637
let port = startPort;
@@ -566,23 +641,12 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
566641
while (attempts < maxAttempts) {
567642
// Skip ports already in use by our servers
568643
if (!portsInUse.has(port)) {
569-
// Check if this port is available on the system
570-
const availablePort = await getPort({
571-
host: 'localhost',
572-
port
573-
});
644+
// Check if this port is actually available on the system by binding to it
645+
const available = await this.isPortAvailable(port);
574646

575-
// If get-port returned the same port, it's available
576-
if (availablePort === port) {
647+
if (available) {
577648
return port;
578649
}
579-
580-
// get-port returned a different port - check if that one is in use
581-
if (!portsInUse.has(availablePort)) {
582-
return availablePort;
583-
}
584-
585-
// Both our requested port and get-port's suggestion are in use, try next
586650
}
587651

588652
// Try next port
@@ -920,14 +984,8 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
920984
pidsToSkip.push({ pid, reason: 'belongs to current session' });
921985
}
922986
} else {
923-
// No lock file - check if orphaned before killing
924-
const isOrphaned = await this.isProcessOrphaned(pid);
925-
if (isOrphaned) {
926-
logger.info(`PID ${pid} has no lock file and is orphaned - will kill`);
927-
pidsToKill.push(pid);
928-
} else {
929-
pidsToSkip.push({ pid, reason: 'no lock file but has active parent process' });
930-
}
987+
// No lock file - assume it's an external/non-managed process and skip it
988+
pidsToSkip.push({ pid, reason: 'no lock file (assuming external process)' });
931989
}
932990
}
933991

0 commit comments

Comments
 (0)