Skip to content

🐛[BUG] -clt proxy / -clt shutdown 对运行中的实例无响应,冷启动也不会按注释拉起程序 #4141

Open

Description

@mengge237

请认真检查以下清单中的每一项

  • 已经搜索过,没有发现类似的 issue
  • 已经搜索过文档,没有发现相关内容
  • 已经尝试使用过最新版,问题依旧存在
  • 使用的是官方版本(未替换及修改过安装目录程序文件)

🐛 描述

src/BD.WTTS.Client/Startup/Startup.Commands.cs 里有 proxy 命令,注释写的是「启用代理服务,静默启动(不弹窗口)」,另有 -clt shutdown(安全结束正在运行的程序)。常量在 Startup.Constants.cs(clt_ = "-clt"、key_proxy = "proxy"、key_shutdown = "shutdown"、key_show = "show"),服务端分支在 Startup.Host.cs 的 case key_proxy → IProxyService.StartOrStopProxyService(bool)。本机 Windows x64 3.1.0 安装目录里能搜到「启用代理服务,静默启动(不弹窗口)」这串字面量,所以应当是带这个功能的版本。

实测三点:

  1. 有实例在跑时,命令送达了但没有任何作用。 客户端退出码 0,而三项外部判据全部不变:hosts 里的 # Steam++ Start 段、443 的监听者(Steam++.Accelerator)、curl -I https://github.com/ 的返回码。当天日志 %Tmp%\Steam++\Logs\nlog-all-2026-09-12.log 是跨实例连续追加的同一个文件,21:56:33 之后直到 23:01:03 之间零新增行,而这段时间内我投了 6 次 -clt proxy 各种写法与 1 次 -clt shutdown。也就是说主实例侧既没有成功日志也没有失败日志。
  2. 没有实例在跑时,-clt proxy -on 不会把程序拉起来。 客户端 1s 内退出(码 0),连续轮询 36s 进程数为 0、hosts 不变。按 Startup.Commands.cs 的注释这条路应当走「未启动 → 类似 -silence 逻辑」。
  3. 中途一次 -clt proxy -on 之后进程数从 3 变 4 且未回收(我没能区分那是第二个完整实例还是子进程,不敢断言)。

顺带一个容易误伤使用者的点:不带任何参数启动时,Startup.SingleInstancePipeline.cs:18 是
SendMessage(sendMessage?.Invoke() ?? (IsMainProcess ? key_show : "")),默认消息就是 key_show,
所以「想用命令行静默开启」而直接执行 Steam++.exe(不带 -clt)必然把主窗顶到前台。这本身是设计如此,但和 proxy 命令注释放在一起时很容易被当成同一条路。

📷 复现步骤

前置:正常启动程序并开启网络加速,确认 hosts 有 # Steam++ Start 段、Get-NetTCPConnection -State Listen -LocalPort 443 的持有者是 Steam++.Accelerator。

  1. 执行 Steam++.exe -clt proxy -off true,等 20s
  2. 复看上面三项判据 —— 本机全部不变,且当天日志零新增行
  3. 把程序完全退出(管理员 Stop-Process,见下方补充),再执行 Steam++.exe -clt proxy -on,等 40s
  4. 复看进程数与 hosts —— 本机为 0 进程、无变化

参数写法已排除是我语法错(用退出码当解析探针,bogus-verb 给 1 作对照):

参数 客户端退出码 主实例反应
-clt proxy --off 1(解析失败) 无
-clt proxy -off true 0 无
-clt proxy -off 0 无
-clt proxy(想 toggle) 0 无(case key_proxy 要求 args.Length >= 2,按源码本来就不该有反应)
-clt shutdown 0 无
-clt bogus-verb 1(对照组) —

📄 日志信息

另一个观察:每次开启加速都会刷 7~8 条同名异常,但加速最终仍然成功。下面两段的栈逐字相同,前者来自 GUI 手点(与 hosts 修改时间 21:56:34 吻合),后者来自命令行冷启动(23:13:24,另一次 23:01:03、23:05:20、23:07:27 各一组,条数 7~8 条):

2026-09-12 23:13:24.7772|Error|StartOrStopProxyService|System.Threading.Tasks.TaskCanceledException: A task was canceled.
 ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(...)
   at System.Net.Http.RedirectHandler.SendAsync(...)
   at System.Net.Http.SocketsHttpHandler.<SendAsync>g__CreateHandlerAndSendAsync|115_0(...)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(...)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpClient.HandleFailure(...)
   at BD.WTTS.Services.Implementation.NetworkTestService.TestDNSOverHttpsAsync(String testDomain, String dohServer, DnsRecordType dnsRecordType, CancellationToken cancellationToken)
   at BD.WTTS.Services.ProxyService.GetValidDNSAsync(String dns, CancellationToken cancellationToken) | 

想确认这是 ProxyBeforeDNSCheck 的预期日志(DoH 探测失败按 Error 记账)还是异常路径。若属预期,建议这几条降级到 Debug 或补一条「哪个 dns、超时多少」的可读信息:自动化侧要靠日志判断「这次到底成没成」,现在只能退回去看 hosts 段和 443 的持有者。

🖼 截图

不放截图,避免把本机窗口标题和账号信息带出去;上面三项判据都能命令行复验,比图更省事。

🚑 其他信息

  • 处理器体系结构:x64
  • 系统版本号:Windows 11 专业版 25H2 Build 26200
  • 下载渠道:本机安装在自定义目录(不是微软商店路径),具体渠道我无法从安装产物反推
  • 程序版本:3.1.0(FileVersion 3.1.2025.0,releases/latest 也是 3.1.0)
  • 是否包含独立运行时:否(安装目录内未见 hostfxr / hostpolicy)

补充一个绕法现状,也是提这个 issue 的原因:我这边「自动开」最后是绕到 冷实例 + ProgramStartupRunProxy=true 实现的(实测约 6s 起效,且 MainWindowHandle=0、不抢前台);但「自动关」没有优雅路径 —— 只能管理员 Stop-Process,而被杀掉的实例不会还原 hosts(本机实测留下 47 条 127.0.0.1 僵尸行,此时 GitHub 直接 curl 000),所以还得自己按 # Steam++ Start / # Steam++ End 标记把段剥掉,并且 hosts 在刚杀完会被短暂锁住、写入要重试。CloseMainWindow() 也不算退出路径,它只是缩进托盘、hosts 照旧。
如果 -clt proxy -off 与 -clt shutdown 能对运行中的实例正常响应,这两件事就都能优雅收口,也就没有必要用「杀进程 + 清 hosts」这种脏办法。

同源、都还 open 的几条相关 issue(现象不同,但都是「启动路径上的窗口/权限」这一类):#3805(每次启动都弹 UAC)、#2004(自启动强制最小化不生效)、#3650(开机弹窗)、#3316(开机自动加速遇 hosts 掉权限会反复弹网页)。

Activity

  1. mengge237 commented on Sep 25, 2026

    @mengge237
    Author

    补一条根因,它比我原帖里的「无响应」更糟:-clt proxy 投不到运行中实例时不是什么都不做,而是杀掉正在跑的实例,再冷启动一个全量实例。下面按源码链说明(行号取默认分支 develop @ d04213147e77,2026-08-31;本机安装是 3.1.0,FileVersion 3.1.2025.0,二进制里能搜到 :395 那句注释原文,所以链条对得上)。

    链路(src/BD.WTTS.Client/Startup/)

    • Startup.SingleInstancePipeline.cs:12-38 InitSingleInstancePipeline():已经有实例时(IsFirstSelfApp == false)先 SendMessage(...)(:18);一旦返回 false 就走到 :24-29 → TryKillCurrentAllProcess() → goto 重新初始化,这一轮自己成了第一个实例,于是启动全量 UI。
    • 同文件 :118-137 TryKillCurrentAllProcess():对本程序全部进程 p.Kill(true) + WaitForExit(15s),异常全 catch。强杀意味着 ProxyService 的清理跑不到。
    • 同文件 :219-236 SendMessage():try { 连接 7.7s → 写一行 } catch { return false; } —— 管道不存在、连接超时、写入失败被压成同一个 false,调用方拿不到任何区分,进程退出码照旧 0。
    • 同文件 :142-146 管道名 = APPLICATION_ID + "_v" + InformationalVersion + "_" + Crc32(Environment.ProcessPath):版本号或 exe 路径任一处对不上,就一定送不到 —— 而送不到的表现就是上面那条 kill + 冷启动。
    • 服务端本身没问题:Startup.Host.cs:276-281 的 case key_proxy 处理完直接 return,不会走 :315-318 的 RestoreMainWindow;Startup.Commands.cs:395 注释写的「启用代理服务,静默启动(不弹窗口)」在消息真送到时是成立的。

    本机读数(原帖那七次投递):退出码 0 共 7 次、主实例日志新增 0 行、hosts 与 443 监听与 curl 三项均无变化、并且进程列表里多出一个全量实例 —— 与 :24-29 那条分支的行为完全吻合。关掉加速只能 Stop-Process,而那次强杀留下 47 条未还原的 hosts 条目,之后 GitHub 直连持续 000。也就是说这个命令的失败路径会把使用者推到一条会脏系统文件的绕行路上,这正是我想改的地方。

    改法参考(按我愿意为它花时间验证的顺序)

    1. 让 -clt 不参与单例抢占:IsConsoleLineToolProcess 为真时,SendMessage 失败就非 0 退出并打一行原因,绝不进 TryKillCurrentAllProcess()。调 -clt 的语义是"转告运行中的实例",不是"我要成为实例";现在的一个只读式调用被这条兜底变成了破坏性操作。
    2. SendMessage 给三态并映射退出码:NoServer / Timeout / IOError(例如 2 / 3 / 4),日志里带上管道名的 CRC32 段与版本段(不要带完整安装路径)。有了这两段,"我这台为什么送不到"是一条命令就能自证的事,而不是像我现在这样靠排除法。
    3. 如果确实要保留接管逻辑,先优雅退出再重启:Startup.Host.cs:246-249 已经有 key_shutdown → app.Shutdown() 这条路,它能走完 hosts 还原;把它当兜底的第一步,等进程真退出再起新实例,别 Kill(true)。
    4. 形状提醒,不用现在改:OnMessageReceived 的 default 分支里以后新增命令时,记得像 key_proxy 那样 return(:280),否则会落到 :315-318 的 RestoreMainWindow —— 那会把一次"静默"调用变成弹主窗。

    怎么算修好了(不要求按我的写法,只要判据能过)

    • 实例在跑时执行 -clt proxy -on:退出码 0,且主实例日志出现一行代理状态变更。
    • 管道名对不上时:退出码非 0 并写明原因,且不新增进程、不杀已有进程。
    • -clt proxy -off 之后:hosts 里的代理段消失(不需要我再手删)。

    以上只是我从源码链推出来的改法,你们更清楚单例接管为什么要保留 kill 这一步;我实际需要的只有第 1、2 两条,因为它们决定这个命令能不能被脚本安全地调用。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions