feature(updater): Implement auto-updating system using AppCast protocol
This commit is contained in:
+4
-1
@@ -45,4 +45,7 @@ app.*.map.json
|
||||
/android/app/release
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
.fvm/
|
||||
|
||||
# Local launcher update signing material
|
||||
.moonwell_signing/
|
||||
|
||||
@@ -46,8 +46,8 @@ dart run build_runner build --delete-conflicting-outputs
|
||||
dart format --set-exit-if-changed .
|
||||
flutter analyze
|
||||
flutter test
|
||||
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
See `docs/linting.md` for the required format, analyze, test, and Lefthook
|
||||
@@ -61,6 +61,15 @@ The launcher API base URL is compile-time configuration:
|
||||
--dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
```
|
||||
|
||||
The optional Windows self-update feed is also compile-time configuration:
|
||||
|
||||
```powershell
|
||||
--dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
Production AppCast feeds must be public HTTPS URLs. Omit the setting to disable
|
||||
self-update in local builds.
|
||||
|
||||
Do not commit private credentials, production secrets, temporary bearer tokens,
|
||||
or local-only API URLs.
|
||||
|
||||
@@ -71,6 +80,9 @@ or local-only API URLs.
|
||||
Do not store sessions or other secrets in `shared_preferences`.
|
||||
- File sync behavior should stay in `ClientSyncUseCase` and
|
||||
`GameInstallationService`.
|
||||
- Launcher self-update behavior should stay behind `LauncherUpdateService`.
|
||||
Windows updates use WinSparkle/AppCast and the signed Inno Setup artifact;
|
||||
they are separate from game-client synchronization.
|
||||
- The server manifest is the source of truth for client files.
|
||||
- Downloads must write to `*.moonwell.part`, verify size and SHA-256, then
|
||||
replace the destination file only after successful verification.
|
||||
|
||||
@@ -41,10 +41,19 @@ Set the launcher Web API base URL at compile time:
|
||||
--dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
```
|
||||
|
||||
Set the public AppCast feed URL to enable Windows launcher self-updates:
|
||||
|
||||
```powershell
|
||||
--dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
The updater is disabled when `MOONWELL_APPCAST_URL` is omitted. Production
|
||||
feeds must use HTTPS.
|
||||
|
||||
Example run command:
|
||||
|
||||
```powershell
|
||||
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -67,10 +76,12 @@ pre-commit workflow.
|
||||
Build the Windows launcher:
|
||||
|
||||
```powershell
|
||||
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host
|
||||
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
The Inno Setup installer script lives at `installer/moonwell_launcher.iss`.
|
||||
See `docs/launcher_self_update.md` for the AppCast format, signing keys, and
|
||||
manual release procedure.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -99,3 +110,4 @@ See `docs/launcher_web_api_spec.md` for the API and sync contract.
|
||||
- `docs/project_review.md`: project review with strengths, weaknesses, and
|
||||
upgrade feasibility notes
|
||||
- `docs/launcher_web_api_spec.md`: current launcher Web API and sync behavior
|
||||
- `docs/launcher_self_update.md`: Windows self-update and release operations
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Launcher Self-Update
|
||||
|
||||
MoonWell Launcher uses `auto_updater` 1.0.0, backed by WinSparkle 0.8.1, to
|
||||
update installed Windows launchers from an AppCast feed. This is independent of
|
||||
the manifest-based World of Warcraft client synchronization.
|
||||
|
||||
The launcher performs one background update check after its window is visible.
|
||||
WinSparkle shows its native prompt only when a newer version is available.
|
||||
Missing configuration, network failures, and malformed feeds do not block the
|
||||
launcher. Periodic WinSparkle checks are disabled.
|
||||
|
||||
## Build Configuration
|
||||
|
||||
Pass the public feed URL at build time:
|
||||
|
||||
```powershell
|
||||
flutter build windows `
|
||||
--dart-define=MOONWELL_API_BASE_URL=https://host `
|
||||
--dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml
|
||||
```
|
||||
|
||||
The feed URL must be an absolute HTTPS URL. Omit
|
||||
`MOONWELL_APPCAST_URL` to disable self-update, including for ordinary local
|
||||
development.
|
||||
|
||||
## Signing Keys
|
||||
|
||||
WinSparkle verifies installer artifacts with a DSA signature. The public key is
|
||||
committed at `windows/runner/resources/dsa_pub.pem` and embedded as the
|
||||
`DSAPub` resource. The corresponding private key is local signing material at
|
||||
`.moonwell_signing/dsa_priv.pem`; it is ignored by Git and must be backed up to
|
||||
the project secret store before publishing the first update.
|
||||
|
||||
Never commit or upload the private key. Losing it prevents existing
|
||||
installations from accepting future updates. Rotating it requires shipping an
|
||||
ordinary installer containing the replacement public key before publishing
|
||||
artifacts signed by the replacement private key.
|
||||
|
||||
The AppCast signature is not Authenticode. Publicly trusted Authenticode
|
||||
certificates and managed signing services are paid products, so MoonWell does
|
||||
not currently Authenticode-sign the installer. Windows can therefore display
|
||||
an unknown-publisher or SmartScreen warning. A free self-signed Authenticode
|
||||
certificate would not establish trust on player machines.
|
||||
|
||||
## Version and AppCast Contract
|
||||
|
||||
Increase the `X.Y.Z` version in `pubspec.yaml` for every launcher release. A
|
||||
build-number-only change such as `1.0.0+2` is not sufficient because WinSparkle
|
||||
compares the Windows product version. Pass the same `X.Y.Z` value to Inno Setup:
|
||||
|
||||
```powershell
|
||||
ISCC.exe /DMyAppVersion=1.1.0 installer\moonwell_launcher.iss
|
||||
```
|
||||
|
||||
The resulting artifact is
|
||||
`build/installer/moonwell_launcher_1.1.0_windows_setup.exe`.
|
||||
|
||||
Use a public AppCast with absolute URLs:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<channel>
|
||||
<title>MoonWell Launcher</title>
|
||||
<description>MoonWell Launcher updates</description>
|
||||
<language>ru</language>
|
||||
<item>
|
||||
<title>MoonWell Launcher 1.1.0</title>
|
||||
<sparkle:releaseNotesLink>https://host/launcher/updates/1.1.0/release-notes.html</sparkle:releaseNotesLink>
|
||||
<pubDate>Sun, 21 Jun 2026 12:00:00 +0400</pubDate>
|
||||
<enclosure
|
||||
url="https://host/launcher/updates/1.1.0/moonwell_launcher_1.1.0_windows_setup.exe"
|
||||
sparkle:version="1.1.0"
|
||||
sparkle:os="windows"
|
||||
sparkle:dsaSignature="SIGNATURE_FROM_SIGN_UPDATE"
|
||||
length="ACTUAL_INSTALLER_SIZE_IN_BYTES"
|
||||
type="application/octet-stream" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
```
|
||||
|
||||
The installer URL and release-notes URL must be absolute. The publication host
|
||||
must serve the feed, notes, and installer without authentication or redirects
|
||||
to an authenticated endpoint.
|
||||
|
||||
## Manual Release
|
||||
|
||||
Perform Windows signing on the Windows release machine because the package
|
||||
invokes WinSparkle's Windows signing utility.
|
||||
|
||||
1. Update `pubspec.yaml` to the new `X.Y.Z+N` version.
|
||||
2. Run `flutter pub get`, formatting, analysis, and all tests.
|
||||
3. Build Windows with both production `--dart-define` values.
|
||||
4. Compile the installer with `ISCC.exe /DMyAppVersion=X.Y.Z`.
|
||||
5. Make the private key available outside the repository and sign the artifact:
|
||||
|
||||
```powershell
|
||||
dart run auto_updater:sign_update `
|
||||
build\installer\moonwell_launcher_X.Y.Z_windows_setup.exe `
|
||||
C:\secure-path\dsa_priv.pem
|
||||
```
|
||||
|
||||
6. Copy the emitted `sparkle:dsaSignature` into the enclosure. The package's
|
||||
Windows helper reports `length="0"`; replace it with the actual value from:
|
||||
|
||||
```powershell
|
||||
(Get-Item build\installer\moonwell_launcher_X.Y.Z_windows_setup.exe).Length
|
||||
```
|
||||
|
||||
7. Upload the versioned installer and release notes first.
|
||||
8. Download the hosted installer, verify its byte length, and test its DSA
|
||||
signature before changing the feed.
|
||||
9. Publish `appcast.xml` last so clients never observe an incomplete release.
|
||||
|
||||
Keep the preceding installer available for rollback and support. Rollback is a
|
||||
new release with a product version higher than the faulty release; WinSparkle
|
||||
does not install a lower version as an update.
|
||||
|
||||
## Release Verification
|
||||
|
||||
Test from an installed older version on a clean Windows x64 machine:
|
||||
|
||||
- offline or invalid feed: launcher remains usable and writes a log entry
|
||||
- same or older feed version: no update prompt
|
||||
- valid newer release: prompt, download, launcher exit, install, and relaunch
|
||||
- modified installer or invalid DSA signature: WinSparkle rejects the update
|
||||
- launcher session and selected game directory remain after installation
|
||||
|
||||
Watch the debug output for Flutter platform-channel thread warnings during the
|
||||
test. `auto_updater` has an open Windows callback-thread issue; do not release
|
||||
if it reproduces. Patch and pin `auto_updater_windows` so native callbacks are
|
||||
marshalled onto Flutter's platform thread, then repeat the complete update test.
|
||||
@@ -1,6 +1,8 @@
|
||||
#define MyAppName "MoonWell Launcher"
|
||||
#define MyAppExeName "MoonWell.exe"
|
||||
#define MyAppVersion "1.0.0"
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "1.0.0"
|
||||
#endif
|
||||
#define MyAppPublisher "MoonWell"
|
||||
#define MyAppURL "https://moon-well.online"
|
||||
#define MyAppAssocName MyAppName + " Launcher"
|
||||
@@ -19,7 +21,7 @@ DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
PrivilegesRequired=lowest
|
||||
OutputDir=..\build\installer
|
||||
OutputBaseFilename=moonwell_launcher_setup
|
||||
OutputBaseFilename=moonwell_launcher_{#MyAppVersion}_windows_setup
|
||||
SetupIconFile=..\windows\runner\resources\app_icon.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
Compression=lzma
|
||||
|
||||
@@ -2,6 +2,9 @@ final class Config {
|
||||
static const launcherApiBaseUrl = String.fromEnvironment(
|
||||
'MOONWELL_API_BASE_URL',
|
||||
);
|
||||
static const launcherAppcastUrl = String.fromEnvironment(
|
||||
'MOONWELL_APPCAST_URL',
|
||||
);
|
||||
static const launcherLogFileName = 'launcher.log';
|
||||
static const gameExecutableName = 'Wow.exe';
|
||||
static const cacheDirectoryName = 'Cache';
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_updater/auto_updater.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:moonwell_launcher/config.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_log_service.dart';
|
||||
|
||||
abstract interface class LauncherUpdateClient {
|
||||
Future<void> setFeedUrl(String feedUrl);
|
||||
|
||||
Future<void> disableScheduledChecks();
|
||||
|
||||
Future<void> checkInBackground();
|
||||
}
|
||||
|
||||
final class AutoUpdaterLauncherUpdateClient implements LauncherUpdateClient {
|
||||
@override
|
||||
Future<void> setFeedUrl(String feedUrl) => autoUpdater.setFeedURL(feedUrl);
|
||||
|
||||
@override
|
||||
Future<void> disableScheduledChecks() =>
|
||||
autoUpdater.setScheduledCheckInterval(0);
|
||||
|
||||
@override
|
||||
Future<void> checkInBackground() =>
|
||||
autoUpdater.checkForUpdates(inBackground: true);
|
||||
}
|
||||
|
||||
@lazySingleton
|
||||
class LauncherUpdateService {
|
||||
LauncherUpdateService(LauncherLogService launcherLogService)
|
||||
: this.forTesting(
|
||||
launcherLogService: launcherLogService,
|
||||
updateClient: AutoUpdaterLauncherUpdateClient(),
|
||||
isWindows: Platform.isWindows,
|
||||
);
|
||||
|
||||
LauncherUpdateService.forTesting({
|
||||
required LauncherLogService launcherLogService,
|
||||
required LauncherUpdateClient updateClient,
|
||||
required bool isWindows,
|
||||
}) : _launcherLogService = launcherLogService,
|
||||
_updateClient = updateClient,
|
||||
_isWindows = isWindows;
|
||||
|
||||
final LauncherLogService _launcherLogService;
|
||||
final LauncherUpdateClient _updateClient;
|
||||
final bool _isWindows;
|
||||
|
||||
bool _hasChecked = false;
|
||||
|
||||
Future<void> checkForUpdates({
|
||||
String feedUrl = Config.launcherAppcastUrl,
|
||||
}) async {
|
||||
if (!_isWindows || _hasChecked) {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedFeedUrl = feedUrl.trim();
|
||||
if (normalizedFeedUrl.isEmpty) {
|
||||
await _launcherLogService.info(
|
||||
'Launcher self-update is disabled because no AppCast URL is configured.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(normalizedFeedUrl);
|
||||
if (uri == null ||
|
||||
!uri.isAbsolute ||
|
||||
uri.scheme != 'https' ||
|
||||
!uri.hasAuthority ||
|
||||
uri.host.isEmpty) {
|
||||
await _launcherLogService.error(
|
||||
'Launcher self-update AppCast URL is invalid.',
|
||||
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasChecked = true;
|
||||
|
||||
try {
|
||||
await _updateClient.setFeedUrl(normalizedFeedUrl);
|
||||
await _updateClient.disableScheduledChecks();
|
||||
await _updateClient.checkInBackground();
|
||||
await _launcherLogService.info(
|
||||
'Launcher self-update check started.',
|
||||
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
await _launcherLogService.error(
|
||||
'Launcher self-update check failed.',
|
||||
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/mw_app.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_update_service.dart';
|
||||
import 'package:moonwell_launcher/service_container.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@@ -10,6 +11,9 @@ Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
await configureDependencies(env: 'flutter');
|
||||
final launcherUpdateService = getIt<LauncherUpdateService>();
|
||||
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(960, 540),
|
||||
center: true,
|
||||
@@ -22,10 +26,9 @@ Future<void> main() async {
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await launcherUpdateService.checkForUpdates();
|
||||
}),
|
||||
);
|
||||
|
||||
await configureDependencies(env: 'flutter');
|
||||
|
||||
runApp(const MoonWellApp());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'features/launcher/application/client_sync_use_case.dart' as _i757;
|
||||
import 'features/launcher/data/game_installation_service.dart' as _i315;
|
||||
import 'features/launcher/data/launcher_api_client.dart' as _i703;
|
||||
import 'features/launcher/data/launcher_log_service.dart' as _i43;
|
||||
import 'features/launcher/data/launcher_update_service.dart' as _i605;
|
||||
import 'features/preferences/data/repositories/local_preferences_repository.dart'
|
||||
as _i658;
|
||||
import 'features/preferences/domain/repositories/preferences_repository.dart'
|
||||
@@ -47,6 +48,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.lazySingleton<_i558.FlutterSecureStorage>(
|
||||
() => flutterSecureStorageModule.storage,
|
||||
);
|
||||
gh.lazySingleton<_i605.LauncherUpdateService>(
|
||||
() => _i605.LauncherUpdateService(gh<_i43.LauncherLogService>()),
|
||||
);
|
||||
gh.lazySingleton<_i703.LauncherApiClient>(
|
||||
() => _i703.LauncherApiClient(
|
||||
launcherLogService: gh<_i43.LauncherLogService>(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import auto_updater_macos
|
||||
import file_picker
|
||||
import flutter_secure_storage_darwin
|
||||
import screen_retriever_macos
|
||||
@@ -12,6 +13,7 @@ import shared_preferences_foundation
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
PODS:
|
||||
- auto_updater_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Sparkle
|
||||
- file_picker (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_darwin (10.0.0):
|
||||
@@ -10,10 +13,12 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sparkle (2.9.3)
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
- flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
@@ -21,7 +26,13 @@ DEPENDENCIES:
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Sparkle
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
auto_updater_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos
|
||||
file_picker:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||
flutter_secure_storage_darwin:
|
||||
@@ -36,11 +47,13 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118
|
||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
screen_retriever_macos: c5508cc3c66ff0d4db650480cf0ab691e220d933
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
Sparkle: 39dbdf28637f6056fbf5ee6494bdbfae54f49f41
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
@@ -41,6 +41,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
auto_updater:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_updater
|
||||
sha256: "74fd008b021d15e4fc50fb5f1923870e6374ae831bb1168241c23bc35a4e898b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
auto_updater_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: auto_updater_macos
|
||||
sha256: ef9de0daf38d782d2243fe131503a9404d62df762f5386e5360020e43c01867f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
auto_updater_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: auto_updater_platform_interface
|
||||
sha256: ed5dff8cea18c58bc5ac787ff9122fedd1644272e02015d28c9b592f8078c5dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
auto_updater_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: auto_updater_windows
|
||||
sha256: "2bba20a71eee072f49b7267fedd5c4f1406c4b1b1e5b83932c634dbab75b80c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
bloc:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -45,6 +45,7 @@ dependencies:
|
||||
flutter_secure_storage: ^10.3.1
|
||||
shared_preferences: ^2.5.3
|
||||
window_manager: ^0.5.1
|
||||
auto_updater: 1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_log_service.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_update_service.dart';
|
||||
|
||||
void main() {
|
||||
group('LauncherUpdateService', () {
|
||||
test('does nothing on unsupported platforms', () async {
|
||||
final client = _FakeLauncherUpdateClient();
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: false,
|
||||
);
|
||||
|
||||
await service.checkForUpdates(
|
||||
feedUrl: 'https://moon-well.online/launcher/updates/appcast.xml',
|
||||
);
|
||||
|
||||
expect(client.calls, isEmpty);
|
||||
expect(logger.entries, isEmpty);
|
||||
});
|
||||
|
||||
test('logs disabled state when the feed URL is empty', () async {
|
||||
final client = _FakeLauncherUpdateClient();
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: true,
|
||||
);
|
||||
|
||||
await service.checkForUpdates(feedUrl: ' ');
|
||||
|
||||
expect(client.calls, isEmpty);
|
||||
expect(logger.entries.single, startsWith('INFO:'));
|
||||
});
|
||||
|
||||
test('rejects a non-HTTPS feed URL without invoking the updater', () async {
|
||||
final client = _FakeLauncherUpdateClient();
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: true,
|
||||
);
|
||||
|
||||
await service.checkForUpdates(
|
||||
feedUrl: 'http://moon-well.online/appcast.xml',
|
||||
);
|
||||
|
||||
expect(client.calls, isEmpty);
|
||||
expect(logger.entries.single, startsWith('ERROR:'));
|
||||
});
|
||||
|
||||
test('rejects an HTTPS URL without a host', () async {
|
||||
final client = _FakeLauncherUpdateClient();
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: true,
|
||||
);
|
||||
|
||||
await service.checkForUpdates(feedUrl: 'https:appcast.xml');
|
||||
|
||||
expect(client.calls, isEmpty);
|
||||
expect(logger.entries.single, startsWith('ERROR:'));
|
||||
});
|
||||
|
||||
test(
|
||||
'configures WinSparkle before starting one background check',
|
||||
() async {
|
||||
final client = _FakeLauncherUpdateClient();
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: true,
|
||||
);
|
||||
const feedUrl = 'https://moon-well.online/launcher/updates/appcast.xml';
|
||||
|
||||
await service.checkForUpdates(feedUrl: feedUrl);
|
||||
await service.checkForUpdates(feedUrl: feedUrl);
|
||||
|
||||
expect(client.calls, <String>[
|
||||
'setFeedUrl:$feedUrl',
|
||||
'disableScheduledChecks',
|
||||
'checkInBackground',
|
||||
]);
|
||||
expect(logger.entries.single, startsWith('INFO:'));
|
||||
},
|
||||
);
|
||||
|
||||
test('logs updater failures without propagating them', () async {
|
||||
final client = _FakeLauncherUpdateClient(failure: Exception('offline'));
|
||||
final logger = _FakeLauncherLogService();
|
||||
final service = LauncherUpdateService.forTesting(
|
||||
launcherLogService: logger,
|
||||
updateClient: client,
|
||||
isWindows: true,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
service.checkForUpdates(
|
||||
feedUrl: 'https://moon-well.online/launcher/updates/appcast.xml',
|
||||
),
|
||||
completes,
|
||||
);
|
||||
|
||||
expect(logger.entries.single, contains('offline'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeLauncherUpdateClient implements LauncherUpdateClient {
|
||||
_FakeLauncherUpdateClient({this.failure});
|
||||
|
||||
final Object? failure;
|
||||
final List<String> calls = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> setFeedUrl(String feedUrl) async {
|
||||
calls.add('setFeedUrl:$feedUrl');
|
||||
if (failure case final error?) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disableScheduledChecks() async {
|
||||
calls.add('disableScheduledChecks');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> checkInBackground() async {
|
||||
calls.add('checkInBackground');
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeLauncherLogService extends LauncherLogService {
|
||||
final List<String> entries = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> info(
|
||||
String message, {
|
||||
String? installationDir,
|
||||
Map<String, Object?> details = const <String, Object?>{},
|
||||
}) async {
|
||||
entries.add('INFO:$message');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> error(
|
||||
String message, {
|
||||
String? installationDir,
|
||||
Map<String, Object?> details = const <String, Object?>{},
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) async {
|
||||
entries.add('ERROR:$message:$error');
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <auto_updater_windows/auto_updater_windows_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AutoUpdaterWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AutoUpdaterWindowsPluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
auto_updater_windows
|
||||
flutter_secure_storage_windows
|
||||
screen_retriever_windows
|
||||
window_manager
|
||||
|
||||
@@ -55,6 +55,14 @@ END
|
||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// WinSparkle update signature verification
|
||||
//
|
||||
|
||||
DSAPub DSAPEM "resources\\dsa_pub.pem"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIDQjCCAjUGByqGSM44BAEwggIoAoIBAQC5rT00uqOtUBNAtY/Kzep4xFXX34Xh
|
||||
ArYMNMsc0lSCm7WzwklN1GTqKwp18gUxU1WX4YepePBwb4Mtm22VdYW76xKUMXpP
|
||||
fZ/d2e+l2jwo/urixdQ9vdGrYTkPJLS9Mn6q/7/XQnIx/MTroOcYNh1QJj3Gynxy
|
||||
y2C95Ty4yaxpjz9BP4TzT8XSvAy4ItdE0Xv2DqvruAP8VLR7HLAYMZFxvCuC65EC
|
||||
3nLIRByL8s7CGq8w2Wn7x8WKI/PQ/RvngMoc0xrK58qV+I2hFefXSYG4T7/aDvOb
|
||||
MTnIHlBZcgSEGCFDhtRqStKcBVW+ybk9SRpePU8rWL6QTtZTaBRlGlyRAh0A9A66
|
||||
ZMG2sKJqs2w6X+dUCXzeNUMSrYpQit6UjwKCAQBJShETJlN90ZvrtYSOUnTrXKlH
|
||||
rka5s6d1xFUNyqVNlI84oRC2gfw+eVAzbjtwFXv342uSHgC+kwQwK5UClxBTDxcm
|
||||
YK4yC3288a4BoCf76SkLcUOdEp2/Ef1MjMSWKorcX9SAKY98RihrRXoHLrmDN3vk
|
||||
/SVqgLv1Auu5X7yHMO2IDWvI29Sh6W13i2joXFt2O5ufsMY1k6sQtnT6R7sLphVw
|
||||
ifE6ulDwRNjSkPMFCrKyKpnqXmC1TvtAdkSd0DsWEfQuqp3878PZoVTJzHxrl1Y0
|
||||
3M7yrbVj5OojdGA9kX0zelrNCIPuH+2vmfeFis8y5XFFEOxc9yWezFYJ3TfEA4IB
|
||||
BQACggEAfoxCOU4ofKti7T7QVV3d5Oh6fIi2eTLG4utP6Fvtb6AktF6OPJWeDmDe
|
||||
g3ffLkKyx8/uA+KuZJO3/t/pHvzUvWNmc487bFZYd0dhlObQEu6Qg5uqDVWG4nqy
|
||||
K1zNmhVO97SqzBjUklyaxQre65w3XI4a1XSk8RAkcMzu5rze4P9BodEChaG/kEih
|
||||
qCJZivBjhMjaKi5cmE/S9G0zMrdTf152V8WaN0EjbFThC1izxk2j+76t+ALAe1Tt
|
||||
I1bY9RT8pzjdfRwWHja0cooPILCWL82pcolOAvrNzhkbR9sIcw5t9F517EQch4VD
|
||||
0W9SMpZl5uKcw3RS0oiFWyyKNWavvQ==
|
||||
-----END PUBLIC KEY-----
|
||||
Reference in New Issue
Block a user