/* * Copyright 2000-2013 JetBrains s.r.o. * Copyright 2014-2014 AS3Boyan * Copyright 2014-2014 Elias Ku * * 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 com.intellij.plugins.haxe.runner.debugger; import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; import com.intellij.execution.configurations.RunProfile; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.executors.DefaultDebugExecutor; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.DefaultProgramRunner; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.ui.ExecutionConsole; import com.intellij.execution.ui.RunContentDescriptor; import com.intellij.icons.AllIcons; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VFileProperty; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.plugins.haxe.HaxeBundle; import com.intellij.plugins.haxe.config.HaxeTarget; import com.intellij.plugins.haxe.config.NMETarget; import com.intellij.plugins.haxe.config.OpenFLTarget; import com.intellij.plugins.haxe.haxelib.HaxelibClasspathUtils; import com.intellij.plugins.haxe.ide.module.HaxeModuleSettings; import com.intellij.plugins.haxe.runner.HaxeApplicationConfiguration; import com.intellij.plugins.haxe.runner.NMERunningState; import com.intellij.plugins.haxe.runner.OpenFLRunningState; import com.intellij.plugins.haxe.util.HaxeResolveUtil; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.ui.ColoredTextContainer; import com.intellij.ui.SimpleTextAttributes; import com.intellij.util.concurrency.QueueProcessor; import com.intellij.xdebugger.*; import com.intellij.xdebugger.breakpoints.XBreakpointHandler; import com.intellij.xdebugger.breakpoints.XBreakpointProperties; import com.intellij.xdebugger.breakpoints.XLineBreakpoint; import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider; import com.intellij.xdebugger.evaluation.XDebuggerEvaluator; import com.intellij.xdebugger.frame.*; import com.intellij.xdebugger.impl.XSourcePositionImpl; import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; import gnu.trove.THashSet; import haxe.root.JavaProtocol; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author: Fedor.Korotkov * @author: Bryan Ischo <bji@tivo.com> * <p/> * This is the singular Haxe debug runner, that can debug: * 1. Flash targets * 2. Hxcpp targets, run locally by the IDE * 3. Hxcpp targets, run by an external command */ public class HaxeDebugRunner extends DefaultProgramRunner { public static final String HAXE_DEBUG_RUNNER_ID = "HaxeDebugRunner"; @NotNull @Override public String getRunnerId() { return HAXE_DEBUG_RUNNER_ID; } @Override public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) { return (DefaultDebugExecutor.EXECUTOR_ID.equals(executorId) && (profile instanceof HaxeApplicationConfiguration)); } @Override protected RunContentDescriptor doExecute(RunProfileState state, ExecutionEnvironment env) throws ExecutionException { final HaxeApplicationConfiguration configuration = (HaxeApplicationConfiguration)(env.getRunProfile()); final Module module = configuration.getConfigurationModule().getModule(); final Executor executor = env.getExecutor(); if (module == null) { throw new ExecutionException (HaxeBundle.message("no.module.for.run.configuration", configuration.getName())); } final HaxeModuleSettings settings = HaxeModuleSettings.getInstance(module); boolean flashDebug = false, hxcppDebug = false; if (settings.isUseHxmlToBuild()) { if (settings.getHaxeTarget() == HaxeTarget.FLASH) { flashDebug = true; } else if (settings.getHaxeTarget() == HaxeTarget.CPP) { hxcppDebug = true; } } else if (settings.isUseNmmlToBuild()) { NMETarget target = settings.getNmeTarget(); if (target == NMETarget.FLASH) { flashDebug = true; } else if ((target == NMETarget.IOS) || (target == NMETarget.ANDROID) || (target == NMETarget.WINDOWS) || (target == NMETarget.MAC) || (target == NMETarget.LINUX) || (target == NMETarget.LINUX64)) { hxcppDebug = true; } } else if (settings.isUseOpenFLToBuild()) { OpenFLTarget target = settings.getOpenFLTarget(); if (target == OpenFLTarget.FLASH) { flashDebug = true; } else if ((target == OpenFLTarget.IOS) || (target == OpenFLTarget.ANDROID) || (target == OpenFLTarget.WINDOWS) || (target == OpenFLTarget.MAC) || (target == OpenFLTarget.LINUX) || (target == OpenFLTarget.LINUX64)) { hxcppDebug = true; } } if (flashDebug) { return runFlash(module, settings, env, executor, configuration.getCustomFileToLaunchPath()); } else if (hxcppDebug) { final Project project = env.getProject(); return runHxcpp(project, module, settings, env, executor, configuration.getCustomDebugPort(), configuration.isCustomRemoteDebugging()); } else { throw new ExecutionException (HaxeBundle.message("haxe.proper.debug.targets")); } } private RunContentDescriptor runFlash(final Module module, final HaxeModuleSettings settings, final ExecutionEnvironment env, final Executor executor, final String launchPath) throws ExecutionException { final IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId("com.intellij.flex")); if (plugin == null) { throw new ExecutionException (HaxeBundle.message("install.flex.plugin")); } if (!plugin.isEnabled()) { throw new ExecutionException (HaxeBundle.message("enable.flex.plugin")); } String flexSdkName = settings.getFlexSdkName(); if (StringUtil.isEmpty(flexSdkName)) { throw new ExecutionException (HaxeBundle.message("flex.sdk.not.specified")); } if (settings.isUseNmmlToBuild()) { return HaxeFlashDebuggingUtil.getNMEDescriptor (this, module, env, executor, flexSdkName); } else if (settings.isUseOpenFLToBuild()) { return HaxeFlashDebuggingUtil.getOpenFLDescriptor (this, module, env, executor, flexSdkName); } else { return HaxeFlashDebuggingUtil.getDescriptor (module, env, launchPath, flexSdkName); } } private RunContentDescriptor runHxcpp(final Project project, final Module module, final HaxeModuleSettings settings, final ExecutionEnvironment env, final Executor executor, final int port, final boolean remoteDebugging) throws ExecutionException { final XDebugSession debugSession = XDebuggerManager.getInstance(project).startSession (env, new XDebugProcessStarter() { @NotNull public XDebugProcess start(@NotNull final XDebugSession session) throws ExecutionException { try { // Start the debugger process, which is a class that // implements the actual debugger functionality. In this // case, it does so by message passing through a socket. final DebugProcess debugProcess = new DebugProcess (session, project, module, port); // If using remote debugging, emit a console message // indicating that the debugger is waiting for the remote // process to start. if (remoteDebugging) { showInfoMessage (project, "Listening for debugged process " + "on port " + port + " ... Press OK after " + "remote debugged process has started.", "Haxe Debugger"); } // Else, start the being-debugged process and make the // local debug process instance aware of it. else { if (settings.isUseOpenFLToBuild()) { debugProcess.setExecutionResult (new OpenFLRunningState (env, module, // runInTest if android or ios ((settings.getOpenFLTarget() == OpenFLTarget.ANDROID) || (settings.getOpenFLTarget() == OpenFLTarget.IOS)), true, port). execute(executor, HaxeDebugRunner.this)); } else { debugProcess.setExecutionResult (new NMERunningState (env, module, // runInTest if android or ios ((settings.getNmeTarget() == NMETarget.ANDROID) || (settings.getNmeTarget() == NMETarget.IOS)), true, port). execute(executor, HaxeDebugRunner.this)); } } // Now accept the connection from the being-debugged // process. debugProcess.start(); return debugProcess; } catch (IOException e) { throw new ExecutionException(e.getMessage(), e); } } }); return debugSession.getRunContentDescriptor(); } private class DebugProcess extends XDebugProcess { public DebugProcess(@NotNull XDebugSession session, Project project, Module module, int port) throws IOException { super(session); mClassesWithStatics = new Vector<String>(); mProject = project; mModule = module; mDeferredQueue = new LinkedList<Pair<debugger.Command, MessageListener>>(); mListenerQueue = new LinkedList<MessageListener>(); mServerSocket = new java.net.ServerSocket(port); mBreakpointHandlers = this.createBreakpointHandlers(); mMap = new HashMap<XLineBreakpoint<XBreakpointProperties>, Integer>(); mWriteQueue = QueueProcessor.createRunnableQueueProcessor(QueueProcessor.ThreadToUse.POOLED); } public void setExecutionResult(ExecutionResult executionResult) { mExecutionResult = executionResult; } public void start() { ApplicationManager.getApplication().executeOnPooledThread (new Runnable() { public void run() { try { DebugProcess.this.readLoop(); } catch (final Throwable t) { SwingUtilities.invokeLater (new Runnable() { public void run() { DebugProcess.this.error ("Debugging loop failed: " + t); } }); } } }); } @Override protected ProcessHandler doGetProcessHandler() { return ((mExecutionResult == null) ? null : mExecutionResult.getProcessHandler()); } @Override @NotNull public ExecutionConsole createConsole() { return ((mExecutionResult == null) ? super.createConsole() : mExecutionResult.getExecutionConsole()); } @Override @NotNull public XBreakpointHandler<?>[] getBreakpointHandlers() { return mBreakpointHandlers; } @Override @NotNull public XDebuggerEditorsProvider getEditorsProvider() { return new HaxeDebuggerEditorsProvider(); } @Override public void startPausing() { this.expectOK(debugger.Command.BreakNow); } @Override public void resume() { this.expectOK(debugger.Command.Continue(1)); } @Override public void startStepOver() { this.expectOK(debugger.Command.Next(1)); } @Override public void startStepInto() { this.expectOK(debugger.Command.Step(1)); } @Override public void startStepOut() { this.expectOK(debugger.Command.Finish(1)); } @Override public void stop() { synchronized (this) { if (mServerSocket != null) { try { mServerSocket.close(); mServerSocket = null; } catch (IOException e) { } } if (mDebugSocket != null) { try { mDebugSocket.close(); mDebugSocket = null; } catch (IOException e) { } } // Stop the write queue. Otherwise we get a bunch of pointless dialogs. mWriteQueue.dismissLastTasks(0); } } @Override public void runToPosition(@NotNull XSourcePosition position) { // Complicated! Basically, just make sure that there is a single // enabled breakpoint at [position], and then when [position] is // hit, set breakpoints back to how they were before ... but ... // what about breaking and setting breakpoints and stuff while // waiting on runToPosition? Have to be clever there ... } private void info(String message) { showInfoMessage(mProject, message, "Haxe Debugger"); } private void warn(String message) { showInfoMessage(mProject, message, "Haxe Debugger Warning"); } private void error(String message) { showInfoMessage(mProject, message, "Haxe Debugger Error"); this.stop(); } private void expectOK(debugger.Command command) { this.enqueueCommand(command, new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId != JavaProtocol.IdOK) { DebugProcess.this.error ("Debugger protocol error: expected OK, but got: " + JavaProtocol.messageToString(message)); } } }); } private void where() { this.enqueueCommand(debugger.Command.WhereCurrentThread(false), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId != JavaProtocol.IdThreadsWhere) { DebugProcess.this.error ("Debugger protocol error: expected " + "IdThreadsWhere, but got: " + JavaProtocol.messageToString(message)); return; } getSession().positionReached (new SuspendContext(DebugProcess.this.mProject, DebugProcess.this.mModule, message)); } }); } private void enqueueCommand(final debugger.Command command, MessageListener listener) { // System.out.println("Writing command: " + // JavaProtocol.commandToString(command)); try { synchronized (this) { if (mDebugSocket == null) { mDeferredQueue.add(Pair.create(command, listener)); return; } mListenerQueue.add(listener); final OutputStream os = mDebugSocket.getOutputStream(); mWriteQueue.add(new Runnable() { public void run() { try { JavaProtocol.writeCommand(os, command); } catch (RuntimeException e) { DebugProcess.this.error ("Debugger protocol error: exception while writing " + "command " + JavaProtocol.commandToString(command) + ": " + e); } } }); } } catch (IOException e) { DebugProcess.this.error ("Debugger error: exception queueing write " + "command " + JavaProtocol.commandToString(command) + ": " + e); } } private void readLoop() throws IOException { java.net.ServerSocket serverSocket; synchronized (this) { serverSocket = mServerSocket; } // Don't synchronize around the accept. It locks up the rest of the debugger still // running on the AWT thread if the application isn't starting correctly. java.net.Socket debugSocket = serverSocket.accept(); synchronized (this) { mDebugSocket = debugSocket; mServerSocket.close(); mServerSocket = null; JavaProtocol.readClientIdentification (mDebugSocket.getInputStream()); // XXX: Put this on the write thread/queue, instead of just posting it? JavaProtocol.writeServerIdentification (mDebugSocket.getOutputStream()); // Enqueue a classList callback to populate the class list this.enqueueCommand(debugger.Command.Classes(null), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdClasses) { DebugProcess.this.handlePartialClassList ((debugger.ClassList)message.params.__a[0]); } } }); } while (true) { synchronized (this) { debugSocket = mDebugSocket; } if (debugSocket == null) { break; } debugger.Message message = JavaProtocol.readMessage (debugSocket.getInputStream()); // System.out.println("Received message: " + // JavaProtocol.messageToString(message)); int messageId = JavaProtocol.getMessageId(message); if (messageId == JavaProtocol.IdThreadCreated) { // Console it out } else if (messageId == JavaProtocol.IdThreadTerminated) { // Console it out } else if (messageId == JavaProtocol.IdThreadStarted) { // Console it out } else if (messageId == JavaProtocol.IdThreadStopped) { if (mStoppedOnce) { // Send a where to solicit current thread stack frame this.where(); } else { mStoppedOnce = true; while (!mDeferredQueue.isEmpty()) { Pair<debugger.Command, MessageListener> p = mDeferredQueue.removeFirst(); this.enqueueCommand(p.getFirst(), p.getSecond()); } this.resume(); } } else { MessageListener listener = null; synchronized (this) { if (!mListenerQueue.isEmpty()) { listener = mListenerQueue.removeFirst(); } } if (listener == null) { DebugProcess.this.error ("Debugger protocol error: unsolicited response: " + JavaProtocol.messageToString(message)); break; } else { listener.handleMessage(messageId, message); } } } } private void handlePartialClassList(debugger.ClassList classList) { while (true) { if (classList.index == 0) { // Terminator break; } if (classList.index == 1) { // Continued this.enqueueCommand (debugger.Command.Classes ((String)classList.params.__a[0]), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdClasses) { DebugProcess.this.handlePartialClassList ((debugger.ClassList) message.params.__a[0]); } else { throw new RuntimeException ("Unexpected message in response " + "to class list request: " + JavaProtocol.messageToString(message)); } } }); break; } // Element if (((Boolean)classList.params.__a[1]).booleanValue()) { mClassesWithStatics.addElement ((String)classList.params.__a[0]); } classList = (debugger.ClassList)classList.params.__a[2]; } } private void registerBreakpoint (@NotNull final XLineBreakpoint<XBreakpointProperties> breakpoint) { final XSourcePosition position = breakpoint.getSourcePosition(); if (position == null) { return; } String path = getRelativePath(mProject, position.getFile()); DebugProcess.this.enqueueCommand (debugger.Command.AddFileLineBreakpoint (path, position.getLine() + 1), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdFileLineBreakpointNumber) { mMap.put(breakpoint, (Integer)(message.params.__a[0])); } else { getSession().updateBreakpointPresentation (breakpoint, AllIcons.Debugger.Db_invalid_breakpoint, null); DebugProcess.this.warn("Cannot set breakpoint"); } } }); } private void unregisterBreakpoint (@NotNull final XLineBreakpoint<XBreakpointProperties> breakpoint, final boolean temporary) { if (!mMap.containsKey(breakpoint)) { return; } int id = mMap.remove(breakpoint); DebugProcess.this.enqueueCommand (debugger.Command.DeleteBreakpointRange(id, id), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { // Could verify that the response was Deleted ... } }); } private XBreakpointHandler<?>[] createBreakpointHandlers() { return new XBreakpointHandler<?>[] { new XBreakpointHandler<XLineBreakpoint<XBreakpointProperties>> (HaxeBreakpointType.class) { public void registerBreakpoint(@NotNull final XLineBreakpoint<XBreakpointProperties> breakpoint) { DebugProcess.this.registerBreakpoint(breakpoint); } public void unregisterBreakpoint(@NotNull final XLineBreakpoint<XBreakpointProperties> breakpoint, final boolean temporary) { DebugProcess.this.unregisterBreakpoint (breakpoint, temporary); } } }; } private abstract class MessageListener { public abstract void handleMessage(int messageId, debugger.Message message); } private class SuspendContext extends XSuspendContext { public SuspendContext(Project project, Module module, debugger.Message threadsWhereMessages) { mExecutionStacks = this.buildWhereList (project, module, (debugger.ThreadWhereList) threadsWhereMessages.params.__a[0]). toArray(new XExecutionStack[0]); } public XExecutionStack getActiveExecutionStack() { return ((mExecutionStacks.length > 0) ? mExecutionStacks[0] : null); } public XExecutionStack[] getExecutionStacks() { return mExecutionStacks; } private Vector<XExecutionStack> buildWhereList (Project project, Module module, debugger.ThreadWhereList whereList) { Vector<XExecutionStack> executionStacks = new Vector<XExecutionStack>(); this.addWhereList(project, module, executionStacks, whereList); return executionStacks; } private void addWhereList(Project project, Module module, Vector<XExecutionStack> executionStacks, debugger.ThreadWhereList whereList) { if (whereList == debugger.ThreadWhereList.Terminator) { return; } int number = ((Integer)whereList.params.__a[0]).intValue(); debugger.ThreadStatus status = (debugger.ThreadStatus) whereList.params.__a[1]; debugger.FrameList frameList = (debugger.FrameList) whereList.params.__a[2]; executionStacks.addElement (new ExecutionStack(project, module, number, frameList)); this.addWhereList(project, module, executionStacks, (debugger.ThreadWhereList) whereList.params.__a[3]); } private XExecutionStack[] mExecutionStacks; } private class ExecutionStack extends XExecutionStack { public ExecutionStack(Project project, Module module, int number, debugger.FrameList frameList) { super("Thread " + number); mStackFrames = new Vector<XStackFrame>(); this.addFrameList(project, module, frameList); } public XStackFrame getTopFrame() { return ((mStackFrames.size() > 0) ? mStackFrames.elementAt(0) : null); } public void computeStackFrames(int firstFrameIndex, XStackFrameContainer container) { if (firstFrameIndex < mStackFrames.size()) { container.addStackFrames (mStackFrames.subList(firstFrameIndex, mStackFrames.size() - 1), true); } } private void addFrameList(Project project, Module module, debugger.FrameList frameList) { if (frameList == debugger.FrameList.Terminator) { return; } mStackFrames.addElement(new StackFrame(project, module, frameList)); this.addFrameList(project, module, (debugger.FrameList) frameList.params.__a[6]); } private Vector<XStackFrame> mStackFrames; } private class StackFrame extends XStackFrame { public StackFrame(Project project, Module module, debugger.FrameList frameList) { mFrameNumber = (Integer)frameList.params.__a[1]; mFileName = (String)frameList.params.__a[4]; mLineNumber = (((Integer)frameList.params.__a[5]).intValue()); mClassAndFunctionName = ((String)frameList.params.__a[2] + "." + (String)frameList.params.__a[3]); VirtualFile file = null; String fileName = VfsUtil.extractFileName(mFileName); if (fileName == null) { fileName = mFileName; } java.util.Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName (project, fileName, GlobalSearchScope.moduleScope(module)); if (files.isEmpty()) { files = FilenameIndex.getVirtualFilesByName (project, fileName, GlobalSearchScope.allScope(project)); } java.util.Collection<VirtualFile> matches = new THashSet<VirtualFile>(); if (!files.isEmpty()) { for (VirtualFile f : files) { if (f.getPath().endsWith(mFileName)) { matches.add(f); } } } if (matches.isEmpty()) { // If we don't have a match yet, then walk the classpath looking for // an appropriate file. file = HaxelibClasspathUtils.findFileOnClasspath(module, mFileName); } else if (matches.size() == 1) { // Got one. If it's a good file, keep it. Otherwise, try to pick it // out of the classpath. VirtualFile possible = matches.iterator().next(); file = possible.isValid() ? possible : HaxelibClasspathUtils.findFileOnClasspath(module, possible.toString()); } else { // Too many matches. Get the first that occurs on the classpath. file = HaxelibClasspathUtils.findFirstFileOnClasspath(module, matches); } // Now, work around the fact that IDEA treats symlinks as separate files. // XXX: This should be controlled via an UI option. if (null != file && file.is(VFileProperty.SYMLINK)) { file = file.getCanonicalFile(); } mSourcePosition = XSourcePositionImpl.create(file, mLineNumber - 1); } public Object getEqualityObject() { return (mFileName + mClassAndFunctionName).intern(); } public XDebuggerEvaluator getEvaluator() { return new XDebuggerEvaluator() { public void evaluate (@NotNull String expression, @NotNull XEvaluationCallback callback, XSourcePosition expressionPosition) { callback.evaluated(new Value(expression)); } }; } public XSourcePosition getSourcePosition() { return mSourcePosition; } @Override public void computeChildren(@NotNull final XCompositeNode node) { // Move to the stack frame DebugProcess.this.enqueueCommand (debugger.Command.SetFrame(mFrameNumber), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdThreadLocation) { StackFrame.this.computeChildrenCurrentFrame (node); } else { DebugProcess.this.warn ("Failed to set stack frame to " + mFrameNumber + "; got message; " + JavaProtocol.messageToString(message)); } } // Get var names }); } public void customizePresentation (@NotNull ColoredTextContainer component) { SimpleTextAttributes attr = (mSourcePosition == null) ? SimpleTextAttributes.GRAYED_ATTRIBUTES : SimpleTextAttributes.REGULAR_ATTRIBUTES; component.append(mClassAndFunctionName + " [" + mFileName + ":" + mLineNumber + "]", attr); component.setIcon(AllIcons.Debugger.StackFrame); } private void computeChildrenCurrentFrame (@NotNull final XCompositeNode node) { DebugProcess.this.enqueueCommand (debugger.Command.Variables(false), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdVariables) { XValueChildrenList childrenList = new XValueChildrenList(); debugger.StringList stringList = (debugger.StringList) message.params.__a[0]; addChildren(childrenList, stringList); if (true) { node.addChildren(childrenList, true); } else { // Note: Removed because it cluttered the variable list. // It's a candidate for reinstatement, possibly with // a control variable. node.addChildren(childrenList, false); // Add all statics to the list of variables. addStaticChildren(node); } } else { DebugProcess.this.warn ("Failed to get variables; got message " + JavaProtocol.messageToString(message)); } } }); } private void addChildren(XValueChildrenList childrenList, debugger.StringList stringList) { while (true) { if (stringList == debugger.StringList.Terminator) { break; } String string = (String)stringList.params.__a[0]; if (! isIntermediateVariableName(string)) { childrenList.add(string, new Value(string)); } stringList = (debugger.StringList)stringList.params.__a[1]; } } /** Determines whether a variable name has been introduced by * the compiler's target backend (e.g. hxcpp). */ private boolean isIntermediateVariableName(String s) { return s.startsWith("_g"); } private void addStaticChildren(@NotNull final XCompositeNode node) { XValueChildrenList childrenList = new XValueChildrenList(); for (String c : DebugProcess.this.mClassesWithStatics) { childrenList.add("statics of " + c, new Value(c, true)); } node.addChildren(childrenList, true); } private class Value extends XValue { public Value(String name) { mName = name; mExpression = name; } public Value(String name, boolean isClassStatics) { if (!isClassStatics) { mName = name; mExpression = name; return; } // Indirect so that the value is not fetched immediately // by the UI - the user has to click to see the statics, // otherwise, all statics of all classes would be fetched // on every breakpoint which would suck mName = "statics of " + name; mExpression = name; mIcon = AllIcons.Debugger.Value; mType = ""; mValue = ""; mChildren = new LinkedList<Value>(); mChildren.add(new Value(name)); } public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) { // If no icon has been calculated, then the value must be // fetched if (mIcon == null) { this.fetchValue(node, place); return; } setPresentation(node); } private void setPresentation(@NotNull XValueNode node) { node.setPresentation (mIcon, mType, mValue, (mChildren != null)); } // getModifierPsi() is temporarily disabled as it does not work // due to PSI errors in the haxe PSI tree. // public XValueModifier getModifierPsi() // { // return new XValueModifier() // { // public void setValue(@NotNull String expression, // @NotNull final XModificationCallback callback) // { // System.out.println("Setting value of " + // Value.this.mName + " with " + // "expression " + // Value.this.mExpression + // " to " + expression); // DebugProcess.this.enqueueCommand // (debugger.Command.SetExpression // (false, mExpression, expression), // new MessageListener() // { // public void handleMessage(int messageId, // debugger.Message message) // { // // Just indicate that the value was // // modified, it may end up being the same // // value if the set expression failed // callback.valueModified(); // } // }); // } // // public String getInitialValueEditorText() // { // System.out.println("Getting initial value of " + // Value.this.mName + " as " + // Value.this.mValue); // return Value.this.mValue; // } // }; // } public String getEvaluationExpression() { return mExpression; } @Override public boolean canNavigateToSource() { return false; } @Override public boolean canNavigateToTypeSource() { // XXX todo -- implement source navigation for class types return false; } @Override public void computeChildren(@NotNull final XCompositeNode node) { internalComputeChildren(node); } private void internalComputeChildren(@NotNull Obsolescent node) { // mWaitingforChildrenResults tells us wether a request for // the children is already in flight. In the case that one // is, we do NOT want to call node.addChildren(list,true), // because that tells the UI that we have computed all of the // children for this node. // // mChildrenComputationRequested notifies the fetchValue // callback that the UI has attempted to draw any elements // and won't request them again, so the callback should // notify the UI that new children are available (by calling // this function again). if (mChildren == null || mWaitingForChildrenResults) { mChildrenComputationRequested = true; return; } mChildrenComputationRequested = false; XValueChildrenList childrenList = new XValueChildrenList(mChildren.size()); for (Value child : mChildren) { childrenList.add(child.mName, child); } // Stupid hack to get around the fact that we're abusing the APIs. if (node instanceof XValueNodeImpl) { ((XValueNodeImpl)node).addChildren(childrenList, true); } else if(node instanceof XCompositeNode) { ((XCompositeNode)node).addChildren(childrenList, true); } else { error("Unexpected node type in debugger screen: " + node.getClass().toString()); } } private void fetchValue(@NotNull final XValueNode node, @NotNull final XValuePlace place) { mWaitingForChildrenResults = true; DebugProcess.this.enqueueCommand (debugger.Command.GetStructured(false, mExpression), new MessageListener() { public void handleMessage(int messageId, debugger.Message message) { if (messageId == JavaProtocol.IdStructured) { debugger.StructuredValue structuredValue = (debugger.StructuredValue) message.params.__a[0]; Value.this.fromStructuredValue (structuredValue); } else { mIcon = AllIcons.General.Error; mValue = mType = "<Unavailable - " + getErrorString(message) + ">"; } // If fromStructuredValue contained a list, the nodes // need to be added to the UI. The UI usually requests // them via computeChildren(). In some cases, // computeChildren() is called before we have retrieved // the results, and we need to re-trigger the computation. mWaitingForChildrenResults = false; if (mChildrenComputationRequested) { Value.this.internalComputeChildren(node); } Value.this.setPresentation(node); } }); } private void fromStructuredValue (debugger.StructuredValue structuredValue) { if (structuredValue.index == 0) { // Elided mExpression = (String)structuredValue.params.__a[1]; } else if (structuredValue.index == 1) { // Single debugger.StructuredValueType type = (debugger.StructuredValueType) structuredValue.params.__a[0]; String value = (String) structuredValue.params.__a[1]; mIcon = AllIcons.Debugger.Value; mType = getTypeString(type); mValue = stripErrorAdornments(value); } else if (structuredValue.index == 2) { // List debugger.StructuredValueListType type = (debugger.StructuredValueListType) structuredValue.params.__a[0]; debugger.StructuredValueList list = (debugger.StructuredValueList) structuredValue.params.__a[1]; mIcon = AllIcons.Debugger.Value; mType = getTypeString(type); mValue = ""; mChildren = new LinkedList<Value>(); this.addChildren(list); } // Anything else, including Elided, is an error else { mIcon = AllIcons.General.Error; mValue = mType = "<Unavailable>"; } } private void addChildren(debugger.StructuredValueList list) { if (list == debugger.StructuredValueList.Terminator) { return; } String name = (String)list.params.__a[0]; debugger.StructuredValue structuredValue = (debugger.StructuredValue)list.params.__a[1]; debugger.StructuredValueList next = (debugger.StructuredValueList)list.params.__a[2]; Value val = new Value(name); val.fromStructuredValue(structuredValue); mChildren.add(val); addChildren(next); } private String getTypeString(debugger.StructuredValueType type) { if (type.index == 0) { return "Null"; } else if (type.index == 1) { return "Bool"; } else if (type.index == 2) { return "Int"; } else if (type.index == 3) { return "Float"; } else if (type.index == 4) { return "String"; } else if (type.index == 5) { return (String)type.params.__a[0]; } else if (type.index == 6) { return (String)type.params.__a[0]; } else if (type.index == 7) { return "{ ... }"; } else if (type.index == 8) { return (String)type.params.__a[0]; } else if (type.index == 9) { return "Function"; } else { return "<Unavailable>"; } } private String getTypeString (debugger.StructuredValueListType type) { if (type.index == 0) { return "{ ... }"; } else if (type.index == 1) { return (String)type.params.__a[0]; } else if (type.index == 2) { return "Array"; } else { return "<Unavailable>"; } } private String getErrorString(debugger.Message debugMessage) { return stripErrorAdornments(debugMessage.toString()); } private String stripErrorAdornments(String message) { // Strip the enumeration text. Pattern wrapperPattern = Pattern.compile("ErrorEvaluatingExpression\\((.*)\\)", Pattern.DOTALL); Matcher m = wrapperPattern.matcher(message); String description = m.matches() ? m.group(1) : message; // Strip the call stack. final String callStackMarker = "\nCalled from "; if (description.contains(callStackMarker)) { description = description.substring(0, description.indexOf(callStackMarker)); } return description; } private String mName; private String mExpression; private javax.swing.Icon mIcon; private String mType; private String mValue; private LinkedList<Value> mChildren; // These two manage how/when the child nodes are fetched. // See internalComputeChildren for an explanation. private boolean mWaitingForChildrenResults; private boolean mChildrenComputationRequested; } private int mFrameNumber; private String mFileName; private int mLineNumber; private String mClassAndFunctionName; private XSourcePosition mSourcePosition; } private Vector<String> mClassesWithStatics; private Project mProject; private Module mModule; private boolean mStoppedOnce; private LinkedList<Pair<debugger.Command, MessageListener>> mDeferredQueue; private QueueProcessor<Runnable> mWriteQueue; private LinkedList<MessageListener> mListenerQueue; private java.net.ServerSocket mServerSocket; private java.net.Socket mDebugSocket; private ExecutionResult mExecutionResult; private XBreakpointHandler[] mBreakpointHandlers; private HashMap<XLineBreakpoint<XBreakpointProperties>, Integer> mMap; } private static String getRelativePath(Project project, VirtualFile file) { PsiFile psiFile = PsiManager.getInstance(project).findFile(file); String packageName = HaxeResolveUtil.getPackageName(psiFile); String fileName = VfsUtil.extractFileName(file.getPath()); return getPath(packageName, fileName); } private static String getPath(String packageName, String fileName) { if (StringUtil.isEmpty(packageName)) { return fileName; } return packageName.replaceAll("\\.", "/") + "/" + fileName; } private static void showInfoMessage(final Project project, final String message, final String title) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { Messages.showInfoMessage(project, message, title); } }); } }