Skip to content

Commit 4bc16c3

Browse files
committed
fix(StateMachine): replace isRunning() with queued-start-aware inProgress()
QStateMachine::start() is queued, so isRunning() stays false until the event loop processes the start. QGCStateMachine::active() returned isRunning(), so Vehicle::isInitialConnectComplete(), which is !active(), reported a newly created vehicle as complete before AUTOPILOT_VERSION had been requested. On loaded macOS CI runners the 20 ms active-vehicle timer can fire ahead of the queued start, so VehicleTest::waitForInitialConnect() returned immediately and tests such as OnboardLogFtpDownloadTest failed on capabilitiesKnown(). This regressed in #13981: the old StateMachine set its active flag synchronously in start(). Having both active() and isRunning() with subtly different meanings is what allowed the regression, so QGCStateMachine now has a single notion: - inProgress() (property, inProgressChanged) is set in start() and cleared at the top of the stopped/finished handlers, so later handlers already see the machine as no longer in progress. - active(), the running property and runningChanged are removed. - isRunning() and setRunning() are deleted on QGCStateMachine, so using them fails to compile. - start() logs qCCritical and returns when already in progress or when there is no initial state, rather than relying on Qt's refusal rules. - Starting without QGCStateMachine::start(), e.g. through a QStateMachine* or setRunning(), is logged with qCCritical. Vehicle, ComponentInformationManager, SubMachineState and the QGCStateMachine lifecycle helpers use inProgress(). ComponentInformationManager's first progress update no longer reports 100% while its start is queued. StateMachineTest::startAndWaitForFinished() now starts QGCStateMachines through QGCStateMachine::start().
1 parent 8ca8598 commit 4bc16c3

12 files changed

Lines changed: 242 additions & 36 deletions

File tree

‎src/Utilities/StateMachine/QGCStateMachine.cc‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,24 @@ QGCStateMachine::QGCStateMachine(const QString& machineName, Vehicle *vehicle, Q
2020

2121
connect(this, &QGCStateMachine::started, this, [this] () {
2222
qCDebug(QGCStateMachineLog) << "State machine started:" << objectName();
23-
emit runningChanged();
23+
if (!_inProgress) {
24+
qCCritical(QGCStateMachineLog)
25+
<< objectName()
26+
<< "started without QGCStateMachine::start() - QStateMachine::start() or setRunning() was used";
27+
_setInProgress(true);
28+
}
2429
});
25-
connect(this, &QGCStateMachine::stopped, this, [this] () {
30+
// Cleared before anything else reacts to stopped/finished so handlers, including the final progress update,
31+
// already see the machine as no longer in progress
32+
connect(this, &QGCStateMachine::stopped, this, [this]() {
33+
_setInProgress(false);
2634
qCDebug(QGCStateMachineLog) << "State machine stopped:" << objectName();
27-
emit runningChanged();
2835
if (_deleteOnStop) {
2936
deleteLater();
3037
}
3138
});
32-
connect(this, &QGCStateMachine::finished, this, [this] () {
39+
connect(this, &QGCStateMachine::finished, this, [this]() {
40+
_setInProgress(false);
3341
qCDebug(QGCStateMachineLog) << "State machine finished:" << objectName();
3442
if (_progressTotalWeight > 0 && _progressLastEmitted < 1.0f) {
3543
_progressLastEmitted = 1.0f;
@@ -61,10 +69,27 @@ QString QGCStateMachine::currentStateName() const
6169

6270
void QGCStateMachine::start()
6371
{
64-
if (isRunning()) {
65-
qCCritical(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections";
72+
if (_inProgress) {
73+
qCCritical(QGCStateMachineLog) << objectName()
74+
<< "start() called but already in progress - check signal connections";
75+
return;
76+
}
77+
if ((childMode() == QState::ExclusiveStates) && !initialState()) {
78+
qCCritical(QGCStateMachineLog) << objectName() << "start() called without an initial state";
79+
return;
6680
}
81+
_inProgress = true;
6782
QStateMachine::start();
83+
// Emitted after Qt's start so a stop() from a listener is honored
84+
emit inProgressChanged();
85+
}
86+
87+
void QGCStateMachine::_setInProgress(bool inProgress)
88+
{
89+
if (_inProgress != inProgress) {
90+
_inProgress = inProgress;
91+
emit inProgressChanged();
92+
}
6893
}
6994

7095
void QGCStateMachine::setGlobalErrorState(QAbstractState* errorState)
@@ -330,7 +355,7 @@ bool QGCStateMachine::resetToState(QAbstractState* state)
330355
qCDebug(QGCStateMachineLog) << objectName() << "resetting to state:" << state->objectName();
331356

332357
// Stop the machine, change initial state, and restart
333-
if (isRunning()) {
358+
if (inProgress()) {
334359
auto conn = std::make_shared<QMetaObject::Connection>();
335360
*conn = connect(this, &QStateMachine::stopped, this, [this, state, conn]() {
336361
disconnect(*conn);
@@ -377,7 +402,7 @@ bool QGCStateMachine::attemptRecovery()
377402

378403
void QGCStateMachine::ensureRunning()
379404
{
380-
if (!isRunning()) {
405+
if (!inProgress()) {
381406
start();
382407
}
383408
}
@@ -390,7 +415,7 @@ void QGCStateMachine::stopMachine(bool deleteOnStop)
390415

391416
void QGCStateMachine::restart()
392417
{
393-
if (isRunning()) {
418+
if (inProgress()) {
394419
// stop() is asynchronous, so connect to stopped signal to start again
395420
auto conn = std::make_shared<QMetaObject::Connection>();
396421
*conn = connect(this, &QStateMachine::stopped, this, [this, conn]() {
@@ -490,7 +515,7 @@ QString QGCStateMachine::dumpCurrentState() const
490515
{
491516
QString result = objectName() + QStringLiteral(": ");
492517

493-
if (!isRunning()) {
518+
if (!inProgress()) {
494519
result += QStringLiteral("(not running)");
495520
return result;
496521
}

‎src/Utilities/StateMachine/QGCStateMachine.h‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ class QGCStateMachine : public QStateMachine
7070
/// Current progress from 0.0 to 1.0 (for QML binding)
7171
Q_PROPERTY(float progress READ progress NOTIFY progressUpdate)
7272

73-
/// Whether the machine is currently running (for QML binding)
74-
Q_PROPERTY(bool running READ isRunning NOTIFY runningChanged)
73+
/// Whether the machine is in progress (for QML binding)
74+
Q_PROPERTY(bool inProgress READ inProgress NOTIFY inProgressChanged)
7575

7676
/// List of recent state names for history display (for QML binding)
7777
Q_PROPERTY(QStringList stateHistory READ stateHistory NOTIFY stateHistoryChanged)
@@ -91,9 +91,15 @@ class QGCStateMachine : public QStateMachine
9191
StateContext& context() { return _context; }
9292
const StateContext& context() const { return _context; }
9393

94-
/// @return true if the state machine is currently running
95-
/// Compatibility method matching old StateMachine::active()
96-
bool active() const { return isRunning(); }
94+
/// @return true from start() until the machine emits stopped or finished
95+
/// Unlike QStateMachine::isRunning(), this is already true before Qt processes the queued start.
96+
bool inProgress() const { return _inProgress; }
97+
98+
/// Use inProgress() instead: isRunning() is false until Qt processes the queued start
99+
bool isRunning() const = delete;
100+
101+
/// Use start() and stop() instead: setRunning() calls QStateMachine::start() and bypasses start()
102+
void setRunning(bool running) = delete;
97103

98104
/// Get the name of the currently active state
99105
/// @return State name or empty string if not running
@@ -370,7 +376,7 @@ class QGCStateMachine : public QStateMachine
370376
// Lifecycle Helpers
371377
// -------------------------------------------------------------------------
372378

373-
/// Start the machine if not already running
379+
/// Start the machine if not already in progress
374380
void ensureRunning();
375381

376382
/// Stop the machine and optionally delete it
@@ -685,8 +691,8 @@ public slots:
685691
/// Emitted when the current state changes (for QML binding)
686692
void currentStateNameChanged();
687693

688-
/// Emitted when the running state changes (for QML binding)
689-
void runningChanged();
694+
/// Emitted when inProgress() changes (for QML binding)
695+
void inProgressChanged();
690696

691697
/// Emitted when the state history changes (for QML binding)
692698
void stateHistoryChanged();
@@ -705,12 +711,14 @@ public slots:
705711

706712
private:
707713
void _onStateEntered();
714+
void _setInProgress(bool inProgress);
708715
float _calculateProgress() const;
709716

710717
Vehicle* _vehicle = nullptr;
711718
QAbstractState* _globalErrorState = nullptr;
712719
QAbstractState* _recoveryState = nullptr;
713720
bool _deleteOnStop = false;
721+
bool _inProgress = false;
714722
EntryCallback _entryCallback;
715723
ExitCallback _exitCallback;
716724
EventHandler _eventHandler;

‎src/Utilities/StateMachine/States/SubMachineState.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ void SubMachineState::onEntry(QEvent* event)
3535

3636
void SubMachineState::onExit(QEvent* event)
3737
{
38-
if (_childMachine && _childMachine->isRunning()) {
38+
if (_childMachine && _childMachine->inProgress()) {
3939
qCDebug(QGCStateMachineLog) << stateName() << "stopping child machine on exit";
4040
_childMachine->stop();
4141
}

‎src/Vehicle/ComponentInformation/ComponentInformationManager.cc‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ void ComponentInformationManager::_updateProgress()
166166

167167
float ComponentInformationManager::progress() const
168168
{
169-
if (!isRunning()) {
169+
if (!inProgress()) {
170170
return 1.0f;
171171
}
172172
return static_cast<float>(_currentStateIndex) / static_cast<float>(_stateCount);
@@ -181,7 +181,8 @@ void ComponentInformationManager::requestAllComponentInformation(RequestAllCompl
181181

182182
// Guard against double-start: a request while already running just updates the
183183
// callback pointers; the running machine still emits requestAllComplete at the end.
184-
if (!isRunning()) {
184+
if (!inProgress()) {
185+
_currentStateIndex = 0;
185186
start();
186187
}
187188
emit progressUpdate(progress());

‎src/Vehicle/Vehicle.cc‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes
711711
case MAVLINK_MSG_ID_AVAILABLE_MODES_MONITOR:
712712
{
713713
// Avoid duplicate requests during initial connection setup
714-
if (!_initialConnectStateMachine || !_initialConnectStateMachine->active()) {
714+
if (!_initialConnectStateMachine || !_initialConnectStateMachine->inProgress()) {
715715
mavlink_available_modes_monitor_t availableModesMonitor;
716716
mavlink_msg_available_modes_monitor_decode(&message, &availableModesMonitor);
717717
_standardModes->availableModesMonitorReceived(availableModesMonitor.seq);
@@ -1655,10 +1655,10 @@ void Vehicle::_updateFlightTime()
16551655

16561656
void Vehicle::_gotProgressUpdate(float progressValue)
16571657
{
1658-
if (sender() != _initialConnectStateMachine && _initialConnectStateMachine->active()) {
1658+
if (sender() != _initialConnectStateMachine && _initialConnectStateMachine->inProgress()) {
16591659
return;
16601660
}
1661-
if (sender() == _initialConnectStateMachine && !_initialConnectStateMachine->active()) {
1661+
if (sender() == _initialConnectStateMachine && !_initialConnectStateMachine->inProgress()) {
16621662
progressValue = 0.f;
16631663
}
16641664
_loadProgress = progressValue;
@@ -2872,7 +2872,7 @@ QString Vehicle::_formatMavCommand(MAV_CMD command, float param1)
28722872

28732873
bool Vehicle::isInitialConnectComplete() const
28742874
{
2875-
return !_initialConnectStateMachine->active();
2875+
return !_initialConnectStateMachine->inProgress();
28762876
}
28772877

28782878
void Vehicle::_initializeCsv()

‎test/UnitTestFramework/BaseClasses/StateMachineTest.cc‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <QtStateMachine/QStateMachine>
55
#include <QtTest/QSignalSpy>
66

7+
#include "QGCStateMachine.h"
8+
79
void StateMachineTest::ignoreTimeoutWarnings()
810
{
911
ignoreLogMessage("Utilities.QGCStateMachine", QtWarningMsg, QRegularExpression(QStringLiteral("^Timeout \"")));
@@ -13,7 +15,11 @@ void StateMachineTest::ignoreTimeoutWarnings()
1315
bool StateMachineTest::startAndWaitForFinished(QStateMachine* machine, int timeoutMs)
1416
{
1517
QSignalSpy finishedSpy(machine, &QStateMachine::finished);
16-
machine->start();
18+
if (auto* qgcMachine = qobject_cast<QGCStateMachine*>(machine)) {
19+
qgcMachine->start();
20+
} else {
21+
machine->start();
22+
}
1723
return spyTriggered(finishedSpy, timeoutMs);
1824
}
1925

‎test/Utilities/StateMachine/QGCStateMachineTest.cc‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,138 @@ void QGCStateMachineTest::_testLocalErrorState()
157157
QVERIFY(localErrorHandled); // Local error should be called
158158
}
159159

160+
void QGCStateMachineTest::_testInProgressSpansQueuedStart_data()
161+
{
162+
QTest::addColumn<bool>("hasInitialState");
163+
QTest::addColumn<bool>("stopBeforeStartRuns");
164+
165+
QTest::newRow("runsToFinish") << true << false;
166+
QTest::newRow("stoppedBeforeStartRuns") << true << true;
167+
QTest::newRow("refusedWithoutInitialState") << false << false;
168+
}
169+
170+
void QGCStateMachineTest::_testInProgressSpansQueuedStart()
171+
{
172+
QFETCH(bool, hasInitialState);
173+
QFETCH(bool, stopBeforeStartRuns);
174+
175+
QGCStateMachine machine(QStringLiteral("InProgressSpansQueuedStartTest"), nullptr);
176+
auto* state = new QGCState(QStringLiteral("State"), &machine);
177+
auto* finalState = machine.addFinalState();
178+
machine.addEventTransition(state, QStringLiteral("finish"), finalState);
179+
QSignalSpy inProgressSpy(&machine, &QGCStateMachine::inProgressChanged);
180+
181+
if (!hasInitialState) {
182+
expectLogMessage("Utilities.QGCStateMachine", QtCriticalMsg,
183+
QRegularExpression(QStringLiteral("start\\(\\) called without an initial state")));
184+
machine.start();
185+
verifyExpectedLogMessage();
186+
QVERIFY(!machine.inProgress());
187+
QCOMPARE(inProgressSpy.count(), 0);
188+
return;
189+
}
190+
191+
machine.setInitialState(state);
192+
QSignalSpy startedSpy(&machine, &QStateMachine::started);
193+
QSignalSpy stoppedSpy(&machine, &QStateMachine::stopped);
194+
QSignalSpy finishedSpy(&machine, &QStateMachine::finished);
195+
196+
// Listeners must already see the machine as not in progress, e.g. to restart it or treat it as complete
197+
int listenerCalls = 0;
198+
bool inProgressInListener = true;
199+
const auto recordInProgress = [&machine, &listenerCalls, &inProgressInListener]() {
200+
listenerCalls++;
201+
inProgressInListener = machine.inProgress();
202+
};
203+
(void) connect(&machine, &QStateMachine::stopped, this, recordInProgress);
204+
(void) connect(&machine, &QStateMachine::finished, this, recordInProgress);
205+
206+
machine.start();
207+
QVERIFY(!machine.QStateMachine::isRunning());
208+
QVERIFY(machine.inProgress());
209+
QCOMPARE(inProgressSpy.count(), 1);
210+
211+
if (stopBeforeStartRuns) {
212+
machine.stop();
213+
QVERIFY(machine.inProgress());
214+
} else {
215+
QVERIFY(spyTriggered(startedSpy));
216+
QVERIFY(machine.inProgress());
217+
machine.postEvent(QStringLiteral("finish"));
218+
}
219+
220+
QVERIFY(spyTriggered(stopBeforeStartRuns ? stoppedSpy : finishedSpy));
221+
QCOMPARE(listenerCalls, 1);
222+
QVERIFY(!inProgressInListener);
223+
QVERIFY(!machine.inProgress());
224+
QCOMPARE(inProgressSpy.count(), 2);
225+
}
226+
227+
void QGCStateMachineTest::_testStopFromInProgressListenerIsHonored()
228+
{
229+
QGCStateMachine machine(QStringLiteral("StopFromInProgressListenerTest"), nullptr);
230+
auto* state = new QGCState(QStringLiteral("State"), &machine);
231+
machine.setInitialState(state);
232+
QSignalSpy stoppedSpy(&machine, &QStateMachine::stopped);
233+
234+
(void) connect(&machine, &QGCStateMachine::inProgressChanged, this, [&machine]() {
235+
if (machine.inProgress()) {
236+
machine.stop();
237+
}
238+
});
239+
240+
machine.start();
241+
242+
QVERIFY(spyTriggered(stoppedSpy));
243+
QVERIFY(!machine.inProgress());
244+
}
245+
246+
void QGCStateMachineTest::_testStartWhileInProgressIsCritical()
247+
{
248+
QGCStateMachine machine(QStringLiteral("StartWhileInProgressTest"), nullptr);
249+
auto* state = new QGCState(QStringLiteral("State"), &machine);
250+
auto* finalState = machine.addFinalState();
251+
machine.addEventTransition(state, QStringLiteral("finish"), finalState);
252+
machine.setInitialState(state);
253+
QSignalSpy startedSpy(&machine, &QStateMachine::started);
254+
QSignalSpy finishedSpy(&machine, &QStateMachine::finished);
255+
256+
machine.start();
257+
expectLogMessage("Utilities.QGCStateMachine", QtCriticalMsg,
258+
QRegularExpression(QStringLiteral("already in progress")));
259+
machine.start();
260+
verifyExpectedLogMessage();
261+
262+
QVERIFY(spyTriggered(startedSpy));
263+
machine.postEvent(QStringLiteral("finish"));
264+
QVERIFY(spyTriggered(finishedSpy));
265+
QCOMPARE(startedSpy.count(), 1);
266+
QVERIFY(!machine.inProgress());
267+
}
268+
269+
void QGCStateMachineTest::_testStartBypassIsCritical()
270+
{
271+
QGCStateMachine machine(QStringLiteral("StartBypassTest"), nullptr);
272+
auto* state = new QGCState(QStringLiteral("State"), &machine);
273+
auto* finalState = machine.addFinalState();
274+
machine.addEventTransition(state, QStringLiteral("finish"), finalState);
275+
machine.setInitialState(state);
276+
QSignalSpy startedSpy(&machine, &QStateMachine::started);
277+
QSignalSpy finishedSpy(&machine, &QStateMachine::finished);
278+
279+
expectLogMessage("Utilities.QGCStateMachine", QtCriticalMsg,
280+
QRegularExpression(QStringLiteral("started without QGCStateMachine::start\\(\\)")));
281+
static_cast<QStateMachine&>(machine).start();
282+
QVERIFY(!machine.inProgress());
283+
QVERIFY(spyTriggered(startedSpy));
284+
verifyExpectedLogMessage();
285+
QVERIFY(machine.inProgress());
286+
287+
machine.postEvent(QStringLiteral("finish"));
288+
QVERIFY(spyTriggered(finishedSpy));
289+
QVERIFY(!machine.inProgress());
290+
}
291+
160292
void QGCStateMachineTest::_testPropertyAssignment()
161293
{
162294
QGCStateMachine machine(QStringLiteral("PropTest"), nullptr);

‎test/Utilities/StateMachine/QGCStateMachineTest.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ private slots:
1212
void _testGlobalErrorState();
1313
void _testGlobalErrorStateWithAbstractState();
1414
void _testLocalErrorState();
15+
void _testInProgressSpansQueuedStart_data();
16+
void _testInProgressSpansQueuedStart();
17+
void _testStopFromInProgressListenerIsHonored();
18+
void _testStartWhileInProgressIsCritical();
19+
void _testStartBypassIsCritical();
1520

1621
// Property assignment
1722
void _testPropertyAssignment();

0 commit comments

Comments
 (0)