425 lines
12 KiB
Dart
425 lines
12 KiB
Dart
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<int> 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<bool> 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<AuthorizeCode> 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<String>(
|
|
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<void> back() async {
|
|
await app?.moveTaskToBack();
|
|
await window?.hide();
|
|
}
|
|
|
|
Future<void> 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<Utf16> hwnd,
|
|
Pointer<Utf16> lpOperation,
|
|
Pointer<Utf16> lpFile,
|
|
Pointer<Utf16> lpParameters,
|
|
Pointer<Utf16> lpDirectory,
|
|
Int32 nShowCmd,
|
|
),
|
|
int Function(
|
|
Pointer<Utf16> hwnd,
|
|
Pointer<Utf16> lpOperation,
|
|
Pointer<Utf16> lpFile,
|
|
Pointer<Utf16> lpParameters,
|
|
Pointer<Utf16> 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 <= 32) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Future<void> _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<WindowsHelperServiceStatus> 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<bool> registerService() async {
|
|
final status = await checkService();
|
|
|
|
if (status == WindowsHelperServiceStatus.running) {
|
|
return true;
|
|
}
|
|
|
|
final command = [
|
|
'/c',
|
|
if (status == WindowsHelperServiceStatus.presence) ...[
|
|
'taskkill',
|
|
'/F',
|
|
'/IM',
|
|
'$appHelperService.exe'
|
|
' & '
|
|
'sc',
|
|
'delete',
|
|
appHelperService,
|
|
'&',
|
|
],
|
|
'sc',
|
|
'create',
|
|
appHelperService,
|
|
'binPath= "${appPath.helperPath}"',
|
|
'start= auto',
|
|
'&&',
|
|
'sc',
|
|
'start',
|
|
appHelperService,
|
|
].join(' ');
|
|
|
|
final res = runas('cmd.exe', command);
|
|
|
|
await Future.delayed(Duration(milliseconds: 300));
|
|
final retryStatus = await retry(
|
|
task: checkService,
|
|
retryIf: (status) => status == WindowsHelperServiceStatus.running,
|
|
delay: commonDuration,
|
|
);
|
|
return res && retryStatus == WindowsHelperServiceStatus.running;
|
|
}
|
|
|
|
Future<bool> registerTask(String appName) async {
|
|
final taskXml =
|
|
'''
|
|
<?xml version="1.0" encoding="UTF-16"?>
|
|
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
<Principals>
|
|
<Principal id="Author">
|
|
<LogonType>InteractiveToken</LogonType>
|
|
<RunLevel>HighestAvailable</RunLevel>
|
|
</Principal>
|
|
</Principals>
|
|
<Triggers>
|
|
<LogonTrigger/>
|
|
</Triggers>
|
|
<Settings>
|
|
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
|
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
<AllowHardTerminate>false</AllowHardTerminate>
|
|
<StartWhenAvailable>false</StartWhenAvailable>
|
|
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
<IdleSettings>
|
|
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
<RestartOnIdle>false</RestartOnIdle>
|
|
</IdleSettings>
|
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
<Enabled>true</Enabled>
|
|
<Hidden>false</Hidden>
|
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
<WakeToRun>false</WakeToRun>
|
|
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
|
|
<Priority>7</Priority>
|
|
</Settings>
|
|
<Actions Context="Author">
|
|
<Exec>
|
|
<Command>"${Platform.resolvedExecutable}"</Command>
|
|
</Exec>
|
|
</Actions>
|
|
</Task>''';
|
|
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<String>? originDns;
|
|
|
|
MacOS._internal();
|
|
|
|
factory MacOS() {
|
|
_instance ??= MacOS._internal();
|
|
return _instance!;
|
|
}
|
|
|
|
Future<String?> 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<List<String>?> 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<void> updateDns(bool restore) async {
|
|
final serviceName = await defaultServiceName;
|
|
if (serviceName == null) {
|
|
return;
|
|
}
|
|
List<String>? 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;
|