Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift
T

326 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - 用户意图与收敛(命令面)
extension ProxyRuntimeController {
// MARK: - 用户意图
/// 激活一个服务器或分组目标(目录 UI 工单复用入口):持久化目标;agent
/// 意图开启时立即把新档推到运行时。激活原子失败时目标与运行时完全不动
/// (D3),点名原因进 `lastActivationFailure` 并随 `.rejectedActivation`
/// 自含返回;意外错误 throws 并进入 `serviceFailed`。
@discardableResult
func activate(_ target: NodeID) async throws -> ActivationCommandOutcome {
refreshPluginSecurityFacts()
let catalog = catalogSnapshotReader.catalogSnapshot
do {
let configuration = try machine.activate(
target, in: catalog, credentials: credentials, 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 开关(issue #60/#71):先持久化意图(显式关闭在 GUI 重启后仍生效),
/// 再收敛运行时。持久化失败保留现状并点名,不静默偏离持久化事实。意图已
/// 开启时的开启命令仅在未达健康态时重收敛——它是启动失败/重置后的显式
/// 重试入口;健康运行中不做无谓的注销重拉。关闭时级联:系统代理意图仍
/// 开启则一并持久化为关闭,该 on→off 迁移触发清理,清理后才停止本地监听;
/// 系统代理意图本就关闭则不做任何系统设置操作。
func setAgentEnabled(_ enabled: Bool) async {
if enabled == settings.agentEnabled {
guard enabled else { return }
if ruleApplicationFailure != nil {
scheduleRuleApplication()
return
}
switch state {
case .off, .launchFailed, .serviceFailed:
await convergeAgent()
case .starting, .running, .firewallBlocked, .requiresApproval:
break
}
return
}
var next = settings
next.agentEnabled = enabled
guard persistSettings(next) else { return }
settings = next
if enabled {
await convergeAgent()
if settings.systemProxyEnabled { systemProxyObserver.startEnabledSystemProxyObservation() }
} else {
let cleanupOutcome = await cascadeSystemProxyOffForAgentOff()
await stopAgent()
if let cleanupOutcome {
systemProxyObserver.presentCleanupOutcome(cleanupOutcome)
} else {
systemProxyObserver.updateSystemProxyActions()
}
}
}
/// 关闭 agent 时的系统代理级联(issue #71):意图仍开启则一并持久化为
/// 关闭并请求清理(返回清理结果);级联持久化失败时迁移未被记录,不清理
/// (story 13),意图与系统设置原样,返回 nil。
private func cascadeSystemProxyOffForAgentOff() async -> SystemProxyControlState? {
guard settings.systemProxyEnabled else { return nil }
var cascade = settings
cascade.systemProxyEnabled = false
do {
try settingsStore.save(cascade)
} catch {
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
return nil
}
settings = cascade
// 持久化的 on→off 迁移才触发清理;先清后停本地监听。
return await systemProxyObserver.clearAndStopSystemProxyObservation()
}
/// 系统代理开关(issue #71):先持久化意图。开启时经 helper 注册门禁收敛
/// 应用;关闭即一次 on→off 意图迁移——请求一次无条件清理(临时观察网络
/// 变化直至安静,失败也不后台重试),不影响 agent 注册与本地监听。
func setSystemProxyEnabled(_ enabled: Bool) async {
guard enabled != settings.systemProxyEnabled else { return }
var next = settings
next.systemProxyEnabled = enabled
guard persistSettings(next) else { return }
settings = next
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 {
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 意图关闭的收敛(issue #71):只停止本地监听,不做系统代理清理。
/// 系统代理状态按意图事实收尾——级联后意图已关闭为空闲;残留的「意图开
/// 启 + agent 关闭」组合保持待应用(不静默清理,也无需网络观察)。
func stopAgent() async {
systemProxyObserver.agentDidStop()
_ = await execute(.stop, document: nil)
state = .off
lastDocument = nil
skippedServers = []
lastActivationFailure = nil
}
/// 无活动目标时 agent 仍提供本地监听(issue #60):空服务器列表文档,
/// SOCKS/HTTP 端点照常绑定;系统代理门禁会因缺少可用出口保持待应用。
func deployListeningWithoutTarget() async {
let document = SslocalRuntimeDocument(
servers: [],
listen: settings.listen)
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
}
/// 活动目标失效(issue #60):清除并持久化 nil,点名原因独立呈现;agent
/// 继续以空服务器列表监听;系统代理清理后保留意图,条件恢复后自动
/// 收敛),不悄悄选择其他服务器。
func handleCleared(_ failure: ActivationFailure) async {
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 {
await deployListeningWithoutTarget()
} else {
// 会话内意图已被关闭的残余:按关闭语义收敛。
_ = await execute(.stop, document: nil)
state = .off
lastDocument = nil
}
}
}
// 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(),
contractOnDisk: runtimeFileStore.readData(), preparedContract: contract)
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:
runtimeFileStore.deleteRuntimeFiles()
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()
return true
}
private func performSignalReload(_ pid: Int32) -> Bool {
guard sendSignal(pid, SIGUSR1) == 0 else {
state = .serviceFailed(.agent)
return false
}
return true
}
}