Revision control
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
#include "qv4debugger.h"
#include "qv4debugjob.h"
#include "qv4datacollector.h"
#include <private/qqmlcontext_p.h>
#include <private/qqmlengine_p.h>
+ #include <private/qv4compileddata_p.h>
+ #include <private/qv4function_p.h>
#include <private/qv4scopedvalue_p.h>
#include <private/qv4script_p.h>
#include <private/qv4stackframe_p.h>
+ #include <algorithm>
+
QT_BEGIN_NAMESPACE
QV4Debugger::BreakPoint::BreakPoint(const QString &fileName, int line)
: fileName(fileName), lineNumber(line)
{}
inline size_t qHash(const QV4Debugger::BreakPoint &b, size_t seed = 0) noexcept
{
return qHash(b.fileName, seed) ^ b.lineNumber;
}
inline bool operator==(const QV4Debugger::BreakPoint &a,
const QV4Debugger::BreakPoint &b)
{
return a.lineNumber == b.lineNumber && a.fileName == b.fileName;
}
QV4Debugger::QV4Debugger(QV4::ExecutionEngine *engine)
: m_engine(engine)
, m_state(Running)
, m_stepping(NotStepping)
, m_pauseRequested(false)
, m_haveBreakPoints(false)
, m_breakOnThrow(false)
, m_returnedValue(engine, QV4::Value::undefinedValue())
, m_gatherSources(nullptr)
, m_runningJob(nullptr)
, m_collector(engine)
{
static int debuggerId = qRegisterMetaType<QV4Debugger*>();
static int pauseReasonId = qRegisterMetaType<QV4Debugger::PauseReason>();
Q_UNUSED(debuggerId);
Q_UNUSED(pauseReasonId);
connect(this, &QV4Debugger::scheduleJob,
this, &QV4Debugger::runJobUnpaused, Qt::QueuedConnection);
}
QV4::ExecutionEngine *QV4Debugger::engine() const
{
return m_engine;
}
const QV4DataCollector *QV4Debugger::collector() const
{
return &m_collector;
}
QV4DataCollector *QV4Debugger::collector()
{
return &m_collector;
}
void QV4Debugger::pause()
{
QMutexLocker locker(&m_lock);
if (m_state == Paused)
return;
m_pauseRequested = true;
}
void QV4Debugger::resume(Speed speed)
{
QMutexLocker locker(&m_lock);
if (m_state != Paused)
return;
if (!m_returnedValue.isUndefined())
m_returnedValue.set(m_engine, QV4::Encode::undefined());
m_currentFrame = m_engine->currentStackFrame;
m_stepping = speed;
m_runningCondition.wakeAll();
}
QV4Debugger::State QV4Debugger::state() const
{
return m_state;
}
void QV4Debugger::addBreakPoint(const QString &fileName, int lineNumber, const QString &condition)
{
QMutexLocker locker(&m_lock);
m_breakPoints.insert(BreakPoint(fileName.mid(fileName.lastIndexOf('/') + 1),
- lineNumber), condition);
+ lineNumber), {fileName, condition});
m_haveBreakPoints = true;
+ m_breakPointsChanged = true;
}
void QV4Debugger::removeBreakPoint(const QString &fileName, int lineNumber)
{
QMutexLocker locker(&m_lock);
m_breakPoints.remove(BreakPoint(fileName.mid(fileName.lastIndexOf('/') + 1),
lineNumber));
m_haveBreakPoints = !m_breakPoints.isEmpty();
+ m_breakPointsChanged = true;
}
void QV4Debugger::setBreakOnThrow(bool onoff)
{
QMutexLocker locker(&m_lock);
m_breakOnThrow = onoff;
}
void QV4Debugger::clearPauseRequest()
{
QMutexLocker locker(&m_lock);
m_pauseRequested = false;
}
+ qsizetype QV4Debugger::executableLinesCacheSize()
+ {
+ QMutexLocker locker(&m_lock);
+ return m_executableLines.size();
+ }
+
QV4Debugger::ExecutionState QV4Debugger::currentExecutionState() const
{
ExecutionState state;
state.fileName = QUrl(getFunction()->sourceFile()).fileName();
state.lineNumber = engine()->currentStackFrame->lineNumber();
return state;
}
bool QV4Debugger::pauseAtNextOpportunity() const {
- return m_pauseRequested || m_haveBreakPoints || m_gatherSources || m_stepping >= StepOver;
+ // A change of breakpoints has to reach maybeBreakAtInstruction() even when
+ // none are left, so that the cache of executable lines gets pruned.
+ return m_pauseRequested || m_haveBreakPoints || m_breakPointsChanged || m_gatherSources
+ || m_stepping >= StepOver;
}
QList<QV4::StackFrame> QV4Debugger::stackTrace(int frameLimit) const
{
return m_engine->stackTrace(frameLimit);
}
void QV4Debugger::maybeBreakAtInstruction()
{
if (m_runningJob) // do not re-enter when we're doing a job for the debugger.
return;
QMutexLocker locker(&m_lock);
if (m_gatherSources) {
m_gatherSources->run();
delete m_gatherSources;
m_gatherSources = nullptr;
}
+ if (m_breakPointsChanged) {
+ m_breakPointsChanged = false;
+ pruneExecutableLines();
+ }
+
switch (m_stepping) {
case StepOver:
if (m_currentFrame != m_engine->currentStackFrame)
break;
Q_FALLTHROUGH();
case StepIn:
pauseAndWait(Step);
return;
case StepOut:
case NotStepping:
break;
}
if (m_pauseRequested) { // Serve debugging requests from the agent
m_pauseRequested = false;
pauseAndWait(PauseRequest);
} else if (m_haveBreakPoints) {
if (QV4::Function *f = getFunction()) {
// lineNumber will be negative for Ret instructions, so those won't match
const int lineNumber = engine()->currentStackFrame->lineNumber();
- if (reallyHitTheBreakPoint(f->sourceFile(), lineNumber))
+ if (reallyHitTheBreakPoint(f, f->sourceFile(), lineNumber))
pauseAndWait(BreakPointHit);
}
}
}
void QV4Debugger::enteringFunction()
{
if (m_runningJob)
return;
QMutexLocker locker(&m_lock);
if (m_stepping == StepIn)
m_currentFrame = m_engine->currentStackFrame;
}
void QV4Debugger::leavingFunction(const QV4::ReturnedValue &retVal)
{
if (m_runningJob)
return;
Q_UNUSED(retVal); // TODO
QMutexLocker locker(&m_lock);
if (m_stepping != NotStepping && m_currentFrame == m_engine->currentStackFrame) {
m_currentFrame = m_currentFrame->parentFrame();
m_stepping = StepOver;
m_returnedValue.set(m_engine, retVal);
}
}
void QV4Debugger::aboutToThrow()
{
if (!m_breakOnThrow)
return;
if (m_runningJob) // do not re-enter when we're doing a job for the debugger.
return;
QMutexLocker locker(&m_lock);
pauseAndWait(Throwing);
}
QV4::Function *QV4Debugger::getFunction() const
{
if (m_engine->currentStackFrame)
return m_engine->currentStackFrame->v4Function;
else
return m_engine->globalCode;
}
void QV4Debugger::runJobUnpaused()
{
QMutexLocker locker(&m_lock);
if (m_runningJob)
m_runningJob->run();
m_jobIsRunning.wakeAll();
}
void QV4Debugger::pauseAndWait(PauseReason reason)
{
if (m_runningJob)
return;
m_state = Paused;
emit debuggerPaused(this, reason);
while (true) {
m_runningCondition.wait(&m_lock);
if (m_runningJob) {
m_runningJob->run();
m_jobIsRunning.wakeAll();
} else {
break;
}
}
m_state = Running;
}
- bool QV4Debugger::reallyHitTheBreakPoint(const QString &filename, int linenr)
+ /*!
+ \internal
+
+ Collects the executable lines of every function in \a unit, i.e. of the
+ whole source file. Resolution has to be file-global: a breakpoint on a
+ comment inside a nested function must not also match the outer function's
+ first line after the comment.
+ */
+ QV4Debugger::ExecutableLines QV4Debugger::collectExecutableLines(
+ const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit)
{
- const auto it = m_breakPoints.constFind(
- BreakPoint(QUrl(filename).fileName(), linenr));
- if (it == m_breakPoints.cend())
- return false;
- QString condition = it.value();
- if (condition.isEmpty())
+ ExecutableLines result;
+ const QV4::CompiledData::Unit *data = unit ? unit->unitData() : nullptr;
+ if (!data)
+ return result;
+ for (uint f = 0, fn = data->functionTableSize; f < fn; ++f) {
+ const QV4::CompiledData::Function *cf = data->functionAt(f);
+ const int start = cf->location.line();
+ result.functionStarts.append(start);
+ const QV4::CompiledData::CodeOffsetToLineAndStatement *table
+ = cf->lineAndStatementNumberTable();
+ for (uint i = 0, n = cf->nLineAndStatementNumbers; i < n; ++i) {
+ const int line = table[i].line;
+ if (line > 0) // debug instructions get negative line numbers
+ result.lines.append({line, start});
+ }
+ }
+ std::sort(result.lines.begin(), result.lines.end());
+ result.lines.erase(std::unique(result.lines.begin(), result.lines.end()),
+ result.lines.end());
+ std::sort(result.functionStarts.begin(), result.functionStarts.end());
+ return result;
+ }
+
+ int QV4Debugger::firstLineAtOrAfter(const ExecutableLines &executable, int requestedLine)
+ {
+ // Lines are sorted by line, then by the start of their function, so the
+ // first one at or after the request is from the earliest starting function.
+ const auto it = std::lower_bound(executable.lines.cbegin(), executable.lines.cend(),
+ ExecutableLine{requestedLine, 0});
+ if (it == executable.lines.cend())
+ return -1;
+ // Only move within code that starts at or before the requested line, and
+ // not past the start of another function or binding. A function starting
+ // later runs at an unrelated time, and in QML the requested line may be a
+ // declaration without code, such as an id or a literal binding. A unit may
+ // also cover only a fragment of its file (a binding compiled from a
+ // string, a chunk passed to QJSEngine::evaluate() with a line offset), and
+ // a line before its code belongs to some other unit.
+ if (it->functionStart > requestedLine)
+ return -1;
+ const QList<int> &starts = executable.functionStarts;
+ const auto start = std::upper_bound(starts.cbegin(), starts.cend(), requestedLine);
+ if (start != starts.cend() && *start < it->line)
+ return -1;
+ return it->line;
+ }
+
+ void QV4Debugger::pruneExecutableLines()
+ {
+ // Drop the units of files without breakpoints, so that the cache does not
+ // keep them from being trimmed. This runs on the engine thread, which is
+ // the only one allowed to release a unit.
+ m_executableLines.removeIf([this](const UnitLines &cached) {
+ const QString file = QUrl(cached.unit->fileName()).fileName();
+ for (auto it = m_breakPoints.keyBegin(), end = m_breakPoints.keyEnd(); it != end; ++it) {
+ if (it->fileName == file)
+ return false;
+ }
return true;
+ });
+ }
- Q_ASSERT(m_runningJob == nullptr);
- EvalJob evilJob(m_engine, condition);
- m_runningJob = &evilJob;
- m_runningJob->run();
- m_runningJob = nullptr;
+ /*!
+ \internal
- return evilJob.resultAsBoolean();
+ Returns the executable lines of the compilation unit \a function belongs
+ to, cached for the most recent units.
+ */
+ const QV4Debugger::ExecutableLines &QV4Debugger::executableLines(QV4::Function *function)
+ {
+ // Enough for execution moving back and forth between the files of a scene
+ // that all have breakpoints, few enough for a linear search.
+ constexpr qsizetype MaxCachedUnits = 8;
+
+ const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit
+ = function->executableCompilationUnit()->baseCompilationUnit();
+ for (qsizetype i = 0, n = m_executableLines.size(); i < n; ++i) {
+ if (m_executableLines.at(i).unit == unit) {
+ if (i > 0)
+ m_executableLines.move(i, 0);
+ return m_executableLines.first().lines;
+ }
+ }
+ if (m_executableLines.size() == MaxCachedUnits)
+ m_executableLines.removeLast();
+ m_executableLines.prepend({unit, collectExecutableLines(unit)});
+ return m_executableLines.first().lines;
+ }
+
+ /*!
+ \internal
+
+ While paused, returns the line a breakpoint requested on \a requestedLine
+ resolves to in the unit currently being executed, or -1. Lets the agent
+ attribute a hit that landed on a shifted line back to the breakpoint the
+ user placed on a non-executable line.
+ */
+ int QV4Debugger::resolvedBreakpointLine(int requestedLine)
+ {
+ QMutexLocker locker(&m_lock);
+ QV4::Function *f = getFunction();
+ if (!f)
+ return -1;
+ // This runs on the agent's thread, so it must not change the cache: dropping
+ // the reference to an evicted unit could destroy that unit here.
+ const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit
+ = f->executableCompilationUnit()->baseCompilationUnit();
+ for (const UnitLines &cached : std::as_const(m_executableLines)) {
+ if (cached.unit == unit)
+ return firstLineAtOrAfter(cached.lines, requestedLine);
+ }
+ return firstLineAtOrAfter(collectExecutableLines(unit), requestedLine);
+ }
+
+ bool QV4Debugger::reallyHitTheBreakPoint(QV4::Function *function, const QString &filename,
+ int linenr)
+ {
+ const QString file = QUrl(filename).fileName();
+
+ // Besides a breakpoint exactly on this line, a breakpoint requested on a
+ // non-executable line (e.g. a comment or blank line) counts here if this is
+ // the first executable line at or after it. Any of them can stop execution,
+ // so all of them have to be checked.
+ QStringList conditions;
+ for (auto bp = m_breakPoints.constBegin(), end = m_breakPoints.constEnd(); bp != end; ++bp) {
+ if (bp.key().fileName != file || bp.key().lineNumber > linenr)
+ continue;
+ // Only relocate in a unit the client's target identifies: a same-named
+ // file elsewhere has unrelated executable lines.
+ if (bp.key().lineNumber != linenr
+ && (!filename.endsWith(bp.value().target)
+ || firstLineAtOrAfter(executableLines(function), bp.key().lineNumber)
+ != linenr)) {
+ continue;
+ }
+ if (bp.value().condition.isEmpty())
+ return true;
+ conditions.append(bp.value().condition);
+ }
+
+ for (const QString &condition : std::as_const(conditions)) {
+ Q_ASSERT(m_runningJob == nullptr);
+ EvalJob evilJob(m_engine, condition);
+ m_runningJob = &evilJob;
+ m_runningJob->run();
+ m_runningJob = nullptr;
+ if (evilJob.resultAsBoolean())
+ return true;
+ }
+ return false;
}
void QV4Debugger::runInEngine(QV4DebugJob *job)
{
QMutexLocker locker(&m_lock);
runInEngine_havingLock(job);
}
void QV4Debugger::runInEngine_havingLock(QV4DebugJob *job)
{
Q_ASSERT(job);
Q_ASSERT(m_runningJob == nullptr);
m_runningJob = job;
if (state() == Paused)
m_runningCondition.wakeAll();
else
emit scheduleJob();
m_jobIsRunning.wait(&m_lock);
m_runningJob = nullptr;
}
QT_END_NAMESPACE
#include "moc_qv4debugger.cpp"