import 'dart:ffi'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:ffi/ffi.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/input.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; class System { static System? _instance; System._internal(); factory System() { _instance ??= System._internal(); return _instance!; } bool get isDesktop => isWindows || isMacOS || isLinux; bool get isWindows => Platform.isWindows; bool get isMacOS => Platform.isMacOS; bool get isAndroid => Platform.isAndroid; bool get isLinux => Platform.isLinux; Future get version async { final deviceInfo = await DeviceInfoPlugin().deviceInfo; return switch (Platform.operatingSystem) { 'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion, 'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt, 'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion, String() => 0, }; } Future checkIsAdmin() async { final corePath = appPath.corePath.replaceAll(' ', '\\\\ '); if (system.isWindows) { final result = await windows?.checkService(); return result == WindowsHelperServiceStatus.running; } else if (system.isMacOS) { final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]); final output = result.stdout.trim(); if (output.startsWith('root:admin') && output.contains('rws')) { return true; } return false; } else if (Platform.isLinux) { final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]); final output = result.stdout.trim(); if (output.startsWith('root:') && output.contains('rws')) { return true; } return false; } return true; } Future authorizeCore() async { if (system.isAndroid) { return AuthorizeCode.error; } final corePath = appPath.corePath.replaceAll(' ', '\\\\ '); final isAdmin = await checkIsAdmin(); if (isAdmin) { return AuthorizeCode.none; } if (system.isWindows) { final result = await windows?.registerService(); if (result == true) { return AuthorizeCode.success; } return AuthorizeCode.error; } if (system.isMacOS) { final shell = 'chown root:admin $corePath; chmod +sx $corePath'; final arguments = [ '-e', 'do shell script "$shell" with administrator privileges', ]; final result = await Process.run('osascript', arguments); if (result.exitCode != 0) { return AuthorizeCode.error; } return AuthorizeCode.success; } else if (Platform.isLinux) { final shell = Platform.environment['SHELL'] ?? 'bash'; final password = await globalState.showCommonDialog( child: InputDialog( obscureText: true, title: appLocalizations.pleaseInputAdminPassword, value: '', ), ); final arguments = [ '-c', 'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"', ]; final result = await Process.run(shell, arguments); if (result.exitCode != 0) { return AuthorizeCode.error; } return AuthorizeCode.success; } return AuthorizeCode.error; } Future back() async { await app?.moveTaskToBack(); await window?.hide(); } Future exit() async { if (system.isAndroid) { await SystemNavigator.pop(); } await window?.close(); } } final system = System(); class Windows { static Windows? _instance; late DynamicLibrary _shell32; Windows._internal() { _shell32 = DynamicLibrary.open('shell32.dll'); } factory Windows() { _instance ??= Windows._internal(); return _instance!; } bool runas(String command, String arguments) { final commandPtr = command.toNativeUtf16(); final argumentsPtr = arguments.toNativeUtf16(); final operationPtr = 'runas'.toNativeUtf16(); final shellExecute = _shell32 .lookupFunction< Int32 Function( Pointer hwnd, Pointer lpOperation, Pointer lpFile, Pointer lpParameters, Pointer lpDirectory, Int32 nShowCmd, ), int Function( Pointer hwnd, Pointer lpOperation, Pointer lpFile, Pointer lpParameters, Pointer lpDirectory, int nShowCmd, ) >('ShellExecuteW'); final result = shellExecute( nullptr, operationPtr, commandPtr, argumentsPtr, nullptr, 1, ); calloc.free(commandPtr); calloc.free(argumentsPtr); calloc.free(operationPtr); commonPrint.log( 'windows runas: $command $arguments resultCode:$result', logLevel: LogLevel.warning, ); if (result < 42) { return false; } return true; } Future _killProcess(int port) async { final result = await Process.run('netstat', ['-ano']); final lines = result.stdout.toString().trim().split('\n'); for (final line in lines) { if (!line.contains(':$port') || !line.contains('LISTENING')) { continue; } final parts = line.trim().split(RegExp(r'\s+')); final pid = int.tryParse(parts.last); if (pid != null) { await Process.run('taskkill', ['/PID', pid.toString(), '/F']); } } } Future checkService() async { // final qcResult = await Process.run('sc', ['qc', appHelperService]); // final qcOutput = qcResult.stdout.toString(); // if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) { // return WindowsHelperServiceStatus.none; // } final result = await Process.run('sc', ['query', appHelperService]); if (result.exitCode != 0) { return WindowsHelperServiceStatus.none; } final output = result.stdout.toString(); if (output.contains('RUNNING') && await request.pingHelper()) { return WindowsHelperServiceStatus.running; } return WindowsHelperServiceStatus.presence; } Future registerService() async { final status = await checkService(); if (status == WindowsHelperServiceStatus.running) { return true; } await _killProcess(helperPort); final command = [ '/c', if (status == WindowsHelperServiceStatus.presence) ...[ 'sc', 'delete', appHelperService, '/force', '&&', ], 'sc', 'create', appHelperService, 'binPath= "${appPath.helperPath}"', 'start= auto', '&&', 'sc', 'start', appHelperService, ].join(' '); final res = runas('cmd.exe', command); await Future.delayed(Duration(milliseconds: 300)); return res; } Future registerTask(String appName) async { final taskXml = ''' InteractiveToken HighestAvailable Parallel false false false false false false false true true false false false PT72H 7 "${Platform.resolvedExecutable}" '''; final taskPath = join(await appPath.tempPath, 'task.xml'); await File(taskPath).create(recursive: true); await File( taskPath, ).writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true); final commandLine = [ '/Create', '/TN', appName, '/XML', '%s', '/F', ].join(' '); return runas('schtasks', commandLine.replaceFirst('%s', taskPath)); } } final windows = system.isWindows ? Windows() : null; class MacOS { static MacOS? _instance; List? originDns; MacOS._internal(); factory MacOS() { _instance ??= MacOS._internal(); return _instance!; } Future get defaultServiceName async { final result = await Process.run('route', ['-n', 'get', 'default']); final output = result.stdout.toString(); final deviceLine = output .split('\n') .firstWhere((s) => s.contains('interface:'), orElse: () => ''); final lineSplits = deviceLine.trim().split(' '); if (lineSplits.length != 2) { return null; } final device = lineSplits[1]; final serviceResult = await Process.run('networksetup', [ '-listnetworkserviceorder', ]); final serviceResultOutput = serviceResult.stdout.toString(); final currentService = serviceResultOutput .split('\n\n') .firstWhere((s) => s.contains('Device: $device'), orElse: () => ''); if (currentService.isEmpty) { return null; } final currentServiceNameLine = currentService .split('\n') .firstWhere( (line) => RegExp(r'^\(\d+\).*').hasMatch(line), orElse: () => '', ); final currentServiceNameLineSplits = currentServiceNameLine.trim().split( ' ', ); if (currentServiceNameLineSplits.length < 2) { return null; } return currentServiceNameLineSplits[1]; } Future?> get systemDns async { final deviceServiceName = await defaultServiceName; if (deviceServiceName == null) { return null; } final result = await Process.run('networksetup', [ '-getdnsservers', deviceServiceName, ]); final output = result.stdout.toString().trim(); if (output.startsWith("There aren't any DNS Servers set on")) { originDns = []; } else { originDns = output.split('\n'); } return originDns; } Future updateDns(bool restore) async { final serviceName = await defaultServiceName; if (serviceName == null) { return; } List? nextDns; if (restore) { nextDns = originDns; } else { final originDns = await systemDns; if (originDns == null) { return; } final needAddDns = '223.5.5.5'; if (originDns.contains(needAddDns)) { return; } nextDns = List.from(originDns)..add(needAddDns); } if (nextDns == null) { return; } await Process.run('networksetup', [ '-setdnsservers', serviceName, if (nextDns.isNotEmpty) ...nextDns, if (nextDns.isEmpty) 'Empty', ]); } } final macOS = system.isMacOS ? MacOS() : null;