/*
* Copyright 2012-2015 Sergey Ignatov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.erlang.debugger.xdebug;
import com.ericsson.otp.erlang.OtpErlangPid;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.PathUtil;
import com.intellij.util.ResourceUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import com.intellij.xdebugger.XDebugProcess;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.breakpoints.XBreakpointHandler;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import com.intellij.xdebugger.evaluation.EvaluationMode;
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import com.intellij.xdebugger.frame.XSuspendContext;
import org.intellij.erlang.ErlangFileType;
import org.intellij.erlang.debugger.node.ErlangDebuggerEventListener;
import org.intellij.erlang.debugger.node.ErlangDebuggerNode;
import org.intellij.erlang.debugger.node.ErlangDebuggerNodeException;
import org.intellij.erlang.debugger.node.ErlangProcessSnapshot;
import org.intellij.erlang.debugger.remote.ErlangRemoteDebugRunConfiguration;
import org.intellij.erlang.debugger.remote.ErlangRemoteDebugRunningState;
import org.intellij.erlang.psi.ErlangFile;
import org.intellij.erlang.runconfig.ErlangRunConfigurationBase;
import org.intellij.erlang.runconfig.ErlangRunningState;
import org.intellij.erlang.utils.ErlangModulesUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.intellij.erlang.debugger.ErlangDebuggerLog.LOG;
public class ErlangXDebugProcess extends XDebugProcess implements ErlangDebuggerEventListener {
private final ExecutionEnvironment myExecutionEnvironment;
private final ErlangRunningState myRunningState;
private final ErlangDebuggerNode myDebuggerNode;
private final OSProcessHandler myErlangProcessHandler;
private final ErlangDebugLocationResolver myLocationResolver;
private XBreakpointHandler<?>[] myBreakpointHandlers = new XBreakpointHandler[]{new ErlangLineBreakpointHandler(this)};
private ConcurrentHashMap<ErlangSourcePosition, XLineBreakpoint<ErlangLineBreakpointProperties>> myPositionToLineBreakpointMap =
new ConcurrentHashMap<>();
public ErlangXDebugProcess(@NotNull XDebugSession session, ExecutionEnvironment env) throws ExecutionException {
//TODO add debug build targets and make sure the project is built using them.
super(session);
session.setPauseActionSupported(false);
myExecutionEnvironment = env;
myRunningState = getRunConfiguration().getState(myExecutionEnvironment.getExecutor(), myExecutionEnvironment);
if (myRunningState == null) {
throw new ExecutionException("Failed to execute a run configuration.");
}
try {
//TODO add the debugger node to disposable hierarchy (we may fail to initialize session so the session will not be stopped!)
myDebuggerNode = new ErlangDebuggerNode(this);
}
catch (ErlangDebuggerNodeException e) {
throw new ExecutionException(e);
}
// it's important to set modules to interpret before running debug target
setModulesToInterpret();
//TODO split running debug target and debugger process spawning
myErlangProcessHandler = runDebugTarget();
ErlangRunConfigurationBase<?> runConfig = getRunConfiguration();
myLocationResolver = new ErlangDebugLocationResolver(runConfig.getProject(),
runConfig.getConfigurationModule().getModule(),
runConfig.isUseTestCodePath());
}
@Override
public void debuggerStarted() {
getSession().reportMessage("Debug process started", MessageType.INFO);
}
@Override
public void failedToInterpretModules(String nodeName, List<String> modules) {
String messagePrefix = "Failed to interpret modules on node " + nodeName + ": ";
String modulesString = StringUtil.join(modules, ", ");
String messageSuffix = ".\nMake sure they are compiled with debug_info option, their sources are located in same directory as .beam files, modules are available on the node.";
String message = messagePrefix + modulesString + messageSuffix;
getSession().reportMessage(message, MessageType.WARNING);
}
@Override
public void failedToDebugRemoteNode(String nodeName, String error) {
String message = "Failed to debug remote node '" + nodeName + "'. Details: " + error;
getSession().reportMessage(message, MessageType.ERROR);
}
@Override
public void unknownMessage(String messageText) {
getSession().reportMessage("Unknown message received: " + messageText, MessageType.WARNING);
}
@Override
public void failedToSetBreakpoint(String module, int line, String errorMessage) {
ErlangSourcePosition sourcePosition = ErlangSourcePosition.create(myLocationResolver, module, line);
XLineBreakpoint<ErlangLineBreakpointProperties> breakpoint = getLineBreakpoint(sourcePosition);
if (breakpoint != null) {
getSession().updateBreakpointPresentation(breakpoint, AllIcons.Debugger.Db_invalid_breakpoint, errorMessage);
}
}
@Override
public void breakpointIsSet(String module, int line) {
}
@Override
public void breakpointReached(final OtpErlangPid pid, List<ErlangProcessSnapshot> snapshots) {
ErlangProcessSnapshot processInBreakpoint = ContainerUtil.find(snapshots, erlangProcessSnapshot -> erlangProcessSnapshot.getPid().equals(pid));
assert processInBreakpoint != null;
ErlangSourcePosition breakPosition = ErlangSourcePosition.create(myLocationResolver, processInBreakpoint);
XLineBreakpoint<ErlangLineBreakpointProperties> breakpoint = getLineBreakpoint(breakPosition);
ErlangSuspendContext suspendContext = new ErlangSuspendContext(myLocationResolver, pid, snapshots);
if (breakpoint == null) {
getSession().positionReached(suspendContext);
}
else {
boolean shouldSuspend = getSession().breakpointReached(breakpoint, null, suspendContext);
if (!shouldSuspend) {
resume(suspendContext);
}
}
}
@Override
public void debuggerStopped() {
getSession().reportMessage("Debug process stopped", MessageType.INFO);
getSession().stop();
}
@Nullable
private XLineBreakpoint<ErlangLineBreakpointProperties> getLineBreakpoint(@Nullable ErlangSourcePosition sourcePosition) {
return sourcePosition != null ? myPositionToLineBreakpointMap.get(sourcePosition) : null;
}
private void setModulesToInterpret() {
Project project = myExecutionEnvironment.getProject();
Collection<ErlangFile> erlangModules = ErlangModulesUtil.getErlangModules(project);
ErlangRunConfigurationBase<?> runConfiguration = getRunConfiguration();
if (runConfiguration.isUseTestCodePath()) {
HashSet<ErlangFile> erlangTestModules = new HashSet<>();
for (Module module : runConfiguration.getModules()) {
erlangTestModules.addAll(ErlangModulesUtil.getErlangModules(module, true));
}
erlangTestModules.addAll(erlangModules);
erlangModules = erlangTestModules;
}
Set<String> notToInterpret = runConfiguration.getDebugOptions().getModulesNotToInterpret();
List<String> moduleSourcePaths = ContainerUtil.newArrayListWithCapacity(erlangModules.size());
for (ErlangFile erlangModule : erlangModules) {
VirtualFile file = erlangModule.getVirtualFile();
if (file != null && !notToInterpret.contains(file.getNameWithoutExtension())) {
moduleSourcePaths.add(PathUtil.getLocalPath(file));
}
}
myDebuggerNode.interpretModules(moduleSourcePaths);
}
@NotNull
@Override
public ExecutionConsole createConsole() {
ConsoleView consoleView = myRunningState.createConsoleView(myExecutionEnvironment.getExecutor());
consoleView.attachToProcess(getProcessHandler());
myErlangProcessHandler.startNotify();
return consoleView;
}
@NotNull
@Override
public XBreakpointHandler<?>[] getBreakpointHandlers() {
return myBreakpointHandlers;
}
@NotNull
@Override
public XDebuggerEditorsProvider getEditorsProvider() {
return new XDebuggerEditorsProvider() {
@NotNull
@Override
public FileType getFileType() {
return ErlangFileType.MODULE;
}
@NotNull
@Override
public Document createDocument(@NotNull Project project,
@NotNull String text,
@Nullable XSourcePosition sourcePosition,
@NotNull EvaluationMode mode) {
LightVirtualFile file = new LightVirtualFile("plain-text-erlang-debugger.txt", text);
//noinspection ConstantConditions
return FileDocumentManager.getInstance().getDocument(file);
}
};
}
@Override
public void startStepOver(@Nullable XSuspendContext context) {
myDebuggerNode.stepOver();
}
@Override
public void startStepInto(@Nullable XSuspendContext context) {
myDebuggerNode.stepInto();
}
@Override
public void startStepOut(@Nullable XSuspendContext context) {
myDebuggerNode.stepOut();
}
@Override
public void stop() {
myDebuggerNode.stop();
}
@Override
public void resume(@Nullable XSuspendContext context) {
myDebuggerNode.resume();
}
@Override
public void runToPosition(@NotNull XSourcePosition position, @Nullable XSuspendContext context) {
//TODO implement me
}
@Nullable
@Override
protected ProcessHandler doGetProcessHandler() {
return myErlangProcessHandler;
}
void addBreakpoint(XLineBreakpoint<ErlangLineBreakpointProperties> breakpoint) {
ErlangSourcePosition breakpointPosition = getErlangSourcePosition(breakpoint);
if (breakpointPosition == null) return;
myPositionToLineBreakpointMap.put(breakpointPosition, breakpoint);
myDebuggerNode.setBreakpoint(breakpointPosition.getErlangModuleName(), breakpointPosition.getLine());
}
void removeBreakpoint(XLineBreakpoint<ErlangLineBreakpointProperties> breakpoint,
@SuppressWarnings("UnusedParameters") boolean temporary) {
ErlangSourcePosition breakpointPosition = getErlangSourcePosition(breakpoint);
if (breakpointPosition == null) return;
myPositionToLineBreakpointMap.remove(breakpointPosition);
myDebuggerNode.removeBreakpoint(breakpointPosition.getErlangModuleName(), breakpointPosition.getLine());
}
@Nullable
private static ErlangSourcePosition getErlangSourcePosition(XLineBreakpoint<ErlangLineBreakpointProperties> breakpoint) {
XSourcePosition sourcePosition = breakpoint.getSourcePosition();
return sourcePosition != null ? ErlangSourcePosition.create(sourcePosition) : null;
}
private ErlangRunConfigurationBase<?> getRunConfiguration() {
ErlangRunConfigurationBase<?> runConfig = (ErlangRunConfigurationBase) getSession().getRunProfile();
assert runConfig != null;
return runConfig;
}
@NotNull
private OSProcessHandler runDebugTarget() throws ExecutionException {
OSProcessHandler erlangProcessHandler;
LOG.debug("Preparing to run debug target.");
try {
GeneralCommandLine commandLine = new GeneralCommandLine();
myRunningState.setExePath(commandLine);
myRunningState.setWorkDirectory(commandLine);
setUpErlangDebuggerCodePath(commandLine);
myRunningState.setCodePath(commandLine);
commandLine.addParameters("-run", "debugnode", "main", String.valueOf(myDebuggerNode.getLocalDebuggerPort()));
myRunningState.setErlangFlags(commandLine);
myRunningState.setNoShellMode(commandLine);
myRunningState.setStopErlang(commandLine);
LOG.debug("Running debugger process. Command line (platform-independent): ");
LOG.debug(commandLine.getCommandLineString());
Process process = commandLine.createProcess();
erlangProcessHandler = new OSProcessHandler(process, commandLine.getCommandLineString());
LOG.debug("Debugger process started.");
if (myRunningState instanceof ErlangRemoteDebugRunningState) {
LOG.debug("Initializing remote node debugging.");
ErlangRemoteDebugRunConfiguration runConfiguration = (ErlangRemoteDebugRunConfiguration) getRunConfiguration();
if (StringUtil.isEmptyOrSpaces(runConfiguration.getRemoteErlangNodeName())) {
throw new ExecutionException("Bad run configuration: remote Erlang node is not specified.");
}
LOG.debug("Remote node: " + runConfiguration.getRemoteErlangNodeName());
LOG.debug("Cookie: " + runConfiguration.getCookie());
myDebuggerNode.debugRemoteNode(runConfiguration.getRemoteErlangNodeName(), runConfiguration.getCookie());
}
else {
LOG.debug("Initializing local debugging.");
ErlangRunningState.ErlangEntryPoint entryPoint = myRunningState.getDebugEntryPoint();
LOG.debug("Entry point: " + entryPoint.getModuleName() + ":" + entryPoint.getFunctionName() +
"(" + StringUtil.join(entryPoint.getArgsList(), ", ") + ")");
myDebuggerNode.runDebugger(entryPoint.getModuleName(), entryPoint.getFunctionName(), entryPoint.getArgsList());
}
}
catch (ExecutionException e) {
LOG.debug("Failed to run debug target.", e);
throw e;
}
LOG.debug("Debug target should now be running.");
return erlangProcessHandler;
}
private static void setUpErlangDebuggerCodePath(GeneralCommandLine commandLine) throws ExecutionException {
LOG.debug("Setting up debugger environment.");
try {
String[] beams = {"debugnode.beam", "remote_debugger.beam", "remote_debugger_listener.beam", "remote_debugger_notifier.beam"};
File tempDirectory = FileUtil.createTempDirectory("intellij_erlang_debugger_", null);
LOG.debug("Debugger beams will be put to: " + tempDirectory.getPath());
for (String beam : beams) {
copyBeamTo(beam, tempDirectory);
}
LOG.debug("Debugger beams were copied successfully.");
commandLine.addParameters("-pa", tempDirectory.getPath());
}
catch (IOException e) {
throw new ExecutionException("Failed to setup debugger environment", e);
}
}
private static void copyBeamTo(String beamName, File directory) throws IOException {
URL beamUrl = ResourceUtil.getResource(ErlangXDebugProcess.class, "/debugger/beams", beamName);
if (beamUrl == null) {
throw new IOException("Failed to locate debugger module: " + beamName);
}
try (BufferedInputStream inputStream = new BufferedInputStream(URLUtil.openStream(beamUrl))) {
try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(new File(directory, beamName)))) {
FileUtil.copy(inputStream, outputStream);
}
}
}
}