forked from microsoft/vscode-python-debugger
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfactory.ts
More file actions
220 lines (197 loc) · 10.3 KB
/
factory.ts
File metadata and controls
220 lines (197 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/* eslint-disable @typescript-eslint/naming-convention */
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import * as path from 'path';
import {
DebugAdapterDescriptor,
DebugAdapterExecutable,
DebugAdapterServer,
DebugSession,
l10n,
WorkspaceFolder,
} from 'vscode';
import { AttachRequestArguments, LaunchRequestArguments } from '../../types';
import { IDebugAdapterDescriptorFactory } from '../types';
import { executeCommand, showErrorMessage } from '../../common/vscodeapi';
import { traceLog, traceVerbose } from '../../common/log/logging';
import { EventName } from '../../telemetry/constants';
import { sendTelemetryEvent } from '../../telemetry';
import { Commands, EXTENSION_ROOT_DIR } from '../../common/constants';
import { Common, DebugConfigStrings, Interpreters } from '../../common/utils/localize';
import { IPersistentStateFactory } from '../../common/types';
import { fileToCommandArgumentForPythonExt } from '../../common/stringUtils';
import { PythonEnvironment } from '../../envExtApi';
import { resolveEnvironment, getInterpreterDetails, runPythonExtensionCommand } from '../../common/python';
// persistent state names, exported to make use of in testing
export enum debugStateKeys {
doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain',
}
export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory {
constructor(private persistentState: IPersistentStateFactory) {}
public async createDebugAdapterDescriptor(
session: DebugSession,
_executable: DebugAdapterExecutable | undefined,
): Promise<DebugAdapterDescriptor | undefined> {
traceLog(`createDebugAdapterDescriptor: request='${session.configuration.request}' name='${session.name}'`);
const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments;
// There are four distinct scenarios here:
//
// 1. "launch";
// 2. "attach" with "processId";
// 3. "attach" with "listen";
// 4. "attach" with "connect" (or legacy "host"/"port");
//
// For the first three, we want to spawn the debug adapter directly.
// For the last one, the adapter is already listening on the specified socket.
if (configuration.request === 'attach') {
if (configuration.connect !== undefined) {
traceLog(
`Connecting to DAP Server at: ${configuration.connect.host ?? '127.0.0.1'}:${
configuration.connect.port
}`,
);
return new DebugAdapterServer(
Number(configuration.connect.port),
configuration.connect.host ?? '127.0.0.1',
);
} else if (configuration.port !== undefined) {
traceLog(`Connecting to DAP Server at: ${configuration.host ?? '127.0.0.1'}:${configuration.port}`);
return new DebugAdapterServer(Number(configuration.port), configuration.host ?? '127.0.0.1');
} else if (configuration.listen === undefined && configuration.processId === undefined) {
throw new Error('"request":"attach" requires either "connect", "listen", or "processId"');
}
traceLog('createDebugAdapterDescriptor: attach scenario using spawned adapter');
}
const command = await this.getDebugAdapterPython(configuration, session.workspaceFolder);
traceLog(`createDebugAdapterDescriptor: python command parts='${command.join(' ')}'`);
if (command.length !== 0) {
if (configuration.request === 'attach' && configuration.processId !== undefined) {
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
}
let executable = command.shift() ?? 'python';
// Always ensure interpreter/command is quoted if necessary. Previously this was
// only done in the debugAdapterPath branch which meant that in the common case
// (using the built‑in adapter path) an interpreter path containing spaces would
// be passed unquoted, resulting in a fork/spawn failure on Windows. See bug
// report for details.
executable = fileToCommandArgumentForPythonExt(executable);
// "logToFile" is not handled directly by the adapter - instead, we need to pass
// the corresponding CLI switch when spawning it.
const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : [];
if (configuration.debugAdapterPath !== undefined) {
const args = command.concat([configuration.debugAdapterPath, ...logArgs]);
traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`);
return new DebugAdapterExecutable(executable, args);
}
const debuggerAdapterPathToUse = path.join(EXTENSION_ROOT_DIR, 'bundled', 'libs', 'debugpy', 'adapter');
const args = command.concat([debuggerAdapterPathToUse, ...logArgs]);
traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`);
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true });
return new DebugAdapterExecutable(executable, args);
} else {
throw new Error(DebugConfigStrings.debugStopped);
}
}
/**
* Get the python executable used to launch the Python Debug Adapter.
* In the case of `attach` scenarios, just use the workspace interpreter.
* It is unlike user won't have a Python interpreter
*
* @private
* @param {(LaunchRequestArguments | AttachRequestArguments)} configuration
* @param {WorkspaceFolder} [workspaceFolder]
* @returns {Promise<string>} Path to the python interpreter for this workspace.
* @memberof DebugAdapterDescriptorFactory
*/
private async getDebugAdapterPython(
configuration: LaunchRequestArguments | AttachRequestArguments,
workspaceFolder?: WorkspaceFolder,
): Promise<string[]> {
traceVerbose('getDebugAdapterPython: Resolving interpreter for debug adapter');
if (configuration.debugAdapterPython !== undefined) {
return this.getExecutableCommand(await resolveEnvironment(configuration.debugAdapterPython));
} else if (configuration.pythonPath) {
return this.getExecutableCommand(await resolveEnvironment(configuration.pythonPath));
}
const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined;
const interpreter = await getInterpreterDetails(resourceUri);
if (interpreter?.path) {
traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path[0]}'`);
return this.getExecutableCommand(await resolveEnvironment(interpreter.path[0]));
}
const prompts = [Interpreters.changePythonInterpreter];
const selection = await showErrorMessage(
l10n.t(
'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Interpreter" in the status bar.',
),
{ modal: true },
...prompts,
);
if (selection === Interpreters.changePythonInterpreter) {
await executeCommand(Commands.Set_Interpreter);
const interpreter = await getInterpreterDetails(resourceUri);
if (interpreter?.path) {
traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path[0]}'`);
return this.getExecutableCommand(await resolveEnvironment(interpreter.path[0]));
}
}
return [];
}
private async showDeprecatedPythonMessage() {
sendTelemetryEvent(EventName.DEBUGGER_PYTHON_37_DEPRECATED);
const notificationPromptEnabled = this.persistentState.createGlobalPersistentState(
debugStateKeys.doNotShowAgain,
false,
);
if (notificationPromptEnabled.value) {
return;
}
const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain];
const selection = await showErrorMessage(
l10n.t('The minimum supported Python version for the debugger extension is 3.9.'),
{ modal: true },
...prompts,
);
if (!selection) {
return;
}
if (selection === Interpreters.changePythonInterpreter) {
await runPythonExtensionCommand(Commands.Set_Interpreter);
}
if (selection === Common.doNotShowAgain) {
// Never show the message again
await this.persistentState
.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false)
.updateValue(true);
}
}
/**
* Extracts the executable command from a resolved Python environment.
*
* This function takes a resolved Python environment and returns the path to the Python
* executable as a string array suitable for spawning processes. It also performs version
* validation, showing a deprecation warning if the Python version is below 3.9.
*
* @param interpreter The resolved Python environment containing executable path and version info
* @returns Promise resolving to an array containing the Python executable path, or empty array if no interpreter
*/
private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise<string[]> {
if (interpreter) {
const executablePath = interpreter.execInfo.run.executable;
const version = interpreter.version;
// Parse version string (e.g., "3.8.10" -> major: 3, minor: 8)
const parseMajorMinor = (v: string) => {
const m = v.match(/^(\d+)(?:\.(\d+))?/);
return { major: m && m[1] ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 };
};
const { major, minor } = parseMajorMinor(version || '');
if (major < 3 || (major <= 3 && minor < 9)) {
this.showDeprecatedPythonMessage();
}
traceLog(`getExecutableCommand: executable='${executablePath}' version='${version}'`);
return executablePath ? [executablePath] : [];
}
return [];
}
}