Skip to content

Commit

Permalink
add built in updater
Browse files Browse the repository at this point in the history
  • Loading branch information
ArthurHeitmann committed Dec 1, 2024
1 parent beb7075 commit 35d2721
Show file tree
Hide file tree
Showing 13 changed files with 622 additions and 4 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/CI_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ jobs:
run: flutter pub get

- name: Build Release
run: flutter build windows --release
run: |
flutter build windows --release
dart compile exe lib\version\updaterCli.dart -o build\windows\x64\runner\Release\data\flutter_assets\assets\bins\updater.exe
- name: Copy dlls
run: |
Expand Down
17 changes: 17 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
Expand All @@ -20,6 +21,7 @@ import 'utils/assetDirFinder.dart';
import 'utils/fileOpenCommand.dart';
import 'utils/loggingWrapper.dart';
import 'utils/utils.dart';
import 'version/updateRestartData.dart';
import 'widgets/EditorLayout.dart';
import 'widgets/misc/ChangeNotifierWidget.dart';
import 'widgets/misc/mousePosition.dart';
Expand Down Expand Up @@ -99,7 +101,22 @@ void init(List<String> args) async {
runApp(const MyApp());

unawaited(waitForNextFrame().then((_) async {
if (args.length >= 2) {
if (args[0] == "--update-data") {
var b64Json = args[1];
var updateDataJson = jsonDecode(utf8.decode(base64Decode(b64Json)));
var updateData = UpdateRestartData.fromJson(updateDataJson);
for (var file in updateData.openedHierarchyFiles) {
await openHierarchyManager.openFile(file);
}
for (var file in updateData.openedFiles) {
areasManager.openFile(file);
}
}
}
for (var arg in args) {
if (!await File(arg).exists() && !await Directory(arg).exists())
continue;
await openHierarchyManager.openFile(arg);
if (await canOpenAsFile(arg))
areasManager.openFile(arg);
Expand Down
3 changes: 3 additions & 0 deletions lib/stateManagement/openFiles/openFileTypes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../utils/utils.dart';
import '../../widgets/filesView/FileType.dart';
import '../hasUuid.dart';
import '../miscValues.dart';
import '../preferencesData.dart';
import '../undoable.dart';
import 'types/BnkFilePlaylistData.dart';
import 'types/BxmFileData.dart';
Expand Down Expand Up @@ -97,6 +98,8 @@ abstract class OpenFileData with HasUuid, Undoable, Disposable, HasUndoHistory {
return WtaWtpData(name, path, secondaryName: secondaryName, isWtb: path.endsWith(".wtb"));
else if (path.endsWith(".est") || path.endsWith(".sst"))
return EstFileData(name, path, secondaryName: secondaryName);
else if (path == "preferences")
return PreferencesData();
else
return TextFileData(name, path, secondaryName: secondaryName);
}
Expand Down
3 changes: 2 additions & 1 deletion lib/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,8 @@ void revealFileInExplorer(String path) {
}

const datExtensions = { ".dat", ".dtt", ".evn", ".eff", ".eft" };
const bxmExtensions = { ".bxm", ".gad", ".sar", ".seq" };
const bxmExtensions = { ".bxm", ".gad", ".sar", ".seq", ".vca", ".sab" };
const datSubExtractDir = "nier2blender_extracted";

bool strEndsWithDat(String str) {
for (var ext in datExtensions) {
Expand Down
129 changes: 129 additions & 0 deletions lib/version/installRelease.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart';
import 'package:http/http.dart' as http;

import '../stateManagement/events/statusInfo.dart';
import '../stateManagement/hierarchy/FileHierarchy.dart';
import '../stateManagement/hierarchy/HierarchyEntryTypes.dart';
import '../stateManagement/openFiles/openFilesManager.dart';
import '../utils/assetDirFinder.dart';
import '../utils/utils.dart';
import 'retrieveReleases.dart';
import 'updateRestartData.dart';

Future<void> installRelease(GitHubReleaseInfo release, StreamController<String> updateStepStream, StreamController<double> updateProgressStream) async {
updateStepStream.add("Preparing update...");
updateProgressStream.add(0);
var downloadName = basename(release.downloadUrl);
var appDir = dirname(Platform.resolvedExecutable);
var updateDir = join(appDir, "_update");
var updateDirCache = join(updateDir, "cache");
var updateDirTemp = join(updateDir, "temp");
String errorMessage = "";
IOSink? updateFile_;
try {
if (await Directory(updateDirTemp).exists()) {
errorMessage = "Failed to delete temp update directory";
await Directory(updateDirTemp).delete(recursive: true);
}
errorMessage = "Failed to create update directory";
await Directory(updateDir).create(recursive: true);
await Directory(updateDirCache).create();
await Directory(updateDirTemp).create();

errorMessage = "7z.exe or dll not found";
var exe7z = await _get7zExe(updateDirCache);
errorMessage = "updater.exe not found";
var exeUpdater = await _getUpdaterExe(updateDirTemp);

var updateFilePath = join(updateDirCache, downloadName);

var needsDownload = !await File(updateFilePath).exists() || await File(updateFilePath).length() != release.downloadSize;
if (needsDownload) {
var updateFile = File(updateFilePath).openWrite();
updateFile_ = updateFile;
errorMessage = "Failed to download $downloadName";
updateStepStream.add("Downloading $downloadName...");
var response = await http.Client().send(http.Request("GET", Uri.parse(release.downloadUrl)));
var totalBytes = response.contentLength ?? release.downloadSize;
var receivedBytes = 0;
var downloadCompleter = Completer<void>();
response.stream.listen(
(chunk) {
updateFile.add(chunk);
receivedBytes += chunk.length;
updateProgressStream.add(receivedBytes / totalBytes);
},
onDone: () async {
downloadCompleter.complete();
},
onError: (e) {
downloadCompleter.completeError(e);
},
);
await downloadCompleter.future;
await updateFile_.close();
updateFile_ = null;
}

errorMessage = "Failed to extract $downloadName";
updateStepStream.add("Extracting $downloadName...");
var extractDir = join(updateDirTemp, basenameWithoutExtension(release.downloadUrl));
await Directory(extractDir).create();
var result = await Process.run(exe7z, ["x", "-y", "-o$extractDir", updateFilePath]);
if (result.exitCode != 0) {
throw Exception("7z failed with exit code ${result.exitCode} and message: ${result.stderr}");
}
await Future.delayed(const Duration(milliseconds: 250));

var backupDir = join(updateDirTemp, "backup");
await Directory(backupDir).create();

var openFiles = areasManager.areas
.map((e) => e.files)
.expand((e) => e)
.map((e) => e.path)
.toList();
var openHierarchies = openHierarchyManager.children
.whereType<FileHierarchyEntry>()
.map((e) => e.path).toList();
var restartData = UpdateRestartData(openFiles, openHierarchies);
var restartDataJson = restartData.toJson();
var restartDataJsonString = base64Encode(utf8.encode(jsonEncode(restartDataJson)));

var logFilePath = join(updateDirTemp, "update.log");
await Process.start(
"$exeUpdater --app-dir $appDir --extracted-dir $extractDir --backup-dir $backupDir --exe-path ${Platform.resolvedExecutable} --restart-data $restartDataJsonString > $logFilePath 2>&1",
[],
runInShell: true,
mode: ProcessStartMode.detachedWithStdio,
);
} catch (e, st) {
messageLog.add("$errorMessage: $e\n$st");
showToast(errorMessage);
throw Exception(errorMessage);
} finally {
await updateFile_?.close();
}
}

Future<String> _get7zExe(String cacheDir) async {
var src7zExe = join(assetsDir!, "bins", "7z.exe");
var src7zDll = join(assetsDir!, "bins", "7z.dll");
var dst7zExe = join(cacheDir, "7z.exe");
var dst7zDll = join(cacheDir, "7z.dll");
await File(src7zExe).copy(dst7zExe);
await File(src7zDll).copy(dst7zDll);
return dst7zExe;
}

Future<String> _getUpdaterExe(String tempDir) async {
var srcUpdaterExe = join(assetsDir!, "bins", "updater.exe");
var dstUpdaterExe = join(tempDir, "updater.exe");
await File(srcUpdaterExe).copy(dstUpdaterExe);
return dstUpdaterExe;
}
49 changes: 49 additions & 0 deletions lib/version/retrieveReleases.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

import 'dart:convert';

import 'package:http/http.dart' as http;

import 'version.dart';

const _apiEndpoint = "https://api.github.com/repos/ArthurHeitmann/F-SERVO/releases";

class GitHubReleaseInfo {
final String tagName;
final String url;
final DateTime publishedAt;
final String downloadUrl;
final int downloadSize;
FServoVersion? _version;
FServoVersion? get version => _version ??= FServoVersion.parse(tagName);

GitHubReleaseInfo(this.tagName, this.url, this.publishedAt, this.downloadUrl, this.downloadSize);
}

Future<List<GitHubReleaseInfo>> retrieveReleases() async {
var response = await http.get(Uri.parse(_apiEndpoint));
if (response.statusCode != 200)
throw Exception("Failed to retrieve releases: ${response.body}");
var json = jsonDecode(response.body);
var releases = (json as List)
.map((e) {
return GitHubReleaseInfo(
e["tag_name"],
e["html_url"],
DateTime.parse(e["published_at"]),
(e["assets"] as List).firstOrNull?["browser_download_url"],
(e["assets"] as List).firstOrNull?["size"],
);
})
.where((e) => e.version != null)
.toList();
releases.sort((a, b) => b.version!.compareTo(a.version!));
return releases;
}

GitHubReleaseInfo? updateRelease(List<GitHubReleaseInfo> releases, FServoVersion currentVersion) {
return releases
.where((e) => e.version != null)
.where((e) => e.version!.branch == currentVersion.branch)
.where((e) => e.version! > currentVersion)
.firstOrNull;
}
19 changes: 19 additions & 0 deletions lib/version/updateRestartData.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

class UpdateRestartData {
final List<String> openedFiles;
final List<String> openedHierarchyFiles;

const UpdateRestartData(this.openedFiles, this.openedHierarchyFiles);

Map toJson() => {
"openedFiles": openedFiles,
"openedHierarchyFiles": openedHierarchyFiles,
};

factory UpdateRestartData.fromJson(Map<String, dynamic> json) {
return UpdateRestartData(
List<String>.from(json["openedFiles"]),
List<String>.from(json["openedHierarchyFiles"]),
);
}
}
109 changes: 109 additions & 0 deletions lib/version/updaterCli.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@

import 'dart:io';

import 'package:args/args.dart';
import 'package:path/path.dart';

void main(List<String> args) async {
var parser = ArgParser();
parser.addOption("app-dir", mandatory: true);
parser.addOption("extracted-dir", mandatory: true);
parser.addOption("backup-dir", mandatory: true);
parser.addOption("exe-path", mandatory: true);
parser.addOption("restart-data", mandatory: true);
var results = parser.parse(args);
var appDir = results.option("app-dir")!;
var extractedDir = results.option("extracted-dir")!;
var backupDir = results.option("backup-dir")!;
var exePath = results.option("exe-path")!;
var restartData = results.option("restart-data")!;

const deletableExtensions = [".dll", ".exe"];
const dataFolderName = "data";
List<FileSystemEntity> oldFilesToMove = Directory(appDir).listSync()
.where((e) => e is File && deletableExtensions.contains(extension(e.path)))
.toList();
oldFilesToMove.add(Directory(join(appDir, dataFolderName)));

var newFiles = Directory(extractedDir).listSync();

print("Killing F-SERVO.exe...");
Process.runSync("taskkill", ["/F", "/IM", "F-SERVO.exe"]);

List<FileSystemEntity> movedOldFiles = [];
List<FileSystemEntity> movedNewFiles = [];
try {
print("Moving files to backup directory...");
for (var file in oldFilesToMove) {
var newPath = join(backupDir, basename(file.path));
print("Moving ${file.path} to $newPath");
await _tryRename(file, newPath);
movedOldFiles.add(file);
}
print("Moving extracted files to app directory...");
for (var file in newFiles) {
var newPath = join(appDir, basename(file.path));
print("Moving ${file.path} to $newPath");
await _tryRename(file, newPath);
movedNewFiles.add(file);
}
} catch (e) {
try {
print("Error moving files, recovering...");
print("Deleting new files...");
for (var file in movedNewFiles) {
print("Deleting ${file.path}");
await _tryDelete(file);
}
print("Moving old files back...");
for (var file in movedOldFiles) {
var newPath = join(appDir, basename(file.path));
print("Moving ${file.path} back to $newPath");
await _tryRename(file, newPath);
}
print("Recovery complete");
} catch (e, st) {
print("Error moving files back: $e\n$st");
}
print("Update failed. Press enter to exit.");
stdin.readLineSync();
rethrow;
}

print("Restarting F-SERVO.exe...");
var result = await Process.start(exePath, ["--update-data", restartData], mode: ProcessStartMode.detached);
await result.exitCode;

print("Update complete");
}

const _maxAttempts = 5;
Future<void> _tryRename(FileSystemEntity file, String path, [int attempt = 1]) async {
try {
await file.rename(path);
} catch (e) {
if (attempt < _maxAttempts) {
await Future.delayed(Duration(milliseconds: attempt * 100));
await _tryRename(file, path, attempt + 1);
} else {
rethrow;
}
}
}

Future<void> _tryDelete(FileSystemEntity file, [int attempt = 1]) async {
try {
if (file is Directory) {
await file.delete(recursive: true);
} else {
await file.delete();
}
} catch (e) {
if (attempt < _maxAttempts) {
await Future.delayed(Duration(milliseconds: attempt * 100));
await _tryDelete(file, attempt + 1);
} else {
rethrow;
}
}
}
Loading

0 comments on commit 35d2721

Please sign in to comment.