22// Licensed under the MIT License.
33
44import * as fs from 'fs-extra' ;
5- import getPort from 'get-port' ;
65import { inject , injectable , named , optional } from 'inversify' ;
76import * as os from 'os' ;
87import { CancellationToken , l10n , Uri } from 'vscode' ;
@@ -19,6 +18,7 @@ import { ISqlIntegrationEnvVarsProvider } from '../../platform/notebooks/deepnot
1918import { PythonEnvironment } from '../../platform/pythonEnvironments/info' ;
2019import * as path from '../../platform/vscode-path/path' ;
2120import { 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