Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/desktop/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include <nlohmann/json.hpp>

#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <mutex>
Expand Down Expand Up @@ -316,6 +317,16 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
#else
int main(int, char**) {
#endif
// 顶层 try/catch:wWinMain 是 Windows 子系统的 EXE 入口,任何未捕获的
// C++ 异常会触发 std::terminate → 系统弹"未经处理的异常"调试器对话框,
// 普通用户既看不懂也无从下手。把整个 body 包成 IIFE lambda 让 catch 兜
// 底:落 LOG_ERROR、弹中文 MessageBox 给用户可执行的修复指引,然后
// return 1 走正常退出。
//
// 注意 logger 初始化也在 lambda 内,因为它本身也可能抛(磁盘满 / 路径
// 受 GPO 锁)。catch 里依然先调 LOG_ERROR(失败时是 no-op,不影响
// MessageBox 给用户提示)。
auto run = []() -> int {
using namespace acecode::desktop;

// desktop 自己的日志路径: ~/.acecode/logs/desktop-<date>.log。和 daemon
Expand Down Expand Up @@ -1068,4 +1079,45 @@ int main(int, char**) {

auto failures = pool.stop_all();
return failures.empty() ? 0 : 100; // 部分失败返回非零便于诊断
}; // end of run lambda

try {
return run();
} catch (const std::exception& e) {
LOG_ERROR(std::string("[desktop] unhandled exception during startup: ") + e.what());
#ifdef _WIN32
const std::string body = std::string(
"ACECode 桌面版启动时遇到未预期的错误,即将退出。\n\n"
"请把以下日志文件发给 IT/开发以便定位:\n"
" %USERPROFILE%\\.acecode\\logs\\desktop-*.log\n\n"
"异常信息:\n") + e.what();
const int wlen = ::MultiByteToWideChar(CP_UTF8, 0, body.c_str(),
static_cast<int>(body.size()), nullptr, 0);
std::wstring wbody;
if (wlen > 0) {
wbody.resize(static_cast<std::size_t>(wlen));
::MultiByteToWideChar(CP_UTF8, 0, body.c_str(),
static_cast<int>(body.size()), wbody.data(), wlen);
}
::MessageBoxW(nullptr,
wbody.empty() ? L"Unhandled exception during startup." : wbody.c_str(),
L"ACECode 启动失败",
MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
#else
std::fprintf(stderr, "[desktop] unhandled exception: %s\n", e.what());
#endif
return 1;
} catch (...) {
LOG_ERROR("[desktop] unhandled non-std::exception during startup");
#ifdef _WIN32
::MessageBoxW(nullptr,
L"ACECode 桌面版启动时遇到未预期的错误,即将退出。\n"
L"请把日志文件 %USERPROFILE%\\.acecode\\logs\\desktop-*.log 发给 IT 团队。",
L"ACECode 启动失败",
MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
#else
std::fprintf(stderr, "[desktop] unhandled non-std::exception during startup\n");
#endif
return 1;
}
}
101 changes: 89 additions & 12 deletions src/desktop/web_host.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "web_host.hpp"

#include "web_host_close_policy.hpp"
#include "webview2_runtime_probe.hpp"
#include "window_chrome.hpp"

#include "../utils/logger.hpp"
Expand Down Expand Up @@ -296,6 +297,37 @@ void center_window_on_monitor(HWND hwnd, const RECT& monitor) {
::SetWindowPos(hwnd, nullptr, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE);
}

// 终态失败弹窗:WebView2 默认路径 + Edge 浏览器 fallback 都失败时,给用户
// 一个可读中文提示(原本是 wWinMain 上面那个"未经处理的异常"调试器对话框,
// 普通用户看不懂也帮不上忙)。reason 透传 webview::exception::what(),通常
// 含 HRESULT;接进 MessageBox 文案末尾,IT 排查时直接复制就行。
void show_webview2_failure_message_box(const char* reason) {
const std::string body =
"ACECode 桌面版无法初始化 WebView2 组件。\n\n"
"可能的原因与解决办法:\n"
" 1. 未安装 \"Microsoft Edge WebView2 Runtime\"(注意:仅有 Edge 浏览器并不等价)。\n"
" 请到 https://developer.microsoft.com/microsoft-edge/webview2/ 下载 Evergreen Standalone Installer 安装。\n"
" 2. WebView2 用户数据目录损坏。请尝试删除以下目录后重试:\n"
" %LOCALAPPDATA%\\acecode-desktop\\EBWebView\n"
" 3. 杀毒/EDR 软件拦截了 msedgewebview2.exe 的启动,请将其加入信任。\n\n"
"详细日志:%USERPROFILE%\\.acecode\\logs\\desktop-*.log\n\n"
"失败原因(供 IT 排查):\n";
std::string full = body + (reason ? reason : "(unknown)");

const int wlen = ::MultiByteToWideChar(CP_UTF8, 0, full.c_str(),
static_cast<int>(full.size()), nullptr, 0);
std::wstring wbody;
if (wlen > 0) {
wbody.resize(static_cast<std::size_t>(wlen));
::MultiByteToWideChar(CP_UTF8, 0, full.c_str(),
static_cast<int>(full.size()), wbody.data(), wlen);
}
::MessageBoxW(nullptr,
wbody.empty() ? L"WebView2 initialization failed." : wbody.c_str(),
L"ACECode 启动失败",
MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
}

} // namespace

struct ComApartment {
Expand Down Expand Up @@ -348,20 +380,65 @@ struct WebHost::Impl {
: nullptr),
offscreen_until_ready(custom_window != nullptr),
com(custom_window != nullptr) {
// 三段式构造:
// (1) 默认 Loader 路径,优先 offscreen custom_window;失败 → 切
// 自管 nullptr 父窗口再试一次(沿用现有降级)。
// (2) (1) 整段还是抛 → 探测 Edge 浏览器自带的 msedgewebview2.exe
// 目录,通过 WEBVIEW2_BROWSER_EXECUTABLE_FOLDER 环境变量
// (WebView2Loader.dll 公开的覆盖钩子)指过去再试。
// (3) Edge fallback 仍失败或没找到 Edge → 弹中文 MessageBox 给用户
// 可执行的修复指引,LOG_ERROR 落盘后 ExitProcess(1) 而不是
// 让 webview::exception 一路裸抛到 wWinMain — 那样普通用户
// 只会看到 Windows "未经处理的异常" 调试器对话框,完全看不懂。
auto make_webview_default_path = [&]() -> std::unique_ptr<webview::webview> {
try {
return std::make_unique<webview::webview>(
debug,
custom_window ? static_cast<void*>(custom_window) : nullptr);
} catch (const webview::exception& e) {
if (!custom_window) throw;
LOG_WARN(std::string("[desktop] offscreen WebView host failed; "
"falling back to default window: ") + e.what());
if (::IsWindow(custom_window)) {
::DestroyWindow(custom_window);
}
custom_window = nullptr;
offscreen_until_ready = false;
return std::make_unique<webview::webview>(debug, nullptr);
}
};

try {
w = std::make_unique<webview::webview>(
debug,
custom_window ? static_cast<void*>(custom_window) : nullptr);
} catch (const webview::exception& e) {
if (!custom_window) throw;
LOG_WARN(std::string("[desktop] offscreen WebView host failed; falling back: ") +
e.what());
if (::IsWindow(custom_window)) {
::DestroyWindow(custom_window);
w = make_webview_default_path();
} catch (const webview::exception& e1) {
LOG_WARN(std::string("[desktop] WebView2 default loader path failed: ") +
e1.what());
auto edge_folder = find_edge_browser_folder();
if (!edge_folder.has_value()) {
LOG_ERROR("[desktop] no Microsoft Edge browser folder found to "
"fall back to; aborting startup");
show_webview2_failure_message_box(e1.what());
::ExitProcess(1);
}
const std::wstring folder_w = edge_folder->wstring();
LOG_INFO(std::string("[desktop] retrying WebView2 with Edge browser "
"folder: ") + edge_folder->string());
if (!::SetEnvironmentVariableW(L"WEBVIEW2_BROWSER_EXECUTABLE_FOLDER",
folder_w.c_str())) {
LOG_ERROR("[desktop] SetEnvironmentVariableW("
"WEBVIEW2_BROWSER_EXECUTABLE_FOLDER) failed, last_error=" +
std::to_string(::GetLastError()));
}
try {
// custom_window 在 default 路径内已被清掉(如果走过 offscreen),
// 这里直接喂 nullptr 让 webview 自己造窗口最稳妥。
w = std::make_unique<webview::webview>(debug, nullptr);
} catch (const webview::exception& e2) {
LOG_ERROR(std::string("[desktop] WebView2 Edge browser folder "
"fallback also failed: ") + e2.what());
show_webview2_failure_message_box(e2.what());
::ExitProcess(1);
}
custom_window = nullptr;
offscreen_until_ready = false;
w = std::make_unique<webview::webview>(debug, nullptr);
}
if (custom_window) {
resize_webview_widget(custom_window);
Expand Down
174 changes: 174 additions & 0 deletions src/desktop/webview2_runtime_probe.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// WebView2 Runtime 探测实现。设计 + 调用时机见 webview2_runtime_probe.hpp。

#include "webview2_runtime_probe.hpp"

#include "../utils/logger.hpp"

#include <array>
#include <cstdint>
#include <string>
#include <system_error>

#ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <shlobj.h>
# include <knownfolders.h>
#endif

namespace fs = std::filesystem;

namespace acecode::desktop {

namespace {

constexpr const char* kEdgeWebViewExecutableName = "msedgewebview2.exe";

// 把版本号字符串("100.0.1234.56")拆 4 段非负整数。失败返回 std::nullopt
// (整段不是 4 位、含非数字字符、任何一段超过 uint32 上限都视为非版本)。
std::optional<std::array<std::uint32_t, 4>> parse_version_4(const std::string& s) {
std::array<std::uint32_t, 4> out{0, 0, 0, 0};
std::size_t segment = 0;
std::uint64_t acc = 0;
bool any_digit_in_segment = false;
for (std::size_t i = 0; i <= s.size(); ++i) {
const bool at_end = (i == s.size());
const char c = at_end ? '.' : s[i];
if (c == '.') {
if (!any_digit_in_segment) return std::nullopt;
if (segment >= 4) return std::nullopt;
if (acc > 0xFFFFFFFFu) return std::nullopt;
out[segment] = static_cast<std::uint32_t>(acc);
++segment;
acc = 0;
any_digit_in_segment = false;
continue;
}
if (c < '0' || c > '9') return std::nullopt;
acc = acc * 10 + static_cast<std::uint64_t>(c - '0');
if (acc > 0xFFFFFFFFu) return std::nullopt;
any_digit_in_segment = true;
}
if (segment != 4) return std::nullopt;
return out;
}

// Edge 浏览器标准布局 <root>/Microsoft/Edge/Application/。我们只用这一条
// 路径 — Edge Beta/Dev/Canary 各有自己的 root,但同事场景下用户装的就是
// 正式版 Edge,本期不扫 Beta/Dev/Canary,避免误用 Canary 这种快速变化通道。
fs::path edge_application_dir(const fs::path& root) {
return root / "Microsoft" / "Edge" / "Application";
}

// 在一个 Application 目录下扫所有合法版本子目录,挑最大版本 + 验证
// msedgewebview2.exe 存在。返回 (version, folder),无候选返回 nullopt。
std::optional<std::pair<std::array<std::uint32_t, 4>, fs::path>>
pick_latest_in_application_dir(const fs::path& application_dir) {
std::error_code ec;
if (!fs::is_directory(application_dir, ec) || ec) {
return std::nullopt;
}

std::optional<std::array<std::uint32_t, 4>> best_version;
fs::path best_folder;

fs::directory_iterator it(application_dir, fs::directory_options::skip_permission_denied, ec);
if (ec) return std::nullopt;

for (const auto& entry : it) {
std::error_code ec_entry;
if (!entry.is_directory(ec_entry) || ec_entry) continue;

const std::string name = entry.path().filename().string();
auto parsed = parse_version_4(name);
if (!parsed.has_value()) continue;

const fs::path candidate_exe = entry.path() / kEdgeWebViewExecutableName;
std::error_code exe_ec;
if (!fs::is_regular_file(candidate_exe, exe_ec) || exe_ec) {
// Edge 浏览器某些 channel 切换时会保留只剩 Resources 的旧版本号
// 目录,但没了 msedgewebview2.exe — 直接跳过,免得后面 set env
// 指向死路径。
continue;
}

if (!best_version.has_value() || *parsed > *best_version) {
best_version = parsed;
best_folder = entry.path();
}
}

if (!best_version.has_value()) return std::nullopt;
return std::make_pair(*best_version, best_folder);
}

} // namespace

std::optional<fs::path> find_edge_browser_folder_in(
const std::vector<fs::path>& roots) {
std::optional<std::array<std::uint32_t, 4>> best_version;
fs::path best_folder;

for (const auto& root : roots) {
if (root.empty()) continue;
auto pick = pick_latest_in_application_dir(edge_application_dir(root));
if (!pick.has_value()) continue;

if (!best_version.has_value() || pick->first > *best_version) {
best_version = pick->first;
best_folder = pick->second;
}
}

if (!best_version.has_value()) return std::nullopt;
return best_folder;
}

#ifdef _WIN32
namespace {

// SHGetKnownFolderPath 拿 KNOWNFOLDERID 对应路径。失败返回空 path。COM
// 不需要 init —— SHGetKnownFolderPath 走的是 NTDLL 路径不依赖 apartment。
fs::path known_folder_path(REFKNOWNFOLDERID id) {
PWSTR raw = nullptr;
HRESULT hr = ::SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, nullptr, &raw);
if (FAILED(hr) || !raw) {
if (raw) ::CoTaskMemFree(raw);
return {};
}
fs::path result(raw);
::CoTaskMemFree(raw);
return result;
}

} // namespace

std::optional<fs::path> find_edge_browser_folder() {
std::vector<fs::path> roots;
if (auto pf = known_folder_path(FOLDERID_ProgramFiles); !pf.empty()) {
roots.push_back(std::move(pf));
}
if (auto pfx86 = known_folder_path(FOLDERID_ProgramFilesX86); !pfx86.empty()) {
roots.push_back(std::move(pfx86));
}
if (roots.empty()) {
LOG_WARN("[webview2_probe] SHGetKnownFolderPath returned no ProgramFiles paths");
return std::nullopt;
}
return find_edge_browser_folder_in(roots);
}

#else // _WIN32

std::optional<fs::path> find_edge_browser_folder() {
return std::nullopt;
}

#endif // _WIN32

} // namespace acecode::desktop
Loading
Loading