Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/StatusMenuModel.swift
T
Qiu Yuzhou d2a8d6105c feat(system-proxy): route writes through privileged LaunchDaemon helper
Implements issue #71 with ADR-0020 (supersedes ADR-0019's endpoint-signature
cleanup, startup cleanup, and health-gate cleanup).

- New ShadowsocksX-NG2SystemProxyHelper target: app-bundled root LaunchDaemon
  (SMAppService.daemon, on-demand via MachServices) that mechanically applies
  a closed typed configuration or executes an unconditional clear. Clients are
  accepted by code-signing bundle identifier only; requests run serially on a
  shared queue (last-writer-wins).
- SystemProxyConfiguration is now the closed typed apply payload (Codable over
  JSON XPC): explicit SOCKS/HTTP/HTTPS enable + endpoints, PAC and
  auto-discovery disabled with URL/JS removed, explicit simple-hostname
  exclusion, complete ExceptionsList. Unmodeled Proxies keys are preserved and
  equivalent values skip the commit.
- Cleanup is a side effect of a persisted system-proxy intent on-to-off
  transition only: the switch itself, or the agent-off cascade (persist both
  intents off, clear, then stop listeners; failed cascade persistence records
  no transition and clears nothing). Launch with the switch off, health-gate
  closure, invalid targets, and passive events never clear. Failed clears are
  surfaced without deferred retry.
- Endpoint-signature store, ownership migration, and direct Authorization
  Services writes are removed; old signature files stay inert. Helper
  unavailability or pending approval keeps an enabled intent pending and
  surfaces the Login Items approval path (HomeView button + menu label).
2026-09-30 12:26:40 +08:00

100 lines
4.1 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
/// 状态菜单的呈现模型(spec #21 D11,issue #31/#47/#60):从代理控制工作流
/// 的整体 snapshot 派生菜单数据。agent 与系统代理是两个独立状态面;不持有
/// AppKit 类型,纯逻辑可测;视觉与交互细节(勾选标记、禁用态)在
/// ProxyStatusMenu 呈现,不建自动测试。
enum StatusMenuModel {
/// 头部状态摘要:agent 运行状态、当前模式、活动目标(含点名失败细节)与
/// 系统代理实际应用状态。
struct Summary: Equatable {
/// Agent 开关意图(开关勾选态的绑定来源)。
let agentIntentEnabled: Bool
/// Agent 运行状态的「在跑」投影。
let isOn: Bool
/// Agent 运行状态文本。
let status: String
/// Agent 侧点名原因:运行失败或激活拒绝/目标清除(独立于运行状态)。
let detail: String?
/// 系统代理开关意图(开关勾选态的绑定来源)。
let systemProxyIntentEnabled: Bool
/// 系统代理实际应用状态文本(与 agent 运行状态分开呈现)。
let systemProxyStatus: String
/// 同上但不带「系统代理:」前缀(首页等已有上下文的呈现面用)。
let systemProxyStateLabel: String
/// 系统代理写入/清理失败的点名原因;非失败态为 nil。
let systemProxyDetail: String?
let modeLabel: String
/// 活动目标从根到节点的显示名路径;未激活为 nil。
let targetPath: String?
}
/// 运行状态 → 摘要映射(与主窗口诊断区口径一致):输入是代理控制工作流
/// 的整体 snapshot(issue #47/#60),菜单不再拆装控制器或运行时字段。
static func summary(from snapshot: ProxyControlSnapshot) -> Summary {
let agentDetail =
snapshot.runtime.failure.map { AppPresentation.message(for: $0) }
?? snapshot.activationFailure.map { AppPresentation.message(for: $0) }
let systemProxy = systemProxyPresentation(
for: snapshot.systemProxyApplication,
approvalRequired: snapshot.systemProxyApprovalRequired)
return Summary(
agentIntentEnabled: snapshot.agentIntentEnabled,
isOn: snapshot.runtime.isOn,
status: statusText(for: snapshot.runtime.status),
detail: agentDetail,
systemProxyIntentEnabled: snapshot.systemProxyIntentEnabled,
systemProxyStatus: "系统代理:\(systemProxy.label)",
systemProxyStateLabel: systemProxy.label,
systemProxyDetail: systemProxy.detail,
modeLabel: modeLabel(for: snapshot),
targetPath: snapshot.activeTarget?.pathSummary)
}
/// 模式标签与首页徽标同口径(issue #67 AC2):规则模式带上子选项。
private static func modeLabel(for snapshot: ProxyControlSnapshot) -> String {
let mode = snapshot.proxyMode
if mode == .rule {
return "\(mode.label) · \(snapshot.ruleDefaultAction.label)"
}
return mode.label
}
private static func statusText(for status: ProxyRuntimeStatus) -> String {
switch status {
case .off:
return "代理未运行"
case .starting:
return "正在启动代理…"
case .running:
return "代理运行中"
case .firewallBlocked:
return "代理运行中(局域网受阻)"
case .launchFailed:
return "启动失败"
case .requiresApproval:
return "等待允许后台代理"
case .serviceFailed:
return "服务管理失败"
}
}
/// 系统代理应用状态 → 菜单文本(issue #71):helper 待批准的待应用态以
/// 「待批准」呈现,提示用户走登录项批准路径。
private static func systemProxyPresentation(
for application: SystemProxyApplicationFacts,
approvalRequired: Bool
) -> (label: String, detail: String?) {
switch application {
case .idle:
return ("未应用", nil)
case .pending:
return approvalRequired ? ("待批准", nil) : ("待应用", nil)
case .applied:
return ("已应用", nil)
case .failed(let facts):
return ("应用失败", AppPresentation.message(for: RuntimeFailureFacts.systemProxy(facts)))
}
}
}