| // 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.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.syntax.Debugger; |
| import com.google.devtools.build.lib.syntax.StarlarkThread; |
| import java.io.IOException; |
| import java.net.ServerSocket; |
| import java.util.List; |
| import javax.annotation.Nullable; |
| |
| /** Manages the network socket and debugging state for threads running Skylark code. */ |
| public final class SkylarkDebugServer implements Debugger { |
| |
| /** |
| * 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 |
| * @param verboseLogging if true, debug-level events will be logged |
| * @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, boolean verboseLogging) throws IOException { |
| ServerSocket serverSocket = new ServerSocket(port, /* backlog */ 1); |
| return createAndWaitForConnection(eventHandler, serverSocket, verboseLogging); |
| } |
| |
| /** |
| * Initializes debugging support, setting up any debugging-specific overrides, then opens the |
| * debug server socket and blocks waiting for an incoming connection. |
| * |
| * @param verboseLogging if true, debug-level events will be logged |
| * @throws IOException if an I/O error occurs while waiting for a connection |
| */ |
| @VisibleForTesting |
| static SkylarkDebugServer createAndWaitForConnection( |
| EventHandler eventHandler, ServerSocket serverSocket, boolean verboseLogging) |
| throws IOException { |
| DebugServerTransport transport = |
| DebugServerTransport.createAndWaitForClient(eventHandler, serverSocket, verboseLogging); |
| return new SkylarkDebugServer(eventHandler, transport, verboseLogging); |
| } |
| |
| 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 final boolean verboseLogging; |
| |
| private SkylarkDebugServer( |
| EventHandler eventHandler, DebugServerTransport transport, boolean verboseLogging) { |
| this.eventHandler = eventHandler; |
| this.threadHandler = new ThreadHandler(); |
| this.transport = transport; |
| this.verboseLogging = verboseLogging; |
| 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))); |
| } |
| } finally { |
| eventHandler.handle( |
| Event.info( |
| "Debug server listener thread closed; shutting down debug server and " |
| + "resuming all threads")); |
| close(); |
| } |
| }); |
| |
| clientThread.setDaemon(true); |
| clientThread.start(); |
| } |
| |
| @Override |
| public void close() { |
| try { |
| if (verboseLogging) { |
| eventHandler.handle(Event.debug("Closing debug server")); |
| } |
| transport.close(); |
| |
| } catch (IOException e) { |
| eventHandler.handle( |
| Event.error( |
| "Error shutting down the debug server: " + Throwables.getStackTraceAsString(e))); |
| } finally { |
| // ensure no threads are left paused, otherwise the build command will never complete |
| threadHandler.resumeAllThreads(); |
| } |
| } |
| |
| /** |
| * Called by the interpreter before execution of the code at the specified location. 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 |
| */ |
| @Override |
| public void before(StarlarkThread thread, Location location) { |
| if (!transport.isClosed()) { |
| threadHandler.pauseIfNecessary(thread, 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_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 GET_CHILDREN: |
| return getChildren(sequenceNumber, request.getGetChildren()); |
| 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 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()); |
| 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.getStatement())); |
| } |
| |
| /** Handles a {@code GetChildrenRequest} and returns its response. */ |
| private SkylarkDebuggingProtos.DebugEvent getChildren( |
| long sequenceNumber, SkylarkDebuggingProtos.GetChildrenRequest request) |
| throws DebugRequestException { |
| return DebugEventHelper.getChildrenResponse( |
| sequenceNumber, |
| threadHandler.getChildrenForValue(request.getThreadId(), request.getValueId())); |
| } |
| |
| /** 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); |
| } |
| } |