fix: 修复部分用户打不开设置页,WebView2 缺失时仍可正常打字 (#801)

* fix(installer): VC++ 运行库要求提到 14.40 并给出直接下载链接

服务端程序用 MSVC 14.40+ 编译并导入 msvcp140.dll,配上更旧的运行库,
std::mutex 首次加锁即访问冲突、进程无声闪退,设置页因此打不开;原先
14.20 的门槛放过了装着 2019 / 早期 2022 运行库的机器。

- 缺失与「已装但过旧」分开提示,并显示当前版本
- 「是」直接下载 VC++ x64 运行库与 WebView2 Evergreen Bootstrapper
- 更正 WebView2 缺失的影响:只有设置页需要它,默认 D2D 候选窗照常打字

* fix(server): WebView2 不可用时设置页给出提示,候选窗退回 D2D

- 设置进程不再忽略 WebView2 environment / controller 创建失败(运行时缺失
  时同步失败、回调不来,界面会一直停在启动画面),改为弹框说明原因、错误码
  与下载直链后退出
- ui_backend = webview2 而检测不到运行时时,本次进程改用 D2D 渲染,保证能
  正常打字;不改配置,装好运行时后下次启动自动恢复
- WebView2 下载地址统一为 Evergreen Bootstrapper 直链
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-10-11 02:21:40 +08:00
1 parent 66e0a6dc76
commit 5aed0fcbf0
9 files changed
+136 -32

No files matched your search

+4 -3
View File
@@ -47,12 +47,13 @@ pwsh -File ./Prepare-PackageFiles.ps1 -TargetVersion 1.2.3 -RepoRoot .. `
## 运行时依赖检查
安装包不携带 WebView2 Runtime 和 VC 运行库——前者有自己的 Evergreen 更新通道,后者是系统级共享组件。但缺了任何一个,输入法装完就是坏的,而故障出现在安装结束之后(Server 起不来,或者候选窗一片空白),用户看不到任何解释。所以 `msime_setup.iss` 的 `InitializeSetup` 在安装开始前先查一遍,缺哪个就当场说清楚,并给出下载地址。
安装包不携带 WebView2 Runtime 和 VC 运行库——前者有自己的 Evergreen 更新通道,后者是系统级共享组件。缺 VC 运行库 Server 起不来;缺 WebView2 设置页打不开,但默认的 D2D 候选窗(以及表情 / 键盘 / 手写面板,都是 `msimeui` 原生绘制)照常可用,选了 `appearance.ui_backend = "webview2"` 的 Server 启动时检测不到运行时会临时退回 D2D(`FallBackToD2dIfWebView2RuntimeMissing`,不改配置,装好后下次启动自动恢复)。这些故障都出现在安装结束之后,用户看不到任何解释。所以 `msime_setup.iss` 的 `InitializeSetup` 在安装开始前先查一遍,缺哪个就当场说清楚,并给出下载地址。
- **Microsoft Edge WebView2 Runtime**:读 EdgeUpdate 客户端键 `{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}` 的 `pv`。机器级安装由 32 位的 EdgeUpdate 写入,所以主查 `HKLM32`,再看 `HKLM64` 和 `HKCU`(用户级安装)。`pv` 缺失或残留成 `0.0.0.0` 都算没装
- **Visual C++ 2015–2022 可再发行组件包(x64)**:读 `Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64` 的 `Installed` / `Major` / `Minor`,`HKLM32` 和 `HKLM64` 两个视图都查。要求 **14.20 以上**(VC++ 2019 起):`server_exe\` 里每个 exe 都导入了 `vcruntime140_1.dll`,它从那一版才开始随 redist 分发,只装了 2015 / 2017 版的机器同样有 `Installed=1`,却照样起不来。32 位 TSF DLL 用静态 CRT(`windows/CMakeLists.txt` 的 `CMAKE_MSVC_RUNTIME_LIBRARY`),所以不检查 x86 redist
- **Visual C++ 2015–2022 可再发行组件包(x64)**:读 `Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64` 的 `Installed` / `Major` / `Minor`,`HKLM32` 和 `HKLM64` 两个视图都查。要求 **14.40 以上**(常量 `VCRuntimeMinMinor`):`server_exe\` 里的 exe 用 MSVC 14.40+(VS 2022 17.10 起)编译并导入 `msvcp140.dll`,这一版起 `std::mutex` 的构造函数是 constexpr,配上更旧的 `msvcp140.dll` 会在第一次加锁时访问冲突、无声闪退——设置页「点了没反应」就是这个现象。装了 2019 / 早期 2022 运行库的机器同样有 `Installed=1`,所以必须比版本号。构建工具链升级到要求更高 redist 时同步调高这个常量。32 位 TSF DLL 用静态 CRT(`windows/CMakeLists.txt` 的 `CMAKE_MSVC_RUNTIME_LIBRARY`),所以不检查 x86 redist
- **不要改成查 `System32` 里的 DLL。** Setup.exe 是 32 位进程,Pascal Script 的 `FileExists` 会被 WOW64 重定向到 `SysWOW64`,查到的是 x86 运行库;只装了 x64 redist 的机器会被误判成缺少 x64 运行库。绕开重定向的办法不可移植:`EnableFsRedirection` 在 Inno Setup 7 里已被移除,而它的替代品在 CI 用的 6.x 里还不存在
- 缺组件时弹一次对话框:选「是」打开下载页并结束安装,选「否」继续装。**不阻断安装**——用户可能马上就要去补组件,也可能只是在准备环境。静默安装(CI、批量部署)用 `SuppressibleMsgBox` 默认「否」直接继续,缺什么已经写进安装日志(`/LOG=`)
- 缺组件或版本过旧时弹一次对话框,区分「没装」和「已装 x.y 但过旧」,列出直接下载链接(VC++:`https://aka.ms/vc14/vc_redist.x64.exe`;WebView2 Evergreen Bootstrapper:`https://go.microsoft.com/fwlink/p/?LinkId=2124703`)和对应的说明页:选「是」直接下载这些安装包并结束安装,选「否」继续装。**不阻断安装**——用户可能马上就要去补组件,也可能只是在准备环境。静默安装(CI、批量部署)用 `SuppressibleMsgBox` 默认「否」直接继续,缺什么已经写进安装日志(`/LOG=`)
- 安装时检查只是第一道关:用户可能选了「否」,也可能事后卸掉或损坏了 WebView2。所以设置进程在 WebView2 environment / controller 创建失败(含运行时缺失时的同步失败)时会弹框说明原因和错误码、给出同一条 WebView2 直链并退出,而不是留一个空白窗口
## 依赖
+45 -21
View File
@@ -200,9 +200,15 @@ const
下载页也统一给简体中文版,省得用户落到英文页上再自己切。}
WebView2DownloadUrl =
'https://developer.microsoft.com/zh-cn/microsoft-edge/webview2';
{ Evergreen Bootstrapper 直链,点开就是安装器。}
WebView2DirectUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
VCRuntimeKey = 'Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64';
VCRuntimeDownloadUrl =
'https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redist?view=msvc-170';
{ 微软给的「最新受支持 v14 x64 运行库」固定直链。}
VCRuntimeDirectUrl = 'https://aka.ms/vc14/vc_redist.x64.exe';
{ 见 VCRuntimeInstalledInView 上方的说明。}
VCRuntimeMinMinor = 40;
var
VersionDirName: String;
@@ -215,9 +221,10 @@ var
DataDirAdoptedEmpty: Boolean;
{ WebView2 Runtime 与 VC 运行库都不随包分发:前者有自己的 Evergreen 更新通道,
后者是系统级共享组件,安装器不该替用户装。但缺了任何一个,输入法装完就是坏的,
而故障现场在安装结束之后——Server 直接起不来,或者候选窗一片空白,用户看不到
任何解释。所以在安装开始前查一遍,把原因当场说清楚。
后者是系统级共享组件,安装器不该替用户装。缺 VC 运行库 Server 直接起不来;
缺 WebView2 设置页打不开(默认 D2D 候选窗照常可用,选了 WebView2 渲染的
Server 启动时会临时退回 D2D)。故障现场都在安装结束之后,用户看不到任何解释,
所以在安装开始前查一遍,把原因当场说清楚。
返回空串表示没装;否则是 pv 里的版本号。}
function ReadWebView2Version: String;
@@ -244,9 +251,11 @@ end;
// {sysnative} 对 64 位安装器又不可用。注册表访问没有这个问题,HKLM32 / HKLM64
// 两个视图都是显式指定的。
//
// x64 redist 的安装状态就记在这个键里。要求 14.20 以上(VC++ 2019 起):server_exe
// 里每个 exe 都导入了 vcruntime140_1.dll,而它是从那一版才开始随 redist 分发的,
// 只装了 2015/2017 版的机器同样有 Installed=1,却照样起不来。
// x64 redist 的安装状态就记在这个键里。要求 14.40 以上:server_exe 里的 exe 都用
// MSVC 14.40+(VS 2022 17.10 起)编译并导入 msvcp140.dll,这一版起 std::mutex 的
// 构造函数改成了 constexpr,配上 14.40 之前的 msvcp140.dll 会在第一次加锁时访问冲突,
// 进程无声闪退(设置页打不开就是这个样子)。装了 2019 / 早期 2022 运行库的机器
// 同样有 Installed=1,所以必须比版本号,不能只看装没装。
// 32 位 TSF DLL 用的是静态 CRT(windows/CMakeLists.txt 里的
// CMAKE_MSVC_RUNTIME_LIBRARY),所以不必检查 x86 redist。
// 注意:Inno Setup 的花括号注释不能嵌套,上面提到的常量会提前闭合 { } 注释,
@@ -262,7 +271,7 @@ begin
(Installed = 1) and
RegQueryDWordValue(RootKey, VCRuntimeKey, 'Major', Major) and
RegQueryDWordValue(RootKey, VCRuntimeKey, 'Minor', Minor) and
((Major > 14) or ((Major = 14) and (Minor >= 20)));
((Major > 14) or ((Major = 14) and (Minor >= VCRuntimeMinMinor)));
end;
{ redist 的安装包是 32 位的,键通常落在 WOW6432Node 下,但较新的版本两个视图都写,
@@ -273,7 +282,7 @@ begin
VCRuntimeInstalledInView(HKLM32) or VCRuntimeInstalledInView(HKLM64);
end;
{ 只用于写日志,不参与判定。}
{ 只用于日志和提示文字,不参与判定。}
function ReadVCRuntimeVersion: String;
begin
Result := '';
@@ -298,6 +307,7 @@ var
WebView2Version: String;
NeedsWebView2: Boolean;
NeedsVCRuntime: Boolean;
VCRuntimeVersion: String;
Prompt: String;
begin
Result := True;
@@ -310,29 +320,43 @@ begin
Log('WebView2 Runtime found: ' + WebView2Version);
NeedsVCRuntime := not VCRuntimeIsInstalled;
{ 有版本号却没通过判定,就是装了但太旧,提示里要和「没装」分开说。}
VCRuntimeVersion := ReadVCRuntimeVersion;
if NeedsVCRuntime then
Log('Prerequisite missing: Visual C++ 2015-2022 Redistributable (x64).')
Log('Prerequisite missing or outdated: Visual C++ 2015-2022 Redistributable (x64), found "' +
VCRuntimeVersion + '", need 14.' + IntToStr(VCRuntimeMinMinor) + ' or later.')
else
Log('Visual C++ runtime found: ' + ReadVCRuntimeVersion);
Log('Visual C++ runtime found: ' + VCRuntimeVersion);
if not (NeedsWebView2 or NeedsVCRuntime) then
exit;
Prompt := '这台电脑缺少输入法运行所需的系统组件:' + #13#10;
Prompt := '这台电脑缺少输入法用到的系统组件,或版本过旧:' + #13#10;
if NeedsVCRuntime then
begin
Prompt := Prompt + #13#10 +
'● Visual C++ 2015-2022 可再发行组件包(x64)' + #13#10 +
' 缺少它输入法主程序根本无法启动(提示找不到 VCRUNTIME140.dll 之类)。' + #13#10 +
' 下载:' + VCRuntimeDownloadUrl + #13#10 +
' 直链:https://aka.ms/vs/17/release/vc_redist.x64.exe' + #13#10;
'● Visual C++ 2015-2022 可再发行组件包(x64),需要 14.' +
IntToStr(VCRuntimeMinMinor) + ' 或更高版本' + #13#10;
if VCRuntimeVersion <> '' then
Prompt := Prompt +
' 当前已安装 ' + VCRuntimeVersion + ',版本过旧,设置页等程序会启动即闪退。' + #13#10
else
Prompt := Prompt +
' 缺少它输入法主程序根本无法启动(提示找不到 VCRUNTIME140.dll 之类)。' + #13#10;
Prompt := Prompt +
' 直接下载:' + VCRuntimeDirectUrl + #13#10 +
' 说明页面:' + VCRuntimeDownloadUrl + #13#10;
end;
if NeedsWebView2 then
Prompt := Prompt + #13#10 +
'● Microsoft Edge WebView2 Runtime' + #13#10 +
' 缺少它候选窗口、设置页和表情 / 手写 / 键盘面板都打不开。' + #13#10 +
' 下载:' + WebView2DownloadUrl + #13#10;
' 缺少它设置页面打不开。默认的 D2D 候选窗不依赖它,打字不受影响;' + #13#10 +
' 选了 WebView2 渲染的候选窗会临时改用 D2D,装好后自动恢复。' + #13#10 +
' 直接下载:' + WebView2DirectUrl + #13#10 +
' 说明页面:' + WebView2DownloadUrl + #13#10;
Prompt := Prompt + #13#10 +
'点击「是」打开下载页面并结束本次安装,装好组件后重新运行本安装程序;' + #13#10 +
'点击「否」继续安装(装完仍需自行补齐上述组件,输入法才能正常工作)。';
'点击「是」直接下载上述组件并结束本次安装,装好后重新运行本安装程序;' + #13#10 +
'点击「否」继续安装(装完仍需自行补齐上述组件,对应功能才能正常使用)。';
{ 静默安装(CI、企业批量部署)不该被一个对话框卡住:默认继续,缺什么已经写进日志了。}
if
@@ -340,9 +364,9 @@ begin
then
begin
if NeedsVCRuntime then
OpenDownloadPage(VCRuntimeDownloadUrl);
OpenDownloadPage(VCRuntimeDirectUrl);
if NeedsWebView2 then
OpenDownloadPage(WebView2DownloadUrl);
OpenDownloadPage(WebView2DirectUrl);
Result := False;
end;
end;
+3
View File
@@ -535,6 +535,9 @@ const std::string &GetConfiguredUiBackend();
bool SetConfiguredUiBackend(const std::string &backend);
// Process-lifetime renderer chosen at InitImeConfig. Independent of later reloads.
bool UseD2dSmallWindowUi();
// Switch this process to the D2D renderer without touching the configured value, so the user's
// choice comes back on the next start. Only valid before the candidate window is created.
void FallBackToD2dSmallWindowUi();
// "fluent" | "wechat" | "graphite" | "willow_green" | "autumn_osmanthus" | "microsoft"
// — candidate-window and floating-toolbar skin.
const std::string &GetConfiguredCandidateSkin();
@@ -231,6 +231,11 @@ bool UseD2dSmallWindowUi()
UiBackendPolicy::Backend::Native;
}
void FallBackToD2dSmallWindowUi()
{
g_ui_backend_active = "d2d";
}
const std::string &GetConfiguredCandidateSkin()
{
return g_candidate_skin;
+1
View File
@@ -116,6 +116,7 @@ int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_ HINSTANCE /*hPrevInstance*/,
// Initialize config
InitImeConfig();
FallBackToD2dIfWebView2RuntimeMissing();
VoiceInput::Initialize();
Global::candidate_ui.page_size = GetConfiguredCandidatePageSize();
+47 -6
View File
@@ -125,6 +125,36 @@ void TraceStartup(const char *stage)
}
}
// WebView2 起不来时设置窗口只剩一块空白(运行时缺失时同步失败、回调根本不来,splash 会一直转),
// 用户看不出是怎么回事。把原因和下载地址说清楚,然后退出,不留一个空窗口。
void ReportWebViewStartupFailure(HWND hwnd, const wchar_t *stage, HRESULT hr)
{
static bool reported = false;
if (reported)
return;
reported = true;
TraceStartup("ui: webview startup failed");
SettingsSplash::Dismiss();
wchar_t message[1024]{};
swprintf_s(message,
L"设置页面需要 Microsoft Edge WebView2 Runtime,但它没能启动(%s,错误码 0x%08X)。\r\n"
L"The settings page needs the Microsoft Edge WebView2 Runtime, but it could not start "
L"(%s, error 0x%08X).\r\n\r\n"
L"输入法打字不受影响,只有设置页面需要它。\r\n"
L"Typing is not affected; only the settings page needs it.\r\n\r\n"
L"请安装或修复 WebView2 Runtime 后重新打开设置:\r\n"
L"Install or repair the WebView2 Runtime, then open the settings again:\r\n"
L"%s\r\n\r\n"
L"是否现在打开下载链接?\r\nOpen the download link now?",
stage, static_cast<unsigned>(hr), stage, static_cast<unsigned>(hr),
CommonUtils::kWebView2RuntimeDownloadUrl);
if (MessageBoxW(hwnd, message, L"水杉输入法 / Metasequoia IME", MB_YESNO | MB_ICONERROR | MB_SETFOREGROUND) ==
IDYES)
ShellExecuteW(hwnd, L"open", CommonUtils::kWebView2RuntimeDownloadUrl, nullptr, nullptr, SW_SHOWNORMAL);
DestroyWindow(hwnd);
}
nlohmann::json CandidateColorsToJson(const CandidateSkinCatalog::CandidateColors &colors)
{
nlohmann::json json = nlohmann::json::object();
@@ -1737,31 +1767,42 @@ void InitWebView(HWND hwnd)
L"--disable-sync "
L"--disable-prompt-on-repost "
L"--no-first-run");
CreateCoreWebView2EnvironmentWithOptions(
const HRESULT create_result = CreateCoreWebView2EnvironmentWithOptions(
nullptr, user_data.c_str(), options.Get(),
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[hwnd](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
TraceStartup("ui: environment created");
if (FAILED(result) || !environment)
{
SettingsSplash::Dismiss();
return FAILED(result) ? result : E_FAIL;
const HRESULT hr = FAILED(result) ? result : E_FAIL;
ReportWebViewStartupFailure(hwnd, L"environment", hr);
return hr;
}
// ICoreWebView2Environment3 需要较新的运行时,过旧的 WebView2 会停在这里。
ComPtr<ICoreWebView2Environment3> environment3;
if (FAILED(environment->QueryInterface(IID_PPV_ARGS(&environment3))))
{
SettingsSplash::Dismiss();
ReportWebViewStartupFailure(hwnd, L"environment3", E_NOINTERFACE);
return E_NOINTERFACE;
}
return environment3->CreateCoreWebView2CompositionController(
const HRESULT controller_request = environment3->CreateCoreWebView2CompositionController(
hwnd,
Callback<ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler>(
[hwnd](HRESULT controller_result, ICoreWebView2CompositionController *controller) -> HRESULT {
return OnControllerCreated(hwnd, controller_result, controller);
const HRESULT hr = OnControllerCreated(hwnd, controller_result, controller);
if (FAILED(hr))
ReportWebViewStartupFailure(hwnd, L"controller", hr);
return hr;
})
.Get());
if (FAILED(controller_request))
ReportWebViewStartupFailure(hwnd, L"controller", controller_request);
return controller_request;
})
.Get());
// 运行时缺失时这里同步返回失败,上面的回调不会被调用。
if (FAILED(create_result))
ReportWebViewStartupFailure(hwnd, L"environment", create_result);
}
UINT32 MouseKeys(WPARAM w_param)
+2
View File
@@ -12,6 +12,8 @@ void SendImeInputs(std::wstring words);
namespace CommonUtils
{
// Evergreen Bootstrapper 的官方直链,点开即下载安装器,用户不用在文档页里自己找。
inline constexpr wchar_t kWebView2RuntimeDownloadUrl[] = L"https://go.microsoft.com/fwlink/p/?LinkId=2124703";
std::wstring get_local_appdata_path_w();
std::wstring get_ime_data_path_w();
// Directory holding config.toml. Same as the data directory unless METASEQUOIA_IME_CONFIG_DIR
+26 -2
View File
@@ -494,9 +494,9 @@ void ReportSmallWindowWebviewUnavailable(HRESULT last_hr, SmallWindowWebviewFail
L"shown for now: {}.\r\n\r\n"
L"请安装或修复 Microsoft Edge WebView2 Runtime 后重新登录:\r\n"
L"Install or repair the Microsoft Edge WebView2 Runtime, then sign in again:\r\n"
L"https://developer.microsoft.com/microsoft-edge/webview2/\r\n\r\n"
L"{}\r\n\r\n"
L"错误码 / error: {:#x}",
hosts_zh, hosts_en, static_cast<unsigned>(last_hr))
hosts_zh, hosts_en, CommonUtils::kWebView2RuntimeDownloadUrl, static_cast<unsigned>(last_hr))
: fmt::format(L"WebView2 运行时已启动,但{}没能创建,暂时无法显示。\r\n"
L"The WebView2 Runtime started, but the following could not be created and cannot be "
L"shown for now: {}.\r\n\r\n"
@@ -1292,6 +1292,30 @@ void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd)
BeginSmallWindowWebviewEnvironmentCreate();
}
// The WebView2 hosts are shown cloaked and only uncloak after their first paint, so without a
// Runtime the candidate window never appears and the IME cannot be used at all. The D2D renderer
// has no such dependency. The configured value is left alone: once the Runtime is installed, the
// next Server start goes back to WebView2 on its own.
void FallBackToD2dIfWebView2RuntimeMissing()
{
if (UseD2dSmallWindowUi())
{
return;
}
LPWSTR version = nullptr;
const HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version);
const bool available = SUCCEEDED(hr) && version && version[0] != L'\0';
CoTaskMemFree(version);
if (available)
{
return;
}
FallBackToD2dSmallWindowUi();
TraceSmallWindowWebview(fmt::format(L"edge webview runtime not found (hr={:#x}); ui_backend=webview2 falls back "
L"to d2d for this session",
static_cast<unsigned>(hr)));
}
bool PrepareTrayMenuWebviewForShow()
{
if (TrayMenuPresenter::Instance().IsBound())
+3
View File
@@ -102,6 +102,9 @@ void InflateCandWnd(const CandidateWindowContent &content, std::function<void()>
std::pair<double, double> LastCandidateSlotMeasuredSize();
void InflateMeasureDivCandWnd(const CandidateWindowContent &content, std::function<void()> onComplete = nullptr);
void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd);
// appearance.ui_backend = "webview2" but no usable WebView2 Runtime: run this process on the D2D
// renderer so typing still works. Call after InitImeConfig and before CreateCandidateWindow.
void FallBackToD2dIfWebView2RuntimeMissing();
void ShutdownWebviews();
void UpdateSmallWindowWebviewVisibility(HWND hwnd, bool visible);
// Kick (or retry) shared small-window WebView2 init. Returns true when the menu