mirror of
https://github.com/Eaglercraft-TeaVM-Fork/eagler-teavm.git
synced 2024-12-22 08:14:09 -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) {
|
||||
return readString("Error reading custom section name").thenAsync(name -> {
|
||||
var sectionParser = sectionParsers.get(name);
|
||||
System.out.println("Section '" + name + "': " + (sectionParser != null ? "parsed" : "unknown"));
|
||||
if (sectionParser != null) {
|
||||
return readBytes(limit - pos, "Error reading section '" + name + "' content")
|
||||
.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 {
|
||||
PENDING,
|
||||
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;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -27,20 +25,13 @@ import java.util.LinkedHashSet;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
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.CallFrameDTO;
|
||||
import org.teavm.chromerdp.data.LocationDTO;
|
||||
import org.teavm.chromerdp.data.Message;
|
||||
import org.teavm.chromerdp.data.PropertyDescriptorDTO;
|
||||
import org.teavm.chromerdp.data.RemoteObjectDTO;
|
||||
import org.teavm.chromerdp.data.Response;
|
||||
import org.teavm.chromerdp.data.ScopeDTO;
|
||||
import org.teavm.chromerdp.messages.CallFunctionCommand;
|
||||
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.JavaScriptVariable;
|
||||
|
||||
public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeConsumer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChromeRDPDebugger.class);
|
||||
public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScriptDebugger {
|
||||
private static final Promise<Map<String, ? extends JavaScriptVariable>> EMPTY_SCOPE =
|
||||
Promise.of(Collections.emptyMap());
|
||||
private volatile ChromeRDPExchange exchange;
|
||||
private Set<JavaScriptDebuggerListener> listeners = new LinkedHashSet<>();
|
||||
private Map<JavaScriptLocation, RDPNativeBreakpoint> breakpointLocationMap = new HashMap<>();
|
||||
private Set<RDPBreakpoint> breakpoints = new LinkedHashSet<>();
|
||||
private Map<String, RDPNativeBreakpoint> breakpointsByChromeId = new HashMap<>();
|
||||
|
@ -77,50 +65,25 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
private Map<String, String> scripts = new HashMap<>();
|
||||
private Map<String, String> scriptIds = new HashMap<>();
|
||||
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 List<JavaScriptDebuggerListener> getListeners() {
|
||||
return new ArrayList<>(listeners);
|
||||
}
|
||||
|
||||
private Executor executor;
|
||||
|
||||
public ChromeRDPDebugger(Executor executor) {
|
||||
this.executor = executor;
|
||||
super(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) {
|
||||
for (RDPBreakpoint breakpoint : breakpoints.toArray(new RDPBreakpoint[0])) {
|
||||
updateBreakpoint(breakpoint.nativeBreakpoint);
|
||||
}
|
||||
for (JavaScriptDebuggerListener listener : getListeners()) {
|
||||
listener.attached();
|
||||
}
|
||||
} else {
|
||||
suspended = false;
|
||||
callStack = null;
|
||||
for (JavaScriptDebuggerListener listener : getListeners()) {
|
||||
listener.detached();
|
||||
}
|
||||
}
|
||||
if (this.exchange != null) {
|
||||
this.exchange.addListener(exchangeListener);
|
||||
protected void onAttach() {
|
||||
for (RDPBreakpoint breakpoint : breakpoints.toArray(new RDPBreakpoint[0])) {
|
||||
updateBreakpoint(breakpoint.nativeBreakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetach() {
|
||||
suspended = false;
|
||||
callStack = null;
|
||||
}
|
||||
|
||||
private Promise<Void> injectFunctions(int contextId) {
|
||||
return enableRuntime()
|
||||
.thenAsync(v -> {
|
||||
|
@ -148,54 +111,17 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
return runtimeEnabledPromise;
|
||||
}
|
||||
|
||||
private ChromeRDPExchangeListener exchangeListener = messageText -> {
|
||||
callInExecutor(() -> receiveMessage(messageText)
|
||||
.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()) {
|
||||
case "Debugger.paused":
|
||||
return firePaused(parseJson(SuspendedNotification.class, message.getParams()));
|
||||
case "Debugger.resumed":
|
||||
return fireResumed();
|
||||
case "Debugger.scriptParsed":
|
||||
return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams()));
|
||||
}
|
||||
return Promise.VOID;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error("Error receiving message from Google Chrome", e);
|
||||
}
|
||||
return Promise.VOID;
|
||||
@Override
|
||||
protected Promise<Void> handleMessage(Message message) throws IOException {
|
||||
switch (message.getMethod()) {
|
||||
case "Debugger.paused":
|
||||
return firePaused(parseJson(SuspendedNotification.class, message.getParams()));
|
||||
case "Debugger.resumed":
|
||||
return fireResumed();
|
||||
case "Debugger.scriptParsed":
|
||||
return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams()));
|
||||
}
|
||||
return Promise.VOID;
|
||||
}
|
||||
|
||||
private Promise<Void> firePaused(SuspendedNotification params) {
|
||||
|
@ -293,24 +219,12 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
|
||||
@Override
|
||||
public boolean isSuspended() {
|
||||
return exchange != null && suspended;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAttached() {
|
||||
return exchange != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
if (exchange != null) {
|
||||
exchange.disconnect();
|
||||
}
|
||||
return isAttached() && suspended;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaScriptCallFrame[] getCallStack() {
|
||||
if (exchange == null) {
|
||||
if (!isAttached()) {
|
||||
return null;
|
||||
}
|
||||
JavaScriptCallFrame[] callStack = this.callStack;
|
||||
|
@ -321,7 +235,7 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
public Promise<JavaScriptBreakpoint> createBreakpoint(JavaScriptLocation location) {
|
||||
RDPBreakpoint breakpoint = new RDPBreakpoint(this);
|
||||
breakpoint.nativeBreakpoint = lockNativeBreakpoint(location, breakpoint);
|
||||
CompletablePromise<JavaScriptBreakpoint> result = new CompletablePromise<>();
|
||||
var result = new CompletablePromise<JavaScriptBreakpoint>();
|
||||
breakpoints.add(breakpoint);
|
||||
breakpoint.nativeBreakpoint.initPromise.thenVoid(v -> result.complete(breakpoint));
|
||||
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) {
|
||||
String scopeId = null;
|
||||
RDPValue thisObject = null;
|
||||
|
@ -600,40 +499,6 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
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) {
|
||||
if (id == null) {
|
||||
return EMPTY_SCOPE;
|
||||
|
@ -646,12 +511,4 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC
|
|||
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.ServerConnector;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.websocket.jsr356.server.ContainerDefaultConfigurator;
|
||||
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
|
||||
|
||||
public class ChromeRDPServer {
|
||||
|
@ -80,6 +81,7 @@ public class ChromeRDPServer {
|
|||
|
||||
private class RPDEndpointConfig implements ServerEndpointConfig {
|
||||
private Map<String, Object> userProperties = new HashMap<>();
|
||||
private ContainerDefaultConfigurator configurator = new ContainerDefaultConfigurator();
|
||||
|
||||
public RPDEndpointConfig() {
|
||||
userProperties.put("chrome.rdp", exchangeConsumer);
|
||||
|
@ -102,7 +104,7 @@ public class ChromeRDPServer {
|
|||
|
||||
@Override
|
||||
public Configurator getConfigurator() {
|
||||
return null;
|
||||
return configurator;
|
||||
}
|
||||
|
||||
@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 url;
|
||||
private int executionContextId;
|
||||
private String scriptLanguage;
|
||||
|
||||
public String getScriptId() {
|
||||
return scriptId;
|
||||
|
@ -46,4 +47,12 @@ public class ScriptParsedNotification {
|
|||
public void setExecutionContextId(int 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() {
|
||||
chrome.debugger.attach(this.debuggee, "1.2", () => {
|
||||
chrome.debugger.attach(this.debuggee, "1.3", () => {
|
||||
this.attachedToDebugger = true;
|
||||
chrome.debugger.sendCommand(this.debuggee, "Debugger.enable", {}, () => this.connectToServer());
|
||||
});
|
||||
|
@ -102,7 +102,7 @@ class DebuggerAgent {
|
|||
|
||||
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));
|
||||
});
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"manifest_version": 3,
|
||||
|
||||
"name": "TeaVM debugger agent",
|
||||
"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_title ": "Connect to TeaVM debugger"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["main.js"]
|
||||
"service_worker": "main.js"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
|
@ -27,4 +27,4 @@
|
|||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user