aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/skylarkdebug
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2018-06-07 14:07:17 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-06-07 14:08:44 -0700
commit5e893626640351de0f12e36bb14d80af0ff1e036 (patch)
treeb9a7083618290b867bd40383881a06e89060ce27 /src/main/java/com/google/devtools/build/lib/skylarkdebug
parent755278df00f65818dc092fe4f8a31bdec1aaaab5 (diff)
Add an initial skylark debug server implementation.
Not intending this to be a complete implementation yet. Among the things still to do: - officially add support for debugging aspects, rules, etc., with corresponding tests. - handle breakpoints at return statements (an edge case which bypasses Eval). PiperOrigin-RevId: 199692670
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/skylarkdebug')
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java14
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto15
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD1
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java203
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java26
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java106
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java261
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java334
9 files changed, 954 insertions, 8 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
index 00fae167a0..0fc7f18c04 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
@@ -13,6 +13,8 @@ java_library(
":options",
"//src/main/java/com/google/devtools/build/lib:events",
"//src/main/java/com/google/devtools/build/lib:runtime",
+ "//src/main/java/com/google/devtools/build/lib:syntax",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/server",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
index 167d3c853b..8dbe9e020a 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
@@ -14,9 +14,13 @@
package com.google.devtools.build.lib.skylarkdebug.module;
+import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.skylarkdebug.server.SkylarkDebugServer;
+import com.google.devtools.build.lib.syntax.DebugServerUtils;
+import java.io.IOException;
/** Blaze module for setting up Skylark debugging. */
public final class SkylarkDebuggerModule extends BlazeModule {
@@ -38,10 +42,16 @@ public final class SkylarkDebuggerModule extends BlazeModule {
}
private static void initializeDebugging(Reporter reporter, int debugPort) {
- // TODO(brendandouglas): implement a debug server
+ try {
+ SkylarkDebugServer server =
+ SkylarkDebugServer.createAndWaitForConnection(reporter, debugPort);
+ DebugServerUtils.initializeDebugServer(server);
+ } catch (IOException e) {
+ reporter.handle(Event.error("Error while setting up the debug server: " + e.getMessage()));
+ }
}
private static void disableDebugging() {
- // TODO(brendandouglas): implement a debug server
+ DebugServerUtils.disableDebugging();
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto b/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
index 804319397a..ed398e70fa 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
@@ -89,8 +89,9 @@ message StartDebuggingRequest {
message PauseThreadRequest {
// The identifier of the thread to be paused.
//
- // If not set (i.e. zero), all current and future Skylark threads will be
- // paused until resumed via a ContinueExecutionRequest.
+ // If not set (i.e. zero), all current Skylark threads will be paused, and
+ // until a ContinueExecutionRequest is sent, any future Skylark threads will
+ // also start off paused.
int64 thread_id = 1;
}
@@ -195,6 +196,7 @@ message ThreadContinuedEvent {
message Breakpoint {
oneof condition {
// A breakpoint that is triggered when a particular line is reached.
+ // Column index will be ignored for breakpoints.
Location location = 1;
}
// An optional condition for the breakpoint. When present, the breakpoint will
@@ -220,11 +222,12 @@ message Location {
// The path of the Skylark source file.
string path = 1;
- // A 0-based line index in the file denoted by path.
- uint32 line_index = 2;
+ // A 1-indexed line number in the file denoted by path.
+ uint32 line_number = 2;
- // A 0-based column index in the file denoted by path.
- uint32 column_index = 3;
+ // A 1-indexed column number in the file denoted by path. 0 (/unset) indicates
+ // column number is unknown or irrelevant.
+ uint32 column_number = 3;
}
// A scope that contains value bindings accessible in a frame.
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
index 42768bb69d..5122e25329 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
@@ -10,6 +10,7 @@ java_library(
name = "server",
srcs = glob(["*.java"]),
deps = [
+ "//src/main/java/com/google/devtools/build/lib:events",
"//src/main/java/com/google/devtools/build/lib:skylarkinterface",
"//src/main/java/com/google/devtools/build/lib:syntax",
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java
new file mode 100644
index 0000000000..0254484062
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java
@@ -0,0 +1,203 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.skylarkdebug.server;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ContinueExecutionResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Error;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.EvaluateResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Frame;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListFramesResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListThreadsResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.PauseThreadResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Scope;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.SetBreakpointsResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.StartDebuggingResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Thread;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadContinuedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadEndedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadPausedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadStartedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Value;
+import com.google.devtools.build.lib.syntax.DebugFrame;
+import com.google.devtools.build.lib.syntax.Debuggable.Stepping;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Helper class for constructing event or response protos to be sent from the debug server to a
+ * debugger client.
+ */
+final class DebugEventHelper {
+ private DebugEventHelper() {}
+
+ private static final long NO_SEQUENCE_NUMBER = 0;
+
+ static DebugEvent error(String message) {
+ return error(NO_SEQUENCE_NUMBER, message);
+ }
+
+ static DebugEvent error(long sequenceNumber, String message) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setError(Error.newBuilder().setMessage(message))
+ .build();
+ }
+
+ static DebugEvent listThreadsResponse(long sequenceNumber, List<Thread> threads) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setListThreads(ListThreadsResponse.newBuilder().addAllThread(threads).build())
+ .build();
+ }
+
+ static DebugEvent setBreakpointsResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setSetBreakpoints(SetBreakpointsResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent continueExecutionResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setContinueExecution(ContinueExecutionResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent evaluateResponse(long sequenceNumber, Value value) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setEvaluate(EvaluateResponse.newBuilder().setResult(value))
+ .build();
+ }
+
+ static DebugEvent listFramesResponse(long sequenceNumber, Collection<Frame> frames) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setListFrames(ListFramesResponse.newBuilder().addAllFrame(frames))
+ .build();
+ }
+
+ static DebugEvent startDebuggingResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setStartDebugging(StartDebuggingResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent pauseThreadResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setPauseThread(PauseThreadResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent threadStartedEvent(long threadId, String threadName) {
+ return DebugEvent.newBuilder()
+ .setThreadStarted(
+ ThreadStartedEvent.newBuilder()
+ .setThread(Thread.newBuilder().setId(threadId).setName(threadName))
+ .build())
+ .build();
+ }
+
+ static DebugEvent threadEndedEvent(long threadId, String threadName) {
+ return DebugEvent.newBuilder()
+ .setThreadEnded(
+ ThreadEndedEvent.newBuilder()
+ .setThread(Thread.newBuilder().setId(threadId).setName(threadName))
+ .build())
+ .build();
+ }
+
+ static DebugEvent threadPausedEvent(Thread thread) {
+ return DebugEvent.newBuilder()
+ .setThreadPaused(ThreadPausedEvent.newBuilder().setThread(thread))
+ .build();
+ }
+
+ static DebugEvent threadContinuedEvent(Thread thread) {
+ return DebugEvent.newBuilder()
+ .setThreadContinued(ThreadContinuedEvent.newBuilder().setThread(thread))
+ .build();
+ }
+
+ @Nullable
+ static SkylarkDebuggingProtos.Location getLocationProto(@Nullable Location location) {
+ if (location == null) {
+ return null;
+ }
+ Location.LineAndColumn lineAndColumn = location.getStartLineAndColumn();
+ if (lineAndColumn == null) {
+ return null;
+ }
+ return SkylarkDebuggingProtos.Location.newBuilder()
+ .setLineNumber(lineAndColumn.getLine())
+ .setColumnNumber(lineAndColumn.getColumn())
+ .setPath(location.getPath().getPathString())
+ .build();
+ }
+
+ static SkylarkDebuggingProtos.Frame getFrameProto(DebugFrame frame) {
+ return SkylarkDebuggingProtos.Frame.newBuilder()
+ .setFunctionName(frame.functionName())
+ .setLocation(getLocationProto(frame.location()))
+ .addAllScope(getScopes(frame))
+ .build();
+ }
+
+ private static ImmutableList<Scope> getScopes(DebugFrame frame) {
+ ImmutableMap<String, Object> localVars = frame.lexicalFrameBindings();
+ if (localVars.isEmpty()) {
+ return ImmutableList.of(getScope("global", frame.globalBindings()));
+ }
+ Map<String, Object> globalVars = new LinkedHashMap<>(frame.globalBindings());
+ // remove shadowed bindings
+ localVars.keySet().forEach(globalVars::remove);
+
+ return ImmutableList.of(getScope("local", localVars), getScope("global", globalVars));
+ }
+
+ private static SkylarkDebuggingProtos.Scope getScope(String name, Map<String, Object> bindings) {
+ SkylarkDebuggingProtos.Scope.Builder builder =
+ SkylarkDebuggingProtos.Scope.newBuilder().setName(name);
+ bindings.forEach((s, o) -> builder.addBinding(DebuggerSerialization.getValueProto(s, o)));
+ return builder.build();
+ }
+
+ static Stepping convertSteppingEnum(SkylarkDebuggingProtos.Stepping stepping) {
+ switch (stepping) {
+ case INTO:
+ return Stepping.INTO;
+ case OUT:
+ return Stepping.OUT;
+ case OVER:
+ return Stepping.OVER;
+ case NONE:
+ return Stepping.NONE;
+ case UNRECOGNIZED:
+ // fall through to exception
+ }
+ throw new IllegalArgumentException("Unsupported stepping type");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java
new file mode 100644
index 0000000000..092ac14fe5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java
@@ -0,0 +1,26 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.skylarkdebug.server;
+
+/**
+ * An exception that occurs while performing a debug request. Should be caught and communicated back
+ * to the debug client.
+ */
+final class DebugRequestException extends Exception {
+
+ DebugRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java
new file mode 100644
index 0000000000..0763fd5047
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java
@@ -0,0 +1,106 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.skylarkdebug.server;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import javax.annotation.Nullable;
+
+/**
+ * Manages the connection to and communication to/from the debugger client. Reading and writing are
+ * internally synchronized by {@link DebugServerTransport}.
+ */
+final class DebugServerTransport {
+
+ /** Sets up the server transport and blocks while waiting for an incoming connection. */
+ static DebugServerTransport createAndWaitForClient(
+ EventHandler eventHandler, ServerSocket serverSocket) throws IOException {
+ // TODO(bazel-team): reject all connections after the first
+ eventHandler.handle(Event.progress("Waiting for debugger..."));
+ Socket clientSocket = serverSocket.accept();
+ return new DebugServerTransport(
+ eventHandler,
+ serverSocket,
+ clientSocket,
+ clientSocket.getInputStream(),
+ clientSocket.getOutputStream());
+ }
+
+ private final EventHandler eventHandler;
+ private final ServerSocket serverSocket;
+ private final Socket clientSocket;
+ private final InputStream requestStream;
+ private final OutputStream eventStream;
+
+ private DebugServerTransport(
+ EventHandler eventHandler,
+ ServerSocket serverSocket,
+ Socket clientSocket,
+ InputStream requestStream,
+ OutputStream eventStream) {
+ this.eventHandler = eventHandler;
+ this.serverSocket = serverSocket;
+ this.clientSocket = clientSocket;
+ this.requestStream = requestStream;
+ this.eventStream = eventStream;
+ }
+
+ /**
+ * Blocks waiting for a properly-formed client request. Returns null if the client connection is
+ * closed.
+ */
+ @Nullable
+ DebugRequest readClientRequest() {
+ synchronized (requestStream) {
+ while (true) {
+ if (!clientSocket.isConnected() || clientSocket.isClosed()) {
+ return null;
+ }
+ try {
+ return DebugRequest.parseDelimitedFrom(requestStream);
+ } catch (IOException e) {
+ postEvent(DebugEventHelper.error("Error parsing debug request: " + e.getMessage()));
+ }
+ }
+ }
+ }
+
+ /** Posts a debug event. */
+ void postEvent(DebugEvent event) {
+ synchronized (eventStream) {
+ try {
+ event.writeDelimitedTo(eventStream);
+ } catch (IOException e) {
+ eventHandler.handle(Event.error("Failed to send debug event to client: " + e.getMessage()));
+ }
+ }
+ }
+
+ void close() throws IOException {
+ clientSocket.close();
+ serverSocket.close();
+ }
+
+ boolean isClosed() {
+ return serverSocket.isClosed() || clientSocket.isClosed();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
new file mode 100644
index 0000000000..71a3d7b41f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
@@ -0,0 +1,261 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Breakpoint.ConditionCase;
+import com.google.devtools.build.lib.syntax.DebugServer;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Eval;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Statement;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.List;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+/** Manages the network socket and debugging state for threads running Skylark code. */
+public final class SkylarkDebugServer implements DebugServer {
+
+ /**
+ * Initializes debugging support, setting up any debugging-specific overrides, then opens the
+ * debug server socket and blocks waiting for an incoming connection.
+ *
+ * @param port the port on which the server should listen for connections
+ * @throws IOException if an I/O error occurs while opening the socket or waiting for a connection
+ */
+ public static SkylarkDebugServer createAndWaitForConnection(EventHandler eventHandler, int port)
+ throws IOException {
+ ServerSocket serverSocket = new ServerSocket(port, /* backlog */ 1);
+ return createAndWaitForConnection(eventHandler, serverSocket);
+ }
+
+ /**
+ * Initializes debugging support, setting up any debugging-specific overrides, then opens the
+ * debug server socket and blocks waiting for an incoming connection.
+ *
+ * @throws IOException if an I/O error occurs while waiting for a connection
+ */
+ @VisibleForTesting
+ static SkylarkDebugServer createAndWaitForConnection(
+ EventHandler eventHandler, ServerSocket serverSocket) throws IOException {
+ DebugServerTransport transport =
+ DebugServerTransport.createAndWaitForClient(eventHandler, serverSocket);
+ return new SkylarkDebugServer(eventHandler, transport);
+ }
+
+ private final EventHandler eventHandler;
+ /** Handles all thread-related state. */
+ private final ThreadHandler threadHandler;
+ /** The server socket for the debug server. */
+ private final DebugServerTransport transport;
+
+ private SkylarkDebugServer(EventHandler eventHandler, DebugServerTransport transport) {
+ this.eventHandler = eventHandler;
+ this.threadHandler = new ThreadHandler();
+ this.transport = transport;
+ listenForClientRequests();
+ }
+
+ /**
+ * Starts a worker thread to listen for and handle incoming client requests, returning any
+ * relevant responses.
+ */
+ private void listenForClientRequests() {
+ Thread clientThread =
+ new Thread(
+ () -> {
+ try {
+ while (true) {
+ SkylarkDebuggingProtos.DebugRequest request = transport.readClientRequest();
+ if (request == null) {
+ return;
+ }
+ SkylarkDebuggingProtos.DebugEvent response = handleClientRequest(request);
+ if (response != null) {
+ transport.postEvent(response);
+ }
+ }
+ } catch (Throwable e) {
+ if (!transport.isClosed()) {
+ eventHandler.handle(
+ Event.error(
+ "Debug server listener thread died: "
+ + Throwables.getStackTraceAsString(e)));
+ }
+ }
+ });
+
+ clientThread.setDaemon(true);
+ clientThread.start();
+ }
+
+ @Override
+ public void close() {
+ try {
+ transport.close();
+
+ } catch (IOException e) {
+ eventHandler.handle(
+ Event.error(
+ "Error shutting down the debug server: " + Throwables.getStackTraceAsString(e)));
+ }
+ }
+
+ @Override
+ public Function<Environment, Eval> evalOverride() {
+ return DebugAwareEval::new;
+ }
+
+ @Override
+ public <T> T runWithDebugging(Environment env, String threadName, DebugCallable<T> callable)
+ throws EvalException, InterruptedException {
+ long threadId = Thread.currentThread().getId();
+ threadHandler.registerThread(threadId, threadName, env);
+ transport.postEvent(DebugEventHelper.threadStartedEvent(threadId, threadName));
+ try {
+ return callable.call();
+ } finally {
+ transport.postEvent(DebugEventHelper.threadEndedEvent(threadId, threadName));
+ threadHandler.unregisterThread(threadId);
+ }
+ }
+
+ /**
+ * Pauses the execution of the current thread if there are conditions that should cause it to be
+ * paused, such as a breakpoint being reached.
+ *
+ * @param location the location of the statement or expression currently being executed
+ */
+ @VisibleForTesting
+ void pauseIfNecessary(Environment env, Location location) {
+ if (!transport.isClosed()) {
+ threadHandler.pauseIfNecessary(env, location, transport);
+ }
+ }
+
+ /** Handles a request from the client, and returns the response, where relevant. */
+ @Nullable
+ private SkylarkDebuggingProtos.DebugEvent handleClientRequest(
+ SkylarkDebuggingProtos.DebugRequest request) {
+ long sequenceNumber = request.getSequenceNumber();
+ try {
+ switch (request.getPayloadCase()) {
+ case START_DEBUGGING:
+ threadHandler.resumeAllThreads();
+ return DebugEventHelper.startDebuggingResponse(sequenceNumber);
+ case LIST_THREADS:
+ return listThreads(sequenceNumber);
+ case LIST_FRAMES:
+ return listFrames(sequenceNumber, request.getListFrames());
+ case SET_BREAKPOINTS:
+ return setBreakpoints(sequenceNumber, request.getSetBreakpoints());
+ case CONTINUE_EXECUTION:
+ return continueExecution(sequenceNumber, request.getContinueExecution());
+ case PAUSE_THREAD:
+ return pauseThread(sequenceNumber, request.getPauseThread());
+ case EVALUATE:
+ return evaluate(sequenceNumber, request.getEvaluate());
+ case PAYLOAD_NOT_SET:
+ DebugEventHelper.error(sequenceNumber, "No request payload found");
+ }
+ return DebugEventHelper.error(
+ sequenceNumber, "Unhandled request type: " + request.getPayloadCase());
+ } catch (DebugRequestException e) {
+ return DebugEventHelper.error(sequenceNumber, e.getMessage());
+ }
+ }
+
+ /** Handles a {@code ListThreadsRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent listThreads(long sequenceNumber) {
+ return DebugEventHelper.listThreadsResponse(sequenceNumber, threadHandler.listThreads());
+ }
+
+ /** Handles a {@code ListFramesRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent listFrames(
+ long sequenceNumber, SkylarkDebuggingProtos.ListFramesRequest request)
+ throws DebugRequestException {
+ List<SkylarkDebuggingProtos.Frame> frames = threadHandler.listFrames(request.getThreadId());
+ return DebugEventHelper.listFramesResponse(sequenceNumber, frames);
+ }
+
+ /** Handles a {@code SetBreakpointsRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent setBreakpoints(
+ long sequenceNumber, SkylarkDebuggingProtos.SetBreakpointsRequest request) {
+ threadHandler.setBreakpoints(
+ request
+ .getBreakpointList()
+ .stream()
+ .filter(b -> b.getConditionCase() == ConditionCase.LOCATION)
+ .map(SkylarkDebuggingProtos.Breakpoint::getLocation)
+ .collect(toImmutableSet()));
+ return DebugEventHelper.setBreakpointsResponse(sequenceNumber);
+ }
+
+ /** Handles a {@code EvaluateRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent evaluate(
+ long sequenceNumber, SkylarkDebuggingProtos.EvaluateRequest request)
+ throws DebugRequestException {
+ return DebugEventHelper.evaluateResponse(
+ sequenceNumber, threadHandler.evaluate(request.getThreadId(), request.getExpression()));
+ }
+
+ /** Handles a {@code ContinueExecutionRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent continueExecution(
+ long sequenceNumber, SkylarkDebuggingProtos.ContinueExecutionRequest request)
+ throws DebugRequestException {
+ long threadId = request.getThreadId();
+ if (threadId == 0) {
+ threadHandler.resumeAllThreads();
+ return DebugEventHelper.continueExecutionResponse(sequenceNumber);
+ }
+ threadHandler.resumeThread(threadId, request.getStepping());
+ return DebugEventHelper.continueExecutionResponse(sequenceNumber);
+ }
+
+ private SkylarkDebuggingProtos.DebugEvent pauseThread(
+ long sequenceNumber, SkylarkDebuggingProtos.PauseThreadRequest request)
+ throws DebugRequestException {
+ long threadId = request.getThreadId();
+ if (threadId == 0) {
+ threadHandler.pauseAllThreads();
+ } else {
+ threadHandler.pauseThread(threadId);
+ }
+ return DebugEventHelper.pauseThreadResponse(sequenceNumber);
+ }
+
+ /** A subclass of {@link Eval} with debugging hooks. */
+ private final class DebugAwareEval extends Eval {
+
+ DebugAwareEval(Environment env) {
+ super(env);
+ }
+
+ @Override
+ public void exec(Statement st) throws EvalException, InterruptedException {
+ pauseIfNecessary(env, st.getLocation());
+ super.exec(st);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
new file mode 100644
index 0000000000..459ebae848
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
@@ -0,0 +1,334 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.syntax.Debuggable;
+import com.google.devtools.build.lib.syntax.Debuggable.ReadyToPause;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Handles all thread-related state and debugging tasks. */
+final class ThreadHandler {
+
+ private static class ThreadState {
+ final long id;
+ final String name;
+ final Debuggable debuggable;
+ /** Non-null if the thread is currently paused. */
+ @Nullable volatile PausedThreadState pausedState;
+ /** Determines when execution should next be paused. Non-null if currently stepping. */
+ @Nullable volatile ReadyToPause readyToPause;
+
+ ThreadState(
+ long id,
+ String name,
+ Debuggable debuggable,
+ @Nullable PausedThreadState pausedState,
+ @Nullable ReadyToPause readyToPause) {
+ this.id = id;
+ this.name = name;
+ this.debuggable = debuggable;
+ this.pausedState = pausedState;
+ this.readyToPause = readyToPause;
+ }
+ }
+
+ /** Information about a paused thread. */
+ private static class PausedThreadState {
+
+ /** The {@link Location} where execution is currently paused. */
+ final Location location;
+
+ /** Used to block execution of threads */
+ final Semaphore semaphore;
+
+ PausedThreadState(Location location) {
+ this.location = location;
+ this.semaphore = new Semaphore(0);
+ }
+ }
+
+ /**
+ * If true, all threads will pause at the earliest possible opportunity. New threads will also be
+ * immediately paused upon creation.
+ *
+ * <p>The debugger starts with all threads paused, until a StartDebuggingRequest is received.
+ */
+ private volatile boolean pausingAllThreads = true;
+
+ /** A map from thread identifiers to their state info. */
+ @GuardedBy("itself")
+ private final Map<Long, ThreadState> threads = new HashMap<>();
+
+ /** All location-based breakpoints (the only type of breakpoint currently supported). */
+ private volatile ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints = ImmutableSet.of();
+
+ /**
+ * Threads which are set to be paused in the next checked execution step.
+ *
+ * <p>Invariant: Every thread id in this set is also in {@link #threads}, provided that we are not
+ * in a synchronized block on that map.
+ */
+ private final Set<Long> threadsToPause = ConcurrentHashMap.newKeySet();
+
+ /** Registers a Skylark thread with the {@link ThreadHandler}. */
+ void registerThread(long threadId, String threadName, Debuggable debuggable) {
+ doRegisterThread(threadId, threadName, debuggable);
+ }
+
+ private ThreadState doRegisterThread(long threadId, String threadName, Debuggable debuggable) {
+ ThreadState thread = new ThreadState(threadId, threadName, debuggable, null, null);
+ synchronized (threads) {
+ threads.put(threadId, thread);
+ }
+ return thread;
+ }
+
+ /** Mark all current and future threads paused. Will take effect asynchronously. */
+ void pauseAllThreads() {
+ synchronized (threads) {
+ threadsToPause.addAll(threads.keySet());
+ }
+ pausingAllThreads = true;
+ }
+
+ /** Mark the given thread paused. Will take effect asynchronously. */
+ void pauseThread(long threadId) throws DebugRequestException {
+ synchronized (threads) {
+ if (!threads.containsKey(threadId)) {
+ throw new DebugRequestException("Unknown thread: " + threadId);
+ }
+ threadsToPause.add(threadId);
+ }
+ }
+
+ /** Called when Skylark execution for this thread is complete. */
+ void unregisterThread(long threadId) {
+ synchronized (threads) {
+ threads.remove(threadId);
+ threadsToPause.remove(threadId);
+ }
+ }
+
+ void setBreakpoints(ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints) {
+ // all breakpoints cover the entire line, so unset the column number.
+ this.breakpoints =
+ breakpoints
+ .stream()
+ .map(location -> location.toBuilder().clearColumnNumber().build())
+ .collect(toImmutableSet());
+ }
+
+ /** Resumes all threads. */
+ void resumeAllThreads() {
+ threadsToPause.clear();
+ pausingAllThreads = false;
+ synchronized (threads) {
+ for (ThreadState thread : threads.values()) {
+ // continue-all doesn't support stepping.
+ resumeThread(thread, SkylarkDebuggingProtos.Stepping.NONE);
+ }
+ }
+ }
+
+ /**
+ * Unpauses the given thread if it is currently paused. Also unsets {@link #pausingAllThreads}.
+ */
+ void resumeThread(long threadId, SkylarkDebuggingProtos.Stepping stepping)
+ throws DebugRequestException {
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ if (thread.pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ resumeThread(thread, stepping);
+ }
+ }
+
+ /**
+ * Unpauses the given thread if it is currently paused. Also unsets {@link #pausingAllThreads}.
+ */
+ @GuardedBy("threads")
+ private void resumeThread(ThreadState thread, SkylarkDebuggingProtos.Stepping stepping) {
+ PausedThreadState pausedState = thread.pausedState;
+ if (pausedState == null) {
+ return;
+ }
+ // once the user has resumed any thread, don't continue pausing future threads
+ pausingAllThreads = false;
+ thread.readyToPause =
+ thread.debuggable.stepControl(DebugEventHelper.convertSteppingEnum(stepping));
+ pausedState.semaphore.release();
+ thread.pausedState = null;
+ }
+
+ void pauseIfNecessary(Environment env, Location location, DebugServerTransport transport) {
+ if (shouldPauseCurrentThread(env, location)) {
+ pauseCurrentThread(env, location, transport);
+ }
+ }
+
+ ImmutableList<SkylarkDebuggingProtos.Thread> listThreads() {
+ ImmutableList.Builder<SkylarkDebuggingProtos.Thread> list = ImmutableList.builder();
+ synchronized (threads) {
+ for (ThreadState thread : threads.values()) {
+ list.add(getThreadProto(thread));
+ }
+ }
+ return list.build();
+ }
+
+ /** Handles a {@code ListFramesRequest} and returns its response. */
+ ImmutableList<SkylarkDebuggingProtos.Frame> listFrames(long threadId)
+ throws DebugRequestException {
+ Debuggable debuggable;
+ PausedThreadState pausedState;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ pausedState = thread.pausedState;
+ if (pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ debuggable = thread.debuggable;
+ }
+ // no need to list frames within the synchronize block: threads can only be resumed in response
+ // to a client request, and requests are handled serially
+ return debuggable
+ .listFrames(pausedState.location)
+ .stream()
+ .map(DebugEventHelper::getFrameProto)
+ .collect(toImmutableList());
+ }
+
+ SkylarkDebuggingProtos.Value evaluate(long threadId, String expression)
+ throws DebugRequestException {
+ Debuggable debuggable;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ if (thread.pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ debuggable = thread.debuggable;
+ }
+ // no need to evaluate within the synchronize block: threads can only be resumed in response
+ // to a client request, and requests are handled serially
+ try {
+ Object result = debuggable.evaluate(expression);
+ return DebuggerSerialization.getValueProto("Evaluation result", result);
+ } catch (EvalException | InterruptedException e) {
+ throw new DebugRequestException(e.getMessage());
+ }
+ }
+
+ /**
+ * Pauses the current thread's execution, blocking until it's resumed via a
+ * ContinueExecutionRequest.
+ */
+ private void pauseCurrentThread(
+ Environment env, Location location, DebugServerTransport transport) {
+ long threadId = Thread.currentThread().getId();
+
+ SkylarkDebuggingProtos.Thread threadProto;
+ PausedThreadState pausedState;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ // this skylark entry point didn't call DebugServer#runWithDebugging. Now that we've hit a
+ // breakpoint, register it anyway.
+ // TODO(bazel-team): once all skylark evaluation routes through
+ // DebugServer#runWithDebugging, report an error here instead
+ String fallbackThreadName = "Untracked thread: " + threadId;
+ transport.postEvent(DebugEventHelper.threadStartedEvent(threadId, fallbackThreadName));
+ thread = doRegisterThread(threadId, fallbackThreadName, env);
+ }
+ threadProto = getThreadProto(thread);
+ pausedState = new PausedThreadState(location);
+ thread.pausedState = pausedState;
+ }
+
+ transport.postEvent(DebugEventHelper.threadPausedEvent(threadProto));
+ pausedState.semaphore.acquireUninterruptibly();
+ transport.postEvent(DebugEventHelper.threadContinuedEvent(threadProto));
+ }
+
+ private boolean shouldPauseCurrentThread(Environment env, Location location) {
+ long threadId = Thread.currentThread().getId();
+ if (threadsToPause.remove(threadId) || pausingAllThreads) {
+ return true;
+ }
+ if (hasBreakpointAtLocation(location)) {
+ return true;
+ }
+
+ // TODO(bazel-team): if contention becomes a problem, consider changing 'threads' to a
+ // concurrent map, and synchronizing on individual entries
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread != null && thread.readyToPause != null && thread.readyToPause.test(env)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasBreakpointAtLocation(Location location) {
+ // breakpoints is volatile, so taking a local copy
+ ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints = this.breakpoints;
+ if (breakpoints.isEmpty()) {
+ return false;
+ }
+ SkylarkDebuggingProtos.Location locationProto = DebugEventHelper.getLocationProto(location);
+ // column data ignored for breakpoints
+ return locationProto != null
+ && breakpoints.contains(locationProto.toBuilder().clearColumnNumber().build());
+ }
+
+ /** Returns a {@code Thread} proto builder with information about the given thread. */
+ private static SkylarkDebuggingProtos.Thread getThreadProto(ThreadState thread) {
+ SkylarkDebuggingProtos.Thread.Builder builder =
+ SkylarkDebuggingProtos.Thread.newBuilder().setId(thread.id).setName(thread.name);
+
+ PausedThreadState pausedState = thread.pausedState;
+ if (pausedState != null) {
+ builder
+ .setIsPaused(true)
+ .setLocation(DebugEventHelper.getLocationProto(pausedState.location));
+ }
+ return builder.build();
+ }
+}