mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
353 lines
13 KiB
Swift
353 lines
13 KiB
Swift
import Foundation
|
|
|
|
// MARK: - 用户意图与收敛(命令面)
|
|
|
|
extension ProxyRuntimeController {
|
|
// MARK: - 用户意图
|
|
|
|
/// 激活一个服务器或分组目标(目录 UI 工单复用入口):持久化目标;agent
|
|
/// 意图开启时立即把新档推到运行时。激活原子失败时目标与运行时完全不动;
|
|
/// 点名原因进 `lastActivationFailure` 并随 `.rejectedActivation` 自含返回;
|
|
/// 意外错误 throws 并进入 `serviceFailed`。
|
|
@discardableResult
|
|
func activate(_ target: NodeID) async throws -> ActivationCommandOutcome {
|
|
guard catalogSnapshotReader.isCatalogAvailable else { throw ServerLibraryError.notLoaded }
|
|
refreshPluginSecurityFacts()
|
|
let catalog = catalogSnapshotReader.catalogSnapshot
|
|
do {
|
|
let configuration = try machine.activate(
|
|
target, in: catalog, plugins: plugins,
|
|
options: settings.runtimeDocumentOptions)
|
|
activeTargetID = target
|
|
skippedServers = configuration.skippedServers
|
|
lastActivationFailure = nil
|
|
do {
|
|
try activationFileStore.save(activeTargetID: target)
|
|
} catch {
|
|
state = .serviceFailed(.persistence)
|
|
throw error
|
|
}
|
|
if settings.agentEnabled {
|
|
await deploy(configuration.document)
|
|
}
|
|
return .activated(skippedInvalid: configuration.skippedServers.count)
|
|
} catch let failure as ActivationFailure {
|
|
lastActivationFailure = failure
|
|
return .rejectedActivation(failure)
|
|
} catch {
|
|
state = .serviceFailed(.unknown)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/// Agent 开关:先持久化意图(显式关闭在 GUI 重启后仍生效),
|
|
/// 再收敛运行时。UserDefaults 不提供同步落盘失败信号。意图已
|
|
/// 开启时的开启命令仅在未达健康态时重收敛——它是启动失败/重置后的显式
|
|
/// 重试入口;健康运行中不做无谓的注销重拉。关闭时级联:系统代理意图仍
|
|
/// 开启则一并持久化为关闭,该 on→off 迁移触发清理,清理后才停止本地监听;
|
|
/// 系统代理意图本就关闭则不做任何系统设置操作。
|
|
func setAgentEnabled(_ enabled: Bool) async {
|
|
let retryingFailure = enabled && runtimeFileStore.readFailure() != nil
|
|
if enabled {
|
|
do { try runtimeFileStore.clearFailure() } catch {
|
|
state = .serviceFailed(.runtimeFile)
|
|
return
|
|
}
|
|
}
|
|
guard enabled != settings.agentEnabled else {
|
|
await handleUnchangedAgentIntent(enabled, retryingFailure: retryingFailure)
|
|
return
|
|
}
|
|
controlPreferences.agentEnabled = enabled
|
|
settings.agentEnabled = enabled
|
|
if enabled {
|
|
await enableAgentAfterIntent()
|
|
} else {
|
|
await disableAgentAfterIntent()
|
|
}
|
|
}
|
|
|
|
private func handleUnchangedAgentIntent(_ enabled: Bool, retryingFailure: Bool) async {
|
|
guard enabled else { return }
|
|
if retryingFailure {
|
|
await convergeAgent()
|
|
return
|
|
}
|
|
if ruleApplicationFailure != nil {
|
|
scheduleRuleApplication()
|
|
return
|
|
}
|
|
switch state {
|
|
case .off, .launchFailed, .serviceFailed: await convergeAgent()
|
|
case .starting, .running, .firewallBlocked, .requiresApproval: break
|
|
}
|
|
}
|
|
|
|
private func enableAgentAfterIntent() async {
|
|
await convergeAgent()
|
|
if settings.systemProxyEnabled { systemProxyObserver.startEnabledSystemProxyObservation() }
|
|
}
|
|
|
|
private func disableAgentAfterIntent() async {
|
|
let cleanupOutcome = await cascadeSystemProxyOffForAgentOff()
|
|
await stopAgent()
|
|
if let cleanupOutcome {
|
|
systemProxyObserver.presentCleanupOutcome(cleanupOutcome)
|
|
} else {
|
|
systemProxyObserver.updateSystemProxyActions()
|
|
}
|
|
}
|
|
|
|
/// 关闭 agent 时的系统代理级联:意图仍开启则一并持久化为
|
|
/// 关闭并请求清理(返回清理结果),不依赖监听文件保存。
|
|
private func cascadeSystemProxyOffForAgentOff() async -> SystemProxyControlState? {
|
|
guard settings.systemProxyEnabled else { return nil }
|
|
controlPreferences.systemProxyEnabled = false
|
|
settings.systemProxyEnabled = false
|
|
// 持久化的 on→off 迁移才触发清理;先清后停本地监听。
|
|
return await systemProxyObserver.clearAndStopSystemProxyObservation()
|
|
}
|
|
|
|
/// 系统代理开关:先持久化意图。开启时经 helper 注册门禁收敛
|
|
/// 应用;关闭即一次 on→off 意图迁移——请求一次无条件清理(临时观察网络
|
|
/// 变化直至安静,失败也不后台重试),不影响 agent 注册与本地监听。
|
|
func setSystemProxyEnabled(_ enabled: Bool) async {
|
|
guard enabled != settings.systemProxyEnabled else { return }
|
|
controlPreferences.systemProxyEnabled = enabled
|
|
settings.systemProxyEnabled = enabled
|
|
if enabled {
|
|
await systemProxyObserver.enableSystemProxyIntent()
|
|
} else {
|
|
await systemProxyObserver.disableSystemProxyIntent()
|
|
}
|
|
}
|
|
|
|
/// GUI startup restores the Agent. Healthy proxy intent only inspects; unhealthy
|
|
/// proxy intent suspends; disabled intent never writes SystemConfiguration.
|
|
func resyncOnLaunch() async {
|
|
guard !settingsUnreadable else {
|
|
state = .launchFailed(.unreadableSettings)
|
|
return
|
|
}
|
|
guard catalogSnapshotReader.isCatalogAvailable else { return }
|
|
refreshPluginSecurityFacts()
|
|
systemProxyObserver.beginStartupResync()
|
|
guard settings.agentEnabled else {
|
|
await stopAgent()
|
|
await systemProxyObserver.settleLaunchWithAgentOff()
|
|
return
|
|
}
|
|
let catalog = catalogSnapshotReader.catalogSnapshot
|
|
await convergeToReexpanded(catalog)
|
|
await systemProxyObserver.settleLaunchAfterAgentConverged()
|
|
}
|
|
|
|
// MARK: - 动作执行
|
|
|
|
/// Agent 意图开启的收敛:注册态不是事实来源,意图才是——未注册也会注册,
|
|
/// 已注册则重校验目标并部署。偏好重置后也走此路径,把运行时收敛到出厂设置。
|
|
func convergeAgent() async {
|
|
await convergeToReexpanded(catalogSnapshotReader.catalogSnapshot)
|
|
}
|
|
|
|
/// Agent 意图关闭的收敛:只停止本地监听,不做系统代理清理。
|
|
/// 系统代理状态按意图事实收尾——级联后意图已关闭为空闲;残留的「意图开
|
|
/// 启 + agent 关闭」组合保持待应用(不静默清理,也无需网络观察)。
|
|
func stopAgent() async {
|
|
systemProxyObserver.agentDidStop()
|
|
let outcome = await execute(.stop, document: nil)
|
|
guard outcome.succeeded else { return }
|
|
state = .off
|
|
runtimeObservation = nil
|
|
skippedServers = []
|
|
lastActivationFailure = nil
|
|
}
|
|
|
|
/// 无活动目标时 agent 仍提供本地监听:空服务器列表文档,
|
|
/// SOCKS/HTTP 端点照常绑定;系统代理门禁会因缺少可用出口保持待应用。
|
|
@discardableResult
|
|
func deployListeningWithoutTarget() async -> ConvergenceResult {
|
|
let document = SslocalRuntimeDocument(
|
|
servers: [],
|
|
listen: settings.listen)
|
|
return await deploy(document)
|
|
}
|
|
|
|
/// 普通部署意图;配置派生、执行与失败呈现由运行状态收敛 module 拥有。
|
|
@discardableResult
|
|
func deploy(_ sourceDocument: SslocalRuntimeDocument) async -> ConvergenceResult {
|
|
refreshPluginSecurityFacts()
|
|
let result = await convergeRuntime(.deployment(sourceDocument))
|
|
if case .failed = result { refreshPluginSecurityFacts() }
|
|
return result
|
|
}
|
|
|
|
/// 活动目标失效:清除并持久化 nil,点名原因独立呈现;agent
|
|
/// 继续以空服务器列表监听;系统代理清理后保留意图,条件恢复后自动
|
|
/// 收敛),不悄悄选择其他服务器。
|
|
@discardableResult
|
|
func handleCleared(_ failure: ActivationFailure) async -> ConvergenceResult {
|
|
RuntimeLog.emit(.activationFailed(reason: String(describing: failure)))
|
|
do {
|
|
try activationFileStore.save(activeTargetID: nil)
|
|
} catch {
|
|
// 清目标失败不阻断收敛:下次重同步会再次收敛(目标已不在状态机中)。
|
|
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
|
}
|
|
lastActivationFailure = failure
|
|
skippedServers = []
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
if settings.agentEnabled {
|
|
return await deployListeningWithoutTarget()
|
|
} else {
|
|
// 会话内意图已被关闭的残余:按关闭语义收敛。
|
|
let outcome = await execute(.stop, document: nil)
|
|
guard outcome.succeeded else { return .failed }
|
|
state = .off
|
|
runtimeObservation = nil
|
|
return .applied
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 计划动作执行
|
|
|
|
extension ProxyRuntimeController {
|
|
/// 把「运行定义档」意图推到运行时:计划动作 → 顺序执行。返回全部动作
|
|
/// 是否成功与本次占用的并发流代际。
|
|
func execute(
|
|
_ intent: RuntimeIntent, document: SslocalRuntimeDocument?,
|
|
preparedContract: PreparedRuntimeContract? = nil
|
|
) async -> RuntimeExecutionOutcome {
|
|
cancelFirewallObservation()
|
|
flowGeneration += 1
|
|
let flow = flowGeneration
|
|
let succeeded = await performPlannedActions(
|
|
intent, document: document, preparedContract: preparedContract, flow: flow)
|
|
return RuntimeExecutionOutcome(flow: flow, succeeded: succeeded)
|
|
}
|
|
|
|
/// 按计划顺序执行动作;返回 false 表示中途失败、状态已呈现(后续动作与
|
|
/// 健康探测都不应继续)。
|
|
private func performPlannedActions(
|
|
_ intent: RuntimeIntent, document: SslocalRuntimeDocument?,
|
|
preparedContract: PreparedRuntimeContract?, flow: Int
|
|
) async -> Bool {
|
|
let preparation = runtimePreparationGeneration
|
|
let contract: PreparedRuntimeContract?
|
|
if case .run(let desired) = intent {
|
|
guard let prepared = preparedContract ?? (try? PreparedRuntimeContract(desired)),
|
|
prepared.document == desired, document == desired
|
|
else {
|
|
state = .serviceFailed(document == nil ? .missingDocument : .runtimeFile)
|
|
return false
|
|
}
|
|
contract = prepared
|
|
} else {
|
|
contract = nil
|
|
}
|
|
let actions = ProxyRuntimePlan.actions(
|
|
intent: intent,
|
|
agentStatus: agent.status,
|
|
wrapper: wrapperState(),
|
|
configurationIsCurrent: contract.map { runtimeFileStore.contains($0) } ?? false)
|
|
if case .run = intent, actions.isEmpty {
|
|
RuntimeLog.emit(.contractUnchanged)
|
|
}
|
|
for action in actions {
|
|
guard flow == flowGeneration, preparation == runtimePreparationGeneration,
|
|
await perform(action, preparedContract: contract)
|
|
else { return false }
|
|
}
|
|
return flow == flowGeneration && preparation == runtimePreparationGeneration
|
|
}
|
|
|
|
/// 执行单个动作;返回 false 表示应终止后续动作(状态已呈现)。
|
|
private func perform(
|
|
_ action: RuntimeAction, preparedContract: PreparedRuntimeContract?
|
|
) async -> Bool {
|
|
switch action {
|
|
case .writeContract:
|
|
return performWriteContract(preparedContract)
|
|
case .registerAgent:
|
|
return performAgentRegistration()
|
|
case .unregisterAgent:
|
|
return await performAgentUnregistration()
|
|
case .signalReload(let pid):
|
|
return performSignalReload(pid)
|
|
case .deleteRuntimeFiles:
|
|
do { try runtimeFileStore.deleteRuntimeFiles() } catch {
|
|
state = .serviceFailed(.runtimeFile)
|
|
return false
|
|
}
|
|
RuntimeLog.emit(.runtimeFilesDeleted)
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// 原子写契约文件;缺文档或写失败即终止。
|
|
private func performWriteContract(_ contract: PreparedRuntimeContract?) -> Bool {
|
|
guard let contract else {
|
|
state = .serviceFailed(.missingDocument)
|
|
return false
|
|
}
|
|
do {
|
|
try runtimeFileStore.write(contract)
|
|
RuntimeLog.emit(.contractWritten(serverCount: contract.document.servers.count))
|
|
return true
|
|
} catch {
|
|
state = .serviceFailed(.runtimeFile)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// 注册 LaunchAgent;需用户批准或注册失败即终止并呈现对应状态。
|
|
private func performAgentRegistration() -> Bool {
|
|
do {
|
|
try agent.register()
|
|
} catch {
|
|
RuntimeLog.emit(.agentRegisterFailed(detail: describe(error)))
|
|
if agent.status == .requiresApproval {
|
|
// 注册请求已被系统接收,等待用户在登录项中批准。
|
|
state = .requiresApproval
|
|
return false
|
|
}
|
|
if agent.status != .registered {
|
|
state = .serviceFailed(.agent)
|
|
return false
|
|
}
|
|
// 注册与状态读取之间的竞态:已注册即达意图,不视为失败。
|
|
}
|
|
RuntimeLog.emit(.agentRegistered)
|
|
if agent.status == .requiresApproval {
|
|
state = .requiresApproval
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// 注销 LaunchAgent 并等待 wrapper 退出;未注册竞态可忽略(尽力而为)。
|
|
private func performAgentUnregistration() async -> Bool {
|
|
do {
|
|
try agent.unregister()
|
|
} catch {
|
|
RuntimeLog.emit(.agentUnregisterFailed(detail: describe(error)))
|
|
}
|
|
RuntimeLog.emit(.agentUnregistered)
|
|
await waitForWrapperExit()
|
|
guard wrapperState() == .notRunning, !runtimeFileStore.hasUnconfirmedChild else {
|
|
state = .serviceFailed(.agent)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private func performSignalReload(_ pid: Int32) -> Bool {
|
|
guard sendSignal(pid, SIGUSR1) == 0 else {
|
|
state = .serviceFailed(.agent)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|