App commands and --ha-remove (iteration 2, step 6)
The platform accepts app commands (D43): flags that get a QCoreApplication like the manifest commands and run prepare() and then the new hook runCommand(). The app's --ha-remove clears the device's retained topics, so HA deletes the device; uninstall.sh runs it after stopping the app and before unregistering from SteamVR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,19 +112,21 @@ into Qt events.
|
||||
- `src/app/DashboardApplication` is the **base class of every app** (in `vrdashboard_core`):
|
||||
CLI, logging, SteamVR connection, backend selection, surfaces, controller navigation, and the
|
||||
modes (overlay, `--desktop`, `--desktop-novr`, `--snapshot`, `--benchmark`, manifest
|
||||
install).
|
||||
install, app commands).
|
||||
- **Inheritance for the app, components for the surfaces** (the user's decision, after
|
||||
trying composition): the app derives from `DashboardApplication`. In its constructor it
|
||||
configures (`addSurface`, optional `setAppKey`/`setDisplayName`) and passes its own
|
||||
command-line options (`QCommandLineOption` list) to the base constructor. It overrides
|
||||
the hooks `prepare(QString*)` (first; mode and app key known, no engine/surfaces/SteamVR
|
||||
yet; returning false ends `exec()` with exit code 1), `loading()` (backends + shared
|
||||
engine exist, QML not loaded) and `started()` (everything runs). In the hooks, protected
|
||||
command-line options and its commands (two `QCommandLineOption` lists, D38, D43) to the
|
||||
base constructor. It overrides the hooks `prepare(QString*)` (first; mode and app key
|
||||
known, no engine/surfaces/SteamVR yet; returning false ends `exec()` with exit code 1),
|
||||
`loading()` (backends + shared engine exist, QML not loaded), `started()` (everything
|
||||
runs) and `runCommand(name)` (mode `Command`, after `prepare()`, nothing else exists). In the hooks, protected
|
||||
accessors give the running state: `mode()`, `appKey()`, `appName()`, `isOptionSet()`,
|
||||
`optionValue()`, `engine()`, `surfaces()`, `surface()` (primary), `surface(id)`,
|
||||
`vrRuntime()`, `navigation()`. The app is `src/Dashboard.h/.cpp`.
|
||||
- The base constructor creates the `QGuiApplication` (`QCoreApplication` for manifest
|
||||
commands), so derived members can be QObjects with timers.
|
||||
- The base constructor creates the `QGuiApplication` (`QCoreApplication` for manifest and
|
||||
app commands: decided from the raw `argv` before parsing), so derived members can be
|
||||
QObjects with timers.
|
||||
- All QML and surfaces are torn down **at the end of `exec()`**, while the derived object
|
||||
and its members (e.g. QML singletons) still exist.
|
||||
- `src/app/Surface` is **one view**: its QML root (from the app's module), renderer, render
|
||||
|
||||
@@ -135,6 +135,7 @@ How the pieces fit together: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||||
| `--max-fps <fps>` | Frame-rate cap (default 60); rendering only happens on change |
|
||||
| `--install-manifest [--launch]` | Register with SteamVR (auto-start) and optionally start it now; works over SSH |
|
||||
| `--uninstall-manifest` | Unregister |
|
||||
| `--ha-remove` | Remove this device from Home Assistant (clears its retained MQTT topics; needs the settings and the broker) and exit; `uninstall.sh` calls it. Works over SSH |
|
||||
| `--manifest <path>` | Manifest for the two options above (default: the app's own) |
|
||||
| `--log-file <path>` | Also log to a file (relative paths: relative to the install folder) |
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ Your app derives from it:
|
||||
`setDisplayName()`, `setQuickStyle()`, `setQmlModule()`). The app's own command-line
|
||||
options go to the base constructor as a list of `QCommandLineOption`s; they are parsed and
|
||||
listed in `--help` with the built-in ones, and read with `isOptionSet()`/`optionValue()`.
|
||||
Its commands (flags such as `--ha-remove`) go there as a second list: like the manifest
|
||||
commands they get a `QCoreApplication` (no display needed, works over SSH), and `exec()`
|
||||
runs `prepare()` and then `runCommand(name)` in mode `Command` instead of a mode.
|
||||
- **Hooks:**
|
||||
- `prepare(QString* error)`: first, once mode and app key are known, before the QML
|
||||
engine, the surfaces and SteamVR exist (not for manifest commands). Returning `false`
|
||||
@@ -48,10 +51,13 @@ Your app derives from it:
|
||||
- `loading()`: the shared QML engine, the surfaces and their render backends exist; QML
|
||||
isn't loaded yet. Set up what the QML needs here.
|
||||
- `started()`: everything runs. Connect to surfaces and input here.
|
||||
- `runCommand(name)`: one of the app's commands, after `prepare()`; no QML engine,
|
||||
surfaces or SteamVR. Its result is the exit code.
|
||||
- **Accessors:** `mode()`, `appKey()`, `appName()`, `isOptionSet()`, `optionValue()`,
|
||||
`engine()`, `surfaces()`, `surface()` (the first), `surface(id)`, `vrRuntime()`,
|
||||
`navigation()`.
|
||||
- **Lifetime:** the base constructor creates the `QGuiApplication`, so your members can be
|
||||
- **Lifetime:** the base constructor creates the `QGuiApplication` (`QCoreApplication` for
|
||||
commands), so your members can be
|
||||
QObjects with timers. All QML and surfaces are torn down at the end of `exec()`, while your
|
||||
object and its members still exist.
|
||||
|
||||
|
||||
+7
-1
@@ -385,6 +385,8 @@ a `QGuiApplication` (no display needed, works over SSH), and `exec()` calls
|
||||
- `exec()`: after the manifest check, an app command runs `prepare()` and then a new hook
|
||||
`virtual int runCommand(const QString& name)`, whose result is the exit code. No engine,
|
||||
surfaces or SteamVR.
|
||||
- The mode is `DashboardMode::Command` (so `prepare()` can tell); the name of the first app
|
||||
command given goes to `runCommand()`. Tested in `tst_surfaces` (`surfaces_command`).
|
||||
- Moving the manifest commands onto this mechanism is possible later, not part of iteration 2.
|
||||
|
||||
### `--ha-remove`
|
||||
@@ -396,7 +398,11 @@ delete).
|
||||
- An app command (D43): loads the settings, connects, publishes **empty retained payloads**
|
||||
to discovery (HA then deletes the device and its entities), device availability, and each
|
||||
entity's state and availability topics; disconnects.
|
||||
- Exit code 0 on success; ≠ 0 with a warning if the broker can't be reached within 10 s.
|
||||
- Its client uses the client id `frameassistant_<device id>_remove` (doesn't knock a running
|
||||
instance off the broker) and no will. The `HaDevice` is created only after the connection
|
||||
is up: a device that sees the connection come up publishes itself first.
|
||||
- Exit code 0 on success; 1 with an error if the broker can't be reached within 10 s, or the
|
||||
removal isn't acknowledged within 10 s.
|
||||
- `uninstall.sh`: `pkill` → `sleep 1` → `--ha-remove` (a failure doesn't stop the uninstall) →
|
||||
`--uninstall-manifest`.
|
||||
- Also usable by hand, e.g. after a device name change without `ha_unique_id`:
|
||||
|
||||
+8
-1
@@ -25,7 +25,8 @@ CLAUDE.md ("Working rules").
|
||||
- **Implementation** follows the order in DESIGN.md (steps 1–7). Steps 1 (libmosquitto in
|
||||
CMake and the containers), 2 (`MqttClient`, `MosquittoClient`, `tst_mqtt`) 3
|
||||
(`HaEntity`, `HaSensor`, `HaDevice`, unit tests) and 4 (app wiring, `--desktop-novr`,
|
||||
UI-1) and 5 (TLS) done; next: step 6 (app commands, `--ha-remove`, `uninstall.sh`).
|
||||
UI-1) 5 (TLS) and 6 (app commands, `--ha-remove`, `uninstall.sh`) done; next: step 7
|
||||
(packaging check, docs).
|
||||
|
||||
## Next steps
|
||||
|
||||
@@ -178,6 +179,12 @@ Known limitation: the bundled-Qt package does not support WebEngine in this iter
|
||||
validation messages.
|
||||
- Still open: which CA store the Frame's OpenSSL uses by default (for `ca_file` empty);
|
||||
check on the device.
|
||||
- Step 6: app commands in the platform (D43: second option list, `QCoreApplication`, mode
|
||||
`Command`, hook `runCommand()`; `tst_surfaces` `surfaces_command`); `--ha-remove` in the
|
||||
app; `uninstall.sh` stops the app, runs `--ha-remove` (failure doesn't stop it) and
|
||||
unregisters. Checked end to end against a local mosquitto, without a display: retained
|
||||
discovery, availability, state and entity availability before, none after (exit 0);
|
||||
broker down → exit 1 after 10 s; missing settings → exit 1.
|
||||
|
||||
### 2026-10-05: requirements approved
|
||||
|
||||
|
||||
@@ -64,9 +64,14 @@ EOF
|
||||
|
||||
cat > "$out/uninstall.sh" <<EOF
|
||||
#!/bin/sh
|
||||
# Unregisters $name from SteamVR and stops it. The folder can be deleted afterwards.
|
||||
# Stops $name, removes its device from Home Assistant (clears its retained MQTT
|
||||
# topics) and unregisters it from SteamVR. The folder can be deleted afterwards.
|
||||
cd "\$(dirname "\$0")" || exit 1
|
||||
pkill -f "/bin/$exe( |\\$)" 2>/dev/null
|
||||
mkdir -p logs
|
||||
# Stopped first, so it can't publish itself again; the broker sends its last will at once.
|
||||
pkill -f "/bin/$exe( |\\$)" 2>/dev/null && sleep 1
|
||||
./bin/$exe --ha-remove --log-file logs/install.log ||
|
||||
echo "Could not remove the device from Home Assistant (see logs/install.log); unregistering anyway." >&2
|
||||
exec ./bin/$exe --uninstall-manifest --log-file logs/install.log
|
||||
EOF
|
||||
chmod +x "$out/install.sh" "$out/uninstall.sh"
|
||||
@@ -92,7 +97,8 @@ chmod +x "$out/install.sh" "$out/uninstall.sh"
|
||||
echo "Install / update: ./install.sh (SteamVR must be running)"
|
||||
echo " SteamVR remembers this folder's location: after moving it,"
|
||||
echo " run ./install.sh again."
|
||||
echo "Uninstall: ./uninstall.sh"
|
||||
echo "Uninstall: ./uninstall.sh (also removes the device from Home Assistant;"
|
||||
echo " needs the settings file and the MQTT broker)"
|
||||
echo "Logs: logs/$exe.log (the app), logs/install.log (install.sh);"
|
||||
echo " run ./bin/$exe --help for options"
|
||||
echo
|
||||
|
||||
+77
-19
@@ -3,6 +3,8 @@
|
||||
#include <QCommandLineOption>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
|
||||
#include "ha/HaDevice.h"
|
||||
#include "ha/HaSensor.h"
|
||||
@@ -11,10 +13,13 @@
|
||||
namespace {
|
||||
|
||||
const QString kConfigOption = QStringLiteral("config");
|
||||
const QString kHaRemoveCommand = QStringLiteral("ha-remove");
|
||||
// How often the dummy sensor's value is updated.
|
||||
constexpr int kUptimeIntervalMs = 10000;
|
||||
// How long a normal exit waits for the broker to acknowledge "offline".
|
||||
constexpr int kDisconnectTimeoutMs = 2000;
|
||||
// --ha-remove: how long it may take to connect, and then to publish.
|
||||
constexpr int kRemoveTimeoutMs = 10000;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -24,7 +29,11 @@ Dashboard::Dashboard(int& argc, char** argv)
|
||||
{QCommandLineOption(kConfigOption,
|
||||
QStringLiteral("Settings file (default: %1).")
|
||||
.arg(Config::defaultPath()),
|
||||
QStringLiteral("path"))}) {
|
||||
QStringLiteral("path"))},
|
||||
{QCommandLineOption(
|
||||
kHaRemoveCommand,
|
||||
QStringLiteral("Remove this device from Home Assistant (clears "
|
||||
"its retained MQTT topics) and exit."))}) {
|
||||
addSurface({.qmlRootType = QStringLiteral("Main")});
|
||||
}
|
||||
|
||||
@@ -33,7 +42,7 @@ Dashboard::~Dashboard() = default;
|
||||
bool Dashboard::prepare(QString* error) {
|
||||
// Snapshot and benchmark only render the UI: no settings needed.
|
||||
if (mode() != DashboardMode::Overlay && mode() != DashboardMode::Desktop &&
|
||||
mode() != DashboardMode::DesktopNoVr)
|
||||
mode() != DashboardMode::DesktopNoVr && mode() != DashboardMode::Command)
|
||||
return true;
|
||||
const QString path = isOptionSet(kConfigOption) ? optionValue(kConfigOption)
|
||||
: Config::defaultPath();
|
||||
@@ -46,30 +55,38 @@ bool Dashboard::prepare(QString* error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Dashboard::loading() {
|
||||
if (!m_config) return;
|
||||
const Config& config = *m_config;
|
||||
MosquittoClient::Settings Dashboard::mqttSettings() const {
|
||||
MosquittoClient::Settings settings;
|
||||
settings.broker = config.mqtt;
|
||||
settings.clientId = QStringLiteral("frameassistant_") + config.deviceId;
|
||||
settings.will = HaDevice::lastWill(config.deviceId);
|
||||
m_mqtt = std::make_unique<MosquittoClient>(settings);
|
||||
settings.broker = m_config->mqtt;
|
||||
settings.clientId = QStringLiteral("frameassistant_") + m_config->deviceId;
|
||||
settings.will = HaDevice::lastWill(m_config->deviceId);
|
||||
return settings;
|
||||
}
|
||||
|
||||
m_device = std::make_unique<HaDevice>(
|
||||
HaDevice::Info{.name = config.deviceName,
|
||||
.id = config.deviceId,
|
||||
.discoveryPrefix = config.discoveryPrefix,
|
||||
.softwareVersion = QCoreApplication::applicationVersion()},
|
||||
m_mqtt.get());
|
||||
m_uptime = m_device->addSensor({.id = QStringLiteral("uptime"),
|
||||
HaDevice::Info Dashboard::deviceInfo() const {
|
||||
return {.name = m_config->deviceName,
|
||||
.id = m_config->deviceId,
|
||||
.discoveryPrefix = m_config->discoveryPrefix,
|
||||
.softwareVersion = QCoreApplication::applicationVersion()};
|
||||
}
|
||||
|
||||
HaSensor* Dashboard::addEntities(HaDevice& device) {
|
||||
return device.addSensor({.id = QStringLiteral("uptime"),
|
||||
.name = QStringLiteral("Uptime"),
|
||||
.deviceClass = QStringLiteral("duration"),
|
||||
.unit = QStringLiteral("s"),
|
||||
.category = QStringLiteral("diagnostic")});
|
||||
}
|
||||
|
||||
m_status.setDevice(
|
||||
config.deviceName,
|
||||
QStringLiteral("%1:%2").arg(config.mqtt.host).arg(config.mqtt.port));
|
||||
void Dashboard::loading() {
|
||||
if (!m_config) return;
|
||||
m_mqtt = std::make_unique<MosquittoClient>(mqttSettings());
|
||||
m_device = std::make_unique<HaDevice>(deviceInfo(), m_mqtt.get());
|
||||
m_uptime = addEntities(*m_device);
|
||||
|
||||
const MqttConfig& broker = m_config->mqtt;
|
||||
m_status.setDevice(m_config->deviceName,
|
||||
QStringLiteral("%1:%2").arg(broker.host).arg(broker.port));
|
||||
connect(m_mqtt.get(), &MqttClient::connectionChanged, &m_status,
|
||||
&ConnectionStatus::setConnection);
|
||||
}
|
||||
@@ -91,3 +108,44 @@ void Dashboard::started() {
|
||||
});
|
||||
m_mqtt->connectToBroker();
|
||||
}
|
||||
|
||||
int Dashboard::runCommand(const QString& name) {
|
||||
if (name == kHaRemoveCommand) return removeFromHa();
|
||||
return DashboardApplication::runCommand(name);
|
||||
}
|
||||
|
||||
int Dashboard::removeFromHa() {
|
||||
MosquittoClient::Settings settings = mqttSettings();
|
||||
// Its own client id, so it doesn't knock a running instance off the
|
||||
// broker; no will, since it publishes nothing to keep.
|
||||
settings.clientId += QStringLiteral("_remove");
|
||||
settings.will = {};
|
||||
MosquittoClient client(settings);
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(kRemoveTimeoutMs, &loop, &QEventLoop::quit);
|
||||
connect(&client, &MqttClient::connectionChanged, &loop,
|
||||
[&loop](MqttClient::State state) {
|
||||
if (state == MqttClient::State::Connected) loop.quit();
|
||||
});
|
||||
client.connectToBroker();
|
||||
loop.exec();
|
||||
if (client.state() != MqttClient::State::Connected) {
|
||||
qCritical().noquote() << "Cannot remove the device from Home Assistant: "
|
||||
"no connection to the broker";
|
||||
return 1;
|
||||
}
|
||||
// Created only now: a device that saw the connection come up would
|
||||
// publish itself first.
|
||||
HaDevice device(deviceInfo(), &client);
|
||||
addEntities(device);
|
||||
const bool published = device.removeFromHa();
|
||||
const bool acknowledged = client.disconnectFromBroker(kRemoveTimeoutMs);
|
||||
if (!published || !acknowledged) {
|
||||
qCritical().noquote() << "Removing the device from Home Assistant failed";
|
||||
return 1;
|
||||
}
|
||||
qInfo().noquote() << "Removed device" << m_config->deviceName
|
||||
<< "(" + m_config->deviceId + ") from Home Assistant";
|
||||
return 0;
|
||||
}
|
||||
|
||||
+12
-2
@@ -7,10 +7,10 @@
|
||||
#include "ConnectionStatus.h"
|
||||
#include "app/DashboardApplication.h"
|
||||
#include "config/Config.h"
|
||||
#include "ha/HaDevice.h"
|
||||
#include "mqtt/MosquittoClient.h"
|
||||
|
||||
class HaDevice;
|
||||
class HaSensor;
|
||||
class MosquittoClient;
|
||||
|
||||
// The dashboard app. Its UI is the QML module set up in src/CMakeLists.txt
|
||||
// (QML_URI FrameAssistant, files in src/qml/).
|
||||
@@ -30,8 +30,18 @@ class Dashboard : public DashboardApplication {
|
||||
void loading() override;
|
||||
// Connects to the broker and starts the sensors.
|
||||
void started() override;
|
||||
// --ha-remove (docs/DESIGN.md, "--ha-remove").
|
||||
int runCommand(const QString& name) override;
|
||||
|
||||
private:
|
||||
// The settings for the MQTT client and the HA device.
|
||||
MosquittoClient::Settings mqttSettings() const;
|
||||
HaDevice::Info deviceInfo() const;
|
||||
// Adds the entities to `device` (the same for the app and --ha-remove);
|
||||
// returns the dummy sensor.
|
||||
static HaSensor* addEntities(HaDevice& device);
|
||||
int removeFromHa();
|
||||
|
||||
std::optional<Config> m_config; // none in snapshot/benchmark mode
|
||||
// The device uses the client: declared after it, so destroyed before it.
|
||||
std::unique_ptr<MosquittoClient> m_mqtt;
|
||||
|
||||
@@ -115,6 +115,7 @@ std::unique_ptr<RenderBackend> startBackend(GraphicsApi api, bool apiExplicit,
|
||||
struct DashboardApplication::Options {
|
||||
QString error; // set: exec() reports it and fails
|
||||
bool manifestCommand = false;
|
||||
QString appCommand; // name of the app command given, if any
|
||||
bool install = false;
|
||||
bool launch = false;
|
||||
QString manifest;
|
||||
@@ -144,6 +145,8 @@ const char* dashboardModeName(DashboardMode mode) {
|
||||
return "snapshot";
|
||||
case DashboardMode::Benchmark:
|
||||
return "benchmark";
|
||||
case DashboardMode::Command:
|
||||
return "command";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
@@ -170,7 +173,8 @@ void DashboardApplication::setDefaultQmlModule(const char* uri) {
|
||||
}
|
||||
|
||||
DashboardApplication::DashboardApplication(
|
||||
int& argc, char** argv, const QList<QCommandLineOption>& appOptions)
|
||||
int& argc, char** argv, const QList<QCommandLineOption>& appOptions,
|
||||
const QList<QCommandLineOption>& appCommands)
|
||||
: m_options(std::make_unique<Options>()), m_qmlModule(defaultQmlModule()) {
|
||||
// When launched by SteamVR, stderr is not a TTY and Qt would log to
|
||||
// journald only; keep messages on stderr so they show up wherever the
|
||||
@@ -178,14 +182,24 @@ DashboardApplication::DashboardApplication(
|
||||
if (!qEnvironmentVariableIsSet("QT_FORCE_STDERR_LOGGING"))
|
||||
qputenv("QT_FORCE_STDERR_LOGGING", "1");
|
||||
|
||||
// Manifest commands only talk to SteamVR: no GUI (and no display
|
||||
// connection) needed, so they also work over SSH.
|
||||
// Manifest commands only talk to SteamVR, app commands do their own thing:
|
||||
// no GUI (and no display connection) needed, so they also work over SSH.
|
||||
// Decided before parsing: the parser needs the application object.
|
||||
Options& o = *m_options;
|
||||
o.manifestCommand = std::any_of(argv + 1, argv + argc, [](const char* arg) {
|
||||
return qstrcmp(arg, "--install-manifest") == 0 ||
|
||||
qstrcmp(arg, "--uninstall-manifest") == 0;
|
||||
});
|
||||
if (o.manifestCommand)
|
||||
const bool appCommand =
|
||||
std::any_of(argv + 1, argv + argc, [&appCommands](const char* arg) {
|
||||
for (const QCommandLineOption& command : appCommands) {
|
||||
for (const QString& name : command.names()) {
|
||||
if (QLatin1String(arg) == QStringLiteral("--") + name) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (o.manifestCommand || appCommand)
|
||||
m_app = std::make_unique<QCoreApplication>(argc, argv);
|
||||
else
|
||||
m_app = std::make_unique<QGuiApplication>(argc, argv);
|
||||
@@ -257,7 +271,7 @@ DashboardApplication::DashboardApplication(
|
||||
benchmarkOption, apiOption, keyOption, nameOption,
|
||||
fpsOption, installOption, uninstallOption, manifestOption,
|
||||
launchOption, logOption});
|
||||
for (const QCommandLineOption& option : appOptions) {
|
||||
for (const QCommandLineOption& option : appOptions + appCommands) {
|
||||
if (!parser.addOption(option))
|
||||
o.error = QStringLiteral("App option --%1 clashes with another option")
|
||||
.arg(option.names().constFirst());
|
||||
@@ -283,6 +297,12 @@ DashboardApplication::DashboardApplication(
|
||||
? resolvePath(parser.value(manifestOption))
|
||||
: shareDir() + QStringLiteral("/manifest.vrmanifest");
|
||||
if (o.manifestCommand) return;
|
||||
for (const QCommandLineOption& command : appCommands) {
|
||||
if (parser.isSet(command)) {
|
||||
o.appCommand = command.names().constFirst();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.isSet(apiOption)) {
|
||||
if (!parseGraphicsApi(parser.value(apiOption), &o.graphicsApi))
|
||||
@@ -338,6 +358,10 @@ void DashboardApplication::setQuickStyle(const QString& style) {
|
||||
|
||||
bool DashboardApplication::prepare(QString*) { return true; }
|
||||
|
||||
int DashboardApplication::runCommand(const QString& name) {
|
||||
return fail(QStringLiteral("Command --%1 is not implemented").arg(name));
|
||||
}
|
||||
|
||||
void DashboardApplication::loading() {}
|
||||
|
||||
void DashboardApplication::started() {}
|
||||
@@ -400,7 +424,7 @@ int DashboardApplication::exec() {
|
||||
QString error;
|
||||
if (!validateConfig(&error)) return fail(error);
|
||||
|
||||
m_mode = o.mode;
|
||||
m_mode = o.appCommand.isEmpty() ? o.mode : DashboardMode::Command;
|
||||
m_appKey = !o.appKey.isEmpty() ? o.appKey
|
||||
: !m_configAppKey.isEmpty()
|
||||
? m_configAppKey
|
||||
@@ -411,6 +435,7 @@ int DashboardApplication::exec() {
|
||||
: QString::fromUtf8(AppConfig::kDisplayName);
|
||||
QCoreApplication::setApplicationName(m_appName);
|
||||
if (!prepare(&error)) return fail(error);
|
||||
if (m_mode == DashboardMode::Command) return runCommand(o.appCommand);
|
||||
if (!m_quickStyle.isEmpty()) QQuickStyle::setStyle(m_quickStyle);
|
||||
|
||||
m_engine = std::make_unique<QQmlEngine>();
|
||||
@@ -432,6 +457,8 @@ int DashboardApplication::exec() {
|
||||
case DashboardMode::Overlay:
|
||||
result = runOverlay();
|
||||
break;
|
||||
case DashboardMode::Command:
|
||||
break; // handled above
|
||||
}
|
||||
// Still within exec(): the derived object (and what it owns) outlives all
|
||||
// QML. Surfaces (their QML scenes, renderers, backends) before the engine.
|
||||
|
||||
@@ -20,7 +20,15 @@ class VrRuntime;
|
||||
|
||||
// Desktop and DesktopNoVr both show desktop windows; DesktopNoVr promises
|
||||
// never to connect to SteamVR (for apps that use SteamVR data otherwise).
|
||||
enum class DashboardMode { Overlay, Desktop, DesktopNoVr, Snapshot, Benchmark };
|
||||
// Command: one of the app's commands runs (runCommand()); no GUI.
|
||||
enum class DashboardMode {
|
||||
Overlay,
|
||||
Desktop,
|
||||
DesktopNoVr,
|
||||
Snapshot,
|
||||
Benchmark,
|
||||
Command
|
||||
};
|
||||
const char* dashboardModeName(DashboardMode mode);
|
||||
|
||||
// Base class of a dashboard app. It owns everything app- and surface-related:
|
||||
@@ -59,8 +67,12 @@ class DashboardApplication : public QObject {
|
||||
// `appOptions` are the app's own command-line options: parsed and listed
|
||||
// in --help together with the built-in ones (names must not clash); read
|
||||
// them with isOptionSet() / optionValue().
|
||||
// `appCommands` are the app's commands (flags, e.g. --ha-remove): like the
|
||||
// manifest commands they need no display (QCoreApplication), and exec()
|
||||
// runs prepare() and then runCommand() instead of a mode.
|
||||
DashboardApplication(int& argc, char** argv,
|
||||
const QList<QCommandLineOption>& appOptions = {});
|
||||
const QList<QCommandLineOption>& appOptions = {},
|
||||
const QList<QCommandLineOption>& appCommands = {});
|
||||
~DashboardApplication() override;
|
||||
|
||||
// Runs the mode selected on the command line; returns the exit code.
|
||||
@@ -98,6 +110,10 @@ class DashboardApplication : public QObject {
|
||||
virtual void loading();
|
||||
// Everything runs; called right before the event loop starts.
|
||||
virtual void started();
|
||||
// Command mode: runs the app command `name` (an appCommands option, see
|
||||
// the constructor) after prepare(); no QML engine, surfaces or SteamVR.
|
||||
// Returns the exit code. The default fails.
|
||||
virtual int runCommand(const QString& name);
|
||||
|
||||
// --- The running app (valid in the hooks and until exec() returns) ---
|
||||
DashboardMode mode() const { return m_mode; }
|
||||
|
||||
@@ -33,6 +33,8 @@ add_test(NAME surfaces_cli_key COMMAND tst_surfaces --graphics-api opengl)
|
||||
set_tests_properties(surfaces_cli_key PROPERTIES ENVIRONMENT "VRDASH_TEST_CLI_KEY=1")
|
||||
add_test(NAME surfaces_prepare_fail COMMAND tst_surfaces --graphics-api opengl)
|
||||
set_tests_properties(surfaces_prepare_fail PROPERTIES ENVIRONMENT "VRDASH_TEST_PREPARE_FAIL=1")
|
||||
add_test(NAME surfaces_command COMMAND tst_surfaces)
|
||||
set_tests_properties(surfaces_command PROPERTIES ENVIRONMENT "VRDASH_TEST_COMMAND=1")
|
||||
add_test(NAME surfaces_desktop_novr COMMAND tst_surfaces)
|
||||
set_tests_properties(surfaces_desktop_novr PROPERTIES
|
||||
ENVIRONMENT "VRDASH_TEST_DESKTOP_NOVR=1;QT_QPA_PLATFORM=offscreen")
|
||||
|
||||
+32
-1
@@ -6,10 +6,14 @@
|
||||
// without calling the other hooks.
|
||||
// VRDASH_TEST_DESKTOP_NOVR=1: --desktop-novr instead of --snapshot; prepare()
|
||||
// checks the mode and then fails (no window, no event loop).
|
||||
// VRDASH_TEST_COMMAND=1: the app command --test-command instead of
|
||||
// --snapshot: prepare(), then runCommand() without GUI, engine or surfaces;
|
||||
// its result is exec()'s.
|
||||
// Exit codes: 0 pass, 1 fail, 77 skipped (no GPU context).
|
||||
|
||||
#include <QCommandLineOption>
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
#include <QImage>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
@@ -34,7 +38,9 @@ class TwoSurfaceApp : public DashboardApplication {
|
||||
QStringLiteral("An app option."),
|
||||
QStringLiteral("value")),
|
||||
QCommandLineOption(QStringLiteral("test-flag"),
|
||||
QStringLiteral("An app flag, never set."))}) {
|
||||
QStringLiteral("An app flag, never set."))},
|
||||
{QCommandLineOption(QStringLiteral("test-command"),
|
||||
QStringLiteral("An app command."))}) {
|
||||
setQmlModule(QStringLiteral("SurfacesTest"));
|
||||
setDisplayName(QStringLiteral("Test App"));
|
||||
setAppKey(QStringLiteral("config.key"));
|
||||
@@ -53,6 +59,7 @@ class TwoSurfaceApp : public DashboardApplication {
|
||||
int prepareCalls = 0;
|
||||
int loadingCalls = 0;
|
||||
int startedCalls = 0;
|
||||
QStringList commands;
|
||||
bool ok = true;
|
||||
|
||||
protected:
|
||||
@@ -94,6 +101,15 @@ class TwoSurfaceApp : public DashboardApplication {
|
||||
"texture size from config");
|
||||
ok &= check(vrRuntime() == nullptr, "no VR outside overlay mode");
|
||||
}
|
||||
int runCommand(const QString& name) override {
|
||||
commands.append(name);
|
||||
ok &= check(mode() == DashboardMode::Command, "mode in runCommand()");
|
||||
ok &= check(engine() == nullptr && surfaces().isEmpty(),
|
||||
"no engine and no surfaces for a command");
|
||||
ok &= check(!qobject_cast<QGuiApplication*>(QCoreApplication::instance()),
|
||||
"no GUI application for a command");
|
||||
return 7;
|
||||
}
|
||||
void started() override {
|
||||
++startedCalls;
|
||||
const QList<Surface*> all = surfaces();
|
||||
@@ -113,11 +129,14 @@ int main(int argc, char* argv[]) {
|
||||
const bool cliKey = qEnvironmentVariableIntValue("VRDASH_TEST_CLI_KEY");
|
||||
const bool desktopNoVr =
|
||||
qEnvironmentVariableIntValue("VRDASH_TEST_DESKTOP_NOVR");
|
||||
const bool command = qEnvironmentVariableIntValue("VRDASH_TEST_COMMAND");
|
||||
const bool failPrepare =
|
||||
qEnvironmentVariableIntValue("VRDASH_TEST_PREPARE_FAIL") || desktopNoVr;
|
||||
std::vector<std::string> args = {argv[0], "--test-value", "42"};
|
||||
if (desktopNoVr) {
|
||||
args.emplace_back("--desktop-novr");
|
||||
} else if (command) {
|
||||
args.emplace_back("--test-command");
|
||||
} else {
|
||||
args.emplace_back("--snapshot");
|
||||
args.emplace_back(snapshot);
|
||||
@@ -136,7 +155,19 @@ int main(int argc, char* argv[]) {
|
||||
cliKey ? QStringLiteral("cli.key") : QStringLiteral("config.key");
|
||||
app.failPrepare = failPrepare;
|
||||
if (desktopNoVr) app.expectedMode = DashboardMode::DesktopNoVr;
|
||||
if (command) app.expectedMode = DashboardMode::Command;
|
||||
const int result = app.exec();
|
||||
if (command) {
|
||||
bool ok = app.ok;
|
||||
ok &= check(result == 7, "exec() returns runCommand()'s result");
|
||||
ok &= check(
|
||||
app.prepareCalls == 1 && app.loadingCalls == 0 && app.startedCalls == 0,
|
||||
"prepare(), but no other hooks for a command");
|
||||
ok &= check(app.commands == QStringList{QStringLiteral("test-command")},
|
||||
"runCommand() once with the command's name");
|
||||
fprintf(stderr, ok ? "PASS\n" : "FAILED\n");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
if (failPrepare) {
|
||||
bool ok = app.ok;
|
||||
ok &= check(result == 1, "exec() returns 1 when prepare() fails");
|
||||
|
||||
Reference in New Issue
Block a user