Skip to content

Commit e3cf474

Browse files
Capture Java process pid and display debug toolbar when running Java in terminal (microsoft#413)
* Capture Java process pid and display debug toolbar when running Java in terminal
1 parent 625fe84 commit e3cf474

File tree

4 files changed

+222
-9
lines changed

4 files changed

+222
-9
lines changed

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018 Microsoft Corporation and others.
2+
* Copyright (c) 2018-2022 Microsoft Corporation and others.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -11,6 +11,8 @@
1111

1212
package com.microsoft.java.debug.core.adapter.handler;
1313

14+
import java.util.Optional;
15+
1416
import com.microsoft.java.debug.core.adapter.IDebugAdapterContext;
1517
import com.microsoft.java.debug.core.protocol.Messages.Response;
1618
import com.microsoft.java.debug.core.protocol.Requests.Arguments;
@@ -25,6 +27,11 @@ public void destroyDebugSession(Command command, Arguments arguments, Response r
2527
Process debuggeeProcess = context.getDebuggeeProcess();
2628
if (debuggeeProcess != null && disconnectArguments.terminateDebuggee) {
2729
debuggeeProcess.destroy();
30+
} else if (context.getProcessId() > 0 && disconnectArguments.terminateDebuggee) {
31+
Optional<ProcessHandle> debuggeeHandle = ProcessHandle.of(context.getProcessId());
32+
if (debuggeeHandle.isPresent()) {
33+
debuggeeHandle.get().destroy();
34+
}
2835
}
2936
}
3037
}

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchUtils.java

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2021 Microsoft Corporation and others.
2+
* Copyright (c) 2021-2022 Microsoft Corporation and others.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -11,28 +11,39 @@
1111

1212
package com.microsoft.java.debug.core.adapter.handler;
1313

14+
import java.io.BufferedReader;
1415
import java.io.File;
1516
import java.io.FileOutputStream;
1617
import java.io.IOException;
18+
import java.io.InputStreamReader;
1719
import java.math.BigInteger;
1820
import java.nio.file.Files;
1921
import java.nio.file.Path;
22+
import java.nio.file.Paths;
2023
import java.security.MessageDigest;
2124
import java.security.NoSuchAlgorithmException;
2225
import java.util.ArrayList;
2326
import java.util.HashSet;
2427
import java.util.List;
28+
import java.util.Objects;
29+
import java.util.Optional;
2530
import java.util.Set;
2631
import java.util.UUID;
2732
import java.util.jar.Attributes;
2833
import java.util.jar.JarOutputStream;
2934
import java.util.jar.Manifest;
35+
import java.util.logging.Level;
36+
import java.util.logging.Logger;
37+
import java.util.stream.Collectors;
3038

39+
import com.microsoft.java.debug.core.Configuration;
3140
import com.microsoft.java.debug.core.adapter.AdapterUtils;
3241

3342
import org.apache.commons.lang3.ArrayUtils;
43+
import org.apache.commons.lang3.SystemUtils;
3444

3545
public class LaunchUtils {
46+
private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME);
3647
private static Set<Path> tempFilesInUse = new HashSet<>();
3748

3849
/**
@@ -102,6 +113,171 @@ public static void releaseTempLaunchFile(Path tempFile) {
102113
}
103114
}
104115

116+
public static ProcessHandle findJavaProcessInTerminalShell(long shellPid, String javaCommand, int timeout/*ms*/) {
117+
ProcessHandle shellProcess = ProcessHandle.of(shellPid).orElse(null);
118+
if (shellProcess != null) {
119+
int retry = 0;
120+
final int INTERVAL = 100;
121+
final int maxRetries = timeout / INTERVAL;
122+
final boolean isCygwinShell = isCygwinShell(shellProcess.info().command().orElse(null));
123+
while (retry <= maxRetries) {
124+
Optional<ProcessHandle> subProcessHandle = shellProcess.descendants().filter(proc -> {
125+
String command = proc.info().command().orElse("");
126+
return Objects.equals(command, javaCommand) || command.endsWith("\\java.exe") || command.endsWith("/java");
127+
}).findFirst();
128+
129+
if (subProcessHandle.isPresent()) {
130+
logger.info("shellPid: " + shellPid + ", javaPid: " + subProcessHandle.get().pid());
131+
return subProcessHandle.get();
132+
} else if (isCygwinShell) {
133+
long javaPid = findJavaProcessByCygwinPsCommand(shellProcess, javaCommand);
134+
if (javaPid > 0) {
135+
logger.info("[Cygwin Shell] shellPid: " + shellPid + ", javaPid: " + javaPid);
136+
return ProcessHandle.of(javaPid).orElse(null);
137+
}
138+
}
139+
140+
retry++;
141+
if (retry > maxRetries) {
142+
break;
143+
}
144+
145+
try {
146+
Thread.sleep(INTERVAL);
147+
} catch (InterruptedException e) {
148+
// do nothing
149+
}
150+
logger.info("Retry to find Java subProcess of shell pid " + shellPid);
151+
}
152+
}
153+
154+
return null;
155+
}
156+
157+
private static long findJavaProcessByCygwinPsCommand(ProcessHandle shellProcess, String javaCommand) {
158+
String psCommand = detectPsCommandPath(shellProcess.info().command().orElse(null));
159+
if (psCommand == null) {
160+
return -1;
161+
}
162+
163+
BufferedReader psReader = null;
164+
List<PsProcess> psProcs = new ArrayList<>();
165+
List<PsProcess> javaCandidates = new ArrayList<>();
166+
try {
167+
String[] headers = null;
168+
int pidIndex = -1;
169+
int ppidIndex = -1;
170+
int winpidIndex = -1;
171+
String line;
172+
String javaExeName = Paths.get(javaCommand).toFile().getName().replaceFirst("\\.exe$", "");
173+
174+
Process p = Runtime.getRuntime().exec(new String[] {psCommand, "-l"});
175+
psReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
176+
/**
177+
* Here is a sample output when running ps command in Cygwin/MINGW64 shell.
178+
* PID PPID PGID WINPID TTY UID STIME COMMAND
179+
* 1869 1 1869 7852 cons2 4096 15:29:27 /usr/bin/bash
180+
* 2271 1 2271 30820 cons4 4096 19:38:30 /usr/bin/bash
181+
* 1812 1 1812 21540 cons1 4096 15:05:03 /usr/bin/bash
182+
* 2216 1 2216 11328 cons3 4096 19:38:18 /usr/bin/bash
183+
* 1720 1 1720 5404 cons0 4096 13:46:42 /usr/bin/bash
184+
* 2269 2216 2269 6676 cons3 4096 19:38:21 /c/Program Files/Microsoft/jdk-11.0.14.9-hotspot/bin/java
185+
* 1911 1869 1869 29708 cons2 4096 15:29:31 /c/Program Files/nodejs/node
186+
* 2315 2271 2315 18064 cons4 4096 19:38:34 /usr/bin/ps
187+
*/
188+
while ((line = psReader.readLine()) != null) {
189+
String[] cols = line.strip().split("\\s+");
190+
if (headers == null) {
191+
headers = cols;
192+
pidIndex = ArrayUtils.indexOf(headers, "PID");
193+
ppidIndex = ArrayUtils.indexOf(headers, "PPID");
194+
winpidIndex = ArrayUtils.indexOf(headers, "WINPID");
195+
if (pidIndex < 0 || ppidIndex < 0 || winpidIndex < 0) {
196+
logger.warning("Failed to find Java process because ps command is not the standard Cygwin ps command.");
197+
return -1;
198+
}
199+
} else if (cols.length >= headers.length) {
200+
long pid = Long.parseLong(cols[pidIndex]);
201+
long ppid = Long.parseLong(cols[ppidIndex]);
202+
long winpid = Long.parseLong(cols[winpidIndex]);
203+
PsProcess process = new PsProcess(pid, ppid, winpid);
204+
psProcs.add(process);
205+
if (cols[cols.length - 1].endsWith("/" + javaExeName) || cols[cols.length - 1].endsWith("/java")) {
206+
javaCandidates.add(process);
207+
}
208+
}
209+
}
210+
} catch (Exception err) {
211+
logger.log(Level.WARNING, "Failed to find Java process by Cygwin ps command.", err);
212+
} finally {
213+
if (psReader != null) {
214+
try {
215+
psReader.close();
216+
} catch (IOException e) {
217+
// ignore
218+
}
219+
}
220+
}
221+
222+
if (!javaCandidates.isEmpty()) {
223+
Set<Long> descendantWinpids = shellProcess.descendants().map(proc -> proc.pid()).collect(Collectors.toSet());
224+
long shellWinpid = shellProcess.pid();
225+
for (PsProcess javaCandidate: javaCandidates) {
226+
if (descendantWinpids.contains(javaCandidate.winpid)) {
227+
return javaCandidate.winpid;
228+
}
229+
230+
for (PsProcess psProc : psProcs) {
231+
if (javaCandidate.ppid != psProc.pid) {
232+
continue;
233+
}
234+
235+
if (descendantWinpids.contains(psProc.winpid) || psProc.winpid == shellWinpid) {
236+
return javaCandidate.winpid;
237+
}
238+
239+
break;
240+
}
241+
}
242+
}
243+
244+
return -1;
245+
}
246+
247+
private static boolean isCygwinShell(String shellPath) {
248+
if (!SystemUtils.IS_OS_WINDOWS || shellPath == null) {
249+
return false;
250+
}
251+
252+
String lowerShellPath = shellPath.toLowerCase();
253+
return lowerShellPath.endsWith("git\\bin\\bash.exe")
254+
|| lowerShellPath.endsWith("git\\usr\\bin\\bash.exe")
255+
|| lowerShellPath.endsWith("mintty.exe")
256+
|| lowerShellPath.endsWith("cygwin64\\bin\\bash.exe")
257+
|| (lowerShellPath.endsWith("bash.exe") && detectPsCommandPath(shellPath) != null)
258+
|| (lowerShellPath.endsWith("sh.exe") && detectPsCommandPath(shellPath) != null);
259+
}
260+
261+
private static String detectPsCommandPath(String shellPath) {
262+
if (shellPath == null) {
263+
return null;
264+
}
265+
266+
Path psPath = Paths.get(shellPath, "..\\ps.exe");
267+
if (!Files.exists(psPath)) {
268+
psPath = Paths.get(shellPath, "..\\..\\usr\\bin\\ps.exe");
269+
if (!Files.exists(psPath)) {
270+
psPath = null;
271+
}
272+
}
273+
274+
if (psPath == null) {
275+
return null;
276+
}
277+
278+
return psPath.normalize().toString();
279+
}
280+
105281
private static Path tmpdir = null;
106282

107283
private static synchronized Path getTmpDir() throws IOException {
@@ -156,4 +332,16 @@ private static String getMd5(String input) {
156332
return Integer.toString(input.hashCode(), Character.MAX_RADIX);
157333
}
158334
}
335+
336+
private static class PsProcess {
337+
long pid;
338+
long ppid;
339+
long winpid;
340+
341+
public PsProcess(long pid, long ppid, long winpid) {
342+
this.pid = pid;
343+
this.ppid = ppid;
344+
this.winpid = winpid;
345+
}
346+
}
159347
}

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2017 Microsoft Corporation and others.
2+
* Copyright (c) 2017-2022 Microsoft Corporation and others.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -116,6 +116,12 @@ public CompletableFuture<Response> launchInTerminal(LaunchArguments launchArgume
116116
vmHandler.connectVirtualMachine(vm);
117117
context.setDebugSession(new DebugSession(vm));
118118
logger.info("Launching debuggee in terminal console succeeded.");
119+
if (context.getShellProcessId() > 0) {
120+
ProcessHandle debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(context.getShellProcessId(), cmds[0], 0);
121+
if (debuggeeProcess != null) {
122+
context.setProcessId(debuggeeProcess.pid());
123+
}
124+
}
119125
resultFuture.complete(response);
120126
} catch (TransportTimeoutException e) {
121127
int commandLength = StringUtils.length(launchArguments.cwd) + 1;

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018 Microsoft Corporation and others.
2+
* Copyright (c) 2018-2022 Microsoft Corporation and others.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -114,20 +114,32 @@ public CompletableFuture<Response> launchInTerminal(LaunchArguments launchArgume
114114
context.getProtocolServer().sendRequest(request, RUNINTERMINAL_TIMEOUT).whenComplete((runResponse, ex) -> {
115115
if (runResponse != null) {
116116
if (runResponse.success) {
117+
ProcessHandle debuggeeProcess = null;
117118
try {
118119
RunInTerminalResponseBody terminalResponse = JsonUtils.fromJson(
119120
JsonUtils.toJson(runResponse.body), RunInTerminalResponseBody.class);
120121
context.setProcessId(terminalResponse.processId);
121122
context.setShellProcessId(terminalResponse.shellProcessId);
123+
124+
if (terminalResponse.processId > 0) {
125+
debuggeeProcess = ProcessHandle.of(terminalResponse.processId).orElse(null);
126+
} else if (terminalResponse.shellProcessId > 0) {
127+
debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(terminalResponse.shellProcessId, cmds[0], 3000);
128+
}
129+
130+
if (debuggeeProcess != null) {
131+
context.setProcessId(debuggeeProcess.pid());
132+
debuggeeProcess.onExit().thenAcceptAsync(proc -> {
133+
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
134+
});
135+
}
122136
} catch (JsonSyntaxException e) {
123137
logger.severe("Failed to resolve runInTerminal response: " + e.toString());
124138
}
125139

126-
// TODO: Since the RunInTerminal request will return the pid or parent shell
127-
// pid now, the debugger is able to use this pid to monitor the lifecycle
128-
// of the running Java process. There is no need to terminate the debug
129-
// session early here.
130-
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
140+
if (debuggeeProcess == null || !debuggeeProcess.isAlive()) {
141+
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
142+
}
131143
resultFuture.complete(response);
132144
} else {
133145
resultFuture.completeExceptionally(

0 commit comments

Comments
 (0)