-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
beb7075
commit 35d2721
Showing
13 changed files
with
622 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.