mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2025-01-08 16:04:10 -08:00
Wasm: working on Chrome RDP debugger
This commit is contained in:
parent
d1feec7ae6
commit
9d3927e196
|
@ -101,7 +101,6 @@ public class DebugInfoParser {
|
||||||
private Promise<Void> parseSection(int limit) {
|
private Promise<Void> parseSection(int limit) {
|
||||||
return readString("Error reading custom section name").thenAsync(name -> {
|
return readString("Error reading custom section name").thenAsync(name -> {
|
||||||
var sectionParser = sectionParsers.get(name);
|
var sectionParser = sectionParsers.get(name);
|
||||||
System.out.println("Section '" + name + "': " + (sectionParser != null ? "parsed" : "unknown"));
|
|
||||||
if (sectionParser != null) {
|
if (sectionParser != null) {
|
||||||
return readBytes(limit - pos, "Error reading section '" + name + "' content")
|
return readBytes(limit - pos, "Error reading section '" + name + "' content")
|
||||||
.thenVoid(sectionParser::parse);
|
.thenVoid(sectionParser::parse);
|
||||||
|
|
|
@ -320,6 +320,17 @@ public class Promise<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void runNow(Runnable runnable) {
|
||||||
|
var queue = processing.get();
|
||||||
|
if (queue == null) {
|
||||||
|
runnable.run();
|
||||||
|
} else {
|
||||||
|
processing.remove();
|
||||||
|
runnable.run();
|
||||||
|
processing.set(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
PENDING,
|
PENDING,
|
||||||
WAITING_PROMISE,
|
WAITING_PROMISE,
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Alexey Andreev.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.teavm.chromerdp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.teavm.chromerdp.data.Message;
|
||||||
|
import org.teavm.chromerdp.data.Response;
|
||||||
|
import org.teavm.common.CompletablePromise;
|
||||||
|
import org.teavm.common.Promise;
|
||||||
|
import org.teavm.debugging.javascript.JavaScriptDebuggerListener;
|
||||||
|
|
||||||
|
public abstract class BaseChromeRDPDebugger implements ChromeRDPExchangeConsumer {
|
||||||
|
protected static final Logger logger = LoggerFactory.getLogger(BaseChromeRDPDebugger.class);
|
||||||
|
private ChromeRDPExchange exchange;
|
||||||
|
protected Set<JavaScriptDebuggerListener> listeners = new LinkedHashSet<>();
|
||||||
|
protected ObjectMapper mapper = new ObjectMapper();
|
||||||
|
private ConcurrentMap<Integer, ChromeRDPDebugger.ResponseHandler<Object>>
|
||||||
|
responseHandlers = new ConcurrentHashMap<>();
|
||||||
|
private ConcurrentMap<Integer, CompletablePromise<Object>> promises = new ConcurrentHashMap<>();
|
||||||
|
private int messageIdGenerator;
|
||||||
|
|
||||||
|
protected List<JavaScriptDebuggerListener> getListeners() {
|
||||||
|
return new ArrayList<>(listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Executor executor;
|
||||||
|
|
||||||
|
public BaseChromeRDPDebugger(Executor executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setExchange(ChromeRDPExchange exchange) {
|
||||||
|
if (this.exchange == exchange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.exchange != null) {
|
||||||
|
this.exchange.removeListener(exchangeListener);
|
||||||
|
}
|
||||||
|
this.exchange = exchange;
|
||||||
|
if (exchange != null) {
|
||||||
|
onAttach();
|
||||||
|
for (var listener : getListeners()) {
|
||||||
|
listener.attached();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onDetach();
|
||||||
|
for (var listener : getListeners()) {
|
||||||
|
listener.detached();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.exchange != null) {
|
||||||
|
this.exchange.addListener(exchangeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChromeRDPExchangeListener exchangeListener = messageText -> {
|
||||||
|
callInExecutor(() -> receiveMessage(messageText)
|
||||||
|
.catchError(e -> {
|
||||||
|
logger.error("Error handling message", e);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
private Promise<Void> receiveMessage(String messageText) {
|
||||||
|
try {
|
||||||
|
var jsonMessage = mapper.readTree(messageText);
|
||||||
|
if (jsonMessage.has("id")) {
|
||||||
|
var response = parseJson(Response.class, jsonMessage);
|
||||||
|
if (response.getError() != null) {
|
||||||
|
if (logger.isWarnEnabled()) {
|
||||||
|
logger.warn("Error message #{} received from browser: {}", jsonMessage.get("id"),
|
||||||
|
response.getError().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var promise = promises.remove(response.getId());
|
||||||
|
try {
|
||||||
|
responseHandlers.remove(response.getId()).received(response.getResult(), promise);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
logger.warn("Error processing message ${}", response.getId(), e);
|
||||||
|
promise.completeWithError(e);
|
||||||
|
}
|
||||||
|
return Promise.VOID;
|
||||||
|
} else {
|
||||||
|
var message = parseJson(Message.class, jsonMessage);
|
||||||
|
if (message.getMethod() == null) {
|
||||||
|
return Promise.VOID;
|
||||||
|
}
|
||||||
|
return handleMessage(message);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (logger.isErrorEnabled()) {
|
||||||
|
logger.error("Error receiving message from Google Chrome", e);
|
||||||
|
}
|
||||||
|
return Promise.VOID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void detach() {
|
||||||
|
if (exchange != null) {
|
||||||
|
exchange.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttached() {
|
||||||
|
return exchange != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onAttach();
|
||||||
|
|
||||||
|
protected abstract void onDetach();
|
||||||
|
|
||||||
|
protected abstract Promise<Void> handleMessage(Message message) throws IOException;
|
||||||
|
|
||||||
|
private <T> Promise<T> callInExecutor(Supplier<Promise<T>> f) {
|
||||||
|
var result = new CompletablePromise<T>();
|
||||||
|
executor.execute(() -> {
|
||||||
|
f.get().thenVoid(result::complete).catchVoid(result::completeWithError);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> T parseJson(Class<T> type, JsonNode node) throws IOException {
|
||||||
|
return mapper.readerFor(type).readValue(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void sendMessage(Message message) {
|
||||||
|
if (exchange == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
exchange.send(mapper.writer().writeValueAsString(message));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R> Promise<R> callMethodAsync(String method, Class<R> returnType, Object params) {
|
||||||
|
if (!isAttached()) {
|
||||||
|
return Promise.of(null);
|
||||||
|
}
|
||||||
|
var message = new Message();
|
||||||
|
message.setId(++messageIdGenerator);
|
||||||
|
message.setMethod(method);
|
||||||
|
if (params != null) {
|
||||||
|
message.setParams(mapper.valueToTree(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message);
|
||||||
|
return setResponseHandler(message.getId(), (JsonNode node, CompletablePromise<R> out) -> {
|
||||||
|
if (node == null) {
|
||||||
|
out.complete(null);
|
||||||
|
} else {
|
||||||
|
R response = returnType != void.class ? mapper.readerFor(returnType).readValue(node) : null;
|
||||||
|
out.complete(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> Promise<T> setResponseHandler(int messageId, ResponseHandler<T> handler) {
|
||||||
|
CompletablePromise<T> promise = new CompletablePromise<>();
|
||||||
|
promises.put(messageId, (CompletablePromise<Object>) promise);
|
||||||
|
responseHandlers.put(messageId, (ResponseHandler<Object>) handler);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseHandler<T> {
|
||||||
|
void received(JsonNode node, CompletablePromise<T> out) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.teavm.chromerdp;
|
package org.teavm.chromerdp;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.NullNode;
|
import com.fasterxml.jackson.databind.node.NullNode;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -27,20 +25,13 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.teavm.chromerdp.data.CallArgumentDTO;
|
import org.teavm.chromerdp.data.CallArgumentDTO;
|
||||||
import org.teavm.chromerdp.data.CallFrameDTO;
|
import org.teavm.chromerdp.data.CallFrameDTO;
|
||||||
import org.teavm.chromerdp.data.LocationDTO;
|
import org.teavm.chromerdp.data.LocationDTO;
|
||||||
import org.teavm.chromerdp.data.Message;
|
import org.teavm.chromerdp.data.Message;
|
||||||
import org.teavm.chromerdp.data.PropertyDescriptorDTO;
|
import org.teavm.chromerdp.data.PropertyDescriptorDTO;
|
||||||
import org.teavm.chromerdp.data.RemoteObjectDTO;
|
import org.teavm.chromerdp.data.RemoteObjectDTO;
|
||||||
import org.teavm.chromerdp.data.Response;
|
|
||||||
import org.teavm.chromerdp.data.ScopeDTO;
|
import org.teavm.chromerdp.data.ScopeDTO;
|
||||||
import org.teavm.chromerdp.messages.CallFunctionCommand;
|
import org.teavm.chromerdp.messages.CallFunctionCommand;
|
||||||
import org.teavm.chromerdp.messages.CallFunctionResponse;
|
import org.teavm.chromerdp.messages.CallFunctionResponse;
|
||||||
|
@ -64,12 +55,9 @@ import org.teavm.debugging.javascript.JavaScriptDebuggerListener;
|
||||||
import org.teavm.debugging.javascript.JavaScriptLocation;
|
import org.teavm.debugging.javascript.JavaScriptLocation;
|
||||||
import org.teavm.debugging.javascript.JavaScriptVariable;
|
import org.teavm.debugging.javascript.JavaScriptVariable;
|
||||||
|
|
||||||
public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeConsumer {
|
public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScriptDebugger {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ChromeRDPDebugger.class);
|
|
||||||
private static final Promise<Map<String, ? extends JavaScriptVariable>> EMPTY_SCOPE =
|
private static final Promise<Map<String, ? extends JavaScriptVariable>> EMPTY_SCOPE =
|
||||||
Promise.of(Collections.emptyMap());
|
Promise.of(Collections.emptyMap());
|
||||||
private volatile ChromeRDPExchange exchange;
|
|
||||||
private Set<JavaScriptDebuggerListener> listeners = new LinkedHashSet<>();
|
|
||||||
private Map<JavaScriptLocation, RDPNativeBreakpoint> breakpointLocationMap = new HashMap<>();
|
private Map<JavaScriptLocation, RDPNativeBreakpoint> breakpointLocationMap = new HashMap<>();
|
||||||
private Set<RDPBreakpoint> breakpoints = new LinkedHashSet<>();
|
private Set<RDPBreakpoint> breakpoints = new LinkedHashSet<>();
|
||||||
private Map<String, RDPNativeBreakpoint> breakpointsByChromeId = new HashMap<>();
|
private Map<String, RDPNativeBreakpoint> breakpointsByChromeId = new HashMap<>();
|
||||||
|
@ -77,48 +65,23 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
private Map<String, String> scripts = new HashMap<>();
|
private Map<String, String> scripts = new HashMap<>();
|
||||||
private Map<String, String> scriptIds = new HashMap<>();
|
private Map<String, String> scriptIds = new HashMap<>();
|
||||||
private volatile boolean suspended;
|
private volatile boolean suspended;
|
||||||
private ObjectMapper mapper = new ObjectMapper();
|
|
||||||
private ConcurrentMap<Integer, ResponseHandler<Object>> responseHandlers = new ConcurrentHashMap<>();
|
|
||||||
private ConcurrentMap<Integer, CompletablePromise<Object>> promises = new ConcurrentHashMap<>();
|
|
||||||
private AtomicInteger messageIdGenerator = new AtomicInteger();
|
|
||||||
private Promise<Void> runtimeEnabledPromise;
|
private Promise<Void> runtimeEnabledPromise;
|
||||||
|
|
||||||
private List<JavaScriptDebuggerListener> getListeners() {
|
|
||||||
return new ArrayList<>(listeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Executor executor;
|
|
||||||
|
|
||||||
public ChromeRDPDebugger(Executor executor) {
|
public ChromeRDPDebugger(Executor executor) {
|
||||||
this.executor = executor;
|
super(executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setExchange(ChromeRDPExchange exchange) {
|
protected void onAttach() {
|
||||||
if (this.exchange == exchange) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.exchange != null) {
|
|
||||||
this.exchange.removeListener(exchangeListener);
|
|
||||||
}
|
|
||||||
this.exchange = exchange;
|
|
||||||
if (exchange != null) {
|
|
||||||
for (RDPBreakpoint breakpoint : breakpoints.toArray(new RDPBreakpoint[0])) {
|
for (RDPBreakpoint breakpoint : breakpoints.toArray(new RDPBreakpoint[0])) {
|
||||||
updateBreakpoint(breakpoint.nativeBreakpoint);
|
updateBreakpoint(breakpoint.nativeBreakpoint);
|
||||||
}
|
}
|
||||||
for (JavaScriptDebuggerListener listener : getListeners()) {
|
|
||||||
listener.attached();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
@Override
|
||||||
|
protected void onDetach() {
|
||||||
suspended = false;
|
suspended = false;
|
||||||
callStack = null;
|
callStack = null;
|
||||||
for (JavaScriptDebuggerListener listener : getListeners()) {
|
|
||||||
listener.detached();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.exchange != null) {
|
|
||||||
this.exchange.addListener(exchangeListener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Promise<Void> injectFunctions(int contextId) {
|
private Promise<Void> injectFunctions(int contextId) {
|
||||||
|
@ -148,38 +111,8 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
return runtimeEnabledPromise;
|
return runtimeEnabledPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChromeRDPExchangeListener exchangeListener = messageText -> {
|
@Override
|
||||||
callInExecutor(() -> receiveMessage(messageText)
|
protected Promise<Void> handleMessage(Message message) throws IOException {
|
||||||
.catchError(e -> {
|
|
||||||
logger.error("Error handling message", e);
|
|
||||||
return null;
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
private Promise<Void> receiveMessage(String messageText) {
|
|
||||||
try {
|
|
||||||
JsonNode jsonMessage = mapper.readTree(messageText);
|
|
||||||
if (jsonMessage.has("id")) {
|
|
||||||
Response response = mapper.readerFor(Response.class).readValue(jsonMessage);
|
|
||||||
if (response.getError() != null) {
|
|
||||||
if (logger.isWarnEnabled()) {
|
|
||||||
logger.warn("Error message #{} received from browser: {}", jsonMessage.get("id"),
|
|
||||||
response.getError().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompletablePromise<Object> promise = promises.remove(response.getId());
|
|
||||||
try {
|
|
||||||
responseHandlers.remove(response.getId()).received(response.getResult(), promise);
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
logger.warn("Error processing message ${}", response.getId(), e);
|
|
||||||
promise.completeWithError(e);
|
|
||||||
}
|
|
||||||
return Promise.VOID;
|
|
||||||
} else {
|
|
||||||
Message message = mapper.readerFor(Message.class).readValue(messageText);
|
|
||||||
if (message.getMethod() == null) {
|
|
||||||
return Promise.VOID;
|
|
||||||
}
|
|
||||||
switch (message.getMethod()) {
|
switch (message.getMethod()) {
|
||||||
case "Debugger.paused":
|
case "Debugger.paused":
|
||||||
return firePaused(parseJson(SuspendedNotification.class, message.getParams()));
|
return firePaused(parseJson(SuspendedNotification.class, message.getParams()));
|
||||||
|
@ -190,13 +123,6 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
}
|
}
|
||||||
return Promise.VOID;
|
return Promise.VOID;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
if (logger.isErrorEnabled()) {
|
|
||||||
logger.error("Error receiving message from Google Chrome", e);
|
|
||||||
}
|
|
||||||
return Promise.VOID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Promise<Void> firePaused(SuspendedNotification params) {
|
private Promise<Void> firePaused(SuspendedNotification params) {
|
||||||
suspended = true;
|
suspended = true;
|
||||||
|
@ -293,24 +219,12 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSuspended() {
|
public boolean isSuspended() {
|
||||||
return exchange != null && suspended;
|
return isAttached() && suspended;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAttached() {
|
|
||||||
return exchange != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void detach() {
|
|
||||||
if (exchange != null) {
|
|
||||||
exchange.disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JavaScriptCallFrame[] getCallStack() {
|
public JavaScriptCallFrame[] getCallStack() {
|
||||||
if (exchange == null) {
|
if (!isAttached()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
JavaScriptCallFrame[] callStack = this.callStack;
|
JavaScriptCallFrame[] callStack = this.callStack;
|
||||||
|
@ -321,7 +235,7 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
public Promise<JavaScriptBreakpoint> createBreakpoint(JavaScriptLocation location) {
|
public Promise<JavaScriptBreakpoint> createBreakpoint(JavaScriptLocation location) {
|
||||||
RDPBreakpoint breakpoint = new RDPBreakpoint(this);
|
RDPBreakpoint breakpoint = new RDPBreakpoint(this);
|
||||||
breakpoint.nativeBreakpoint = lockNativeBreakpoint(location, breakpoint);
|
breakpoint.nativeBreakpoint = lockNativeBreakpoint(location, breakpoint);
|
||||||
CompletablePromise<JavaScriptBreakpoint> result = new CompletablePromise<>();
|
var result = new CompletablePromise<JavaScriptBreakpoint>();
|
||||||
breakpoints.add(breakpoint);
|
breakpoints.add(breakpoint);
|
||||||
breakpoint.nativeBreakpoint.initPromise.thenVoid(v -> result.complete(breakpoint));
|
breakpoint.nativeBreakpoint.initPromise.thenVoid(v -> result.complete(breakpoint));
|
||||||
return result;
|
return result;
|
||||||
|
@ -550,21 +464,6 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T parseJson(Class<T> type, JsonNode node) throws IOException {
|
|
||||||
return mapper.readerFor(type).readValue(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessage(Message message) {
|
|
||||||
if (exchange == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
exchange.send(mapper.writer().writeValueAsString(message));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RDPCallFrame map(CallFrameDTO dto) {
|
private RDPCallFrame map(CallFrameDTO dto) {
|
||||||
String scopeId = null;
|
String scopeId = null;
|
||||||
RDPValue thisObject = null;
|
RDPValue thisObject = null;
|
||||||
|
@ -600,40 +499,6 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <R> Promise<R> callMethodAsync(String method, Class<R> returnType, Object params) {
|
|
||||||
if (exchange == null) {
|
|
||||||
return Promise.of(null);
|
|
||||||
}
|
|
||||||
Message message = new Message();
|
|
||||||
message.setId(messageIdGenerator.incrementAndGet());
|
|
||||||
message.setMethod(method);
|
|
||||||
if (params != null) {
|
|
||||||
message.setParams(mapper.valueToTree(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(message);
|
|
||||||
return setResponseHandler(message.getId(), (JsonNode node, CompletablePromise<R> out) -> {
|
|
||||||
if (node == null) {
|
|
||||||
out.complete(null);
|
|
||||||
} else {
|
|
||||||
R response = returnType != void.class ? mapper.readerFor(returnType).readValue(node) : null;
|
|
||||||
out.complete(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private <T> Promise<T> setResponseHandler(int messageId, ResponseHandler<T> handler) {
|
|
||||||
CompletablePromise<T> promise = new CompletablePromise<>();
|
|
||||||
promises.put(messageId, (CompletablePromise<Object>) promise);
|
|
||||||
responseHandlers.put(messageId, (ResponseHandler<Object>) handler);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResponseHandler<T> {
|
|
||||||
void received(JsonNode node, CompletablePromise<T> out) throws IOException;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise<Map<String, ? extends JavaScriptVariable>> createScope(String id) {
|
Promise<Map<String, ? extends JavaScriptVariable>> createScope(String id) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
return EMPTY_SCOPE;
|
return EMPTY_SCOPE;
|
||||||
|
@ -646,12 +511,4 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
||||||
return Collections.unmodifiableMap(newBackingMap);
|
return Collections.unmodifiableMap(newBackingMap);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> Promise<T> callInExecutor(Supplier<Promise<T>> f) {
|
|
||||||
CompletablePromise<T> result = new CompletablePromise<>();
|
|
||||||
executor.execute(() -> {
|
|
||||||
f.get().thenVoid(result::complete).catchVoid(result::completeWithError);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import javax.websocket.server.ServerEndpointConfig;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.server.ServerConnector;
|
import org.eclipse.jetty.server.ServerConnector;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
import org.eclipse.jetty.websocket.jsr356.server.ContainerDefaultConfigurator;
|
||||||
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
|
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
|
||||||
|
|
||||||
public class ChromeRDPServer {
|
public class ChromeRDPServer {
|
||||||
|
@ -80,6 +81,7 @@ public class ChromeRDPServer {
|
||||||
|
|
||||||
private class RPDEndpointConfig implements ServerEndpointConfig {
|
private class RPDEndpointConfig implements ServerEndpointConfig {
|
||||||
private Map<String, Object> userProperties = new HashMap<>();
|
private Map<String, Object> userProperties = new HashMap<>();
|
||||||
|
private ContainerDefaultConfigurator configurator = new ContainerDefaultConfigurator();
|
||||||
|
|
||||||
public RPDEndpointConfig() {
|
public RPDEndpointConfig() {
|
||||||
userProperties.put("chrome.rdp", exchangeConsumer);
|
userProperties.put("chrome.rdp", exchangeConsumer);
|
||||||
|
@ -102,7 +104,7 @@ public class ChromeRDPServer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Configurator getConfigurator() {
|
public Configurator getConfigurator() {
|
||||||
return null;
|
return configurator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Alexey Andreev.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.teavm.chromerdp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import org.teavm.backend.wasm.debug.parser.DebugInfoParser;
|
||||||
|
import org.teavm.backend.wasm.debug.parser.DebugInfoReader;
|
||||||
|
import org.teavm.chromerdp.data.Message;
|
||||||
|
import org.teavm.chromerdp.messages.GetScriptSourceCommand;
|
||||||
|
import org.teavm.chromerdp.messages.ScriptParsedNotification;
|
||||||
|
import org.teavm.chromerdp.messages.ScriptSource;
|
||||||
|
import org.teavm.common.CompletablePromise;
|
||||||
|
import org.teavm.common.Promise;
|
||||||
|
|
||||||
|
public class WasmChromeRDPDebugger extends BaseChromeRDPDebugger {
|
||||||
|
public WasmChromeRDPDebugger(Executor executor) {
|
||||||
|
super(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttach() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetach() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Promise<Void> handleMessage(Message message) throws IOException {
|
||||||
|
switch (message.getMethod()) {
|
||||||
|
case "Debugger.scriptParsed":
|
||||||
|
return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams()));
|
||||||
|
}
|
||||||
|
return Promise.VOID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Promise<Void> scriptParsed(ScriptParsedNotification params) {
|
||||||
|
if (Objects.equals(params.getScriptLanguage(), "WebAssembly")) {
|
||||||
|
var callArgs = new GetScriptSourceCommand();
|
||||||
|
callArgs.scriptId = params.getScriptId();
|
||||||
|
return callMethodAsync("Debugger.getScriptSource", ScriptSource.class, callArgs)
|
||||||
|
.thenVoid(source -> parseWasm(source, params.getUrl()));
|
||||||
|
}
|
||||||
|
return Promise.VOID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseWasm(ScriptSource source, String url) {
|
||||||
|
if (source.bytecode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bytes = Base64.getDecoder().decode(source.bytecode);
|
||||||
|
var reader = new DebugInfoReaderImpl(bytes);
|
||||||
|
var debugInfoParser = new DebugInfoParser(reader);
|
||||||
|
Promise.runNow(() -> {
|
||||||
|
debugInfoParser.parse().catchVoid(Throwable::printStackTrace);
|
||||||
|
reader.complete();
|
||||||
|
});
|
||||||
|
var lineInfo = debugInfoParser.getLineInfo();
|
||||||
|
if (lineInfo != null) {
|
||||||
|
System.out.println("Debug information found in script: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DebugInfoReaderImpl implements DebugInfoReader {
|
||||||
|
private byte[] data;
|
||||||
|
private int ptr;
|
||||||
|
private CompletablePromise<Integer> promise;
|
||||||
|
private byte[] target;
|
||||||
|
private int offset;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
DebugInfoReaderImpl(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Promise<Integer> skip(int amount) {
|
||||||
|
promise = new CompletablePromise<>();
|
||||||
|
count = amount;
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Promise<Integer> read(byte[] buffer, int offset, int count) {
|
||||||
|
promise = new CompletablePromise<>();
|
||||||
|
this.target = buffer;
|
||||||
|
this.offset = offset;
|
||||||
|
this.count = count;
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void complete() {
|
||||||
|
while (promise != null) {
|
||||||
|
var p = promise;
|
||||||
|
count = Math.min(count, data.length - ptr);
|
||||||
|
promise = null;
|
||||||
|
if (target != null) {
|
||||||
|
System.arraycopy(data, ptr, target, offset, count);
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
ptr += count;
|
||||||
|
if (count == 0) {
|
||||||
|
count = -1;
|
||||||
|
}
|
||||||
|
p.complete(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Alexey Andreev.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.teavm.chromerdp;
|
||||||
|
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
public class WasmChromeRDPRunner {
|
||||||
|
private ChromeRDPServer server;
|
||||||
|
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
private WasmChromeRDPRunner() {
|
||||||
|
server = new ChromeRDPServer();
|
||||||
|
server.setPort(2357);
|
||||||
|
var wasmDebugger = new WasmChromeRDPDebugger(queue::offer);
|
||||||
|
server.setExchangeConsumer(wasmDebugger);
|
||||||
|
|
||||||
|
new Thread(server::start).start();
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||||
|
System.err.println("Uncaught exception in thread " + t);
|
||||||
|
e.printStackTrace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
var runner = new WasmChromeRDPRunner();
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
runner.run();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.out.println("Interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() throws InterruptedException {
|
||||||
|
while (true) {
|
||||||
|
queue.take().run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Alexey Andreev.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.teavm.chromerdp.messages;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class GetScriptSourceCommand {
|
||||||
|
public String scriptId;
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ public class ScriptParsedNotification {
|
||||||
private String scriptId;
|
private String scriptId;
|
||||||
private String url;
|
private String url;
|
||||||
private int executionContextId;
|
private int executionContextId;
|
||||||
|
private String scriptLanguage;
|
||||||
|
|
||||||
public String getScriptId() {
|
public String getScriptId() {
|
||||||
return scriptId;
|
return scriptId;
|
||||||
|
@ -46,4 +47,12 @@ public class ScriptParsedNotification {
|
||||||
public void setExecutionContextId(int executionContextId) {
|
public void setExecutionContextId(int executionContextId) {
|
||||||
this.executionContextId = executionContextId;
|
this.executionContextId = executionContextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getScriptLanguage() {
|
||||||
|
return scriptLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScriptLanguage(String scriptLanguage) {
|
||||||
|
this.scriptLanguage = scriptLanguage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Alexey Andreev.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.teavm.chromerdp.messages;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class ScriptSource {
|
||||||
|
public String scriptSource;
|
||||||
|
public String bytecode;
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ class DebuggerAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
chrome.debugger.attach(this.debuggee, "1.2", () => {
|
chrome.debugger.attach(this.debuggee, "1.3", () => {
|
||||||
this.attachedToDebugger = true;
|
this.attachedToDebugger = true;
|
||||||
chrome.debugger.sendCommand(this.debuggee, "Debugger.enable", {}, () => this.connectToServer());
|
chrome.debugger.sendCommand(this.debuggee, "Debugger.enable", {}, () => this.connectToServer());
|
||||||
});
|
});
|
||||||
|
@ -102,7 +102,7 @@ class DebuggerAgent {
|
||||||
|
|
||||||
DebuggerAgent.MAX_MESSAGE_SIZE = 65534;
|
DebuggerAgent.MAX_MESSAGE_SIZE = 65534;
|
||||||
|
|
||||||
chrome.browserAction.onClicked.addListener(function(tab) {
|
chrome.action.onClicked.addListener(tab => {
|
||||||
chrome.storage.sync.get({ port: 2357 }, items => attachDebugger(tab, items.port));
|
chrome.storage.sync.get({ port: 2357 }, items => attachDebugger(tab, items.port));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
|
|
||||||
"name": "TeaVM debugger agent",
|
"name": "TeaVM debugger agent",
|
||||||
"description": "TeaVM debugger agent, that sends RDP commands over WebSocket",
|
"description": "TeaVM debugger agent, that sends RDP commands over WebSocket",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
|
|
||||||
"permissions" : ["debugger", "activeTab", "tabs", "storage", "*://*/*"],
|
"permissions" : ["debugger", "activeTab", "tabs", "storage", "scripting"],
|
||||||
|
|
||||||
"browser_action" : {
|
"action" : {
|
||||||
"default_icon": "teavm-16.png",
|
"default_icon": "teavm-16.png",
|
||||||
"default_title ": "Connect to TeaVM debugger"
|
"default_title ": "Connect to TeaVM debugger"
|
||||||
},
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["main.js"]
|
"service_worker": "main.js"
|
||||||
},
|
},
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
|
|
Loading…
Reference in New Issue
Block a user