Skip to content

fix(everest): 安装器原生崩溃时给出能照做的提示,而不是只甩一段栈 - #94

Open
std-external wants to merge 2 commits into
std-microblock:masterfrom
std-external:fix/everest-installer-native-crash-hint
Open

std-external wants to merge 2 commits into
std-microblock:masterfrom
std-external:fix/everest-installer-native-crash-hint

Conversation

@std-external

Copy link
Copy Markdown
Contributor

现象

群里一位用户用 CeleMod 装 Everest 时反复失败,CeleMod 只弹出一整段 .NET 的崩溃栈:

Failed to install Everest: Command failed with error: Fatal error. System.AccessViolationException:
Attempted to read or write protected memory. ...
at Interop+Globalization.<StartsWith>g____PInvoke|15_0(...)
at System.Globalization.CompareInfo.IsPrefix(...)
at System.String.StartsWith(String, StringComparison)
at MonoMod.MonoModder.<>c__DisplayClass88_0.<FindType>g__CrawlDependencies|0(Boolean)
...
at MiniInstaller.DepCalls.RunMonoMod(...) at MiniInstaller.Program.StandardMode(...)

这段栈没有任何可执行的信息,用户只能去问 AI / 搜 B 站(他最后是「更新显卡驱动」装上的)。

不是重链竞态(先说清楚,免得 PR 修错了地方)

  • MiniInstaller/Program.cs:53 StandardMode 在本进程里同步调 DepCalls.RunMonoMod(:108/:109/:112),DepCalls.RunMonoMod 只是 AsmMonoMod.EntryPoint.Invoke → MonoModder.AutoPatch;上游 MonoModder.cs(MonoMod reorganize 分支)里没有任何 Parallel / Task / 线程,这条重链路径是单线程的。
  • 官方 Everest 没有并行 relink / 并行加载;EVEREST_PARALLEL_RELINK、EVEREST_PARALLEL_LOAD 是 EverestUltra 的开关(CeleMod 的 is_everest_ultra 正是靠 EVEREST_PARALLEL_LOAD 这个字符串认 EU 的)。安装阶段也不会走到游戏里的加载器。
  • 崩的地方是 String.StartsWith 走到 原生 globalization P/Invoke 才被 AV 打断 —— 托管字符串不会凭空指向受保护内存,这类 AV 说明进程里的原生层已经被打坏了(杀软/驱动覆盖层注入、游戏目录里那份 piton-runtime 被打坏等),不是 CeleMod 或 Everest 的哪一行逻辑写错。用户「更新显卡驱动后就好了」也符合这一类(换掉的只是注入到每个进程里的东西)。

改了什么

src-tauri/src/backend.rs:新增 with_installer_crash_hint,在安装失败的错误串里认出这类原生崩溃(System.AccessViolationException / Attempted to read or write protected memory / 中文运行时的「尝试读取或写入受保护的内存」),在原错误后面补一段能照做的建议:

  • 先临时关掉杀毒软件/安全卫士的实时防护、显卡驱动的游戏内覆盖层,再装一次;
  • 还不行就手动装:everestapi.github.io 下 Everest 的 Main.zip,解压到游戏目录,再跑 MiniInstaller-win64.exe。

接在两处(下载安装 + 本地包安装),沿用已有的 with_file_in_use_hint(#89)的写法;其他安装失败原样返回,不做任何改写。

验证

  • cargo test --lib:116 passed, 0 failed(新增 3 个:英文栈 / 中文栈都会补提示、原始错误保持在最前;正常的安装失败不被改写)。
  • cargo fmt 已跑,无额外改动。

没做 / 留给后面

  • 没有在安装前删 piton-runtime:InGameUpdaterHelper 只在自己更新(everest-update 目录)时才删,CeleMod 的正常安装是一层层覆盖解压。是否要顺手清掉这份运行时值得单独讨论(删早了、解压又失败的话游戏会连运行时都没有),先不动。
  • 没改用户的处理流程本身(删 Mods/Cache、Steam 校验后重装等)——那些已经在给用户的手册里。

来源:Celeste Coders 群用户报告。

MiniInstaller 有时不是报安装错误,而是直接死在 .NET 运行时自己的原生代码里
(AccessViolationException 打在 relinker 的 String.StartsWith → Globalization
P/Invoke 上)。这段栈对用户没有用,还会让人以为游戏或 CeleMod 坏了。

CELEMOD 侧确认过的事实:MiniInstaller 的 StandardMode 是在本进程里单线程调
MonoMod 的(DepCalls.RunMonoMod → MonoModder.AutoPatch),MonoModder 自己没有
任何 Parallel/Task,官方 Everest 也没有并行 relink/并行加载开关(那两个
EVEREST_PARALLEL_* 是 EverestUltra 的),所以这次崩溃不是重链竞态,而是进程里
被别的东西打坏了原生内存/运行时:常见是杀毒软件、显卡驱动的游戏内覆盖层之类
注入到每个进程的东西,或者游戏目录里那份 piton-runtime 被打坏。

安装失败时按栈里的特征串认出这类崩溃,在原始错误后面补一条能照做的建议
(先关实时防护和游戏内覆盖层再装一次;不行就手动解压 Main.zip 跑
MiniInstaller-win64.exe),本地包安装走同一个提示。其他安装失败原样返回。
用户反馈这类崩溃是偶发的:同一台机器、同样的步骤,之前成功过一次。所以它的
性质是「偶发 + 崩在运行时原生层」,不是确定性的环境损坏。

- everest.rs:抽出 extract_everest_archive,安装失败且错误串带原生崩溃特征
  (is_installer_native_crash:AccessViolationException / 受保护内存中英文)时,
  重新解压一遍再跑一次 MiniInstaller(崩溃那次会把游戏目录留在半装状态,不
  重解压直接重跑不安全),只重试一次,两次都崩才把错误抛给用户。
- everest.rs:在 Windows 上给安装器子进程设 DOTNET_SYSTEM_GLOBALIZATION_USENLS=1。
  MonoMod 在 FindType 里用文化相关的 String.StartsWith 比依赖名,.NET 在非 en*
  区域设置下会走 ICU(dotnet/runtime#132702 是同一个栈,运行时团队判定该路径是
  外部堆破坏的最早受害者);让安装器进程用 NLS 只影响这些 ASCII 前缀的比较结果
  (完全一致),对子进程来说是安全的默认值。
- backend.rs:提示文案改成「已经自动重装过一遍还是崩」,并把重试放在第一位。

cargo test --lib: 119 passed(新增 3 个:崩溃特征识别、失败后确实重试并成功、连续崩两次不循环)。
@std-external

Copy link
Copy Markdown
Contributor Author

追加(用户补了关键信息:这条崩溃是偶发的)

「同一台机器、同样的步骤之前成功过一次」→ 说明不是确定性的环境损坏,而 CelebrMod 这边能兜的正好是「偶发」:失败自动重试一次。顺带找到了同栈的上游报告。

新增改动(第二个提交)

  1. 失败自动重试一次:everest.rs::install_everest_archive_with_steps —— 错误串命中 is_installer_native_crash(AccessViolationException / 中英文「受保护的内存」)时,重新解压 + 再跑一遍 MiniInstaller(崩溃那次会把游戏目录留在半装状态,不重新解压直接重跑不安全),两次都崩才把错误交给用户。抽出 extract_everest_archive 复用解压逻辑。
  2. 安装器子进程避开 ICU:Windows 上给 MiniInstaller 子进程设 DOTNET_SYSTEM_GLOBALIZATION_USENLS=1(INSTALLER_GLOBALIZATION_ENV)。MonoMod 在 FindType 里用文化敏感的 String.StartsWith 比依赖名,非 en* 区域设置下 .NET 会走 ICU —— dotnet/runtime#132702 是完全同一条栈(也是那个用户遇到的坑:中文 Windows 正好落进这条路径;运行时团队判定该路径是最热的分配点、是外部堆破坏的最早受害者而非元凶)。让安装器进程改用 NLS 只影响这些 ASCII 前缀的比较实现,结果完全一致。
  3. 提示文案改成「已自动重装过一次还是崩」,把「再点一次」放在最前面。

澄清一点:并行 relink 这条不适用

官方 Everest 和 MiniInstaller 里没有 EVEREST_PARALLEL_RELINK / EVEREST_PARALLEL_LOAD(那两个是 EverestUltra 的,Everest.Relinker.cs:39、Everest.Loader.cs:184),而且安装阶段 MiniInstaller 是在本进程里同步调 MonoMod(DepCalls.RunMonoMod → MonoModder.AutoPatch,上游 MonoModder 无任何 Parallel/Task)。用户提的「设 EVEREST_PARALLEL_*_RELINK=0 再装」在官方 Everest 上是个 no-op,所以没有照做。

验证

  • cargo test --lib:119 passed, 0 failed(新增 3 个:崩溃特征识别、失败后确实重试并成功、连续崩两次不无限循环)。
  • cargo fmt 已跑;cargo clippy --lib 对改动文件无告警(仓库原有 7 条告警在别处)。

上游情况

  • MonoMod / Everest:没有同栈 issue(Everest #328 是老 issue,不是这个);两侧都没提 PR。
  • 若上游想根治:Everest 侧给 MiniInstaller.csproj 加 <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />;MonoMod 侧 MonoModder.cs:1117 的 StartsWith 应改用 StringComparison.Ordinal(拿文化敏感比较比装配名本来就不对)。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant