fix(debugger): resolve IO read problems, proper socket closing (PR #1414)
* fix(debugger): several IO read problems fixed * merged latest changes * fixed read loop * Update jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java Co-authored-by: skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
@@ -33,7 +33,6 @@ import jadx.gui.device.debugger.SmaliDebugger.RuntimeField;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeRegister;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeValue;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.RuntimeVarInfo;
|
||||
import jadx.gui.device.debugger.SmaliDebugger.SmaliDebuggerException;
|
||||
import jadx.gui.device.debugger.smali.Smali;
|
||||
import jadx.gui.device.debugger.smali.SmaliRegister;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
@@ -42,8 +41,6 @@ import jadx.gui.ui.panel.JDebuggerPanel;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.IListElement;
|
||||
import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode;
|
||||
|
||||
import static jadx.gui.device.debugger.SmaliDebugger.RuntimeType;
|
||||
|
||||
public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DebugController.class);
|
||||
|
||||
@@ -359,7 +356,7 @@ public final class DebugController implements SmaliDebugger.SuspendListener, IDe
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuspendEvent(SmaliDebugger.SuspendInfo info) {
|
||||
public void onSuspendEvent(SuspendInfo info) {
|
||||
if (!isDebugging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
import io.github.hqktech.JDWP.Event.Composite.BreakpointEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ClassPrepareEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ClassUnloadEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ExceptionEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.FieldAccessEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.FieldModificationEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodEntryEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodExitEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MethodExitWithReturnValueEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnterEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorContendedEnteredEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.MonitorWaitedEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.SingleStepEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ThreadDeathEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.ThreadStartEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.VMDeathEvent;
|
||||
import io.github.hqktech.JDWP.Event.Composite.VMStartEvent;
|
||||
|
||||
abstract class EventListenerAdapter {
|
||||
void onVMStart(VMStartEvent event) {
|
||||
}
|
||||
|
||||
void onVMDeath(VMDeathEvent event) {
|
||||
}
|
||||
|
||||
void onSingleStep(SingleStepEvent event) {
|
||||
}
|
||||
|
||||
void onBreakpoint(BreakpointEvent event) {
|
||||
}
|
||||
|
||||
void onMethodEntry(MethodEntryEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExit(MethodExitEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEnter(MonitorContendedEnterEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEntered(MonitorContendedEnteredEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWait(MonitorWaitEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWaited(MonitorWaitedEvent event) {
|
||||
}
|
||||
|
||||
void onException(ExceptionEvent event) {
|
||||
}
|
||||
|
||||
void onThreadStart(ThreadStartEvent event) {
|
||||
}
|
||||
|
||||
void onThreadDeath(ThreadDeathEvent event) {
|
||||
}
|
||||
|
||||
void onClassPrepare(ClassPrepareEvent event) {
|
||||
}
|
||||
|
||||
void onClassUnload(ClassUnloadEvent event) {
|
||||
}
|
||||
|
||||
void onFieldAccess(FieldAccessEvent event) {
|
||||
}
|
||||
|
||||
void onFieldModification(FieldModificationEvent event) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
import io.github.hqktech.JDWP;
|
||||
|
||||
public enum RuntimeType {
|
||||
ARRAY(91, "[]"),
|
||||
BYTE(66, "byte"),
|
||||
CHAR(67, "char"),
|
||||
OBJECT(76, "object"),
|
||||
FLOAT(70, "float"),
|
||||
DOUBLE(68, "double"),
|
||||
INT(73, "int"),
|
||||
LONG(74, "long"),
|
||||
SHORT(83, "short"),
|
||||
VOID(86, "void"),
|
||||
BOOLEAN(90, "boolean"),
|
||||
STRING(115, "string"),
|
||||
THREAD(116, "thread"),
|
||||
THREAD_GROUP(103, "thread_group"),
|
||||
CLASS_LOADER(108, "class_loader"),
|
||||
CLASS_OBJECT(99, "class_object");
|
||||
|
||||
private final int jdwpTag;
|
||||
private final String desc;
|
||||
|
||||
RuntimeType(int tag, String desc) {
|
||||
this.jdwpTag = tag;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public int getTag() {
|
||||
return jdwpTag;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a <code>JDWP.Tag</code> to a {@link RuntimeType}
|
||||
*
|
||||
* @param tag
|
||||
* @return
|
||||
* @throws SmaliDebuggerException
|
||||
*/
|
||||
public static RuntimeType fromJdwpTag(int tag) throws SmaliDebuggerException {
|
||||
switch (tag) {
|
||||
case JDWP.Tag.ARRAY:
|
||||
return RuntimeType.ARRAY;
|
||||
case JDWP.Tag.BYTE:
|
||||
return RuntimeType.BYTE;
|
||||
case JDWP.Tag.CHAR:
|
||||
return RuntimeType.CHAR;
|
||||
case JDWP.Tag.OBJECT:
|
||||
return RuntimeType.OBJECT;
|
||||
case JDWP.Tag.FLOAT:
|
||||
return RuntimeType.FLOAT;
|
||||
case JDWP.Tag.DOUBLE:
|
||||
return RuntimeType.DOUBLE;
|
||||
case JDWP.Tag.INT:
|
||||
return RuntimeType.INT;
|
||||
case JDWP.Tag.LONG:
|
||||
return RuntimeType.LONG;
|
||||
case JDWP.Tag.SHORT:
|
||||
return RuntimeType.SHORT;
|
||||
case JDWP.Tag.VOID:
|
||||
return RuntimeType.VOID;
|
||||
case JDWP.Tag.BOOLEAN:
|
||||
return RuntimeType.BOOLEAN;
|
||||
case JDWP.Tag.STRING:
|
||||
return RuntimeType.STRING;
|
||||
case JDWP.Tag.THREAD:
|
||||
return RuntimeType.THREAD;
|
||||
case JDWP.Tag.THREAD_GROUP:
|
||||
return RuntimeType.THREAD_GROUP;
|
||||
case JDWP.Tag.CLASS_LOADER:
|
||||
return RuntimeType.CLASS_LOADER;
|
||||
case JDWP.Tag.CLASS_OBJECT:
|
||||
return RuntimeType.CLASS_OBJECT;
|
||||
default:
|
||||
throw new SmaliDebuggerException("Unexpected value: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
import jadx.gui.device.debugger.smali.RegisterInfo;
|
||||
import jadx.gui.utils.IOUtils;
|
||||
import jadx.gui.utils.ObjectPool;
|
||||
|
||||
// TODO: Finish error notification, inner errors should be logged let user notice.
|
||||
@@ -83,9 +84,9 @@ public class SmaliDebugger {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SmaliDebugger.class);
|
||||
private final JDWP jdwp;
|
||||
private int localTcpPort;
|
||||
private InputStream inputStream;
|
||||
private OutputStream outputStream;
|
||||
private final int localTcpPort;
|
||||
private final InputStream inputStream;
|
||||
private final OutputStream outputStream;
|
||||
|
||||
// All event callbacks will be called in this queue, e.g. class prepare/unload
|
||||
private static final Executor EVENT_LISTENER_QUEUE = Executors.newSingleThreadExecutor();
|
||||
@@ -118,10 +119,13 @@ public class SmaliDebugger {
|
||||
private static final ICommandResult SKIP_RESULT = res -> {
|
||||
};
|
||||
|
||||
private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp) {
|
||||
private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp, InputStream inputStream,
|
||||
OutputStream outputStream) {
|
||||
this.jdwp = jdwp;
|
||||
this.localTcpPort = localTcpPort;
|
||||
this.suspendListener = suspendListener;
|
||||
this.inputStream = inputStream;
|
||||
this.outputStream = outputStream;
|
||||
|
||||
oneOffEventReq = jdwp.eventRequest().cmdSet().newCountRequest();
|
||||
oneOffEventReq.count = 1;
|
||||
@@ -135,6 +139,7 @@ public class SmaliDebugger {
|
||||
try {
|
||||
byte[] bytes = JDWP.IDSizes.encode().getBytes();
|
||||
JDWP.setPacketID(bytes, 1);
|
||||
LOG.debug("Connecting to ADB {}:{}", host, port);
|
||||
Socket socket = new Socket(host, port);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
@@ -143,9 +148,7 @@ public class SmaliDebugger {
|
||||
JDWP jdwp = initJDWP(outputStream, inputStream);
|
||||
socket.setSoTimeout(0); // set back to 0 so the decodingLoop won't break for timeout.
|
||||
|
||||
SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp);
|
||||
debugger.inputStream = inputStream;
|
||||
debugger.outputStream = outputStream;
|
||||
SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp, inputStream, outputStream);
|
||||
|
||||
debugger.decodingLoop();
|
||||
debugger.listenClassUnloadEvent();
|
||||
@@ -262,7 +265,7 @@ public class SmaliDebugger {
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
ObjectReference.GetValues.GetValuesReplyDataValues value = values.get(i);
|
||||
flds.get(i).setValue(value.value.idOrValue)
|
||||
.setType(getType(value.value.tag));
|
||||
.setType(RuntimeType.fromJdwpTag(value.value.tag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +505,7 @@ public class SmaliDebugger {
|
||||
ObjectReference.GetValues.GetValuesReplyData data =
|
||||
jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE);
|
||||
fld.setValue(data.values.get(0).value.idOrValue)
|
||||
.setType(getType(data.values.get(0).value.tag));
|
||||
.setType(RuntimeType.fromJdwpTag(data.values.get(0).value.tag));
|
||||
}
|
||||
|
||||
private long createString(String localStr) throws SmaliDebuggerException {
|
||||
@@ -636,7 +639,7 @@ public class SmaliDebugger {
|
||||
byte[] buf;
|
||||
try {
|
||||
outputStream.write(JDWP.encodeHandShakePacket());
|
||||
buf = readBytes(inputStream, 14);
|
||||
buf = IOUtils.readNBytes(inputStream, 14);
|
||||
} catch (Exception e) {
|
||||
throw new SmaliDebuggerException("jdwp handshake failed", e);
|
||||
}
|
||||
@@ -1128,7 +1131,7 @@ public class SmaliDebugger {
|
||||
@Nullable
|
||||
private static Packet readPacket(InputStream inputStream) throws SmaliDebuggerException {
|
||||
try {
|
||||
byte[] header = readBytes(inputStream, JDWP.PACKET_HEADER_SIZE);
|
||||
byte[] header = IOUtils.readNBytes(inputStream, JDWP.PACKET_HEADER_SIZE);
|
||||
if (header == null) {
|
||||
// stream ended
|
||||
return null;
|
||||
@@ -1137,7 +1140,7 @@ public class SmaliDebugger {
|
||||
if (bodyLength <= 0) {
|
||||
return Packet.make(header);
|
||||
}
|
||||
byte[] body = readBytes(inputStream, bodyLength);
|
||||
byte[] body = IOUtils.readNBytes(inputStream, bodyLength);
|
||||
if (body == null) {
|
||||
throw new SmaliDebuggerException("Stream truncated");
|
||||
}
|
||||
@@ -1147,21 +1150,6 @@ public class SmaliDebugger {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readBytes(InputStream inputStream, int len) throws IOException {
|
||||
byte[] payload = new byte[len];
|
||||
int readSize = 0;
|
||||
while (true) {
|
||||
int read = inputStream.read(payload, readSize, len - readSize);
|
||||
if (read == -1) {
|
||||
return null;
|
||||
}
|
||||
readSize += read;
|
||||
if (readSize == len) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] buf1, byte[] buf2) {
|
||||
byte[] tempBuf = new byte[buf1.length + buf2.length];
|
||||
System.arraycopy(buf1, 0, tempBuf, 0, buf1.length);
|
||||
@@ -1183,62 +1171,6 @@ public class SmaliDebugger {
|
||||
void onCommandReply(Packet res) throws SmaliDebuggerException;
|
||||
}
|
||||
|
||||
private abstract class EventListenerAdapter {
|
||||
void onVMStart(VMStartEvent event) {
|
||||
}
|
||||
|
||||
void onVMDeath(VMDeathEvent event) {
|
||||
}
|
||||
|
||||
void onSingleStep(SingleStepEvent event) {
|
||||
}
|
||||
|
||||
void onBreakpoint(BreakpointEvent event) {
|
||||
}
|
||||
|
||||
void onMethodEntry(MethodEntryEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExit(MethodExitEvent event) {
|
||||
}
|
||||
|
||||
void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEnter(MonitorContendedEnterEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorContendedEntered(MonitorContendedEnteredEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWait(MonitorWaitEvent event) {
|
||||
}
|
||||
|
||||
void onMonitorWaited(MonitorWaitedEvent event) {
|
||||
}
|
||||
|
||||
void onException(ExceptionEvent event) {
|
||||
}
|
||||
|
||||
void onThreadStart(ThreadStartEvent event) {
|
||||
}
|
||||
|
||||
void onThreadDeath(ThreadDeathEvent event) {
|
||||
}
|
||||
|
||||
void onClassPrepare(ClassPrepareEvent event) {
|
||||
}
|
||||
|
||||
void onClassUnload(ClassUnloadEvent event) {
|
||||
}
|
||||
|
||||
void onFieldAccess(FieldAccessEvent event) {
|
||||
}
|
||||
|
||||
void onFieldModification(FieldModificationEvent event) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class RuntimeField extends RuntimeValue {
|
||||
private final String name;
|
||||
private final String fldType;
|
||||
@@ -1296,46 +1228,7 @@ public class SmaliDebugger {
|
||||
}
|
||||
|
||||
private RuntimeRegister buildRegister(int num, int tag, ByteBuffer buf) throws SmaliDebuggerException {
|
||||
return new RuntimeRegister(num, getType(tag), buf);
|
||||
}
|
||||
|
||||
private RuntimeType getType(int tag) throws SmaliDebuggerException {
|
||||
switch (tag) {
|
||||
case JDWP.Tag.ARRAY:
|
||||
return RuntimeType.ARRAY;
|
||||
case JDWP.Tag.BYTE:
|
||||
return RuntimeType.BYTE;
|
||||
case JDWP.Tag.CHAR:
|
||||
return RuntimeType.CHAR;
|
||||
case JDWP.Tag.OBJECT:
|
||||
return RuntimeType.OBJECT;
|
||||
case JDWP.Tag.FLOAT:
|
||||
return RuntimeType.FLOAT;
|
||||
case JDWP.Tag.DOUBLE:
|
||||
return RuntimeType.DOUBLE;
|
||||
case JDWP.Tag.INT:
|
||||
return RuntimeType.INT;
|
||||
case JDWP.Tag.LONG:
|
||||
return RuntimeType.LONG;
|
||||
case JDWP.Tag.SHORT:
|
||||
return RuntimeType.SHORT;
|
||||
case JDWP.Tag.VOID:
|
||||
return RuntimeType.VOID;
|
||||
case JDWP.Tag.BOOLEAN:
|
||||
return RuntimeType.BOOLEAN;
|
||||
case JDWP.Tag.STRING:
|
||||
return RuntimeType.STRING;
|
||||
case JDWP.Tag.THREAD:
|
||||
return RuntimeType.THREAD;
|
||||
case JDWP.Tag.THREAD_GROUP:
|
||||
return RuntimeType.THREAD_GROUP;
|
||||
case JDWP.Tag.CLASS_LOADER:
|
||||
return RuntimeType.CLASS_LOADER;
|
||||
case JDWP.Tag.CLASS_OBJECT:
|
||||
return RuntimeType.CLASS_OBJECT;
|
||||
default:
|
||||
throw new SmaliDebuggerException("Unexpected value: " + tag);
|
||||
}
|
||||
return new RuntimeRegister(num, RuntimeType.fromJdwpTag(tag), buf);
|
||||
}
|
||||
|
||||
public static class RuntimeValue {
|
||||
@@ -1428,41 +1321,6 @@ public class SmaliDebugger {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RuntimeType {
|
||||
ARRAY(91, "[]"),
|
||||
BYTE(66, "byte"),
|
||||
CHAR(67, "char"),
|
||||
OBJECT(76, "object"),
|
||||
FLOAT(70, "float"),
|
||||
DOUBLE(68, "double"),
|
||||
INT(73, "int"),
|
||||
LONG(74, "long"),
|
||||
SHORT(83, "short"),
|
||||
VOID(86, "void"),
|
||||
BOOLEAN(90, "boolean"),
|
||||
STRING(115, "string"),
|
||||
THREAD(116, "thread"),
|
||||
THREAD_GROUP(103, "thread_group"),
|
||||
CLASS_LOADER(108, "class_loader"),
|
||||
CLASS_OBJECT(99, "class_object");
|
||||
|
||||
private final int jdwpTag;
|
||||
private final String desc;
|
||||
|
||||
RuntimeType(int tag, String desc) {
|
||||
this.jdwpTag = tag;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
private int getTag() {
|
||||
return jdwpTag;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Frame {
|
||||
private final long id;
|
||||
private final long clsID;
|
||||
@@ -1503,35 +1361,6 @@ public class SmaliDebugger {
|
||||
void onUnloaded(String cls);
|
||||
}
|
||||
|
||||
public static class SmaliDebuggerException extends Exception {
|
||||
private final int errCode;
|
||||
private static final long serialVersionUID = -1111111202102191403L;
|
||||
|
||||
public SmaliDebuggerException(Exception e) {
|
||||
super(e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg) {
|
||||
super(msg);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, int errCode) {
|
||||
super(msg);
|
||||
this.errCode = errCode;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
return errCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for breakpoint, watch, step, etc.
|
||||
*/
|
||||
@@ -1543,99 +1372,4 @@ public class SmaliDebugger {
|
||||
void onSuspendEvent(SuspendInfo current);
|
||||
}
|
||||
|
||||
public static class SuspendInfo {
|
||||
private boolean terminated;
|
||||
private boolean newRound;
|
||||
private final InfoSetter updater = new InfoSetter();
|
||||
|
||||
public long getThreadID() {
|
||||
return updater.thread;
|
||||
}
|
||||
|
||||
public long getClassID() {
|
||||
return updater.clazz;
|
||||
}
|
||||
|
||||
public long getMethodID() {
|
||||
return updater.method;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return updater.offset;
|
||||
}
|
||||
|
||||
private InfoSetter update() {
|
||||
updater.changed = false;
|
||||
updater.nextRound(newRound);
|
||||
this.newRound = false;
|
||||
return updater;
|
||||
}
|
||||
|
||||
// called by decodingLoop, to tell the updater even though the values are the same,
|
||||
// they are decoded from another packet, they should be treated as new.
|
||||
private void nextRound() {
|
||||
newRound = true;
|
||||
}
|
||||
|
||||
// according to JDWP document it's legal to fire two or more events on a same location,
|
||||
// e.g. one for single step and the other for breakpoint, so when this happened we only
|
||||
// want one of them.
|
||||
private boolean isAnythingChanged() {
|
||||
return updater.changed;
|
||||
}
|
||||
|
||||
public boolean isTerminated() {
|
||||
return terminated;
|
||||
}
|
||||
|
||||
private void setTerminated() {
|
||||
terminated = true;
|
||||
}
|
||||
|
||||
private static class InfoSetter {
|
||||
private long thread;
|
||||
private long clazz;
|
||||
private long method;
|
||||
private long offset; // code offset;
|
||||
private boolean changed;
|
||||
|
||||
private void nextRound(boolean newRound) {
|
||||
if (!changed) {
|
||||
changed = newRound;
|
||||
}
|
||||
}
|
||||
|
||||
private InfoSetter updateThread(long thread) {
|
||||
if (!changed) {
|
||||
changed = this.thread != thread;
|
||||
}
|
||||
this.thread = thread;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateClass(long clazz) {
|
||||
if (!changed) {
|
||||
changed = this.clazz != clazz;
|
||||
}
|
||||
this.clazz = clazz;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateMethod(long method) {
|
||||
if (!changed) {
|
||||
changed = this.method != method;
|
||||
}
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
private InfoSetter updateOffset(long offset) {
|
||||
if (!changed) {
|
||||
changed = this.offset != offset;
|
||||
}
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
public class SmaliDebuggerException extends Exception {
|
||||
private final int errCode;
|
||||
private static final long serialVersionUID = -1111111202102191403L;
|
||||
|
||||
public SmaliDebuggerException(Exception e) {
|
||||
super(e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg) {
|
||||
super(msg);
|
||||
this.errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, Exception e) {
|
||||
super(msg, e);
|
||||
errCode = -1;
|
||||
}
|
||||
|
||||
public SmaliDebuggerException(String msg, int errCode) {
|
||||
super(msg);
|
||||
this.errCode = errCode;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
return errCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package jadx.gui.device.debugger;
|
||||
|
||||
public class SuspendInfo {
|
||||
private boolean terminated;
|
||||
private boolean newRound;
|
||||
private final InfoSetter updater = new InfoSetter();
|
||||
|
||||
public long getThreadID() {
|
||||
return updater.thread;
|
||||
}
|
||||
|
||||
public long getClassID() {
|
||||
return updater.clazz;
|
||||
}
|
||||
|
||||
public long getMethodID() {
|
||||
return updater.method;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return updater.offset;
|
||||
}
|
||||
|
||||
InfoSetter update() {
|
||||
updater.changed = false;
|
||||
updater.nextRound(newRound);
|
||||
this.newRound = false;
|
||||
return updater;
|
||||
}
|
||||
|
||||
// called by decodingLoop, to tell the updater even though the values are the same,
|
||||
// they are decoded from another packet, they should be treated as new.
|
||||
void nextRound() {
|
||||
newRound = true;
|
||||
}
|
||||
|
||||
// according to JDWP document it's legal to fire two or more events on a same location,
|
||||
// e.g. one for single step and the other for breakpoint, so when this happened we only
|
||||
// want one of them.
|
||||
boolean isAnythingChanged() {
|
||||
return updater.changed;
|
||||
}
|
||||
|
||||
public boolean isTerminated() {
|
||||
return terminated;
|
||||
}
|
||||
|
||||
void setTerminated() {
|
||||
terminated = true;
|
||||
}
|
||||
|
||||
static class InfoSetter {
|
||||
private long thread;
|
||||
private long clazz;
|
||||
private long method;
|
||||
private long offset; // code offset;
|
||||
private boolean changed;
|
||||
|
||||
void nextRound(boolean newRound) {
|
||||
if (!changed) {
|
||||
changed = newRound;
|
||||
}
|
||||
}
|
||||
|
||||
InfoSetter updateThread(long thread) {
|
||||
if (!changed) {
|
||||
changed = this.thread != thread;
|
||||
}
|
||||
this.thread = thread;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateClass(long clazz) {
|
||||
if (!changed) {
|
||||
changed = this.clazz != clazz;
|
||||
}
|
||||
this.clazz = clazz;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateMethod(long method) {
|
||||
if (!changed) {
|
||||
changed = this.method != method;
|
||||
}
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
InfoSetter updateOffset(long offset) {
|
||||
if (!changed) {
|
||||
changed = this.offset != offset;
|
||||
}
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -7,19 +8,17 @@ import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.gui.utils.IOUtils;
|
||||
|
||||
public class ADB {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADB.class);
|
||||
@@ -28,13 +27,11 @@ public class ADB {
|
||||
private static final String DEFAULT_ADDR = "localhost";
|
||||
|
||||
private static final String CMD_FEATURES = "000dhost:features";
|
||||
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
|
||||
private static final String CMD_TRACK_DEVICES = "0014host:track-devices-l";
|
||||
private static final byte[] OKAY = "OKAY".getBytes();
|
||||
|
||||
private static boolean isOkay(InputStream stream) throws IOException {
|
||||
byte[] buf = new byte[4];
|
||||
stream.read(buf, 0, 4);
|
||||
static boolean isOkay(InputStream stream) throws IOException {
|
||||
byte[] buf = IOUtils.readNBytes(stream, 4);
|
||||
return Arrays.equals(buf, OKAY);
|
||||
}
|
||||
|
||||
@@ -44,9 +41,9 @@ public class ADB {
|
||||
|
||||
public static byte[] exec(String cmd) throws IOException {
|
||||
byte[] res;
|
||||
Socket socket = connect();
|
||||
res = exec(cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
socket.close();
|
||||
try (Socket socket = connect()) {
|
||||
res = exec(cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -58,7 +55,7 @@ public class ADB {
|
||||
return new Socket(host, port);
|
||||
}
|
||||
|
||||
private static boolean execCommandAsync(OutputStream outputStream,
|
||||
static boolean execCommandAsync(OutputStream outputStream,
|
||||
InputStream inputStream, String cmd) throws IOException {
|
||||
outputStream.write(cmd.getBytes());
|
||||
return isOkay(inputStream);
|
||||
@@ -73,29 +70,21 @@ public class ADB {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] readServiceProtocol(InputStream stream) {
|
||||
byte[] bytes = null;
|
||||
byte[] buf = new byte[4];
|
||||
static byte[] readServiceProtocol(InputStream stream) {
|
||||
try {
|
||||
int len = stream.read(buf, 0, 4);
|
||||
if (len == 4) {
|
||||
len = unhex(buf);
|
||||
if (len == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
if (len != -1) {
|
||||
buf = new byte[len];
|
||||
if (stream.read(buf, 0, len) == len) {
|
||||
bytes = buf;
|
||||
}
|
||||
}
|
||||
byte[] buf = IOUtils.readNBytes(stream, 4);
|
||||
int len = unhex(buf);
|
||||
if (len == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
} catch (IOException ignore) {
|
||||
return IOUtils.readNBytes(stream, len);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to read readServiceProtocol: {}", e.toString());
|
||||
return null;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
String setSerialCmd = String.format("host:tport:serial:%s", serial);
|
||||
setSerialCmd = String.format("%04x%s", setSerialCmd.length(), setSerialCmd);
|
||||
outputStream.write(setSerialCmd.getBytes());
|
||||
@@ -107,9 +96,7 @@ public class ADB {
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static byte[] execShellCommandRaw(String cmd,
|
||||
OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
|
||||
private static byte[] execShellCommandRaw(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
cmd = String.format("shell,v2,TERM=xterm-256color,raw:%s", cmd);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
outputStream.write(cmd.getBytes());
|
||||
@@ -119,7 +106,7 @@ public class ADB {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] execShellCommandRaw(String serial, String cmd,
|
||||
static byte[] execShellCommandRaw(String serial, String cmd,
|
||||
OutputStream outputStream, InputStream inputStream) throws IOException {
|
||||
if (setSerial(serial, outputStream, inputStream)) {
|
||||
return execShellCommandRaw(cmd, outputStream, inputStream);
|
||||
@@ -148,17 +135,19 @@ public class ADB {
|
||||
proc.destroyForcibly();
|
||||
return false;
|
||||
}
|
||||
InputStream is = proc.getInputStream();
|
||||
int size = is.available();
|
||||
byte[] bytes = new byte[size];
|
||||
is.read(bytes, 0, size);
|
||||
return new String(bytes).contains(tcpPort);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (InputStream in = proc.getInputStream()) {
|
||||
int read;
|
||||
byte[] buf = new byte[1024];
|
||||
while ((read = in.read(buf)) >= 0) {
|
||||
out.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
return new String(out.toByteArray()).contains(tcpPort);
|
||||
}
|
||||
|
||||
public static boolean isServerRunning(String host, int port) {
|
||||
try {
|
||||
Socket sock = new Socket(host, port);
|
||||
sock.close();
|
||||
try (Socket sock = new Socket(host, port)) {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
@@ -184,10 +173,10 @@ public class ADB {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] deviceLines = payload.split("\n");
|
||||
List<DeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
|
||||
List<ADBDeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
|
||||
for (String deviceLine : deviceLines) {
|
||||
if (!deviceLine.trim().isEmpty()) {
|
||||
deviceInfoList.add(DeviceInfo.make(deviceLine, host, port));
|
||||
deviceInfoList.add(ADBDeviceInfo.make(deviceLine, host, port));
|
||||
}
|
||||
}
|
||||
listener.onDeviceStatusChange(deviceInfoList);
|
||||
@@ -213,10 +202,7 @@ public class ADB {
|
||||
byte[] bytes = readServiceProtocol(inputStream);
|
||||
if (bytes != null) {
|
||||
String[] forwards = new String(bytes).split("\n");
|
||||
List<String> forwardList = new ArrayList<>(forwards.length);
|
||||
for (String forward : forwards) {
|
||||
forwardList.add(forward.trim());
|
||||
}
|
||||
List<String> forwardList = Arrays.stream(forwards).map(s -> s.trim()).collect(Collectors.toList());
|
||||
socket.close();
|
||||
return forwardList;
|
||||
}
|
||||
@@ -300,287 +286,17 @@ public class ADB {
|
||||
}
|
||||
|
||||
public interface JDWPProcessListener {
|
||||
void jdwpProcessOccurred(Device device, Set<String> id);
|
||||
void jdwpProcessOccurred(ADBDevice device, Set<String> id);
|
||||
|
||||
void jdwpListenerClosed(Device device);
|
||||
void jdwpListenerClosed(ADBDevice device);
|
||||
}
|
||||
|
||||
public interface DeviceStateListener {
|
||||
void onDeviceStatusChange(List<DeviceInfo> deviceInfoList);
|
||||
void onDeviceStatusChange(List<ADBDeviceInfo> deviceInfoList);
|
||||
|
||||
void adbDisconnected();
|
||||
}
|
||||
|
||||
public static class Device {
|
||||
DeviceInfo info;
|
||||
String androidReleaseVer;
|
||||
volatile Socket jdwpListenerSock;
|
||||
|
||||
public Device(DeviceInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public boolean updateDeviceInfo(DeviceInfo info) {
|
||||
boolean matched = this.info.serial.equals(info.serial);
|
||||
if (matched) {
|
||||
this.info = info;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return info.serial;
|
||||
}
|
||||
|
||||
public boolean removeForward(String localPort) throws IOException {
|
||||
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
|
||||
}
|
||||
|
||||
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
ForwardResult rst;
|
||||
if (setSerial(info.serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (!isOkay(inputStream)) {
|
||||
rst = new ForwardResult(1, readServiceProtocol(inputStream));
|
||||
} else if (!isOkay(inputStream)) {
|
||||
rst = new ForwardResult(2, readServiceProtocol(inputStream));
|
||||
} else {
|
||||
rst = new ForwardResult(0, null);
|
||||
}
|
||||
} else {
|
||||
rst = new ForwardResult(1, "Unknown error.".getBytes());
|
||||
}
|
||||
socket.close();
|
||||
return rst;
|
||||
}
|
||||
|
||||
public static class ForwardResult {
|
||||
/**
|
||||
* 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote.
|
||||
*/
|
||||
public int state;
|
||||
public String desc;
|
||||
|
||||
public ForwardResult(int state, byte[] desc) {
|
||||
if (desc != null) {
|
||||
this.desc = new String(desc);
|
||||
} else {
|
||||
this.desc = "";
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pid otherwise -1
|
||||
*/
|
||||
public int launchApp(String fullAppName) throws IOException, InterruptedException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
String cmd = "am start -D -n " + fullAppName;
|
||||
byte[] res = execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
socket.close();
|
||||
String rst = new String(res).trim();
|
||||
if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) {
|
||||
Thread.sleep(40);
|
||||
String pkg = fullAppName.split("/")[0];
|
||||
for (Process process : getProcessByPkg(pkg)) {
|
||||
return Integer.parseInt(process.pid);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getAndroidReleaseVersion() {
|
||||
if (!StringUtils.isEmpty(androidReleaseVer)) {
|
||||
return androidReleaseVer;
|
||||
}
|
||||
try {
|
||||
List<String> list = getProp("ro.build.version.release");
|
||||
if (list.size() != 0) {
|
||||
androidReleaseVer = list.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get android release version", e);
|
||||
androidReleaseVer = "";
|
||||
}
|
||||
return androidReleaseVer;
|
||||
}
|
||||
|
||||
public List<String> getProp(String entry) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
List<String> props = Collections.emptyList();
|
||||
String cmd = "getprop";
|
||||
if (!StringUtils.isEmpty(entry)) {
|
||||
cmd += " " + entry;
|
||||
}
|
||||
byte[] payload = execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
props = new ArrayList<>();
|
||||
String[] lines = new String(payload).split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
props.add(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return props;
|
||||
}
|
||||
|
||||
public List<Process> getProcessByPkg(String pkg) throws IOException {
|
||||
return getProcessList("ps | grep " + pkg, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Process> getProcessList() throws IOException {
|
||||
return getProcessList("ps", 1);
|
||||
}
|
||||
|
||||
private List<Process> getProcessList(String cmd, int index) throws IOException {
|
||||
Socket socket = connect(info.adbHost, info.adbPort);
|
||||
List<Process> procs = Collections.emptyList();
|
||||
byte[] payload = execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
String ps = new String(payload);
|
||||
String[] psLines = ps.split("\n");
|
||||
for (int i = index; i < psLines.length; i++) {
|
||||
Process proc = Process.make(psLines[i]);
|
||||
if (proc != null) {
|
||||
if (procs.isEmpty()) {
|
||||
procs = new ArrayList<>();
|
||||
}
|
||||
procs.add(proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.close();
|
||||
return procs;
|
||||
}
|
||||
|
||||
public boolean listenForJDWP(JDWPProcessListener listener) throws IOException {
|
||||
if (this.jdwpListenerSock != null) {
|
||||
return false;
|
||||
}
|
||||
jdwpListenerSock = connect(this.info.adbHost, this.info.adbPort);
|
||||
InputStream inputStream = jdwpListenerSock.getInputStream();
|
||||
OutputStream outputStream = jdwpListenerSock.getOutputStream();
|
||||
if (setSerial(info.serial, outputStream, inputStream)
|
||||
&& execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
|
||||
Executors.newFixedThreadPool(1).execute(() -> {
|
||||
for (;;) {
|
||||
byte[] res = readServiceProtocol(inputStream);
|
||||
if (res != null) {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] ids = payload.split("\n");
|
||||
Set<String> idList = new HashSet<>(ids.length);
|
||||
for (String id : ids) {
|
||||
if (!id.trim().isEmpty()) {
|
||||
idList.add(id);
|
||||
}
|
||||
}
|
||||
listener.jdwpProcessOccurred(this, idList);
|
||||
}
|
||||
} else { // socket disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
this.jdwpListenerSock = null;
|
||||
listener.jdwpListenerClosed(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
jdwpListenerSock.close();
|
||||
jdwpListenerSock = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stopListenForJDWP() {
|
||||
if (jdwpListenerSock != null) {
|
||||
try {
|
||||
jdwpListenerSock.close();
|
||||
} catch (Exception e) {
|
||||
LOG.error("JDWP socket close failed", e);
|
||||
}
|
||||
}
|
||||
this.jdwpListenerSock = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return info.serial.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Device) {
|
||||
return ((Device) obj).getDeviceInfo().serial.equals(info.serial);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info.allInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeviceInfo {
|
||||
public String adbHost;
|
||||
public int adbPort;
|
||||
public String serial;
|
||||
public String state;
|
||||
public String model;
|
||||
public String allInfo;
|
||||
|
||||
public boolean isOnline() {
|
||||
return state.equals("device");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return allInfo;
|
||||
}
|
||||
|
||||
static DeviceInfo make(String info, String host, int port) {
|
||||
DeviceInfo deviceInfo = new DeviceInfo();
|
||||
String[] infoFields = info.trim().split("\\s+");
|
||||
deviceInfo.allInfo = String.join(" ", infoFields);
|
||||
if (infoFields.length > 2) {
|
||||
deviceInfo.serial = infoFields[0];
|
||||
deviceInfo.state = infoFields[1];
|
||||
}
|
||||
int pos = info.indexOf("model:");
|
||||
if (pos != -1) {
|
||||
int spacePos = info.indexOf(" ", pos);
|
||||
if (spacePos != -1) {
|
||||
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
|
||||
}
|
||||
}
|
||||
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
|
||||
deviceInfo.model = deviceInfo.serial;
|
||||
}
|
||||
deviceInfo.adbHost = host;
|
||||
deviceInfo.adbPort = port;
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Process {
|
||||
public String user;
|
||||
public String pid;
|
||||
@@ -619,23 +335,23 @@ public class ADB {
|
||||
|
||||
public static byte[] readStdout(InputStream inputStream) throws IOException {
|
||||
byte[] header = new byte[5];
|
||||
byte[] payload = new byte[0];
|
||||
byte[] tempBuf = new byte[0];
|
||||
ByteArrayOutputStream payload = new ByteArrayOutputStream();
|
||||
byte[] tempBuf = new byte[1024];
|
||||
for (boolean exit = false; !exit;) {
|
||||
if (inputStream.read(header, 0, 5) == 5) {
|
||||
exit = header[0] == ID_EXIT;
|
||||
int payloadSize = readInt(header, 1);
|
||||
if (tempBuf.length < payloadSize) {
|
||||
tempBuf = new byte[payloadSize];
|
||||
}
|
||||
int readSize = inputStream.read(tempBuf, 0, payloadSize);
|
||||
if (readSize != payloadSize) {
|
||||
return null; // we don't want corrupted data.
|
||||
}
|
||||
payload = appendBytes(payload, tempBuf, readSize);
|
||||
IOUtils.read(inputStream, header);
|
||||
exit = header[0] == ID_EXIT;
|
||||
int payloadSize = readInt(header, 1);
|
||||
if (tempBuf.length < payloadSize) {
|
||||
tempBuf = new byte[payloadSize];
|
||||
}
|
||||
int readSize = IOUtils.read(inputStream, tempBuf, 0, payloadSize);
|
||||
if (readSize != payloadSize) {
|
||||
LOG.error("Failed to read ShellProtocol data");
|
||||
return null; // we don't want corrupted data.
|
||||
}
|
||||
payload.write(tempBuf, 0, readSize);
|
||||
}
|
||||
return payload;
|
||||
return payload.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.gui.device.protocol.ADB.JDWPProcessListener;
|
||||
import jadx.gui.device.protocol.ADB.Process;
|
||||
|
||||
public class ADBDevice {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class);
|
||||
|
||||
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
|
||||
|
||||
ADBDeviceInfo info;
|
||||
String androidReleaseVer;
|
||||
volatile Socket jdwpListenerSock;
|
||||
|
||||
public ADBDevice(ADBDeviceInfo info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public ADBDeviceInfo getDeviceInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public boolean updateDeviceInfo(ADBDeviceInfo info) {
|
||||
boolean matched = this.info.serial.equals(info.serial);
|
||||
if (matched) {
|
||||
this.info = info;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return info.serial;
|
||||
}
|
||||
|
||||
public boolean removeForward(String localPort) throws IOException {
|
||||
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
|
||||
}
|
||||
|
||||
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
|
||||
cmd = String.format("%04x%s", cmd.length(), cmd);
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
ForwardResult rst;
|
||||
if (ADB.setSerial(info.serial, outputStream, inputStream)) {
|
||||
outputStream.write(cmd.getBytes());
|
||||
if (!ADB.isOkay(inputStream)) {
|
||||
rst = new ForwardResult(1, ADB.readServiceProtocol(inputStream));
|
||||
} else if (!ADB.isOkay(inputStream)) {
|
||||
rst = new ForwardResult(2, ADB.readServiceProtocol(inputStream));
|
||||
} else {
|
||||
rst = new ForwardResult(0, null);
|
||||
}
|
||||
} else {
|
||||
rst = new ForwardResult(1, "Unknown error.".getBytes());
|
||||
}
|
||||
return rst;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ForwardResult {
|
||||
/**
|
||||
* 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote.
|
||||
*/
|
||||
public int state;
|
||||
public String desc;
|
||||
|
||||
public ForwardResult(int state, byte[] desc) {
|
||||
if (desc != null) {
|
||||
this.desc = new String(desc);
|
||||
} else {
|
||||
this.desc = "";
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pid otherwise -1
|
||||
*/
|
||||
public int launchApp(String fullAppName) throws IOException, InterruptedException {
|
||||
byte[] res;
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
String cmd = "am start -D -n " + fullAppName;
|
||||
res = ADB.execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
|
||||
}
|
||||
String rst = new String(res).trim();
|
||||
if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) {
|
||||
Thread.sleep(40);
|
||||
String pkg = fullAppName.split("/")[0];
|
||||
for (Process process : getProcessByPkg(pkg)) {
|
||||
return Integer.parseInt(process.pid);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getAndroidReleaseVersion() {
|
||||
if (!StringUtils.isEmpty(androidReleaseVer)) {
|
||||
return androidReleaseVer;
|
||||
}
|
||||
try {
|
||||
List<String> list = getProp("ro.build.version.release");
|
||||
if (list.size() != 0) {
|
||||
androidReleaseVer = list.get(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get android release version", e);
|
||||
androidReleaseVer = "";
|
||||
}
|
||||
return androidReleaseVer;
|
||||
}
|
||||
|
||||
public List<String> getProp(String entry) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
List<String> props = Collections.emptyList();
|
||||
String cmd = "getprop";
|
||||
if (!StringUtils.isEmpty(entry)) {
|
||||
cmd += " " + entry;
|
||||
}
|
||||
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
props = new ArrayList<>();
|
||||
String[] lines = new String(payload).split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
props.add(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Process> getProcessByPkg(String pkg) throws IOException {
|
||||
return getProcessList("ps | grep " + pkg, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Process> getProcessList() throws IOException {
|
||||
return getProcessList("ps", 1);
|
||||
}
|
||||
|
||||
private List<Process> getProcessList(String cmd, int index) throws IOException {
|
||||
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
|
||||
List<Process> procs = new ArrayList<>();
|
||||
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
|
||||
socket.getOutputStream(), socket.getInputStream());
|
||||
if (payload != null) {
|
||||
String ps = new String(payload);
|
||||
String[] psLines = ps.split("\n");
|
||||
for (int i = index; i < psLines.length; i++) {
|
||||
Process proc = Process.make(psLines[i]);
|
||||
if (proc != null) {
|
||||
procs.add(proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return procs;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean listenForJDWP(JDWPProcessListener listener) throws IOException {
|
||||
if (this.jdwpListenerSock != null) {
|
||||
return false;
|
||||
}
|
||||
jdwpListenerSock = ADB.connect(this.info.adbHost, this.info.adbPort);
|
||||
InputStream inputStream = jdwpListenerSock.getInputStream();
|
||||
OutputStream outputStream = jdwpListenerSock.getOutputStream();
|
||||
if (ADB.setSerial(info.serial, outputStream, inputStream)
|
||||
&& ADB.execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
|
||||
Executors.newFixedThreadPool(1).execute(() -> {
|
||||
for (;;) {
|
||||
byte[] res = ADB.readServiceProtocol(inputStream);
|
||||
if (res != null) {
|
||||
if (listener != null) {
|
||||
String payload = new String(res);
|
||||
String[] ids = payload.split("\n");
|
||||
Set<String> idList = new HashSet<>(ids.length);
|
||||
for (String id : ids) {
|
||||
if (!id.trim().isEmpty()) {
|
||||
idList.add(id);
|
||||
}
|
||||
}
|
||||
listener.jdwpProcessOccurred(this, idList);
|
||||
}
|
||||
} else { // socket disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (listener != null) {
|
||||
this.jdwpListenerSock = null;
|
||||
listener.jdwpListenerClosed(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
jdwpListenerSock.close();
|
||||
jdwpListenerSock = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stopListenForJDWP() {
|
||||
if (jdwpListenerSock != null) {
|
||||
try {
|
||||
jdwpListenerSock.close();
|
||||
} catch (Exception e) {
|
||||
LOG.error("JDWP socket close failed", e);
|
||||
}
|
||||
}
|
||||
this.jdwpListenerSock = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return info.serial.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ADBDevice) {
|
||||
return ((ADBDevice) obj).getDeviceInfo().serial.equals(info.serial);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return info.allInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package jadx.gui.device.protocol;
|
||||
|
||||
public class ADBDeviceInfo {
|
||||
public String adbHost;
|
||||
public int adbPort;
|
||||
public String serial;
|
||||
public String state;
|
||||
public String model;
|
||||
public String allInfo;
|
||||
|
||||
public boolean isOnline() {
|
||||
return state.equals("device");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return allInfo;
|
||||
}
|
||||
|
||||
static ADBDeviceInfo make(String info, String host, int port) {
|
||||
ADBDeviceInfo deviceInfo = new ADBDeviceInfo();
|
||||
String[] infoFields = info.trim().split("\\s+");
|
||||
deviceInfo.allInfo = String.join(" ", infoFields);
|
||||
if (infoFields.length > 2) {
|
||||
deviceInfo.serial = infoFields[0];
|
||||
deviceInfo.state = infoFields[1];
|
||||
}
|
||||
int pos = info.indexOf("model:");
|
||||
if (pos != -1) {
|
||||
int spacePos = info.indexOf(" ", pos);
|
||||
if (spacePos != -1) {
|
||||
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
|
||||
}
|
||||
}
|
||||
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
|
||||
deviceInfo.model = deviceInfo.serial;
|
||||
}
|
||||
deviceInfo.adbHost = host;
|
||||
deviceInfo.adbPort = port;
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.gui.device.debugger.DbgUtils;
|
||||
import jadx.gui.device.protocol.ADB;
|
||||
import jadx.gui.device.protocol.ADBDevice;
|
||||
import jadx.gui.device.protocol.ADBDeviceInfo;
|
||||
import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.ui.panel.IDebugController;
|
||||
@@ -49,7 +51,7 @@ import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.SystemInfo;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static jadx.gui.device.protocol.ADB.Device.ForwardResult;
|
||||
import static jadx.gui.device.protocol.ADBDevice.ForwardResult;
|
||||
|
||||
public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.JDWPProcessListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ADBDialog.class);
|
||||
@@ -273,9 +275,9 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceStatusChange(List<ADB.DeviceInfo> deviceInfoList) {
|
||||
public void onDeviceStatusChange(List<ADBDeviceInfo> deviceInfoList) {
|
||||
List<DeviceNode> nodes = new ArrayList<>(deviceInfoList.size());
|
||||
info_loop: for (ADB.DeviceInfo info : deviceInfoList) {
|
||||
info_loop: for (ADBDeviceInfo info : deviceInfoList) {
|
||||
for (DeviceNode deviceNode : deviceNodes) {
|
||||
if (deviceNode.device.updateDeviceInfo(info)) {
|
||||
deviceNode.refresh();
|
||||
@@ -283,7 +285,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
continue info_loop;
|
||||
}
|
||||
}
|
||||
ADB.Device device = new ADB.Device(info);
|
||||
ADBDevice device = new ADBDevice(info);
|
||||
device.getAndroidReleaseVersion();
|
||||
nodes.add(new DeviceNode(device));
|
||||
listenJDWP(device);
|
||||
@@ -402,7 +404,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeviceNode getDeviceNode(ADB.Device device) {
|
||||
private DeviceNode getDeviceNode(ADBDevice device) {
|
||||
for (DeviceNode deviceNode : deviceNodes) {
|
||||
if (deviceNode.device.equals(device)) {
|
||||
return deviceNode;
|
||||
@@ -411,7 +413,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
throw new JadxRuntimeException("Unexpected device: " + device);
|
||||
}
|
||||
|
||||
private void listenJDWP(ADB.Device device) {
|
||||
private void listenJDWP(ADBDevice device) {
|
||||
try {
|
||||
device.listenForJDWP(this);
|
||||
} catch (Exception e) {
|
||||
@@ -441,7 +443,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jdwpProcessOccurred(ADB.Device device, Set<String> id) {
|
||||
public void jdwpProcessOccurred(ADBDevice device, Set<String> id) {
|
||||
List<ADB.Process> procs;
|
||||
try {
|
||||
Thread.sleep(40); /*
|
||||
@@ -514,7 +516,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
return;
|
||||
}
|
||||
String fullName = pkg + "/" + cls.getCls().getClassNode().getClassInfo().getFullName();
|
||||
ADB.Device device = deviceNodes.get(0).device; // TODO: if multiple devices presented should let user select the one they desire.
|
||||
ADBDevice device = deviceNodes.get(0).device; // TODO: if multiple devices presented should let user select the one they desire.
|
||||
if (device != null) {
|
||||
try {
|
||||
device.launchApp(fullName);
|
||||
@@ -547,7 +549,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jdwpListenerClosed(ADB.Device device) {
|
||||
public void jdwpListenerClosed(ADBDevice device) {
|
||||
|
||||
}
|
||||
|
||||
@@ -556,27 +558,29 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
}
|
||||
|
||||
private static class DeviceNode {
|
||||
ADB.Device device;
|
||||
ADBDevice device;
|
||||
DeviceTreeNode tNode;
|
||||
|
||||
DeviceNode(ADB.Device adbDevice) {
|
||||
DeviceNode(ADBDevice adbDevice) {
|
||||
this.device = adbDevice;
|
||||
tNode = new DeviceTreeNode();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
ADB.DeviceInfo info = device.getDeviceInfo();
|
||||
ADBDeviceInfo info = device.getDeviceInfo();
|
||||
String text = info.model;
|
||||
if (!text.equals(info.serial)) {
|
||||
text += String.format(" [serial: %s]", info.serial);
|
||||
if (text != null) {
|
||||
if (!text.equals(info.serial)) {
|
||||
text += String.format(" [serial: %s]", info.serial);
|
||||
}
|
||||
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
|
||||
tNode.setUserObject(text);
|
||||
}
|
||||
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
|
||||
tNode.setUserObject(text);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupArgs(ADB.Device device, String pid, String name) {
|
||||
private boolean setupArgs(ADBDevice device, String pid, String name) {
|
||||
String ver = device.getAndroidReleaseVersion();
|
||||
if (StringUtils.isEmpty(ver)) {
|
||||
if (JOptionPane.showConfirmDialog(mainWindow,
|
||||
@@ -605,12 +609,12 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
|
||||
private String ver;
|
||||
private String pid;
|
||||
private String name;
|
||||
private ADB.Device device;
|
||||
private ADBDevice device;
|
||||
private int forwardTcpPort = FORWARD_TCP_PORT;
|
||||
private String expectPkg = "";
|
||||
private boolean autoAttachPkg = false;
|
||||
|
||||
private void set(ADB.Device device, String ver, String pid, String name) {
|
||||
private void set(ADBDevice device, String ver, String pid, String name) {
|
||||
this.ver = ver;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class IOUtils {
|
||||
|
||||
/**
|
||||
* This method can be deleted once Jadx is Java11+
|
||||
*
|
||||
* @param inputStream
|
||||
* @param len
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static byte[] readNBytes(InputStream inputStream, int len) throws IOException {
|
||||
byte[] payload = new byte[len];
|
||||
int readSize = 0;
|
||||
while (true) {
|
||||
int read = inputStream.read(payload, readSize, len - readSize);
|
||||
if (read == -1) {
|
||||
return null;
|
||||
}
|
||||
readSize += read;
|
||||
if (readSize == len) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int read(InputStream inputStream, byte[] buf) throws IOException {
|
||||
return read(inputStream, buf, 0, buf.length);
|
||||
}
|
||||
|
||||
public static int read(InputStream inputStream, byte[] buf, int off, int len) throws IOException {
|
||||
int remainingBytes = len;
|
||||
while (remainingBytes > 0) {
|
||||
int start = len - remainingBytes;
|
||||
int bytesRead = inputStream.read(buf, off + start, remainingBytes);
|
||||
if (bytesRead == -1) {
|
||||
break;
|
||||
}
|
||||
remainingBytes -= bytesRead;
|
||||
}
|
||||
return len - remainingBytes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user