Skip to content

Commit 9005608

Browse files
karthiknadigPavel Minaev
and
Pavel Minaev
authored
Fix #10437: Update launch.json handling to support "listen" and "connect" (#10517) (#10654)
Co-authored-by: Pavel Minaev <[email protected]>
1 parent 31cd1f2 commit 9005608

File tree

4 files changed

+118
-53
lines changed

4 files changed

+118
-53
lines changed

news/1 Enhancements/10437.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support reverse connection ("listen" in launch.json) from debug adapter to VSCode.

package.json

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,15 +1363,46 @@
13631363
},
13641364
"attach": {
13651365
"properties": {
1366+
"connect": {
1367+
"type": "object",
1368+
"label": "Attach by connecting to debugpy over a socket.",
1369+
"properties": {
1370+
"port": {
1371+
"type": "number",
1372+
"description": "Port to connect to."
1373+
},
1374+
"host": {
1375+
"type": "string",
1376+
"description": "Hostname or IP address to connect to.",
1377+
"default": "127.0.0.1"
1378+
}
1379+
},
1380+
"required": ["port"]
1381+
},
1382+
"listen": {
1383+
"type": "object",
1384+
"label": "Attach by listening for incoming socket connection from debugpy",
1385+
"properties": {
1386+
"port": {
1387+
"type": "number",
1388+
"description": "Port to listen on."
1389+
},
1390+
"host": {
1391+
"type": "string",
1392+
"description": "Hostname or IP address of the interface to listen on.",
1393+
"default": "127.0.0.1"
1394+
}
1395+
},
1396+
"required": ["port"]
1397+
},
13661398
"port": {
13671399
"type": "number",
1368-
"description": "Debug port to attach",
1369-
"default": 0
1400+
"description": "Port to connect to."
13701401
},
13711402
"host": {
13721403
"type": "string",
1373-
"description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).",
1374-
"default": "localhost"
1404+
"description": "Hostname or IP address to connect to.",
1405+
"default": "127.0.0.1"
13751406
},
13761407
"pathMappings": {
13771408
"type": "array",

src/client/debugger/extension/adapter/factory.ts

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,48 +38,60 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
3838
const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments;
3939

4040
if (this.experimentsManager.inExperiment(DebugAdapterNewPtvsd.experiment)) {
41-
const isAttach = configuration.request === 'attach';
42-
const port = configuration.port ?? 0;
43-
// When processId is provided we may have to inject the debugger into the process.
44-
// This is done by the debug adapter, so we need to start it. The adapter will handle injecting the debugger when it receives the attach request.
45-
const processId = configuration.processId ?? 0;
46-
47-
if (isAttach && processId === 0) {
48-
if (port === 0) {
49-
throw new Error('Port or processId must be specified for request type attach');
50-
} else {
51-
return new DebugAdapterServer(port, configuration.host);
41+
// There are four distinct scenarios here:
42+
//
43+
// 1. "launch";
44+
// 2. "attach" with "processId";
45+
// 3. "attach" with "listen";
46+
// 4. "attach" with "connect" (or legacy "host"/"port");
47+
//
48+
// For the first three, we want to spawn the debug adapter directly.
49+
// For the last one, the adapter is already listening on the specified socket.
50+
// When "debugServer" is used, the standard adapter factory takes care of it - no need to check here.
51+
52+
if (configuration.request === 'attach') {
53+
if (configuration.connect !== undefined) {
54+
return new DebugAdapterServer(
55+
configuration.connect.port,
56+
configuration.connect.host ?? '127.0.0.1'
57+
);
58+
} else if (configuration.port !== undefined) {
59+
return new DebugAdapterServer(configuration.port, configuration.host ?? '127.0.0.1');
60+
} else if (configuration.listen === undefined && configuration.processId === undefined) {
61+
throw new Error('"request":"attach" requires either "connect", "listen", or "processId"');
5262
}
53-
} else {
54-
const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder);
55-
// If logToFile is set in the debug config then pass --log-dir <path-to-extension-dir> when launching the debug adapter.
63+
}
64+
65+
const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder);
66+
if (pythonPath.length !== 0) {
67+
if (configuration.request === 'attach' && configuration.processId !== undefined) {
68+
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
69+
}
70+
71+
// "logToFile" is not handled directly by the adapter - instead, we need to pass
72+
// the corresponding CLI switch when spawning it.
5673
const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : [];
74+
75+
if (configuration.debugAdapterPath !== undefined) {
76+
return new DebugAdapterExecutable(pythonPath, [configuration.debugAdapterPath, ...logArgs]);
77+
}
78+
5779
const debuggerPathToUse = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy');
5880

59-
if (pythonPath.length !== 0) {
60-
if (processId) {
61-
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
62-
}
63-
64-
if (configuration.debugAdapterPath) {
65-
return new DebugAdapterExecutable(pythonPath, [configuration.debugAdapterPath, ...logArgs]);
66-
}
67-
68-
if (await this.useNewDebugger(pythonPath)) {
69-
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true });
70-
return new DebugAdapterExecutable(pythonPath, [
71-
path.join(debuggerPathToUse, 'wheels', 'debugpy', 'adapter'),
72-
...logArgs
73-
]);
74-
} else {
75-
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, {
76-
usingWheels: false
77-
});
78-
return new DebugAdapterExecutable(pythonPath, [
79-
path.join(debuggerPathToUse, 'no_wheels', 'debugpy', 'adapter'),
80-
...logArgs
81-
]);
82-
}
81+
if (await this.useNewDebugger(pythonPath)) {
82+
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true });
83+
return new DebugAdapterExecutable(pythonPath, [
84+
path.join(debuggerPathToUse, 'wheels', 'debugpy', 'adapter'),
85+
...logArgs
86+
]);
87+
} else {
88+
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, {
89+
usingWheels: false
90+
});
91+
return new DebugAdapterExecutable(pythonPath, [
92+
path.join(debuggerPathToUse, 'no_wheels', 'debugpy', 'adapter'),
93+
...logArgs
94+
]);
8395
}
8496
}
8597
} else {

src/test/debugger/extension/adapter/factory.unit.test.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,37 +206,58 @@ suite('Debugging - Adapter Factory', () => {
206206
assert.deepEqual(descriptor, nodeExecutable);
207207
});
208208

209-
test('Return Debug Adapter server if in DA experiment, configuration is attach and port is specified', async () => {
209+
test('Return Debug Adapter server if in DA experiment, request is "attach", and port is specified directly', async () => {
210210
const session = createSession({ request: 'attach', port: 5678, host: 'localhost' });
211211
const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host);
212212

213213
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
214214
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
215215

216-
// Interpreter not needed for attach
216+
// Interpreter not needed for host/port
217217
verify(interpreterService.getInterpreters(anything())).never();
218218
assert.deepEqual(descriptor, debugServer);
219219
});
220220

221-
test('Throw error if in DA experiment, configuration is attach, port is 0 and process ID is not specified', async () => {
222-
const session = createSession({ request: 'attach', port: 0, host: 'localhost' });
221+
test('Return Debug Adapter server if in DA experiment, request is "attach", and connect is specified', async () => {
222+
const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } });
223+
const debugServer = new DebugAdapterServer(
224+
session.configuration.connect.port,
225+
session.configuration.connect.host
226+
);
223227

224228
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
225-
const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable);
229+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
226230

227-
await expect(promise).to.eventually.be.rejectedWith(
228-
'Port or processId must be specified for request type attach'
229-
);
231+
// Interpreter not needed for connect
232+
verify(interpreterService.getInterpreters(anything())).never();
233+
assert.deepEqual(descriptor, debugServer);
234+
});
235+
236+
test('Return Debug Adapter executable if in DA experiment, request is "attach", and listen is specified', async () => {
237+
const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } });
238+
const debugExecutable = new DebugAdapterExecutable(pythonPath, [ptvsdAdapterPathWithWheels]);
239+
240+
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
241+
when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter);
242+
243+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
244+
assert.deepEqual(descriptor, debugExecutable);
230245
});
231246

232-
test('Throw error if in DA experiment, configuration is attach and port and process ID are not specified', async () => {
233-
const session = createSession({ request: 'attach', port: undefined, processId: undefined });
247+
test('Throw error if in DA experiment, request is "attach", and neither port, processId, listen, nor connect is specified', async () => {
248+
const session = createSession({
249+
request: 'attach',
250+
port: undefined,
251+
processId: undefined,
252+
listen: undefined,
253+
connect: undefined
254+
});
234255

235256
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
236257
const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable);
237258

238259
await expect(promise).to.eventually.be.rejectedWith(
239-
'Port or processId must be specified for request type attach'
260+
'"request":"attach" requires either "connect", "listen", or "processId"'
240261
);
241262
});
242263

0 commit comments

Comments
 (0)