From d478650e9bcefe2c2c808466df881caf94bff87a Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 22 May 2024 16:49:03 +0200 Subject: [PATCH] 9.0.4 --- cli/lib/cli.dart | 19 +- cli/lib/src/game.dart | 4 +- cli/lib/src/server.dart | 22 +- common/lib/common.dart | 1 + common/lib/src/model/process.dart | 23 ++ common/lib/src/util/authenticator.dart | 16 +- common/lib/src/util/build.dart | 123 +++++++---- common/lib/src/util/dll.dart | 14 +- common/lib/src/util/matchmaker.dart | 11 +- common/lib/src/util/path.dart | 2 +- common/lib/src/util/process.dart | 199 ++++++++++++------ .../controller/authenticator_controller.dart | 6 +- .../src/controller/hosting_controller.dart | 2 +- .../src/controller/matchmaker_controller.dart | 6 +- gui/lib/src/controller/server_controller.dart | 73 ++++--- gui/lib/src/dialog/implementation/server.dart | 1 + gui/lib/src/util/daemon.dart | 6 +- gui/lib/src/util/dll.dart | 2 +- gui/lib/src/widget/game_start_button.dart | 70 +++--- gui/lib/test.dart | 12 ++ 20 files changed, 383 insertions(+), 229 deletions(-) create mode 100644 common/lib/src/model/process.dart create mode 100644 gui/lib/test.dart diff --git a/cli/lib/cli.dart b/cli/lib/cli.dart index 0fc1a7f..4f30256 100644 --- a/cli/lib/cli.dart +++ b/cli/lib/cli.dart @@ -57,9 +57,24 @@ void main(List args) async { throw Exception("Missing game executable at: ${version.location.path}"); } + final serverHost = result["server-host"]?.trim(); + if(serverHost?.isEmpty == true){ + throw Exception("Missing host name"); + } + + final serverPort = result["server-port"]?.trim(); + if(serverPort?.isEmpty == true){ + throw Exception("Missing port"); + } + + final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort); + if(serverPort != null && serverPortNumber == null){ + throw Exception("Invalid port, use only numbers"); + } + var started = await startServerCli( - result["server-host"], - result["server-port"], + serverHost, + serverPortNumber, ServerType.values.firstWhere((element) => element.name == result["server-type"]) ); if(!started){ diff --git a/cli/lib/src/game.dart b/cli/lib/src/game.dart index ff5dd50..5af0a14 100644 --- a/cli/lib/src/game.dart +++ b/cli/lib/src/game.dart @@ -30,11 +30,11 @@ Future startGame() async { Future _startLauncherProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.launcher == null) { + if (dummyVersion.launcherExecutable == null) { return; } - _launcherProcess = await Process.start(dummyVersion.launcher!.path, []); + _launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []); suspend(_launcherProcess!.pid); } diff --git a/cli/lib/src/server.dart b/cli/lib/src/server.dart index fa74c04..1b737e8 100644 --- a/cli/lib/src/server.dart +++ b/cli/lib/src/server.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; import 'package:reboot_common/src/util/authenticator.dart' as server; -Future startServerCli(String? host, String? port, ServerType type) async { +Future startServerCli(String? host, int? port, ServerType type) async { stdout.writeln("Starting backend server..."); switch(type){ case ServerType.local: - var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort.toString()); + var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort); if(result == null){ throw Exception("Local backend server is not running"); } @@ -17,7 +17,7 @@ Future startServerCli(String? host, String? port, ServerType type) async { case ServerType.embedded: stdout.writeln("Starting an embedded server..."); await server.startEmbeddedAuthenticator(false); - var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort.toString()); + var result = await pingAuthenticator(host ?? kDefaultAuthenticatorHost, port ?? kDefaultAuthenticatorPort); if(result == null){ throw Exception("Cannot start embedded server"); } @@ -37,21 +37,7 @@ Future startServerCli(String? host, String? port, ServerType type) async { } } -Future _changeReverseProxyState(String host, String port) async { - host = host.trim(); - if(host.isEmpty){ - throw Exception("Missing host name"); - } - - port = port.trim(); - if(port.isEmpty){ - throw Exception("Missing port"); - } - - if(int.tryParse(port) == null){ - throw Exception("Invalid port, use only numbers"); - } - +Future _changeReverseProxyState(String host, int port) async { try{ var uri = await pingAuthenticator(host, port); if(uri == null){ diff --git a/common/lib/common.dart b/common/lib/common.dart index aa36d96..f63802b 100644 --- a/common/lib/common.dart +++ b/common/lib/common.dart @@ -9,6 +9,7 @@ export 'package:reboot_common/src/model/server_result.dart'; export 'package:reboot_common/src/model/server_type.dart'; export 'package:reboot_common/src/model/update_status.dart'; export 'package:reboot_common/src/model/update_timer.dart'; +export 'package:reboot_common/src/model/process.dart'; export 'package:reboot_common/src/util/authenticator.dart'; export 'package:reboot_common/src/util/build.dart'; export 'package:reboot_common/src/util/dll.dart'; diff --git a/common/lib/src/model/process.dart b/common/lib/src/model/process.dart new file mode 100644 index 0000000..574b90c --- /dev/null +++ b/common/lib/src/model/process.dart @@ -0,0 +1,23 @@ +class Win32Process { + final int pid; + final Stream stdOutput; + final Stream errorOutput; + + Win32Process({ + required this.pid, + required this.stdOutput, + required this.errorOutput + }); +} + +class PrimitiveWin32Process { + final int pid; + final int stdOutputHandle; + final int errorOutputHandle; + + PrimitiveWin32Process({ + required this.pid, + required this.stdOutputHandle, + required this.errorOutputHandle + }); +} \ No newline at end of file diff --git a/common/lib/src/util/authenticator.dart b/common/lib/src/util/authenticator.dart index cf6a9f5..cbbc6b8 100644 --- a/common/lib/src/util/authenticator.dart +++ b/common/lib/src/util/authenticator.dart @@ -7,17 +7,15 @@ import 'package:shelf_proxy/shelf_proxy.dart'; final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator"); final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe"); -Future startEmbeddedAuthenticator(bool detached) async => startBackgroundProcess( +Future startEmbeddedAuthenticator(bool detached) async => startProcess( executable: authenticatorStartExecutable, - window: detached + window: detached, + ); -Future startRemoteAuthenticatorProxy(Uri uri) async { - print("CALLED: $uri"); - return await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort); -} +Future startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort); -Future isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort.toString()) == null; +Future isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort) == null; Future freeAuthenticatorPort() async { await killProcessByPort(kDefaultAuthenticatorPort); @@ -29,14 +27,14 @@ Future freeAuthenticatorPort() async { return false; } -Future pingAuthenticator(String host, String port, [bool https=false]) async { +Future pingAuthenticator(String host, int port, [bool https=false]) async { var hostName = _getHostName(host); var declaredScheme = _getScheme(host); try{ var uri = Uri( scheme: declaredScheme ?? (https ? "https" : "http"), host: hostName, - port: int.parse(port), + port: port, path: "unknown" ); var client = HttpClient() diff --git a/common/lib/src/util/build.dart b/common/lib/src/util/build.dart index 9521216..fec2db5 100644 --- a/common/lib/src/util/build.dart +++ b/common/lib/src/util/build.dart @@ -5,18 +5,35 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; const String kStopBuildDownloadSignal = "kill"; -final Dio _dio = Dio(); +final Dio _dio = _buildDioInstance(); +Dio _buildDioInstance() { + final dio = Dio(); + final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter; + httpClientAdapter.createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + return client; + }; + return dio; +} + final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"; final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); const String _manifestSourceUrl = "https://manifest.fnbuilds.services"; const int _maxDownloadErrors = 30; Future> fetchBuilds(ignored) async { + (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () => + HttpClient() + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]); final data = []; for(final result in results) { @@ -108,12 +125,6 @@ Future downloadArchiveBuild(FortniteBuildDownloadOptions options) async { final response = _downloadArchive(options, tempFile, startTime); await Future.any([stopped.future, response]); if(!stopped.isCompleted) { - var awaitedResponse = await response; - if (!awaitedResponse.statusCode.toString().startsWith("20")) { - options.port.send("Erroneous status code: ${awaitedResponse.statusCode}"); - return; - } - await _extractArchive(stopped, extension, tempFile, options); } @@ -211,9 +222,23 @@ Future _downloadArchive(FortniteBuildDownloadOptions options, File tem }, deleteOnError: false, options: Options( - headers: byteStart == null ? null : { - "Range": "bytes=${byteStart}-" + validateStatus: (statusCode) { + if(statusCode == 200) { + return true; } + + if(statusCode == 403 || statusCode == 503) { + throw Exception("The connection was denied: your firewall might be blocking the download"); + } + + throw Exception("The build downloader is not available right now"); + }, + headers: byteStart == null || byteStart <= 0 ? { + "Cookie": "_c_t_c=1" + } : { + "Cookie": "_c_t_c=1", + "Range": "bytes=${byteStart}-" + }, ) ); }catch(error) { @@ -226,17 +251,28 @@ Future _downloadArchive(FortniteBuildDownloadOptions options, File tem } Future _extractArchive(Completer stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { - var startTime = DateTime.now().millisecondsSinceEpoch; - Process? process; + final startTime = DateTime.now().millisecondsSinceEpoch; + Win32Process? process; switch (extension.toLowerCase()) { case ".zip": - process = await Process.start( - "${assetsDirectory.path}\\build\\7zip.exe", - ["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path] + final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe"); + if(!sevenZip.existsSync()) { + throw "Corrupted installation: missing 7zip.exe"; + } + + process = await startProcess( + executable: sevenZip, + args: [ + "x", + "-bsp1", + '-o"${options.destination.path}"', + "-y", + '"${tempFile.path}"' + ], ); - process.stdout.listen((bytes) { + process.stdOutput.listen((data) { + print(data); final now = DateTime.now().millisecondsSinceEpoch; - final data = utf8.decode(bytes); if(data == "Everything is Ok") { options.port.send(FortniteBuildDownloadProgress(100, 0, true)); return; @@ -252,42 +288,45 @@ Future _extractArchive(Completer stopped, String extension, File }); break; case ".rar": - process = await Process.start( - "${assetsDirectory.path}\\build\\winrar.exe", - ["x", "-o+", tempFile.path, "*.*", options.destination.path] + final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); + if(!winrar.existsSync()) { + throw "Corrupted installation: missing winrar.exe"; + } + + process = await startProcess( + executable: winrar, + args: [ + "x", + "-o+", + tempFile.path, + "*.*", + options.destination.path + ] ); - process.stdout.listen((event) { + process.stdOutput.listen((data) { + print(data); final now = DateTime.now().millisecondsSinceEpoch; - final data = utf8.decode(event); - data.replaceAll("\r", "") - .replaceAll("\b", "") - .trim() - .split("\n") - .forEach((entry) { - if(entry == "All OK") { - options.port.send(FortniteBuildDownloadProgress(100, 0, true)); - return; - } + data.replaceAll("\r", "").replaceAll("\b", "").trim(); + if(data == "All OK") { + options.port.send(FortniteBuildDownloadProgress(100, 0, true)); + return; + } - final element = _rarProgressRegex.firstMatch(entry)?.group(1); - if(element == null) { - return; - } + final element = _rarProgressRegex.firstMatch(data)?.group(1); + if(element == null) { + return; + } - final percentage = int.parse(element).toDouble(); - _onProgress(startTime, now, percentage, true, options); - }); - }); - process.stderr.listen((event) { - final data = utf8.decode(event); - options.port.send(data); + final percentage = int.parse(element).toDouble(); + _onProgress(startTime, now, percentage, true, options); }); + process.errorOutput.listen((data) => options.port.send(data)); break; default: throw ArgumentError("Unexpected file extension: $extension}"); } - await Future.any([stopped.future, process.exitCode]); + await Future.any([stopped.future, watchProcess(process.pid)]); } void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) { diff --git a/common/lib/src/util/dll.dart b/common/lib/src/util/dll.dart index 32fa68c..a719b7d 100644 --- a/common/lib/src/util/dll.dart +++ b/common/lib/src/util/dll.dart @@ -19,9 +19,13 @@ Future hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = Future downloadCriticalDll(String name, String outputPath) async { final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/tree/master/gui/assets/dlls/$name")); + if(response.statusCode != 200) { + throw Exception("Cannot download $name: status code ${response.statusCode}"); + } + final output = File(outputPath); await output.parent.create(recursive: true); - await output.writeAsBytes(response.bodyBytes); + await output.writeAsBytes(response.bodyBytes, flush: true); } Future downloadRebootDll(String url) async { @@ -29,12 +33,16 @@ Future downloadRebootDll(String url) async { final now = DateTime.now(); try { final response = await http.get(Uri.parse(url)); + if(response.statusCode != 200) { + throw Exception("Cannot download reboot.zip: status code ${response.statusCode}"); + } + outputDir = await installationDirectory.createTemp("reboot_out"); final tempZip = File("${outputDir.path}\\reboot.zip"); - await tempZip.writeAsBytes(response.bodyBytes); + await tempZip.writeAsBytes(response.bodyBytes, flush: true); await extractFileToDisk(tempZip.path, outputDir.path); final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path); - await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes()); + await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true); return now.millisecondsSinceEpoch; } finally{ if(outputDir != null) { diff --git a/common/lib/src/util/matchmaker.dart b/common/lib/src/util/matchmaker.dart index 79cbe7a..36ad4be 100644 --- a/common/lib/src/util/matchmaker.dart +++ b/common/lib/src/util/matchmaker.dart @@ -13,9 +13,10 @@ String? _lastIp; String? _lastPort; Semaphore _semaphore = Semaphore(); -Future startEmbeddedMatchmaker(bool detached) async => startBackgroundProcess( +Future startEmbeddedMatchmaker(bool detached) async => startProcess( executable: matchmakerStartExecutable, - window: detached + window: detached, + ); Stream watchMatchmakingIp() async* { @@ -74,7 +75,7 @@ Future writeMatchmakingIp(String text) async { await matchmakerConfigFile.writeAsString(config.toString(), flush: true); } -Future isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort.toString()) == null; +Future isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort) == null; Future freeMatchmakerPort() async { await killProcessByPort(kDefaultMatchmakerPort); @@ -86,14 +87,14 @@ Future freeMatchmakerPort() async { return false; } -Future pingMatchmaker(String host, String port, [bool wss=false]) async { +Future pingMatchmaker(String host, int port, [bool wss=false]) async { var hostName = _getHostName(host); var declaredScheme = _getScheme(host); try{ var uri = Uri( scheme: declaredScheme ?? (wss ? "wss" : "ws"), host: hostName, - port: int.parse(port) + port: port ); var completer = Completer(); var socket = await WebSocket.connect(uri.toString()); diff --git a/common/lib/src/util/path.dart b/common/lib/src/util/path.dart index fbb7711..07ab079 100644 --- a/common/lib/src/util/path.dart +++ b/common/lib/src/util/path.dart @@ -72,7 +72,7 @@ extension FortniteVersionExtension on FortniteVersion { return output; } - File? get launcher => findExecutable(location, "FortniteLauncher.exe"); + File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe"); File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index 560877d..40e9569 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -1,12 +1,14 @@ // ignore_for_file: non_constant_identifier_names import 'dart:async'; +import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'package:ffi/ffi.dart'; +import 'package:reboot_common/src/model/process.dart'; import 'package:win32/win32.dart'; import '../constant/game.dart'; @@ -30,15 +32,16 @@ final _CreateRemoteThread = _kernel32.lookupFunction< Pointer lpParameter, int dwCreationFlags, Pointer lpThreadId)>('CreateRemoteThread'); +const chunkSize = 1024; Future injectDll(int pid, String dll) async { - var process = OpenProcess( + final process = OpenProcess( 0x43A, 0, pid ); - var processAddress = GetProcAddress( + final processAddress = GetProcAddress( GetModuleHandle("KERNEL32".toNativeUtf16()), "LoadLibraryA".toNativeUtf8() ); @@ -47,7 +50,7 @@ Future injectDll(int pid, String dll) async { throw Exception("Cannot get process address for pid $pid"); } - var dllAddress = VirtualAllocEx( + final dllAddress = VirtualAllocEx( process, nullptr, dll.length + 1, @@ -55,7 +58,7 @@ Future injectDll(int pid, String dll) async { 0x4 ); - var writeMemoryResult = WriteProcessMemory( + final writeMemoryResult = WriteProcessMemory( process, dllAddress, dll.toNativeUtf8(), @@ -67,7 +70,7 @@ Future injectDll(int pid, String dll) async { throw Exception("Memory write failed"); } - var createThreadResult = _CreateRemoteThread( + final createThreadResult = _CreateRemoteThread( process, nullptr, 0, @@ -81,90 +84,152 @@ Future injectDll(int pid, String dll) async { throw Exception("Thread creation failed"); } - var closeResult = CloseHandle(process); + final closeResult = CloseHandle(process); if(closeResult != 1){ throw Exception("Cannot close handle"); } } -bool runElevatedProcess(String executable, String args) { - final shellInput = calloc(); - shellInput.ref.lpFile = executable.toNativeUtf16(); - shellInput.ref.lpParameters = args.toNativeUtf16(); - shellInput.ref.nShow = SW_HIDE; - shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; - shellInput.ref.lpVerb = "runas".toNativeUtf16(); - shellInput.ref.cbSize = sizeOf(); - final result = ShellExecuteEx(shellInput) == 1; - free(shellInput); - return result; -} - -void _startBackgroundProcess(_BackgroundProcessParameters params) { - var args = params.args; - var concatenatedArgs = args == null ? "" : " ${args.map((entry) => '"$entry"').join(" ")}"; - var executablePath = TEXT('cmd.exe /k "${params.executable.path}"$concatenatedArgs'); - var startupInfo = calloc(); - var processInfo = calloc(); - var windowFlag = params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW; - var success = CreateProcess( - nullptr, - executablePath, - nullptr, - nullptr, - FALSE, - NORMAL_PRIORITY_CLASS | windowFlag | CREATE_NEW_PROCESS_GROUP, - nullptr, - TEXT(params.executable.parent.path), - startupInfo, - processInfo - ); - if (success == 0) { - var error = GetLastError(); - params.port.send("Cannot start process: $error"); +void _startProcess(_ProcessParameters params) { + final args = params.args; + final port = params.port; + final concatenatedArgs = args == null ? "" : " ${args.join(" ")}"; + final command = params.window ? 'cmd.exe /k ""${params.executable.path}"$concatenatedArgs"' : '"${params.executable.path}"$concatenatedArgs'; + print(command); + final processInfo = calloc(); + final lpStartupInfo = calloc(); + lpStartupInfo.ref.cb = sizeOf(); + lpStartupInfo.ref.dwFlags |= STARTF_USESTDHANDLES; + final securityAttributes = calloc(); + securityAttributes.ref.nLength = sizeOf(); + securityAttributes.ref.bInheritHandle = TRUE; + final hStdOutRead = calloc(); + final hStdOutWrite = calloc(); + final hStdErrRead = calloc(); + final hStdErrWrite = calloc(); + if (CreatePipe(hStdOutRead, hStdOutWrite, securityAttributes, 0) == 0 || CreatePipe(hStdErrRead, hStdErrWrite, securityAttributes, 0) == 0) { + final error = GetLastError(); + port.send("Cannot create process pipe: $error"); + return; + } + + if(SetHandleInformation(hStdOutRead.value, HANDLE_FLAG_INHERIT, 0) == 0 || SetHandleInformation(hStdErrRead.value, HANDLE_FLAG_INHERIT, 0) == 0) { + final error = GetLastError(); + port.send("Cannot set process pipe information: $error"); return; } - var pid = processInfo.ref.dwProcessId; - free(startupInfo); + lpStartupInfo.ref.hStdOutput = hStdOutWrite.value; + lpStartupInfo.ref.hStdError = hStdErrWrite.value; + final success = CreateProcess( + nullptr, + TEXT(command), + nullptr, + nullptr, + TRUE, + NORMAL_PRIORITY_CLASS | (params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW) | CREATE_NEW_PROCESS_GROUP, + nullptr, + TEXT(params.executable.parent.path), + lpStartupInfo, + processInfo + ); + if (success == 0) { + final error = GetLastError(); + port.send("Cannot start process: $error"); + return; + } + + CloseHandle(processInfo.ref.hProcess); + CloseHandle(processInfo.ref.hThread); + CloseHandle(hStdOutWrite.value); + CloseHandle(hStdErrWrite.value); + final pid = processInfo.ref.dwProcessId; + free(lpStartupInfo); free(processInfo); - params.port.send(pid); + port.send(PrimitiveWin32Process( + pid: pid, + stdOutputHandle: hStdOutRead.value, + errorOutputHandle: hStdErrRead.value + )); } -class _BackgroundProcessParameters { +class _PipeReaderParams { + final int handle; + final SendPort port; + + _PipeReaderParams(this.handle, this.port); +} + +void _pipeToStreamChannelled(_PipeReaderParams params) { + final buf = calloc(chunkSize); + while(true) { + final bytesReadPtr = calloc(); + final success = ReadFile(params.handle, buf, chunkSize, bytesReadPtr, nullptr); + if (success == FALSE) { + break; + } + + final bytesRead = bytesReadPtr.value; + if (bytesRead == 0) { + break; + } + + final lines = utf8.decode(buf.asTypedList(bytesRead)).split('\n'); + for(final line in lines) { + params.port.send(line); + } + } + + CloseHandle(params.handle); + free(buf); + Isolate.current.kill(); +} + +Stream _pipeToStream(int pipeHandle) { + final port = ReceivePort(); + Isolate.spawn( + _pipeToStreamChannelled, + _PipeReaderParams(pipeHandle, port.sendPort) + ); + return port.map((event) => event as String); +} + +class _ProcessParameters { File executable; List? args; bool window; SendPort port; - _BackgroundProcessParameters(this.executable, this.args, this.window, this.port); + _ProcessParameters(this.executable, this.args, this.window, this.port); } -Future startBackgroundProcess({required File executable, List? args, bool window = false}) async { - var completer = Completer(); - var port = ReceivePort(); - port.listen((message) => message is int ? completer.complete(message) : completer.completeError(message)); - var isolate = await Isolate.spawn( - _startBackgroundProcess, - _BackgroundProcessParameters(executable, args, window, port.sendPort), +Future startProcess({required File executable, List? args, bool output = true, bool window = false}) async { + final completer = Completer(); + final port = ReceivePort(); + port.listen((message) { + if(message is PrimitiveWin32Process) { + completer.complete(Win32Process( + pid: message.pid, + stdOutput: _pipeToStream(message.stdOutputHandle), + errorOutput: _pipeToStream(message.errorOutputHandle) + )); + } else { + completer.completeError(message); + } + }); + Isolate.spawn( + _startProcess, + _ProcessParameters(executable, args, window, port.sendPort), errorsAreFatal: true ); - var result = await completer.future; - isolate.kill(priority: Isolate.immediate); - return result; + return await completer.future; } -int _NtResumeProcess(int hWnd) { - final function = _ntdll.lookupFunction('NtResumeProcess'); - return function(hWnd); -} +final _NtResumeProcess = _ntdll.lookupFunction('NtResumeProcess'); -int _NtSuspendProcess(int hWnd) { - final function = _ntdll.lookupFunction('NtSuspendProcess'); - return function(hWnd); -} +final _NtSuspendProcess = _ntdll.lookupFunction('NtSuspendProcess'); bool suspend(int pid) { final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); @@ -214,7 +279,7 @@ List createRebootArgs(String username, String password, bool host, bool } password = password.isNotEmpty ? password : "Rebooted"; - var args = [ + final args = [ "-epicapp=Fortnite", "-epicenv=Prod", "-epiclocale=en-us", diff --git a/gui/lib/src/controller/authenticator_controller.dart b/gui/lib/src/controller/authenticator_controller.dart index cca4334..a3ccfda 100644 --- a/gui/lib/src/controller/authenticator_controller.dart +++ b/gui/lib/src/controller/authenticator_controller.dart @@ -16,7 +16,7 @@ class AuthenticatorController extends ServerController { String get defaultHost => kDefaultAuthenticatorHost; @override - String get defaultPort => kDefaultAuthenticatorPort.toString(); + int get defaultPort => kDefaultAuthenticatorPort; @override Future get isPortFree => isAuthenticatorPortFree(); @@ -28,8 +28,8 @@ class AuthenticatorController extends ServerController { RebootPageType get pageType => RebootPageType.authenticator; @override - Future startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value); + Future startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value); @override - Future pingServer(String host, String port) => pingAuthenticator(host, port); + Future pingServer(String host, int port) => pingAuthenticator(host, port); } \ No newline at end of file diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index 2f91be0..4aea008 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -30,7 +30,7 @@ class HostingController extends GetxController { description.addListener(() => _storage.write("description", description.text)); password = TextEditingController(text: _storage.read("password") ?? ""); password.addListener(() => _storage.write("password", password.text)); - discoverable = RxBool(_storage.read("discoverable") ?? true); + discoverable = RxBool(_storage.read("discoverable") ?? false); discoverable.listen((value) => _storage.write("discoverable", value)); headless = RxBool(_storage.read("headless") ?? true); headless.listen((value) => _storage.write("headless", value)); diff --git a/gui/lib/src/controller/matchmaker_controller.dart b/gui/lib/src/controller/matchmaker_controller.dart index d955655..b5ced53 100644 --- a/gui/lib/src/controller/matchmaker_controller.dart +++ b/gui/lib/src/controller/matchmaker_controller.dart @@ -45,7 +45,7 @@ class MatchmakerController extends ServerController { String get defaultHost => kDefaultMatchmakerHost; @override - String get defaultPort => kDefaultMatchmakerPort.toString(); + int get defaultPort => kDefaultMatchmakerPort; @override Future get isPortFree => isMatchmakerPortFree(); @@ -57,8 +57,8 @@ class MatchmakerController extends ServerController { RebootPageType get pageType => RebootPageType.matchmaker; @override - Future startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value); + Future startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value); @override - Future pingServer(String host, String port) => pingMatchmaker(host, port); + Future pingServer(String host, int port) => pingMatchmaker(host, port); } \ No newline at end of file diff --git a/gui/lib/src/controller/server_controller.dart b/gui/lib/src/controller/server_controller.dart index e2429de..a5c6220 100644 --- a/gui/lib/src/controller/server_controller.dart +++ b/gui/lib/src/controller/server_controller.dart @@ -51,9 +51,9 @@ abstract class ServerController extends GetxController { String get defaultHost; - String get defaultPort; + int get defaultPort; - Future pingServer(String host, String port); + Future pingServer(String host, int port); Future get isPortFree; @@ -64,17 +64,17 @@ abstract class ServerController extends GetxController { Future freePort(); @protected - Future startEmbeddedInternal(); + Future startEmbeddedInternal(); void reset() async { type.value = ServerType.values.elementAt(0); - for (var type in ServerType.values) { + for (final type in ServerType.values) { storage.write("${type.name}_host", null); storage.write("${type.name}_port", null); } host.text = type.value != ServerType.remote ? defaultHost : ""; - port.text = defaultPort; + port.text = defaultPort.toString(); detached.value = false; } @@ -85,48 +85,48 @@ abstract class ServerController extends GetxController { } String _readPort() => - storage.read("${type.value.name}_port") ?? defaultPort; + storage.read("${type.value.name}_port") ?? defaultPort.toString(); Stream start() async* { - if(started.value) { - return; - } - - if(type() != ServerType.local) { - started.value = true; - yield ServerResult(ServerResultType.starting); - }else { - started.value = false; - if(port != defaultPort) { - yield ServerResult(ServerResultType.starting); - } - } - try { - var host = this.host.text.trim(); - if (host.isEmpty) { + if(started.value) { + return; + } + + final hostData = this.host.text.trim(); + final portData = this.port.text.trim(); + if(type() != ServerType.local) { + started.value = true; + yield ServerResult(ServerResultType.starting); + }else { + started.value = false; + if(portData != defaultPort) { + yield ServerResult(ServerResultType.starting); + } + } + + if (hostData.isEmpty) { yield ServerResult(ServerResultType.missingHostError); started.value = false; return; } - var port = this.port.text.trim(); - if (port.isEmpty) { + if (portData.isEmpty) { yield ServerResult(ServerResultType.missingPortError); started.value = false; return; } - var portNumber = int.tryParse(port); + final portNumber = int.tryParse(portData); if (portNumber == null) { yield ServerResult(ServerResultType.illegalPortError); started.value = false; return; } - if ((type() != ServerType.local || port != defaultPort) && await isPortTaken) { + if ((type() != ServerType.local || portData != defaultPort) && await isPortTaken) { yield ServerResult(ServerResultType.freeingPort); - var result = await freePort(); + final result = await freePort(); yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); if(!result) { started.value = false; @@ -136,8 +136,15 @@ abstract class ServerController extends GetxController { switch(type()){ case ServerType.embedded: - final pid = await startEmbeddedInternal(); - watchProcess(pid).then((value) { + final process = await startEmbeddedInternal(); + final processPid = process.pid; + if(processPid == null) { + yield ServerResult(ServerResultType.startError); + started.value = false; + return; + } + + watchProcess(processPid).then((value) { if(started()) { started.value = false; } @@ -145,7 +152,7 @@ abstract class ServerController extends GetxController { break; case ServerType.remote: yield ServerResult(ServerResultType.pingingRemote); - var uriResult = await pingServer(host, port); + final uriResult = await pingServer(hostData, portNumber); if(uriResult == null) { yield ServerResult(ServerResultType.pingError); started.value = false; @@ -155,7 +162,7 @@ abstract class ServerController extends GetxController { remoteServer = await startRemoteAuthenticatorProxy(uriResult); break; case ServerType.local: - if(port != defaultPort) { + if(portData != defaultPort) { localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$port")); } @@ -163,7 +170,7 @@ abstract class ServerController extends GetxController { } yield ServerResult(ServerResultType.pingingLocal); - var uriResult = await pingServer(defaultHost, defaultPort); + final uriResult = await pingServer(defaultHost, defaultPort); if(uriResult == null) { yield ServerResult(ServerResultType.pingError); remoteServer?.close(force: true); @@ -195,7 +202,7 @@ abstract class ServerController extends GetxController { try{ switch(type()){ case ServerType.embedded: - killProcessByPort(int.parse(defaultPort)); + killProcessByPort(defaultPort); break; case ServerType.remote: await remoteServer?.close(force: true); diff --git a/gui/lib/src/dialog/implementation/server.dart b/gui/lib/src/dialog/implementation/server.dart index 51d0d40..5802046 100644 --- a/gui/lib/src/dialog/implementation/server.dart +++ b/gui/lib/src/dialog/implementation/server.dart @@ -24,6 +24,7 @@ extension ServerControllerDialog on ServerController { var stream = toggle(); var completer = Completer(); worker = stream.listen((event) { + print(event.type); switch (event.type) { case ServerResultType.starting: showInfoBar( diff --git a/gui/lib/src/util/daemon.dart b/gui/lib/src/util/daemon.dart index 614a885..fcdd082 100644 --- a/gui/lib/src/util/daemon.dart +++ b/gui/lib/src/util/daemon.dart @@ -28,7 +28,7 @@ extension GameInstanceWatcher on GameInstance { } }); - observerPid = await startBackgroundProcess( + final process = await startProcess( executable: _executable, args: [ hostingController.uuid, @@ -36,8 +36,10 @@ extension GameInstanceWatcher on GameInstance { launcherPid?.toString() ?? "-1", eacPid?.toString() ?? "-1", hosting.toString() - ] + ], + ); + observerPid = process.pid; } bool get _nestedHosting { diff --git a/gui/lib/src/util/dll.dart b/gui/lib/src/util/dll.dart index 8640978..5de0986 100644 --- a/gui/lib/src/util/dll.dart +++ b/gui/lib/src/util/dll.dart @@ -11,7 +11,7 @@ import 'package:reboot_launcher/src/util/translations.dart'; final UpdateController _updateController = Get.find(); Future downloadCriticalDllInteractive(String filePath) async { try { - final fileName = path.basename(filePath); + final fileName = path.basename(filePath).toLowerCase(); if (fileName == "reboot.dll") { _updateController.update(true); return; diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 1aaf4bc..78b4365 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -51,11 +51,11 @@ class _LaunchButtonState extends State { child: Obx(() => SizedBox( height: 48, child: Button( - onPressed: () => _operation = CancelableOperation.fromFuture(_start()), - child: Align( - alignment: Alignment.center, - child: Text(_hasStarted ? _stopMessage : _startMessage) - ) + onPressed: () => _operation = CancelableOperation.fromFuture(_start()), + child: Align( + alignment: Alignment.center, + child: Text(_hasStarted ? _stopMessage : _startMessage) + ) ), )), ), @@ -72,11 +72,11 @@ class _LaunchButtonState extends State { Future _start() async { if (_hasStarted) { _onStop( - reason: _StopReason.normal + reason: _StopReason.normal ); return; } - + if(_operation != null) { return; } @@ -149,7 +149,7 @@ class _LaunchButtonState extends State { if(_hostingController.started()){ return null; } - + if(!_hostingController.automaticServer()) { return null; } @@ -160,10 +160,12 @@ class _LaunchButtonState extends State { } Future _startGameProcesses(FortniteVersion version, bool host, GameInstance? linkedHosting) async { - final launcherProcess = await _createLauncherProcess(version); - final eacProcess = await _createEacProcess(version); + final launcherProcess = await _createPausedProcess(version.launcherExecutable); + print("Created launcher process"); + final eacProcess = await _createPausedProcess(version.eacExecutable); + print("Created eac process"); final executable = await version.executable; - final gameProcess = await _createGameProcess(executable!.path, host); + final gameProcess = await _createGameProcess(executable!, host); if(gameProcess == null) { return null; } @@ -187,7 +189,7 @@ class _LaunchButtonState extends State { return instance; } - Future _createGameProcess(String gamePath, bool host) async { + Future _createGameProcess(File executable, bool host) async { if(!_hasStarted) { return null; } @@ -199,38 +201,32 @@ class _LaunchButtonState extends State { _hostingController.headless.value, _gameController.customLaunchArgs.text ); - final gameProcess = await Process.start( - gamePath, - gameArgs + final gameProcess = await startProcess( + executable: executable, + args: gameArgs, + window: false ); - gameProcess - ..exitCode.then((_) => _onStop(reason: _StopReason.exitCode)) - ..outLines.forEach((line) => _onGameOutput(line, host, false)) - ..errLines.forEach((line) => _onGameOutput(line, host, true)); + gameProcess.stdOutput.listen((line) => _onGameOutput(line, host, false)); + gameProcess.errorOutput.listen((line) => _onGameOutput(line, host, true)); + watchProcess(gameProcess.pid).then((_) => _onStop(reason: _StopReason.exitCode)); return gameProcess.pid; } - Future _createLauncherProcess(FortniteVersion version) async { - final launcherFile = version.launcher; - if (launcherFile == null) { + Future _createPausedProcess(File? file) async { + if (file == null) { return null; } - final launcherProcess = await Process.start(launcherFile.path, []); - final pid = launcherProcess.pid; - suspend(pid); - return pid; - } - - Future _createEacProcess(FortniteVersion version) async { - final eacFile = version.eacExecutable; - if (eacFile == null) { - return null; - } - - final eacProcess = await Process.start(eacFile.path, []); - final pid = eacProcess.pid; + final process = await startProcess( + executable: file, + args: [], + window: false, + output: false + ); + print("Started process: ${process.pid}"); + final pid = process.pid; suspend(pid); + print("Suspended"); return pid; } @@ -473,7 +469,7 @@ class _LaunchButtonState extends State { return _settingsController.memoryLeakDll.text; } } - + Future _getDllFileOrStop(_Injectable injectable, bool host) async { final path = _getDllPath(injectable); final file = File(path); diff --git a/gui/lib/test.dart b/gui/lib/test.dart new file mode 100644 index 0000000..4319230 --- /dev/null +++ b/gui/lib/test.dart @@ -0,0 +1,12 @@ +import 'dart:io'; + +import 'package:reboot_common/common.dart'; + +void main() async { + final process = await startProcess( + executable: File("C:\\FortniteBuilds\\Fortnite 4.2\\Fortnite 4.2\\Fortnite1\\FortniteGame\\Binaries\\Win64\\FortniteClient-Win64-Shipping-Reboot.exe"), + args: "-epicapp=Fortnite -epicenv=Prod -epiclocale=en-us -epicportal -skippatchcheck -nobe -fromfl=eac -fltoken=3db3ba5dcbd2e16703f3978d -caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ -AUTH_LOGIN=Player698@projectreboot.dev -AUTH_PASSWORD=Rebooted -AUTH_TYPE=epic -nullrhi -nosplash -nosound".split(" ") + ); + process.stdOutput.listen((event) => stdout.writeln(event)); + process.errorOutput.listen((event) => stdout.writeln(event)); +} \ No newline at end of file