/* * Copyright (C) 2010-2024 JPEXS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.jpexs.decompiler.flash.gui; import com.jpexs.debugger.flash.DebugConnectionListener; import com.jpexs.debugger.flash.DebugMessageListener; import com.jpexs.debugger.flash.Debugger; import com.jpexs.debugger.flash.DebuggerCommands; import com.jpexs.debugger.flash.DebuggerConnection; import com.jpexs.debugger.flash.Variable; import com.jpexs.debugger.flash.VariableFlags; import com.jpexs.debugger.flash.VariableType; import com.jpexs.debugger.flash.messages.in.InBreakAt; import com.jpexs.debugger.flash.messages.in.InBreakAtExt; import com.jpexs.debugger.flash.messages.in.InBreakReason; import com.jpexs.debugger.flash.messages.in.InCallFunction; import com.jpexs.debugger.flash.messages.in.InConstantPool; import com.jpexs.debugger.flash.messages.in.InContinue; import com.jpexs.debugger.flash.messages.in.InErrorException; import com.jpexs.debugger.flash.messages.in.InFrame; import com.jpexs.debugger.flash.messages.in.InGetVariable; import com.jpexs.debugger.flash.messages.in.InNumScript; import com.jpexs.debugger.flash.messages.in.InProcessTag; import com.jpexs.debugger.flash.messages.in.InScript; import com.jpexs.debugger.flash.messages.in.InSetBreakpoint; import com.jpexs.debugger.flash.messages.in.InSwfInfo; import com.jpexs.debugger.flash.messages.in.InTrace; import com.jpexs.debugger.flash.messages.in.InVersion; import com.jpexs.debugger.flash.messages.out.OutAddWatch2; import com.jpexs.debugger.flash.messages.out.OutGetBreakReason; import com.jpexs.debugger.flash.messages.out.OutPlay; import com.jpexs.debugger.flash.messages.out.OutProcessedTag; import com.jpexs.debugger.flash.messages.out.OutRewind; import com.jpexs.debugger.flash.messages.out.OutSwfInfo; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.graph.DottedChain; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author JPEXS */ public class DebuggerHandler implements DebugConnectionListener { private boolean connected = false; private DebuggerCommands commands = null; //private List swfs = new ArrayList<>(); private boolean paused = true; private Map modulePaths = new HashMap<>(); private Map moduleToSwfIndex = new HashMap<>(); //Marks swfIndices that are fully loaded - at least one break was on it (including onloaded break) private Set swfIndicesCommitted = new HashSet<>(); private Map swfIndicesNewToSwfHash = new HashMap<>(); private Map scriptToModule = new HashMap<>(); private Map moduleToTraitIndex = new HashMap<>(); private Map moduleToClassIndex = new HashMap<>(); private Map moduleToMethodIndex = new HashMap<>(); private Map>> toAddBPointMap = new WeakHashMap<>(); private Map>> confirmedPointMap = new WeakHashMap<>(); private Map>> invalidBreakPointMap = new WeakHashMap<>(); private Map>> toRemoveBPointMap = new WeakHashMap<>(); private int breakIp = -1; private String breakScriptName = null; private List stackScriptNames = new ArrayList<>(); private List stackLines = new ArrayList<>(); private List debuggedSwfs = new ArrayList<>(); public static class ActionScriptException extends Exception { private String errorClass; public ActionScriptException(String errorClass, String message) { super(message); this.errorClass = errorClass; } public ActionScriptException(String errorClsMessage) { this(errorClsMessage.substring(0, errorClsMessage.indexOf(": ")), errorClsMessage.substring(errorClsMessage.indexOf(": ") + 2)); } public String getErrorClass() { return errorClass; } } public void setMainDebuggedSwf(SWF debuggedSwf) { debuggedSwfs.clear(); //debuggedSwfs.add(debuggedSwf); } public List getDebuggedSwfs() { return debuggedSwfs; } public int getBreakIp() { if (!isPaused()) { return -1; } return breakIp; } public int getDepth() { return depth; } public String getBreakScriptName() { if (!isPaused()) { return "-"; } return breakScriptName; } public synchronized List getStackScripts() { if (!isPaused()) { return new ArrayList<>(); } return stackScriptNames; } public synchronized List getStackLines() { if (!isPaused()) { return new ArrayList<>(); } return stackLines; } public InGetVariable getVariable(long parentId, String varName, boolean children, boolean useGetter) { try { return commands.getVariable(parentId, varName, useGetter, children); } catch (IOException ex) { return null; } } public void setVariable(long parentId, String varName, int valueType, Object value) { try { String svalue = ""; switch (valueType) { case VariableType.STRING: svalue = "" + value; break; case VariableType.NUMBER: svalue = "" + value; break; case VariableType.BOOLEAN: svalue = ((Boolean) value) ? "true" : "false"; break; case VariableType.UNDEFINED: svalue = "undefined"; break; case VariableType.NULL: svalue = "undefined"; break; } commands.setVariable(parentId, varName, valueType, svalue); } catch (IOException ex) { //ignore } } public synchronized void removeBreakPoint(SWF swf, String scriptName, int line) { if (isBreakpointInvalid(swf, scriptName, line)) { invalidBreakPointMap.get(swf).get(scriptName).remove(line); if (invalidBreakPointMap.get(swf).get(scriptName).isEmpty()) { invalidBreakPointMap.get(swf).remove(scriptName); } return; } if (isBreakpointToAdd(swf, scriptName, line)) { toAddBPointMap.get(swf).get(scriptName).remove(line); if (toAddBPointMap.get(swf).get(scriptName).isEmpty()) { toAddBPointMap.get(swf).remove(scriptName); } } else if (isBreakpointConfirmed(swf, scriptName, line)) { if (!toRemoveBPointMap.containsKey(swf)) { toRemoveBPointMap.put(swf, new HashMap<>()); } if (!toRemoveBPointMap.get(swf).containsKey(scriptName)) { toRemoveBPointMap.get(swf).put(scriptName, new TreeSet<>()); } toRemoveBPointMap.get(swf).get(scriptName).add(line); } try { sendBreakPoints(); } catch (IOException ex) { //ignore } } private int watchTag = 1; public synchronized com.jpexs.debugger.flash.DebuggerCommands.Watch addWatch(Variable v, long v_id, boolean watchRead, boolean watchWrite) { int tag = watchTag++; try { return commands.addWatch(v_id, v.name, (watchRead ? OutAddWatch2.FLAG_READ : 0) | (watchWrite ? OutAddWatch2.FLAG_WRITE : 0), tag); } catch (IOException ex) { return null; } } public synchronized Set getBreakPoints(SWF swf, String scriptName, boolean onlyValid) { Set lines = new TreeSet<>(); if (confirmedPointMap.containsKey(swf) && confirmedPointMap.get(swf).containsKey(scriptName)) { lines.addAll(confirmedPointMap.get(swf).get(scriptName)); } if (toAddBPointMap.containsKey(swf) && toAddBPointMap.get(swf).containsKey(scriptName)) { lines.addAll(toAddBPointMap.get(swf).get(scriptName)); } if (!onlyValid && invalidBreakPointMap.containsKey(swf) && invalidBreakPointMap.get(swf).containsKey(scriptName)) { lines.addAll(invalidBreakPointMap.get(swf).get(scriptName)); } return lines; } public synchronized void clearBreakPoints(SWF swf) { Map> breakpoints = getAllBreakPoints(swf, false); for (String scriptName : breakpoints.keySet()) { for (int line : breakpoints.get(scriptName)) { removeBreakPoint(swf, scriptName, line); } } } public synchronized void makeBreakPointsUnconfirmed(SWF swf) { if (confirmedPointMap.containsKey(swf)) { for (String scriptName : confirmedPointMap.get(swf).keySet()) { if (!toAddBPointMap.containsKey(swf)) { toAddBPointMap.put(swf, new HashMap<>()); } if (!toAddBPointMap.get(swf).containsKey(scriptName)) { toAddBPointMap.get(swf).put(scriptName, new TreeSet<>()); } toAddBPointMap.get(swf).get(scriptName).addAll(confirmedPointMap.get(swf).get(scriptName)); } confirmedPointMap.get(swf).clear(); } if (invalidBreakPointMap.containsKey(swf)) { for (String scriptName : invalidBreakPointMap.get(swf).keySet()) { if (!toAddBPointMap.containsKey(swf)) { toAddBPointMap.put(swf, new HashMap<>()); } if (!toAddBPointMap.get(swf).containsKey(scriptName)) { toAddBPointMap.get(swf).put(scriptName, new TreeSet<>()); } toAddBPointMap.get(swf).get(scriptName).addAll(invalidBreakPointMap.get(swf).get(scriptName)); } invalidBreakPointMap.get(swf).clear(); } } public synchronized Map> getAllBreakPoints(SWF swf, boolean validOnly) { Map> ret = new HashMap<>(); if (confirmedPointMap.containsKey(swf)) { for (String scriptName : confirmedPointMap.get(swf).keySet()) { Set lines = new TreeSet<>(); lines.addAll(confirmedPointMap.get(swf).get(scriptName)); ret.put(scriptName, lines); } } if (toAddBPointMap.containsKey(swf)) { for (String scriptName : toAddBPointMap.get(swf).keySet()) { if (!ret.containsKey(scriptName)) { ret.put(scriptName, new TreeSet<>()); } ret.get(scriptName).addAll(toAddBPointMap.get(swf).get(scriptName)); } } if (!validOnly) { if (invalidBreakPointMap.containsKey(swf)) { for (String scriptName : invalidBreakPointMap.get(swf).keySet()) { if (!ret.containsKey(scriptName)) { ret.put(scriptName, new TreeSet<>()); } ret.get(scriptName).addAll(invalidBreakPointMap.get(swf).get(scriptName)); } } } return ret; } public boolean addBreakPoint(SWF swf, String scriptName, int line) { synchronized (this) { Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "adding bp {0}:{1}", new Object[]{scriptName, line}); if (isBreakpointToRemove(swf, scriptName, line)) { toRemoveBPointMap.get(swf).get(scriptName).remove(line); if (toRemoveBPointMap.get(swf).get(scriptName).isEmpty()) { toRemoveBPointMap.get(swf).remove(scriptName); } } if (isBreakpointConfirmed(swf, scriptName, line)) { Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp {0}:{1} already confirmed", new Object[]{scriptName, line}); return true; } if (isBreakpointInvalid(swf, scriptName, line)) { Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp {0}:{1} already invalid", new Object[]{scriptName, line}); return false; } if (!toAddBPointMap.containsKey(swf)) { toAddBPointMap.put(swf, new HashMap<>()); } if (!toAddBPointMap.get(swf).containsKey(scriptName)) { toAddBPointMap.get(swf).put(scriptName, new TreeSet<>()); } toAddBPointMap.get(swf).get(scriptName).add(line); Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp {0}:{1} added to todo", new Object[]{scriptName, line}); } try { sendBreakPoints(); } catch (IOException ex) { //ignored } return true; } public synchronized boolean isBreakpointConfirmed(SWF swf, String scriptName, int line) { return confirmedPointMap.containsKey(swf) && confirmedPointMap.get(swf).containsKey(scriptName) && confirmedPointMap.get(swf).get(scriptName).contains(line); } public synchronized boolean isBreakpointToAdd(SWF swf, String scriptName, int line) { return toAddBPointMap.containsKey(swf) && toAddBPointMap.get(swf).containsKey(scriptName) && toAddBPointMap.get(swf).get(scriptName).contains(line); } public synchronized boolean isBreakpointToRemove(SWF swf, String scriptName, int line) { return toRemoveBPointMap.containsKey(swf) && toRemoveBPointMap.get(swf).containsKey(scriptName) && toRemoveBPointMap.get(swf).get(scriptName).contains(line); } public synchronized boolean isBreakpointInvalid(SWF swf, String scriptName, int line) { return invalidBreakPointMap.containsKey(swf) && invalidBreakPointMap.get(swf).containsKey(scriptName) && invalidBreakPointMap.get(swf).get(scriptName).contains(line); } private synchronized void markBreakPointInvalid(SWF swf, String scriptName, int line) { if (!invalidBreakPointMap.containsKey(swf)) { invalidBreakPointMap.put(swf, new HashMap<>()); } if (!invalidBreakPointMap.get(swf).containsKey(scriptName)) { invalidBreakPointMap.get(swf).put(scriptName, new TreeSet<>()); } invalidBreakPointMap.get(swf).get(scriptName).add(line); } private synchronized void markBreakPointConfirmed(SWF swf, String scriptName, int line) { if (!confirmedPointMap.containsKey(swf)) { confirmedPointMap.put(swf, new HashMap<>()); } if (!confirmedPointMap.get(swf).containsKey(scriptName)) { confirmedPointMap.get(swf).put(scriptName, new TreeSet<>()); } confirmedPointMap.get(swf).get(scriptName).add(line); } private synchronized void markBreakPointValid(SWF swf, String scriptName, int line) { if (!invalidBreakPointMap.containsKey(swf)) { return; } if (!invalidBreakPointMap.get(swf).containsKey(scriptName)) { return; } invalidBreakPointMap.get(swf).get(scriptName).remove(line); if (invalidBreakPointMap.get(swf).get(scriptName).isEmpty()) { invalidBreakPointMap.get(swf).remove(scriptName); } if (invalidBreakPointMap.get(swf).isEmpty()) { invalidBreakPointMap.remove(swf); } } private InFrame frame; private int depth; private InConstantPool pool; private InBreakAtExt breakInfo; private InBreakReason breakReason; private final List breakListeners = new CopyOnWriteArrayList<>(); private final List traceListeners = new ArrayList<>(); private final List errorListeners = new ArrayList<>(); private final List frameChangeListeners = new ArrayList<>(); private final List clisteners = new ArrayList<>(); public void setDepth(int depth) { this.depth = depth; refreshFrame(); } public String moduleToString(int file) { if (!modulePaths.containsKey(file)) { return "unknown"; } return modulePaths.get(file); } public Integer moduleToMethodIndex(int file) { return moduleToMethodIndex.get(file); } public Integer moduleToClassIndex(int file) { return moduleToClassIndex.get(file); } public Integer moduleToTraitIndex(int file) { return moduleToTraitIndex.get(file); } public synchronized InBreakAtExt getBreakInfo() { if (!paused) { return null; } return breakInfo; } public synchronized InBreakReason getBreakReason() { if (!paused) { return null; } return breakReason; } public static interface ConnectionListener { public void connected(); public void disconnected(); } public static interface TraceListener { public void trace(String... val); } public static interface ErrorListener { public void errorException(String message, Variable thrownVar); } public static interface FrameChangeListener { public void frameChanged(); } public static interface BreakListener { public void breakAt(String scriptName, int line, int classIndex, int traitIndex, int methodIndex); public void doContinue(); } public void addBreakListener(BreakListener l) { breakListeners.add(l); } public void addFrameChangeListener(FrameChangeListener l) { frameChangeListeners.add(l); } public void removeFrameChangeListener(FrameChangeListener l) { frameChangeListeners.remove(l); } public void addTraceListener(TraceListener l) { traceListeners.add(l); } public void removeTraceListener(TraceListener l) { traceListeners.remove(l); } public void addErrorListener(ErrorListener l) { errorListeners.add(l); } public void removeErrorListener(ErrorListener l) { errorListeners.remove(l); } public void removeBreakListener(BreakListener l) { breakListeners.remove(l); } public void addConnectionListener(ConnectionListener l) { clisteners.add(l); } public void removeConnectionListener(ConnectionListener l) { clisteners.remove(l); } public synchronized void refreshFrame() { if (!paused) { return; } try { frame = commands.getFrame(depth); pool = commands.getConstantPool(0); } catch (IOException ex) { //ignore } for (FrameChangeListener l : frameChangeListeners) { l.frameChanged(); } } public synchronized InFrame getFrame() { if (!paused) { return null; } return frame; } public synchronized int moduleIdOf(String packWithHash) { if (!scriptToModule.containsKey(packWithHash)) { return -1; } return scriptToModule.get(packWithHash); } public boolean isPaused() { if (!isConnected()) { return false; } synchronized (this) { return paused; } } public void disconnect() { frame = null; pool = null; breakInfo = null; breakReason = null; connected = false; if (commands != null) { commands.disconnect(); } commands = null; synchronized (this) { for (SWF debuggedSwf : debuggedSwfs) { if (confirmedPointMap.containsKey(debuggedSwf)) { for (String scriptName : confirmedPointMap.get(debuggedSwf).keySet()) { if (!toAddBPointMap.containsKey(debuggedSwf)) { toAddBPointMap.put(debuggedSwf, new HashMap<>()); } if (!toAddBPointMap.get(debuggedSwf).containsKey(scriptName)) { toAddBPointMap.get(debuggedSwf).put(scriptName, new TreeSet<>()); } toAddBPointMap.get(debuggedSwf).get(scriptName).addAll(confirmedPointMap.get(debuggedSwf).get(scriptName)); } confirmedPointMap.get(debuggedSwf).clear(); } if (invalidBreakPointMap.containsKey(debuggedSwf)) { for (String scriptName : invalidBreakPointMap.get(debuggedSwf).keySet()) { if (!toAddBPointMap.containsKey(debuggedSwf)) { toAddBPointMap.put(debuggedSwf, new HashMap<>()); } if (!toAddBPointMap.get(debuggedSwf).containsKey(scriptName)) { toAddBPointMap.get(debuggedSwf).put(scriptName, new TreeSet<>()); } toAddBPointMap.get(debuggedSwf).get(scriptName).addAll(invalidBreakPointMap.get(debuggedSwf).get(scriptName)); } invalidBreakPointMap.get(debuggedSwf).clear(); } } } for (ConnectionListener l : clisteners) { l.disconnected(); } debuggedSwfs.clear(); } public synchronized boolean isConnected() { return connected; } public DebuggerCommands getCommands() throws IOException { if (!isConnected() || commands == null) { throw new IOException("Not connected"); } return commands; } @Override public void failedListen(IOException ex) { View.execInEventDispatch(new Runnable() { @Override public void run() { disconnect(); Main.stopRun(); Main.stopWork(); ViewMessages.showMessageDialog(Main.getMainFrame().getPanel(), AppStrings.translate("error.debug.listen").replace("%port%", "" + Debugger.DEBUG_PORT)); Main.getMainFrame().getPanel().updateMenu(); } }); } @Override public void connected(DebuggerConnection con) { /*for (SWF debuggedSwf : debuggedSwfs) { makeBreakPointsUnconfirmed(debuggedSwf); }*/ Main.startWork(AppStrings.translate("work.debugging"), null); synchronized (this) { paused = false; } Main.getMainFrame().getPanel().updateMenu(); //enlog(DebuggerConnection.class); //enlog(DebuggerCommands.class); //enlog(DebuggerHandler.class); try { con.getMessage(InVersion.class); } catch (IOException ex) { Logger.getLogger(DebuggerHandler.class.getName()).log(Level.SEVERE, null, ex); } //Respond to InProcessTag with OutProcessedTag con.addMessageListener(new DebugMessageListener() { @Override public void message(InProcessTag message) { try { con.writeMessage(new OutProcessedTag(con)); } catch (IOException ex) { //disconnect(); //ignore } } }); swfIndicesCommitted.clear(); swfIndicesNewToSwfHash.clear(); Map moduleNames = new HashMap<>(); final Pattern patAS3 = Pattern.compile("^(.*);(.*);(.*)\\.as$"); final Pattern patAS3PCode = Pattern.compile("^(?[0-9a-z_]+):#PCODE abc:(?[0-9]+),script:(?