f3d5a8abb1
A client couldn't break execution during eval. There used to be a lot of barriers to making that right, but I fixed most of them with a previous diff on unifying client-side event loops. Now the only barrier was that a server-side thread processing an interrupt was blocking the signal polling thread by holding a mutex while processing the interrupt. Changed to set a flag to disable polling when starting to process the interrupt (and unsetting it when done), while still synchronizing with the signal polling thread to ensure only one thread is sending the client messages at a time. Added logic to re-enable polling while executing PHP for eval, print, etc. Plumbed the proxy thru to the point where we check the clause on conditional breakpoints, too, since that's the third (and final) place we do this.
1057 linhas
32 KiB
C++
1057 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/break_point.h"
|
|
#include "hphp/runtime/debugger/debugger.h"
|
|
#include "hphp/runtime/debugger/debugger_proxy.h"
|
|
#include "hphp/runtime/debugger/debugger_thrift_buffer.h"
|
|
#include "hphp/runtime/base/preg.h"
|
|
#include "hphp/runtime/base/execution_context.h"
|
|
#include "hphp/runtime/base/class_info.h"
|
|
#include "hphp/runtime/base/stat_cache.h"
|
|
#include "hphp/runtime/vm/jit/translator-inline.h"
|
|
#include "hphp/runtime/base/comparisons.h"
|
|
|
|
namespace HPHP { namespace Eval {
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
TRACE_SET_MOD(debugger);
|
|
|
|
int InterruptSite::getFileLen() const {
|
|
TRACE(2, "InterruptSite::getFileLen\n");
|
|
if (m_file.empty()) {
|
|
getFile();
|
|
}
|
|
return m_file.size();
|
|
}
|
|
|
|
std::string InterruptSite::desc() const {
|
|
TRACE(2, "InterruptSite::desc\n");
|
|
string ret;
|
|
if (m_error.isNull()) {
|
|
ret = "Break";
|
|
} else if (m_error.isObject()) {
|
|
ret = "Exception thrown";
|
|
} else {
|
|
ret = "Error occurred";
|
|
}
|
|
|
|
const char *cls = getClass();
|
|
const char *func = getFunction();
|
|
if (func && *func) {
|
|
ret += " at ";
|
|
if (cls && *cls) {
|
|
ret += cls;
|
|
ret += "::";
|
|
}
|
|
ret += func;
|
|
ret += "()";
|
|
}
|
|
|
|
string file = getFile();
|
|
int line0 = getLine0();
|
|
if (line0) {
|
|
ret += " on line " + lexical_cast<string>(line0);
|
|
if (!file.empty()) {
|
|
ret += " of " + file;
|
|
}
|
|
} else if (!file.empty()) {
|
|
ret += " in " + file;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
InterruptSite::InterruptSite(bool hardBreakPoint, CVarRef error)
|
|
: m_error(error), m_activationRecord(nullptr),
|
|
m_callingSite(nullptr), m_class(nullptr),
|
|
m_function(nullptr), m_file((StringData*)nullptr),
|
|
m_line0(0), m_char0(0), m_line1(0), m_char1(0),
|
|
m_offset(InvalidAbsoluteOffset), m_unit(nullptr), m_valid(false),
|
|
m_funcEntry(false) {
|
|
TRACE(2, "InterruptSite::InterruptSite\n");
|
|
#define bail_on(c) if (c) { return; }
|
|
VMExecutionContext* context = g_vmContext;
|
|
ActRec *fp = context->getFP();
|
|
bail_on(!fp);
|
|
if (hardBreakPoint && fp->skipFrame()) {
|
|
// for hard breakpoint, the fp is for an extension function,
|
|
// so we need to construct the site on the caller
|
|
fp = context->getPrevVMState(fp, &m_offset);
|
|
} else {
|
|
const uchar *pc = context->getPC();
|
|
bail_on(!fp->m_func);
|
|
m_unit = fp->m_func->unit();
|
|
bail_on(!m_unit);
|
|
m_offset = m_unit->offsetOf(pc);
|
|
if (m_offset == fp->m_func->base()) {
|
|
m_funcEntry = true;
|
|
}
|
|
}
|
|
#undef bail_on
|
|
this->Initialize(fp);
|
|
}
|
|
|
|
// Only used to look for callers by function name. No need to
|
|
// to retrieve source line information for this kind of site.
|
|
InterruptSite::InterruptSite(ActRec *fp, Offset offset, CVarRef error)
|
|
: m_error(error), m_activationRecord(nullptr),
|
|
m_callingSite(nullptr), m_class(nullptr),
|
|
m_function(nullptr), m_file((StringData*)nullptr),
|
|
m_line0(0), m_char0(0), m_line1(0), m_char1(0),
|
|
m_offset(offset), m_unit(nullptr), m_valid(false),
|
|
m_funcEntry(false) {
|
|
TRACE(2, "InterruptSite::InterruptSite(fp)\n");
|
|
this->Initialize(fp);
|
|
}
|
|
|
|
void InterruptSite::Initialize(ActRec *fp) {
|
|
TRACE(2, "InterruptSite::Initialize\n");
|
|
|
|
#define bail_on(c) if (c) { return; }
|
|
assert(fp);
|
|
m_activationRecord = fp;
|
|
bail_on(!fp->m_func);
|
|
m_unit = fp->m_func->unit();
|
|
bail_on(!m_unit);
|
|
m_file = m_unit->filepath()->data();
|
|
if (m_unit->getSourceLoc(m_offset, m_sourceLoc)) {
|
|
m_line0 = m_sourceLoc.line0;
|
|
m_char0 = m_sourceLoc.char0;
|
|
m_line1 = m_sourceLoc.line1;
|
|
m_char1 = m_sourceLoc.char1;
|
|
}
|
|
m_function = fp->m_func->name()->data();
|
|
if (fp->m_func->preClass()) {
|
|
m_class = fp->m_func->preClass()->name()->data();
|
|
} else {
|
|
m_class = "";
|
|
}
|
|
#undef bail_on
|
|
m_valid = true;
|
|
}
|
|
|
|
// Returns an Interrupt site for the function that called the
|
|
// function that contains this site. This site retains ownership
|
|
// of the returned site and it will be deleted when this site
|
|
// is destructed, so do not hold on to the returned object for
|
|
// longer than there is a guarantee that this site will be alive.
|
|
const InterruptSite *InterruptSite::getCallingSite() const {
|
|
if (m_callingSite != nullptr) return m_callingSite.get();
|
|
VMExecutionContext* context = g_vmContext;
|
|
Offset parentOffset;
|
|
auto parentFp = context->getPrevVMState(m_activationRecord, &parentOffset);
|
|
if (parentFp == nullptr) return nullptr;
|
|
m_callingSite.reset(new InterruptSite(parentFp, parentOffset, m_error));
|
|
return m_callingSite.get();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
const char *BreakPointInfo::ErrorClassName = "@";
|
|
|
|
const char *BreakPointInfo::GetInterruptName(InterruptType interrupt) {
|
|
TRACE(2, "BreakPointInfo::GetInterruptName\n");
|
|
switch (interrupt) {
|
|
case RequestStarted: return "start of request";
|
|
case RequestEnded: return "end of request or start of psp";
|
|
case PSPEnded: return "end of psp";
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
BreakPointInfo::BreakPointInfo(bool regex, State state,
|
|
const std::string &file, int line)
|
|
: m_index(0), m_state(state), m_valid(true),
|
|
m_interruptType(BreakPointReached),
|
|
m_file(file), m_line1(line), m_line2(line),
|
|
m_regex(regex), m_check(false) {
|
|
TRACE(2, "BreakPointInfo::BreakPointInfo..const std::string &file, int)\n");
|
|
createIndex();
|
|
}
|
|
|
|
BreakPointInfo::BreakPointInfo(bool regex, State state,
|
|
InterruptType interrupt,
|
|
const std::string &url)
|
|
: m_index(0), m_state(state), m_valid(true), m_interruptType(interrupt),
|
|
m_line1(0), m_line2(0), m_url(url),
|
|
m_regex(regex), m_check(false) {
|
|
TRACE(2, "BreakPointInfo::BreakPointInfo..const std::string &url)\n");
|
|
createIndex();
|
|
}
|
|
|
|
BreakPointInfo::BreakPointInfo(bool regex, State state,
|
|
InterruptType interrupt,
|
|
const std::string &exp,
|
|
const std::string &file)
|
|
: m_index(0), m_state(state), m_valid(true), m_interruptType(interrupt),
|
|
m_line1(0), m_line2(0),
|
|
m_regex(regex), m_check(false) {
|
|
TRACE(2, "BreakPointInfo::BreakPointInfo..const std::string &file)\n");
|
|
assert(m_interruptType != ExceptionHandler); // Server-side only.
|
|
if (m_interruptType == ExceptionThrown) {
|
|
parseExceptionThrown(exp);
|
|
} else {
|
|
parseBreakPointReached(exp, file);
|
|
}
|
|
createIndex();
|
|
}
|
|
|
|
static int s_max_breakpoint_index = 0;
|
|
void BreakPointInfo::createIndex() {
|
|
TRACE(2, "BreakPointInfo::createIndex\n");
|
|
m_index = ++s_max_breakpoint_index;
|
|
}
|
|
|
|
BreakPointInfo::~BreakPointInfo() {
|
|
TRACE(2, "BreakPointInfo::~BreakPointInfo\n");
|
|
if (m_index && m_index == s_max_breakpoint_index) {
|
|
--s_max_breakpoint_index;
|
|
}
|
|
}
|
|
|
|
void BreakPointInfo::sendImpl(int version, DebuggerThriftBuffer &thrift) {
|
|
TRACE(2, "BreakPointInfo::sendImpl\n");
|
|
thrift.write((int8_t)m_state);
|
|
if (version >= 1) thrift.write((int8_t)m_bindState);
|
|
thrift.write((int8_t)m_interruptType);
|
|
thrift.write(m_file);
|
|
thrift.write(m_line1);
|
|
thrift.write(m_line2);
|
|
thrift.write(m_namespace);
|
|
thrift.write(m_class);
|
|
thrift.write(m_funcs);
|
|
thrift.write(m_url);
|
|
thrift.write(m_regex);
|
|
thrift.write(m_check);
|
|
thrift.write(m_clause);
|
|
thrift.write(m_output);
|
|
thrift.write(m_exceptionClass);
|
|
thrift.write(m_exceptionObject);
|
|
}
|
|
|
|
void BreakPointInfo::recvImpl(int version, DebuggerThriftBuffer &thrift) {
|
|
TRACE(2, "BreakPointInfo::recvImpl\n");
|
|
int8_t tmp;
|
|
thrift.read(tmp); m_state = (State)tmp;
|
|
if (version >= 1) {
|
|
thrift.read(tmp); m_bindState = (BindState)tmp;
|
|
}
|
|
thrift.read(tmp); m_interruptType = (InterruptType)tmp;
|
|
thrift.read(m_file);
|
|
thrift.read(m_line1);
|
|
thrift.read(m_line2);
|
|
thrift.read(m_namespace);
|
|
thrift.read(m_class);
|
|
thrift.read(m_funcs);
|
|
thrift.read(m_url);
|
|
thrift.read(m_regex);
|
|
thrift.read(m_check);
|
|
thrift.read(m_clause);
|
|
thrift.read(m_output);
|
|
thrift.read(m_exceptionClass);
|
|
thrift.read(m_exceptionObject);
|
|
}
|
|
|
|
void BreakPointInfo::setClause(const std::string &clause, bool check) {
|
|
TRACE(2, "BreakPointInfo::setClause\n");
|
|
m_clause = clause;
|
|
m_check = check;
|
|
}
|
|
|
|
void BreakPointInfo::changeBreakPointDepth(int stackDepth) {
|
|
TRACE(2, "BreakPointInfo::changeBreakPointDepth\n");
|
|
// if the breakpoint is equal or lower than the stack depth
|
|
// delete it
|
|
breakDepthStack.remove_if(
|
|
std::bind2nd(std::greater_equal<int>(), stackDepth)
|
|
);
|
|
}
|
|
|
|
void BreakPointInfo::unsetBreakable(int stackDepth) {
|
|
TRACE(2, "BreakPointInfo::unsetBreakable\n");
|
|
breakDepthStack.push_back(stackDepth);
|
|
}
|
|
|
|
void BreakPointInfo::setBreakable(int stackDepth) {
|
|
TRACE(2, "BreakPointInfo::setBreakable\n");
|
|
if (!breakDepthStack.empty() && breakDepthStack.back() == stackDepth) {
|
|
breakDepthStack.pop_back();
|
|
}
|
|
}
|
|
|
|
bool BreakPointInfo::breakable(int stackDepth) const {
|
|
TRACE(2, "BreakPointInfo::breakable\n");
|
|
if (!breakDepthStack.empty() && breakDepthStack.back() == stackDepth) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void BreakPointInfo::toggle() {
|
|
TRACE(2, "BreakPointInfo::toggle\n");
|
|
switch (m_state) {
|
|
case Always: setState(Once); break;
|
|
case Once: setState(Disabled); break;
|
|
case Disabled: setState(Always); break;
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool BreakPointInfo::valid() {
|
|
TRACE(2, "BreakPointInfo::valid\n");
|
|
if (m_valid) {
|
|
switch (m_interruptType) {
|
|
case BreakPointReached:
|
|
if (!getFuncName().empty()) {
|
|
if (!m_file.empty() || m_line1 != 0) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (m_file.empty() || m_line1 == 0 || m_line2 != m_line1) {
|
|
return false;
|
|
}
|
|
}
|
|
if (m_regex) {
|
|
return false;
|
|
}
|
|
return (m_line1 && m_line2) || !m_file.empty() || !m_funcs.empty();
|
|
case ExceptionThrown:
|
|
return !m_class.empty();
|
|
case RequestStarted:
|
|
case RequestEnded:
|
|
case PSPEnded:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool BreakPointInfo::same(BreakPointInfoPtr bpi) {
|
|
TRACE(2, "BreakPointInfo::same\n");
|
|
return desc() == bpi->desc();
|
|
}
|
|
|
|
bool BreakPointInfo::match(DebuggerProxy &proxy, InterruptType interrupt,
|
|
InterruptSite &site) {
|
|
TRACE(2, "BreakPointInfo::match\n");
|
|
if (m_interruptType == interrupt) {
|
|
switch (interrupt) {
|
|
case RequestStarted:
|
|
case RequestEnded:
|
|
case PSPEnded:
|
|
return
|
|
checkUrl(site.url());
|
|
case ExceptionThrown:
|
|
return
|
|
checkExceptionOrError(site.getError()) &&
|
|
checkUrl(site.url()) && checkClause(proxy);
|
|
case BreakPointReached:
|
|
{
|
|
bool match =
|
|
Match(site.getFile(), site.getFileLen(), m_file, m_regex, false) &&
|
|
checkLines(site.getLine0()) && checkStack(site) &&
|
|
checkUrl(site.url()) && checkClause(proxy);
|
|
|
|
if (!getFuncName().empty()) {
|
|
// function entry breakpoint
|
|
match = match && site.funcEntry();
|
|
}
|
|
return match;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string BreakPointInfo::state(bool padding) const {
|
|
TRACE(2, "BreakPointInfo::state\n");
|
|
switch (m_state) {
|
|
case Always: return padding ? "ALWAYS " : "ALWAYS" ;
|
|
case Once: return padding ? "ONCE " : "ONCE" ;
|
|
case Disabled: return padding ? "DISABLED" : "DISABLED";
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string BreakPointInfo::regex(const std::string &name) const {
|
|
TRACE(2, "BreakPointInfo::regex\n");
|
|
if (m_regex) {
|
|
return "regex{" + name + "}";
|
|
}
|
|
return name;
|
|
}
|
|
|
|
std::string BreakPointInfo::getNamespace() const {
|
|
TRACE(2, "BreakPointInfo::getNamespace\n");
|
|
if (!m_funcs.empty()) {
|
|
return m_funcs[0]->m_namespace;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string BreakPointInfo::getClass() const {
|
|
TRACE(2, "BreakPointInfo::getClass\n");
|
|
if (!m_funcs.empty()) {
|
|
return m_funcs[0]->m_class;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string BreakPointInfo::getFunction() const {
|
|
TRACE(2, "BreakPointInfo::getFunction\n");
|
|
if (!m_funcs.empty()) {
|
|
return m_funcs[0]->m_function;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string BreakPointInfo::getFuncName() const {
|
|
TRACE(2, "BreakPointInfo::getFuncName\n");
|
|
if (!m_funcs.empty()) {
|
|
return m_funcs[0]->getName();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string BreakPointInfo::site() const {
|
|
TRACE(2, "BreakPointInfo::site\n");
|
|
string ret;
|
|
|
|
string preposition = "at ";
|
|
if (!m_funcs.empty()) {
|
|
ret = m_funcs[0]->site(preposition);
|
|
for (unsigned int i = 1; i < m_funcs.size(); i++) {
|
|
ret += " called by ";
|
|
string tmp;
|
|
ret += m_funcs[i]->site(tmp);
|
|
}
|
|
}
|
|
|
|
if (!m_file.empty() || m_line1) {
|
|
if (!ret.empty()) {
|
|
ret += " ";
|
|
} else {
|
|
preposition = "";
|
|
}
|
|
if (m_line1) {
|
|
ret += "on line " + lexical_cast<string>(m_line1);
|
|
if (!m_file.empty()) {
|
|
ret += " of " + m_file;
|
|
}
|
|
} else {
|
|
ret += "in " + m_file;
|
|
}
|
|
}
|
|
|
|
return preposition + ret;
|
|
}
|
|
|
|
std::string BreakPointInfo::descBreakPointReached() const {
|
|
TRACE(2, "BreakPointInfo::descBreakPointReached\n");
|
|
string ret;
|
|
for (unsigned int i = 0; i < m_funcs.size(); i++) {
|
|
ret += (i == 0 ? "upon entering " : " called by ");
|
|
ret += m_funcs[i]->desc(this);
|
|
}
|
|
|
|
if (!m_file.empty() || m_line1 || m_line2) {
|
|
if (!ret.empty()) {
|
|
ret += " ";
|
|
}
|
|
if (m_line1 || m_line2) {
|
|
if (m_line1 == m_line2) {
|
|
ret += "on line " + lexical_cast<string>(m_line1);
|
|
} else if (m_line2 == -1) {
|
|
ret += "between line " + lexical_cast<string>(m_line1) + " and end";
|
|
} else {
|
|
ret += "between line " + lexical_cast<string>(m_line1) +
|
|
" and line " + lexical_cast<string>(m_line2);
|
|
}
|
|
if (!m_file.empty()) {
|
|
ret += " of " + regex(m_file);
|
|
} else {
|
|
ret += " of any file";
|
|
}
|
|
} else {
|
|
ret += "on any lines in " + regex(m_file);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
std::string BreakPointInfo::descExceptionThrown() const {
|
|
TRACE(2, "BreakPointInfo::descExceptionThrown\n");
|
|
string ret;
|
|
if (!m_namespace.empty() || !m_class.empty()) {
|
|
if (m_class == ErrorClassName) {
|
|
ret = "right after an error";
|
|
} else {
|
|
ret = "right before throwing ";
|
|
if (!m_class.empty()) {
|
|
if (!m_namespace.empty()) {
|
|
ret += regex(m_namespace) + "::";
|
|
}
|
|
ret += regex(m_class);
|
|
} else {
|
|
ret += "any exceptions in namespace " + regex(m_namespace);
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
std::string BreakPointInfo::desc() const {
|
|
TRACE(2, "BreakPointInfo::desc\n");
|
|
string ret;
|
|
switch (m_interruptType) {
|
|
case BreakPointReached:
|
|
ret = descBreakPointReached();
|
|
break;
|
|
case ExceptionThrown:
|
|
ret = descExceptionThrown();
|
|
break;
|
|
default:
|
|
ret = GetInterruptName((InterruptType)m_interruptType);
|
|
break;
|
|
}
|
|
|
|
if (!m_url.empty()) {
|
|
ret += " when request is " + regex(m_url);
|
|
}
|
|
|
|
if (!m_clause.empty()) {
|
|
if (m_check) {
|
|
ret += " if " + m_clause;
|
|
} else {
|
|
ret += " && " + m_clause;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void mangleXhpName(const std::string &source, std::string &target) {
|
|
auto oldLen = source.length();
|
|
size_t newLen = 0;
|
|
size_t index = 0;
|
|
for (; index < oldLen; index++) {
|
|
auto ch = source[index];
|
|
if (ch != ':' && ch != '-') continue;
|
|
newLen = 4+index;
|
|
break;
|
|
}
|
|
if (newLen == 0) {
|
|
target = source;
|
|
return;
|
|
}
|
|
for (; index < oldLen; index++) {
|
|
if (source[index] == ':') newLen += 2; else newLen +=1;
|
|
}
|
|
target.clear();
|
|
target.reserve(newLen);
|
|
target.append("xhp_");
|
|
for (index = 0; index < oldLen; index++) {
|
|
auto ch = source[index];
|
|
if (ch == '-') {
|
|
target.push_back('_');
|
|
} else if (ch == ':') {
|
|
if (index > 0) {
|
|
target.push_back('_');
|
|
target.push_back('_');
|
|
}
|
|
} else {
|
|
target.push_back(ch);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t scanName(const std::string &str, int32_t offset) {
|
|
auto len = str.length();
|
|
assert(0 <= offset && offset < len);
|
|
while (offset < len) {
|
|
char ch = str[offset];
|
|
if (ch == ':' || ch == '\\' || ch == ',' || ch == '(' || ch == '=' ||
|
|
ch == '@') {
|
|
if (offset+1 >= len) return offset;
|
|
char ch1 = str[offset+1];
|
|
if (ch == ':') {
|
|
if (ch1 == ':' || ('0' <= ch1 && ch1 <= '9')) return offset;
|
|
} else if (ch == '(') {
|
|
if (ch1 == ')') return offset;
|
|
} else if (ch == '=') {
|
|
if (ch1 == '>') return offset;
|
|
} else {
|
|
assert (ch == '\\' || ch == ',' || ch == '@');
|
|
return offset;
|
|
}
|
|
}
|
|
offset++;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
int32_t scanNumber(const std::string &str, int32_t offset, int32_t& value) {
|
|
value = 0;
|
|
auto len = str.length();
|
|
assert(0 <= offset && offset < len);
|
|
while (offset < len) {
|
|
char ch = str[offset];
|
|
if (ch < '0' || ch > '9') return offset;
|
|
value = value*10 + (ch - '0');
|
|
offset++;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
int32_t BreakPointInfo::parseFileLocation(const std::string &str,
|
|
int32_t offset) {
|
|
auto len = str.length();
|
|
assert(0 <= offset && offset < len);
|
|
auto offset1 = scanNumber(str, offset, m_line1);
|
|
if (offset1 == offset) return offset; //Did not find a number
|
|
m_line2 = m_line1; //so that we always have a range
|
|
if (offset1 >= len) return len; //Nothing follows the number
|
|
auto ch = str[offset1];
|
|
if (ch == '-') {
|
|
if (offset1+1 >= len) return offset; //Invalid file location
|
|
auto offset2 = scanNumber(str, offset1+1, m_line2);
|
|
if (offset1+1 == offset2) return offset; //Invalid file location
|
|
return offset2;
|
|
}
|
|
return offset1;
|
|
}
|
|
|
|
/* The parser accepts the following syntax, which harks back to pre VM days
|
|
(all components are optional, as long as there is at least one component):
|
|
|
|
{file location},{call}=>{call}()@{url}
|
|
{call}=>{call}(),{file location}@{url}
|
|
|
|
file location: {file}:{line1}-{line2}
|
|
call: \{namespace}\{cls}::{func}
|
|
|
|
Currently semantic checks will disallow expressions that specify
|
|
both file locations and calls.
|
|
*/
|
|
void BreakPointInfo::parseBreakPointReached(const std::string &exp,
|
|
const std::string &file) {
|
|
TRACE(2, "BreakPointInfo::parseBreakPointReached\n");
|
|
|
|
string name;
|
|
auto len = exp.length();
|
|
auto offset0 = 0;
|
|
//Look for leading number by itself
|
|
auto offset1 = scanNumber(exp, offset0, m_line1);
|
|
if (offset1 == len) {
|
|
m_line2 = m_line1;
|
|
m_file = file;
|
|
return;
|
|
}
|
|
// Skip over a leading backslash
|
|
if (len > 0 && exp[0] == '\\') offset0++;
|
|
offset1 = scanName(exp, offset0);
|
|
// check that exp starts with a file or method name
|
|
if (offset1 == offset0) goto returnInvalid;
|
|
name = exp.substr(offset0, offset1-offset0);
|
|
|
|
if (offset0 == 0) {
|
|
// parse {file location} if appropriate
|
|
if (offset1 < len && exp[offset1] == ',') {
|
|
m_file = name;
|
|
name.clear();
|
|
offset1 += 1;
|
|
} else if (offset1 < len-1 && exp[offset1] == ':' &&
|
|
exp[offset1+1] != ':') {
|
|
m_file = name;
|
|
name.clear();
|
|
offset1 += 1;
|
|
auto offset2 = parseFileLocation(exp, offset1);
|
|
// check for {file}:{something that is not a number}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
offset1 = offset2;
|
|
if (offset1 >= len) return; // file location without anything else
|
|
if (exp[offset1] == '@') goto parseUrl; // file location followed by url
|
|
// check for {file}{ something other than @ or :}
|
|
if (exp[offset1] != ',') goto returnInvalid;
|
|
offset1 += 1;
|
|
}
|
|
}
|
|
|
|
// parse {func}() or {func}=>{func}() or {func}=>{func}=>{func}() and so on
|
|
while (true) {
|
|
if (name.empty()) {
|
|
if (len > offset1 && exp[offset1] == '\\') offset1++;
|
|
auto offset2 = scanName(exp, offset1);
|
|
// check for {something other than a name}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
name = exp.substr(offset1, offset2-offset1);
|
|
offset1 = offset2;
|
|
}
|
|
while (offset1 < len && exp[offset1] == '\\') {
|
|
if (!m_namespace.empty()) m_namespace += "\\";
|
|
m_namespace += name;
|
|
offset1 += 1;
|
|
auto offset2 = scanName(exp, offset1);
|
|
// check for {namespace}\{something that is not a name}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
name = exp.substr(offset1, offset2-offset1);
|
|
offset1 = offset2;
|
|
}
|
|
if (offset1 < len-1 && exp[offset1] == ':' && exp[offset1+1] == ':') {
|
|
m_class = name;
|
|
offset1 += 2;
|
|
auto offset2 = scanName(exp, offset1);
|
|
// check for {namespace}\{class}::{something that is not a name}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
name = exp.substr(offset1, offset2-offset1);
|
|
offset1 = offset2;
|
|
}
|
|
// Now we have a namespace, class and func name.
|
|
// The namespace only or the namespace and class might be empty.
|
|
DFunctionInfoPtr pfunc(new DFunctionInfo());
|
|
if (m_class.empty()) {
|
|
if (m_namespace.empty())
|
|
pfunc->m_function = name;
|
|
else {
|
|
// Yes this does seem beyond strange, but that is what the PHP parser
|
|
// does when it sees a function declared inside a namespace, so we
|
|
// too have to pretend there is no namespace here. At some point
|
|
// the parser hack may have to go away. At that stage, this code
|
|
// will have to change, as well as other parts of the debugger.
|
|
pfunc->m_function = m_namespace + "\\" + name;
|
|
}
|
|
} else {
|
|
mangleXhpName(m_class, pfunc->m_class);
|
|
if (!m_namespace.empty()) {
|
|
// Emulate parser hack. See longer comment above.
|
|
pfunc->m_class = m_namespace + "\\" + pfunc->m_class;
|
|
}
|
|
pfunc->m_function = name;
|
|
}
|
|
m_funcs.insert(m_funcs.begin(), pfunc);
|
|
m_namespace.clear();
|
|
m_class.clear();
|
|
name.clear();
|
|
// If we are now at () we skip over it and terminate the loop
|
|
if (offset1 < len && exp[offset1] == '(') {
|
|
// check for {func}{(}{not )}
|
|
if (offset1+1 >= len || exp[offset1+1] != ')') goto returnInvalid;
|
|
offset1 += 2;
|
|
break; // parsed the last (perhaps only) call in a function call chain
|
|
}
|
|
// If we are now at => we need to carry on with the loop
|
|
if (offset1 < len-1 && exp[offset1] == '=' && exp[offset1+1] == '>') {
|
|
offset1 += 2;
|
|
continue;
|
|
}
|
|
goto returnInvalid; // {func calls}{not () or =>}
|
|
}
|
|
if (m_file.empty()) {
|
|
if (offset1 < len && exp[offset1] == ',') {
|
|
auto offset2 = scanName(exp, ++offset1);
|
|
// check for {func calls}:{not a filename}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
m_file = exp.substr(offset1, offset2-offset1);
|
|
offset1 = offset2;
|
|
if (offset1 < len && exp[offset1] == ':') {
|
|
offset2 = parseFileLocation(exp, offset1+1);
|
|
// check for {file}:{something that is not a number}
|
|
if (offset2 == offset2+1) goto returnInvalid;
|
|
offset1 = offset2;
|
|
}
|
|
}
|
|
}
|
|
parseUrl:
|
|
if (offset1 < len-2 && exp[offset1] == '@') {
|
|
offset1++;
|
|
m_url = exp.substr(offset1, len-offset1);
|
|
} else {
|
|
// check for unparsed characters at end of exp
|
|
if (offset1 != len) goto returnInvalid;
|
|
}
|
|
return;
|
|
|
|
returnInvalid:
|
|
m_valid = false;
|
|
}
|
|
|
|
void BreakPointInfo::parseExceptionThrown(const std::string &exp) {
|
|
TRACE(2, "BreakPointInfo::parseExceptionThrown\n");
|
|
|
|
string name;
|
|
auto len = exp.length();
|
|
auto offset0 = 0;
|
|
// Skip over a leading backslash
|
|
if (len > 0 && exp[0] == '\\') offset0++;
|
|
auto offset1 = scanName(exp, offset0);
|
|
// check that exp starts with a name
|
|
if (offset1 == offset0) goto returnInvalid;
|
|
name = exp.substr(offset0, offset1-offset0);
|
|
if (name.empty()) {
|
|
if (len > offset1 && exp[offset1] == '\\') offset1++;
|
|
auto offset2 = scanName(exp, offset1);
|
|
// check for {something other than a name}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
name = exp.substr(offset1, offset2-offset1);
|
|
}
|
|
while (offset1 < len && exp[offset1] == '\\') {
|
|
if (!m_namespace.empty()) m_namespace += "\\";
|
|
m_namespace += name;
|
|
offset1 += 1;
|
|
auto offset2 = scanName(exp, offset1);
|
|
// check for {namespace}\{something that is not a name}
|
|
if (offset2 == offset1) goto returnInvalid;
|
|
name = exp.substr(offset1, offset2-offset1);
|
|
offset1 = offset2;
|
|
}
|
|
m_class = name;
|
|
// Now we have a namespace and class name.
|
|
// The namespace might be empty.
|
|
mangleXhpName(m_class, m_class);
|
|
if (m_class == "error") m_class = ErrorClassName;
|
|
if (!m_namespace.empty()) {
|
|
m_class = m_namespace + "\\" + m_class;
|
|
m_namespace.clear();
|
|
}
|
|
if (offset1 < len-2 && exp[offset1] == '@') {
|
|
offset1++;
|
|
m_url = exp.substr(offset1, len-offset1);
|
|
} else {
|
|
// check for unparsed characters at end of exp
|
|
if (offset1 != len) goto returnInvalid;
|
|
}
|
|
return;
|
|
|
|
returnInvalid:
|
|
m_valid = false;
|
|
}
|
|
|
|
bool BreakPointInfo::MatchFile(const char *haystack, int haystack_len,
|
|
const std::string &needle) {
|
|
TRACE(2, "BreakPointInfo::MatchFile(const char *haystack\n");
|
|
int pos = haystack_len - needle.size();
|
|
if ((pos == 0 || haystack[pos - 1] == '/') &&
|
|
strcasecmp(haystack + pos, needle.c_str()) == 0) {
|
|
return true;
|
|
}
|
|
if (strcasecmp(StatCache::realpath(needle.c_str()).c_str(), haystack)
|
|
== 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Returns true if file is a suffix path of fullPath
|
|
bool BreakPointInfo::MatchFile(const std::string& file,
|
|
const std::string& fullPath) {
|
|
TRACE(2, "BreakPointInfo::MatchFile(const std::string&\n");
|
|
if (file == fullPath) {
|
|
return true;
|
|
}
|
|
if (file.size() > 0 && file[0] != '/') {
|
|
auto pos = fullPath.rfind(file);
|
|
// check for match
|
|
if (pos == std::string::npos) return false;
|
|
// check if match is a suffix
|
|
if (pos + file.size() > fullPath.size()) return false;
|
|
// check if suffix is a sub path
|
|
if (pos == 0 || fullPath[pos-1] != '/') return false;
|
|
return true;
|
|
}
|
|
// Perhaps file or fullPath is a symlink.
|
|
auto realFile = StatCache::realpath(file.c_str());
|
|
auto realFullPath = StatCache::realpath(fullPath.c_str());
|
|
if (realFile != file || realFullPath != fullPath) {
|
|
return MatchFile(realFile, realFullPath);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool BreakPointInfo::MatchClass(const char *fcls, const std::string &bcls,
|
|
bool regex, const char *func) {
|
|
TRACE(2, "BreakPointInfo::MatchClass\n");
|
|
if (bcls.empty()) return true;
|
|
if (!fcls || !*fcls) return false;
|
|
if (regex || !func || !*func) {
|
|
return Match(fcls, 0, bcls, true, true);
|
|
}
|
|
|
|
StackStringData sdBClsName(bcls.c_str());
|
|
Class* clsB = Unit::lookupClass(&sdBClsName);
|
|
if (!clsB) return false;
|
|
StackStringData sdFClsName(fcls);
|
|
Class* clsF = Unit::lookupClass(&sdFClsName);
|
|
if (clsB == clsF) return true;
|
|
StackStringData sdFuncName(func);
|
|
Func* f = clsB->lookupMethod(&sdFuncName);
|
|
return f && f->baseCls() == clsF;
|
|
}
|
|
|
|
bool BreakPointInfo::Match(const char *haystack, int haystack_len,
|
|
const std::string &needle, bool regex, bool exact) {
|
|
TRACE(2, "BreakPointInfo::Match\n");
|
|
if (needle.empty()) {
|
|
return true;
|
|
}
|
|
if (!haystack || !*haystack) {
|
|
return false;
|
|
}
|
|
|
|
if (!regex) {
|
|
if (exact) {
|
|
return strcasecmp(haystack, needle.c_str()) == 0;
|
|
}
|
|
return MatchFile(haystack, haystack_len, needle);
|
|
}
|
|
|
|
Variant matches;
|
|
Variant r = preg_match(String(needle.c_str(), needle.size(),
|
|
AttachLiteral),
|
|
String(haystack, haystack_len, AttachLiteral),
|
|
matches);
|
|
return HPHP::same(r, 1);
|
|
}
|
|
|
|
bool BreakPointInfo::checkExceptionOrError(CVarRef e) {
|
|
TRACE(2, "BreakPointInfo::checkException\n");
|
|
assert(!e.isNull());
|
|
if (e.isObject()) {
|
|
if (m_regex) {
|
|
return Match(m_class.c_str(), m_class.size(),
|
|
e.toObject()->o_getClassName().data(), true, false);
|
|
}
|
|
return e.instanceof(m_class.c_str());
|
|
}
|
|
return Match(m_class.c_str(), m_class.size(), ErrorClassName, m_regex,
|
|
false);
|
|
}
|
|
|
|
bool BreakPointInfo::checkUrl(std::string &url) {
|
|
TRACE(2, "BreakPointInfo::checkUrl\n");
|
|
if (!m_url.empty()) {
|
|
if (url.empty()) {
|
|
url = "/";
|
|
Transport *transport = g_context->getTransport();
|
|
if (transport) {
|
|
url += transport->getCommand();
|
|
}
|
|
}
|
|
return Match(url.c_str(), url.size(), m_url, m_regex, false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool BreakPointInfo::checkLines(int line) {
|
|
TRACE(2, "BreakPointInfo::checkLines\n");
|
|
if (m_line1) {
|
|
assert(m_line2 == -1 || m_line2 >= m_line1);
|
|
return line >= m_line1 && (m_line2 == -1 || line <= m_line2);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Checks if m_funcs[0] matches the top of the execution stack and
|
|
// if m_funcs[1] (if not null) matches an earlier stack frame, and so on.
|
|
// I.e. m_funcs[1] need only be caller, not a direct caller, of m_funcs[0].
|
|
bool BreakPointInfo::checkStack(InterruptSite &site) {
|
|
TRACE(2, "BreakPointInfo::checkStack\n");
|
|
const InterruptSite* s = &site;
|
|
for (int i = 0; i < m_funcs.size(); ) {
|
|
if (s == nullptr) return false;
|
|
if (!Match(s->getNamespace(), 0, m_funcs[i]->m_namespace, m_regex, true) ||
|
|
!Match(s->getFunction(), 0, m_funcs[i]->m_function, m_regex, true) ||
|
|
!MatchClass(s->getClass(), m_funcs[i]->m_class, m_regex,
|
|
s->getFunction())) {
|
|
if (i == 0) return false; //m_funcs[0] must match the very first frame.
|
|
// there is a mismatch for this frame, but the calling frame may match
|
|
// so carry on.
|
|
} else {
|
|
i++; // matched m_funcs[i], proceed to match m_funcs[i+1]
|
|
}
|
|
s = s->getCallingSite();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool BreakPointInfo::checkClause(DebuggerProxy &proxy) {
|
|
TRACE(2, "BreakPointInfo::checkClause\n");
|
|
if (!m_clause.empty()) {
|
|
if (m_php.empty()) {
|
|
if (m_check) {
|
|
m_php = DebuggerProxy::MakePHPReturn(m_clause);
|
|
} else {
|
|
m_php = DebuggerProxy::MakePHP(m_clause);
|
|
}
|
|
}
|
|
String output;
|
|
{
|
|
// Don't hit more breakpoints while attempting to decide if we should stop
|
|
// at this breakpoint.
|
|
EvalBreakControl eval(true);
|
|
Variant ret = proxy.ExecutePHP(m_php, output, 0,
|
|
DebuggerProxy::ExecutePHPFlagsNone);
|
|
if (m_check) {
|
|
return ret.toBoolean();
|
|
}
|
|
}
|
|
m_output = std::string(output.data(), output.size());
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
void BreakPointInfo::SendImpl(int version, const BreakPointInfoPtrVec &bps,
|
|
DebuggerThriftBuffer &thrift) {
|
|
TRACE(2, "BreakPointInfo::SendImpl\n");
|
|
int16_t size = bps.size();
|
|
thrift.write(size);
|
|
for (int i = 0; i < size; i++) {
|
|
bps[i]->sendImpl(version, thrift);
|
|
}
|
|
}
|
|
|
|
void BreakPointInfo::RecvImpl(int version, BreakPointInfoPtrVec &bps,
|
|
DebuggerThriftBuffer &thrift) {
|
|
TRACE(2, "BreakPointInfo::RecvImpl\n");
|
|
int16_t size;
|
|
thrift.read(size);
|
|
bps.resize(size);
|
|
for (int i = 0; i < size; i++) {
|
|
BreakPointInfoPtr bpi(new BreakPointInfo());
|
|
bpi->recvImpl(version, thrift);
|
|
bps[i] = bpi;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
}}
|