Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions src/extension/debugger/configuration/resolvers/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,76 @@

'use strict';

import * as path from 'path';
import { PYTHON_LANGUAGE } from '../../../common/constants';
import { getSearchPathEnvVarNames } from '../../../common/utils/exec';
import { EnvironmentVariables } from '../../../common/variables/types';
import { getActiveTextEditor } from '../../../common/vscodeapi';
import { LaunchRequestArguments } from '../../../types';
import * as envParser from '../../../common/variables/environment';

function getSearchPathEnvVar(vars: EnvironmentVariables, names: ('Path' | 'PATH')[]): string | undefined {
return names.map((name) => vars[name]).find((value) => typeof value === 'string' && value.length > 0);
}

function normalizeSearchPathEnvVar(env: EnvironmentVariables, names: ('Path' | 'PATH')[]) {
const [pathVariableName, ...alternatePathVariableNames] = names;
for (const alternatePathVariableName of alternatePathVariableNames) {
const alternateValue = env[alternatePathVariableName];
if (typeof alternateValue !== 'string' || alternateValue.length === 0) {
delete env[alternatePathVariableName];
continue;
}

const currentValue = env[pathVariableName];
if (typeof currentValue === 'string' && currentValue.length > 0) {
const currentSegments = new Set(currentValue.split(path.delimiter).filter((item) => item.length > 0));
const alternateSegments = alternateValue
.split(path.delimiter)
.filter((item) => item.length > 0 && !currentSegments.has(item));
if (alternateSegments.length > 0) {
env[pathVariableName] = [currentValue, ...alternateSegments].join(path.delimiter);
}
} else {
env[pathVariableName] = alternateValue;
}
delete env[alternatePathVariableName];
}
}

export async function getDebugEnvironmentVariables(args: LaunchRequestArguments): Promise<EnvironmentVariables> {
const pathVariableName = getSearchPathEnvVarNames()[0];
const pathVariableNames = getSearchPathEnvVarNames();
const pathVariableName = pathVariableNames[0];

// Merge variables from both .env file and env json variables.
const debugLaunchEnvVars: Record<string, string> =
args.env && Object.keys(args.env).length > 0
? ({ ...args.env } as Record<string, string>)
: ({} as Record<string, string>);
const envFileVars = await envParser.parseFile(args.envFile, debugLaunchEnvVars);
normalizeSearchPathEnvVar(debugLaunchEnvVars, pathVariableNames);
const env = envFileVars ? { ...envFileVars } : {};
normalizeSearchPathEnvVar(env, pathVariableNames);

// "overwrite: true" to ensure that debug-configuration env variable values
// take precedence over env file.
envParser.mergeVariables(debugLaunchEnvVars, env, { overwrite: true });
normalizeSearchPathEnvVar(env, pathVariableNames);

// Append the PYTHONPATH and PATH variables.
envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]);
const debugLaunchSearchPath = getSearchPathEnvVar(debugLaunchEnvVars, pathVariableNames);
if (debugLaunchSearchPath) {
envParser.appendPath(env, debugLaunchSearchPath);
}
envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH);

if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) {
// Now merge this path with the current system path.
// We need to do this to ensure the PATH variable always has the system PATHs as well.
envParser.appendPath(env, process.env[pathVariableName]!);
const processSearchPath = getSearchPathEnvVar(process.env, pathVariableNames);
if (processSearchPath) {
envParser.appendPath(env, processSearchPath);
}
}
if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) {
// We didn't have a value for PATH earlier and now we do.
Expand All @@ -47,14 +87,17 @@ export async function getDebugEnvironmentVariables(args: LaunchRequestArguments)
// As we're spawning the process, we need to ensure all env variables are passed.
// Including those from the current process (i.e. everything, not just custom vars).
envParser.mergeVariables(process.env, env);
normalizeSearchPathEnvVar(env, pathVariableNames);

if (env[pathVariableName] === undefined && typeof process.env[pathVariableName] === 'string') {
env[pathVariableName] = process.env[pathVariableName];
const processSearchPath = getSearchPathEnvVar(process.env, pathVariableNames);
if (env[pathVariableName] === undefined && typeof processSearchPath === 'string') {
env[pathVariableName] = processSearchPath;
}
if (env.PYTHONPATH === undefined && typeof process.env.PYTHONPATH === 'string') {
env.PYTHONPATH = process.env.PYTHONPATH;
}
}
normalizeSearchPathEnvVar(env, pathVariableNames);

if (!env.hasOwnProperty('PYTHONIOENCODING')) {
env.PYTHONIOENCODING = 'UTF-8';
Expand Down
45 changes: 44 additions & 1 deletion src/test/unittest/configuration/resolvers/helper.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import * as sinon from 'sinon';
import * as typemoq from 'typemoq';
import { TextDocument, TextEditor } from 'vscode';
import { PYTHON_LANGUAGE } from '../../../../extension/common/constants';
import * as platform from '../../../../extension/common/platform';
import * as vscodeapi from '../../../../extension/common/vscodeapi';
import { getProgram } from '../../../../extension/debugger/configuration/resolvers/helper';
import {
getDebugEnvironmentVariables,
getProgram,
} from '../../../../extension/debugger/configuration/resolvers/helper';
import { LaunchRequestArguments } from '../../../../extension/types';

suite('Debugging - Helpers', () => {
let getActiveTextEditorStub: sinon.SinonStub;
Expand Down Expand Up @@ -67,4 +72,42 @@ suite('Debugging - Helpers', () => {

expect(program).to.be.equal(undefined, 'Not undefined');
});

test('Debug environment should not include duplicate Windows search path keys', async () => {
sinon.stub(platform, 'getOSType').returns(platform.OSType.Windows);

const originalPath = process.env.Path;
const originalPATH = process.env.PATH;
process.env.Path = 'C:\\Windows\\System32';
process.env.PATH = 'C:\\Tools';

try {
const launchEnv = { ['PATH']: 'C:\\Project\\.venv\\Scripts' };
const env = await getDebugEnvironmentVariables({
name: 'Python Debug Test',
type: 'debugpy',
request: 'launch',
env: launchEnv,
console: 'internalConsole',
} as unknown as LaunchRequestArguments);

expect(env).to.have.property('Path');
expect(env).not.to.have.property('PATH');
expect(env.Path).to.contain('C:\\Project\\.venv\\Scripts');
expect(env.Path).to.contain('C:\\Tools');
expect(env.Path!.split(';').filter((item) => item === 'C:\\Project\\.venv\\Scripts')).to.have.lengthOf(1);
expect(env.Path!.split(';').filter((item) => item === 'C:\\Tools')).to.have.lengthOf(1);
} finally {
if (originalPath === undefined) {
delete process.env.Path;
} else {
process.env.Path = originalPath;
}
if (originalPATH === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = originalPATH;
}
}
});
});