Skip to content

Commit 74c3a13

Browse files
Support picking a Java process to auto attach (#799)
Signed-off-by: Jinbo Wang <[email protected]>
1 parent 874b225 commit 74c3a13

File tree

7 files changed

+354
-7
lines changed

7 files changed

+354
-7
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@ Please also check the documentation of [Language Support for Java by Red Hat](ht
7676

7777
### Attach
7878

79-
- `hostName` (required) - The host name or IP address of remote debuggee.
80-
- `port` (required) - The debug port of remote debuggee.
79+
- `hostName` (required, unless using `processId`) - The host name or IP address of remote debuggee.
80+
- `port` (required, unless using `processId`) - The debug port of remote debuggee.
81+
- `processId` - Use process picker to select a process to attach, or Process ID as integer.
82+
- `${command:pickJavaProcess}` - Use process picker to select a process to attach.
83+
- an integer pid - Attach to the specified local process.
8184
- `timeout` - Timeout value before reconnecting, in milliseconds (default to 30000ms).
8285
- `sourcePaths` - The extra source directories of the program. The debugger looks for source code from project settings by default. This option allows the debugger to look for source code in extra directories.
8386
- `projectName` - The preferred project in which the debugger searches for classes. There could be duplicated class names in different projects. It is required when the workspace has multiple java projects, otherwise the expression evaluation and conditional breakpoint may not work.

package.json

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,6 @@
333333
}
334334
},
335335
"attach": {
336-
"required": [
337-
"hostName",
338-
"port"
339-
],
340336
"properties": {
341337
"hostName": {
342338
"type": "string",
@@ -347,6 +343,21 @@
347343
"type": "number",
348344
"description": "%java.debugger.attach.port.description%"
349345
},
346+
"processId": {
347+
"anyOf": [
348+
{
349+
"enum": [
350+
"${command:PickJavaProcess}"
351+
],
352+
"description": "%java.debugger.attach.processPicker.description%",
353+
"default": "${command:pickJavaProcess}"
354+
},
355+
{
356+
"type": "integer",
357+
"description": "%java.debugger.attach.processId.description%"
358+
}
359+
]
360+
},
350361
"timeout": {
351362
"type": "number",
352363
"default": 30000,
@@ -471,6 +482,16 @@
471482
"port": "<debug port of the debuggee>"
472483
}
473484
},
485+
{
486+
"label": "Java: Attach to Process",
487+
"description": "%java.debugger.snippet.attachProcess.description%",
488+
"body": {
489+
"type": "java",
490+
"request": "attach",
491+
"name": "Attach by Process ID",
492+
"processId": "^\"\\${command:PickJavaProcess}\""
493+
}
494+
},
474495
{
475496
"label": "Java: Attach to Remote Program",
476497
"description": "%java.debugger.snippet.attachRemote.description%",

package.nls.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
"java.debugger.launch.skipConstructors.description": "Skip constructor methods when stepping.",
2727
"java.debugger.attach.hostName.description": "The host name or ip address of remote debuggee.",
2828
"java.debugger.attach.port.description": "The debug port of remote debuggee.",
29+
"java.debugger.attach.processPicker.description": "Use process picker to select a process to attach, or Process ID as integer.",
30+
"java.debugger.attach.processId.description": "ID of the local process to attach to.",
2931
"java.debugger.attach.timeout.description": "Timeout value before reconnecting, in milliseconds (default to 30000ms).",
3032
"java.debugger.attach.projectName.description": "The preferred project in which the debugger searches for classes. There could be duplicated class names in different projects.",
3133
"java.debugger.snippet.launch.description": "Add a new configuration for launching a java program.",
3234
"java.debugger.snippet.launchInExternalTerminal.description": "Add a new configuration for launching a java program in the external terminal.",
3335
"java.debugger.snippet.launchCurrentFile.description": "Add a new configuration for launching current java file.",
3436
"java.debugger.snippet.launchWithArgumentsPrompt.description": "Add a new configuration for launching a java program with arguments prompt.",
3537
"java.debugger.snippet.attach.description": "Add a new configuration for attaching to a running java program.",
38+
"java.debugger.snippet.attachProcess.description": "Use process picker to select a Java process to attach to.",
3639
"java.debugger.snippet.attachRemote.description": "Add a new configuration for attaching to a remote java program.",
3740
"java.debugger.configuration.title": "Java Debugger",
3841
"java.debugger.configuration.logLevel.description": "Minimum level of debugger logs that are sent to VS Code.",

package.nls.zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
"java.debugger.launch.skipConstructors.description": "Step时跳过构造函数。",
2727
"java.debugger.attach.hostName.description": "远程调试进程所在的主机名或IP地址。",
2828
"java.debugger.attach.port.description": "远程调试进程的调试端口。",
29+
"java.debugger.attach.processPicker.description": "使用进程选取器选择要附加的进程,或直接配置进程ID。",
30+
"java.debugger.attach.processId.description": "要附加到的本地进程的ID。",
2931
"java.debugger.attach.timeout.description": "重新连接前的超时值,以毫秒为单位(默认为30000ms)。",
3032
"java.debugger.attach.projectName.description": "调试器搜索类的首选工程。不同工程中可能存在重复的类名。",
3133
"java.debugger.snippet.launch.description": "启动java程序。",
3234
"java.debugger.snippet.launchInExternalTerminal.description": "在外部终端中启动java程序。",
3335
"java.debugger.snippet.launchCurrentFile.description": "启动当前java文件中的程序。",
3436
"java.debugger.snippet.launchWithArgumentsPrompt.description": "启动java程序时动态提示命令行参数。",
3537
"java.debugger.snippet.attach.description": "附加到正在运行的java程序。",
38+
"java.debugger.snippet.attachProcess.description": "使用进程选择器选择要附加的Java进程。",
3639
"java.debugger.snippet.attachRemote.description": "附加到远程java程序。",
3740
"java.debugger.configuration.title": "Java调试器",
3841
"java.debugger.configuration.logLevel.description": "Java调试器的日志级别。",

src/configurationProvider.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as commands from "./commands";
1313
import * as lsPlugin from "./languageServerPlugin";
1414
import { detectLaunchCommandStyle, validateRuntime } from "./launchCommand";
1515
import { logger, Type } from "./logger";
16+
import { resolveProcessId } from "./processPicker";
1617
import * as utility from "./utility";
1718
import { VariableResolver } from "./variableResolver";
1819

@@ -229,7 +230,16 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration
229230
config.launcherScript = utility.getLauncherScriptPath();
230231
}
231232
} else if (config.request === "attach") {
232-
if (!config.hostName || !config.port) {
233+
if (config.processId !== undefined) {
234+
try {
235+
if (!(await resolveProcessId(config))) {
236+
return undefined;
237+
}
238+
} catch (error) {
239+
vscode.window.showErrorMessage(String(error));
240+
return undefined;
241+
}
242+
} else if (!config.hostName || !config.port) {
233243
throw new utility.UserError({
234244
message: "Please specify the host name and the port of the remote debuggee in the launch.json.",
235245
type: Type.USAGEERROR,

src/processPicker.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as path from "path";
5+
import { DebugConfiguration, window } from "vscode";
6+
import { getProcesses, getProcessTree } from "./processTree";
7+
8+
const JAVA_PATTERN = /(?:java|javaw|j9|j9w)$/i;
9+
const DEBUG_MODE_PATTERN = /(-agentlib|-Xrunjdwp):\S*(address=[^\s,]+)/i;
10+
11+
interface IJavaProcess {
12+
pid: number;
13+
command: string;
14+
args: string;
15+
hostName: string;
16+
debugPort: number;
17+
}
18+
19+
export async function resolveProcessId(config: DebugConfiguration): Promise<boolean> {
20+
let javaProcess;
21+
// tslint:disable-next-line
22+
if (!config.processId || config.processId === "${command:PickJavaProcess}") {
23+
javaProcess = await pickJavaProcess();
24+
} else {
25+
javaProcess = await resolveJavaProcess(parseInt(String(config.processId), 10));
26+
if (!javaProcess) {
27+
throw new Error(`Attach to process: pid '${config.processId}' doesn't look like a debuggable Java process. `
28+
+ `Please ensure the process has enabled debug mode with vmArgs like `
29+
+ `'-agentlib:jdwp=transport=dt_socket,server=y,address=5005.'`);
30+
}
31+
}
32+
33+
if (javaProcess) {
34+
config.processId = undefined;
35+
config.hostName = javaProcess.hostName;
36+
config.port = javaProcess.debugPort;
37+
}
38+
39+
return !!javaProcess;
40+
}
41+
42+
function convertToJavaProcess(pid: number, command: string, args: string): IJavaProcess | undefined {
43+
if (process.platform === "win32" && command.indexOf("\\??\\") === 0) {
44+
// remove leading device specifier
45+
command = command.replace("\\??\\", "");
46+
}
47+
48+
const simpleName = path.basename(command, ".exe");
49+
if (JAVA_PATTERN.test(simpleName) && args) {
50+
const match = args.match(DEBUG_MODE_PATTERN);
51+
if (match && match.length) {
52+
const address = match[2].split("=")[1].split(":");
53+
const hostName = address.length > 1 ? address[0] : "127.0.0.1";
54+
const debugPort = parseInt(address[address.length - 1], 10);
55+
const exeName = path.basename(command);
56+
const binPath = path.dirname(command);
57+
const commandPath = path.basename(binPath) === "bin" ?
58+
path.join(path.basename(path.dirname(binPath)), "bin", exeName) : exeName;
59+
return {
60+
pid,
61+
command: commandPath,
62+
args,
63+
hostName,
64+
debugPort,
65+
};
66+
}
67+
}
68+
}
69+
70+
async function pickJavaProcess(): Promise<IJavaProcess> {
71+
try {
72+
const javaProcesses: IJavaProcess[] = [];
73+
await getProcesses((pid: number, ppid: number, command: string, args: string, date: number) => {
74+
const javaProcess = convertToJavaProcess(pid, command, args);
75+
if (javaProcess) {
76+
javaProcesses.push(javaProcess);
77+
}
78+
});
79+
80+
if (!javaProcesses.length) {
81+
throw new Error("Process picker: No debuggable Java process found. Please ensure enable debugging for "
82+
+ "your application with vmArgs like '-agentlib:jdwp=transport=dt_socket,server=y,address=5005'.");
83+
}
84+
85+
const items = javaProcesses.map((process) => {
86+
return {
87+
label: process.command,
88+
description: process.args,
89+
detail: `process id: ${process.pid}, debug port: ${process.debugPort}`,
90+
process,
91+
};
92+
});
93+
94+
const pick = await window.showQuickPick(items, {
95+
placeHolder: "Pick Java process to attach to",
96+
});
97+
98+
if (pick) {
99+
return pick.process;
100+
}
101+
} catch (error) {
102+
throw new Error("Process picker failed: " + error);
103+
}
104+
}
105+
106+
async function resolveJavaProcess(pid: number): Promise<IJavaProcess | undefined> {
107+
const processTree = await getProcessTree(pid);
108+
if (!processTree || processTree.pid !== pid) {
109+
return undefined;
110+
}
111+
112+
return convertToJavaProcess(processTree.pid, processTree.command, processTree.args);
113+
}

0 commit comments

Comments
 (0)