/*
* 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:(?