Arquivos
hhvm/hphp/runtime/debugger/debugger_proxy.cpp
T
Herman Venter 7c420a7524 Sending the breakpoint list from the client to the proxy clobbers the data structures that track which breakpoints are temporarily disabled
Whenever the client sends a command to the proxy that includes a list of breakpoints, the proxy clobbers its list of breakpoints with the new list received from the client. Unfortunately it also uses its list of breakpoints to track which breakpoints are temporarily disabled because the debugger is currently stopped there. As a result, just listing the active breakpoints while stopped at a break point will cause the next continue command to hit the current break point if more byte codes have yet to be processed.

Reviewed By: @mikemag

Differential Revision: D962443
2013-09-11 11:39:48 -07:00

854 linhas
32 KiB
C++

/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/debugger/debugger_proxy.h"
#include "hphp/runtime/debugger/cmd/cmd_interrupt.h"
#include "hphp/runtime/debugger/cmd/cmd_flow_control.h"
#include "hphp/runtime/debugger/cmd/cmd_signal.h"
#include "hphp/runtime/debugger/cmd/cmd_machine.h"
#include "hphp/runtime/debugger/debugger.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/ext/ext_socket.h"
#include "hphp/runtime/vm/debugger-hook.h"
#include "hphp/util/process.h"
#include "hphp/util/logger.h"
namespace HPHP { namespace Eval {
///////////////////////////////////////////////////////////////////////////////
TRACE_SET_MOD(debugger);
DebuggerProxy::DebuggerProxy(SmartPtr<Socket> socket, bool local)
: m_stopped(false), m_local(local), m_dummySandbox(nullptr),
m_hasBreakPoints(false), m_threadMode(Normal), m_thread(0),
m_signalThread(this, &DebuggerProxy::pollSignal),
m_okayToPoll(true), m_signum(CmdSignal::SignalNone) {
TRACE(2, "DebuggerProxy::DebuggerProxy\n");
m_thrift.create(socket);
m_dummyInfo = DSandboxInfo::CreateDummyInfo((int64_t)this);
Variant address;
Variant port;
std::string clientDetails;
if (!local) {
if (getClientConnectionInfo(ref(address), ref(port))) {
clientDetails = folly::stringPrintf("From %s:%d",
address.toString().data(),
port.toInt32());
} else {
clientDetails = "Failed to get client connection details";
}
} else {
clientDetails = "Local script connection";
}
Debugger::UsageLog("server", m_dummyInfo.id(), "connect", clientDetails);
}
// Cleanup all resources owned by this proxy, including any threads it
// owns. If a thread doesn't stop, return false so we can try again
// later. This can be called multiple times, and this function leaves
// the proxy usable by request threads which may still be handling an
// interrupt.
bool DebuggerProxy::cleanup(int timeout) {
TRACE_RB(2, "DebuggerProxy::cleanup starting\n");
// If we're not already marked as stopping then there may be other
// threads still attempting to use this object!
assert(m_stopped);
// No more client operation is possible, so drop the connection.
m_thrift.close();
TRACE(2, "Stopping signal thread...\n");
if (!m_signalThread.waitForEnd(timeout)) return false;
TRACE(2, "Stopping dummy sandbox...\n");
if (m_dummySandbox) {
if (!m_dummySandbox->stop(timeout)) return false;
delete m_dummySandbox;
m_dummySandbox = nullptr;
}
TRACE_RB(2, "DebuggerProxy::cleanup complete\n");
return true;
}
// Stop the proxy and ensure that no new uses of it can
// occur. Schedule it for final cleanup on another thread. This may be
// called from any thread wishing to stop the proxy, for any reason.
void DebuggerProxy::stop() {
// Shared ref to keep us alive. Often the only reference to a proxy
// is in the debugger's proxy map, which we're about to alter below.
auto this_ = shared_from_this();
Debugger::UsageLog("server", getSandboxId(), "disconnect");
// After this we'll no longer be able to get this_ from
// Debugger::findProxy(), which means no new interrupts for this
// proxy. There may still be threads in existing interrupts, though.
Debugger::RemoveProxy(this_);
// Pop the signal polling thread, and any interrupting threads, out
// of their event loops and cause them to exit.
DSandboxInfo invalid;
{
Lock lock(this);
m_sandbox = invalid;
m_stopped = true;
}
// Retire the proxy, which will schedule it for cleanup and
// destruction on another thread. Threads for the dummy sandbox and
// signal polling can't destroy the proxy directly, since the proxy
// owns them and will want to destroy them.
Debugger::RetireProxy(this_);
}
// Stop the proxy, and stop execution of the current request.
void DebuggerProxy::stopAndThrow() {
stop();
throw DebuggerClientExitException();
}
const char *DebuggerProxy::getThreadType() const {
TRACE(2, "DebuggerProxy::getThreadType\n");
return isLocal() ? "Command Line Script" : "Dummy Sandbox";
}
DSandboxInfo DebuggerProxy::getSandbox() {
TRACE(2, "DebuggerProxy::getSandbox\n");
Lock lock(this);
return m_sandbox;
}
std::string DebuggerProxy::getSandboxId() {
TRACE(2, "DebuggerProxy::getSandboxId\n");
Lock lock(this);
return m_sandbox.id();
}
// Forms a list of all threads which are currently blocked within this
// proxy. There is the thread currently processing an interrupt, plus
// any other threads stacked up in blockUntilOwn() (represented in the
// m_threads set).
void DebuggerProxy::getThreads(DThreadInfoPtrVec &threads) {
TRACE(2, "DebuggerProxy::getThreads\n");
Lock lock(this);
std::stack<void *> &interrupts =
ThreadInfo::s_threadInfo->m_reqInjectionData.interrupts;
assert(!interrupts.empty());
if (!interrupts.empty()) {
CmdInterrupt *tint = (CmdInterrupt*)interrupts.top();
assert(tint);
if (tint) {
threads.push_back(createThreadInfo(tint->desc()));
}
}
for (std::map<int64_t, DThreadInfoPtr>::const_iterator iter =
m_threads.begin(); iter != m_threads.end(); ++iter) {
DThreadInfoPtr thread(new DThreadInfo());
*thread = *iter->second;
threads.push_back(thread);
}
}
// Switch this proxy to debug the given sandbox.
bool DebuggerProxy::switchSandbox(const std::string &newId, bool force) {
TRACE(2, "DebuggerProxy::switchSandbox\n");
return Debugger::SwitchSandbox(shared_from_this(), newId, force);
}
// Callback made by Debugger::SwitchSandbox() when the switch is successful.
// NB: this is called with a read lock on the corresponding entry in the sandbox
// map.
void DebuggerProxy::updateSandbox(DSandboxInfoPtr sandbox) {
TRACE(2, "DebuggerProxy::updateSandbox\n");
Lock lock(this);
if (sandbox) {
if (m_sandbox.id() != sandbox->id()) {
m_sandbox = *sandbox;
} else {
m_sandbox.update(*sandbox);
}
}
}
// Cause the proxy to debug the given thread. Other threads will stack
// up in blockUntilOwn() (depending on the thread mode).
bool DebuggerProxy::switchThread(DThreadInfoPtr thread) {
TRACE(2, "DebuggerProxy::switchThread\n");
Lock lock(this);
if (m_threads.find(thread->m_id) != m_threads.end()) {
m_newThread = thread;
return true;
}
return false;
}
// Change the thread mode, and mark the given (or calling) thread as
// the current thread for this proxy, depending on the thread mode.
//
// We have three thread modes: Normal, Sticky, and Exclusive. For more
// details, see the help for CmdThread. In short, in Normal mode any
// thread may hit and process an interrupt. The first thread to hit an
// interrupt becomes the current thread, and all others block until
// the current thread is done with its interrupt.
//
// In Exclusive mode, only the current thread may process interrupts;
// All others will ignore interrupts. In Sticky mode, the current
// thread continues to block others (even while running) until the
// mode is switched back to Normal.
void DebuggerProxy::switchThreadMode(ThreadMode mode,
int64_t threadId /* = 0 */) {
TRACE(2, "DebuggerProxy::switchThreadMode\n");
Lock lock(this);
m_threadMode = mode;
if (threadId) {
m_thread = threadId;
notifyAll(); // since threadId != 0, we're still waking up just one thread
} else if (mode == Normal) {
m_thread = 0;
notify();
} else {
m_thread = (int64_t)Process::GetThreadId();
}
TRACE(2, "Current thread is now %" PRIx64 "\n", m_thread);
}
void DebuggerProxy::startDummySandbox() {
TRACE(2, "DebuggerProxy::startDummySandbox\n");
m_dummySandbox =
new DummySandbox(this, RuntimeOption::DebuggerDefaultSandboxPath,
RuntimeOption::DebuggerStartupDocument);
m_dummySandbox->start();
}
void DebuggerProxy::notifyDummySandbox() {
TRACE(2, "DebuggerProxy::notifyDummySandbox\n");
if (m_dummySandbox) m_dummySandbox->notifySignal(CmdSignal::SignalBreak);
}
void DebuggerProxy::setBreakPoints(BreakPointInfoPtrVec &breakpoints) {
TRACE(2, "DebuggerProxy::setBreakPoints\n");
// Hold the break mutex while we update the proxy's state. There's no need
// to hold it over the longer operation to set breakpoints in each file later.
{
WriteLock lock(m_breakMutex);
// breakpoints holds a list of fresh new BreakPointInfo objects that
// have been deserialized from the client's list of breakpoints.
// The existing BreakPointInfo objects may include non empty values
// for m_stack. If these get thrown away, breakpoints that are temporarily
// disabled will suddenly fire again, which is not what we want to
// happen if we create a new breakpoint or just even list breakpoints.
auto it = m_breakpoints.begin();
for (auto it1 = breakpoints.begin(); it1 != breakpoints.end(); ++it1) {
BreakPointInfoPtr newBreakPoint = *it1;
do {
for (auto it2 = it; it2 != m_breakpoints.end(); ) {
BreakPointInfoPtr oldBreakPoint = *it2++;
if (oldBreakPoint->same(newBreakPoint)) {
newBreakPoint->transferStack(oldBreakPoint);
it = it2;
goto next_breakpoint;
}
}
if (it == m_breakpoints.begin()) goto next_breakpoint;
// Only searched a part of m_breakpoints. Reset it and try again.
it = m_breakpoints.begin();
} while (true);
next_breakpoint: continue;
}
m_breakpoints = breakpoints;
m_hasBreakPoints = !m_breakpoints.empty();
}
phpSetBreakPoints(this);
}
void DebuggerProxy::getBreakPoints(BreakPointInfoPtrVec &breakpoints) {
TRACE(7, "DebuggerProxy::getBreakPoints\n");
ReadLock lock(m_breakMutex);
breakpoints = m_breakpoints;
}
// Proxy needs to be interrupted because it has something setup which may need
// processing; breakpoints, flow control commands, a signal.
bool DebuggerProxy::needInterrupt() {
TRACE(2, "DebuggerProxy::needInterrupt\n");
return m_hasBreakPoints || m_flow || m_signum != CmdSignal::SignalNone;
}
// We need VM interrupts if we're in a state that requires the debugger to be
// interrupted for every opcode.
bool DebuggerProxy::needVMInterrupts() {
TRACE(2, "DebuggerProxy::needVMInterrupts\n");
bool flowNeedsInterrupt = (m_flow && m_flow->needsVMInterrupt());
return flowNeedsInterrupt || m_signum != CmdSignal::SignalNone;
}
// Handle an interrupt from the VM.
void DebuggerProxy::interrupt(CmdInterrupt &cmd) {
TRACE_RB(2, "DebuggerProxy::interrupt\n");
// Make any breakpoints that have passed breakable again.
setBreakableForBreakpointsNotMatching(cmd);
// At this point we have an interrupt, but we don't know if we're on the
// thread the proxy considers "current".
// NB: BreakPointReached really means we've got control of a VM thread from
// the opcode hook. This could be for a breakpoint, stepping, etc.
// Wait until this thread is the one this proxy wants to debug.
if (!blockUntilOwn(cmd, true)) return;
// We know we're on the "current" thread, so we can process any active flow
// command, stop if we're at a breakpoint, handle other interrupts, etc.
if (checkFlowBreak(cmd)) {
// We've hit a breakpoint and now need to make sure that breakpoints
// wont be hit again for this site until control leaves this site.
// (Breakpoints can still get hit if control reaches this site during
// a call that is part of this site because the flags are stacked.)
unsetBreakableForBreakpointsMatching(cmd);
while (true) {
try {
// We're about to send the client an interrupt and start
// waiting for commands back from it. Disable signal polling
// during this time, since our protocol requires that only one
// thread talk to the client at a time.
disableSignalPolling();
SCOPE_EXIT { enableSignalPolling(); };
processInterrupt(cmd);
} catch (const DebuggerException &e) {
TRACE(2, "DebuggerException from processInterrupt!\n");
switchThreadMode(Normal);
throw;
} catch (...) {
TRACE(2, "Unknown exception from processInterrupt!\n");
assert(false); // no other exceptions should be seen here
switchThreadMode(Normal);
throw;
}
if (cmd.getInterruptType() == PSPEnded) break;
if (!m_newThread) break; // we're not switching threads
switchThreadMode(Normal, m_newThread->m_id);
m_newThread.reset();
blockUntilOwn(cmd, false);
}
}
if ((m_threadMode == Normal) || (cmd.getInterruptType() == PSPEnded)) {
// If the thread mode is Normal we let other threads with
// interrupts go ahead and process them. We also do this when the
// thread is at PSPEnded because the thread is done.
switchThreadMode(Normal);
}
}
// Sends a copy of the given command to the associated client
// using the buffer in m_thrift. Returns false if an exception
// occurs during the send (typically a socket error).
bool DebuggerProxy::sendToClient(DebuggerCommand *cmd) {
TRACE(2, "DebuggerProxy::sendToClient\n");
return cmd->send(m_thrift);
}
// Allow the signal polling thread to send CmdSignal messages to the
// client to see if it there is a signal to repond to.
void DebuggerProxy::enableSignalPolling() {
Lock lock(m_signalMutex);
m_okayToPoll = true;
}
// Prevent the signal polling thread from sending CmdSignal messages
// to the client. Polling is disabled while a thread is stopped at an
// interrupt and responding to messages from the client.
void DebuggerProxy::disableSignalPolling() {
Lock lock(m_signalMutex);
m_okayToPoll = false;
// Drop any pending signal since we're about to start processing
// interrupts again anyway.
m_signum = CmdSignal::SignalNone;
}
void DebuggerProxy::startSignalThread() {
TRACE(2, "DebuggerProxy::startSignalThread\n");
m_signalThread.start();
}
// This gets it's own thread, and polls the client once per second to see if
// there is a signal, i.e., if the user has pressed Ctrl-C, etc. If there is a
// signal, it is passed as an interrupt to the proxy in an attempt to get other
// threads in the sandbox to stop.
//
// If another thread in the sandbox fails to stop and consume the signal then
// it will be passed to the dummy sandbox instead.
void DebuggerProxy::pollSignal() {
TRACE_RB(2, "DebuggerProxy::pollSignal: starting\n");
int signalTimeout = RuntimeOption::DebuggerSignalTimeout;
while (!m_stopped) {
sleep(1);
// Block any threads that might be interrupting from communicating with the
// client until we're done with this poll.
Lock lock(m_signalMutex);
// After DebuggerSignalTimeout seconds that no active thread picks
// up the signal, we send it to dummy sandbox.
if ((m_signum != CmdSignal::SignalNone) && m_dummySandbox &&
(--signalTimeout <= 0)) {
TRACE_RB(2, "DebuggerProxy::pollSignal: sending to dummy sandbox\n");
m_dummySandbox->notifySignal(m_signum);
m_signum = CmdSignal::SignalNone;
}
// Don't actually poll if another thread is already in a command
// processing loop with the client.
if (!m_okayToPoll) continue;
// Send CmdSignal over to the client and wait for a response.
CmdSignal cmd;
if (!cmd.onServer(*this)) {
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"Failed to send CmdSignal to client\n");
break;
}
// We've sent the client a command, and we expect an immediate
// response. Wait 10 times to give it a chance on especially
// overloaded computers.
DebuggerCommandPtr res;
for (int i = 0; i < 10; i++) {
if (DebuggerCommand::Receive(m_thrift, res,
"DebuggerProxy::pollSignal()")) break;
if (m_stopped) {
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"signal thread asked to stop while waiting "
"for CmdSignal back from the client\n");
break;
}
}
if (!res) {
if (!m_stopped) {
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"Failed to get CmdSignal back from client\n");
}
break;
}
CmdSignalPtr sig = dynamic_pointer_cast<CmdSignal>(res);
if (!sig) {
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"bad response from signal polling: %d", res->getType());
break;
}
auto newSignum = sig->getSignal();
if (newSignum != CmdSignal::SignalNone) {
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"got interrupt signal from client\n");
m_signum = newSignum;
signalTimeout = RuntimeOption::DebuggerSignalTimeout;
Debugger::RequestInterrupt(shared_from_this());
}
}
if (!m_stopped) {
// We've noticed that the socket has closed. Stop and destory this proxy.
TRACE_RB(2, "DebuggerProxy::pollSignal: "
"lost communication with the client, stopping proxy\n");
Debugger::UsageLog("server", getSandboxId(), "ProxyError", "Signal poll");
stop();
}
TRACE_RB(2, "DebuggerProxy::pollSignal: ended\n");
}
// Grab the ip address and port of the client that is connected to this proxy.
bool DebuggerProxy::getClientConnectionInfo(VRefParam address,
VRefParam port) {
Resource s(getSocket().get());
return f_socket_getpeername(s, address, port);
}
///////////////////////////////////////////////////////////////////////////////
// helpers
std::string DebuggerProxy::MakePHP(const std::string &php) {
TRACE(2, "DebuggerProxy::MakePHP\n");
return "<?php " + php + ";";
}
std::string DebuggerProxy::MakePHPReturn(const std::string &php) {
TRACE(2, "DebuggerProxy::MakePHPReturn\n");
return "<?php return " + php + ";";
}
// Passed to the ExecutionContext during Eval to add writes to stdout
// to the output buffer string.
static void append_stdout(const char *s, int len, void *data) {
TRACE(2, "DebuggerProxy::append_stdout\n");
StringBuffer *sb = (StringBuffer*)data;
if (s_stdout_color) {
sb->append(s_stdout_color);
}
sb->append(s, len);
if (s_stdout_color) {
sb->append(ANSI_COLOR_END);
}
}
// Passed to the ExecutionContext during Eval to add writes to stderr
// to the output buffer string.
static void append_stderr(const char *header, const char *msg,
const char *ending, void *data) {
TRACE(2, "DebuggerProxy::append_stderr\n");
StringBuffer *sb = (StringBuffer*)data;
if (s_stderr_color) {
sb->append(s_stderr_color);
}
sb->append(msg);
sb->append(ending);
if (s_stderr_color) {
sb->append(ANSI_COLOR_END);
}
}
// Record info about the current thread for the debugger client to use
// when listing all threads which are interrupted.
DThreadInfoPtr DebuggerProxy::createThreadInfo(const std::string &desc) {
TRACE(2, "DebuggerProxy::createThreadInfo\n");
DThreadInfoPtr info(new DThreadInfo());
info->m_id = (int64_t)Process::GetThreadId();
info->m_desc = desc;
Transport *transport = g_context->getTransport();
if (transport) {
info->m_type = transport->getThreadTypeName();
info->m_url = transport->getCommand();
} else {
info->m_type = getThreadType();
}
return info;
}
// Waits until this thread is the one that the proxy considers the current
// thread.
// NB: if the thread is not the current thread, and we're asked to check
// breakpoints, then if there are no breakpoints which could effect this
// thread we simply return false and stop processing the current interrupt.
bool DebuggerProxy::blockUntilOwn(CmdInterrupt &cmd, bool check) {
int64_t self = cmd.getThreadId();
TRACE(2, "DebuggerProxy::blockUntilOwn for thread %" PRIx64 "\n", self);
Lock lock(this);
if (m_thread && m_thread != self) {
if (check && (m_threadMode == Exclusive || !checkBreakPoints(cmd))) {
// Flow control commands only belong to sticky thread
TRACE(2, "No need to interrupt this thread\n");
return false;
}
m_threads[self] = createThreadInfo(cmd.desc());
while (!m_stopped && m_thread && m_thread != self) {
TRACE(2, "Waiting...\n");
wait(1);
// If for whatever reason, m_thread isn't debugging anymore (for example,
// it runs in Sticky mode, but it finishes running), kick it out.
if (m_thread && !Debugger::IsThreadDebugging(m_thread)) {
TRACE(2, "Old thread abandoned debugging, taking over!\n");
m_threadMode = Normal;
m_thread = 0;
m_newThread.reset();
m_flow.reset();
}
}
TRACE(2, "Ready to become current thread for this proxy\n");
m_threads.erase(self);
if (m_stopped) return false;
if (!checkBreakPoints(cmd)) {
// The breakpoint might have been removed while I'm waiting
return false;
}
}
// This thread is now the one the proxy considers the current thread.
if (m_thread == 0) {
TRACE(2, "Updating current thread\n");
m_thread = self;
}
TRACE(2, "Current thread for this proxy tid=%" PRIx64 "\n", self);
return true;
}
// Checks whether the cmd has any breakpoints that match the current Site.
// Also returns true for cmds that should always break, like SessionStarted,
// and returns true when we have special modes setup for, say, breaking on
// RequestEnded, PSPEnded, etc.
bool DebuggerProxy::checkBreakPoints(CmdInterrupt &cmd) {
TRACE(2, "DebuggerProxy::checkBreakPoints\n");
ReadLock lock(m_breakMutex);
return cmd.shouldBreak(*this, m_breakpoints, getRealStackDepth());
}
// Check if we should stop due to flow control, breakpoints, and signals.
bool DebuggerProxy::checkFlowBreak(CmdInterrupt &cmd) {
TRACE(2, "DebuggerProxy::checkFlowBreak\n");
// If there is an outstanding Ctrl-C from the client, go ahead and break now.
// Note: this stops any flow control command we might have in-flight.
if (m_signum == CmdSignal::SignalBreak) {
Lock lock(m_signalMutex);
if (m_signum == CmdSignal::SignalBreak) {
m_signum = CmdSignal::SignalNone;
m_flow.reset();
return true;
}
}
// Note that even if fcShouldBreak, we still need to test bpShouldBreak
// so to correctly report breakpoint reached + evaluating watchpoints;
// vice versa, so to give a flow cmd a chance to react.
// Note: a Continue cmd is a bit special. We need to process it _only_ if
// we decide to break.
bool fcShouldBreak = false; // should I break according to flow control?
bool bpShouldBreak = false; // should I break according to breakpoints?
if ((cmd.getInterruptType() == BreakPointReached ||
cmd.getInterruptType() == HardBreakPoint ||
cmd.getInterruptType() == ExceptionHandler) && m_flow) {
if (!m_flow->is(DebuggerCommand::KindOfContinue)) {
m_flow->onBeginInterrupt(*this, cmd);
if (m_flow->complete()) {
fcShouldBreak = true;
m_flow.reset();
}
}
}
// NB: this also checks whether we should be stopping at special interrupt
// sites, like SessionStarted, RequestEnded, ExceptionThrown, etc.
bpShouldBreak = checkBreakPoints(cmd);
// This is done before KindOfContinue testing.
if (!fcShouldBreak && !bpShouldBreak) {
return false;
}
if ((cmd.getInterruptType() == BreakPointReached ||
cmd.getInterruptType() == HardBreakPoint) && m_flow) {
if (m_flow->is(DebuggerCommand::KindOfContinue)) {
m_flow->onBeginInterrupt(*this, cmd);
if (m_flow->complete()) m_flow.reset();
return false;
}
}
return true;
}
void DebuggerProxy::checkStop() {
TRACE(5, "DebuggerProxy::checkStop\n");
if (m_stopped) stopAndThrow();
}
void DebuggerProxy::processInterrupt(CmdInterrupt &cmd) {
TRACE_RB(2, "DebuggerProxy::processInterrupt\n");
// Do the server-side work for this interrupt, which just notifies the client.
if (!cmd.onServer(*this)) {
TRACE_RB(1, "Failed to send CmdInterrupt to client\n");
Debugger::UsageLog("server", getSandboxId(), "ProxyError",
"Send interrupt");
stopAndThrow();
}
Debugger::UsageLogInterrupt("server", getSandboxId(), cmd);
// Wait for commands from the debugger client and process them. We'll stay
// here until we get a command that should cause the thread to continue.
while (true) {
DebuggerCommandPtr res;
while (!DebuggerCommand::Receive(m_thrift, res,
"DebuggerProxy::processInterrupt()")) {
// we will wait forever until DebuggerClient sends us something
checkStop();
}
checkStop();
if (res) {
TRACE_RB(2, "Proxy got cmd type %d\n", res->getType());
Debugger::UsageLog("server", getSandboxId(),
boost::lexical_cast<string>(res->getType()));
// Any control flow command gets installed here and we continue execution.
m_flow = dynamic_pointer_cast<CmdFlowControl>(res);
if (m_flow) {
m_flow->onSetup(*this, cmd);
if (!m_flow->complete()) {
TRACE_RB(2, "Incomplete flow command %d remaining on proxy for "
"further processing\n", m_flow->getType());
if (m_threadMode == Normal) {
// We want the flow command to complete on the thread that
// starts it.
switchThreadMode(Sticky);
}
} else {
// The flow cmd has determined that it is done with its work and
// doesn't need to remain for later processing.
TRACE_RB(2, "Flow command %d completed\n", m_flow->getType());
m_flow.reset();
}
return;
}
if (res->is(DebuggerCommand::KindOfQuit)) {
TRACE_RB(2, "Received quit command\n");
res->onServer(*this); // acknowledge receipt so that client can quit.
stopAndThrow();
}
}
bool cmdFailure = false;
try {
// Perform the server-side work for this command.
if (res) {
if (!res->onServer(*this)) {
TRACE_RB(1, "Failed to execute cmd %d from client\n", res->getType());
Debugger::UsageLog("server", getSandboxId(), "ProxyError",
"Command failed");
cmdFailure = true;
}
} else {
TRACE_RB(1, "Failed to receive cmd from client\n");
Debugger::UsageLog("server", getSandboxId(), "ProxyError",
"Command receive failed");
cmdFailure = true;
}
} catch (const DebuggerException &e) {
throw;
} catch (const std::exception& e) {
Logger::Warning(DEBUGGER_LOG_TAG
"Cmd type %d onServer() threw exception %s", res->getType(), e.what());
Debugger::UsageLog("server", getSandboxId(), "ProxyError",
"Command exception");
cmdFailure = true;
} catch (...) {
Logger::Warning(DEBUGGER_LOG_TAG
"Cmd type %d onServer() threw non standard exception", res->getType());
Debugger::UsageLog("server", getSandboxId(), "ProxyError",
"Command exception");
cmdFailure = true;
}
if (cmdFailure) stopAndThrow();
if (res->shouldExitInterrupt()) return;
}
}
///////////////////////////////////////////////////////////////////////////////
Variant DebuggerProxy::ExecutePHP(const std::string &php, String &output,
int frame, bool &failed, int flags) {
TRACE(2, "DebuggerProxy::ExecutePHP\n");
Variant ret;
// Wire up stdout and stderr to our own string buffer so we can pass
// any output back to the client.
StringBuffer sb;
StringBuffer *save = g_context->swapOutputBuffer(nullptr);
g_context->setStdout(append_stdout, &sb);
if (flags & ExecutePHPFlagsLog) {
Logger::SetThreadHook(append_stderr, &sb);
}
String code(php.c_str(), php.size(), CopyString);
// We're about to start executing more PHP. This is typically done
// in response to commands from the client, and the client expects
// those commands to send more interrupts since, of course, the
// user might want to debug the code we're about to run. If we're
// already processing an interrupt, enable signal polling around
// the execution of the new PHP to ensure that we can handle
// signals while doing so.
//
// Note: we must switch the thread mode to Sticky so we block
// other threads which may hit interrupts while we're running,
// since nested processInterrupt() calls would normally release
// other threads on the way out.
assert(m_thread == (int64_t)Process::GetThreadId());
ThreadMode origThreadMode = m_threadMode;
switchThreadMode(Sticky, m_thread);
if (flags & ExecutePHPFlagsAtInterrupt) enableSignalPolling();
SCOPE_EXIT {
if (flags & ExecutePHPFlagsAtInterrupt) disableSignalPolling();
switchThreadMode(origThreadMode, m_thread);
};
failed = g_vmContext->evalPHPDebugger((TypedValue*)&ret, code.get(), frame);
g_context->setStdout(nullptr, nullptr);
g_context->swapOutputBuffer(save);
if (flags & ExecutePHPFlagsLog) {
Logger::SetThreadHook(nullptr, nullptr);
}
output = sb.detach();
return ret;
}
int DebuggerProxy::getRealStackDepth() {
TRACE(2, "DebuggerProxy::getRealStackDepth\n");
int depth = 0;
VMExecutionContext* context = g_vmContext;
ActRec *fp = context->getFP();
if (!fp) return 0;
while (fp != nullptr) {
fp = context->getPrevVMState(fp, nullptr, nullptr);
depth++;
}
return depth;
}
int DebuggerProxy::getStackDepth() {
TRACE(2, "DebuggerProxy::getStackDepth\n");
int depth = 0;
VMExecutionContext* context = g_vmContext;
ActRec *fp = context->getFP();
if (!fp) return 0;
ActRec *prev = fp->arGetSfp();
while (fp != prev) {
fp = prev;
prev = fp->arGetSfp();
depth++;
}
return depth;
}
// Allow breaks for previously disabled breakpoints that do not match the site
// of cmd. (Call this when processing an interrupt since this probably means
// that execution has moved away from the previous interrupt site.)
void DebuggerProxy::setBreakableForBreakpointsNotMatching(CmdInterrupt& cmd) {
TRACE(2, "DebuggerProxy::setBreakableForBreakpointsNotMatching\n");
auto site = cmd.getSite();
if (site != nullptr) {
auto stackDepth = getRealStackDepth();
for (unsigned int i = 0; i < m_breakpoints.size(); ++i) {
BreakPointInfoPtr bp = m_breakpoints[i];
if (bp != nullptr && bp->m_state != BreakPointInfo::Disabled &&
!bp->match(*this, cmd.getInterruptType(), *site)) {
bp->setBreakable(stackDepth);
}
}
}
}
// Do not allow further breaks on the site of cmd, except during
// calls made from the current site.
void DebuggerProxy::unsetBreakableForBreakpointsMatching(CmdInterrupt& cmd) {
TRACE(2, "DebuggerProxy::unsetBreakableForBreakpointsMatching\n");
auto site = cmd.getSite();
if (site != nullptr) {
auto offset = site->getCurOffset();
auto stackDepth = getRealStackDepth();
for (unsigned int i = 0; i < m_breakpoints.size(); ++i) {
BreakPointInfoPtr bp = m_breakpoints[i];
if (bp != nullptr && bp->m_state != BreakPointInfo::Disabled &&
bp->match(*this, cmd.getInterruptType(), *site)) {
bp->unsetBreakable(stackDepth, offset);
}
}
}
}
///////////////////////////////////////////////////////////////////////////////
}}