Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture Java process pid and display debug toolbar when running Java in terminal #413

Merged
merged 4 commits into from
May 24, 2022
Merged
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2018 Microsoft Corporation and others.
* Copyright (c) 2018-2022 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand All @@ -11,6 +11,8 @@

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

import java.util.Optional;

import com.microsoft.java.debug.core.adapter.IDebugAdapterContext;
import com.microsoft.java.debug.core.protocol.Messages.Response;
import com.microsoft.java.debug.core.protocol.Requests.Arguments;
Expand All @@ -25,6 +27,11 @@ public void destroyDebugSession(Command command, Arguments arguments, Response r
Process debuggeeProcess = context.getDebuggeeProcess();
if (debuggeeProcess != null && disconnectArguments.terminateDebuggee) {
debuggeeProcess.destroy();
} else if (context.getProcessId() > 0 && disconnectArguments.terminateDebuggee) {
Optional<ProcessHandle> debuggeeHandle = ProcessHandle.of(context.getProcessId());
if (debuggeeHandle.isPresent()) {
debuggeeHandle.get().destroy();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2021 Microsoft Corporation and others.
* Copyright (c) 2021-2022 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand All @@ -11,28 +11,39 @@

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import com.microsoft.java.debug.core.Configuration;
import com.microsoft.java.debug.core.adapter.AdapterUtils;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;

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

/**
Expand Down Expand Up @@ -102,6 +113,171 @@ public static void releaseTempLaunchFile(Path tempFile) {
}
}

public static ProcessHandle findJavaProcessInTerminalShell(long shellPid, String javaCommand, int timeout/*ms*/) {
ProcessHandle shellProcess = ProcessHandle.of(shellPid).orElse(null);
if (shellProcess != null) {
int retry = 0;
final int INTERVAL = 100;
final int maxRetries = timeout / INTERVAL;
final boolean isCygwinShell = isCygwinShell(shellProcess.info().command().orElse(null));
while (retry <= maxRetries) {
Optional<ProcessHandle> subProcessHandle = shellProcess.descendants().filter(proc -> {
String command = proc.info().command().orElse("");
return Objects.equals(command, javaCommand) || command.endsWith("\\java.exe") || command.endsWith("/java");
}).findFirst();

if (subProcessHandle.isPresent()) {
logger.info("shellPid: " + shellPid + ", javaPid: " + subProcessHandle.get().pid());
return subProcessHandle.get();
} else if (isCygwinShell) {
long javaPid = findJavaProcessByCygwinPsCommand(shellProcess, javaCommand);
if (javaPid > 0) {
logger.info("[Cygwin Shell] shellPid: " + shellPid + ", javaPid: " + javaPid);
return ProcessHandle.of(javaPid).orElse(null);
}
}

retry++;
if (retry > maxRetries) {
break;
}

try {
Thread.sleep(INTERVAL);
} catch (InterruptedException e) {
// do nothing
}
logger.info("Retry to find Java subProcess of shell pid " + shellPid);
}
}

return null;
}

private static long findJavaProcessByCygwinPsCommand(ProcessHandle shellProcess, String javaCommand) {
String psCommand = detectPsCommandPath(shellProcess.info().command().orElse(null));
if (psCommand == null) {
return -1;
}

BufferedReader psReader = null;
List<PsProcess> psProcs = new ArrayList<>();
List<PsProcess> javaCandidates = new ArrayList<>();
try {
String[] headers = null;
int pidIndex = -1;
int ppidIndex = -1;
int winpidIndex = -1;
String line;
String javaExeName = Paths.get(javaCommand).toFile().getName().replaceFirst("\\.exe$", "");

Process p = Runtime.getRuntime().exec(new String[] {psCommand, "-l"});
psReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
/**
* Here is a sample output when running ps command in Cygwin/MINGW64 shell.
* PID PPID PGID WINPID TTY UID STIME COMMAND
* 1869 1 1869 7852 cons2 4096 15:29:27 /usr/bin/bash
* 2271 1 2271 30820 cons4 4096 19:38:30 /usr/bin/bash
* 1812 1 1812 21540 cons1 4096 15:05:03 /usr/bin/bash
* 2216 1 2216 11328 cons3 4096 19:38:18 /usr/bin/bash
* 1720 1 1720 5404 cons0 4096 13:46:42 /usr/bin/bash
* 2269 2216 2269 6676 cons3 4096 19:38:21 /c/Program Files/Microsoft/jdk-11.0.14.9-hotspot/bin/java
* 1911 1869 1869 29708 cons2 4096 15:29:31 /c/Program Files/nodejs/node
* 2315 2271 2315 18064 cons4 4096 19:38:34 /usr/bin/ps
*/
while ((line = psReader.readLine()) != null) {
String[] cols = line.strip().split("\\s+");
if (headers == null) {
headers = cols;
pidIndex = ArrayUtils.indexOf(headers, "PID");
ppidIndex = ArrayUtils.indexOf(headers, "PPID");
winpidIndex = ArrayUtils.indexOf(headers, "WINPID");
if (pidIndex < 0 || ppidIndex < 0 || winpidIndex < 0) {
logger.warning("Failed to find Java process because ps command is not the standard Cygwin ps command.");
return -1;
}
} else if (cols.length >= headers.length) {
long pid = Long.parseLong(cols[pidIndex]);
long ppid = Long.parseLong(cols[ppidIndex]);
long winpid = Long.parseLong(cols[winpidIndex]);
PsProcess process = new PsProcess(pid, ppid, winpid);
psProcs.add(process);
if (cols[cols.length - 1].endsWith("/" + javaExeName) || cols[cols.length - 1].endsWith("/java")) {
javaCandidates.add(process);
}
}
}
} catch (Exception err) {
logger.log(Level.WARNING, "Failed to find Java process by Cygwin ps command.", err);
} finally {
if (psReader != null) {
try {
psReader.close();
} catch (IOException e) {
// ignore
}
}
}

if (!javaCandidates.isEmpty()) {
Set<Long> descendantWinpids = shellProcess.descendants().map(proc -> proc.pid()).collect(Collectors.toSet());
long shellWinpid = shellProcess.pid();
for (PsProcess javaCandidate: javaCandidates) {
if (descendantWinpids.contains(javaCandidate.winpid)) {
return javaCandidate.winpid;
}

for (PsProcess psProc : psProcs) {
if (javaCandidate.ppid != psProc.pid) {
continue;
}

if (descendantWinpids.contains(psProc.winpid) || psProc.winpid == shellWinpid) {
return javaCandidate.winpid;
}

break;
}
}
}

return -1;
}

private static boolean isCygwinShell(String shellPath) {
if (!SystemUtils.IS_OS_WINDOWS || shellPath == null) {
return false;
}

String lowerShellPath = shellPath.toLowerCase();
return lowerShellPath.endsWith("git\\bin\\bash.exe")
|| lowerShellPath.endsWith("git\\usr\\bin\\bash.exe")
|| lowerShellPath.endsWith("mintty.exe")
|| lowerShellPath.endsWith("cygwin64\\bin\\bash.exe")
|| (lowerShellPath.endsWith("bash.exe") && detectPsCommandPath(shellPath) != null)
|| (lowerShellPath.endsWith("sh.exe") && detectPsCommandPath(shellPath) != null);
}

private static String detectPsCommandPath(String shellPath) {
if (shellPath == null) {
return null;
}

Path psPath = Paths.get(shellPath, "..\\ps.exe");
if (!Files.exists(psPath)) {
psPath = Paths.get(shellPath, "..\\..\\usr\\bin\\ps.exe");
if (!Files.exists(psPath)) {
psPath = null;
}
}

if (psPath == null) {
return null;
}

return psPath.normalize().toString();
}

private static Path tmpdir = null;

private static synchronized Path getTmpDir() throws IOException {
Expand Down Expand Up @@ -156,4 +332,16 @@ private static String getMd5(String input) {
return Integer.toString(input.hashCode(), Character.MAX_RADIX);
}
}

private static class PsProcess {
long pid;
long ppid;
long winpid;

public PsProcess(long pid, long ppid, long winpid) {
this.pid = pid;
this.ppid = ppid;
this.winpid = winpid;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Microsoft Corporation and others.
* Copyright (c) 2017-2022 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -116,6 +116,12 @@ public CompletableFuture<Response> launchInTerminal(LaunchArguments launchArgume
vmHandler.connectVirtualMachine(vm);
context.setDebugSession(new DebugSession(vm));
logger.info("Launching debuggee in terminal console succeeded.");
if (context.getShellProcessId() > 0) {
ProcessHandle debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(context.getShellProcessId(), cmds[0], 0);
if (debuggeeProcess != null) {
context.setProcessId(debuggeeProcess.pid());
}
}
resultFuture.complete(response);
} catch (TransportTimeoutException e) {
int commandLength = StringUtils.length(launchArguments.cwd) + 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2018 Microsoft Corporation and others.
* Copyright (c) 2018-2022 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -114,20 +114,32 @@ public CompletableFuture<Response> launchInTerminal(LaunchArguments launchArgume
context.getProtocolServer().sendRequest(request, RUNINTERMINAL_TIMEOUT).whenComplete((runResponse, ex) -> {
if (runResponse != null) {
if (runResponse.success) {
ProcessHandle debuggeeProcess = null;
try {
RunInTerminalResponseBody terminalResponse = JsonUtils.fromJson(
JsonUtils.toJson(runResponse.body), RunInTerminalResponseBody.class);
context.setProcessId(terminalResponse.processId);
context.setShellProcessId(terminalResponse.shellProcessId);

if (terminalResponse.processId > 0) {
debuggeeProcess = ProcessHandle.of(terminalResponse.processId).orElse(null);
} else if (terminalResponse.shellProcessId > 0) {
debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(terminalResponse.shellProcessId, cmds[0], 3000);
}

if (debuggeeProcess != null) {
context.setProcessId(debuggeeProcess.pid());
debuggeeProcess.onExit().thenAcceptAsync(proc -> {
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
});
}
} catch (JsonSyntaxException e) {
logger.severe("Failed to resolve runInTerminal response: " + e.toString());
}

// TODO: Since the RunInTerminal request will return the pid or parent shell
// pid now, the debugger is able to use this pid to monitor the lifecycle
// of the running Java process. There is no need to terminate the debug
// session early here.
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
if (debuggeeProcess == null || !debuggeeProcess.isAlive()) {
context.getProtocolServer().sendEvent(new Events.TerminatedEvent());
}
resultFuture.complete(response);
} else {
resultFuture.completeExceptionally(
Expand Down