From 5a05b8a6619d0c0dbc8eaf0625276aa105695fc1 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sun, 27 Sep 2026 17:51:34 +0800 Subject: [PATCH] refactor(ng2): use sslocal runtime defaults --- CONTEXT.md | 4 +-- ShadowsocksX-NG2/Agent/AgentContract.swift | 6 +--- ShadowsocksX-NG2/App/ErrorPresentation.swift | 4 +-- .../App/ProxyRuntimeController+Commands.swift | 4 +-- ShadowsocksX-NG2/App/SettingsView.swift | 12 ------- .../SettingsDraftAdapter.swift | 6 ---- .../SettingsWorkflowModels.swift | 9 ++--- .../Domain/ActivationStateMachine.swift | 11 ++---- ShadowsocksX-NG2/Domain/ProxySettings.swift | 10 ------ .../Domain/ProxySettingsFileStore.swift | 10 +----- .../Domain/SslocalListenSettings.swift | 4 +-- .../Domain/SslocalRuntimeDocument.swift | 32 +++-------------- .../Tests/AppPresentationTests.swift | 1 - .../Tests/LegacyImportTests.swift | 1 - ...imeControllerTests+ResyncAndSettings.swift | 6 ++-- .../Tests/ProxySettingsFileStoreTests.swift | 11 +++--- .../Tests/ProxySettingsTests.swift | 4 --- .../Tests/RuntimeDocumentTests.swift | 17 +++++----- ...orkflowInterfaceTests+CommitFailures.swift | 13 ++++--- .../SettingsWorkflowInterfaceTests.swift | 34 +++++++------------ .../Tests/SettingsWorkflowTests.swift | 17 +++++----- ShadowsocksX-NG2/Tests/SystemProxyTests.swift | 4 +-- docs/adr/0014-delegate-runtime-defaults.md | 3 ++ 23 files changed, 65 insertions(+), 158 deletions(-) create mode 100644 docs/adr/0014-delegate-runtime-defaults.md diff --git a/CONTEXT.md b/CONTEXT.md index af5668dd..57bfe983 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -28,12 +28,12 @@ - **Proxy runtime**: The per-user background host process, independent of the GUI's lifetime, that runs the external tunnel service and loads the optional generated ACL sidecar. - **Legacy configuration**: The server list and preferences persisted by the frozen implementation in `Legacy/`. - **Legacy import**: The user-confirmed, read-only translation of a Legacy configuration's server records into a separate manual group in 2.0. It migrates no preferences, activates no target, and neither starts proxying nor alters the Legacy source. -- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target; it is not the user-managed server configuration or subscription document. +- **Runtime configuration file**: The derived JSON document used by the external tunnel service for the active target. Fields NG2 omits are left to the bundled `sslocal` defaults; the file is not the user-managed server configuration or subscription document. - **Sensitive information**: Server passwords, plugin options (sensitive in their entirety), and nonempty user-provided remote URLs, including subscription URLs. - **Credential exposure boundary**: This product reduces accidental disclosure and exposure to other user accounts, but does not guarantee protection against a compromised same-user process, root access, or APFS snapshots and backups; it does not promise secure erasure. - **Credential reference**: A non-secret association from a server configuration, subscription, or remote-URL setting to its durable credential; the credential value is kept outside the configuration tree and resolved only when needed. - **Diagnostic report**: A user-requested, redacted document containing only approved runtime state, endpoint metadata, aggregate catalog facts, managed-plugin presence, file metadata, and safe GUI event lines. It never contains passwords, plugin options, Keychain values, full remote URLs or tokens, runtime JSON, server addresses, remarks, or raw wrapper log text. -- **Diagnostic log view**: The explicit view/copy surface for GUI event lines and the raw wrapper `agent.log` tail. Raw wrapper log text is not part of a diagnostic report. +- **Diagnostic log view**: The explicit view/copy surface for GUI event lines and the raw wrapper `agent.log` tail; child-process verbosity follows `sslocal` defaults unless an external `RUST_LOG` is inherited. Raw wrapper log text is not part of a diagnostic report. - **Diagnostic facts**: Aggregate, redacted facts about the catalog and proxy runtime that can enter a diagnostic projection without exposing catalog entries, server addresses, remarks, credential references, URLs, paths, or other sensitive information. - **System-level shortcut**: A user-configurable keyboard action registered by the app that can trigger an app action even when the app has no focus. _Avoid_: global hotkey, global shortcut. diff --git a/ShadowsocksX-NG2/Agent/AgentContract.swift b/ShadowsocksX-NG2/Agent/AgentContract.swift index bb6117e3..4376afb9 100644 --- a/ShadowsocksX-NG2/Agent/AgentContract.swift +++ b/ShadowsocksX-NG2/Agent/AgentContract.swift @@ -48,11 +48,7 @@ func spawnSslocal(_ document: SslocalRuntimeDocument) -> Process? { child.executableURL = sslocalURL child.arguments = ["-c", contractURL.path] var childEnvironment = environment - // 上游默认日志级别会把服务器地址写进普通日志(D5):常规模式压到 warn, - // 用户明确打开 verbose 后才放宽到 debug。显式外部 RUST_LOG 仍可用于诊断。 - if childEnvironment["RUST_LOG"] == nil { - childEnvironment["RUST_LOG"] = document.listen.verbose ? "debug" : "warn" - } + // 不配置 sslocal 日志级别;未设置时使用上游默认值,显式外部 RUST_LOG 原样透传。 // macOS 的 resolv.conf 可能含带 zone id 的链路本地 nameserver(RA 下发的 // RDNSS,如 fe80::…%en0),hickory 解析不了该后缀(hickory-dns#3713), // 每次启动都报错再回退 builtin。强制 builtin getaddrinfo 即回退后的实际 diff --git a/ShadowsocksX-NG2/App/ErrorPresentation.swift b/ShadowsocksX-NG2/App/ErrorPresentation.swift index 958ff948..b31693e8 100644 --- a/ShadowsocksX-NG2/App/ErrorPresentation.swift +++ b/ShadowsocksX-NG2/App/ErrorPresentation.swift @@ -128,7 +128,7 @@ extension AppPresentation { static func message(for issue: SettingsFieldIssue) -> String { switch issue { - case .port(_, let error), .advertisedAddress(let error), .timeoutSeconds(let error): + case .port(_, let error), .advertisedAddress(let error): return message(for: error) } } @@ -275,8 +275,6 @@ extension AppPresentation { return Self.port(.portOutOfRange(endpoint: endpoint, port: port)) case .duplicatePort(let endpoint, let otherEndpoint, let port): return Self.port(.duplicatePort(endpoint: endpoint, otherEndpoint: otherEndpoint, port: port)) - case .invalidTimeout(let seconds): - return "超时 \(seconds) 秒无效,必须是 1–86400 之间的整数" case .invalidHostAddress: return "主机地址无效,必须是非回环 IPv4 地址" } } diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift index 1b0cfba9..b491347c 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift @@ -144,9 +144,7 @@ extension ProxyRuntimeController { func deployListeningWithoutTarget() async { let document = SslocalRuntimeDocument( servers: [], - listen: settings.listen, - timeout: settings.timeoutSeconds, - verbose: settings.verboseLogging) + listen: settings.listen) await deploy(document) } diff --git a/ShadowsocksX-NG2/App/SettingsView.swift b/ShadowsocksX-NG2/App/SettingsView.swift index fa4dcbc3..9349112b 100644 --- a/ShadowsocksX-NG2/App/SettingsView.swift +++ b/ShadowsocksX-NG2/App/SettingsView.swift @@ -72,18 +72,6 @@ struct SettingsView: View { } } .padding(.vertical, 4) - settingRow("超时", note: "连接超时秒数(1–86400)") { - Stepper(value: $workflow.draft.timeoutSeconds, in: 1...86_400) { - Text("\(workflow.draft.timeoutSeconds) 秒") - .monospacedDigit() - } - .frame(maxWidth: 160, alignment: .trailing) - } - issuesRow(.timeoutSeconds) - Toggle(isOn: $workflow.draft.verboseLogging) { - settingCopy("详细日志(verbose)", note: "仅在需要排障时临时启用") - } - .toggleStyle(.switch) } header: { sectionHeader("常规", subtitle: "应用启动与常用代理行为") } diff --git a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsDraftAdapter.swift b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsDraftAdapter.swift index c7d7dfe7..fd7f9488 100644 --- a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsDraftAdapter.swift +++ b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsDraftAdapter.swift @@ -38,8 +38,6 @@ enum SettingsDraftAdapter { advertisedAddress: advertisedAddress, socksPort: settings.listen.socksPort, httpPort: settings.listen.httpPort, - timeoutSeconds: settings.timeoutSeconds, - verboseLogging: settings.verboseLogging, proxyExceptions: settings.proxyExceptions) } @@ -54,8 +52,6 @@ enum SettingsDraftAdapter { listen.httpPort = draft.httpPort return ProxySettings( listen: listen, - timeoutSeconds: draft.timeoutSeconds, - verboseLogging: draft.verboseLogging, proxyExceptions: draft.proxyExceptions, preferredMode: base.preferredMode) } @@ -91,8 +87,6 @@ enum SettingsDraftAdapter { .port(portID(for: endpoint), error: error), .port(portID(for: otherEndpoint), error: error), ] - case .invalidTimeout: - return [.timeoutSeconds(error: error)] case .invalidHostAddress: return [.advertisedAddress(error: error)] } diff --git a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflowModels.swift b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflowModels.swift index 1da4b58e..2d44fa68 100644 --- a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflowModels.swift +++ b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflowModels.swift @@ -30,15 +30,13 @@ struct SettingsPortDraft: Equatable, Sendable { } /// 平坦的 UI 形状编辑草稿:字段全部是 UI 原语。监听范围拆成布尔开关与公布 -/// 地址文本两个字段,端口与超时保持整数;持久化的当前模式不在草稿中。 +/// 地址文本两个字段;持久化的当前模式不在草稿中。 /// 与 Domain 快照的互转只在 `SettingsDraftAdapter`。 struct SettingsDraft: Equatable, Sendable { var isHostScope: Bool var advertisedAddress: String var socksPort: Int var httpPort: Int - var timeoutSeconds: Int - var verboseLogging: Bool var proxyExceptions: String func portValue(for id: SettingsPortID) -> Int { @@ -62,7 +60,6 @@ struct SettingsDraft: Equatable, Sendable { enum SettingsFieldID: Hashable, Sendable { case advertisedAddress case port(SettingsPortID) - case timeoutSeconds } /// 按字段归位的校验问题:每条保留 Domain 的 typed fact;文案由 App @@ -70,19 +67,17 @@ enum SettingsFieldID: Hashable, Sendable { enum SettingsFieldIssue: Hashable, Sendable { case port(SettingsPortID, error: ProxySettingsValidationError) case advertisedAddress(error: ProxySettingsValidationError) - case timeoutSeconds(error: ProxySettingsValidationError) var field: SettingsFieldID { switch self { case .port(let id, _): .port(id) case .advertisedAddress: .advertisedAddress - case .timeoutSeconds: .timeoutSeconds } } var error: ProxySettingsValidationError { switch self { - case .port(_, let error), .advertisedAddress(let error), .timeoutSeconds(let error): + case .port(_, let error), .advertisedAddress(let error): error } } diff --git a/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift b/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift index 44ffb14e..dbe00bcb 100644 --- a/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift +++ b/ShadowsocksX-NG2/Domain/ActivationStateMachine.swift @@ -9,15 +9,12 @@ enum ActivationEffect: Equatable, Sendable { /// 派生运行时文档的监听与行为设置;聚合状态机接口的传参。 struct RuntimeDocumentOptions: Equatable, Sendable { var listen: SslocalListenSettings - var timeout: Int = 60 - var verbose: Bool = false } extension ProxySettings { - /// 激活派生所用选项即用户设置中的运行时文档旋钮。 + /// 激活派生所用的监听配置。 var runtimeDocumentOptions: RuntimeDocumentOptions { - RuntimeDocumentOptions( - listen: listen, timeout: timeoutSeconds, verbose: verboseLogging) + RuntimeDocumentOptions(listen: listen) } } @@ -123,9 +120,7 @@ struct ActivationStateMachine: Equatable, Sendable { targetID: target, document: SslocalRuntimeDocument( servers: servers, - listen: options.listen, - timeout: options.timeout, - verbose: options.verbose), + listen: options.listen), skippedServers: skippedServers)) } diff --git a/ShadowsocksX-NG2/Domain/ProxySettings.swift b/ShadowsocksX-NG2/Domain/ProxySettings.swift index 0b6d0266..41e7a040 100644 --- a/ShadowsocksX-NG2/Domain/ProxySettings.swift +++ b/ShadowsocksX-NG2/Domain/ProxySettings.swift @@ -8,8 +8,6 @@ struct ProxySettings: Equatable, Sendable { "127.0.0.1, localhost, 192.168.0.0/16, 10.0.0/8, FE80::/64, ::1, FD00::/8" var listen: SslocalListenSettings - var timeoutSeconds: Int - var verboseLogging: Bool var proxyExceptions: String /// The persisted current mode: the mode selector's choice survives GUI /// restarts. First run defaults to rule mode (issue #67). @@ -25,8 +23,6 @@ struct ProxySettings: Equatable, Sendable { init( listen: SslocalListenSettings = SslocalListenSettings(), - timeoutSeconds: Int = 60, - verboseLogging: Bool = false, proxyExceptions: String = ProxySettings.defaultProxyExceptions, preferredMode: ProxyModeKind = .rule, ruleDefaultAction: RuleDefaultAction = .proxyWhenUnmatched, @@ -34,8 +30,6 @@ struct ProxySettings: Equatable, Sendable { systemProxyEnabled: Bool = false ) { self.listen = listen - self.timeoutSeconds = timeoutSeconds - self.verboseLogging = verboseLogging self.proxyExceptions = proxyExceptions self.preferredMode = preferredMode self.ruleDefaultAction = ruleDefaultAction @@ -60,9 +54,6 @@ struct ProxySettings: Equatable, Sendable { var validationErrors: [ProxySettingsValidationError] { var errors = listen.portValidationErrors().map(ProxySettingsValidationError.init) - if !(1...86_400).contains(timeoutSeconds) { - errors.append(.invalidTimeout(timeoutSeconds)) - } if case .host(let address) = listen.scope, !Self.isUsableHostAddress(address) { errors.append(.invalidHostAddress(address)) } @@ -80,7 +71,6 @@ struct ProxySettings: Equatable, Sendable { enum ProxySettingsValidationError: Error, Hashable, Sendable { case portOutOfRange(endpoint: ProxyEndpointKind, port: Int) case duplicatePort(endpoint: ProxyEndpointKind, otherEndpoint: ProxyEndpointKind, port: Int) - case invalidTimeout(Int) case invalidHostAddress(String) init(_ error: PortSettingError) { diff --git a/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift b/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift index 368f2946..948aaa89 100644 --- a/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift +++ b/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift @@ -101,8 +101,6 @@ struct ProxySettingsFileStore: ProxySettingsStoring { return try validated( ProxySettings( listen: listen, - timeoutSeconds: record.timeoutSeconds, - verboseLogging: record.verboseLogging, proxyExceptions: record.proxyExceptions, preferredMode: record.preferredMode, ruleDefaultAction: record.ruleDefaultAction, @@ -119,8 +117,6 @@ struct ProxySettingsFileStore: ProxySettingsStoring { }() record.socksPort = settings.listen.socksPort record.httpPort = settings.listen.httpPort - record.timeoutSeconds = settings.timeoutSeconds - record.verboseLogging = settings.verboseLogging record.proxyExceptions = settings.proxyExceptions record.preferredMode = settings.preferredMode record.ruleDefaultAction = settings.ruleDefaultAction @@ -141,8 +137,6 @@ private struct ProxySettingsRecord: Codable, Equatable, Sendable { var advertisedAddress: String? var socksPort: Int = SslocalListenSettings.defaultSocksPort var httpPort: Int = SslocalListenSettings.defaultHTTPPort - var timeoutSeconds: Int = 60 - var verboseLogging: Bool = false var proxyExceptions: String = ProxySettings.defaultProxyExceptions var preferredMode: ProxyModeKind = .rule var ruleDefaultAction: RuleDefaultAction = .proxyWhenUnmatched @@ -161,8 +155,6 @@ private struct ProxySettingsRecord: Codable, Equatable, Sendable { httpPort = try container.decodeIfPresent(Int.self, forKey: .httpPort) ?? SslocalListenSettings.defaultHTTPPort - timeoutSeconds = try container.decodeIfPresent(Int.self, forKey: .timeoutSeconds) ?? 60 - verboseLogging = try container.decodeIfPresent(Bool.self, forKey: .verboseLogging) ?? false proxyExceptions = try container.decodeIfPresent(String.self, forKey: .proxyExceptions) ?? ProxySettings.defaultProxyExceptions @@ -189,7 +181,7 @@ private struct ProxySettingsRecord: Codable, Equatable, Sendable { extension ProxySettingsRecord { fileprivate enum CodingKeys: String, CodingKey { case scopeKind, advertisedAddress, socksPort, httpPort - case timeoutSeconds, verboseLogging, proxyExceptions + case proxyExceptions case preferredMode case ruleDefaultAction case agentEnabled, systemProxyEnabled diff --git a/ShadowsocksX-NG2/Domain/SslocalListenSettings.swift b/ShadowsocksX-NG2/Domain/SslocalListenSettings.swift index 9e36b67e..c043a393 100644 --- a/ShadowsocksX-NG2/Domain/SslocalListenSettings.swift +++ b/ShadowsocksX-NG2/Domain/SslocalListenSettings.swift @@ -82,10 +82,8 @@ struct RuntimeListenFacts: Equatable, Sendable { } /// 监听指纹(spec #21 D5/D7):服务器列表变化可热重载;任一本地入站、 -/// ACL 内容/路径/摘要或 verbose(决定 spawn 时的 RUST_LOG)变化都必须由 -/// wrapper 完整重启 sslocal。 +/// ACL 内容/路径/摘要变化都必须由 wrapper 完整重启 sslocal。 struct SslocalListenFingerprint: Equatable, Sendable { let locals: [SslocalLocalDocument] let acl: ProxyACLDocument? - let verbose: Bool } diff --git a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift index e94fda1a..f38c5c2d 100644 --- a/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift +++ b/ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift @@ -52,30 +52,16 @@ struct SslocalLocalDocument: Codable, Equatable, Sendable { /// wrapper 自有的监听身份扩展(issue #67 取代已删除的 PAC 扩展)。字段收在 /// `x_shadowsocksx_ng_listen` 下,上游 sslocal 会忽略该扩展;wrapper 与 GUI -/// 仍从同一原子文件读取同一份事实。verbose 决定 sslocal 的 RUST_LOG 级别。 +/// 仍从同一原子文件读取同一份事实。 struct RuntimeListenDocument: Codable, Equatable, Sendable { let listenScope: ListenScopeKind let bindAddress: String let advertisedAddress: String - let verbose: Bool enum CodingKeys: String, CodingKey { case listenScope = "listen_scope" case bindAddress = "bind_address" case advertisedAddress = "advertised_address" - case verbose - } - - init( - listenScope: ListenScopeKind, - bindAddress: String, - advertisedAddress: String, - verbose: Bool = false - ) { - self.listenScope = listenScope - self.bindAddress = bindAddress - self.advertisedAddress = advertisedAddress - self.verbose = verbose } } @@ -85,14 +71,13 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { let servers: [SslocalServerDocument] let locals: [SslocalLocalDocument] let listen: RuntimeListenDocument - let timeout: Int /// Upstream sslocal ACL file path. The wrapper-owned extension carries the /// matching content and digest so both inbounds use the same validated file. let aclFilePath: String? let aclRuntime: ProxyACLDocument? enum CodingKeys: String, CodingKey { - case servers, locals, timeout + case servers, locals case aclFilePath = "acl" case aclRuntime = "x_shadowsocksx_ng_acl" case listen = "x_shadowsocksx_ng_listen" @@ -101,8 +86,6 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { init( servers: [SslocalServerDocument], listen: SslocalListenSettings, - timeout: Int = 60, - verbose: Bool = false, acl: ProxyACLDocument? = nil ) { self.servers = servers @@ -110,9 +93,7 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { self.listen = RuntimeListenDocument( listenScope: listen.scope.kind, bindAddress: listen.bindAddress, - advertisedAddress: listen.advertisedAddress, - verbose: verbose) - self.timeout = timeout + advertisedAddress: listen.advertisedAddress) aclFilePath = acl?.path aclRuntime = acl } @@ -121,13 +102,11 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { servers: [SslocalServerDocument], locals: [SslocalLocalDocument], listen: RuntimeListenDocument, - timeout: Int, acl: ProxyACLDocument? ) { self.servers = servers self.locals = locals self.listen = listen - self.timeout = timeout aclFilePath = acl?.path aclRuntime = acl } @@ -137,7 +116,6 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { servers = try container.decode([SslocalServerDocument].self, forKey: .servers) locals = try container.decode([SslocalLocalDocument].self, forKey: .locals) listen = try container.decode(RuntimeListenDocument.self, forKey: .listen) - timeout = try container.decodeIfPresent(Int.self, forKey: .timeout) ?? 60 aclFilePath = try container.decodeIfPresent(String.self, forKey: .aclFilePath) aclRuntime = try container.decodeIfPresent(ProxyACLDocument.self, forKey: .aclRuntime) } @@ -156,7 +134,6 @@ struct SslocalRuntimeDocument: Codable, Equatable, Sendable { servers: servers, locals: locals, listen: listen, - timeout: timeout, acl: acl) } @@ -205,7 +182,7 @@ extension SslocalRuntimeDocument { } var listenFingerprint: SslocalListenFingerprint { - SslocalListenFingerprint(locals: locals, acl: aclRuntime, verbose: listen.verbose) + SslocalListenFingerprint(locals: locals, acl: aclRuntime) } var socksLocal: SslocalLocalDocument? { @@ -227,7 +204,6 @@ extension SslocalRuntimeDocument { /// 接受空 servers 并照常绑定本地入站。 var isWellFormed: Bool { guard - (1...86_400).contains(timeout), !listen.advertisedAddress.isEmpty else { return false } diff --git a/ShadowsocksX-NG2/Tests/AppPresentationTests.swift b/ShadowsocksX-NG2/Tests/AppPresentationTests.swift index ee39706f..6d5f51b6 100644 --- a/ShadowsocksX-NG2/Tests/AppPresentationTests.swift +++ b/ShadowsocksX-NG2/Tests/AppPresentationTests.swift @@ -101,7 +101,6 @@ final class AppPresentationTests: XCTestCase { let issues: [SettingsFieldIssue] = [ .port(.socks, error: .portOutOfRange(endpoint: .socks, port: 0)), .advertisedAddress(error: .invalidHostAddress("127.0.0.1")), - .timeoutSeconds(error: .invalidTimeout(0)), ] for issue in issues { XCTAssertFalse(AppPresentation.message(for: issue).isEmpty) diff --git a/ShadowsocksX-NG2/Tests/LegacyImportTests.swift b/ShadowsocksX-NG2/Tests/LegacyImportTests.swift index 737bd468..cd77b9b7 100644 --- a/ShadowsocksX-NG2/Tests/LegacyImportTests.swift +++ b/ShadowsocksX-NG2/Tests/LegacyImportTests.swift @@ -72,7 +72,6 @@ final class LegacyImportTests: XCTestCase { var existingSettings = ProxySettings() existingSettings.listen.socksPort = 2086 existingSettings.listen.httpPort = 2087 - existingSettings.timeoutSeconds = 120 existingSettings.preferredMode = .global let settingsStore = ProxySettingsFileStore( fileURL: settingsURL, diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift index eca413ac..1ce4c759 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+ResyncAndSettings.swift @@ -78,16 +78,14 @@ extension ProxyRuntimeControllerTests { await controller.setAgentEnabled(true) var next = controller.settings - next.timeoutSeconds = 120 - next.verboseLogging = true + next.listen.httpPort = 2088 next.proxyExceptions = "localhost, 127.0.0.1" try await controller.updateSettings(next) let document = try XCTUnwrap(RuntimeFileStore(fileURL: runtime.contract).loadDocument()) XCTAssertEqual(controller.settings, next) XCTAssertEqual(settingsStore.saved, next) - XCTAssertEqual(document.timeout, 120) - XCTAssertTrue(document.listen.verbose) + XCTAssertEqual(document.httpPort, 2088) XCTAssertEqual( systemProxy.applied.last?.exceptions, FixedLocalProxyRanges.systemProxyExceptions(including: ["localhost", "127.0.0.1"]), diff --git a/ShadowsocksX-NG2/Tests/ProxySettingsFileStoreTests.swift b/ShadowsocksX-NG2/Tests/ProxySettingsFileStoreTests.swift index 0b35dcef..05a4e34d 100644 --- a/ShadowsocksX-NG2/Tests/ProxySettingsFileStoreTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxySettingsFileStoreTests.swift @@ -42,8 +42,6 @@ final class ProxySettingsFileStoreTests: XCTestCase { settings.listen.scope = .host(advertisedAddress: "192.168.2.89") settings.listen.socksPort = 2086 settings.listen.httpPort = 2087 - settings.timeoutSeconds = 120 - settings.verboseLogging = true settings.proxyExceptions = "localhost, 127.0.0.1" settings.preferredMode = .direct @@ -52,6 +50,8 @@ final class ProxySettingsFileStoreTests: XCTestCase { XCTAssertEqual(try store.load(), settings) let raw = try String(contentsOf: store.fileURL, encoding: .utf8) XCTAssertFalse(raw.contains("udpRelayEnabled")) + XCTAssertFalse(raw.contains("timeoutSeconds")) + XCTAssertFalse(raw.contains("verboseLogging")) XCTAssertEqual(try store.load().listen.mode, "tcp_and_udp") } @@ -86,6 +86,7 @@ final class ProxySettingsFileStoreTests: XCTestCase { try writeRaw( """ {"preferredMode":"pac","socksPort":2086,"httpPort":2087,"timeoutSeconds":120, + "verboseLogging":true, "pacUserRules":"@@example.com","gfwListURLConfigured":true,"pacPort":2089} """) @@ -93,18 +94,17 @@ final class ProxySettingsFileStoreTests: XCTestCase { XCTAssertEqual(loaded.preferredMode, .rule) XCTAssertEqual(loaded.listen.socksPort, 2086) XCTAssertEqual(loaded.listen.httpPort, 2087) - XCTAssertEqual(loaded.timeoutSeconds, 120) } func testInvalidSaveDoesNotTouchExistingSettings() throws { try store.save(ProxySettings()) var invalid = ProxySettings() - invalid.timeoutSeconds = 0 + invalid.listen.socksPort = 0 XCTAssertThrowsError(try store.save(invalid)) { error in XCTAssertEqual( error as? ProxySettingsStoreError, - .invalid([.invalidTimeout(0)])) + .invalid([.portOutOfRange(endpoint: .socks, port: 0)])) } XCTAssertEqual(try store.load(), ProxySettings()) } @@ -127,6 +127,5 @@ final class ProxySettingsFileStoreTests: XCTestCase { let loaded = try store.load() XCTAssertEqual(loaded.listen, legacy) - XCTAssertEqual(loaded.timeoutSeconds, 60) } } diff --git a/ShadowsocksX-NG2/Tests/ProxySettingsTests.swift b/ShadowsocksX-NG2/Tests/ProxySettingsTests.swift index cf24bb59..98b56810 100644 --- a/ShadowsocksX-NG2/Tests/ProxySettingsTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxySettingsTests.swift @@ -10,8 +10,6 @@ final class ProxySettingsTests: XCTestCase { XCTAssertEqual(settings.listen.socksPort, 11086) XCTAssertEqual(settings.listen.httpPort, 11087) - XCTAssertEqual(settings.timeoutSeconds, 60) - XCTAssertFalse(settings.verboseLogging) XCTAssertEqual(settings.listen.mode, "tcp_and_udp") XCTAssertFalse(settings.agentEnabled, "全新用户无服务器配置,Agent 默认 off") XCTAssertFalse(settings.systemProxyEnabled) @@ -27,7 +25,6 @@ final class ProxySettingsTests: XCTestCase { func testInvalidAdvancedSettingsNameEveryProblem() { var settings = ProxySettings() - settings.timeoutSeconds = 0 settings.listen.scope = .host(advertisedAddress: "127.0.0.1") settings.listen.socksPort = settings.listen.httpPort @@ -35,7 +32,6 @@ final class ProxySettingsTests: XCTestCase { settings.validationErrors, [ .duplicatePort(endpoint: .socks, otherEndpoint: .http, port: 11087), - .invalidTimeout(0), .invalidHostAddress("127.0.0.1"), ]) } diff --git a/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift b/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift index 4d246066..215eaac9 100644 --- a/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift +++ b/ShadowsocksX-NG2/Tests/RuntimeDocumentTests.swift @@ -49,7 +49,8 @@ final class RuntimeDocumentTests: XCTestCase { XCTAssertEqual(listen["listen_scope"] as? String, "loopback") XCTAssertEqual(listen["bind_address"] as? String, "127.0.0.1") XCTAssertEqual(listen["advertised_address"] as? String, "127.0.0.1") - XCTAssertEqual(listen["verbose"] as? Bool, false) + XCTAssertFalse(listen.keys.contains("verbose")) + XCTAssertFalse(dictionary.keys.contains("timeout")) let servers = try XCTUnwrap(dictionary["servers"] as? [[String: Any]]) XCTAssertEqual(servers.count, 1) let server = servers[0] @@ -86,21 +87,19 @@ final class RuntimeDocumentTests: XCTestCase { XCTAssertEqual(listen.locals.map(\.mode), ["tcp_and_udp", "tcp_only"]) } - func testRuntimeDocumentCarriesTimeoutAndVerbose() throws { + func testRuntimeDocumentLeavesTimeoutAndLoggingAtUpstreamDefaults() throws { let document = SslocalRuntimeDocument( servers: [ SslocalServerDocument( id: "server-1", remarks: "test", server: "example.com", serverPort: 8388, password: "pw", method: "aes-256-gcm", plugin: nil, pluginOpts: nil) ], - listen: SslocalListenSettings(), - timeout: 120, - verbose: true) + listen: SslocalListenSettings()) - let decoded = try XCTUnwrap(SslocalRuntimeDocument.decodeValidated(try document.jsonData())) - - XCTAssertEqual(decoded.timeout, 120) - XCTAssertTrue(decoded.listen.verbose) + let dictionary = try encodedDictionary(document) + let listen = try XCTUnwrap(dictionary["x_shadowsocksx_ng_listen"] as? [String: Any]) + XCTAssertFalse(dictionary.keys.contains("timeout")) + XCTAssertFalse(listen.keys.contains("verbose")) } func testACLPathAndMetadataRoundTripAsUpstreamAndWrapperFields() throws { diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests+CommitFailures.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests+CommitFailures.swift index 68d3c6a6..0232aaa0 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests+CommitFailures.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests+CommitFailures.swift @@ -8,7 +8,7 @@ extension SettingsWorkflowInterfaceTests { func testCommitFailureSurfacesNamedReasonAndKeepsDraft() async throws { committing.updateError = FakeCommitError.io let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" _ = await workflow.save() await waitUntil(workflow.lastFailure != nil) @@ -16,14 +16,17 @@ extension SettingsWorkflowInterfaceTests { XCTAssertEqual(workflow.lastFailure, .unknown) XCTAssertEqual(workflow.lastFailure?.presentableMessage, AppPresentation.unknownError) XCTAssertFalse(workflow.isCommitting) - XCTAssertEqual(workflow.draft.timeoutSeconds, 120, "失败不吞掉草稿") - XCTAssertEqual(committing.committedSettings.timeoutSeconds, 60, "失败不半提交") + XCTAssertEqual(workflow.draft.proxyExceptions, "localhost", "失败不吞掉草稿") + XCTAssertEqual( + committing.committedSettings.proxyExceptions, + ProxySettings.defaultProxyExceptions, + "失败不半提交") XCTAssertTrue(workflow.isDirty) } func testStorageAndCredentialFailuresNameTheReason() async throws { let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" committing.updateError = ProxySettingsStoreError.ioFailure(detail: "disk full") _ = await workflow.save() @@ -47,7 +50,7 @@ extension SettingsWorkflowInterfaceTests { func testCommittingStateBlocksRepeatedSave() async throws { committing.updateGate = AsyncGate() let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" let first = Task { await workflow.save() } await Task.yield() diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift index 08facfd5..a2001de5 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift @@ -52,16 +52,6 @@ final class SettingsWorkflowInterfaceTests: XCTestCase { XCTAssertEqual(workflow.issues(for: .port(.http)), [.port(.http, error: error)]) } - func testTimeoutIssueLandsOnTimeoutField() { - let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 0 - - XCTAssertEqual( - workflow.fieldIssues, - [.timeoutSeconds(error: .invalidTimeout(0))]) - XCTAssertTrue(workflow.issues(for: .port(.socks)).isEmpty) - } - func testHostAddressIssueLandsOnAdvertisedAddressField() { let workflow = makeWorkflow() workflow.draft.isHostScope = true @@ -196,7 +186,7 @@ extension SettingsWorkflowInterfaceTests { func testValidationIssuesBlockSave() async throws { let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 0 + workflow.draft.socksPort = 0 XCTAssertFalse(workflow.canSave) @@ -220,24 +210,24 @@ extension SettingsWorkflowInterfaceTests { func testSaveCommitsWhenGateClear() async throws { let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" XCTAssertTrue(workflow.canSave) let outcome = await workflow.save() XCTAssertEqual(outcome, .persisted(runtime: .notRunning)) - XCTAssertEqual(committing.updateCalls.first?.timeoutSeconds, 120) + XCTAssertEqual(committing.updateCalls.first?.proxyExceptions, "localhost") } func testSaveReturnsAValidationRejectionWithoutStartingACommit() async throws { let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 0 + workflow.draft.socksPort = 0 let outcome = await workflow.save() XCTAssertEqual( outcome, - .rejected(.validation([.timeoutSeconds(error: .invalidTimeout(0))]))) + .rejected(.validation([.port(.socks, error: .portOutOfRange(endpoint: .socks, port: 0))]))) XCTAssertFalse(workflow.isCommitting) XCTAssertTrue(committing.updateCalls.isEmpty) } @@ -246,19 +236,19 @@ extension SettingsWorkflowInterfaceTests { let runtimeFailure = RuntimeFailureFacts.service(.persistence) committing.updateOutcome = .failed(runtimeFailure) let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" let outcome = await workflow.save() XCTAssertEqual(outcome, .persisted(runtime: .failed(runtimeFailure))) XCTAssertEqual(workflow.lastFailure, .runtime(runtimeFailure)) - XCTAssertEqual(committing.committedSettings.timeoutSeconds, 120) + XCTAssertEqual(committing.committedSettings.proxyExceptions, "localhost") } func testRepeatedSaveReturnsAnInProgressRejection() async throws { committing.updateGate = AsyncGate() let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" let first = Task { await workflow.save() } await Task.yield() @@ -278,17 +268,17 @@ extension SettingsWorkflowInterfaceTests { let workflow = makeWorkflow() XCTAssertFalse(workflow.isDirty) - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" XCTAssertTrue(workflow.isDirty) _ = await workflow.reloadFromCommitted() XCTAssertFalse(workflow.isDirty) - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" _ = await workflow.save() await waitUntil(!workflow.isCommitting) XCTAssertFalse(workflow.isDirty, "提交成功后草稿回到已提交快照") - XCTAssertEqual(workflow.draft.timeoutSeconds, 120) + XCTAssertEqual(workflow.draft.proxyExceptions, "localhost") } func testHiddenHostAddressLeftoverDoesNotReportDirty() { @@ -309,7 +299,7 @@ extension SettingsWorkflowInterfaceTests { committing.committedSettings = committed let workflow = makeWorkflow() - workflow.draft.timeoutSeconds = 120 + workflow.draft.proxyExceptions = "localhost" _ = await workflow.save() await waitUntil(!workflow.isCommitting) diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift index 6831e23e..1a2ea783 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift @@ -36,11 +36,11 @@ final class SettingsWorkflowTests: XCTestCase { func testSaveCommitsThroughTheRuntimeController() async throws { let pair = makePair() - pair.workflow.draft.timeoutSeconds = 120 + pair.workflow.draft.proxyExceptions = "localhost" _ = await pair.workflow.save() - XCTAssertEqual(settingsStore.saved?.timeoutSeconds, 120) - XCTAssertEqual(pair.controller.settings.timeoutSeconds, 120) + XCTAssertEqual(settingsStore.saved?.proxyExceptions, "localhost") + XCTAssertEqual(pair.controller.settings.proxyExceptions, "localhost") XCTAssertEqual( pair.workflow.draft, SettingsDraftAdapter.draft(from: pair.controller.settings), "提交后草稿回到已提交快照") @@ -61,23 +61,24 @@ final class SettingsWorkflowTests: XCTestCase { XCTAssertFalse(pair.workflow.hasBlockingPortOccupancy) XCTAssertTrue(pair.workflow.canSave) - pair.workflow.draft.timeoutSeconds = 120 + pair.workflow.draft.proxyExceptions = "localhost" _ = await pair.workflow.save() - XCTAssertEqual(settingsStore.saved?.timeoutSeconds, 120) + XCTAssertEqual(settingsStore.saved?.proxyExceptions, "localhost") } func testCommitFailureSurfacesPresentedReasonAndKeepsCommittedValues() async throws { settingsStore.saveError = FakeSaveError.io let pair = makePair() - pair.workflow.draft.timeoutSeconds = 120 + pair.workflow.draft.proxyExceptions = "localhost" _ = await pair.workflow.save() XCTAssertEqual(pair.workflow.lastFailure, .unknown) XCTAssertEqual(pair.workflow.lastFailure?.presentableMessage, AppPresentation.unknownError) XCTAssertNil(settingsStore.saved) - XCTAssertEqual(pair.controller.settings.timeoutSeconds, 60, "失败保留旧值") - XCTAssertEqual(pair.workflow.draft.timeoutSeconds, 120, "草稿保留待修改值") + XCTAssertEqual( + pair.controller.settings.proxyExceptions, ProxySettings.defaultProxyExceptions, "失败保留旧值") + XCTAssertEqual(pair.workflow.draft.proxyExceptions, "localhost", "草稿保留待修改值") } // MARK: - 夹具 diff --git a/ShadowsocksX-NG2/Tests/SystemProxyTests.swift b/ShadowsocksX-NG2/Tests/SystemProxyTests.swift index 5542e26e..4aa04c0a 100644 --- a/ShadowsocksX-NG2/Tests/SystemProxyTests.swift +++ b/ShadowsocksX-NG2/Tests/SystemProxyTests.swift @@ -27,9 +27,9 @@ final class SystemProxyTests: XCTestCase { // 无效端口 0,而应按模式错误拒绝。 let json = """ {"servers":[],"locals":[{"protocol":"socks","local_address":"127.0.0.1",\ - "local_port":11086,"mode":"tcp_and_udp"}],"timeout":60,\ + "local_port":11086,"mode":"tcp_and_udp"}],\ "x_shadowsocksx_ng_listen":{"listen_scope":"loopback",\ - "bind_address":"127.0.0.1","advertised_address":"127.0.0.1","verbose":false}} + "bind_address":"127.0.0.1","advertised_address":"127.0.0.1"}} """ let document = try XCTUnwrap(SslocalRuntimeDocument.decodeValidated(Data(json.utf8))) diff --git a/docs/adr/0014-delegate-runtime-defaults.md b/docs/adr/0014-delegate-runtime-defaults.md new file mode 100644 index 00000000..fd0e3c2d --- /dev/null +++ b/docs/adr/0014-delegate-runtime-defaults.md @@ -0,0 +1,3 @@ +# Delegate timeout and log level to sslocal defaults + +NG2 removes the user settings that overrode `sslocal`'s connection timeout and log level, omits those values from its runtime document, and no longer injects `RUST_LOG=warn` or `debug`; an explicitly inherited `RUST_LOG` remains effective. Stale `timeoutSeconds` and `verboseLogging` preference keys are ignored on load and omitted by later saves, with no migration. This keeps these controls with the bundled `sslocal` version while internal application timeouts and the diagnostic log view remain independent; upstream default logs may include server addresses in the raw `agent.log`.