This commit is contained in:
Alessandro Autiero
2024-06-01 16:26:00 +02:00
parent d478650e9b
commit efb508bd0c
243 changed files with 486662 additions and 2948 deletions

View File

@@ -1,4 +1,4 @@
export 'package:reboot_common/src/constant/authenticator.dart';
export 'package:reboot_common/src/constant/backend.dart';
export 'package:reboot_common/src/constant/game.dart';
export 'package:reboot_common/src/constant/matchmaker.dart';
export 'package:reboot_common/src/constant/supabase.dart';
@@ -9,8 +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/backend.dart';
export 'package:reboot_common/src/util/build.dart';
export 'package:reboot_common/src/util/dll.dart';
export 'package:reboot_common/src/util/matchmaker.dart';

View File

@@ -1,2 +0,0 @@
const String kDefaultAuthenticatorHost = "127.0.0.1";
const int kDefaultAuthenticatorPort = 3551;

View File

@@ -0,0 +1,2 @@
const String kDefaultBackendHost = "127.0.0.1";
const int kDefaultBackendPort = 3551;

View File

@@ -1,11 +1,17 @@
const String kDefaultPlayerName = "Player";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String kConsoleLine = "Region ";
const String kInitializedLine = "Game Engine Initialized";
const List<String> kLoggedInLines = [
"[UOnlineAccountCommon::ContinueLoggingIn]",
"(Completed)"
];
const String kShutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> kCorruptedBuildErrors = [
"Critical error",
"when 0 bytes remain",
"Pak chunk signature verification failed!"
"Pak chunk signature verification failed!",
"Couldn't find pak signature file"
];
const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused",

View File

@@ -20,7 +20,11 @@ class FortniteBuildDownloadProgress {
final int? minutesLeft;
final bool extracting;
FortniteBuildDownloadProgress(this.progress, this.minutesLeft, this.extracting);
FortniteBuildDownloadProgress({
required this.progress,
required this.extracting,
this.minutesLeft,
});
}
class FortniteBuildDownloadOptions {

View File

@@ -6,7 +6,6 @@ class GameInstance {
final int gamePid;
final int? launcherPid;
final int? eacPid;
int? observerPid;
bool hosting;
bool launched;
bool tokenError;
@@ -29,9 +28,18 @@ class GameInstance {
if(eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
}
if(observerPid != null) {
Process.killPid(observerPid!, ProcessSignal.sigabrt);
}
bool get nestedHosting {
GameInstance? child = this;
while(child != null) {
if(child.hosting) {
return true;
}
child = child.child;
}
child?.kill();
return false;
}
}

View File

@@ -1,23 +0,0 @@
class Win32Process {
final int pid;
final Stream<String> stdOutput;
final Stream<String> 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
});
}

View File

@@ -24,4 +24,6 @@ enum ServerResultType {
pingError;
bool get isError => name.contains("Error");
bool get isSuccess => this == ServerResultType.startSuccess || this == ServerResultType.stopSuccess;
}

View File

@@ -1,53 +0,0 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator");
final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe");
Future<Win32Process> startEmbeddedAuthenticator(bool detached) async => startProcess(
executable: authenticatorStartExecutable,
window: detached,
);
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort);
Future<bool> isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort) == null;
Future<bool> freeAuthenticatorPort() async {
await killProcessByPort(kDefaultAuthenticatorPort);
final standardResult = await isAuthenticatorPortFree();
if(standardResult) {
return true;
}
return false;
}
Future<Uri?> 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: port,
path: "unknown"
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
}catch(_){
return https || declaredScheme != null || isLocalHost(host) ? null : await pingAuthenticator(host, port, true);
}
}
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;

View File

@@ -1,24 +1,61 @@
import 'dart:async';
import 'dart:io';
import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:sync/semaphore.dart';
final matchmakerDirectory = Directory("${assetsDirectory.path}\\matchmaker");
final matchmakerStartExecutable = File("${matchmakerDirectory.path}\\fortmatchmaker.exe");
final matchmakerKillExecutable = File("${authenticatorDirectory.path}\\kill.bat");
final matchmakerConfigFile = File("${authenticatorDirectory.path}\\Config\\config.ini");
final Directory backendDirectory = Directory("${assetsDirectory.path}\\backend");
final File backendStartExecutable = File("${backendDirectory.path}\\lawinserver.exe");
final File matchmakerConfigFile = File("${backendDirectory.path}\\Config\\config.ini");
final Semaphore _semaphore = Semaphore();
String? _lastIp;
String? _lastPort;
Semaphore _semaphore = Semaphore();
Future<Win32Process> startEmbeddedMatchmaker(bool detached) async => startProcess(
executable: matchmakerStartExecutable,
Future<Process> startEmbeddedBackend(bool detached) async => startProcess(
executable: backendStartExecutable,
window: detached,
);
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost, kDefaultBackendPort) == null;
Future<bool> freeBackendPort() async {
await killProcessByPort(kDefaultBackendPort);
final standardResult = await isBackendPortFree();
if(standardResult) {
return true;
}
return false;
}
Future<Uri?> pingBackend(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: port,
path: "unknown"
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
}catch(_){
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
}
}
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
Stream<String?> watchMatchmakingIp() async* {
if(!matchmakerConfigFile.existsSync()){
return;
@@ -99,7 +136,7 @@ Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
var completer = Completer<bool>();
var socket = await WebSocket.connect(uri.toString());
socket.listen(
(data) {
(data) {
if(!completer.isCompleted) {
completer.complete(true);
}
@@ -125,20 +162,4 @@ Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
String? _getHostName(String host) => host.replaceFirst("ws://", "").replaceFirst("wss://", "");
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
extension StringExtension on String {
bool get isBlank {
if(isEmpty) {
return true;
}
for(var char in this.split("")) {
if(char != " ") {
return false;
}
}
return true;
}
}
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;

View File

@@ -25,8 +25,11 @@ Dio _buildDioInstance() {
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;
const String _manifestSourceUrl = "http://manifest.simplyblk.xyz";
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
const String _unavailableError = "The build downloader is not available right now";
const String _genericError = "The build downloader is not working correctly";
const int _maxErrors = 100;
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =>
@@ -35,7 +38,7 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
(X509Certificate cert, String host, int port) => true;
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
final data = <FortniteBuild>[];
final data = <FortniteBuild>[];
for(final result in results) {
data.addAll(result);
}
@@ -45,7 +48,15 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
Future<List<FortniteBuild>> _fetchManifestBuilds() async {
try {
final response = await _dio.get<String>("$_manifestSourceUrl/versions.json");
final response = await _dio.get<String>(
"$_manifestSourceUrl/versions.json",
options: Options(
headers: {
"Accept-Encoding": "*",
"Cookie": "_c_t_c=1"
}
)
);
final body = response.data;
return jsonDecode(body!).map((version) {
final nameParts = version.split("-");
@@ -131,7 +142,14 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
delete(outputDir);
break;
case FortniteBuildSource.manifest:
final response = await _dio.get<String>(options.build.link);
final response = await _dio.get<String>(
options.build.link,
options: Options(
headers: {
"Cookie": "_c_t_c=1"
}
)
);
final manifest = FortniteBuildManifestFile.fromJson(jsonDecode(response.data!));
final totalBytes = manifest.size;
@@ -171,7 +189,8 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
options: Options(
responseType: ResponseType.bytes,
headers: {
"Accept-Encoding": "gzip"
"Accept-Encoding": "gzip",
"Cookie": "_c_t_c=1"
}
),
);
@@ -201,24 +220,23 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
}
});
await Future.any([stopped.future, Future.wait(writers)]);
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
break;
}
}catch(error, stackTrace) {
options.port.send("$error\n$stackTrace");
}catch(error) {
_onError(error, options);
}
}
Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
var received = byteStart ?? 0;
try {
return await _dio.download(
await _dio.download(
options.build.link,
tempFile.path,
onReceiveProgress: (data, length) {
received = data;
final percentage = (received / length) * 100;
_onProgress(startTime, DateTime.now().millisecondsSinceEpoch, percentage, false, options);
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
},
deleteOnError: false,
options: Options(
@@ -228,10 +246,14 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
}
if(statusCode == 403 || statusCode == 503) {
throw Exception("The connection was denied: your firewall might be blocking the download");
throw _deniedConnectionError;
}
throw Exception("The build downloader is not available right now");
if(statusCode == 404) {
throw _unavailableError;
}
throw _genericError;
},
headers: byteStart == null || byteStart <= 0 ? {
"Cookie": "_c_t_c=1"
@@ -242,17 +264,18 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
)
);
}catch(error) {
if(errorsCount >= _maxDownloadErrors) {
throw error;
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
_onError(error, options);
return;
}
return await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
}
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
final startTime = DateTime.now().millisecondsSinceEpoch;
Win32Process? process;
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
@@ -271,10 +294,10 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
],
);
process.stdOutput.listen((data) {
print(data);
final now = DateTime.now().millisecondsSinceEpoch;
if(data == "Everything is Ok") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
if(data.toLowerCase().contains("everything is ok")) {
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -286,6 +309,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
@@ -298,17 +326,17 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
args: [
"x",
"-o+",
tempFile.path,
'"${tempFile.path}"',
"*.*",
options.destination.path
'"${options.destination.path}"'
]
);
process.stdOutput.listen((data) {
print(data);
final now = DateTime.now().millisecondsSinceEpoch;
data.replaceAll("\r", "").replaceAll("\b", "").trim();
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -320,7 +348,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
process.errorOutput.listen((data) => options.port.send(data));
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
@@ -329,17 +361,31 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
await Future.any([stopped.future, watchProcess(process.pid)]);
}
void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
if(percentage == 0) {
options.port.send(FortniteBuildDownloadProgress(percentage, null, extracting));
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting
));
return;
}
final msLeft = startTime + (now - startTime) * 100 / percentage - now;
final minutesLeft = (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(percentage, minutesLeft, extracting));
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
minutesLeft: minutesLeft
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();

View File

@@ -8,7 +8,7 @@ import 'package:reboot_common/common.dart';
bool _watcher = false;
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
const String kRebootDownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
@@ -18,7 +18,7 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
}
Future<void> 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"));
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/assets/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
@@ -64,7 +64,6 @@ Stream<String> watchDlls() async* {
_watcher = true;
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
print(event);
if (event.path.endsWith(".dll")) {
yield event.path;
}

View File

@@ -31,27 +31,36 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
var read = await file.readAsBytes();
var length = await file.length();
var offset = 0;
var counter = 0;
while(offset < length){
if(read[offset] == original[counter]){
counter++;
}else {
counter = 0;
}
offset++;
if(counter == original.length){
for(var index = 0; index < patched.length; index++){
read[offset - counter + index] = patched[index];
var readOffset = 0;
var patchOffset = -1;
var patchCount = 0;
while(readOffset < length){
if(read[readOffset] == original[patchCount]){
if(patchOffset == -1) {
patchOffset = readOffset;
}
await file.writeAsBytes(read, mode: FileMode.write);
return true;
if(++patchCount == original.length) {
break;
}
}else {
patchOffset = -1;
}
readOffset++;
}
return false;
print("Offset: $patchOffset");
if(patchOffset == -1) {
return false;
}
for(var i = 0; i < patched.length; i++) {
read[patchOffset + i] = patched[i];
}
await file.writeAsBytes(read, flush: true);
return true;
}catch(_){
return false;
}

View File

@@ -52,8 +52,10 @@ extension FortniteVersionExtension on FortniteVersion {
}
}
Future<File?> get executable async {
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.exe");
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
Future<File?> get headlessGameExecutable async {
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
if(result != null) {
return result;
}
@@ -63,12 +65,9 @@ extension FortniteVersionExtension on FortniteVersion {
return null;
}
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Reboot.exe");
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
await original.copy(output.path);
await Future.wait([
Isolate.run(() => patchMatchmaking(output)),
Isolate.run(() => patchHeadless(output)),
]);
await Isolate.run(() => patchHeadless(output));
return output;
}

View File

@@ -6,13 +6,13 @@ import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:path/path.dart' as path;
import 'package:ffi/ffi.dart';
import 'package:reboot_common/src/model/process.dart';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
import 'package:win32/win32.dart';
import '../constant/game.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
@@ -90,139 +90,53 @@ Future<void> injectDll(int pid, String dll) async {
}
}
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<PROCESS_INFORMATION>();
final lpStartupInfo = calloc<STARTUPINFO>();
lpStartupInfo.ref.cb = sizeOf<STARTUPINFO>();
lpStartupInfo.ref.dwFlags |= STARTF_USESTDHANDLES;
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
securityAttributes.ref.nLength = sizeOf<SECURITY_ATTRIBUTES>();
securityAttributes.ref.bInheritHandle = TRUE;
final hStdOutRead = calloc<HANDLE>();
final hStdOutWrite = calloc<HANDLE>();
final hStdErrRead = calloc<HANDLE>();
final hStdErrWrite = calloc<HANDLE>();
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;
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
final argsOrEmpty = args ?? [];
if(wrapProcess) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}/process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start(
tempScriptFile.path,
[],
workingDirectory: executable.parent.path,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _withLogger(name, executable, process, window);
}
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
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: executable.parent.path,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
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);
port.send(PrimitiveWin32Process(
pid: pid,
stdOutputHandle: hStdOutRead.value,
errorOutputHandle: hStdErrRead.value
));
return _withLogger(name, executable, process, window);
}
class _PipeReaderParams {
final int handle;
final SendPort port;
_PipeReaderParams(this.handle, this.port);
}
void _pipeToStreamChannelled(_PipeReaderParams params) {
final buf = calloc<Uint8>(chunkSize);
while(true) {
final bytesReadPtr = calloc<Uint32>();
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);
}
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
final extendedProcess = _ExtendedProcess(process, true);
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
loggingFile.parent.createSync(recursive: true);
if(loggingFile.existsSync()) {
loggingFile.deleteSync();
}
CloseHandle(params.handle);
free(buf);
Isolate.current.kill();
}
Stream<String> _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<String>? args;
bool window;
SendPort port;
_ProcessParameters(this.executable, this.args, this.window, this.port);
}
Future<Win32Process> startProcess({required File executable, List<String>? args, bool output = true, bool window = false}) async {
final completer = Completer<Win32Process>();
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
);
return await completer.future;
final semaphore = Semaphore(1);
void logEvent(String event) async {
await semaphore.acquire();
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
semaphore.release();
}
extendedProcess.stdOutput.listen(logEvent);
extendedProcess.stdError.listen(logEvent);
if(!window) {
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
}
return extendedProcess;
}
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
@@ -247,8 +161,11 @@ bool resume(int pid) {
void _watchProcess(int pid) {
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
WaitForSingleObject(processHandle, INFINITE);
CloseHandle(processHandle);
try {
WaitForSingleObject(processHandle, INFINITE);
}finally {
CloseHandle(processHandle);
}
}
Future<bool> watchProcess(int pid) async {
@@ -261,16 +178,14 @@ Future<bool> watchProcess(int pid) async {
});
var errorPort = ReceivePort();
errorPort.listen((_) => completer.complete(false));
var isolate = await Isolate.spawn(
await Isolate.spawn(
_watchProcess,
pid,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true
);
var result = await completer.future;
isolate.kill(priority: Isolate.immediate);
return result;
return await completer.future;
}
List<String> createRebootArgs(String username, String password, bool host, bool headless, String additionalArgs) {
@@ -324,4 +239,53 @@ String _parseUsername(String username, bool host) {
}
return username;
}
class _ExtendedProcess extends Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _delegate.exitCode;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
@override
int get pid => _delegate.pid;
@override
IOSink get stdin => _delegate.stdin;
@override
Stream<List<int>> get stdout {
final out = _stdout;
if(out == null) {
throw StateError("Output is not attached");
}
return out;
}
@override
Stream<List<int>> get stderr {
final err = _stderr;
if(err == null) {
throw StateError("Output is not attached");
}
return err;
}
}
extension ProcessExtension on Process {
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n"));
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n"));
}