diff options
author | Googler <noreply@google.com> | 2018-06-07 14:07:17 -0700 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2018-06-07 14:08:44 -0700 |
commit | 5e893626640351de0f12e36bb14d80af0ff1e036 (patch) | |
tree | b9a7083618290b867bd40383881a06e89060ce27 /src/main/java/com/google/devtools/build/lib/skylarkdebug | |
parent | 755278df00f65818dc092fe4f8a31bdec1aaaab5 (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')
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(); + } +} |