── Radio-side display-stream inventory / leak detector (#3856) ────────────── `get pans` can never show a radio-side leak: the client tears down its own view on the "removed" echo, so it always looks clean. This verb reports two independent radio-authoritative views: Layer A (`streams`) — VITA-49 UDP truth: streams the radio is STILL transmitting for an id we no longer own (catches continued-
| 4111 | // resource-level lingering that emits no UDP (#3843). |
| 4112 | // `streams reset` clears the Layer-A orphan tally to re-baseline a before/after. |
| 4113 | QJsonObject AutomationServer::doStreams(const QString& action) |
| 4114 | { |
| 4115 | if (!m_radioModel) |
| 4116 | return err(QStringLiteral("no radio model available")); |
| 4117 | |
| 4118 | const auto hex = [](quint32 id) { |
| 4119 | return QStringLiteral("0x") + QString::number(id, 16); |
| 4120 | }; |
| 4121 | const auto ownStr = [](DisplayInventory::Ownership o) { |
| 4122 | switch (o) { |
| 4123 | case DisplayInventory::Ownership::Ours: return QStringLiteral("ours"); |
| 4124 | case DisplayInventory::Ownership::Foreign: return QStringLiteral("foreign"); |
| 4125 | default: return QStringLiteral("orphan"); |
| 4126 | } |
| 4127 | }; |
| 4128 | |
| 4129 | // Layer B — radio-authoritative display-object inventory. |
| 4130 | if (action.compare(QLatin1String("radio"), Qt::CaseInsensitive) == 0 |
| 4131 | || action.compare(QLatin1String("inventory"), Qt::CaseInsensitive) == 0) { |
| 4132 | const DisplayInventory::Report rep = m_radioModel->displayInventoryReport(); |
| 4133 | QJsonArray pans; |
| 4134 | for (const auto& p : rep.pans) |
| 4135 | pans.append(QJsonObject{{QStringLiteral("panId"), p.id}, |
| 4136 | {QStringLiteral("clientHandle"), hex(p.clientHandle)}, |
| 4137 | {QStringLiteral("ownership"), ownStr(p.ownership)}}); |
| 4138 | QJsonArray wfs; |
| 4139 | for (const auto& w : rep.waterfalls) { |
| 4140 | QJsonObject o{{QStringLiteral("waterfallId"), w.id}, |
| 4141 | {QStringLiteral("clientHandle"), hex(w.clientHandle)}, |
| 4142 | {QStringLiteral("ownership"), ownStr(w.ownership)}, |
| 4143 | {QStringLiteral("parentMissing"), w.parentMissing}}; |
| 4144 | if (!w.parentPanId.isEmpty()) |
| 4145 | o[QStringLiteral("parentPanId")] = w.parentPanId; |
| 4146 | wfs.append(o); |
| 4147 | } |
| 4148 | QJsonArray leaked; |
| 4149 | for (const auto& id : rep.leakedWaterfalls) leaked.append(id); |
| 4150 | return QJsonObject{ |
| 4151 | {QStringLiteral("ok"), true}, |
| 4152 | {QStringLiteral("scope"), QStringLiteral("radio")}, |
| 4153 | {QStringLiteral("pans"), pans}, |
| 4154 | {QStringLiteral("waterfalls"), wfs}, |
| 4155 | {QStringLiteral("radioPanCount"), rep.pans.size()}, |
| 4156 | {QStringLiteral("radioWaterfallCount"), rep.waterfalls.size()}, |
| 4157 | {QStringLiteral("orphanPanCount"), rep.orphanPanCount}, |
| 4158 | {QStringLiteral("orphanWaterfallCount"), rep.orphanWfCount}, |
| 4159 | {QStringLiteral("foreignPanCount"), rep.foreignPanCount}, |
| 4160 | {QStringLiteral("foreignWaterfallCount"), rep.foreignWfCount}, |
| 4161 | {QStringLiteral("leakedWaterfalls"), leaked}, |
| 4162 | {QStringLiteral("leakCount"), leaked.size()}, |
| 4163 | }; |
| 4164 | } |
| 4165 | |
| 4166 | // `streams resync` — force the radio to re-dump its authoritative display |
| 4167 | // set, then re-poll `streams radio` after a moment. Closes the gap where a |
| 4168 | // waterfall lingers as a radio resource but no longer emits UDP (Layer A |
| 4169 | // can't see it) and the client already purged its view (Layer B looked |
| 4170 | // clean). The re-dump is async, so this just triggers and the driver reads |
nothing calls this directly
no test coverage detected