Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.spencerpark.jupyter.kernel;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import io.github.spencerpark.jupyter.channels.JupyterConnection;
import io.github.spencerpark.jupyter.channels.JupyterSocket;
Expand All @@ -19,7 +20,6 @@
import io.github.spencerpark.jupyter.messages.Header;
import io.github.spencerpark.jupyter.messages.Message;
import io.github.spencerpark.jupyter.messages.MessageType;
import io.github.spencerpark.jupyter.messages.adapters.JsonBox;
import io.github.spencerpark.jupyter.messages.publish.PublishError;
import io.github.spencerpark.jupyter.messages.publish.PublishExecuteInput;
import io.github.spencerpark.jupyter.messages.publish.PublishExecuteResult;
Expand Down Expand Up @@ -492,7 +492,7 @@ private void handleDebugRequest(ShellReplyEnvironment env, Message<DebugRequest>
JupyterDebugEventPublisher pub = new JupyterDebugEventPublisher(env);
env.defer(() -> pub.retractEnv(env));

JsonBox.Wrapper response = debugger.handleDapRequest(pub, request.getDapRequest());
JsonElement response = debugger.handleDapRequest(request.getDapRequest());
env.defer().reply(new DebugReply(response));
} catch (Exception e) {
env.replyError(DebugReply.MESSAGE_TYPE.error(), ErrorReply.of(e));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.spencerpark.jupyter.kernel;

import com.google.gson.JsonElement;
import io.github.spencerpark.jupyter.channels.ShellReplyEnvironment;
import io.github.spencerpark.jupyter.kernel.debugger.DapEventPublisher;
import io.github.spencerpark.jupyter.messages.adapters.JsonBox;
import io.github.spencerpark.jupyter.messages.publish.PublishDebugEvent;

public class JupyterDebugEventPublisher implements DapEventPublisher {
Expand All @@ -12,6 +12,8 @@ public JupyterDebugEventPublisher(ShellReplyEnvironment env) {
this.env = env;
}

// TODO should be kept after the reply it seems? Or maybe parent doesn't matter and python just uses
// the most recent one.
protected void retractEnv(ShellReplyEnvironment env) {
if (this.env == env)
this.env = null;
Expand All @@ -22,7 +24,7 @@ public boolean isAttached() {
}

@Override
public void emit(JsonBox.Wrapper dapEvent) {
public void emit(JsonElement dapEvent) {
if (this.env != null) {
this.env.publish(new PublishDebugEvent(dapEvent));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.spencerpark.jupyter.kernel.debugger;

@FunctionalInterface
public interface DapCommandHandler<A, B> {
B handle(A arguments) throws DapException;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.spencerpark.jupyter.kernel.debugger;

import io.github.spencerpark.jupyter.messages.adapters.JsonBox;
import com.google.gson.JsonElement;

public interface DapEventPublisher {
void emit(JsonBox.Wrapper dapEvent);
void emit(JsonElement dapEvent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.github.spencerpark.jupyter.kernel.debugger;

import io.github.spencerpark.jupyter.messages.debug.DapErrorMessage;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class DapException extends RuntimeException {
// Convention for names is if it starts with an underscore then the value doesn't contain PII and can be used for telemetry.
private static final Pattern formatVariableRefPattern = Pattern.compile("\\{(?<var>[^}]*)}");

public static String formatMessage(String format, Map<String, String> variables) {
if (variables == null || variables.isEmpty()) {
return format;
}

StringBuilder formattedMessage = new StringBuilder();
Matcher matcher = formatVariableRefPattern.matcher(format);
while (matcher.find()) {
String variable = matcher.group("var");
String value = variables.getOrDefault(variable, "");

matcher.appendReplacement(formattedMessage, Matcher.quoteReplacement(value));
}
matcher.appendTail(formattedMessage);

return formattedMessage.toString();
}

public abstract int getTypeId();

public String getShortMessageCode() {
return null;
}

public boolean shouldSendTelemetry() {
return false;
}

public boolean shouldShowUser() {
return false;
}

public String getErrorHelpUrl() {
return null;
}

public String getErrorHelpUrlLabel() {
return null;
}

public String getRawMessage() {
return super.getMessage();
}

public Map<String, String> getVariables() {
return null;
}

@Override
public String getMessage() {
return formatMessage(this.getRawMessage(), this.getVariables());
}

public final DapErrorMessage toDapMessage() {
return new DapErrorMessage(
getTypeId(),
this.getRawMessage(),
this.getVariables(),
this.shouldSendTelemetry() ? true : null,
this.shouldShowUser() ? true : null,
this.getErrorHelpUrl(),
this.getErrorHelpUrlLabel()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.spencerpark.jupyter.kernel.debugger;

@FunctionalInterface
public interface DapNoArgCommandHandler<B> {
B handle() throws DapException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package io.github.spencerpark.jupyter.kernel.debugger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import io.github.spencerpark.jupyter.messages.adapters.JsonBox;
import io.github.spencerpark.jupyter.messages.debug.DapCommandType;
import io.github.spencerpark.jupyter.messages.debug.DapErrorMessage;
import io.github.spencerpark.jupyter.messages.debug.DapEvent;
import io.github.spencerpark.jupyter.messages.debug.DapEventType;
import io.github.spencerpark.jupyter.messages.debug.DapProtocolMessage;
import io.github.spencerpark.jupyter.messages.debug.DapRequest;
import io.github.spencerpark.jupyter.messages.debug.DapResponse;
import io.github.spencerpark.jupyter.messages.debug.adapters.DapCommandTypeAdapter;
import io.github.spencerpark.jupyter.messages.debug.adapters.DapEventTypeAdapter;
import io.github.spencerpark.jupyter.messages.debug.adapters.DapProtocolMessageAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public abstract class DapProxy implements Debugger {
private static final Logger LOG = LoggerFactory.getLogger(DapProxy.class);

private final Gson gson = JsonBox.registerTypeAdapters(new GsonBuilder())
.registerTypeAdapter(DapCommandType.class, DapCommandTypeAdapter.INSTANCE)
.registerTypeAdapter(DapEventType.class, DapEventTypeAdapter.INSTANCE)
.registerTypeAdapter(DapProtocolMessage.class, DapProtocolMessageAdapter.INSTANCE)
.create();
private final Gson untypedGson = JsonBox.registerTypeAdapters(new GsonBuilder())
.registerTypeAdapter(DapCommandType.class, DapCommandTypeAdapter.UNTYPED_INSTANCE)
.registerTypeAdapter(DapEventType.class, DapEventTypeAdapter.UNTYPED_INSTANCE)
.registerTypeAdapter(DapProtocolMessage.class, DapProtocolMessageAdapter.INSTANCE)
.create();

private final AtomicInteger seq = new AtomicInteger();

private final Set<DapEventPublisher> eventSubscribers = new HashSet<>();

private final Map<DapCommandType<?, ?>, DapCommandHandler<?, ?>> requestHandlers = new HashMap<>();

public final <A, B> void registerHandler(DapCommandType<A, B> type, DapCommandHandler<A, B> handler) {
if (this.requestHandlers.put(type, handler) != null) {
LOG.warn("Overwriting existing handler for {}", type);
}
}

@SuppressWarnings("unchecked")
public final <B> void registerNoArgHandler(DapCommandType<?, B> type, DapNoArgCommandHandler<B> handler) {
this.registerHandler((DapCommandType<Object, B>) type, _args -> handler.handle());
}

@Override
public Runnable subscribe(DapEventPublisher pub) {
synchronized (this.eventSubscribers) {
this.eventSubscribers.add(pub);
return () -> {
synchronized (this.eventSubscribers) {
this.eventSubscribers.remove(pub);
}
};
}
}

private void publishDapEvent(DapEvent<?> event) {
synchronized (this.eventSubscribers) {
if (this.eventSubscribers.isEmpty()) {
LOG.debug("No event subscribers, dropping '{}' event.", event.getEvent());
return;
}

JsonElement jsonEvent = this.gson.toJsonTree(event);
LOG.debug("Publishing '{}' event: {}", event.getEvent(), jsonEvent);

for (DapEventPublisher pub : this.eventSubscribers) {
try {
pub.emit(jsonEvent);
} catch (Exception e) {
LOG.warn("Error publishing event:", e);
}
}
}
}

protected final <B> void publishDapEvent(DapEventType<B> type, B body) {
this.publishDapEvent(new DapEvent<>(this.seq.getAndIncrement(), type, body));
}

protected final <B> void publishDapEvent(DapEventType<B> type) {
this.publishDapEvent(type, null);
}

protected final void publishDapEvent(JsonElement dapEvent) {
DapEvent<?> event;
try {
event = this.untypedGson.fromJson(dapEvent, DapEvent.class);
} catch (Exception e) {
LOG.error("Cannot parse DAP event " + dapEvent + ". Skipping publish.", e);
return;
}

this.publishDapEvent(event.withSeq(this.seq.getAndIncrement()));
}

/**
* Forwards any commands not handled by this proxy. Typically, the jupyter specific extension commands would be
* handled and the rest forwarded to an existing DAP server.
*
* @param dapRequest the wrapped request.
* @return the wrapped reply.
*/
protected abstract JsonElement forwardRequest(JsonElement dapRequest) throws DapException;

protected DapErrorMessage wrapUnknownException(Exception e) {
return null;
}

@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public JsonElement handleDapRequest(JsonElement jsonRequest) {
DapRequest<?, ?> request = this.gson.fromJson(jsonRequest, DapRequest.class);
DapCommandType commandType = request.getCommand();

try {
DapCommandHandler handler = this.requestHandlers.get(commandType);
if (handler == null) {
LOG.debug("No handler for {}. Forwarding request: json={}", commandType, jsonRequest);
JsonElement forwardedJsonResponse = this.forwardRequest(jsonRequest);
LOG.debug("Forwarded {} command returned: json={}", commandType, forwardedJsonResponse);
DapResponse<?, ?> response = this.untypedGson.fromJson(forwardedJsonResponse, DapResponse.class)
.withSeq(this.seq.getAndIncrement());
return this.gson.toJsonTree(response);
} else {
LOG.debug("Handling {}({} args): args={}", commandType, commandType.getArgumentsType(), request.getArguments());
Object body = handler.handle(request.getArguments());
LOG.debug("Handler for {} command returned: body={}", commandType, body);
return this.gson.toJsonTree(
DapResponse.success(this.seq.getAndIncrement(), request.getSeq(), commandType, body));
}
} catch (DapException e) {
return this.gson.toJsonTree(
DapResponse.error(this.seq.getAndIncrement(), request.getSeq(), commandType, e.getShortMessageCode(), e.toDapMessage()));
} catch (Exception e) {
DapErrorMessage msg = wrapUnknownException(e);
if (msg == null) {
LOG.error("Unhandled exception thrown while handling DAP request for " + commandType.getName() + ":", e);
throw e;
} else {
return this.gson.toJsonTree(
DapResponse.error(this.seq.getAndIncrement(), request.getSeq(), commandType, null, msg));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.github.spencerpark.jupyter.kernel.debugger;

import io.github.spencerpark.jupyter.messages.adapters.JsonBox;
import com.google.gson.JsonElement;

public interface Debugger {
Runnable subscribe(DapEventPublisher pub);

JsonBox.Wrapper handleDapRequest(DapEventPublisher pub, JsonBox.Wrapper dapRequest);
JsonElement handleDapRequest(JsonElement dapRequest);
}
Loading