22// Licensed under the MIT License.
33
44import { inject , injectable , named } from 'inversify' ;
5- import { Uri } from 'vscode' ;
5+ import { CancellationToken , Uri } from 'vscode' ;
66import { PythonEnvironment } from '../../platform/pythonEnvironments/info' ;
77import { IDeepnoteServerStarter , IDeepnoteToolkitInstaller , DeepnoteServerInfo , DEEPNOTE_DEFAULT_PORT } from './types' ;
88import { IProcessServiceFactory , ObservableExecutionResult } from '../../platform/common/process/types.node' ;
99import { logger } from '../../platform/logging' ;
10- import { IOutputChannel , IDisposable } from '../../platform/common/types' ;
10+ import { IOutputChannel , IDisposable , IHttpClient } from '../../platform/common/types' ;
1111import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants' ;
1212import { sleep } from '../../platform/common/utils/async' ;
13- import { HttpClient } from '../../platform/common/net/httpClient ' ;
13+ import { Cancellation , raceCancellationError } from '../../platform/common/cancellation ' ;
1414import getPort from 'get-port' ;
1515
1616/**
@@ -21,31 +21,74 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter {
2121 private readonly serverProcesses : Map < string , ObservableExecutionResult < string > > = new Map ( ) ;
2222 private readonly serverInfos : Map < string , DeepnoteServerInfo > = new Map ( ) ;
2323 private readonly disposablesByFile : Map < string , IDisposable [ ] > = new Map ( ) ;
24- private readonly httpClient = new HttpClient ( ) ;
24+ // Track in-flight operations per file to prevent concurrent start/stop
25+ private readonly pendingOperations : Map < string , Promise < DeepnoteServerInfo | void > > = new Map ( ) ;
2526
2627 constructor (
2728 @inject ( IProcessServiceFactory ) private readonly processServiceFactory : IProcessServiceFactory ,
2829 @inject ( IDeepnoteToolkitInstaller ) private readonly toolkitInstaller : IDeepnoteToolkitInstaller ,
29- @inject ( IOutputChannel ) @named ( STANDARD_OUTPUT_CHANNEL ) private readonly outputChannel : IOutputChannel
30+ @inject ( IOutputChannel ) @named ( STANDARD_OUTPUT_CHANNEL ) private readonly outputChannel : IOutputChannel ,
31+ @inject ( IHttpClient ) private readonly httpClient : IHttpClient
3032 ) { }
3133
32- public async getOrStartServer ( interpreter : PythonEnvironment , deepnoteFileUri : Uri ) : Promise < DeepnoteServerInfo > {
34+ public async getOrStartServer (
35+ interpreter : PythonEnvironment ,
36+ deepnoteFileUri : Uri ,
37+ token ?: CancellationToken
38+ ) : Promise < DeepnoteServerInfo > {
3339 const fileKey = deepnoteFileUri . fsPath ;
3440
41+ // Wait for any pending operations on this file to complete
42+ const pendingOp = this . pendingOperations . get ( fileKey ) ;
43+ if ( pendingOp ) {
44+ logger . info ( `Waiting for pending operation on ${ fileKey } to complete...` ) ;
45+ try {
46+ await pendingOp ;
47+ } catch {
48+ // Ignore errors from previous operations
49+ }
50+ }
51+
3552 // If server is already running for this file, return existing info
3653 const existingServerInfo = this . serverInfos . get ( fileKey ) ;
3754 if ( existingServerInfo && ( await this . isServerRunning ( existingServerInfo ) ) ) {
3855 logger . info ( `Deepnote server already running at ${ existingServerInfo . url } for ${ fileKey } ` ) ;
3956 return existingServerInfo ;
4057 }
4158
59+ // Start the operation and track it
60+ const operation = this . startServerImpl ( interpreter , deepnoteFileUri , token ) ;
61+ this . pendingOperations . set ( fileKey , operation ) ;
62+
63+ try {
64+ const result = await operation ;
65+ return result ;
66+ } finally {
67+ // Remove from pending operations when done
68+ if ( this . pendingOperations . get ( fileKey ) === operation ) {
69+ this . pendingOperations . delete ( fileKey ) ;
70+ }
71+ }
72+ }
73+
74+ private async startServerImpl (
75+ interpreter : PythonEnvironment ,
76+ deepnoteFileUri : Uri ,
77+ token ?: CancellationToken
78+ ) : Promise < DeepnoteServerInfo > {
79+ const fileKey = deepnoteFileUri . fsPath ;
80+
81+ Cancellation . throwIfCanceled ( token ) ;
82+
4283 // Ensure toolkit is installed
4384 logger . info ( `Ensuring deepnote-toolkit is installed for ${ fileKey } ...` ) ;
44- const installed = await this . toolkitInstaller . ensureInstalled ( interpreter , deepnoteFileUri ) ;
85+ const installed = await this . toolkitInstaller . ensureInstalled ( interpreter , deepnoteFileUri , token ) ;
4586 if ( ! installed ) {
4687 throw new Error ( 'Failed to install deepnote-toolkit. Please check the output for details.' ) ;
4788 }
4889
90+ Cancellation . throwIfCanceled ( token ) ;
91+
4992 // Find available port
5093 const port = await getPort ( { host : 'localhost' , port : DEEPNOTE_DEFAULT_PORT } ) ;
5194 logger . info ( `Starting deepnote-toolkit server on port ${ port } for ${ fileKey } ` ) ;
@@ -88,7 +131,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter {
88131 const serverInfo = { url, port } ;
89132 this . serverInfos . set ( fileKey , serverInfo ) ;
90133
91- const serverReady = await this . waitForServer ( serverInfo , 30000 ) ;
134+ const serverReady = await this . waitForServer ( serverInfo , 30000 , token ) ;
92135 if ( ! serverReady ) {
93136 await this . stopServer ( deepnoteFileUri ) ;
94137 throw new Error ( 'Deepnote server failed to start within timeout period' ) ;
@@ -102,6 +145,34 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter {
102145
103146 public async stopServer ( deepnoteFileUri : Uri ) : Promise < void > {
104147 const fileKey = deepnoteFileUri . fsPath ;
148+
149+ // Wait for any pending operations on this file to complete
150+ const pendingOp = this . pendingOperations . get ( fileKey ) ;
151+ if ( pendingOp ) {
152+ logger . info ( `Waiting for pending operation on ${ fileKey } before stopping...` ) ;
153+ try {
154+ await pendingOp ;
155+ } catch {
156+ // Ignore errors from previous operations
157+ }
158+ }
159+
160+ // Start the stop operation and track it
161+ const operation = this . stopServerImpl ( deepnoteFileUri ) ;
162+ this . pendingOperations . set ( fileKey , operation ) ;
163+
164+ try {
165+ await operation ;
166+ } finally {
167+ // Remove from pending operations when done
168+ if ( this . pendingOperations . get ( fileKey ) === operation ) {
169+ this . pendingOperations . delete ( fileKey ) ;
170+ }
171+ }
172+ }
173+
174+ private async stopServerImpl ( deepnoteFileUri : Uri ) : Promise < void > {
175+ const fileKey = deepnoteFileUri . fsPath ;
105176 const serverProcess = this . serverProcesses . get ( fileKey ) ;
106177
107178 if ( serverProcess ) {
@@ -123,13 +194,18 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter {
123194 }
124195 }
125196
126- private async waitForServer ( serverInfo : DeepnoteServerInfo , timeout : number ) : Promise < boolean > {
197+ private async waitForServer (
198+ serverInfo : DeepnoteServerInfo ,
199+ timeout : number ,
200+ token ?: CancellationToken
201+ ) : Promise < boolean > {
127202 const startTime = Date . now ( ) ;
128203 while ( Date . now ( ) - startTime < timeout ) {
204+ Cancellation . throwIfCanceled ( token ) ;
129205 if ( await this . isServerRunning ( serverInfo ) ) {
130206 return true ;
131207 }
132- await sleep ( 500 ) ;
208+ await raceCancellationError ( token , sleep ( 500 ) ) ;
133209 }
134210 return false ;
135211 }
@@ -143,4 +219,35 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter {
143219 return false ;
144220 }
145221 }
222+
223+ public dispose ( ) : void {
224+ logger . info ( 'Disposing DeepnoteServerStarter - stopping all servers...' ) ;
225+
226+ // Stop all server processes
227+ for ( const [ fileKey , serverProcess ] of this . serverProcesses . entries ( ) ) {
228+ try {
229+ logger . info ( `Stopping Deepnote server for ${ fileKey } ...` ) ;
230+ serverProcess . proc ?. kill ( ) ;
231+ } catch ( ex ) {
232+ logger . error ( `Error stopping Deepnote server for ${ fileKey } : ${ ex } ` ) ;
233+ }
234+ }
235+
236+ // Dispose all tracked disposables
237+ for ( const [ fileKey , disposables ] of this . disposablesByFile . entries ( ) ) {
238+ try {
239+ disposables . forEach ( ( d ) => d . dispose ( ) ) ;
240+ } catch ( ex ) {
241+ logger . error ( `Error disposing resources for ${ fileKey } : ${ ex } ` ) ;
242+ }
243+ }
244+
245+ // Clear all maps
246+ this . serverProcesses . clear ( ) ;
247+ this . serverInfos . clear ( ) ;
248+ this . disposablesByFile . clear ( ) ;
249+ this . pendingOperations . clear ( ) ;
250+
251+ logger . info ( 'DeepnoteServerStarter disposed successfully' ) ;
252+ }
146253}
0 commit comments