Repository navigation
feat(network): 添加只读网络监视器与按需地图库 - #10
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 726e936eda
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| moduleEnabled = enabled | ||
| self.visible = visible | ||
| if !enabled && !records.isEmpty { records = [] } | ||
| if !enabled && (wantsRunning || status != .idle) { stop() } |
There was a problem hiding this comment.
Disable the persisted filter from an idle controller
When the filter was enabled in an earlier app session, it remains enabled in NEFilterManager after quitting, but a relaunched controller starts with status == .idle and wantsRunning == false. If the user then turns off the Network Monitor feature without starting it again, this condition skips stop(), so setFilterEnabled(false) is never called and the system extension continues receiving every socket flow despite the feature being disabled. Reconcile the persisted filter state on disable even when this controller has not entered the running state.
AGENTS.md reference: AGENTS.md:L20-L21
Useful? React with 👍 / 👎.
| candidate.exportedInterface = NSXPCInterface(with: NetworkObservationService.self) | ||
| // 连接建立不等于认证通过。只有 macOS 校验签名后分发的首次读取才能替换观察者, | ||
| // 未认证客户端不能通过反复连接来关闭现有观察或夺走其租约。 | ||
| candidate.exportedObject = ConnectionObservationSession { [weak self] candidate, firstRead, cursor, epoch in |
There was a problem hiding this comment.
Break the XPC session retain cycle
Each accepted connection strongly owns its exportedObject, whose readBatch closure in turn captures candidate strongly, creating candidate → session → closure → candidate. Neither the invalidation handler nor the replacement path clears exportedObject, so every visibility or pause transition that invalidates and recreates the connection can leave another connection/session cycle resident in the long-lived system extension. Capture the candidate weakly or explicitly sever the exported-object reference during invalidation.
AGENTS.md reference: AGENTS.md:L26-L26
Useful? React with 👍 / 👎.
| for (file, data) in zip(manifest.files, pair) { | ||
| try data.write(to: directory.appendingPathComponent(file.name), options: .atomic) | ||
| } | ||
| let record = CacheRecord(manifest: manifest, directory: directoryName, checkedAt: Date()) | ||
| // 两个文件均验证完成后才原子切换索引;更新失败不会把IPv4新库与IPv6旧库混用。 | ||
| let mapped = try manifest.files.map { try read($0, directory: directory) } | ||
| try JSONEncoder().encode(record).write(to: cacheRoot.appendingPathComponent("current.json"), options: .atomic) | ||
| install(record, pair: mapped) |
There was a problem hiding this comment.
Honor cancellation while committing geography data
If the viewer is hidden after the final cancellation check but while this commit block is running, cancelling the task cannot interrupt the synchronous writes, rereads, hashes, index write, or install. With the allowed asset sizes this can continue tens of MiB of disk I/O after demand ends, and the cancelled generation can still become the fresh cache that a subsequent reopen reuses. Add cancellation checks between the file loop, verification, pointer write, and in-memory installation.
AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.
| let known = Set(records.map(\.id)) | ||
| records = Array((records + updates.values.filter { !known.contains($0.id) }.sorted { $0.sequence < $1.sequence }).suffix(ObservationLimits.displayCount)) | ||
| } | ||
| if let activeIDs = batch.activeIDs { activeConnectionIDs = Set(activeIDs) } |
There was a problem hiding this comment.
Avoid invalidating the whole page on unchanged active IDs
Every two-second read includes activeIDs, so this unconditional assignment emits an Observation mutation even when the set is unchanged. While connections are idle, that needlessly rebuilds the groups, table, and the world-map canvas with roughly ten thousand path points on every poll. Compare the new set with activeConnectionIDs before assigning so an unchanged snapshot does not cause continuous redraw work.
AGENTS.md reference: AGENTS.md:L25-L25
Useful? React with 👍 / 👎.
| records = Array((records + updates.values.filter { !known.contains($0.id) }.sorted { $0.sequence < $1.sequence }).suffix(ObservationLimits.displayCount)) | ||
| } | ||
| if let activeIDs = batch.activeIDs { activeConnectionIDs = Set(activeIDs) } | ||
| if discarded != batch.discarded { discarded = batch.discarded } |
There was a problem hiding this comment.
Surface journal eviction to the viewer
When more than 256 open/close events accumulate before the reader catches up, the extension ring overwrites events and increments discarded, but no view reads this controller property. The connection and aggregate views therefore silently present an incomplete observation as if it were complete; even the newly added localized eviction warning is never used. Display the discarded count or an explicit truncation warning whenever it increases.
Useful? React with 👍 / 👎.
| <key>CFBundleDisplayName</key> | ||
| <string>XStats Network Extension</string> |
There was a problem hiding this comment.
Localize the system extension display name
CFBundleDisplayName is user-visible in system extension approval and System Settings, but it is fixed to English here and none of the newly added localized InfoPlist.strings files overrides it. Consequently every non-English locale displays “XStats Network Extension” even though the feature's other text is localized. Add a translated CFBundleDisplayName entry for every supported language.
AGENTS.md reference: AGENTS.md:L30-L30
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Filter cleanup, application-hide handling and cancellation-safe cache commits remain unresolved.
Review effort: Balanced
Findings: 3
Open (3)
What changed in this PR
Adds an optional, read-only Network Monitor for macOS 15+, while retaining macOS 14 support for existing features.
Changes:
- Adds bounded connection observation, grouping, search and country maps.
- Introduces verified, on-demand geography downloads and release-time data publishing.
- Updates signing, localization, privacy documentation and regression coverage.
| File | Description |
|---|---|
| web/privacy.html | Documents observation and download privacy. |
| ThirdPartyNotices.md | Attributes map and IP datasets. |
| Taskfile.yml | Adds extension profile and geography tests. |
| scripts/sync_network_geography.py | Prepares and publishes verified datasets. |
| scripts/sign_sparkle.sh | Updates nested and preview signing. |
| scripts/release.sh | Verifies extension architecture and signing. |
| scripts/publish_release.sh | Requires geography publication before app uploads. |
| scripts/network_geography_test.py | Tests conversion and publication boundaries. |
| scripts/compress_network_geography.swift | Compresses and verifies geography tables. |
| scripts/build_network_geography.py | Converts IP ranges and world outlines. |
| scripts/arm64_release_test.sh | Checks extension configuration and entitlements. |
| project.yml | Configures extension target and build 133. |
| Packages/XStatsKit/Tests/XStatsUITests/NetworkMonitorTests.swift | Tests monitor lifecycle and protocol handling. |
| Packages/XStatsKit/Tests/XStatsUITests/GeographyDownloadTests.swift | Tests downloads, caching and cancellation. |
| Packages/XStatsKit/Tests/NetworkObservationTests/ConnectionJournalTests.swift | Tests bounded journals and observation leases. |
| Packages/XStatsKit/Tests/LocalizationTests/LanguageSupportTests.swift | Checks monitor translations. |
| Packages/XStatsKit/Sources/XStatsUI/State/SettingsDocument.swift | Backs up the feature preference. |
| Packages/XStatsKit/Sources/XStatsUI/State/AppSettings.swift | Adds preference and availability gates. |
| Packages/XStatsKit/Sources/XStatsUI/State/AppModel.swift | Owns monitor and geography controllers. |
| Packages/XStatsKit/Sources/XStatsUI/Snapshot/SnapshotRenderer.swift | Adds synthetic monitor previews. |
| Packages/XStatsKit/Sources/XStatsUI/Settings/FeatureSettings.swift | Adds the feature switch. |
| Packages/XStatsKit/Sources/XStatsUI/Resources/NetworkGeography/ATTRIBUTION.md | Bundles dataset attribution. |
| Packages/XStatsKit/Sources/XStatsUI/Resources/Legal/Privacy.zh-Hans.md | Updates Chinese privacy disclosures. |
| Packages/XStatsKit/Sources/XStatsUI/Resources/Legal/Privacy.en.md | Updates English privacy disclosures. |
| Packages/XStatsKit/Sources/XStatsUI/Panel/MainWindowView.swift | Adds monitor page and sidebar entry. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/OfflineGeography.swift | Manages cached tables and offline lookups. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/NetworkMonitorSupport.swift | Gates support to macOS 15+. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/NetworkMonitorController.swift | Coordinates observation lifecycle and records. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/NetworkMonitorBackend.swift | Manages extension activation and authenticated communication. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/NetworkGeographyController.swift | Coordinates demand-driven geography updates. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/NetworkConnectionsPage.swift | Displays grouped connections and country maps. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/GeographyManifest.swift | Validates manifests and compressed assets. |
| Packages/XStatsKit/Sources/XStatsUI/NetworkObservation/GeographyDownload.swift | Implements bounded, cancellable downloads. |
| Packages/XStatsKit/Sources/XStatsUI/AppKit/AppDeepLink.swift | Adds gated connections routing. |
| Packages/XStatsKit/Sources/XStatsUI/AppKit/AppController.swift | Connects observation demand to app lifecycle. |
| Packages/XStatsKit/Sources/NetworkObservation/ObservedConnection.swift | Defines metadata and communication contracts. |
| Packages/XStatsKit/Sources/NetworkObservation/ObservationBuffer.swift | Tracks leases and active connections. |
| Packages/XStatsKit/Sources/NetworkObservation/ConnectionJournal.swift | Provides bounded incremental event storage. |
| Packages/XStatsKit/Sources/Localization/Translations.swift | Adds English monitor translations. |
| Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv | Adds Traditional Chinese translations. |
| Packages/XStatsKit/Sources/Localization/Resources/ko.tsv | Adds Korean translations. |
| Packages/XStatsKit/Sources/Localization/Resources/ja.tsv | Adds Japanese translations. |
| Packages/XStatsKit/Sources/Localization/Resources/fr.tsv | Adds French translations. |
| Packages/XStatsKit/Sources/Localization/Resources/es.tsv | Adds Spanish translations. |
| Packages/XStatsKit/Sources/Localization/Resources/de.tsv | Adds German translations. |
| Packages/XStatsKit/Sources/Localization/Resources/ar.tsv | Adds Arabic translations. |
| Packages/XStatsKit/Package.swift | Registers observation module, resources and tests. |
| NetworkExtension/zh-Hant.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/zh-Hans.lproj/InfoPlist.strings | Supplies Chinese extension permission text. |
| NetworkExtension/XStatsNetworkExtension.entitlements | Declares sandbox and network permissions. |
| NetworkExtension/main.swift | Starts the system extension. |
| NetworkExtension/ko.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/ja.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/Info.plist | Declares provider and communication service. |
| NetworkExtension/fr.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/es.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/en.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/de.lproj/InfoPlist.strings | Localizes extension permission text. |
| NetworkExtension/ConnectionObservationSession.swift | Exports session-scoped metadata reads. |
| NetworkExtension/ConnectionFilterProvider.swift | Observes connections while allowing traffic. |
| NetworkExtension/ar.lproj/InfoPlist.strings | Localizes extension permission text. |
| LICENSING.md | Clarifies configuration and dataset licensing. |
| DEVELOPMENT.md | Documents setup, signing and publication. |
| ARCHITECTURE.md | Describes observation and geography architecture. |
| App/zh-Hant.lproj/InfoPlist.strings | Localizes app permission text. |
| App/zh-Hans.lproj/InfoPlist.strings | Supplies Chinese app permission text. |
| App/XStats.entitlements | Adds extension and shared-group permissions. |
| App/ko.lproj/InfoPlist.strings | Localizes app permission text. |
| App/ja.lproj/InfoPlist.strings | Localizes app permission text. |
| App/Info.plist | Adds permission description and service name. |
| App/fr.lproj/InfoPlist.strings | Localizes app permission text. |
| App/es.lproj/InfoPlist.strings | Localizes app permission text. |
| App/en.lproj/InfoPlist.strings | Localizes app permission text. |
| App/de.lproj/InfoPlist.strings | Localizes app permission text. |
| App/ar.lproj/InfoPlist.strings | Localizes app permission text. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| model.connectionMonitor.setDemand(enabled: model.settings.canViewNetworkConnections, | ||
| visible: model.isMainWindowVisible && model.settings.panelTab == .connections) | ||
| model.networkGeography.setDemand(enabled: model.connectionMonitor.isReading) |
- 读取端暂停或切换时仅结束租约,保留活动连接与日志 - 批次读满时立即续读,并提示扩展缓冲区淘汰记录 - 重启后关闭模块仍写入停用,避免系统过滤器残留 - 应用隐藏(⌘H)时视为主窗口不可见,停止按需观察与下载 - 地图库提交前响应取消;缓存新鲜时回到页面不再闪更新提示 - 过滤器启动失败与停止共用收尾,listener 启停成对串行 - 裸可执行文件名保留点号;扩展显示名补齐九种语言 - DB-IP 当月文件未发布时回退上月;entitlements 许可记录于 LICENSING.md Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: debd250c64
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <key>NEMachServiceName</key> | ||
| <string>$(TeamIdentifierPrefix)work.12306.xstats.network-observation.ipc</string> |
There was a problem hiding this comment.
Version the extension Mach service across updates
When an already activated network system extension is replaced by a normal app update, this static service name can remain mapped to the old extension or yield immediately invalidated XPC connections even though the new extension activation succeeds. Apple is tracking this as FB17032197 and recommends including the app version in NEMachServiceName as the workaround (Apple Developer Forums); otherwise upgraded users can repeatedly hit the viewer's timeout/unavailable state until they manually recover the extension. Include CURRENT_PROJECT_VERSION in this value and in the matching NetworkObservationMachService value in the host app.
Useful? React with 👍 / 👎.
| private func install(_ record: CacheRecord, pair: [Data]) { | ||
| cache = record | ||
| v4 = pair[record.manifest.files.firstIndex { $0.name == "country-ipv4.bin" }!] | ||
| v6 = pair[record.manifest.files.firstIndex { $0.name == "country-ipv6.bin" }!] |
There was a problem hiding this comment.
Release geography tables after the feature is disabled
After the map is used once, these properties retain both complete IP tables for the lifetime of the menu-bar process, either as file mappings or heap-backed Data; each table permits 32 MiB, while NetworkGeographyController.setDemand(false) only cancels downloads and never unloads them. Consequently, turning the Network Monitor feature off cannot reclaim up to 64 MiB of cache memory even though no lookup can use it. Add an explicit bounded lifetime or unload path when the feature is disabled.
AGENTS.md reference: AGENTS.md:L20-L23
Useful? React with 👍 / 👎.
改用 SMAppService 按需控制服务,统一组件安装、更新和卸载入口,并按观察租约切换全部放行规则。 验证:完整 task test 824 项通过;组件签名、公证、c-ip 发布、在线首次安装及 Sparkle 检查更新通过。 验证边界:尚未执行组件跨版本 Sparkle 安装和可达 IPv6 目标的端到端通信测试。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3133fda27e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| status = .preview | ||
| } | ||
|
|
||
| var terminationRequiresPreparation: Bool { isSupported && (wantsRunning || isBusy || isReading || terminationStopPending) } |
There was a problem hiding this comment.
Reconcile the persisted filter before every normal quit
After a crash or forced quit leaves NEFilterManager.isEnabled persisted, a relaunch starts with wantsRunning == false, so quitting again before starting the viewer makes this property false and bypasses prepareForTermination; shutdown() only cancels local work and never saves isEnabled = false. The earlier disable-path fix does not cover this normal-quit case, leaving the system filter enabled after an ordinary exit; termination must reconcile the persisted configuration even when this controller is idle.
AGENTS.md reference: AGENTS.md:L20-L21
Useful? React with 👍 / 👎.
| reply.finish(.failure(NetworkMonitorError.unavailable)); return | ||
| } | ||
| proxy.perform(action) { @Sendable data in reply.finish(.success(data)) } | ||
| DispatchQueue.global().asyncAfter(deadline: .now() + 65) { reply.finish(.failure(NetworkMonitorError.timeout)) } |
There was a problem hiding this comment.
Keep approval RPC alive until the system request resolves
When system-extension approval takes longer than 65 seconds, this timer fails the client reply while NetworkComponentServiceHost is still awaiting the submitted OSSystemExtensionRequest. reconcileConfiguration() then changes the viewer to .failed, and the eventual successful activation reply is discarded, so returning from System Settings does not enable the filter until the user manually starts again; approval-bearing actions need to remain pending or reconcile their eventual completion instead of using this fixed timeout.
Useful? React with 👍 / 👎.
| info["CFBundleIdentifier"] as? String == bundleIdentifier, | ||
| info["CFBundlePackageType"] as? String == "APPL", | ||
| let team = dependencies.teamIdentifier(hostApp), !team.isEmpty, | ||
| dependencies.teamIdentifier(app) == team else { throw NetworkComponentInstallError.untrustedComponent } |
There was a problem hiding this comment.
Reject incompatible component protocols before installation
When an older XStats performs a first-time install after the fixed download alias has advanced to a component with a newer protocol, this validation accepts and commits that component because it checks only the bundle identity, type, and signing team. prepareComponentImpl() then rejects the installed metadata with protocolMismatch, while subsequent install attempts only validate the now-existing bundle and never replace it, leaving the viewer unusable until the user manually deletes the component or upgrades XStats. Validate the protocol and expected control service before the exclusive rename.
Useful? React with 👍 / 👎.
| <key>NSHumanReadableCopyright</key> | ||
| <string>© 2026 XStats</string> | ||
| <key>NSSystemExtensionUsageDescription</key> | ||
| <string>只在本机查看应用的网络连接,不阻断连接或读取通信内容。</string> |
There was a problem hiding this comment.
Add localized InfoPlist strings for the component host
On a non-Chinese system, the system-extension approval request is submitted by XStats Network Monitor.app, but this target has no localized InfoPlist.strings; despite declaring nine localizations, its NSSystemExtensionUsageDescription therefore remains the hard-coded Simplified Chinese fallback in every locale. Add the translated usage description to this host target, not only to the embedded extension.
AGENTS.md reference: AGENTS.md:L30-L30
Useful? React with 👍 / 👎.
未安装组件时弹出安装面板,安装成功后继续启用;关闭、离页或失败时不执行待启用操作。关闭按钮补充 Escape 支持和本地化文案。 独立 Monitor 应用复用 XStats 图标集,补齐资源、资产目录和 Info.plist 配置。 验证:完整测试 827 项通过,安装流程定向测试与打包配置检查通过;主程序和组件签名、公证、本地安装及登录项图标验收通过。
| let arguments = CommandLine.arguments | ||
| if arguments.contains("--register-service") || arguments.contains("--refresh-service") || arguments.contains("--unregister-service") { |
| SUVerifyUpdateBeforeExtraction: true | ||
| LSApplicationCategoryType: public.app-category.utilities | ||
| NSHumanReadableCopyright: © 2026 XStats | ||
| NSSystemExtensionUsageDescription: 只在本机查看应用的网络连接,不阻断连接或读取通信内容。 |
- 退出时停用失败只拦截一次,再次退出直接放行 - 等待系统扩展授权超时保留授权提示,并提供重试入口 - 旧过滤器停止等待在截止时间内重试并丢弃失效连接 - 状态刷新与变更命令串行,避免系统扩展请求冲突 - agent 空闲退出仅重建连接,失效时才重新注册 - 连接页分组每次渲染只计算一次 - 扩展复用 NetworkCodeIdentity 获取团队标识 Co-Authored-By: Claude <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
有效续租推动失败的观察设置重试,持续失败通过读取错误进入主程序现有重试与停止流程;使用代次和请求序号隔离旧完成。 组件 GUI 初始化主程序语言偏好,服务和注册 CLI 返回中文源文案键,由接收界面翻译;系统过滤配置使用显式语言翻译。组件及扩展构建号推进至 139。 验证:完整 task test 833 项通过,包含真实匿名 XPC 与模拟 apply 失败联动测试;组件及扩展 Release 签名构建通过。未在实际系统扩展中注入 apply 故障,未安装或发布本次构建。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c769f319c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| nonisolated func componentStatusChanged(_ data: Data) { | ||
| guard data.count <= 256 * 1024, let value = try? JSONDecoder().decode(NetworkComponentStatus.self, from: data) else { return } | ||
| Task { @MainActor in status = value } |
There was a problem hiding this comment.
Refresh the standalone status message after pushed updates
When system-extension approval takes longer than the standalone client's 120-second RPC timeout, the request reports a timeout while the service continues processing it; the eventual componentStatusChanged push updates only status, leaving message stuck on the timeout or approval text even when filterEnabled is now true. Recompute the displayed message from the pushed status so the standalone component UI reflects the eventual successful activation.
Useful? React with 👍 / 👎.
| return Dictionary(grouping: matching) { record in | ||
| grouping == .domain ? (record.hostname ?? record.address ?? tr("目标未知")) | ||
| : (record.address.flatMap { countries[$0] } ?? "??") | ||
| }.map { key, records in | ||
| NetworkConnectionGroup(id: key, name: grouping == .country ? networkRegionName(key) : key, | ||
| applicationPath: nil, records: records) |
There was a problem hiding this comment.
Sort domain and country groups by connection time
When a domain or country contains connections from multiple applications, matching is flattened from application groups that are sorted by application name, and the grouped records array is stored without re-sorting. Selecting that domain or country therefore shows connections in application-group order rather than newest-first, unlike the app and “all connections” views. Sort each cross-application group's records by timestamp before constructing it.
Useful? React with 👍 / 👎.
固定下载地址可能已指向更新协议的组件;在排他 rename 前校验协议版本与控制服务名, 避免装上后无法使用、也无法通过组件服务卸载。 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5269691292
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| installationCancelled = false | ||
| preparedForHandoff = false | ||
| lifetime.setSDKActive(true) | ||
| driver?.check(userInitiated: true, action: .install(release)) |
There was a problem hiding this comment.
Gate self-updates on protocol compatibility
When the component feed eventually advances to a newer NetworkObservationProtocolVersion while a user keeps this v2 main app, this path installs the release after validating only version/build and extension metadata; the signed component manifest does not carry the protocol or control-service contract. After replacement, NativeNetworkMonitorBackend.prepareComponentImpl() rejects every status, enable, and uninstall command with protocolMismatch, leaving the viewer unusable until the main app is upgraded or the component is manually removed. Add signed compatibility metadata and reject an incompatible target before invoking Sparkle's .install action.
Useful? React with 👍 / 👎.
| // 在目标文件系统暂存,最后使用排他 rename;目标即使在检查后出现也不会被覆盖。 | ||
| let staged = destination.deletingLastPathComponent().appendingPathComponent(".xstats-network-\(UUID().uuidString).app") | ||
| defer { try? manager.removeItem(at: staged) } | ||
| try manager.copyItem(at: candidate, to: staged) |
There was a problem hiding this comment.
Route the /Applications install through elevation
When XStats runs under a standard macOS account, this copy targets a staging path directly under /Applications, where the process normally has no write permission, and no authorization or privileged-helper path is used. The first-time component install therefore downloads and validates successfully but fails at the final copy, making the feature unavailable to non-admin users; perform the destination write through the repository's privileged helper or an explicit authenticated installation flow.
AGENTS.md reference: AGENTS.md:L7-L7
Useful? React with 👍 / 👎.
- 组件独立窗口空闲时按推送状态刷新提示,授权超时后最终启用不再停留在超时文案 - 域名与国家或地域分组先整体按时间倒序,避免按应用名分段 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72b887886c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Label(tr("网络监视器"), systemImage: "network").font(.title2.bold()) | ||
| Text(model.message).foregroundStyle(.secondary).textSelection(.enabled) | ||
| HStack { | ||
| Button(tr("启用连接查看")) { Task { await model.run("enable") } }.disabled(model.busy) |
There was a problem hiding this comment.
Route standalone enable to an observation reader
When a user launches XStats Network Monitor.app directly and clicks this button, enable only turns on the NEFilterManager configuration; this GUI never calls readEvents to create an observation lease. The provider therefore remains in its no-demand, allow-only mode and records no connections, while statusMessage reports “正在监视” solely because filterEnabled is true. Open the main viewer or establish a reader before presenting this action as active monitoring.
AGENTS.md reference: AGENTS.md:L21-L21
Useful? React with 👍 / 👎.
| } catch { self.error = error.localizedDescription } | ||
| } | ||
| } | ||
| var terminationRequiresPreparation: Bool { operation != nil || handoff != nil } |
There was a problem hiding this comment.
Include component downloads in quit preparation
When the component updater is downloading or verifying, the installUpdate RPC has already returned, so both operation and handoff are nil and this property reports that no termination preparation is needed. Quitting XStats therefore neither sends cancelUpdate nor waits for cleanup; the on-demand component service remains alive through sdkActive and can finish replacing the component after the controlling app has exited. This is especially unsafe during a simultaneous main-app update, because the late component replacement can disable the filter after the relaunched viewer has restored it, with no controller left tracking the handoff. Treat all active component-update phases as requiring preparation and explicitly cancel or await the service-side cycle.
AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.
| func uninstall() async throws -> Bool { | ||
| guard ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 15 else { throw NetworkMonitorError.unsupportedVersion } | ||
| stopReading() | ||
| try await setFilterEnabled(false) |
There was a problem hiding this comment.
Allow removal when another filter owns the configuration
When XStats's system extension is installed but the user has since enabled another product's content filter, setFilterEnabled(false) throws wrongConfiguration for that foreign provider. Calling it unconditionally here aborts before the XStats deactivation request and before component cleanup, so the user cannot uninstall XStats without first disabling the unrelated filter. Only disable and remove preferences when the provider identifier belongs to XStats; otherwise leave the foreign configuration untouched and continue deactivating the XStats extension.
Useful? React with 👍 / 👎.
- /Applications 无写权限时返回明确的本地化错误,不引入提权写入 - 开发文档记录协议版本升级前必须补齐的兼容与重装路径 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be42c7c1f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func filterEnabled() async throws -> Bool { | ||
| let manager = NEFilterManager.shared() | ||
| try await manager.loadFromPreferences() | ||
| return manager.isEnabled |
There was a problem hiding this comment.
Verify the provider before reporting the filter enabled
When another security product owns the shared NEFilterManager configuration, this returns true solely because that foreign filter is enabled. The resulting NetworkComponentStatus makes the component-management sheet report XStats's filter as enabled and makes the standalone app display “正在监视,” even though XStats is not receiving flows. Return true only when the configured data-provider identifier belongs to XStats and its socket-filter settings are valid.
Useful? React with 👍 / 👎.
| `ReleaseNotes.json` 中自行编写的更新摘要译文采用 AGPL-3.0-or-later。该 JSON 文件不支持注释,许可在本文件中记录。 | ||
| `ReleaseNotes.json` 和 `NetworkComponentReleaseNotes.json` 中自行编写的更新摘要译文采用 AGPL-3.0-or-later。这些 JSON 文件不支持注释,许可在本文件中记录。 | ||
|
|
||
| `App/XStats.entitlements`、`NetworkExtension/Info.plist` 与 `NetworkExtension/XStatsNetworkExtension.entitlements` 为 XcodeGen 生成的项目配置,重新生成时不保留注释文件头,采用 AGPL-3.0-or-later,许可在本文记录。网络连接模块为 XStats 自行编写;FreeSnitch 仅用于架构调研,未复制其源码或素材。 |
There was a problem hiding this comment.
Record the component plist files in the license exception
The commit adds NetworkMonitorApp/Info.plist and NetworkMonitorApp/XStatsNetworkMonitor.entitlements without SPDX headers, but this exception records only the equivalent main-app and extension files. Since these component files are also regenerated by XcodeGen and therefore intentionally omit comment headers, include both in this AGPL declaration (or add preserved SPDX headers); otherwise the new repository-owned configuration files have no recorded license designation.
AGENTS.md reference: AGENTS.md:L37-L38
Useful? React with 👍 / 👎.

新增可选的网络监视器,查看应用连接及目标国家分布。功能默认关闭,仅在 macOS 15+ 开放;主应用的其它功能继续支持 macOS 14。
功能与边界
验证
task test通过:766 个 Swift 测试、12 个地图库发布流程测试及其它脚本/API 检查。正式构建需要包含 Network Extension 权限的主应用及扩展 provisioning profiles。地图位置为国家级示意,不代表远端设备精确位置。