Skip to content

Commit ecf857b

Browse files
authored
Merge pull request #22 from ModbusScope/claude/route-logging-diagnostics-4Awjy
Claude/route logging diagnostics
2 parents fdf55f1 + d4b9de6 commit ecf857b

24 files changed

Lines changed: 477 additions & 56 deletions

adapters/describe.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
{
1313
"id": 0,
1414
"ip": "127.0.0.1",
15-
"persistent": false,
15+
"persistent": true,
1616
"port": 502,
1717
"timeout": 1000,
1818
"type": "tcp"
@@ -23,7 +23,7 @@
2323
"connectionId": 0,
2424
"consecutiveMax": 125,
2525
"id": 1,
26-
"int32LittleEndian": false,
26+
"int32LittleEndian": true,
2727
"slaveId": 1
2828
}
2929
],

adapters/dummymodbusadapter

23.4 KB
Binary file not shown.

adapters/dummymodbusadapter.exe

78.2 KB
Binary file not shown.

adapters/json-rpc-spec.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ Notifications are sent by the adapter to the client without a corresponding requ
338338

339339
### `adapter.diagnostic`
340340

341-
_(Reserved for future use.)_ Carries a log or diagnostic message from the adapter.
341+
Carries a log or diagnostic message from the adapter. Emitted for every Qt log message (`qDebug`, `qInfo`, `qWarning`, `qCritical`, `qFatal`) produced during adapter operation.
342342

343343
```json
344344
{
@@ -356,6 +356,7 @@ _(Reserved for future use.)_ Carries a log or diagnostic message from the adapte
356356
| `"debug"` | Verbose internal trace |
357357
| `"info"` | Informational |
358358
| `"warning"` | Non-fatal issue |
359+
| `"error"` | Critical or fatal error |
359360

360361
---
361362

adapters/modbusadapter

27.4 KB
Binary file not shown.

adapters/modbusadapter.exe

77.7 KB
Binary file not shown.

src/ProtocolAdapter/adapterclient.cpp

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
#include <QJsonArray>
66

7-
AdapterClient::AdapterClient(AdapterProcess* pProcess, QObject* parent) : QObject(parent), _pProcess(pProcess)
7+
AdapterClient::AdapterClient(AdapterProcess* pProcess, QObject* parent, int handshakeTimeoutMs)
8+
: QObject(parent), _pProcess(pProcess), _handshakeTimeoutMs(handshakeTimeoutMs)
89
{
910
Q_ASSERT(pProcess);
1011
_pProcess->setParent(this);
@@ -16,6 +17,7 @@ AdapterClient::AdapterClient(AdapterProcess* pProcess, QObject* parent) : QObjec
1617
connect(_pProcess, &AdapterProcess::errorReceived, this, &AdapterClient::onErrorReceived);
1718
connect(_pProcess, &AdapterProcess::processError, this, &AdapterClient::onProcessError);
1819
connect(_pProcess, &AdapterProcess::processFinished, this, &AdapterClient::onProcessFinished);
20+
connect(_pProcess, &AdapterProcess::notificationReceived, this, &AdapterClient::onNotificationReceived);
1921
}
2022

2123
AdapterClient::~AdapterClient() = default;
@@ -35,7 +37,7 @@ void AdapterClient::prepareAdapter(const QString& adapterPath)
3537

3638
qCInfo(scopeComm) << "AdapterClient: process started, sending initialize";
3739
_state = State::INITIALIZING;
38-
_handshakeTimer.start(cHandshakeTimeoutMs);
40+
_handshakeTimer.start(_handshakeTimeoutMs);
3941
_pProcess->sendRequest("adapter.initialize", QJsonObject());
4042
}
4143

@@ -50,7 +52,7 @@ void AdapterClient::provideConfig(QJsonObject config, QStringList registerExpres
5052
_pendingExpressions = registerExpressions;
5153
_pendingConfig = config;
5254
_state = State::CONFIGURING;
53-
_handshakeTimer.start(cHandshakeTimeoutMs);
55+
_handshakeTimer.start(_handshakeTimeoutMs);
5456
QJsonObject params;
5557
params["config"] = _pendingConfig;
5658
_pProcess->sendRequest("adapter.configure", params);
@@ -91,12 +93,13 @@ void AdapterClient::stopSession()
9193
{
9294
_state = State::STOPPING;
9395
_pProcess->sendRequest("adapter.shutdown", QJsonObject());
96+
_handshakeTimer.start(_handshakeTimeoutMs);
9497
}
9598
else
9699
{
100+
_state = State::STOPPING;
97101
_pProcess->stop();
98-
_state = State::IDLE;
99-
emit sessionStopped();
102+
/* sessionStopped is emitted from onProcessFinished once the process exits */
100103
}
101104
}
102105

@@ -111,8 +114,10 @@ void AdapterClient::onResponseReceived(int id, const QString& method, const QJso
111114
{
112115
qCWarning(scopeComm) << "AdapterClient: unexpected non-object result for" << method;
113116
_handshakeTimer.stop();
117+
/* Set IDLE before stop() so onProcessFinished's IDLE guard suppresses any
118+
duplicate sessionError emission when the process exits asynchronously. */
114119
_state = State::IDLE;
115-
QTimer::singleShot(0, this, [this]() { _pProcess->stop(); });
120+
_pProcess->stop();
116121
emit sessionError(QString("Unexpected non-object result for %1").arg(method));
117122
}
118123
}
@@ -125,8 +130,10 @@ void AdapterClient::onErrorReceived(int id, const QString& method, const QJsonOb
125130
qCWarning(scopeComm) << "AdapterClient: error for" << method << ":" << errorMsg;
126131

127132
State previousState = _state;
133+
/* Set IDLE before stop() so onProcessFinished's IDLE guard suppresses any
134+
duplicate sessionError emission when the process exits asynchronously. */
128135
_state = State::IDLE;
129-
QTimer::singleShot(0, this, [this]() { _pProcess->stop(); });
136+
_pProcess->stop();
130137

131138
if (previousState != State::STOPPING)
132139
{
@@ -137,14 +144,22 @@ void AdapterClient::onErrorReceived(int id, const QString& method, const QJsonOb
137144
void AdapterClient::onProcessError(const QString& message)
138145
{
139146
_handshakeTimer.stop();
140-
_state = State::IDLE;
141-
emit sessionError(message);
147+
if (_state != State::STOPPING)
148+
{
149+
_state = State::IDLE;
150+
emit sessionError(message);
151+
}
142152
}
143153

144154
void AdapterClient::onProcessFinished()
145155
{
146156
_handshakeTimer.stop();
147-
if (_state != State::IDLE)
157+
if (_state == State::STOPPING)
158+
{
159+
_state = State::IDLE;
160+
emit sessionStopped();
161+
}
162+
else if (_state != State::IDLE)
148163
{
149164
_state = State::IDLE;
150165
emit sessionError("Adapter process exited unexpectedly");
@@ -154,9 +169,35 @@ void AdapterClient::onProcessFinished()
154169
void AdapterClient::onHandshakeTimeout()
155170
{
156171
qCWarning(scopeComm) << "AdapterClient: handshake timed out in state" << static_cast<int>(_state);
172+
bool wasStopping = (_state == State::STOPPING);
157173
_state = State::IDLE;
158-
QTimer::singleShot(0, this, [this]() { _pProcess->stop(); });
159-
emit sessionError("Adapter handshake timed out");
174+
_pProcess->stop();
175+
if (wasStopping)
176+
{
177+
emit sessionStopped();
178+
}
179+
else
180+
{
181+
emit sessionError("Adapter handshake timed out");
182+
}
183+
}
184+
185+
void AdapterClient::onNotificationReceived(QString method, QJsonValue params)
186+
{
187+
if (method != QStringLiteral("adapter.diagnostic"))
188+
{
189+
return;
190+
}
191+
192+
if (!params.isObject())
193+
{
194+
qCWarning(scopeComm) << "AdapterClient: adapter.diagnostic params is not an object";
195+
return;
196+
}
197+
198+
QJsonObject obj = params.toObject();
199+
emit diagnosticReceived(obj.value(QStringLiteral("level")).toString(),
200+
obj.value(QStringLiteral("message")).toString());
160201
}
161202

162203
void AdapterClient::handleLifecycleResponse(const QString& method, const QJsonObject& result)
@@ -215,8 +256,7 @@ void AdapterClient::handleLifecycleResponse(const QString& method, const QJsonOb
215256
{
216257
qCInfo(scopeComm) << "AdapterClient: shutdown acknowledged";
217258
_pProcess->stop();
218-
_state = State::IDLE;
219-
emit sessionStopped();
259+
/* sessionStopped is emitted from onProcessFinished once the process exits */
220260
}
221261
else
222262
{

src/ProtocolAdapter/adapterclient.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AdapterClient : public QObject
2626
Q_OBJECT
2727

2828
public:
29-
explicit AdapterClient(AdapterProcess* pProcess, QObject* parent = nullptr);
29+
explicit AdapterClient(AdapterProcess* pProcess, QObject* parent = nullptr, int handshakeTimeoutMs = 10000);
3030
~AdapterClient();
3131

3232
/*!
@@ -71,7 +71,10 @@ class AdapterClient : public QObject
7171
void requestStatus();
7272

7373
/*!
74-
* \brief Send adapter.shutdown and terminate the adapter process.
74+
* \brief Send adapter.shutdown and signal the adapter process to stop.
75+
*
76+
* The sessionStopped() signal is emitted asynchronously once the process
77+
* has fully exited.
7578
*/
7679
void stopSession();
7780

@@ -114,6 +117,13 @@ class AdapterClient : public QObject
114117
*/
115118
void sessionStopped();
116119

120+
/*!
121+
* \brief Emitted when an adapter.diagnostic notification is received from the adapter.
122+
* \param level Severity level string: "debug", "info", or "warning".
123+
* \param message The diagnostic message from the adapter.
124+
*/
125+
void diagnosticReceived(QString level, QString message);
126+
117127
protected:
118128
enum class State
119129
{
@@ -135,6 +145,7 @@ private slots:
135145
void onProcessError(const QString& message);
136146
void onProcessFinished();
137147
void onHandshakeTimeout();
148+
void onNotificationReceived(QString method, QJsonValue params);
138149

139150
private:
140151
void handleLifecycleResponse(const QString& method, const QJsonObject& result);
@@ -143,6 +154,7 @@ private slots:
143154

144155
AdapterProcess* _pProcess;
145156
QTimer _handshakeTimer;
157+
int _handshakeTimeoutMs;
146158
QJsonObject _pendingConfig;
147159
QStringList _pendingExpressions;
148160
};

src/ProtocolAdapter/adapterprocess.cpp

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@
33
#include "util/scopelogging.h"
44

55
#include <QJsonDocument>
6+
#include <QTimer>
67

78
static constexpr int cStartTimeoutMs = 3000;
9+
static constexpr int cStopTimeoutMs = 3000;
810

911
AdapterProcess::AdapterProcess(QObject* parent) : QObject(parent)
1012
{
1113
_pProcess = new QProcess(this);
1214
_pFramingReader = new FramingReader(this);
1315

16+
_pKillTimer = new QTimer(this);
17+
_pKillTimer->setSingleShot(true);
18+
connect(_pKillTimer, &QTimer::timeout, this, [this]() {
19+
if (_pProcess->state() != QProcess::NotRunning)
20+
{
21+
_pProcess->kill();
22+
}
23+
});
24+
1425
connect(_pProcess, &QProcess::readyReadStandardOutput, this, &AdapterProcess::onReadyReadStdout);
1526
connect(_pProcess, &QProcess::readyReadStandardError, this, &AdapterProcess::onReadyReadStderr);
1627
connect(_pProcess, &QProcess::finished, this, &AdapterProcess::onProcessFinished);
@@ -25,6 +36,7 @@ bool AdapterProcess::start(const QString& path)
2536
return true;
2637
}
2738

39+
_pKillTimer->stop();
2840
_pendingMethods.clear();
2941
_nextRequestId = 1;
3042

@@ -49,11 +61,8 @@ void AdapterProcess::stop()
4961
if (_pProcess->state() != QProcess::NotRunning)
5062
{
5163
_pProcess->closeWriteChannel();
52-
if (!_pProcess->waitForFinished(3000))
53-
{
54-
_pProcess->kill();
55-
_pProcess->waitForFinished(1000);
56-
}
64+
_pKillTimer->stop();
65+
_pKillTimer->start(cStopTimeoutMs);
5766
}
5867
}
5968

@@ -100,11 +109,10 @@ bool AdapterProcess::writeFramed(const QByteArray& json)
100109
qint64 written = _pProcess->write(frame);
101110
if (written != frame.size())
102111
{
103-
emit processError(
104-
QString("Failed to write to adapter process (wrote %1 of %2 bytes, error: %3)")
105-
.arg(written)
106-
.arg(frame.size())
107-
.arg(_pProcess->errorString()));
112+
emit processError(QString("Failed to write to adapter process (wrote %1 of %2 bytes, error: %3)")
113+
.arg(written)
114+
.arg(frame.size())
115+
.arg(_pProcess->errorString()));
108116
return false;
109117
}
110118
return true;
@@ -171,5 +179,6 @@ void AdapterProcess::onProcessFinished(int exitCode, QProcess::ExitStatus exitSt
171179
{
172180
qCInfo(scopeComm) << "AdapterProcess: process finished, exit code:" << exitCode << "status:" << exitStatus;
173181
_pendingMethods.clear();
182+
_pKillTimer->stop();
174183
emit processFinished();
175184
}

src/ProtocolAdapter/adapterprocess.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <QObject>
1010
#include <QProcess>
1111
#include <QString>
12+
#include <QTimer>
1213

1314
/*!
1415
* \brief Transport layer for an external adapter process communicating via JSON-RPC 2.0 over stdio.
@@ -37,7 +38,11 @@ class AdapterProcess : public QObject
3738
virtual bool start(const QString& path);
3839

3940
/*!
40-
* \brief Kill the adapter process and wait for it to finish.
41+
* \brief Signal the adapter process to stop and return immediately.
42+
*
43+
* Closes stdin so the adapter exits cleanly. If it has not exited within
44+
* the stop timeout, it is killed. The \c processFinished signal is emitted
45+
* asynchronously when the process actually exits.
4146
*/
4247
virtual void stop();
4348

@@ -100,6 +105,7 @@ private slots:
100105
bool writeFramed(const QByteArray& json);
101106

102107
QProcess* _pProcess;
108+
QTimer* _pKillTimer;
103109
FramingReader* _pFramingReader;
104110
QMap<int, QString> _pendingMethods;
105111
int _nextRequestId{ 1 };

0 commit comments

Comments
 (0)