/* * Copyright 2012-2015 Sergey Ignatov * Copyright 2017 Jake Becker * Copyright 2017 Luke Imhoff * * 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.elixir_lang.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.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.psi.PsiElement; import com.intellij.psi.PsiManager; import com.intellij.testFramework.LightVirtualFile; 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 org.elixir_lang.ElixirFileType; import org.elixir_lang.debugger.node.ElixirDebuggerEventListener; import org.elixir_lang.debugger.node.ElixirDebuggerNode; import org.elixir_lang.debugger.node.ElixirDebuggerNodeException; import org.elixir_lang.debugger.node.ElixirProcessSnapshot; import org.elixir_lang.mix.runner.MixRunConfigurationBase; import org.elixir_lang.mix.runner.MixRunningState; import org.elixir_lang.mix.runner.MixRunningStateUtil; import org.elixir_lang.mix.runner.exunit.MixExUnitRunConfiguration; import org.elixir_lang.psi.ElixirFile; import org.elixir_lang.psi.impl.ElixirPsiImplUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import static org.elixir_lang.debugger.ElixirDebuggerLog.LOG; class ElixirXDebugProcess extends XDebugProcess implements ElixirDebuggerEventListener { private final ExecutionEnvironment myExecutionEnvironment; @NotNull private final MixRunningState myRunningState; @NotNull private final ElixirDebuggerNode myDebuggerNode; @NotNull private final OSProcessHandler myElixirProcessHandler; @NotNull private final XBreakpointHandler<?>[] myBreakpointHandlers = new XBreakpointHandler[]{new ElixirLineBreakpointHandler(this)}; @NotNull private final ConcurrentHashMap<ElixirSourcePosition, XLineBreakpoint<ElixirLineBreakpointProperties>> myPositionToLineBreakpointMap = new ConcurrentHashMap<>(); ElixirXDebugProcess(@NotNull XDebugSession session, ExecutionEnvironment env) throws ExecutionException { super(session); session.setPauseActionSupported(false); myExecutionEnvironment = env; myRunningState = (MixRunningState)getRunConfiguration().getState(myExecutionEnvironment.getExecutor(), myExecutionEnvironment); try { //TODO add the debugger node to disposable hierarchy (we may fail to initialize session so the session will not be stopped!) myDebuggerNode = new ElixirDebuggerNode(this); } catch (ElixirDebuggerNodeException e) { throw new ExecutionException(e); } //TODO split running debug target and debugger process spawning myElixirProcessHandler = runDebugTarget(); } @Nullable private static ElixirSourcePosition getElixirSourcePosition(@NotNull XLineBreakpoint<ElixirLineBreakpointProperties> breakpoint) { XSourcePosition sourcePosition = breakpoint.getSourcePosition(); return sourcePosition != null ? ElixirSourcePosition.create(sourcePosition) : null; } @NotNull private static List<String> setUpElixirDebuggerCodePath() throws ExecutionException { LOG.debug("Setting up debugger environment."); try { String[] files = {"Elixir.Mix.Tasks.IntellijElixir.DebugTask.beam", "Elixir.IntellijElixir.DebugServer.beam"}; File tempDirectory = FileUtil.createTempDirectory("intellij_elixir_debugger", null); LOG.debug("Debugger elixir files will be put to: " + tempDirectory.getPath()); for (String file : files) { copyFileTo(file, tempDirectory); } LOG.debug("Debugger elixir files were copied successfully."); return Arrays.asList("-pa", tempDirectory.getPath()); } catch (IOException e) { throw new ExecutionException("Failed to setup debugger environment", e); } } private static void copyFileTo(@NotNull String fileName, File directory) throws IOException { URL fileUrl = ResourceUtil.getResource(ElixirXDebugProcess.class, "/debugger/_build/shared/lib/intellij_elixir_debugger/ebin", fileName); if (fileUrl == null) { throw new IOException("Failed to locate debugger module: " + fileName); } try (BufferedInputStream inputStream = new BufferedInputStream(URLUtil.openStream(fileUrl))) { try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(new File(directory, fileName)))) { FileUtil.copy(inputStream, outputStream); } } } @Override public void debuggerStarted() { getSession().reportMessage("Debug process started", MessageType.INFO); } @Override public void sessionInitialized() { myDebuggerNode.runTask(); } @Override public void failedToInterpretModules(String nodeName, @NotNull 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, @NotNull String file, int line, String errorMessage) { ElixirSourcePosition sourcePosition = ElixirSourcePosition.create(file, line); XLineBreakpoint<ElixirLineBreakpointProperties> breakpoint = getLineBreakpoint(sourcePosition); if (breakpoint != null) { getSession().updateBreakpointPresentation(breakpoint, AllIcons.Debugger.Db_invalid_breakpoint, errorMessage); } getSession().reportMessage("Failed to set breakpoint. Module: " + module + " Line: " + (line + 1), MessageType.WARNING); } @Override public void breakpointIsSet(String module, String file, int line) { } @Override public void breakpointReached(@NotNull final OtpErlangPid pid, @NotNull List<ElixirProcessSnapshot> snapshots) { ElixirProcessSnapshot processInBreakpoint = ContainerUtil.find(snapshots, elixirProcessSnapshot -> elixirProcessSnapshot.getPid().equals(pid)); assert processInBreakpoint != null; ElixirSourcePosition breakPosition = ElixirSourcePosition.create(processInBreakpoint); XLineBreakpoint<ElixirLineBreakpointProperties> breakpoint = getLineBreakpoint(breakPosition); ElixirSuspendContext suspendContext = new ElixirSuspendContext(pid, snapshots); if (breakpoint == null) { getSession().positionReached(suspendContext); } else { boolean shouldSuspend = getSession().breakpointReached(breakpoint, null, suspendContext); if (!shouldSuspend) { resume(); } } } @Override public void debuggerStopped() { getSession().reportMessage("Debug process stopped", MessageType.INFO); getSession().stop(); } @Nullable private XLineBreakpoint<ElixirLineBreakpointProperties> getLineBreakpoint(@Nullable ElixirSourcePosition sourcePosition) { return sourcePosition != null ? myPositionToLineBreakpointMap.get(sourcePosition) : null; } @NotNull @Override public ExecutionConsole createConsole() { ConsoleView consoleView = myRunningState.createConsoleView(myExecutionEnvironment.getExecutor()); consoleView.attachToProcess(getProcessHandler()); myElixirProcessHandler.startNotify(); return consoleView; } @NotNull @Override public XBreakpointHandler<?>[] getBreakpointHandlers() { return myBreakpointHandlers; } @NotNull @Override public XDebuggerEditorsProvider getEditorsProvider() { return new XDebuggerEditorsProvider() { @NotNull @Override public FileType getFileType() { return ElixirFileType.INSTANCE; } @NotNull @Override public Document createDocument(@NotNull Project project, @NotNull String text, @Nullable XSourcePosition sourcePosition, @NotNull EvaluationMode mode) { LightVirtualFile file = new LightVirtualFile("plain-text-elixir-debugger.txt", text); //noinspection ConstantConditions return FileDocumentManager.getInstance().getDocument(file); } }; } @Override @SuppressWarnings("deprecation") public void startStepOver() { myDebuggerNode.stepOver(); } @Override @SuppressWarnings("deprecation") public void startStepInto() { myDebuggerNode.stepInto(); } @Override @SuppressWarnings("deprecation") public void startStepOut() { myDebuggerNode.stepOut(); } @Override public void stop() { myDebuggerNode.stop(); } @Override @SuppressWarnings("deprecation") public void resume() { myDebuggerNode.resume(); } @Override @SuppressWarnings("deprecation") public void runToPosition(@NotNull XSourcePosition position) { //TODO implement me } @Nullable @Override protected ProcessHandler doGetProcessHandler() { return myElixirProcessHandler; } void addBreakpoint(@NotNull XLineBreakpoint<ElixirLineBreakpointProperties> breakpoint) { ElixirSourcePosition breakpointPosition = getElixirSourcePosition(breakpoint); if (breakpointPosition == null) return; String moduleName = getModuleName(breakpointPosition); if (moduleName != null) { myPositionToLineBreakpointMap.put(breakpointPosition, breakpoint); myDebuggerNode.setBreakpoint(moduleName, breakpointPosition.getFile().getPath(), breakpointPosition.getLine()); } else { final String message = "Unable to determine module for breakpoint at " + breakpointPosition.getFile() + " line " + breakpointPosition.getLine(); getSession().reportMessage(message, MessageType.ERROR); } } @Nullable private String getModuleName(@NotNull ElixirSourcePosition breakpointPosition) { ElixirFile psiFile = (ElixirFile)PsiManager.getInstance(getRunConfiguration().getProject()).findFile(breakpointPosition.getFile()); if (psiFile == null) return null; PsiElement elem = psiFile.findElementAt(breakpointPosition.getSourcePosition().getOffset()); return ElixirPsiImplUtil.getModuleName(elem); } void removeBreakpoint(@NotNull XLineBreakpoint<ElixirLineBreakpointProperties> breakpoint, @SuppressWarnings("UnusedParameters") boolean temporary) { ElixirSourcePosition breakpointPosition = getElixirSourcePosition(breakpoint); if (breakpointPosition == null) return; myPositionToLineBreakpointMap.remove(breakpointPosition); String moduleName = getModuleName(breakpointPosition); if (moduleName != null) { myDebuggerNode.removeBreakpoint(moduleName, breakpointPosition.getLine()); } } @NotNull private MixRunConfigurationBase getRunConfiguration() { MixRunConfigurationBase runConfig = (MixRunConfigurationBase) getSession().getRunProfile(); assert runConfig != null; return runConfig; } @NotNull private OSProcessHandler runDebugTarget() throws ExecutionException { OSProcessHandler elixirProcessHandler; LOG.debug("Preparing to run debug target."); ArrayList<String> elixirParams = new ArrayList<>(); elixirParams.addAll(myRunningState.setupElixirParams()); elixirParams.addAll(setUpElixirDebuggerCodePath()); List<String> mixParams = new ArrayList<>(); mixParams.addAll(Arrays.asList("intellij_elixir.debug_task", "--debugger-port", "" + myDebuggerNode.getLocalDebuggerPort(), "--")); List<String> mixCommandArgs = getRunConfiguration().getMixArgs(); mixParams.addAll(mixCommandArgs); if (getRunConfiguration() instanceof MixExUnitRunConfiguration && !mixCommandArgs.contains("--trace")) { // Prevents tests from timing out while debugging mixParams.add("--trace"); } GeneralCommandLine commandLine = MixRunningStateUtil.commandLine(getRunConfiguration(), elixirParams, mixParams); LOG.debug("Running debugger process. Command line (platform-independent): "); LOG.debug(commandLine.getCommandLineString()); Process process = commandLine.createProcess(); elixirProcessHandler = new OSProcessHandler(process, commandLine.getCommandLineString()); LOG.debug("Debugger process started."); return elixirProcessHandler; } }