Files
FlClash/lib/providers/actions/setup.dart
T
chen08209 d780cb863d fix(core,helper): only let the owner drive the elevated Core
Changelog: The elevated Core only answers its owner
2026-10-11 12:28:52 +08:00

633 lines
19 KiB
Dart

part of '../action.dart';
enum _SetupTaskResult { completed, handoffToCoreRestart, failed }
class _RunRequest {
final bool running;
final bool initialize;
final DateTime? previousStartTime;
const _RunRequest({
required this.running,
required this.initialize,
required this.previousStartTime,
});
}
@Riverpod(keepAlive: true)
class SetupAction extends _$SetupAction {
CoreController get _core => ref.read(coreHandlerProvider);
Timer? _runtimeTimer;
final _setupScheduler = SerialTaskScheduler();
final _listenerScheduler = SerialTaskScheduler();
_RunRequest? _latestRunRequest;
DateTime? _startTime;
bool get _isRunning => _startTime != null && _startTime!.isBeforeNow;
@override
void build() {
ref.onDispose(_stopRuntimeTicker);
ref.listen(appVisibleProvider, (_, _) => _syncRuntimeTicker());
ref.listen(
appSettingProvider.select((state) => state.showTrayTitle),
(_, _) => _syncRuntimeTicker(),
);
}
bool get _isTrafficShown =>
ref.read(appVisibleProvider) ||
(system.isMacOS &&
!ref.read(safeModeProvider) &&
ref.read(appSettingProvider).showTrayTitle);
SetupParams get _setupParams {
final selectedMap = ref.read(selectedMapProvider);
final testUrl = ref.read(
appSettingProvider.select((state) => state.testUrl),
);
return SetupParams(selectedMap: selectedMap, testUrl: testUrl);
}
Future<bool> fullSetup() async {
if (!ref.read(initProvider)) return true;
ref.read(proxiesActionProvider.notifier).cancelDelayTests();
ref.read(delayDataSourceProvider.notifier).value = {};
final setupResult = applyProfile(force: true);
ref.read(logsProvider.notifier).value = FixedList(maxLogsLength);
ref.read(requestsProvider.notifier).value = FixedList(maxRequestsLength);
ref.read(dnsQueriesProvider.notifier).value = FixedList(
maxDnsQueriesLength,
);
ref.read(dnsQueryCountProvider.notifier).value = 0;
ref.read(requestCountProvider.notifier).value = 0;
try {
return await setupResult;
} catch (e, s) {
commonPrint.log('fullSetup ===> ${compactError(e)}, $s');
return false;
}
}
void _setLocalRunning(bool running) {
_stopRuntimeTicker();
if (!running) {
_startTime = null;
debouncer.cancel(FunctionTag.applyProfile);
_updateRunTime();
return;
}
_startTime ??= DateTime.now();
_updateRunTime();
_syncRuntimeTicker();
}
void _syncRuntimeTicker() {
if (_startTime == null || !_isTrafficShown) {
_stopRuntimeTicker();
return;
}
if (_runtimeTimer != null) {
return;
}
_refreshRunningState();
_runtimeTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _refreshRunningState(),
);
}
void _stopRuntimeTicker() {
_runtimeTimer?.cancel();
_runtimeTimer = null;
}
void _refreshRunningState() {
_updateRunTime();
unawaited(ref.read(commonActionProvider.notifier).updateTraffic());
}
void _updateRunTime() {
final startTime = _startTime;
ref.read(runTimeProvider.notifier).value = startTime == null
? null
: DateTime.now().millisecondsSinceEpoch -
startTime.millisecondsSinceEpoch;
}
Future<void> _updateStartTime() async {
_startTime = await service?.getRunTime();
}
Future<void> initStatus() async {
if (!globalState.needInitStatus) {
commonPrint.log('init status cancel');
return;
}
commonPrint.log('init status');
if (system.isAndroid) {
await _updateStartTime();
}
final shouldRun = _isRunning || ref.read(appSettingProvider).autoRun;
if (shouldRun) {
await setRunning(true, initialize: true);
} else {
await globalState.safeRun(() => applyProfile(force: true));
}
}
Future<bool> setRunning(bool running, {bool initialize = false}) {
if (running && !initialize && !ref.read(initProvider)) {
return Future.value(true);
}
final request = _RunRequest(
running: running,
initialize: running && initialize,
previousStartTime: _startTime,
);
_latestRunRequest = request;
_setLocalRunning(running);
if (request.initialize) {
globalState.needInitStatus = false;
}
return running ? _start(request) : _stop(request);
}
Future<bool> _start(_RunRequest request) async {
if (request.initialize) {
var applied = false;
try {
applied = await applyProfile(
force: true,
preloadInvoke: () => _setCoreRunning(request),
);
} catch (_) {
applied = false;
}
if (!applied && _isCurrent(request)) {
await globalState.safeRun(() => setRunning(false));
}
return applied;
}
try {
await _setCoreRunning(request);
} catch (_) {
_rollbackRunning(request);
rethrow;
}
if (_isCurrent(request)) {
applyProfileDebounce(force: true, silence: true);
}
return true;
}
Future<bool> _stop(_RunRequest request) async {
try {
await _setCoreRunning(request);
} catch (_) {
_rollbackRunning(request);
rethrow;
}
if (!_isCurrent(request)) {
return true;
}
resetCoreTraffic();
ref.read(trafficsProvider.notifier).clear();
ref.read(totalTrafficProvider.notifier).value = const Traffic();
return true;
}
Future<void> _setCoreRunning(_RunRequest request) {
return _listenerScheduler.run(() async {
if (!_isCurrent(request)) {
return;
}
if (request.running && ref.read(suspendProvider)) {
return;
}
if (ref.read(safeModeProvider)) {
return;
}
await setCoreRunning(request.running);
});
}
void _rollbackRunning(_RunRequest request) {
if (!_isCurrent(request)) {
return;
}
_startTime = request.previousStartTime;
_setLocalRunning(!request.running);
}
bool _isCurrent(_RunRequest request) => identical(_latestRunRequest, request);
Future<void> updateConfigDebounce() async {
debouncer.call(FunctionTag.updateConfig, updateConfig);
}
@protected
Future<bool> setCoreRunning(bool running) {
return running ? _core.startListener() : _core.stopListener();
}
@protected
void resetCoreTraffic() {
_core.resetTraffic();
}
@visibleForTesting
Future<void> updateConfig() async {
await globalState.safeRun(() async {
final patchConfig = ref.read(patchClashConfigProvider);
final shouldContinueSetup = await requestAdmin(patchConfig.tun.enable);
if (!shouldContinueSetup) {
await _restartCoreAfterAuthorization();
return;
}
final networkSetting = ref.read(networkSettingProvider);
final message = await _core.updateConfig(
_effectivePatchConfig(patchConfig).toUpdateParams(
routeMode: networkSetting.routeMode,
authentication: networkSetting.authentication.credentials,
),
);
if (message.isNotEmpty) throw MessageException(message);
});
}
void applyProfileDebounce({bool silence = false, bool force = false}) {
debouncer.call(FunctionTag.applyProfile, (silence, force) {
applyProfile(silence: silence, force: force);
}, args: [silence, force]);
}
void changeMode(Mode mode) {
ref
.read(patchClashConfigProvider.notifier)
.update((state) => state.copyWith(mode: mode));
if (mode == Mode.global) {
ref
.read(proxiesActionProvider.notifier)
.updateCurrentGroupName(GroupName.GLOBAL.name);
}
}
void autoApplyProfile() {
runAfterFrame(applyProfile);
}
// False means building the profile, the config write, or the Core setup
// step failed; a profile that fails to build is still pushed to the Core
// as the empty config so it never keeps serving the previous one.
// authorizeCore failures still throw.
Future<bool> applyProfile({
bool silence = false,
bool force = false,
Future<void> Function()? preloadInvoke,
}) async {
final result = await _runSetup(
force: force,
silence: silence,
preloadInvoke: preloadInvoke,
);
return result != _SetupTaskResult.failed;
}
Future<_SetupTaskResult> _runSetup({
bool silence = false,
bool force = false,
Future<void> Function()? preloadInvoke,
}) async {
final result = await _setupScheduler.run(() {
return _setupConfig(
force: force,
silence: silence,
preloadInvoke: preloadInvoke,
onUpdated: () async {
await ref.read(proxiesActionProvider.notifier).updateGroups();
await ref.read(providersProvider.notifier).syncProviders();
},
);
});
if (result != _SetupTaskResult.handoffToCoreRestart) {
return result;
}
// Release the current serial task before restartCore reapplies the profile.
final restarted = await _restartCoreAfterAuthorization();
return restarted ? _SetupTaskResult.completed : _SetupTaskResult.failed;
}
Future<bool> _restartCoreAfterAuthorization() async {
try {
return await ref.read(coreActionProvider.notifier).restartCore();
} catch (_) {
ref.read(authorizedTunEnableProvider.notifier).value =
TunAuthorizationState.none;
rethrow;
}
}
Future<({String yaml, String md5})> getProfile({
required SetupState setupState,
required PatchClashConfig patchConfig,
}) async {
final profileId = setupState.profileId;
if (profileId == null) return (yaml: '', md5: '');
final defaultUA = globalState.packageInfo.ua;
final networkSetting = ref.read(
networkSettingProvider.select(
(state) => (
appendSystemDns: state.appendSystemDns,
routeMode: state.routeMode,
authentication: state.authentication,
),
),
);
final overrideDns = ref.read(overrideDnsProvider);
final overrideNtp = ref.read(overrideNtpProvider);
final appendSystemDns = networkSetting.appendSystemDns;
final routeMode = networkSetting.routeMode;
final configMap = await _core.getConfig(profileId);
String? scriptContent;
final List<Rule> addedRules = [];
final List<CustomProxy> proxies = [];
final List<ProxyGroup> proxyGroups = [];
final List<Rule> rules = [];
if (setupState.overwriteType == OverwriteType.script) {
scriptContent = await setupState.script?.content;
} else if (setupState.overwriteType == OverwriteType.standard) {
addedRules.addAll(setupState.addedRules);
} else {
proxies.addAll(setupState.customProxies);
proxyGroups.addAll(setupState.proxyGroups);
rules.addAll(setupState.rules);
}
final realPatchConfig = patchConfig.copyWith(
tun: patchConfig.tun.getRealTun(routeMode),
);
Map<String, dynamic> rawConfig = configMap;
if (scriptContent?.isNotEmpty == true) {
rawConfig = await handleEvaluate(scriptContent!, rawConfig);
}
final directory = await appPath.profilesPath;
final injected = await _resolveInjectedProviders(
setupState,
proxyGroups: proxyGroups,
rules: rules,
);
final res = makeRealProfileTask(
MakeRealProfileState(
rules: rules,
proxies: proxies,
proxyGroups: proxyGroups,
injectedProxyProviders: injected.proxies,
injectedRuleProviders: injected.rules,
profilesPath: directory,
profileId: profileId,
rawConfig: rawConfig,
realPatchConfig: realPatchConfig,
overrideDns: overrideDns,
overrideNtp: overrideNtp,
appendSystemDns: appendSystemDns,
addedRules: addedRules,
defaultUA: defaultUA,
authentication: networkSetting.authentication.credentials,
matchTarget: setupState.matchTarget,
safeMode: ref.read(safeModeProvider),
),
);
return res;
}
/// Only what the overwrite actually references is injected, so an app-level
/// provider costs nothing in the profiles that never name it.
Future<({Map<String, dynamic> proxies, Map<String, dynamic> rules})>
_resolveInjectedProviders(
SetupState setupState, {
required List<ProxyGroup> proxyGroups,
required List<Rule> rules,
}) async {
final usedProxyProviders = <String>{
for (final proxyGroup in proxyGroups) ...?proxyGroup.use,
};
final usedRuleProviders = <String>{
for (final rule in rules)
if (rule.ruleAction == RuleAction.RULE_SET && rule.ruleProvider != null)
rule.ruleProvider!,
};
if (usedProxyProviders.isEmpty && usedRuleProviders.isEmpty) {
return (
proxies: const <String, dynamic>{},
rules: const <String, dynamic>{},
);
}
final proxies = <String, dynamic>{};
for (final entry in setupState.profileProviders.entries) {
if (!usedProxyProviders.contains(entry.key)) {
continue;
}
proxies[entry.key] = {
'type': 'file',
'path': await appPath.getProfilePath(entry.value.toString()),
};
}
final ruleProviders = <String, dynamic>{};
for (final provider in setupState.clashProviders) {
final used = switch (provider.kind) {
ProviderKind.proxy => usedProxyProviders,
ProviderKind.rule => usedRuleProviders,
};
if (!used.contains(provider.label)) {
continue;
}
if (provider.kind == ProviderKind.proxy &&
proxies.containsKey(provider.label)) {
commonPrint.log(
'proxy provider ${provider.label} shadowed by the profile of that name',
logLevel: LogLevel.warning,
);
continue;
}
final definition = provider.definition(await provider.path);
switch (provider.kind) {
case ProviderKind.proxy:
proxies[provider.label] = definition;
case ProviderKind.rule:
ruleProviders[provider.label] = definition;
}
}
return (proxies: proxies, rules: ruleProviders);
}
Future<String> getProfileWithId(int profileId) async {
try {
final setupState = await ref.read(setupStateProvider(profileId).future);
final patchClashConfig = ref.read(patchClashConfigProvider);
final res = await getProfile(
setupState: setupState,
patchConfig: patchClashConfig,
);
return res.yaml;
} catch (e) {
dialogs.showNotifier(e.toString(), level: MessageLevel.error);
}
return '';
}
PatchClashConfig _effectivePatchConfig(PatchClashConfig patchConfig) {
if (ref.read(safeModeProvider)) {
return patchConfig.copyWith(
tun: patchConfig.tun.copyWith(enable: false),
externalController: ExternalControllerStatus.close,
);
}
final authorized =
ref.read(authorizedTunEnableProvider) ==
TunAuthorizationState.authorized;
return patchConfig.copyWith(
tun: patchConfig.tun.copyWith(
enable: patchConfig.tun.enable && authorized,
),
externalController: patchConfig.secret.isEmpty
? ExternalControllerStatus.close
: patchConfig.externalController,
);
}
@protected
Future<AuthorizeCode> authorizeCore() {
return system.authorizeCore();
}
@visibleForTesting
Future<bool> requestAdmin(bool enableTun) async {
if (!enableTun || ref.read(safeModeProvider)) {
return true;
}
final authorizationState = ref.read(authorizedTunEnableProvider);
if (authorizationState != TunAuthorizationState.none) {
return true;
}
final authorizationNotifier = ref.read(
authorizedTunEnableProvider.notifier,
);
authorizationNotifier.value = TunAuthorizationState.unauthorized;
final code = await authorizeCore();
switch (code) {
case AuthorizeCode.success:
authorizationNotifier.value = TunAuthorizationState.authorized;
return false;
case AuthorizeCode.none:
authorizationNotifier.value = TunAuthorizationState.authorized;
return true;
case AuthorizeCode.error:
return true;
}
}
/// An empty profile list is left alone: it is the first-run state, and it is
/// what the profile stream holds before its first emission.
@visibleForTesting
Profile? recoverMissingProfile() {
final profileId = ref.read(currentProfileIdProvider);
if (profileId == null) return null;
final profiles = ref.read(profilesProvider);
if (profiles.isEmpty) return null;
final fallback = profiles.first;
commonPrint.log(
'profile $profileId is missing, falling back to ${fallback.id}',
logLevel: LogLevel.warning,
);
ref.read(currentProfileIdProvider.notifier).value = fallback.id;
return fallback;
}
Future<_SetupTaskResult> _setupConfig({
bool force = false,
bool silence = false,
Future<void> Function()? preloadInvoke,
FutureOr Function()? onUpdated,
}) async {
var profile = ref.read(currentProfileProvider) ?? recoverMissingProfile();
// A refresh failure is surfaced by safeRun; setup keeps the old profile.
final nextProfile = await globalState.safeRun(
() => profile?.checkAndUpdateAndCopy(
validate: (path) => _core.validateConfig(path),
),
);
if (nextProfile != null) {
profile = nextProfile;
ref.read(profilesProvider.notifier).put(nextProfile);
}
commonPrint.log('setup ===> ${profile?.realLabel}');
final patchConfig = ref.read(patchClashConfigProvider);
final shouldContinueSetup = await requestAdmin(patchConfig.tun.enable);
if (!shouldContinueSetup) {
return _SetupTaskResult.handoffToCoreRestart;
}
final realPatchConfig = _effectivePatchConfig(patchConfig);
final realProfile = await globalState.safeRun(() async {
final setupState = await ref.read(setupStateProvider(profile?.id).future);
return getProfile(setupState: setupState, patchConfig: realPatchConfig);
}, title: 'build profile');
final profileFailed = realProfile == null;
final yamlString = realProfile?.yaml ?? '';
final yamlMd5 = realProfile?.md5 ?? '';
if (!profileFailed && yamlMd5 == globalState.lastConfigMd5 && !force) {
return _SetupTaskResult.completed;
}
if (system.isAndroid) {
globalState.lastVpnState = ref.read(vpnStateProvider);
final sharedState = ref.read(sharedStateProvider);
await preferences.saveShareState(sharedState);
}
// Recaptured so _start's catch can roll back after safeRun swallows it.
(Object, StackTrace)? handoffFailure;
var setupFailed = false;
await globalState.loadingRun(
() async {
try {
final configFilePath = await appPath.configFilePath;
await File(configFilePath).safeWriteAsString(yamlString);
final profileId = profile?.id;
if (profileId != null) {
await appPath.ensureProviderDirs(profileId);
}
final message = await _core.setupConfig(
params: _setupParams,
preloadInvoke: preloadInvoke,
);
if (message.isNotEmpty) {
throw MessageException(message);
}
} catch (e, s) {
setupFailed = true;
if (preloadInvoke != null) {
handoffFailure = (e, s);
}
rethrow;
}
globalState.lastConfigMd5 = yamlMd5;
await onUpdated?.call();
},
silence: true,
tag: !silence ? LoadingTag.proxies : null,
);
if (handoffFailure != null) {
Error.throwWithStackTrace(handoffFailure!.$1, handoffFailure!.$2);
}
if (setupFailed || profileFailed) {
return _SetupTaskResult.failed;
}
return _SetupTaskResult.completed;
}
}