com.google.devtools.build.lib.skylarkdebug.server.ThreadHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.skylarkdebug.server.ThreadHandler.java

Source

// 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 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.Breakpoint;
import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Error;
import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.PauseReason;
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 com.google.devtools.build.lib.syntax.EvalUtils;
import java.util.Collection;
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 {

    /** The state of a thread that is paused. */
    private static class PausedThreadState {
        final long id;
        final String name;
        final Debuggable debuggable;
        /** The {@link Location} where execution is currently paused. */
        final Location location;
        /** Used to block execution of threads */
        final Semaphore semaphore;

        PausedThreadState(long id, String name, Debuggable debuggable, Location location) {
            this.id = id;
            this.name = name;
            this.debuggable = debuggable;
            this.location = location;
            this.semaphore = new Semaphore(0);
        }
    }

    /**
     * The state of a thread that is stepping, i.e. currently running but expected to stop at a
     * subsequent statement even without a breakpoint. This may include threads that have completed
     * running while stepping, since the ThreadHandler doesn't know when a thread terminates.
     */
    private static class SteppingThreadState {
        /** Determines when execution should next be paused. */
        final ReadyToPause readyToPause;

        SteppingThreadState(ReadyToPause readyToPause) {
            this.readyToPause = readyToPause;
        }
    }

    /**
     * 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 identifiers of paused threads to their state info. */
    @GuardedBy("this")
    private final Map<Long, PausedThreadState> pausedThreads = new HashMap<>();

    /** A map from identifiers of stepping threads to their state. */
    @GuardedBy("this")
    private final Map<Long, SteppingThreadState> steppingThreads = new HashMap<>();

    /** All location-based breakpoints (the only type of breakpoint currently supported). */
    private volatile ImmutableMap<SkylarkDebuggingProtos.Location, SkylarkDebuggingProtos.Breakpoint> breakpoints = ImmutableMap
            .of();

    /**
     * True if the thread is currently performing a debugger-requested evaluation. If so, we don't
     * check for breakpoints during the evaluation.
     */
    private final ThreadLocal<Boolean> servicingEvalRequest = ThreadLocal.withInitial(() -> false);

    /**
     * Threads which are not paused now, but that are set to be paused in the next checked execution
     * step as the result of a PauseThreadRequest.
     *
     * <p>Invariant: Every thread id in this set is also in {@link #steppingThreads}, provided that we
     * are not in a synchronized block on the class instance.
     */
    private final Set<Long> threadsToPause = ConcurrentHashMap.newKeySet();

    /** Mark all current and future threads paused. Will take effect asynchronously. */
    void pauseAllThreads() {
        pausingAllThreads = true;
    }

    /** Mark the given thread paused. Will take effect asynchronously. */
    void pauseThread(long threadId) throws DebugRequestException {
        synchronized (this) {
            if (!steppingThreads.containsKey(threadId)) {
                String error = pausedThreads.containsKey(threadId) ? "Thread is already paused"
                        : "Unknown thread: only threads which are currently stepping can be paused";
                throw new DebugRequestException(error);
            }
            threadsToPause.add(threadId);
        }
    }

    void setBreakpoints(Collection<Breakpoint> breakpoints) {
        Map<SkylarkDebuggingProtos.Location, SkylarkDebuggingProtos.Breakpoint> map = new HashMap<>();
        for (SkylarkDebuggingProtos.Breakpoint breakpoint : breakpoints) {
            if (breakpoint.getConditionCase() != SkylarkDebuggingProtos.Breakpoint.ConditionCase.LOCATION) {
                continue;
            }
            // all breakpoints cover the entire line, so unset the column number
            SkylarkDebuggingProtos.Location location = breakpoint.getLocation().toBuilder().clearColumnNumber()
                    .build();
            map.put(location, breakpoint);
        }
        this.breakpoints = ImmutableMap.copyOf(map);
    }

    /**
     * Resumes all threads. Any currently stepping threads have their stepping behavior cleared, so
     * will run unconditionally.
     */
    void resumeAllThreads() {
        threadsToPause.clear();
        pausingAllThreads = false;
        synchronized (this) {
            for (PausedThreadState thread : ImmutableList.copyOf(pausedThreads.values())) {
                // continue-all doesn't support stepping.
                resumePausedThread(thread, SkylarkDebuggingProtos.Stepping.NONE);
            }
            steppingThreads.clear();
        }
    }

    /**
     * Unpauses the given thread if it is currently paused. Also unsets {@link #pausingAllThreads}. If
     * the thread is not paused, but currently stepping, it clears the stepping behavior so it will
     * run unconditionally.
     */
    void resumeThread(long threadId, SkylarkDebuggingProtos.Stepping stepping) throws DebugRequestException {
        // once the user has requested any thread be resumed, don't continue pausing future threads
        pausingAllThreads = false;
        synchronized (this) {
            threadsToPause.remove(threadId);
            if (steppingThreads.remove(threadId) != null) {
                return;
            }
            PausedThreadState thread = pausedThreads.get(threadId);
            if (thread == null) {
                throw new DebugRequestException(String.format("Unknown thread %s: cannot resume.", threadId));
            }
            resumePausedThread(thread, stepping);
        }
    }

    /** Unpauses a currently-paused thread. */
    @GuardedBy("this")
    private void resumePausedThread(PausedThreadState thread, SkylarkDebuggingProtos.Stepping stepping) {
        pausedThreads.remove(thread.id);
        ReadyToPause readyToPause = thread.debuggable.stepControl(DebugEventHelper.convertSteppingEnum(stepping));
        if (readyToPause != null) {
            steppingThreads.put(thread.id, new SteppingThreadState(readyToPause));
        }
        thread.semaphore.release();
    }

    void pauseIfNecessary(Environment env, Location location, DebugServerTransport transport) {
        if (servicingEvalRequest.get()) {
            return;
        }
        PauseReason pauseReason;
        Error error = null;
        try {
            pauseReason = shouldPauseCurrentThread(env, location);
        } catch (ConditionalBreakpointException e) {
            pauseReason = PauseReason.CONDITIONAL_BREAKPOINT_ERROR;
            error = Error.newBuilder().setMessage(e.getMessage()).build();
        }
        if (pauseReason == null) {
            return;
        }
        long threadId = Thread.currentThread().getId();
        threadsToPause.remove(threadId);
        synchronized (this) {
            steppingThreads.remove(threadId);
        }
        pauseCurrentThread(env, location, transport, pauseReason, error);
    }

    /** Handles a {@code ListFramesRequest} and returns its response. */
    ImmutableList<SkylarkDebuggingProtos.Frame> listFrames(long threadId) throws DebugRequestException {
        synchronized (this) {
            PausedThreadState thread = pausedThreads.get(threadId);
            if (thread == null) {
                throw new DebugRequestException(
                        String.format("Thread %s is not paused or does not exist.", threadId));
            }
            return thread.debuggable.listFrames(thread.location).stream().map(DebugEventHelper::getFrameProto)
                    .collect(toImmutableList());
        }
    }

    SkylarkDebuggingProtos.Value evaluate(long threadId, String statement) throws DebugRequestException {
        Debuggable debuggable;
        synchronized (this) {
            PausedThreadState thread = pausedThreads.get(threadId);
            if (thread == null) {
                throw new DebugRequestException(
                        String.format("Thread %s is not paused or does not exist.", threadId));
            }
            debuggable = thread.debuggable;
        }
        // no need to evaluate within the synchronize block: for paused threads, debuggable is only
        // accessed in response to a client request, and requests are handled serially
        // TODO(bazel-team): support asynchronous replies, and use finer-grained locks
        try {
            Object result = doEvaluate(debuggable, statement);
            return DebuggerSerialization.getValueProto("Evaluation result", result);
        } catch (EvalException | InterruptedException e) {
            throw new DebugRequestException(e.getMessage());
        }
    }

    /**
     * Evaluate the given expression in the environment defined by the provided {@link Debuggable}.
     *
     * <p>The caller is responsible for ensuring that the associated skylark thread isn't currently
     * running.
     */
    private Object doEvaluate(Debuggable debuggable, String expression) throws EvalException, InterruptedException {
        try {
            servicingEvalRequest.set(true);
            return debuggable.evaluate(expression);
        } finally {
            servicingEvalRequest.set(false);
        }
    }

    /**
     * Pauses the current thread's execution, blocking until it's resumed via a
     * ContinueExecutionRequest.
     */
    private void pauseCurrentThread(Environment env, Location location, DebugServerTransport transport,
            PauseReason pauseReason, @Nullable Error conditionalBreakpointError) {
        long threadId = Thread.currentThread().getId();

        PausedThreadState pausedState = new PausedThreadState(threadId, Thread.currentThread().getName(), env,
                location);
        synchronized (this) {
            pausedThreads.put(threadId, pausedState);
        }
        SkylarkDebuggingProtos.PausedThread threadProto = getPausedThreadProto(pausedState, pauseReason,
                conditionalBreakpointError);
        transport.postEvent(DebugEventHelper.threadPausedEvent(threadProto));
        pausedState.semaphore.acquireUninterruptibly();
        transport.postEvent(DebugEventHelper.threadContinuedEvent(threadId));
    }

    @Nullable
    private PauseReason shouldPauseCurrentThread(Environment env, Location location)
            throws ConditionalBreakpointException {
        long threadId = Thread.currentThread().getId();
        if (pausingAllThreads) {
            return PauseReason.ALL_THREADS_PAUSED;
        }
        if (threadsToPause.contains(threadId)) {
            return PauseReason.PAUSE_THREAD_REQUEST;
        }
        if (hasBreakpointMatchedAtLocation(env, location)) {
            return PauseReason.HIT_BREAKPOINT;
        }

        // TODO(bazel-team): if contention becomes a problem, consider changing 'threads' to a
        // concurrent map, and synchronizing on individual entries
        synchronized (this) {
            SteppingThreadState steppingState = steppingThreads.get(threadId);
            if (steppingState != null && steppingState.readyToPause.test(env)) {
                return PauseReason.STEPPING;
            }
        }
        return null;
    }

    /**
     * Returns true if there's a breakpoint at the current location, with a satisfied condition if
     * relevant.
     */
    private boolean hasBreakpointMatchedAtLocation(Environment env, Location location)
            throws ConditionalBreakpointException {
        // breakpoints is volatile, so taking a local copy
        ImmutableMap<SkylarkDebuggingProtos.Location, SkylarkDebuggingProtos.Breakpoint> breakpoints = this.breakpoints;
        if (breakpoints.isEmpty()) {
            return false;
        }
        SkylarkDebuggingProtos.Location locationProto = DebugEventHelper.getLocationProto(location);
        if (locationProto == null) {
            return false;
        }
        locationProto = locationProto.toBuilder().clearColumnNumber().build();
        SkylarkDebuggingProtos.Breakpoint breakpoint = breakpoints.get(locationProto);
        if (breakpoint == null) {
            return false;
        }
        String condition = breakpoint.getExpression();
        if (condition.isEmpty()) {
            return true;
        }
        try {
            return EvalUtils.toBoolean(doEvaluate(env, condition));
        } catch (EvalException | InterruptedException e) {
            throw new ConditionalBreakpointException(e.getMessage());
        }
    }

    /** Returns a {@code Thread} proto builder with information about the given thread. */
    private static SkylarkDebuggingProtos.PausedThread getPausedThreadProto(PausedThreadState thread,
            PauseReason pauseReason, @Nullable Error conditionalBreakpointError) {
        SkylarkDebuggingProtos.PausedThread.Builder builder = SkylarkDebuggingProtos.PausedThread.newBuilder()
                .setId(thread.id).setName(thread.name).setPauseReason(pauseReason)
                .setLocation(DebugEventHelper.getLocationProto(thread.location));
        if (conditionalBreakpointError != null) {
            builder.setConditionalBreakpointError(conditionalBreakpointError);
        }
        return builder.build();
    }
}