Replace the hybrid form (LSUIElement + runtime activation-policy
switching + AppKit-owned NSWindow) with a plain SwiftUI Window scene:
the app stays accessory at all times, launch presents the workspace
window via defaultLaunchBehavior(.presented), and the status menu
reopens it after close via openWindow. Drop AppDelegate and the
window-opening adapter and intents; WorkspaceRoute keeps navigation
state only, with the scene id defined there. Documented in ADR 0016
and CONTEXT.md.
Apps honoring only the HTTP proxy family were never proxied under
the SOCKS-only projection. Point system HTTP and HTTPS at the
always-on sslocal HTTP inbound (CONNECT); reject documents lacking
that inbound instead of aiming at port 0. See ADR 0012.
Not every preference warrants a reset; future resets are per
configuration item, added only where a field truly needs one (tracked
in #69). The whole chain goes: the 恢复默认 button and its confirmation
alert in SettingsView, the SettingsWorkflow reset/confirm/cancel
commands and the confirmation machinery they existed for (the
SettingsConfirmation enum had a single case), the
SettingsCommitting.resetPreferences seam entry, the runtime
resetPreferences() and ProxySettingsFileStore.reset() with its
file-rollback helpers and the rollbackFailed store error.
CONTEXT.md drops the three stale reset phrases and now records the
domain decision: no restore-all-factory-defaults action for
preferences; a reset, where it exists, is scoped to one configuration
item. ADR-0004/0005/0011 carry amendment notes since their reset
sentences describe a removed capability (0004's also predate the
PAC removal).
Stop rewriting the whole ACL on every proxy-mode switch: materialize one
file per routing shape (acl-<summary>.ini), keep acl-active.ini as a stable
symlink the contract points at, and skip unchanged writes via a digest
manifest. The contract carries only path/summary/sha256 — the agent checks
the link stays inside the runtime directory and never reads ACL bytes.
Agent switch now defaults to off (no server config on a fresh install) and
survives preferences reset, which also returns any held system proxy and
converges a still-running agent onto the factory runtime. See ADR-0011.
Real sslocal smoke (11 tests, ~27s) sat on every task test as the
slow tail. Tests/Smoke now builds as a separate target behind the
ShadowsocksX-NG2Smoke scheme; task test:smoke runs it on demand and
the README release gate requires it green next to packaging-gate.
SignalRecorder also gains kill(_, 0) semantics (stale pids report
ESRCH): the GUI-restart test burned the 5s waitForWrapperExit
ceiling on pid 99999, now 0.18s. Default lane: 617 tests ~40s.
Negative-path tests with a refusing FakeProbe each burned the full
production 15s launch health window; the three 30s tests run two
windows (activate + setAgentEnabled). Five tests accounted for 120s
of the 188.5s suite.
All test-side controller constructors now inject 0.05s; production
default stays 15s. Deterministic fakes exercise the same timeout
presentation path, so the suite drops to 71.4s with no behavior
change. Remaining slow tests are real-process smoke tests.
SMAppService does not resolve relative ProgramArguments paths reliably
when the app runs from DerivedData or other non-/Applications locations
(launchd reports inferred program + partial import and rejects spawn
with EX_CONFIG). The same binary and plist format work correctly when
registered via launchctl bootstrap with an absolute path.
Write the plist to ~/Library/LaunchAgents/ with the wrapper's absolute
path and register via launchctl bootstrap. Keep SMAppService only for
reading BTM approval status.
SMAppService does not reliably resolve relative ProgramArguments paths
for launchd (launchd reports inferred program + partial import and
rejects spawn with EX_CONFIG). Rewrite the plist's ProgramArguments[0]
to the wrapper's absolute path just before register().
SMAppService resolves ProgramArguments relative to the app bundle's
Contents/ directory, not the bundle root. The plist used
Contents/MacOS/ShadowsocksX-NG2Agent which resolved to a nonexistent
Contents/Contents/MacOS/... path, causing launchd to report
EX_CONFIG / spawn failed and the wrapper never started.
Change to MacOS/ShadowsocksX-NG2Agent so launchd finds the binary.
An old settings.json with preferredMode "pac" made decodeIfPresent throw
on ProxyModeKind, which marked the whole snapshot unreadable and refused
to deploy. Decode the raw string and fall back to rule mode for unknown
values so retained fields (ports, timeout, exceptions) survive.
Closes the startup failure reported after #67.
Remove the PAC product feature entirely: PAC HTTP service, port, health
probe, runtime extension, PAC URL system proxy projection, PAC rule
editing, and GFWList URL settings. The product now exposes rule / global /
direct modes only, all projected as a local SOCKS system-proxy target.
First-run defaults are rule + proxyWhenUnmatched, agent on, system proxy
off. Homepage and status menu share one ProxyControlSnapshot showing the
three modes and rule sub-options. Manual servers, subscriptions, Legacy
import, and independent SOCKS/HTTP inbounds are retained.
Fix: include verbose in SslocalListenFingerprint so a verbose-only
contract change restarts sslocal instead of leaving RUST_LOG stale.
Revise CONTEXT.md vocabulary, ADR-0007, and ADR-0009. Record GFWList
exception shadowing, IPv6 system-exception, DNS, and traffic-coverage
boundaries. Add host acceptance evidence document.
Closes#67
Persist user custom rules (exact domain, domain suffix, IPv4/IPv6 CIDR)
with stable identity and source metadata. Reject rules that conflict
with the fixed local scope or are shadowed by sslocal domain-proxy /
IP-direct priority, with explainable reasons. Merge valid custom rules
into both rule sub-modes ACL; global and direct modes do not load them.
Rule content changes recompile ACL via full restart with rollback.
Diagnostics expose only count and content-version summary.
Pin gaoyifan/china-operator-ip china.txt and compile its IPv4 CIDRs into
the proxy_all ACL alongside China domain candidates. Explicit maintainer
update normalizes and deduplicates CIDRs and blocks replacement on
abnormal format, corruption, scale change, or all-invalid input.
Closes#64
77 → 0 warnings. Split oversized functions and files: controller and
agent extension files, standalone RuleSet/ProxySettingsFileStore/
SslocalListenSettings/LegacyImportService, test classes into topic
extensions. Regroup activation options into RuntimeDocumentOptions and
converter metadata into Provenance.
Runtime controller DI seams widen private → internal so the extension
files stay cohesive; new controller files are registered in the
architecture guard exemption list; Agent target adds RuleSet and
SslocalListenSettings to project.yml. Test JSON literals wrap inside
the string literal only.
Introduce a source-independent rule model (action, domain/CIDR match,
source identity, conflict metadata) and ship a pinned geolocation-cn
snapshot produced by an explicit maintainer update. Rule mode with
proxy-when-unmatched compiles China domains as direct candidates into a
proxy_all ACL; the sub-option is selectable and persisted from home and
the status menu. Ordinary builds only read the local snapshot and fail
when it is missing or corrupt.
Global mode now deploys a minimal proxy_all ACL carrying only the fixed
local safety rules, so public targets default through Shadowsocks while
loopback/RFC1918/link-local/ULA/localhost/*.local/no-dot hosts stay
direct for SOCKS, HTTP, and LAN-sharing clients. System proxy exceptions
include that fixed scope; IPv6 system-exception limits are documented
with the ACL as the routing guarantee. Mode switches keep the
full-restart and rollback path.
Closes#62
The toggle only gated a convenience endpoint: system proxy never used
the HTTP port, which serves the terminal export line and LAN proxy.
Stored httpProxyEnabled:false is silently ignored, no migration.
Proxy-endpoint settings rows lose their subtitles; glossary term is
now the permanent "HTTP proxy inbound".
- window.title tracks the destination via composition-root wiring
into WorkspaceWindowOpeningAdapter.bindTitle; drop the static
"ShadowsocksX-NG 2.0" title
- delete the 28pt shell destinationHeader; subscription and
diagnostics actions move into the unified toolbar
- settings save/reset stay with the form as a top action row in
SettingsView; form-level commit actions read wrong in a toolbar