v0.2.0 (11/N): periodic auto-update check

PLAN.md §11 *Auto-update* described "check on launch and once per N
hours; offer install on next restart, never auto-restart". v0.1.0
shipped checkForUpdates() and applyUpdate() Q_INVOKABLEs but only
manual triggers — no scheduling. This wires the polling.

armAutoUpdateOnFirstOnline() runs from setState(Online) in bundled
mode:
- A QTimer::singleShot fires checkForUpdates() 10 s after the first
  Online transition (lets cold-boot bandwidth/CPU settle first).
- A recurring QTimer fires checkForUpdates() every 6 hours after
  that.
- One-shot guard via m_autoUpdateArmed so reconnect cycles don't
  re-arm the timers.

Dev mode skips entirely (developers don't want their `make dev`
workflow polling AppImageUpdate). Env-var knobs:
- BRIDGE_AUTO_UPDATE_DISABLE=1 — skip entirely (respect-opt-out
  baseline; user-facing settings UI can layer on top later).
- BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes> — override the period
  (handy for testing or shorter intervals on power-user opt-in).

The actual install (apply + restart) stays manual — never auto-
restart, per PLAN.md's UX rule. checkForUpdates emits
updatesAvailable(); QML decides whether/when to show a banner and
call applyUpdate().

Verified locally with QT_LOGGING_RULES=phpqml.bridge.bundled.info=true:
"phpqml.bridge.bundled: auto-update armed: launch check in 10000 ms,
period 360 min" appears in the host log after the BackendConnection
probe sees /healthz=200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 20:50:59 +02:00
parent da097051ca
commit 82de6cae36
3 changed files with 51 additions and 0 deletions

View File

@@ -615,6 +615,45 @@ void BackendConnection::setState(ConnectionState s)
if (m_state == s) return;
m_state = s;
emit connectionStateChanged();
if (s == ConnectionState::Online) {
armAutoUpdateOnFirstOnline();
}
}
void BackendConnection::armAutoUpdateOnFirstOnline()
{
if (m_autoUpdateArmed) return;
if (m_mode != Mode::Bundled) return;
if (qEnvironmentVariableIsSet("BRIDGE_AUTO_UPDATE_DISABLE")
&& qgetenv("BRIDGE_AUTO_UPDATE_DISABLE") == "1") {
qCInfo(lcBundled) << "auto-update disabled via BRIDGE_AUTO_UPDATE_DISABLE";
m_autoUpdateArmed = true;
return;
}
m_autoUpdateArmed = true;
int periodMs = kAutoUpdateDefaultPeriodMs;
bool ok = false;
const int overrideMin = qgetenv("BRIDGE_AUTO_UPDATE_PERIOD_MIN").toInt(&ok);
if (ok && overrideMin > 0) {
periodMs = overrideMin * 60 * 1000;
}
// First check: a few seconds after first Online so a fresh launch
// doesn't fight for bandwidth/CPU with the cold boot. Subsequent
// checks: every periodMs (default 6h).
QTimer::singleShot(kAutoUpdateLaunchDelayMs, this, &BackendConnection::checkForUpdates);
if (!m_autoUpdateTimer) {
m_autoUpdateTimer = new QTimer(this);
m_autoUpdateTimer->setSingleShot(false);
connect(m_autoUpdateTimer, &QTimer::timeout,
this, &BackendConnection::checkForUpdates);
}
m_autoUpdateTimer->start(periodMs);
qCInfo(lcBundled) << "auto-update armed: launch check in"
<< kAutoUpdateLaunchDelayMs << "ms, period"
<< (periodMs / 60000) << "min";
}
void BackendConnection::setError(const QString& msg)

View File

@@ -175,6 +175,17 @@ private:
QProcess* m_updateCheck = nullptr;
QProcess* m_updateApply = nullptr;
/// Periodic AppImageUpdate poll. Armed on first Online transition
/// in bundled mode; PLAN.md §11 *Auto-update*. Disabled by env
/// `BRIDGE_AUTO_UPDATE_DISABLE=1`; period override via
/// `BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes>`.
QTimer* m_autoUpdateTimer = nullptr;
bool m_autoUpdateArmed = false;
static constexpr int kAutoUpdateLaunchDelayMs = 10'000; // 10s after first Online
static constexpr int kAutoUpdateDefaultPeriodMs = 6 * 60 * 60 * 1000; // 6h
void armAutoUpdateOnFirstOnline();
QString resolveSidecarUpdater() const;
QString currentAppImagePath() const;
};