blob: 800dffe8eeda11618497f4b0f62bd5f9f8007edc [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_DEVELOPER_DEBUG_ZXDB_CLIENT_THREAD_CONTROLLER_H_
#define SRC_DEVELOPER_DEBUG_ZXDB_CLIENT_THREAD_CONTROLLER_H_
#include <vector>
#include "lib/fit/function.h"
#include "src/developer/debug/ipc/protocol.h"
#include "src/developer/debug/zxdb/client/frame_fingerprint.h"
#include "src/developer/debug/zxdb/common/address_range.h"
#include "src/lib/fxl/macros.h"
#include "src/lib/fxl/memory/weak_ptr.h"
namespace zxdb {
class Breakpoint;
class Err;
class Frame;
class Location;
class Thread;
// Abstract base class that provides the policy decisions for various types of thread stepping.
//
// HOW THREAD CONTROLLERS WORK
// ---------------------------
// Thread controllers are responsible for implementing the various complex step operations that
// are more complex that run/stop/single-step-instruction. They are composable ("next" is just a
// sequence of "step into"/"step out" operations until a new line is reached) and there can be
// multiple active ones (if a breakpoint is hit in a stack frame being stepped over, the stepping
// can continue after the breakpoint is resumed from).
//
// Once installed, the thread will ask the topmost thread controller how (and whether) to continue
// via OnThreadStop(). This function is given the exception and breakpoint information regarding the
// stop. The thread controllers installed on a thread will get notified for each exception and
// indicate whether they want to handle the stop or continue. Each thread controller is queried for
// each stop since completions could happen in the in any order.
//
// The thread may also delete thread controllers. This can happen when the thread is terminated or
// when there is an internal error stepping. If a controller has a callback it executes on
// completion it should be prepared to issue the callback from its destructor in such a way to
// indicate that the step operation failed.
//
// "NONE" EXCEPTION TYPES
// ----------------------
// The special exception type "kNone" should cause a thread controller to evaluate the current
// state of the thread without making assumptions about the exact exception type. This is most
// commonly used when a controller makes a child controller to perform some operation and wants to
// immediately ask if it the thread should stop now. The current exception might be a breakpoint or
// something that the parent controller set up that the child controller might otherwise ignore.
//
// ASYNC COMPLETION
// ----------------
// Some thread controller need to perform async operations from OnThreadStop(). In this case they
// can return StopOp::kFuture. The thread will interpret this to mean leave the thread stopped but
// not to issue notifications that it has done so. The thread controller is responsible for calling
// Thread::ResumeFromAsyncThreadController() once its operation has completed.
//
// Thread::ResumeFromAsyncThreadController() doesn't continue the thread (since the async operation
// may want to report "stop"). Instead, it re-issues the same stop and the controllers should then
// re-evaluate their location and issue a real stop or continue. See also
// ThreadController::MakeResumeAsyncThreadCallback() for some extra complications.
//
// There is also some opportunity for asynchronous work via the Thread's AddPostStopTask() function.
// This can inject asynchronous work after the thread controllers run but before the stop or
// continue is processed.
class ThreadController {
public:
enum StopOp {
// Resume the thread. A controller can indicate "continue" but if another indicates "stop", the
// "stop" will take precedence.
kContinue,
// Keeps the thread stopped and reports the stop to the user. The controller is marked done and
// should be deleted. This takes precedence over any "continue" votes.
kStopDone,
// Reports that the controller doesn't know what to do with this thread stop. This is
// effectively a neutral vote for what should happen in response to a thread stop. If all active
// controllers report "unexpected", the thread will stop.
kUnexpected,
// Reports that the controller is performing asynchronous work and will re-fire this stop in the
// future by calling ResumeFromAsyncThreadController(). This value takes precedence over other
// ones and if any thread controllers issue this stop, execution of thread controllers will be
// suspended.
kFuture,
};
// How the thread should run when it is executing this controller.
struct ContinueOp {
// Factory helper functions.
static ContinueOp Continue() {
return ContinueOp(); // Defaults are good for this case.
}
static ContinueOp StepInstruction() {
ContinueOp result;
result.how = debug_ipc::ResumeRequest::How::kStepInstruction;
return result;
}
static ContinueOp StepInRange(AddressRange range) {
ContinueOp result;
result.how = debug_ipc::ResumeRequest::How::kStepInRange;
result.range = range;
return result;
}
// See synthetic_stop_ below.
static ContinueOp SyntheticStop() {
ContinueOp result;
result.synthetic_stop_ = true;
return result;
}
// A synthetic stop means that the thread remains stopped but a synthetic stop notification is
// broadcast to make it look like the thread continued and stopped again. This will call back
// into the top controller's OnThreadStop().
//
// This is useful when modifying the stack for inline routines, where the code didn't execute
// but from a user perspective they stepped into an inline subroutine. In this case the thread
// controller will update the Stack to reflect the new state, and return
// ContinueOp::SyntheticStop().
//
// Why isn't this a StopOp instead? This only makes sense as the initial state of the
// ThreadController that decides it doesn't need to do anything but wants to pretend that it
// did. When a ThreadController is in OnThreadStop and about to return a StopOp, returning kStop
// is a real thread stop and nothing needs to be synthetic.
//
// See GetContinueOp() for more.
bool synthetic_stop_ = false;
// Valid when synthetic_stop = false.
debug_ipc::ResumeRequest::How how = debug_ipc::ResumeRequest::How::kResolveAndContinue;
// When how == kStepInRange, this defines the address range to step in. As long as the
// instruction pointer is inside, execution will continue.
AddressRange range;
};
// See MakeResumeAsyncThreadCallback().
struct ResumeAsyncCallbackInfo {
ResumeAsyncCallbackInfo(fxl::WeakPtr<Thread> thread, debug_ipc::ExceptionType exception_type);
~ResumeAsyncCallbackInfo();
debug_ipc::ExceptionType exception_type;
// Shared between the callback and this struct to coordinate what's happening.
std::shared_ptr<bool> called; // Indicates the callback has been issued.
std::shared_ptr<bool> is_sync; // Indicates the callback should run in a synchronous context.
fit::callback<void(const Err&)> callback;
// If the callback has completed, calls controller->OnStop() with the given arguments. If it
// has not, returns kFuture.
StopOp ForwardStopOrReturnFuture(ThreadController* controller,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints);
};
ThreadController();
virtual ~ThreadController();
// Registers the thread with the controller. The controller will be owned by the thread (possibly
// indirectly) so the pointer will remain valid for the rest of the lifetime of the controller.
//
// The implementation should call SetThread() with the thread.
//
// When the implementation is ready, it will issue the given callback to run the thread. The
// callback can be issued reentrantly from inside this function if the controller is ready
// or fails synchronously.
//
// If the callback does not specify an error, the thread will be resumed when it is called. If the
// callback has an error, it will be reported and the thread will remain stopped.
virtual void InitWithThread(Thread* thread, fit::callback<void(const Err&)> cb) = 0;
// Returns how to continue the thread when running this controller. This will be called after
// InitWithThread and after every subsequent kContinue response from OnThreadStop to see how the
// controller wishes to run.
//
// A thread controller can return a "synthetic stop" from this function which will schedule an
// OnThreadStop() call in the future without running the thread. This can be used to adjust the
// ambiguous inline stack state (see Stack object) to implement step commands.
//
// GetContinueOp() should not change thread state and controllers should be prepared for only
// InitWithThread() followe by OnThreadStop() calls. When thread controllers embed other thread
// controllers, the embedding controller may create the nested one and want it to evaluate the
// current stop, and this happens without ever continuing.
virtual ContinueOp GetContinueOp() = 0;
// Notification that the thread has stopped. The return value indicates what the thread should do
// in response.
//
// At this call, the stop location will be thread().GetStack()[0]. Thread controllers will only
// be called when there is a valid location for the stop, so there is guaranteed to be at least
// one stack entry (in constrast to general thread exception observers).
//
// ARGUMENTS
// ---------
// The exception type may be "kNone" if the exception type shouldn't matter to this controller.
// Controllers should treak "kNone" as being relevant to themselves. When a controller is used as
// a component of another controller, the exception type may have been "consumed" and a nested
// controller merely needs to evaluate its opinion of the current location.
//
// The stop type and breakpoint information should be passed to the first thread controller that
// handles the stop (this might be a sub controller if a controller is delegating the current
// execution to another one). Other controllers that might handle the stop (say, if a second
// sub-controller is created when the first one is done) don't care and might get confused by stop
// information originally handled by another one. In this second case, "kNone" and an empty
// breakpoint list should be sent to OnThreadStop().
//
// RETURN VALUE
// ------------
// If the ThreadController returns |kStop|, its assumed the controller has completed its job and
// it will be deleted. |kContinue| doesn't necessarily mean the thread will continue, as there
// could be multiple controllers active and any of them can report "stop". When a thread is being
// continued, the main controller will get GetContinueOp() called to see what type of continuation
// it wants.
virtual StopOp OnThreadStop(debug_ipc::ExceptionType stop_type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) = 0;
// Writes the log message prefixed with the thread controller type. Callers should pass constant
// strings through here so the Log function takes almost no time if it's disabled: in the future
// we may want to make this run-time enable-able
void Log(const char* format, ...) const;
// Returns the given frame's function name or a placeholder string if unavailable. Does nothing if
// logging is disabled (computing this is non-trivial).
static std::string FrameFunctionNameForLog(const Frame* frame);
protected:
// How the frame argument to SetInlineFrameIfAmbiguous() is interpreted.
enum class InlineFrameIs {
// Set the inline frame equal to the given one.
kEqual,
// Set the inline frame to the frame immediately before the given one. This exists so that
// calling code can reference the previuos frame without actually having to compute the
// fingerprint of the previous frame (it may not be available if previous stack frames haven't
// been synced).
kOneBefore
};
Thread* thread() { return thread_; }
void SetThread(Thread* thread);
// Returns the name of this thread controller. This will be visible in logs. This should be
// something simple and short like "Step" or "Step Over".
virtual const char* GetName() const = 0;
// The beginning of an inline function is ambiguous about whether you're at the beginning of the
// function or about to call it (see Stack object for more).
//
// Many stepping functions know what frame they think they should be in, and identify this based
// on the frame fingerprint. As a concrete example, if a "finish" command exits a stack frame, but
// the next instruction is the beginning of an inlined function, the "finish" controller would
// like to say you're in the stack it returned to, not the inlined function.
//
// This function checks if there is ambiguity of inline frames and whether one of those ambiguous
// frames matches the given fingerprint. In this case, it will set the top stack frame to be the
// requested one.
//
// If there is no ambiguity or one of the possibly ambiguous frames doesn't match the given
// fingerprint, the inline frame hide count will be unchanged.
void SetInlineFrameIfAmbiguous(InlineFrameIs comparison, FrameFingerprint fingerprint);
// Tells the owner of this class that this ThreadController has completed its work. Normally
// returning kStop from OnThreadStop() will do this, but if the controller has another way to get
// events (like breakpoints), it may notice out-of-band that its work is done.
//
// This function will likely cause |this| to be deleted.
void NotifyControllerDone();
// Makes a callback that calls the current thread ResumeFromAsyncThreadController() function to
// resume from a previous "kFuture" stop operation. This is a convenience function to deal with
// some delicacies including weak Thread pointers and the sync/async issue described below.
//
// The Err parameter to the callback is ignored, we use this function type to match the callback
// to InitWithThread().
//
// The type parameter is passed to ResumeFromAsyncThreadController().
//
// SYNC/ASYNC ISSUES
// -----------------
// The normal sync pattern is to make a new ThreadController, call its InitWithThread(), and
// then send it a "kNone" exception if you need to ask it about the current location. The problem
// is the callback may:
//
// - Complete synchronously from within InitWithThread(), in which case you want to immediately
// do e.g. "return controller->OnThreadStop(kNone)") and do nothing from the callback.
//
// - Complete asynchronously in which case you want to return kFuture to the Thread and have the
// callback issue ResumeFromAsyncThreadController() when it completes.
//
// This function makes a ResumeAsyncCallbackInfo which implements this behavior. The callback will
// only call ResumeFromAsyncThreadController() if it is issued after the ResumeAsyncCallbackInfo
// is destructed.
//
// Typical use:
//
// sub_controller_ = std::make_unique<MyController>();
// ResumeAsyncCallbackInfo resume_info =
// MakeResumeAsyncThreadCallback(debug_ipc::ExceptionType::kNone);
// sub_controller_->InitWithThread(std::move(resume_info.callback));
//
// return resume_info.ForwardStopOrReturnFuture(sub_controller_.get(), hit_bps);
ResumeAsyncCallbackInfo MakeResumeAsyncThreadCallback(debug_ipc::ExceptionType type) const;
// Returns true if this controller has debug logging enabled. This is only valid after the thread
// has been set.
bool enable_debug_logging() const { return enable_debug_logging_; }
private:
Thread* thread_ = nullptr;
// Initialized from the setting when the thread is known.
bool enable_debug_logging_ = false;
FXL_DISALLOW_COPY_AND_ASSIGN(ThreadController);
};
} // namespace zxdb
#endif // SRC_DEVELOPER_DEBUG_ZXDB_CLIENT_THREAD_CONTROLLER_H_