Skip to content

Commit 655df48

Browse files
Merge pull request #735 from fernandotonon/feat/updater-install-446-448
feat(updater): install + relauncher (#446–448)
2 parents a9174cb + 8047c70 commit 655df48

14 files changed

Lines changed: 1059 additions & 82 deletions

.github/workflows/deploy.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -895,10 +895,9 @@ jobs:
895895
needs: [build-n-cache-assimp-linux, build-n-cache-ogre-linux]
896896
runs-on: ubuntu-latest
897897
# Hard cap on the whole job. The full sweep takes ~12 min normally
898-
# (build + xvfb + ~25 suites + coverage + sonar). Cap at 45 so a
899-
# hung suite or a deadlock surfaces in well under an hour instead
900-
# of camping on the runner for GitHub's default 6-hour ceiling.
901-
timeout-minutes: 45
898+
# (build + xvfb + ~25 suites + coverage + sonar). Cap at 60 so a
899+
# cold ccache build on a large PR still finishes under the limit.
900+
timeout-minutes: 60
902901
permissions: read-all
903902
env:
904903
LD_LIBRARY_PATH: gcc_64/lib/:/usr/local/lib/:/usr/local/lib/OGRE/:/usr/local/lib/pkgconfig/:/lib/x86_64-linux-gnu/
@@ -987,9 +986,12 @@ jobs:
987986
create-symlink: true
988987

989988
- name: Setup headless environment for Qt tests
989+
env:
990+
DEBIAN_FRONTEND: noninteractive
990991
run: |
991992
sudo apt-get update
992-
sudo apt-get install -y libxcb-cursor0 libxcb-xinerama0 libx11-dev xvfb \
993+
sudo apt-get install -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold \
994+
libxcb-cursor0 libxcb-xinerama0 libx11-dev xvfb \
993995
mesa-utils libgl1-mesa-dri libegl-mesa0 libgbm1 libglx-mesa0 \
994996
libsecret-1-dev libretro-beetle-psx libsodium-dev
995997
# Start Xvfb with GLX support for Ogre GL initialization

qml/UpdaterDialog.qml

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Window {
3737
if (event.key === Qt.Key_Escape) {
3838
if (UpdaterController.state === UpdaterController.Checking
3939
|| UpdaterController.state === UpdaterController.Downloading
40-
|| UpdaterController.state === UpdaterController.Verifying) {
40+
|| UpdaterController.state === UpdaterController.Verifying
41+
|| UpdaterController.state === UpdaterController.Installing) {
4142
UpdaterController.cancel()
4243
}
4344
UpdaterController.dismiss()
@@ -100,6 +101,7 @@ Window {
100101
case UpdaterController.Downloading: return "Downloading update…"
101102
case UpdaterController.Verifying: return "Verifying download…"
102103
case UpdaterController.ReadyToInstall: return "Ready to install"
104+
case UpdaterController.Installing: return "Installing update…"
103105
default: return "Software updates"
104106
}
105107
}
@@ -119,6 +121,7 @@ Window {
119121
running: UpdaterController.state === UpdaterController.Checking
120122
|| UpdaterController.state === UpdaterController.Downloading
121123
|| UpdaterController.state === UpdaterController.Verifying
124+
|| UpdaterController.state === UpdaterController.Installing
122125
visible: running
123126
}
124127

@@ -272,7 +275,7 @@ Window {
272275
}
273276
}
274277

275-
// Ready to install (#444 — install step lands in #446–448)
278+
// Ready to install (#446–448)
276279
ColumnLayout {
277280
Layout.fillWidth: true
278281
visible: UpdaterController.state === UpdaterController.ReadyToInstall
@@ -283,11 +286,33 @@ Window {
283286
color: PropertiesPanelController.textColor
284287
font.pixelSize: 12
285288
text: "Update " + UpdaterController.latestVersion +
286-
" downloaded and verified. Automatic installation will be available in a future release."
289+
" downloaded and verified. Install now to replace this copy and restart."
287290
}
288-
InspectorButton {
289-
label: "Close"
290-
onClicked: { UpdaterController.dismiss(); dialog.close() }
291+
RowLayout {
292+
spacing: 8
293+
InspectorButton {
294+
label: "Install & restart"
295+
primary: true
296+
onClicked: UpdaterController.installUpdate()
297+
}
298+
InspectorButton {
299+
label: "Later"
300+
onClicked: { UpdaterController.dismiss(); dialog.close() }
301+
}
302+
}
303+
}
304+
305+
// Installing
306+
ColumnLayout {
307+
Layout.fillWidth: true
308+
visible: UpdaterController.state === UpdaterController.Installing
309+
spacing: 8
310+
Text {
311+
Layout.fillWidth: true
312+
wrapMode: Text.WordWrap
313+
color: PropertiesPanelController.textColor
314+
font.pixelSize: 12
315+
text: "Preparing installation. The application will close and restart automatically."
291316
}
292317
}
293318

src/CMakeLists.txt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,6 @@ TextureAtlasPacker.cpp
9797
PaintBufferImageProvider.cpp
9898
PaintSelectionMask.cpp
9999
TexturePaintBuffer.cpp
100-
UpdateVersion.cpp
101-
updater/InstallFlavor.cpp
102-
updater/ArtifactResolver.cpp
103-
updater/UpdateVerifier.cpp
104-
updater/MinisignVerify.cpp
105-
updater/GitHubReleaseParser.cpp
106-
updater/UpdaterWorker.cpp
107-
updater/UpdaterController.cpp
108100
TexturePaintController.cpp
109101
VertexColorBaker.cpp
110102
VATBaker.cpp
@@ -239,13 +231,6 @@ PaintBufferImageProvider.h
239231
PaintSelectionMask.h
240232
TexturePaintBuffer.h
241233
UpdateVersion.h
242-
updater/InstallFlavor.h
243-
updater/ArtifactResolver.h
244-
updater/UpdateVerifier.h
245-
updater/MinisignVerify.h
246-
updater/GitHubReleaseParser.h
247-
updater/UpdaterWorker.h
248-
updater/UpdaterController.h
249234
TexturePaintController.h
250235
VertexColorBaker.h
251236
ApplyAtlas.h
@@ -298,6 +283,8 @@ ADD_SUBDIRECTORY("${CMAKE_CURRENT_SOURCE_DIR}/PS1")
298283
# if we don't include this CMake will not include ui headers properly:
299284
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${BUILD_INCLUDE_DIR} ${BUILD_UIH_DIR})
300285

286+
add_subdirectory(updater)
287+
301288
##############################################################
302289
# Processing ogre-procedural
303290
##############################################################
@@ -553,6 +540,7 @@ Qt::QuickControls2
553540
meshoptimizer
554541
xatlas
555542
qtmesh_sodium
543+
qtmesh_updater
556544
)
557545

558546
# One-time legacy secret-store read for migration (see CloudCredentialStore).
@@ -588,6 +576,18 @@ if(ENABLE_PS1_RIP AND TARGET Qt6::Gamepad)
588576
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE HAVE_QT_GAMEPAD)
589577
target_link_libraries(${CMAKE_PROJECT_NAME} Qt6::Gamepad)
590578
endif()
579+
580+
if(ENABLE_AUTO_UPDATER AND TARGET qtmesh-relauncher)
581+
add_dependencies(${CMAKE_PROJECT_NAME} qtmesh-relauncher)
582+
if(APPLE)
583+
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
584+
COMMAND ${CMAKE_COMMAND} -E copy
585+
"${CMAKE_BINARY_DIR}/bin/qtmesh-relauncher"
586+
"$<TARGET_BUNDLE_DIR:${CMAKE_PROJECT_NAME}>/Contents/MacOS/qtmesh-relauncher"
587+
COMMENT "Copying qtmesh-relauncher into app bundle"
588+
)
589+
endif()
590+
endif()
591591
ENDIF()
592592

593593
##############################################################
@@ -646,7 +646,7 @@ if(BUILD_TESTS)
646646
${OGRE_LIBRARIES}
647647
${ASSIMP_LIBRARIES}
648648
Qt::Widgets Qt::Core Qt::Gui Qt::Test Qt::Network Qt::Qml Qt::Quick Qt::QuickWidgets Qt::QuickControls2
649-
meshoptimizer xatlas qtmesh_sodium)
649+
meshoptimizer xatlas qtmesh_sodium qtmesh_updater)
650650

651651
# CloudCredentialStore.cpp (compiled into the tests) does a one-time legacy
652652
# secret-store read in migrateLegacySettingsIfNeeded(), which

src/updater/CMakeLists.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
set(QTMESH_UPDATER_SOURCES
2+
${CMAKE_CURRENT_SOURCE_DIR}/../UpdateVersion.cpp
3+
InstallFlavor.cpp
4+
ArtifactResolver.cpp
5+
UpdateVerifier.cpp
6+
MinisignVerify.cpp
7+
GitHubReleaseParser.cpp
8+
UpdaterWorker.cpp
9+
UpdaterController.cpp
10+
UpdaterInstaller.cpp
11+
)
12+
13+
add_library(qtmesh_updater STATIC ${QTMESH_UPDATER_SOURCES})
14+
set_target_properties(qtmesh_updater PROPERTIES
15+
AUTOMOC ON
16+
POSITION_INDEPENDENT_CODE ON
17+
)
18+
target_include_directories(qtmesh_updater
19+
PUBLIC
20+
${CMAKE_CURRENT_SOURCE_DIR}/..
21+
${CMAKE_CURRENT_SOURCE_DIR}
22+
)
23+
target_link_libraries(qtmesh_updater
24+
PUBLIC
25+
Qt6::Core
26+
Qt6::Network
27+
Qt6::Qml
28+
qtmesh_sodium
29+
)
30+
31+
if(BUILD_TESTS)
32+
target_compile_definitions(qtmesh_updater PRIVATE QTMESH_UNIT_TESTS)
33+
endif()
34+
35+
if(ENABLE_AUTO_UPDATER AND BUILD_QT_MESH_EDITOR)
36+
add_subdirectory(relauncher)
37+
endif()

src/updater/UpdaterController.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "UpdaterController.h"
22
#include "ArtifactResolver.h"
3+
#include "UpdaterInstaller.h"
34
#include "UpdaterWorker.h"
45
#include "AppSettingsKeys.h"
56
#include "SentryReporter.h"
@@ -34,6 +35,7 @@ QString stateToString(UpdaterController::State state)
3435
case UpdaterController::State::Downloading: return QStringLiteral("downloading");
3536
case UpdaterController::State::Verifying: return QStringLiteral("verifying");
3637
case UpdaterController::State::ReadyToInstall: return QStringLiteral("ready_to_install");
38+
case UpdaterController::State::Installing: return QStringLiteral("installing");
3739
}
3840
return QStringLiteral("idle");
3941
}
@@ -369,6 +371,54 @@ void UpdaterController::downloadAndInstall()
369371
beginDownloadIfNeeded(true);
370372
}
371373

374+
void UpdaterController::installUpdate()
375+
{
376+
if (m_state != State::ReadyToInstall) {
377+
return;
378+
}
379+
380+
SentryReporter::addBreadcrumb(QStringLiteral("updater.install.start"),
381+
QStringLiteral("version=%1").arg(m_latestVersion));
382+
setError(QString());
383+
setState(State::Installing);
384+
logDialogStateBreadcrumb();
385+
386+
UpdaterInstaller::InstallContext context;
387+
context.stagedArtifactPath = m_stagedArtifactPath;
388+
context.releaseTag = m_latestVersion;
389+
context.executablePath = QCoreApplication::applicationFilePath();
390+
context.installRoot = UpdaterInstaller::resolveInstallRoot(context.executablePath);
391+
context.parentPid = QCoreApplication::applicationPid();
392+
393+
const UpdaterInstaller::InstallPlan plan = UpdaterInstaller::prepareInstall(context);
394+
if (!plan.ok) {
395+
SentryReporter::addBreadcrumb(QStringLiteral("updater.install.error"),
396+
plan.errorMessage,
397+
QStringLiteral("error"));
398+
setError(plan.errorMessage.isEmpty()
399+
? tr("Could not prepare the update for installation.")
400+
: plan.errorMessage);
401+
setState(State::Error);
402+
logDialogStateBreadcrumb();
403+
return;
404+
}
405+
406+
if (!UpdaterInstaller::launchRelauncher(plan)) {
407+
SentryReporter::addBreadcrumb(QStringLiteral("updater.install.error"),
408+
QStringLiteral("relauncher missing or failed to start"),
409+
QStringLiteral("error"));
410+
setError(tr("Could not launch the update installer. "
411+
"Make sure qtmesh-relauncher is next to the application binary."));
412+
setState(State::Error);
413+
logDialogStateBreadcrumb();
414+
return;
415+
}
416+
417+
SentryReporter::addBreadcrumb(QStringLiteral("updater.install.relaunch"),
418+
plan.manifestPath);
419+
QApplication::quit();
420+
}
421+
372422
void UpdaterController::beginDownloadIfNeeded(bool userInitiated)
373423
{
374424
if (m_state != State::UpdateAvailable) {

src/updater/UpdaterController.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
* @brief QML-facing singleton orchestrating update checks (#441, #443, #449).
1616
*
1717
* Mirrors the LLMManager / SDManager pattern: main-thread controller with
18-
* a worker thread for HTTP. Install/relaunch land in #446–448.
18+
* a worker thread for HTTP. Install/relaunch (#446–448) runs on the main thread
19+
* via UpdaterInstaller before the app exits.
1920
*/
2021
class UpdaterController : public QObject
2122
{
@@ -52,6 +53,7 @@ class UpdaterController : public QObject
5253
Downloading,
5354
Verifying,
5455
ReadyToInstall,
56+
Installing,
5557
};
5658
Q_ENUM(State)
5759

@@ -84,6 +86,7 @@ class UpdaterController : public QObject
8486
Q_INVOKABLE void requestCheckDialog();
8587
Q_INVOKABLE void confirmUnknownInstall();
8688
Q_INVOKABLE void downloadAndInstall();
89+
Q_INVOKABLE void installUpdate();
8790
Q_INVOKABLE void cancel();
8891
Q_INVOKABLE void dismiss();
8992
Q_INVOKABLE void remindLater();

0 commit comments

Comments
 (0)