1
+ /* eslint-disable max-classes-per-file */
1
2
// Copyright (c) Microsoft Corporation. All rights reserved.
2
3
// Licensed under the MIT License.
4
+
3
5
'use strict' ;
6
+
4
7
import '../../common/extensions' ;
5
8
6
9
import { inject , injectable } from 'inversify' ;
7
10
11
+ import { ProgressLocation , ProgressOptions } from 'vscode' ;
8
12
import { IApplicationShell , IWorkspaceService } from '../../common/application/types' ;
9
13
import { PYTHON_WARNINGS } from '../../common/constants' ;
10
14
import { IPlatformService } from '../../common/platform/types' ;
@@ -33,7 +37,6 @@ import {
33
37
import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda' ;
34
38
import { StopWatch } from '../../common/utils/stopWatch' ;
35
39
import { Interpreters } from '../../common/utils/localize' ;
36
- import { ProgressLocation , ProgressOptions } from 'vscode' ;
37
40
import { TerminalAutoActivation } from '../../common/experiments/groups' ;
38
41
import { IExtensionSingleActivationService } from '../../activation/types' ;
39
42
@@ -55,6 +58,8 @@ const condaRetryMessages = [
55
58
'The directory is not empty' ,
56
59
] ;
57
60
61
+ type EnvironmentVariables = { [ key : string ] : string | undefined } ;
62
+
58
63
/**
59
64
* This class exists so that the environment variable fetching can be cached in between tests. Normally
60
65
* this cache resides in memory for the duration of the EnvironmentActivationService's lifetime, but in the case
@@ -63,39 +68,43 @@ const condaRetryMessages = [
63
68
*/
64
69
export class EnvironmentActivationServiceCache {
65
70
private static useStatic = false ;
71
+
66
72
private static staticMap = new Map < string , InMemoryCache < NodeJS . ProcessEnv | undefined > > ( ) ;
73
+
67
74
private normalMap = new Map < string , InMemoryCache < NodeJS . ProcessEnv | undefined > > ( ) ;
68
75
69
- public static forceUseStatic ( ) {
76
+ public static forceUseStatic ( ) : void {
70
77
EnvironmentActivationServiceCache . useStatic = true ;
71
78
}
72
- public static forceUseNormal ( ) {
79
+
80
+ public static forceUseNormal ( ) : void {
73
81
EnvironmentActivationServiceCache . useStatic = false ;
74
82
}
83
+
75
84
public get ( key : string ) : InMemoryCache < NodeJS . ProcessEnv | undefined > | undefined {
76
85
if ( EnvironmentActivationServiceCache . useStatic ) {
77
86
return EnvironmentActivationServiceCache . staticMap . get ( key ) ;
78
87
}
79
88
return this . normalMap . get ( key ) ;
80
89
}
81
90
82
- public set ( key : string , value : InMemoryCache < NodeJS . ProcessEnv | undefined > ) {
91
+ public set ( key : string , value : InMemoryCache < NodeJS . ProcessEnv | undefined > ) : void {
83
92
if ( EnvironmentActivationServiceCache . useStatic ) {
84
93
EnvironmentActivationServiceCache . staticMap . set ( key , value ) ;
85
94
} else {
86
95
this . normalMap . set ( key , value ) ;
87
96
}
88
97
}
89
98
90
- public delete ( key : string ) {
99
+ public delete ( key : string ) : void {
91
100
if ( EnvironmentActivationServiceCache . useStatic ) {
92
101
EnvironmentActivationServiceCache . staticMap . delete ( key ) ;
93
102
} else {
94
103
this . normalMap . delete ( key ) ;
95
104
}
96
105
}
97
106
98
- public clear ( ) {
107
+ public clear ( ) : void {
99
108
// Don't clear during a test as the environment isn't going to change
100
109
if ( ! EnvironmentActivationServiceCache . useStatic ) {
101
110
this . normalMap . clear ( ) ;
@@ -110,10 +119,15 @@ export class EnvironmentActivationService
110
119
untrustedWorkspace : false ,
111
120
virtualWorkspace : false ,
112
121
} ;
122
+
113
123
private readonly disposables : IDisposable [ ] = [ ] ;
124
+
114
125
private deferred : Deferred < void > | undefined ;
115
- private previousEnvVars = process . env ;
126
+
127
+ private previousEnvVars = normCaseKeys ( process . env ) ;
128
+
116
129
private readonly activatedEnvVariablesCache = new EnvironmentActivationServiceCache ( ) ;
130
+
117
131
constructor (
118
132
@inject ( ITerminalHelper ) private readonly helper : ITerminalHelper ,
119
133
@inject ( IPlatformService ) private readonly platform : IPlatformService ,
@@ -164,6 +178,7 @@ export class EnvironmentActivationService
164
178
public dispose ( ) : void {
165
179
this . disposables . forEach ( ( d ) => d . dispose ( ) ) ;
166
180
}
181
+
167
182
@traceDecoratorVerbose ( 'getActivatedEnvironmentVariables' , TraceOptions . Arguments )
168
183
public async getActivatedEnvironmentVariables (
169
184
resource : Resource ,
@@ -203,6 +218,7 @@ export class EnvironmentActivationService
203
218
throw ex ;
204
219
} ) ;
205
220
}
221
+
206
222
public async getEnvironmentActivationShellCommands (
207
223
resource : Resource ,
208
224
interpreter ?: PythonEnvironment ,
@@ -213,18 +229,19 @@ export class EnvironmentActivationService
213
229
}
214
230
return this . helper . getEnvironmentActivationShellCommands ( resource , shellInfo . shellType , interpreter ) ;
215
231
}
232
+
216
233
public async getActivatedEnvironmentVariablesImpl (
217
234
resource : Resource ,
218
235
interpreter ?: PythonEnvironment ,
219
236
allowExceptions ?: boolean ,
220
237
) : Promise < NodeJS . ProcessEnv | undefined > {
221
238
const shellInfo = defaultShells [ this . platform . osType ] ;
222
239
if ( ! shellInfo ) {
223
- return ;
240
+ return undefined ;
224
241
}
225
242
try {
226
243
let command : string | undefined ;
227
- let [ args , parse ] = internalScripts . printEnvVariables ( ) ;
244
+ const [ args , parse ] = internalScripts . printEnvVariables ( ) ;
228
245
args . forEach ( ( arg , i ) => {
229
246
args [ i ] = arg . toCommandArgumentForPythonExt ( ) ;
230
247
} ) ;
@@ -247,10 +264,10 @@ export class EnvironmentActivationService
247
264
) ;
248
265
traceVerbose ( `Activation Commands received ${ activationCommands } for shell ${ shellInfo . shell } ` ) ;
249
266
if ( ! activationCommands || ! Array . isArray ( activationCommands ) || activationCommands . length === 0 ) {
250
- return ;
267
+ return undefined ;
251
268
}
252
269
// Run the activate command collect the environment from it.
253
- const activationCommand = this . fixActivationCommands ( activationCommands ) . join ( ' && ' ) ;
270
+ const activationCommand = fixActivationCommands ( activationCommands ) . join ( ' && ' ) ;
254
271
// In order to make sure we know where the environment output is,
255
272
// put in a dummy echo we can look for
256
273
command = `${ activationCommand } && echo '${ ENVIRONMENT_PREFIX } ' && python ${ args . join ( ' ' ) } ` ;
@@ -342,15 +359,13 @@ export class EnvironmentActivationService
342
359
throw e ;
343
360
}
344
361
}
362
+ return undefined ;
345
363
}
346
364
347
- protected fixActivationCommands ( commands : string [ ] ) : string [ ] {
348
- // Replace 'source ' with '. ' as that works in shell exec
349
- return commands . map ( ( cmd ) => cmd . replace ( / ^ s o u r c e \s + / , '. ' ) ) ;
350
- }
351
365
@traceDecoratorError ( 'Failed to parse Environment variables' )
352
366
@traceDecoratorVerbose ( 'parseEnvironmentOutput' , TraceOptions . None )
353
- protected parseEnvironmentOutput ( output : string , parse : ( out : string ) => NodeJS . ProcessEnv | undefined ) {
367
+ // eslint-disable-next-line class-methods-use-this
368
+ private parseEnvironmentOutput ( output : string , parse : ( out : string ) => NodeJS . ProcessEnv | undefined ) {
354
369
if ( output . indexOf ( ENVIRONMENT_PREFIX ) === - 1 ) {
355
370
return parse ( output ) ;
356
371
}
@@ -363,24 +378,31 @@ export class EnvironmentActivationService
363
378
const env = await this . getActivatedEnvironmentVariables ( resource ) ;
364
379
if ( ! env ) {
365
380
this . context . environmentVariableCollection . clear ( ) ;
366
- this . previousEnvVars = process . env ;
381
+ this . previousEnvVars = normCaseKeys ( process . env ) ;
367
382
return ;
368
383
}
369
384
const previousEnv = this . previousEnvVars ;
370
- Object . keys ( previousEnv ) . forEach ( ( key ) => {
371
- // If the previous env var is not in the current env, then explicitly add it so it can cleared later.
372
- if ( ! ( key in env ) ) {
373
- env [ key ] = '' ;
374
- }
375
- } ) ;
376
385
this . previousEnvVars = env ;
377
- for ( const key in env ) {
386
+ Object . keys ( env ) . forEach ( ( key ) => {
378
387
const value = env [ key ] ;
379
388
const prevValue = previousEnv [ key ] ;
380
- if ( value !== undefined && prevValue !== value ) {
381
- this . context . environmentVariableCollection . replace ( key , value ) ;
389
+ if ( prevValue !== value ) {
390
+ if ( value !== undefined ) {
391
+ traceVerbose ( `Setting environment variable ${ key } in collection to ${ value } ` ) ;
392
+ this . context . environmentVariableCollection . replace ( key , value ) ;
393
+ } else {
394
+ traceVerbose ( `Clearing environment variable ${ key } from collection` ) ;
395
+ this . context . environmentVariableCollection . delete ( key ) ;
396
+ }
382
397
}
383
- }
398
+ } ) ;
399
+ Object . keys ( previousEnv ) . forEach ( ( key ) => {
400
+ // If the previous env var is not in the current env, clear it from collection.
401
+ if ( ! ( key in env ) ) {
402
+ traceVerbose ( `Clearing environment variable ${ key } from collection` ) ;
403
+ this . context . environmentVariableCollection . delete ( key ) ;
404
+ }
405
+ } ) ;
384
406
}
385
407
386
408
@traceDecoratorVerbose ( 'Display activating terminals' )
@@ -409,3 +431,19 @@ export class EnvironmentActivationService
409
431
} ) ;
410
432
}
411
433
}
434
+
435
+ function normCaseKeys ( env : EnvironmentVariables ) : EnvironmentVariables {
436
+ const result : EnvironmentVariables = { } ;
437
+ Object . keys ( env ) . forEach ( ( key ) => {
438
+ // `os.environ` script used to get env vars normalizes keys to upper case:
439
+ // https://github.com/python/cpython/issues/101754
440
+ // So convert `process.env` keys to upper case to match.
441
+ result [ key . toUpperCase ( ) ] = env [ key ] ;
442
+ } ) ;
443
+ return result ;
444
+ }
445
+
446
+ function fixActivationCommands ( commands : string [ ] ) : string [ ] {
447
+ // Replace 'source ' with '. ' as that works in shell exec
448
+ return commands . map ( ( cmd ) => cmd . replace ( / ^ s o u r c e \s + / , '. ' ) ) ;
449
+ }
0 commit comments