@@ -6,29 +6,26 @@ import { Kernel } from '@jupyterlab/services';
6
6
import { inject , injectable , named } from 'inversify' ;
7
7
import * as path from 'path' ;
8
8
import { CancellationToken , CancellationTokenSource } from 'vscode' ;
9
+ import { IWorkspaceService } from '../../common/application/types' ;
9
10
import { wrapCancellationTokens } from '../../common/cancellation' ;
10
- import { traceInfo } from '../../common/logger' ;
11
+ import { traceError , traceInfo } from '../../common/logger' ;
11
12
import { IFileSystem , IPlatformService } from '../../common/platform/types' ;
12
13
import { IExtensionContext , IInstaller , InstallerResponse , IPathUtils , Product , Resource } from '../../common/types' ;
13
- import {
14
- IInterpreterLocatorService ,
15
- IInterpreterService ,
16
- KNOWN_PATH_SERVICE ,
17
- PythonInterpreter
18
- } from '../../interpreter/contracts' ;
14
+ import { IInterpreterLocatorService , IInterpreterService , KNOWN_PATH_SERVICE } from '../../interpreter/contracts' ;
19
15
import { captureTelemetry } from '../../telemetry' ;
20
16
import { Telemetry } from '../constants' ;
21
17
import { JupyterKernelSpec } from '../jupyter/kernels/jupyterKernelSpec' ;
22
18
import { IJupyterKernelSpec } from '../types' ;
23
19
import { getKernelInterpreter } from './helpers' ;
24
20
import { IKernelFinder } from './types' ;
21
+ // tslint:disable-next-line:no-require-imports no-var-requires
22
+ const flatten = require ( 'lodash/flatten' ) as typeof import ( 'lodash/flatten' ) ;
23
+
24
+ const winJupyterPath = path . join ( 'AppData' , 'Roaming' , 'jupyter' , 'kernels' ) ;
25
+ const linuxJupyterPath = path . join ( '.local' , 'share' , 'jupyter' , 'kernels' ) ;
26
+ const macJupyterPath = path . join ( 'Library' , 'Jupyter' , 'kernels' ) ;
27
+ const baseKernelPath = path . join ( 'share' , 'jupyter' , 'kernels' ) ;
25
28
26
- const kernelPaths = new Map ( [
27
- [ 'winJupyterPath' , path . join ( 'AppData' , 'Roaming' , 'jupyter' , 'kernels' ) ] ,
28
- [ 'linuxJupyterPath' , path . join ( '.local' , 'share' , 'jupyter' , 'kernels' ) ] ,
29
- [ 'macJupyterPath' , path . join ( 'Library' , 'Jupyter' , 'kernels' ) ] ,
30
- [ 'kernel' , path . join ( 'share' , 'jupyter' , 'kernels' ) ]
31
- ] ) ;
32
29
const cacheFile = 'kernelSpecPathCache.json' ;
33
30
const defaultSpecName = 'python_defaultSpec_' ;
34
31
@@ -47,6 +44,13 @@ export function findIndexOfConnectionFile(kernelSpec: Readonly<IJupyterKernelSpe
47
44
@injectable ( )
48
45
export class KernelFinder implements IKernelFinder {
49
46
private cache : string [ ] = [ ] ;
47
+ private cacheDirty = false ;
48
+
49
+ // Store our results when listing all possible kernelspecs for a resource
50
+ private workspaceToKernels = new Map < string , Promise < IJupyterKernelSpec [ ] > > ( ) ;
51
+
52
+ // Store any json file that we have loaded from disk before
53
+ private pathToKernelSpec = new Map < string , Promise < IJupyterKernelSpec | undefined > > ( ) ;
50
54
51
55
constructor (
52
56
@inject ( IInterpreterService ) private interpreterService : IInterpreterService ,
@@ -57,7 +61,8 @@ export class KernelFinder implements IKernelFinder {
57
61
@inject ( IFileSystem ) private file : IFileSystem ,
58
62
@inject ( IPathUtils ) private readonly pathUtils : IPathUtils ,
59
63
@inject ( IInstaller ) private installer : IInstaller ,
60
- @inject ( IExtensionContext ) private readonly context : IExtensionContext
64
+ @inject ( IExtensionContext ) private readonly context : IExtensionContext ,
65
+ @inject ( IWorkspaceService ) private readonly workspaceService : IWorkspaceService
61
66
) { }
62
67
63
68
@captureTelemetry ( Telemetry . KernelFinderPerf )
@@ -68,7 +73,6 @@ export class KernelFinder implements IKernelFinder {
68
73
) : Promise < IJupyterKernelSpec > {
69
74
this . cache = await this . readCache ( ) ;
70
75
let foundKernel : IJupyterKernelSpec | undefined ;
71
- const activeInterpreter = await this . interpreterService . getActiveInterpreter ( resource ) ;
72
76
73
77
if ( kernelName && ! kernelName . includes ( defaultSpecName ) ) {
74
78
let kernelSpec = await this . searchCache ( kernelName ) ;
@@ -78,32 +82,27 @@ export class KernelFinder implements IKernelFinder {
78
82
}
79
83
80
84
// Check in active interpreter first
81
- if ( activeInterpreter ) {
82
- kernelSpec = await this . getKernelSpecFromActiveInterpreter ( kernelName , activeInterpreter ) ;
83
- }
85
+ kernelSpec = await this . getKernelSpecFromActiveInterpreter ( kernelName , resource ) ;
84
86
85
87
if ( kernelSpec ) {
86
88
this . writeCache ( this . cache ) . ignoreErrors ( ) ;
87
89
return kernelSpec ;
88
90
}
89
91
90
92
const diskSearch = this . findDiskPath ( kernelName ) ;
91
- const interpreterSearch = this . interpreterLocator
92
- . getInterpreters ( resource , { ignoreCache : false } )
93
- . then ( ( interpreters ) => {
94
- const interpreterPaths = interpreters . map ( ( interp ) => interp . sysPrefix ) ;
95
- return this . findInterpreterPath ( interpreterPaths , kernelName ) ;
96
- } ) ;
93
+ const interpreterSearch = this . getInterpreterPaths ( resource ) . then ( ( interpreterPaths ) => {
94
+ return this . findInterpreterPath ( interpreterPaths , kernelName ) ;
95
+ } ) ;
97
96
98
97
let result = await Promise . race ( [ diskSearch , interpreterSearch ] ) ;
99
98
if ( ! result ) {
100
99
const both = await Promise . all ( [ diskSearch , interpreterSearch ] ) ;
101
100
result = both [ 0 ] ? both [ 0 ] : both [ 1 ] ;
102
101
}
103
102
104
- foundKernel = result ? result : await this . getDefaultKernelSpec ( activeInterpreter ) ;
103
+ foundKernel = result ? result : await this . getDefaultKernelSpec ( resource ) ;
105
104
} else {
106
- foundKernel = await this . getDefaultKernelSpec ( activeInterpreter ) ;
105
+ foundKernel = await this . getDefaultKernelSpec ( resource ) ;
107
106
}
108
107
109
108
this . writeCache ( this . cache ) . ignoreErrors ( ) ;
@@ -113,8 +112,149 @@ export class KernelFinder implements IKernelFinder {
113
112
}
114
113
115
114
// Search all our local file system locations for installed kernel specs and return them
116
- public async listKernelSpecs ( _cancelToken ?: CancellationToken ) : Promise < IJupyterKernelSpec [ ] > {
117
- throw new Error ( 'Not yet implmented' ) ;
115
+ public async listKernelSpecs ( resource : Resource ) : Promise < IJupyterKernelSpec [ ] > {
116
+ if ( ! resource ) {
117
+ // We need a resource to search for related kernel specs
118
+ return [ ] ;
119
+ }
120
+
121
+ // Get an id for the workspace folder, if we don't have one, use the fsPath of the resource
122
+ const workspaceFolderId = this . workspaceService . getWorkspaceFolderIdentifier ( resource , resource . fsPath ) ;
123
+
124
+ // If we have not already searched for this resource, then generate the search
125
+ if ( ! this . workspaceToKernels . has ( workspaceFolderId ) ) {
126
+ this . workspaceToKernels . set ( workspaceFolderId , this . findResourceKernelSpecs ( resource ) ) ;
127
+ }
128
+
129
+ this . writeCache ( this . cache ) . ignoreErrors ( ) ;
130
+
131
+ // ! as the has and set above verify that we have a return here
132
+ return this . workspaceToKernels . get ( workspaceFolderId ) ! ;
133
+ }
134
+
135
+ private async findResourceKernelSpecs ( resource : Resource ) : Promise < IJupyterKernelSpec [ ] > {
136
+ const results : IJupyterKernelSpec [ ] = [ ] ;
137
+
138
+ // Find all the possible places to look for this resource
139
+ const paths = await this . findAllResourcePossibleKernelPaths ( resource ) ;
140
+
141
+ const searchResults = await this . kernelGlobSearch ( paths ) ;
142
+
143
+ await Promise . all (
144
+ searchResults . map ( async ( resultPath ) => {
145
+ // Add these into our path cache to speed up later finds
146
+ this . updateCache ( resultPath ) ;
147
+ const kernelspec = await this . getKernelSpec ( resultPath ) ;
148
+
149
+ if ( kernelspec ) {
150
+ results . push ( kernelspec ) ;
151
+ }
152
+ } )
153
+ ) ;
154
+
155
+ return results ;
156
+ }
157
+
158
+ // Load the IJupyterKernelSpec for a given spec path, check the ones that we have already loaded first
159
+ private async getKernelSpec ( specPath : string ) : Promise < IJupyterKernelSpec | undefined > {
160
+ // If we have not already loaded this kernel spec, then load it
161
+ if ( ! this . pathToKernelSpec . has ( specPath ) ) {
162
+ this . pathToKernelSpec . set ( specPath , this . loadKernelSpec ( specPath ) ) ;
163
+ }
164
+
165
+ // ! as the has and set above verify that we have a return here
166
+ return this . pathToKernelSpec . get ( specPath ) ! . then ( ( value ) => {
167
+ if ( value ) {
168
+ return value ;
169
+ }
170
+
171
+ // If we failed to get a kernelspec pull path from our cache and loaded list
172
+ this . pathToKernelSpec . delete ( specPath ) ;
173
+ this . cache = this . cache . filter ( ( itempath ) => itempath !== specPath ) ;
174
+ return undefined ;
175
+ } ) ;
176
+ }
177
+
178
+ // Load kernelspec json from disk
179
+ private async loadKernelSpec ( specPath : string ) : Promise < IJupyterKernelSpec | undefined > {
180
+ let kernelJson ;
181
+ try {
182
+ kernelJson = JSON . parse ( await this . file . readFile ( specPath ) ) ;
183
+ } catch {
184
+ traceError ( `Failed to parse kernelspec ${ specPath } ` ) ;
185
+ return undefined ;
186
+ }
187
+ return new JupyterKernelSpec ( kernelJson , specPath ) ;
188
+ }
189
+
190
+ // For the given resource, find atll the file paths for kernel specs that wewant to associate with this
191
+ private async findAllResourcePossibleKernelPaths (
192
+ resource : Resource ,
193
+ _cancelToken ?: CancellationToken
194
+ ) : Promise < string [ ] > {
195
+ const [ activePath , interpreterPaths , diskPaths ] = await Promise . all ( [
196
+ this . getActiveInterpreterPath ( resource ) ,
197
+ this . getInterpreterPaths ( resource ) ,
198
+ this . getDiskPaths ( )
199
+ ] ) ;
200
+
201
+ return [ ...activePath , ...interpreterPaths , ...diskPaths ] ;
202
+ }
203
+
204
+ private async getActiveInterpreterPath ( resource : Resource ) : Promise < string [ ] > {
205
+ const activeInterpreter = await this . interpreterService . getActiveInterpreter ( resource ) ;
206
+
207
+ if ( activeInterpreter ) {
208
+ return [ path . join ( activeInterpreter . sysPrefix , 'share' , 'jupyter' , 'kernels' ) ] ;
209
+ }
210
+
211
+ return [ ] ;
212
+ }
213
+
214
+ private async getInterpreterPaths ( resource : Resource ) : Promise < string [ ] > {
215
+ const interpreters = await this . interpreterLocator . getInterpreters ( resource , { ignoreCache : false } ) ;
216
+ const interpreterPrefixPaths = interpreters . map ( ( interpreter ) => interpreter . sysPrefix ) ;
217
+ // We can get many duplicates here, so de-dupe the list
218
+ const uniqueInterpreterPrefixPaths = [ ...new Set ( interpreterPrefixPaths ) ] ;
219
+ return uniqueInterpreterPrefixPaths . map ( ( prefixPath ) => path . join ( prefixPath , baseKernelPath ) ) ;
220
+ }
221
+
222
+ private async getDiskPaths ( ) : Promise < string [ ] > {
223
+ let paths = [ ] ;
224
+
225
+ if ( this . platformService . isWindows ) {
226
+ paths = [ path . join ( this . pathUtils . home , winJupyterPath ) ] ;
227
+
228
+ if ( process . env . ALLUSERSPROFILE ) {
229
+ paths . push ( path . join ( process . env . ALLUSERSPROFILE , 'jupyter' , 'kernels' ) ) ;
230
+ }
231
+ } else {
232
+ // Unix based
233
+ const secondPart = this . platformService . isMac ? macJupyterPath : linuxJupyterPath ;
234
+
235
+ paths = [
236
+ path . join ( 'usr' , 'share' , 'jupyter' , 'kernels' ) ,
237
+ path . join ( 'usr' , 'local' , 'share' , 'jupyter' , 'kernels' ) ,
238
+ path . join ( this . pathUtils . home , secondPart )
239
+ ] ;
240
+ }
241
+
242
+ return paths ;
243
+ }
244
+
245
+ // Given a set of paths, search for kernel.json files and return back the full paths of all of them that we find
246
+ private async kernelGlobSearch ( paths : string [ ] ) : Promise < string [ ] > {
247
+ const promises = paths . map ( ( kernelPath ) => this . file . search ( `**/kernel.json` , kernelPath ) ) ;
248
+ const searchResults = await Promise . all ( promises ) ;
249
+
250
+ // Append back on the start of each path so we have the full path in the results
251
+ const fullPathResults = searchResults . map ( ( result , index ) => {
252
+ return result . map ( ( partialSpecPath ) => {
253
+ return path . join ( paths [ index ] , partialSpecPath ) ;
254
+ } ) ;
255
+ } ) ;
256
+
257
+ return flatten ( fullPathResults ) ;
118
258
}
119
259
120
260
// For the given kernelspec return back the kernelspec with ipykernel installed into it or error
@@ -143,21 +283,17 @@ export class KernelFinder implements IKernelFinder {
143
283
144
284
private async getKernelSpecFromActiveInterpreter (
145
285
kernelName : string ,
146
- activeInterpreter : PythonInterpreter
286
+ resource : Resource
147
287
) : Promise < IJupyterKernelSpec | undefined > {
148
- return this . getKernelSpecFromDisk (
149
- [ path . join ( activeInterpreter . sysPrefix , 'share' , 'jupyter' , 'kernels' ) ] ,
150
- kernelName
151
- ) ;
288
+ const activePath = await this . getActiveInterpreterPath ( resource ) ;
289
+ return this . getKernelSpecFromDisk ( activePath , kernelName ) ;
152
290
}
153
291
154
292
private async findInterpreterPath (
155
293
interpreterPaths : string [ ] ,
156
294
kernelName : string
157
295
) : Promise < IJupyterKernelSpec | undefined > {
158
- const promises = interpreterPaths . map ( ( intPath ) =>
159
- this . getKernelSpecFromDisk ( [ path . join ( intPath , kernelPaths . get ( 'kernel' ) ! ) ] , kernelName )
160
- ) ;
296
+ const promises = interpreterPaths . map ( ( intPath ) => this . getKernelSpecFromDisk ( [ intPath ] , kernelName ) ) ;
161
297
162
298
const specs = await Promise . all ( promises ) ;
163
299
return specs . find ( ( sp ) => sp !== undefined ) ;
@@ -166,46 +302,23 @@ export class KernelFinder implements IKernelFinder {
166
302
// Jupyter looks for kernels in these paths:
167
303
// https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs
168
304
private async findDiskPath ( kernelName : string ) : Promise < IJupyterKernelSpec | undefined > {
169
- let paths = [ ] ;
170
-
171
- if ( this . platformService . isWindows ) {
172
- paths = [ path . join ( this . pathUtils . home , kernelPaths . get ( 'winJupyterPath' ) ! ) ] ;
173
-
174
- if ( process . env . ALLUSERSPROFILE ) {
175
- paths . push ( path . join ( process . env . ALLUSERSPROFILE , 'jupyter' , 'kernels' ) ) ;
176
- }
177
- } else {
178
- // Unix based
179
- const secondPart = this . platformService . isMac
180
- ? kernelPaths . get ( 'macJupyterPath' ) !
181
- : kernelPaths . get ( 'linuxJupyterPath' ) ! ;
182
-
183
- paths = [
184
- path . join ( 'usr' , 'share' , 'jupyter' , 'kernels' ) ,
185
- path . join ( 'usr' , 'local' , 'share' , 'jupyter' , 'kernels' ) ,
186
- path . join ( this . pathUtils . home , secondPart )
187
- ] ;
188
- }
305
+ const paths = await this . getDiskPaths ( ) ;
189
306
190
307
return this . getKernelSpecFromDisk ( paths , kernelName ) ;
191
308
}
192
309
193
310
private async getKernelSpecFromDisk ( paths : string [ ] , kernelName : string ) : Promise < IJupyterKernelSpec | undefined > {
194
- const promises = paths . map ( ( kernelPath ) => this . file . search ( '**/kernel.json' , kernelPath ) ) ;
195
- const searchResults = await Promise . all ( promises ) ;
196
- searchResults . forEach ( ( result , i ) => {
197
- result . forEach ( ( res ) => {
198
- const specPath = path . join ( paths [ i ] , res ) ;
199
- if ( ! this . cache . includes ( specPath ) ) {
200
- this . cache . push ( specPath ) ;
201
- }
202
- } ) ;
311
+ const searchResults = await this . kernelGlobSearch ( paths ) ;
312
+ searchResults . forEach ( ( specPath ) => {
313
+ this . updateCache ( specPath ) ;
203
314
} ) ;
204
315
205
316
return this . searchCache ( kernelName ) ;
206
317
}
207
318
208
- private async getDefaultKernelSpec ( activeInterpreter ?: PythonInterpreter ) : Promise < IJupyterKernelSpec > {
319
+ private async getDefaultKernelSpec ( resource : Resource ) : Promise < IJupyterKernelSpec > {
320
+ const activeInterpreter = await this . interpreterService . getActiveInterpreter ( resource ) ;
321
+
209
322
// This creates a default kernel spec. When launched, 'python' argument will map to using the interpreter
210
323
// associated with the current resource for launching.
211
324
const defaultSpec : Kernel . ISpecModel = {
@@ -231,8 +344,18 @@ export class KernelFinder implements IKernelFinder {
231
344
}
232
345
}
233
346
347
+ private updateCache ( newPath : string ) {
348
+ if ( ! this . cache . includes ( newPath ) ) {
349
+ this . cache . push ( newPath ) ;
350
+ this . cacheDirty = true ;
351
+ }
352
+ }
353
+
234
354
private async writeCache ( cache : string [ ] ) {
235
- await this . file . writeFile ( path . join ( this . context . globalStoragePath , cacheFile ) , JSON . stringify ( cache ) ) ;
355
+ if ( this . cacheDirty ) {
356
+ await this . file . writeFile ( path . join ( this . context . globalStoragePath , cacheFile ) , JSON . stringify ( cache ) ) ;
357
+ this . cacheDirty = false ;
358
+ }
236
359
}
237
360
238
361
private async searchCache ( kernelName : string ) : Promise < IJupyterKernelSpec | undefined > {
@@ -246,10 +369,12 @@ export class KernelFinder implements IKernelFinder {
246
369
} ) ;
247
370
248
371
if ( kernelJsonFile ) {
249
- const kernelJson = JSON . parse ( await this . file . readFile ( kernelJsonFile ) ) ;
250
- const spec = new JupyterKernelSpec ( kernelJson , kernelJsonFile ) ;
251
- spec . name = kernelName ;
252
- return spec ;
372
+ const spec = await this . getKernelSpec ( kernelJsonFile ) ;
373
+
374
+ if ( spec ) {
375
+ spec . name = kernelName ;
376
+ return spec ;
377
+ }
253
378
}
254
379
255
380
return undefined ;
0 commit comments