Skip to content

v2.2.5

Choose a tag to compare

@github-actions github-actions released this 11 Aug 18:01
· 100 commits to master since this release

Synced upstream docmirror/dev-sidecar master (29 commits). Upstream introduced a CLI rewrite (native ds-cli binary with SEA packaging), single-instance mutex, and Linux/macOS environment-variable proxy support. All fork-specific changes (CA cert passthrough, Xray plugin, keep-alive socket fix, log.debug hot-path demotion, conditional linuxTargets, native module rebuild, CSS variable theme) were preserved. The fork's desktop-detection logic in set-system-proxy was merged with upstream's env-var support so that headless servers now write proxy env vars (previously the early return true skipped them).

Added

  • CLI rewritten as native ds-cli binary (upstream). The CLI (packages/cli/) was rewritten from a plain Node script into a native command-line tool ds-cli with Sea-of-Nodejs (SEA) packaging support. New packages/cli/scripts/build.js performs incremental builds with SHA256 checksums and parallel downloads, auto-cleans stale build products while preserving the node-bin cache, dynamically fetches the Node.js supported platform list, and displays OS type/version/arch. New .github/workflows/build-cli.yml CI workflow builds the CLI binary. New packages/cli/README.md documents SEA packaging and cross-compilation. packages/cli/src/sea-entry.js is the SEA entry point; packages/cli/.gitignore ignores build products except sea-config.json.
  • CLI new commands (upstream): proxy on/off (fork worker sets system proxy immediately), plugin start/stop (persists to config.json, takes effect on restart), service install/uninstall (boot autostart), help (command form ds-cli help only, --help/-h removed), status (shows running instance and plugin state), start/stop/restart. Workers split into proxy-worker.js, plugin-worker.js, free-eye-worker.js. packages/cli/src/user_config.json5 removed (no longer shipped as a static template).
  • CLI/GUI single-instance mutex via proper-lockfile long-lock (packages/core/src/modules/instance/index.js, 140 lines). packages/core/src/expose.js exposes api.instance. GUI background.js acquires the lock on app.whenReady() and writes instance info (type: 'gui', pid, command, startTime) to running.json; if another instance holds the lock, the GUI quits with an error. CLI does the same on start/proxy on/plugin start. packages/core/test/instanceTest.js (153 lines) covers acquire/release/write/cleanup.
  • Linux/macOS environment-variable proxy support (upstream). packages/core/src/shell/scripts/set-system-proxy/index.js gained writeProxyEnvFile / addProxyEnvToShellProfile / removeProxyEnvFromShellProfile helpers and a setEnv param. When setEnv is true, the proxy env vars (http_proxy, https_proxy, no_proxy) are written to a file and appended to the detected shell profile (~/.bashrc / ~/.zshrc), independent of gsettings — so CLI tools (curl, git, npm) on headless servers pick up the proxy even without a desktop. macOS uses the same env-var file mechanism alongside networksetup.
  • Plugin status events (upstream). overwall and pip plugins now fire event.fire('status', ...) on start/close and log 开启/关闭【X】代理成功. overwall gained a status: { enabled: false } block. packages/core/src/modules/server/index.js emits status.server.enabled events. packages/gui/src/view/pages/proxy.vue reads the new status.
  • CLI test suite (upstream). 60+ new Mocha test cases: packages/cli/test/{gui,index,plugin,proxy,service,start,status}.test.js covering proxy on/off, config persistence, shell detection, service/help/version/unknown commands.
  • Xray Stage3 零中断热刷新(Phase 2) (fork). Stage3 后台热刷新改为基于 Xray HandlerService gRPC API 的动态增删 outbound,不再重写 config.json + 重启 xray 进程,现有连接完全不中断。新增 packages/core/src/modules/plugin/xray/xray_api.js 模块,封装 xray api ado(AddOutbound,stdin 传 JSON)/ xray api rmo(RemoveOutbound,按 tag)/ xray api lso(ListOutbounds)三个 CLI 子进程调用(execFile + 5s 超时)。packages/core/src/modules/plugin/xray/gen_config.js 新增 apiPort 参数和 api 块生成(listen: 127.0.0.1:<apiPort>,services: [HandlerService, ObservatoryService, RoutingService]),并新增 7 个单元测试(packages/core/test/xrayGenConfig.test.js)。balancer.selector 和 observatory.subjectSelector 从显式 tag 列表 ["proxy_0",...] 改为前缀 ["proxy_"],通过 Xray strings.HasPrefix 自动包含运行时动态新增的 proxy_N tag。RemoveHandler 只从 manager 的 map 删除 tag 引用、不调 handler.Close(),已建立连接继续完成、新连接不再路由到该 tag;Observatory background() 每个探测周期自动发现新 outbound 并探测。packages/core/src/modules/plugin/xray/index.js 路径 B 新增 API 热刷新逻辑:维护 currentLiveNodeTags(Map: fingerprint→tag)和 nextProxyTagIndex 计数器,先 addOutbounds 新节点(observatory 下个周期探测后才可选,正好支持"先 Add 后 Remove"策略),再 removeOutbounds 坏节点;API 调用失败时回滚 in-memory 状态并 fallback 到重启路径(Phase 1 行为)。兼容 Xray-core v26.3.27+(v26.3.27 observatory 不清理已移除 outbound 状态但无害)。73 个 core 测试全部通过无回归。
  • Xray 主进程启用 metrics 端口供运行时调试 (fork). 主进程(live xray)的 3 个 genConfig 调用点(冷启动 regen / fallback 重启 / 初始启动)新增 metricsPort 参数,生成 metrics 块(expvar /debug/vars)。运维人员可通过 curl -s http://127.0.0.1:<metricsPort>/debug/vars | jq '.observatory' 查看主进程各节点的 alive/delay/lastErrorReason,无需依赖上游未合并的 xray api obs 命令。metricsPort 和 apiPort 通过 event.fire('status', ...) 写入 running.json 的 app.status.plugin.xray,方便随时查看。packages/core/src/modules/instance/index.js 的 watchStatusEvents 过滤逻辑从仅同步 *.enabled 扩展为同时同步 plugin.xray.port/apiPort/metricsPort 三个端口字段。复用已有 config 时从 metrics.listen 提取端口(与 api.listen 同逻辑)。
  • 新增 doc/xray-devops.md (fork). Xray 插件开发与运维文档,面向调试运行时状态或排查 Stage3 热刷新行为的开发者/运维人员。包含:Stage3 热刷新机制(工作原理 + 日志关键词解读)、运行时调试方式(curl /debug/vars 查看 observatory 节点延时 [推荐] / xray api lso/obs/bi 子命令)、Xray-core v26.3.27 vs v26.7.28 版本兼容性对比表、部署后首次重启注意事项。

Changed

  • Xray Stage3 热刷新路径 B 改为 API 动态增删 (fork). packages/core/src/modules/plugin/xray/index.js 的 maybeRegenerateLiveConfigFromCache 路径 B(热刷新,config.json 已有节点)不再调 processApi.restart(stop SIGTERM + 200ms + start,中断流量),改为通过 HandlerService gRPC API 动态增删 outbound。仅在冷启动(路径 A,config.json 无节点)或 API 调用失败 fallback 时才重启 xray 进程。运行时增删纯走 API,config.json 定位保持"上次启动时的快照"语义,不同步写回(避免写文件并发问题,Stage1 冷启动筛选本身可靠不依赖 config.json 实时性)。
  • 简化 Stage3 重启判断条件(Phase 1) (fork). packages/core/src/modules/plugin/xray/index.js 的短路条件从 keptNodes.length === currentConfigNodes.length && keptNodes.length >= startupNodeLimit 改为 keptNodes.length >= startupNodeLimit。旧条件要求"可用节点数达标且节点集合未变化"才跳过重启,导致 5 个节点全可用但 startupNodeLimit=10 时因数量变化触发无谓重启。新条件只要可用节点数达标即跳过,节点集合的变化由 Phase 2 的 API 动态增删处理,无需重启。
  • Stage1 候选节点 SQL 层提前过滤 country/owner (fork). packages/core/src/modules/plugin/xray/index.js 的 5 处 buildCacheEntryQueryOptions 调用(启动主路径/冷启动 regen/热刷新)新增 allowedCountries 和 allowedOwners 参数,让 SQL WHERE 子句直接过滤 country IN (...) 和 owner NOT LIKE '%cloudflare%',而不是在 JS 层 collectBootstrapCandidateEntries 事后过滤。之前 LIMIT 100 取出的节点可能大部分不符合 country/owner 条件(如前 100 个低延迟节点大多是 CN/JP),导致 probe 后筛出的可用节点不足 startupNodeLimit。修复后 LIMIT 100 取的全是符合 country/owner 条件的节点,probe 后能筛出更多符合 maxDelayMs 的节点。bootstrapCandidateLimit 默认值从 31 提高到 100。
  • Stage1 快速复检改用 observatoryProbeUrl 探测 (fork). packages/core/src/modules/plugin/xray/index.js 的 probeNodesBatch 新增 probeUrl 参数,Stage1 bootstrap 传 cfg.observatoryProbeUrl(严格,如 chatgpt.com),Stage3 周期探测传 cfg.probeUrl(宽松,gstatic 204)。之前 Stage1 用 probeUrl(宽松)探测,导致通过 gstatic 筛选的节点在主进程 observatory 用 observatoryProbeUrl(严格)重新探测时大量 dead(delay=99999999)。修复后 Stage1 和主进程 observatory 用同一探测目标,确保进 config.json 的节点都能通过 observatory 探测。
  • Stage1 不再用未 probe 的 stable 节点凑数 (fork). packages/core/src/modules/plugin/xray/index.js 启动节点选择不再将 supportedFallbackEntries(只检查格式、未 probe 的 stable 节点)与 bootstrapSelectedEntries(probe 验证过的节点)合并凑满 startupNodeLimit。现在只用 probe 验证过的节点,仅在 probe 完全失败(0 个节点)时才 fallback 到 stable 节点兜底,避免未验证的 delay=0 节点进入 config.json 导致主进程 observatory 标记为 dead。
  • 移除 Stage3/cache 和 bootstrap 探测的人为超时 (fork). packages/core/src/modules/plugin/xray/index.js 的两处 probeNodesBatch 调用(Stage1 bootstrap + Stage3 cache 周期探测)从传递 cacheBatchTimeout/bootstrapBatchTimeout 秒数改为直接传 timeoutMs: 0。packages/core/src/modules/plugin/xray/probe.js 的 waitForObservatoryMetrics 在 timeoutMs <= 0 时使用 deadline = Infinity,即不设超时上限,让 observatory 自然收集完所有 sample 再返回;探测进程崩溃通过 child.exitCode 检查捕获。之前 cacheBatchTimeout: 120(2 分钟)和 bootstrap 超时会在 v26.3.27 observatory 慢速收集时提前中断探测,导致 "Observatory metrics have not collected N samples yet" 错误和节点遗漏。同时删除了不再需要的 getCacheBatchTimeoutSeconds/getBootstrapBatchTimeoutSeconds 辅助函数,并从 packages/core/src/modules/plugin/xray/config.js 移除 cacheBatchTimeout 配置项。
  • 探测从 burst observatory 切换到 regular observatory + 并发探测 (fork). packages/core/src/modules/plugin/xray/index.js 的 runSingleProbePass 将 probeMode 从 'burst' 改为 'observatory'。v26.3.27 的 burst observatory 是串行探测的——每个 alive 节点需要 timeout × sampling 秒(15s × 2 = 30s),128 个 alive 节点需要 64 分钟;之前之所以快是因为大部分节点 dead(连接拒绝毫秒级完成),SQL 过滤后节点质量提升、alive 比例增加后速度暴跌。regular observatory 配合 enableConcurrency: true 并发探测所有节点,128 节点仅需 5-6 秒。实测 Stage3 第一轮 14332 节点(112 批)从预估 93+ 小时降至 ~11 分钟完成。同时修复了 buildCacheEntriesFromObservatory(packages/core/src/modules/plugin/xray/cache.js)不处理 regular observatory 格式的问题——regular observatory 的节点状态是 {alive, delay, outbound_tag}(无 HealthPing 字段),旧代码只处理 HealthPing 路径、跳过无 HealthPing 的状态,导致 regular observatory 的 alive 节点全部被丢弃;新增 fallback 路径直接从 alive/delay 字段构建 cache entry。probe.js 的 isObservationReady 也同步更新:当检测到无 HealthPing 字段时(regular observatory),改用 delay > 0 判断所有节点是否已被探测(dead 节点的 delay=99999999 也满足此条件),而非等待 healthPing.all >= expectedSamples。
  • set-system-proxy complementary merge (fork + upstream). The fork's desktop-detection logic (skip gsettings when /usr/bin/gsettings is absent or the X server socket /tmp/.X11-unix/X* does not exist, to avoid spawning dbus-launch + dbus-daemon + dconf-service on headless servers) was refactored from an early return true into a hasDesktop flag. Upstream's env-var support (writeProxyEnvFile + addProxyEnvToShellProfile) now runs unconditionally; gsettings calls are guarded by if (!hasDesktop) return true after env vars are written. This fixes a regression where the fork's early return skipped env-var setup on headless servers, leaving CLI tools unable to use the proxy.
  • Duplicate version keys in package.json (packages/cli, packages/core, packages/mitmproxy) cleaned up after conflict resolution (the ours/theirs merge left duplicate "version": "2.2.4" lines). Versions kept at 2.2.4 (will bump to 2.2.5 on release).
  • packages/core/src/expose.js: startup() now checks !status.server.enabled before calling server.start(), preventing a duplicate-start when the server is already running.
  • packages/core/package.json: added proper-lockfile: ^4.1.2 dependency.
  • Root package.json: merged fork's electron-builder 25.1.8 / app-builder-lib / dmg-builder / electron-builder-squirrel-windows pins with upstream's security overrides (axios, brace-expansion, cross-spawn, dns-packet, form-data, hoek, ip, minimist, qs, tough-cookie).
  • packages/gui/src/view/composables/theme.js: adopted upstream's document.body.setAttribute('data-theme', theme) sync (complements the fork's classList.add('theme-dark') in App.vue, which was deduplicated from 13 accidental copies to 1).
  • README.md: upstream revised the mirror website section.

Fixed

  • GUI process detection false positives (upstream). packages/cli/src/commands/gui.js and status.js no longer misreport a running GUI when matching unrelated processes.
  • status.json periodic writes removed (upstream). Replaced with event-driven status updates merged into running.json, eliminating the separate status.json file and its timer.
  • Instance PID reuse false positive (fork fix for upstream regression). service-entry.cjs and instance.isLocked() only checked if a PID was alive (process.kill(pid, 0)), without verifying the process was actually dev-sidecar. When a stale service.pid was reused by an unrelated process (e.g. kiro-go at PID 234), the instance check always passed, causing systemd to fail restarting dev-sidecar in an infinite loop (81 restarts observed on a corporate network). Fix: isDevSidecarProcess() in service-entry.cjs and isDevSidecarPid() in instance/index.js now read /proc/<pid>/cmdline and check for dev-sidecar/service-entry/@docmirrordev-sidecar keywords; non-Linux falls back to process.kill(pid, 0). Added 2 test cases to instanceTest.js.
  • mitmproxy OOM + respawn deadlock under stage3 cache thrash (fork fix). Two coupled bugs caused mitmproxy to crash and the auto-respawn to silently fail on a corporate network after ~5h of operation:
    • mitmproxy --max-old-space-size=96 too small for sub2api traffic + corporate SASE: the 96 MB cap was sized for omniroute traffic (steady-state ~15 MB, 6× headroom) in v2.2.4. With sub2api's high-concurrency router.huggingface.co SNI bursts stacked on the *.huggingface.co fakeServer (shared by all huggingface subdomains via getDnsName) plus the corporate SASE 141-cert PEM ca array spread into every HttpsAgent, live V8 objects reached 94.5 MB (pooled: 0.0 MB, average mu = 0.214, mark-compact ineffective) and SIGABRT'd. Raised --max-old-space-size from 96 to 192 in packages/core/src/modules/server/index.js (12× headroom). Also demoted the one remaining hot-path log.info in FakeServersCenter.js SNICallback to log.debug (v2.2.4's 57-call demotion missed this single line, which fired on every TLS SNI handshake).
    • Stage3 overrun → cleanup while-loop blocks main thread → SIGCHLD cannot dispatch: cacheRefreshInterval was configured to 10800 s (3 h) but the Stage3 round took 5 h 40 min, so resolveNextCacheRefreshDelay returned 0, immediately scheduling the next round. The next round's Stage2 cache-only path hit cacheSizeBeforeStage2 >= CACHE_SIZE_LIMIT_BYTES (1 GB) and entered cleanupOutdatedToSizeLimit's while (getSqliteDatabaseSizeBytes(db) > target) { delete batch; incremental_vacuum(2048) } — a fully synchronous better-sqlite3 transaction on a 1 GB database under MemoryHigh=280M cgroup thrash (PSI memory/io both ~100%) that blocked the main thread's libuv event loop for 25+ minutes. When mitmproxy SIGABRT'd during this window, the kernel delivered SIGCHLD but libuv could not dispatch it (no event-loop tick) → serverProcess.on('exit') never fired → respawnState 30-second/3-attempt sliding window was skipped entirely → the child became a zombie (<defunct>) and systemd showed active (running) while the proxy port was dead. Fix: (1) cleanupOutdatedToSizeLimit in packages/core/src/modules/plugin/xray/cache.js is now async and yields to the event loop (await new Promise(r => setImmediate(r))) every 4 batches by closing and reopening the SQLite handle, letting libuv dispatch pending SIGCHLD/IPC; (2) resolveNextCacheRefreshDelay in packages/core/src/modules/plugin/xray/index.js now enforces a minimum 30-minute cooldown (STAGE3_OVERRUN_COOLDOWN_MS) when the previous round overran cacheRefreshInterval, preventing immediate re-entry into the cleanup loop. The 2 cleanupOutdatedToSizeLimit test cases in xrayCacheOrdering.test.js were updated to async/await. Verified by 66 passing core tests.