mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:34:51 +00:00
248 lines
9.0 KiB
Swift
248 lines
9.0 KiB
Swift
import Foundation
|
|
|
|
private struct LocalEndpointFailure {
|
|
let local: SslocalLocalDocument
|
|
let host: String
|
|
let outcome: EndpointHealthProbe.Outcome
|
|
}
|
|
|
|
private enum LaunchHealthAttempt {
|
|
case ready(RuntimeDeploymentReceipt?)
|
|
case retry(LocalEndpointFailure?)
|
|
case agentLost
|
|
}
|
|
|
|
private struct LaunchHealthTimeout {
|
|
let expectedDigest: String?
|
|
let endpointFailure: LocalEndpointFailure?
|
|
let generation: Int
|
|
let preparation: Int
|
|
let preserveProxyOnFailure: Bool
|
|
}
|
|
|
|
extension ProxyRuntimeController {
|
|
/// 启动健康呈现:确认本地入站;直连模式额外要求
|
|
/// 回执持续指向当前健康子进程,防止旧监听通过共享端口冒充新实例。
|
|
@discardableResult
|
|
func presentLaunchHealth(
|
|
_ document: SslocalRuntimeDocument,
|
|
requiresReceipt: Bool = false,
|
|
convergeProxyOnSuccess: Bool = true,
|
|
preserveProxyOnFailure: Bool = false,
|
|
preparedContract: PreparedRuntimeContract? = nil,
|
|
reloadingProcessID: Int32? = nil
|
|
) async -> Bool {
|
|
guard preparedContract == nil || preparedContract?.document == document else {
|
|
return await reportRuntimeFileFailure(preserveProxyOnFailure: preserveProxyOnFailure)
|
|
}
|
|
let facts = RuntimeDeploymentFacts(document)
|
|
let generation = flowGeneration
|
|
let preparation = runtimePreparationGeneration
|
|
let expectedDigest =
|
|
requiresReceipt ? runtimeFileStore.receiptIdentity(for: document.intentSHA256) : nil
|
|
guard !requiresReceipt || expectedDigest != nil else {
|
|
return await reportRuntimeFileFailure(preserveProxyOnFailure: preserveProxyOnFailure)
|
|
}
|
|
|
|
let deadline = Date().addingTimeInterval(launchHealthTimeoutSeconds)
|
|
var endpointFailure: LocalEndpointFailure?
|
|
while Date() < deadline {
|
|
guard generation == flowGeneration, preparation == runtimePreparationGeneration else {
|
|
return false
|
|
}
|
|
let attempt = await launchHealthAttempt(
|
|
facts, expectedDigest: expectedDigest, reloadingProcessID: reloadingProcessID)
|
|
guard generation == flowGeneration, preparation == runtimePreparationGeneration else {
|
|
return false
|
|
}
|
|
switch attempt {
|
|
case .ready(let receipt):
|
|
return await finishHealthyLaunch(
|
|
facts,
|
|
receipt: receipt,
|
|
generation: (generation, preparation),
|
|
convergeProxyOnSuccess: convergeProxyOnSuccess,
|
|
preserveProxyOnFailure: preserveProxyOnFailure)
|
|
case .retry(let endpoint):
|
|
endpointFailure = endpoint
|
|
case .agentLost:
|
|
return await reportAgentLost(preserveProxyOnFailure: preserveProxyOnFailure)
|
|
}
|
|
try? await launchHealthRetryDelay()
|
|
}
|
|
return await reportLaunchHealthTimeout(
|
|
LaunchHealthTimeout(
|
|
expectedDigest: expectedDigest,
|
|
endpointFailure: endpointFailure,
|
|
generation: generation, preparation: preparation,
|
|
preserveProxyOnFailure: preserveProxyOnFailure))
|
|
}
|
|
|
|
private func launchHealthAttempt(
|
|
_ document: RuntimeDeploymentFacts,
|
|
expectedDigest: String?, reloadingProcessID: Int32? = nil
|
|
) async -> LaunchHealthAttempt {
|
|
if runtimeFileStore.blocksDeployment(document.intentSHA256) { return .agentLost }
|
|
var receiptForHealth: RuntimeDeploymentReceipt?
|
|
if let expectedDigest {
|
|
guard let receipt = runtimeFileStore.readRuntimeReceipt(),
|
|
receipt.deploymentID == expectedDigest
|
|
else {
|
|
return .retry(nil)
|
|
}
|
|
guard receipt.processID > 0, processIsAlive(receipt.processID) else {
|
|
return .agentLost
|
|
}
|
|
receiptForHealth = receipt
|
|
}
|
|
|
|
if reloadingProcessID == nil || receiptForHealth?.processID != reloadingProcessID {
|
|
if reloadingProcessID != nil { state = .starting }
|
|
if let endpointFailure = await unhealthyLocalEndpoint(in: document) {
|
|
return .retry(endpointFailure)
|
|
}
|
|
}
|
|
|
|
if let receiptForHealth {
|
|
guard runtimeFileStore.readRuntimeReceipt() == receiptForHealth else {
|
|
return .retry(nil)
|
|
}
|
|
guard processIsAlive(receiptForHealth.processID) else { return .agentLost }
|
|
}
|
|
return .ready(receiptForHealth)
|
|
}
|
|
|
|
private func finishHealthyLaunch(
|
|
_ document: RuntimeDeploymentFacts,
|
|
receipt: RuntimeDeploymentReceipt?,
|
|
generation: (flow: Int, preparation: Int),
|
|
convergeProxyOnSuccess: Bool,
|
|
preserveProxyOnFailure: Bool
|
|
) async -> Bool {
|
|
guard generation.flow == flowGeneration,
|
|
generation.preparation == runtimePreparationGeneration
|
|
else { return false }
|
|
await presentFirewallStatus(for: document)
|
|
guard generation.flow == flowGeneration,
|
|
generation.preparation == runtimePreparationGeneration
|
|
else { return false }
|
|
if let receipt, !receiptIsCurrentAndLive(receipt) {
|
|
return await reportAgentLost(preserveProxyOnFailure: preserveProxyOnFailure)
|
|
}
|
|
if convergeProxyOnSuccess { await systemProxyObserver.convergeSystemProxy() }
|
|
return true
|
|
}
|
|
|
|
private func receiptIsCurrentAndLive(_ receipt: RuntimeDeploymentReceipt) -> Bool {
|
|
guard receipt.processID > 0,
|
|
runtimeFileStore.readRuntimeReceipt() == receipt
|
|
else { return false }
|
|
return processIsAlive(receipt.processID)
|
|
}
|
|
|
|
private func reportAgentLost(preserveProxyOnFailure: Bool) async -> Bool {
|
|
guard !preserveProxyOnFailure else { return false }
|
|
state = .serviceFailed(.agent)
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
return false
|
|
}
|
|
|
|
private func reportRuntimeFileFailure(preserveProxyOnFailure: Bool) async -> Bool {
|
|
guard !preserveProxyOnFailure else { return false }
|
|
state = .serviceFailed(.runtimeFile)
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
return false
|
|
}
|
|
|
|
private func reportLaunchHealthTimeout(_ timeout: LaunchHealthTimeout) async -> Bool {
|
|
guard timeout.generation == flowGeneration,
|
|
timeout.preparation == runtimePreparationGeneration
|
|
else { return false }
|
|
guard !timeout.preserveProxyOnFailure else { return false }
|
|
if let expectedDigest = timeout.expectedDigest,
|
|
!hasLiveReceipt(expectedDigest: expectedDigest)
|
|
{
|
|
return await reportAgentLost(preserveProxyOnFailure: false)
|
|
}
|
|
if let endpointFailure = timeout.endpointFailure {
|
|
presentEndpointFailure(endpointFailure)
|
|
} else {
|
|
state = .launchFailed(
|
|
.localEndpoint(endpoint: "SOCKS", host: "127.0.0.1", port: 0, cause: .unknown))
|
|
}
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
return false
|
|
}
|
|
|
|
private func hasLiveReceipt(expectedDigest: String) -> Bool {
|
|
guard let receipt = runtimeFileStore.readRuntimeReceipt(),
|
|
receipt.deploymentID == expectedDigest
|
|
else { return false }
|
|
return receiptIsCurrentAndLive(receipt)
|
|
}
|
|
|
|
private func unhealthyLocalEndpoint(
|
|
in document: RuntimeDeploymentFacts
|
|
) async -> LocalEndpointFailure? {
|
|
for local in document.locals {
|
|
let outcome = await probeAsync(
|
|
host: local.probeHost, port: local.localPort, timeout: 1.5)
|
|
if outcome != .reachable {
|
|
return LocalEndpointFailure(local: local, host: local.probeHost, outcome: outcome)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func presentEndpointFailure(_ failure: LocalEndpointFailure) {
|
|
let detail: String
|
|
let cause: RuntimeEndpointFailure
|
|
switch failure.outcome {
|
|
case .reachable:
|
|
detail = "已连通"
|
|
cause = .unknown
|
|
case .refused(let reason):
|
|
detail = reason
|
|
cause = .refused
|
|
case .timedOut:
|
|
detail = "连接超时"
|
|
cause = .timedOut
|
|
}
|
|
RuntimeLog.emit(
|
|
.endpointProbeFailed(
|
|
host: failure.host, port: failure.local.localPort, detail: detail))
|
|
let endpointName = failure.local.inboundProtocol.uppercased()
|
|
state = .launchFailed(
|
|
.localEndpoint(
|
|
endpoint: endpointName, host: failure.host, port: failure.local.localPort, cause: cause))
|
|
}
|
|
}
|
|
|
|
extension ProxyRuntimeController {
|
|
/// Agent 独占稳定运行期间的故障判定;GUI 只读取明确失败,避免重复连接入站。
|
|
/// 任务柄仍由系统代理观察机持有,并保留批准状态与待首次应用的刷新。
|
|
func runSystemProxyHealthObservationLoop() async {
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(nanoseconds: systemProxyHealthPollIntervalNanoseconds)
|
|
guard !Task.isCancelled,
|
|
settings.systemProxyEnabled || systemProxyState.hasOperationFailure
|
|
else { return }
|
|
systemProxyObserver.refreshSystemProxyApproval()
|
|
systemProxyObserver.updateSystemProxyActions()
|
|
guard settings.systemProxyEnabled, state != .starting, runtimeReloadProcesses.isEmpty,
|
|
!systemProxyObserver.systemProxyStartupInProgress, settings.agentEnabled,
|
|
let document = runtimeObservation
|
|
else { continue }
|
|
if let failure = runtimeFileStore.readFailure(),
|
|
let deploymentID = runtimeFileStore.receiptIdentity(for: document.intentSHA256),
|
|
failure.deploymentID == deploymentID
|
|
{
|
|
state = .serviceFailed(.agent)
|
|
await systemProxyObserver.suspendSystemProxy()
|
|
} else if systemProxyObserver.systemProxyInitialApplyPending {
|
|
await systemProxyObserver.convergeSystemProxy()
|
|
}
|
|
}
|
|
}
|
|
}
|