22// Licensed under the MIT License.
33'use strict' ;
44
5+ import * as fsextra from 'fs-extra' ;
56import { inject , injectable } from 'inversify' ;
7+ import * as os from 'os' ;
8+ import * as path from 'path' ;
69import * as portfinder from 'portfinder' ;
710import { promisify } from 'util' ;
811import * as uuid from 'uuid/v4' ;
12+ import { isTestExecution } from '../../common/constants' ;
13+ import { traceInfo } from '../../common/logger' ;
914import { IProcessServiceFactory } from '../../common/process/types' ;
1015import { Resource } from '../../common/types' ;
1116import { captureTelemetry } from '../../telemetry' ;
@@ -16,20 +21,62 @@ import { KernelDaemonPool } from './kernelDaemonPool';
1621import { KernelProcess } from './kernelProcess' ;
1722import { IKernelConnection , IKernelLauncher , IKernelProcess } from './types' ;
1823
19- const PortToStartFrom = 9_000 ;
24+ const PortFormatString = `kernelLauncherPortStart_{0}.tmp` ;
2025
2126// Launches and returns a kernel process given a resource or python interpreter.
2227// If the given interpreter is undefined, it will try to use the selected interpreter.
2328// If the selected interpreter doesn't have a kernel, it will find a kernel on disk and use that.
2429@injectable ( )
2530export class KernelLauncher implements IKernelLauncher {
26- private static nextFreePortToTryAndUse = PortToStartFrom ;
31+ private static startPortPromise = KernelLauncher . computeStartPort ( ) ;
32+ private static nextFreePortToTryAndUsePromise = KernelLauncher . startPortPromise ;
2733 constructor (
2834 @inject ( IProcessServiceFactory ) private processExecutionFactory : IProcessServiceFactory ,
2935 @inject ( IDataScienceFileSystem ) private readonly fs : IDataScienceFileSystem ,
3036 @inject ( KernelDaemonPool ) private readonly daemonPool : KernelDaemonPool
3137 ) { }
3238
39+ // This function is public so it can be called when a test shuts down
40+ public static async cleanupStartPort ( ) {
41+ try {
42+ // Destroy the file
43+ const port = await KernelLauncher . startPortPromise ;
44+ traceInfo ( `Cleaning up port start file : ${ port } ` ) ;
45+
46+ const filePath = path . join ( os . tmpdir ( ) , PortFormatString . format ( port . toString ( ) ) ) ;
47+ await fsextra . remove ( filePath ) ;
48+ } catch ( exc ) {
49+ // If it fails it doesn't really matter. Just a temp file
50+ traceInfo ( `Kernel port mutex failed to cleanup: ` , exc ) ;
51+ }
52+ }
53+
54+ private static async computeStartPort ( ) : Promise < number > {
55+ if ( isTestExecution ( ) ) {
56+ // Since multiple instances of a test may be running, write our best guess to a shared file
57+ let portStart = 9_000 ;
58+ let result = 0 ;
59+ while ( result === 0 && portStart < 65_000 ) {
60+ try {
61+ // Try creating a file with the port in the name
62+ const filePath = path . join ( os . tmpdir ( ) , PortFormatString . format ( portStart . toString ( ) ) ) ;
63+ await fsextra . open ( filePath , 'wx' ) ;
64+
65+ // If that works, we have our port
66+ result = portStart ;
67+ } catch {
68+ // If that fails, it should mean the file already exists
69+ portStart += 1_000 ;
70+ }
71+ }
72+ traceInfo ( `Computed port start for KernelLauncher is : ${ result } ` ) ;
73+
74+ return result ;
75+ } else {
76+ return 9_000 ;
77+ }
78+ }
79+
3380 @captureTelemetry ( Telemetry . KernelLauncherPerf )
3481 public async launch (
3582 kernelConnectionMetadata : KernelSpecConnectionMetadata | PythonKernelConnectionMetadata ,
@@ -49,18 +96,28 @@ export class KernelLauncher implements IKernelLauncher {
4996 return kernelProcess ;
5097 }
5198
52- private async getKernelConnection ( ) : Promise < IKernelConnection > {
99+ private async getConnectionPorts ( ) : Promise < number [ ] > {
53100 const getPorts = promisify ( portfinder . getPorts ) ;
101+
102+ // Have to wait for static port lookup (it handles case where two VS code instances are running)
103+ const nextFreePort = await KernelLauncher . nextFreePortToTryAndUsePromise ;
104+ const startPort = await KernelLauncher . startPortPromise ;
105+
54106 // Ports may have been freed, hence start from begining.
55- const port =
56- KernelLauncher . nextFreePortToTryAndUse > PortToStartFrom + 1_000
57- ? PortToStartFrom
58- : KernelLauncher . nextFreePortToTryAndUse ;
107+ const port = nextFreePort > startPort + 1_000 ? startPort : nextFreePort ;
108+
109+ // Then get the next set starting at that point
59110 const ports = await getPorts ( 5 , { host : '127.0.0.1' , port } ) ;
111+
60112 // We launch restart kernels in the background, its possible other session hasn't started.
61113 // Ensure we do not use same ports.
62- KernelLauncher . nextFreePortToTryAndUse = Math . max ( ...ports ) + 1 ;
114+ KernelLauncher . nextFreePortToTryAndUsePromise = Promise . resolve ( Math . max ( ...ports ) + 1 ) ;
63115
116+ return ports ;
117+ }
118+
119+ private async getKernelConnection ( ) : Promise < IKernelConnection > {
120+ const ports = await this . getConnectionPorts ( ) ;
64121 return {
65122 version : 1 ,
66123 key : uuid ( ) ,
0 commit comments