Skip to content

Commit 943e5ff

Browse files
authored
Merge pull request #67 from SunkenInTime/update/prerelease
Fix Windows desktop auto-updater and prerelease release flow
2 parents 07e528b + 4da9891 commit 943e5ff

37 files changed

Lines changed: 1155 additions & 76 deletions

.env.local

Lines changed: 0 additions & 6 deletions
This file was deleted.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ node_modules/
3333
.pub-cache/
3434
.pub/
3535
/build/
36+
/dist/
3637
release/out/
3738

3839
# Symbolication related
@@ -48,3 +49,7 @@ app.*.map.json
4849

4950
# FVM Version Cache
5051
.fvm/
52+
53+
# Local environment files
54+
.env
55+
.env.local

lib/const/settings.dart

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ import 'package:icarus/const/color_option.dart';
44
import 'package:shadcn_ui/shadcn_ui.dart';
55
import 'package:toastification/toastification.dart';
66

7+
const String kUpdateChannel = String.fromEnvironment(
8+
'ICARUS_UPDATE_CHANNEL',
9+
defaultValue: 'stable',
10+
);
11+
final String kResolvedUpdateChannel = normalizeUpdateChannel(kUpdateChannel);
12+
13+
String normalizeUpdateChannel(String channel) {
14+
switch (channel.trim().toLowerCase()) {
15+
case 'prerelease':
16+
case 'pre-release':
17+
case 'pre_release':
18+
return 'prerelease';
19+
case 'stable':
20+
default:
21+
return 'stable';
22+
}
23+
}
24+
25+
String updateChannelLabel(String channel) {
26+
return switch (normalizeUpdateChannel(channel)) {
27+
'prerelease' => 'Pre-release',
28+
_ => 'Stable',
29+
};
30+
}
31+
32+
Uri buildDesktopUpdaterArchiveUrl(String channel) {
33+
final resolvedChannel = normalizeUpdateChannel(channel);
34+
return Uri.parse(
35+
"https://sunkenintime.github.io/icarus/updates/windows/$resolvedChannel/app-archive.json",
36+
);
37+
}
38+
739
class Settings {
840
static const double agentSize = 35;
941
static const double agentSizeMin = 15;
@@ -50,11 +82,10 @@ class Settings {
5082
static final Uri dicordLink = Uri.parse("https://discord.gg/PN2uKwCqYB");
5183

5284
static const Duration autoSaveOffset = Duration(seconds: 15);
53-
static const int versionNumber = 64;
54-
static const String versionName = "4.2.1";
55-
static final Uri desktopUpdaterArchiveUrl = Uri.parse(
56-
"https://sunkenintime.github.io/icarus/updates/windows/stable/app-archive.json",
57-
);
85+
static const int versionNumber = 84;
86+
static const String versionName = "4.3.0";
87+
static final Uri desktopUpdaterArchiveUrl =
88+
buildDesktopUpdaterArchiveUrl(kResolvedUpdateChannel);
5889

5990
static const double sideBarContentWidth = 325;
6091
static const double sideBarPanelWidth = sideBarContentWidth + 20;

lib/services/app_error_reporter.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ class AppErrorReporter {
172172
final buffer = StringBuffer()
173173
..writeln('Icarus Debug Report')
174174
..writeln('Generated: ${formatDebugLogTimestamp(DateTime.now())}')
175+
..writeln(
176+
'Update channel: '
177+
'${updateChannelLabel(kResolvedUpdateChannel)} ($kResolvedUpdateChannel)',
178+
)
179+
..writeln(
180+
'Desktop updater manifest: ${Settings.desktopUpdaterArchiveUrl}')
175181
..writeln(
176182
'Application support directory: '
177183
'${_applicationSupportDirectoryPath ?? 'Unavailable'}',

lib/services/windows_desktop_update_controller.dart

Lines changed: 148 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
88
import 'package:http/http.dart' as http;
99
import 'package:path/path.dart' as path;
1010

11+
import 'package:icarus/services/app_error_reporter.dart';
1112
import 'package:icarus/services/windows_desktop_update_restart_service.dart';
1213

1314
class WindowsDesktopUpdateController extends ChangeNotifier {
@@ -22,8 +23,8 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
2223
_updater = updater ?? DesktopUpdater(),
2324
_httpClient = httpClient ?? http.Client(),
2425
_ownsHttpClient = httpClient == null,
25-
_restartService =
26-
restartService ?? WindowsDesktopUpdateRestartService(updater: updater) {
26+
_restartService = restartService ??
27+
WindowsDesktopUpdateRestartService(updater: updater) {
2728
if (autoCheck) {
2829
unawaited(checkVersion());
2930
}
@@ -107,6 +108,16 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
107108
0,
108109
(int total, FileHashModel file) => total + file.length,
109110
);
111+
_reportInfoSafely(
112+
'Desktop update detected.',
113+
source: 'WindowsDesktopUpdateController.checkVersion',
114+
error: <String, Object?>{
115+
'version': versionResponse.version,
116+
'changedFileCount': files.length,
117+
'downloadSizeBytes': _downloadSizeBytes,
118+
'updateUrl': versionResponse.url,
119+
},
120+
);
110121
notifyListeners();
111122
} catch (_) {
112123
// Leave the direct installer updater silent if the remote metadata fails.
@@ -131,6 +142,16 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
131142
final installDirectory = await _resolveInstallDirectory();
132143
final updateDirectory = Directory(path.join(installDirectory, 'update'));
133144
await _resetUpdateDirectory(updateDirectory);
145+
_reportInfoSafely(
146+
'Starting desktop update download.',
147+
source: 'WindowsDesktopUpdateController.downloadUpdate',
148+
error: <String, Object?>{
149+
'installDirectory': installDirectory,
150+
'updateDirectory': updateDirectory.path,
151+
'changedFileCount': files.length,
152+
'downloadSizeBytes': _downloadSizeBytes,
153+
},
154+
);
134155

135156
_skipUpdate = false;
136157
_isDownloading = true;
@@ -151,7 +172,8 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
151172
);
152173
completedBytes += file.length;
153174
_downloadedBytes = completedBytes;
154-
_downloadProgress = _calculateProgress(_downloadedBytes, _downloadSizeBytes);
175+
_downloadProgress =
176+
_calculateProgress(_downloadedBytes, _downloadSizeBytes);
155177
notifyListeners();
156178
}
157179

@@ -161,10 +183,32 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
161183
);
162184

163185
_isDownloaded = true;
164-
} catch (_) {
186+
_reportInfoSafely(
187+
'Desktop update download completed and verified.',
188+
source: 'WindowsDesktopUpdateController.downloadUpdate',
189+
error: <String, Object?>{
190+
'updateDirectory': updateDirectory.path,
191+
'changedFileCount': files.length,
192+
'downloadSizeBytes': _downloadSizeBytes,
193+
},
194+
);
195+
} catch (error, stackTrace) {
165196
_isDownloaded = false;
166-
await _cleanupPartialDownload(updateDirectory);
167-
rethrow;
197+
try {
198+
await _cleanupPartialDownload(updateDirectory);
199+
} catch (cleanupError, cleanupStackTrace) {
200+
_reportInfoSafely(
201+
'Failed to clean up a partial desktop update download.',
202+
source: 'WindowsDesktopUpdateController.downloadUpdate',
203+
error: <String, Object?>{
204+
'updateDirectory': updateDirectory.path,
205+
'downloadError': error.toString(),
206+
'cleanupError': cleanupError.toString(),
207+
'cleanupStackTrace': cleanupStackTrace.toString(),
208+
},
209+
);
210+
}
211+
Error.throwWithStackTrace(error, stackTrace);
168212
} finally {
169213
_isDownloading = false;
170214
notifyListeners();
@@ -177,6 +221,16 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
177221
throw StateError('No desktop update is available to apply.');
178222
}
179223

224+
_reportInfoSafely(
225+
'Restart requested to apply downloaded desktop update.',
226+
source: 'WindowsDesktopUpdateController.restartApp',
227+
error: <String, Object?>{
228+
'version': update.version,
229+
'changedFileCount': _requiredChangedFiles(update).length,
230+
'updaterLogPath':
231+
WindowsDesktopUpdateRestartService.resolveUpdaterLogPath(),
232+
},
233+
);
180234
await _restartService.restartIntoDownloadedUpdate(
181235
expectedFiles: _requiredChangedFiles(update),
182236
);
@@ -211,7 +265,8 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
211265
await destination.delete();
212266
}
213267
if (attempt < 2) {
214-
await Future<void>.delayed(Duration(milliseconds: 400 * (attempt + 1)));
268+
await Future<void>.delayed(
269+
Duration(milliseconds: 400 * (attempt + 1)));
215270
}
216271
}
217272
}
@@ -242,27 +297,15 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
242297
var fileBytesReceived = 0;
243298

244299
try {
245-
await response.stream.listen(
246-
(List<int> chunk) {
247-
sink.add(chunk);
248-
fileBytesReceived += chunk.length;
249-
_downloadedBytes = completedBytes + fileBytesReceived;
250-
_downloadProgress =
251-
_calculateProgress(_downloadedBytes, totalBytes);
252-
notifyListeners();
253-
},
254-
onDone: () async {
255-
await sink.close();
256-
},
257-
onError: (Object error) async {
258-
await sink.close();
259-
throw error;
260-
},
261-
cancelOnError: true,
262-
).asFuture<void>();
263-
} catch (_) {
300+
await for (final List<int> chunk in response.stream) {
301+
sink.add(chunk);
302+
fileBytesReceived += chunk.length;
303+
_downloadedBytes = completedBytes + fileBytesReceived;
304+
_downloadProgress = _calculateProgress(_downloadedBytes, totalBytes);
305+
notifyListeners();
306+
}
307+
} finally {
264308
await sink.close();
265-
rethrow;
266309
}
267310
}
268311

@@ -307,7 +350,9 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
307350
}
308351

309352
Future<String> _resolveInstallDirectory() async {
310-
final executablePath = (await _updater.getExecutablePath())?.trim();
353+
final executablePath =
354+
WindowsDesktopUpdateRestartService.normalizeExecutablePath(
355+
await _updater.getExecutablePath());
311356
if (executablePath == null || executablePath.isEmpty) {
312357
throw const FileSystemException(
313358
'Unable to resolve the installed executable path.',
@@ -318,18 +363,25 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
318363
}
319364

320365
Future<void> _resetUpdateDirectory(Directory updateDirectory) async {
321-
if (await updateDirectory.exists()) {
322-
await updateDirectory.delete(recursive: true);
323-
}
366+
await _deleteDirectoryIfExistsWithRetry(
367+
updateDirectory,
368+
source: 'WindowsDesktopUpdateController._resetUpdateDirectory',
369+
bestEffort: false,
370+
);
324371
await updateDirectory.create(recursive: true);
325372
}
326373

327374
Future<void> _cleanupPartialDownload(Directory updateDirectory) async {
328-
if (await updateDirectory.exists()) {
329-
await updateDirectory.delete(recursive: true);
375+
try {
376+
await _deleteDirectoryIfExistsWithRetry(
377+
updateDirectory,
378+
source: 'WindowsDesktopUpdateController._cleanupPartialDownload',
379+
bestEffort: true,
380+
);
381+
} finally {
382+
_downloadedBytes = 0;
383+
_downloadProgress = 0;
330384
}
331-
_downloadedBytes = 0;
332-
_downloadProgress = 0;
333385
}
334386

335387
List<FileHashModel> _requiredChangedFiles(ItemModel update) {
@@ -380,4 +432,65 @@ class WindowsDesktopUpdateController extends ChangeNotifier {
380432
final hash = await Blake2b().hash(bytes);
381433
return base64Encode(hash.bytes);
382434
}
435+
436+
Future<void> _deleteDirectoryIfExistsWithRetry(
437+
Directory directory, {
438+
required String source,
439+
required bool bestEffort,
440+
}) async {
441+
if (!await directory.exists()) {
442+
return;
443+
}
444+
445+
Object? lastError;
446+
StackTrace? lastStackTrace;
447+
448+
for (var attempt = 0; attempt < 5; attempt++) {
449+
try {
450+
await directory.delete(recursive: true);
451+
return;
452+
} catch (error, stackTrace) {
453+
lastError = error;
454+
lastStackTrace = stackTrace;
455+
if (attempt == 4) {
456+
break;
457+
}
458+
459+
await Future<void>.delayed(
460+
Duration(milliseconds: 150 * (attempt + 1)),
461+
);
462+
}
463+
}
464+
465+
if (bestEffort) {
466+
_reportInfoSafely(
467+
'Unable to delete the staged desktop update directory after retries.',
468+
source: source,
469+
error: <String, Object?>{
470+
'directory': directory.path,
471+
'error': lastError.toString(),
472+
'stackTrace': lastStackTrace.toString(),
473+
},
474+
);
475+
return;
476+
}
477+
478+
Error.throwWithStackTrace(lastError!, lastStackTrace!);
479+
}
480+
481+
void _reportInfoSafely(
482+
String message, {
483+
required String source,
484+
Object? error,
485+
}) {
486+
try {
487+
AppErrorReporter.reportInfo(
488+
message,
489+
source: source,
490+
error: error,
491+
);
492+
} catch (_) {
493+
// Logging must never interrupt the update flow.
494+
}
495+
}
383496
}

0 commit comments

Comments
 (0)