From 89bd3f3af03feaa5847da9dcfc01e7650327f591 Mon Sep 17 00:00:00 2001 From: Levi Date: Wed, 11 Nov 2026 20:08:05 +0800 Subject: [PATCH] fix(windows): release FFmpeg memory and capture native crashes --- .github/workflows/test.yml | 3 + pkg/download/crash_log_test.go | 26 ++++ pkg/download/downloader.go | 5 +- pkg/download/engine/ffmpeg/README.md | 9 ++ pkg/download/engine/ffmpeg/memory.go | 32 +++++ pkg/download/engine/ffmpeg/memory_other.go | 7 ++ pkg/download/engine/ffmpeg/memory_test.go | 88 +++++++++++++ pkg/download/engine/ffmpeg/memory_windows.go | 70 +++++++++++ .../engine/ffmpeg/memory_windows_test.go | 54 ++++++++ pkg/download/engine/ffmpeg/runtime.go | 4 +- ui/flutter/windows/runner/CMakeLists.txt | 2 + .../windows/runner/CRASH_DIAGNOSTICS.md | 31 +++++ ui/flutter/windows/runner/crash_handler.cpp | 119 ++++++++++++++++++ ui/flutter/windows/runner/crash_handler.h | 12 ++ ui/flutter/windows/runner/flutter_window.cpp | 4 +- ui/flutter/windows/runner/main.cpp | 7 +- ui/flutter/windows/runner/win32_window.cpp | 30 +++-- 17 files changed, 486 insertions(+), 17 deletions(-) create mode 100644 pkg/download/crash_log_test.go create mode 100644 pkg/download/engine/ffmpeg/memory.go create mode 100644 pkg/download/engine/ffmpeg/memory_other.go create mode 100644 pkg/download/engine/ffmpeg/memory_test.go create mode 100644 pkg/download/engine/ffmpeg/memory_windows.go create mode 100644 pkg/download/engine/ffmpeg/memory_windows_test.go create mode 100644 ui/flutter/windows/runner/CRASH_DIAGNOSTICS.md create mode 100644 ui/flutter/windows/runner/crash_handler.cpp create mode 100644 ui/flutter/windows/runner/crash_handler.h diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 467044a6..9cdfc95e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -134,6 +134,9 @@ jobs: if: runner.os == 'macOS' with: flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Test Windows FFmpeg memory and stream lifecycle + if: runner.os == 'Windows' + run: go test -v -count=1 -timeout 180s ./pkg/download/engine/ffmpeg ./pkg/download/engine/inject/stream ./pkg/download/engine - name: Install Linux WebView dependencies if: runner.os == 'Linux' run: | diff --git a/pkg/download/crash_log_test.go b/pkg/download/crash_log_test.go new file mode 100644 index 00000000..a7594163 --- /dev/null +++ b/pkg/download/crash_log_test.go @@ -0,0 +1,26 @@ +package download + +import ( + "os" + "path/filepath" + "runtime/debug" + "testing" +) + +func TestLogPanicPreservesPreviousCrash(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "crash.log") + const previous = "previous crash traceback\n" + if err := os.WriteFile(path, []byte(previous), 0600); err != nil { + t.Fatal(err) + } + logPanic(dir) + t.Cleanup(func() { _ = debug.SetCrashOutput(nil, debug.CrashOptions{}) }) + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(data) != previous { + t.Fatalf("previous crash was overwritten: %q", data) + } +} diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 3f841157..94ecbc4f 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -2659,11 +2659,12 @@ func logPanic(logDir string) { if err := util.CreateDirIfNotExist(logDir); err != nil { return } - f, err := os.Create(filepath.Join(logDir, "crash.log")) + f, err := os.OpenFile(filepath.Join(logDir, "crash.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return } - debug.SetCrashOutput(f, debug.CrashOptions{}) + defer f.Close() // SetCrashOutput duplicates the descriptor. + _ = debug.SetCrashOutput(f, debug.CrashOptions{}) } func (d *Downloader) assignFetcherManager(task *Task) error { diff --git a/pkg/download/engine/ffmpeg/README.md b/pkg/download/engine/ffmpeg/README.md index 6c759699..348b7885 100644 --- a/pkg/download/engine/ffmpeg/README.md +++ b/pkg/download/engine/ffmpeg/README.md @@ -174,3 +174,12 @@ compilation memory, host buffers and JS allocations are additional to the linear-memory limit. Playback support depends on the selected codecs, not just the `.mp4` suffix. Platform execution and performance must be tested on target devices, especially mobile/32-bit devices. + +On 64-bit Windows, instance linear memory uses a reserved virtual address range with +pages committed as it grows. Module completion, errors and cancellation release +this range directly through `VirtualFree`, rather than waiting for Go's garbage +collector to reclaim large buffers. Growing an instance preserves its address +without copying the previous buffer. The 512 MiB limit is unchanged; reservation +does not commit that entire amount. Shared compilation remains resident after +the first merge, so process memory is not expected to return exactly to the +pre-FFmpeg baseline. This does not diagnose unrelated idle memory growth. diff --git a/pkg/download/engine/ffmpeg/memory.go b/pkg/download/engine/ffmpeg/memory.go new file mode 100644 index 00000000..57dee379 --- /dev/null +++ b/pkg/download/engine/ffmpeg/memory.go @@ -0,0 +1,32 @@ +package ffmpeg + +import ( + "context" + "sync" + + "github.com/tetratelabs/wazero/experimental" +) + +// The runtime does not always invoke Free when a module's start function traps. +// Keep host ownership until execution returns, including failed instantiation. +func withOwnedMemory(ctx context.Context, allocator experimental.MemoryAllocator) (context.Context, func()) { + var memories []*ownedLinearMemory + ctx = experimental.WithMemoryAllocator(ctx, experimental.MemoryAllocatorFunc( + func(cap, max uint64) experimental.LinearMemory { + memory := &ownedLinearMemory{LinearMemory: allocator.Allocate(cap, max)} + memories = append(memories, memory) + return memory + })) + return ctx, func() { + for _, memory := range memories { + memory.Free() + } + } +} + +type ownedLinearMemory struct { + experimental.LinearMemory + once sync.Once +} + +func (m *ownedLinearMemory) Free() { m.once.Do(m.LinearMemory.Free) } diff --git a/pkg/download/engine/ffmpeg/memory_other.go b/pkg/download/engine/ffmpeg/memory_other.go new file mode 100644 index 00000000..34e94757 --- /dev/null +++ b/pkg/download/engine/ffmpeg/memory_other.go @@ -0,0 +1,7 @@ +//go:build !windows || (!amd64 && !arm64) + +package ffmpeg + +import "context" + +func withLinearMemory(ctx context.Context) (context.Context, func()) { return ctx, func() {} } diff --git a/pkg/download/engine/ffmpeg/memory_test.go b/pkg/download/engine/ffmpeg/memory_test.go new file mode 100644 index 00000000..f1b5cc16 --- /dev/null +++ b/pkg/download/engine/ffmpeg/memory_test.go @@ -0,0 +1,88 @@ +package ffmpeg + +import ( + "context" + "encoding/hex" + "testing" + "time" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/experimental" +) + +// Verify the pinned runtime's allocator lifecycle on every development host: +// host-owned buffers must not be released before execution ends, or retained +// after failed instantiation, traps or cancellation. +func TestLinearMemoryModuleLifecycle(t *testing.T) { + testLinearMemoryModuleLifecycle(t, func(_, _ uint64) experimental.LinearMemory { + return &testLinearMemory{} + }) +} + +type testLinearMemory struct{ buffer []byte } + +func (m *testLinearMemory) Reallocate(size uint64) []byte { + m.buffer = append(m.buffer, make([]byte, int(size)-len(m.buffer))...) + return m.buffer +} +func (m *testLinearMemory) Free() { m.buffer = nil } + +type trackedLinearMemory struct { + experimental.LinearMemory + freeCount int +} + +func (m *trackedLinearMemory) Free() { + m.freeCount++ + m.LinearMemory.Free() +} + +func testLinearMemoryModuleLifecycle(t *testing.T, allocate experimental.MemoryAllocatorFunc) { + t.Helper() + for _, tt := range []struct { + name string + // A module with one memory (min=1/max=2 pages), optionally a start + // function that traps or loops until its context is cancelled. + wasm string + wantErr bool + }{ + {"close", "0061736d01000000050401010102", false}, + {"trap", "0061736d01000000010401600000030201000504010101020801000a05010300000b", true}, + {"cancel", "0061736d01000000010401600000030201000504010101020801000a0901070003400c000b0b", true}, + } { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + var memory *trackedLinearMemory + ctx, release := withOwnedMemory(ctx, experimental.MemoryAllocatorFunc( + func(_, max uint64) experimental.LinearMemory { + memory = &trackedLinearMemory{LinearMemory: allocate(0, max)} + return memory + })) + defer release() + rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCloseOnContextDone(true)) + defer rt.Close(context.Background()) + binary, err := hex.DecodeString(tt.wasm) + if err != nil { + t.Fatal(err) + } + module, err := rt.Instantiate(ctx, binary) + if (err != nil) != tt.wantErr { + t.Fatalf("instantiate: %v", err) + } + if !tt.wantErr { + if memory == nil || memory.freeCount != 0 { + t.Fatal("module did not allocate native memory") + } + if err := module.Close(ctx); err != nil { + t.Fatal(err) + } + } + release() + release() + if memory == nil || memory.freeCount != 1 { + t.Fatal("host did not release memory exactly once") + } + }) + } +} diff --git a/pkg/download/engine/ffmpeg/memory_windows.go b/pkg/download/engine/ffmpeg/memory_windows.go new file mode 100644 index 00000000..7218414d --- /dev/null +++ b/pkg/download/engine/ffmpeg/memory_windows.go @@ -0,0 +1,70 @@ +//go:build windows && (amd64 || arm64) + +package ffmpeg + +import ( + "context" + "fmt" + "unsafe" + + "github.com/tetratelabs/wazero/experimental" + "golang.org/x/sys/windows" +) + +// Keep large, short-lived WASM buffers out of the Go heap. Closing a module +// releases its committed pages immediately, without waiting for GC/scavenging. +func withLinearMemory(ctx context.Context) (context.Context, func()) { + return withOwnedMemory(ctx, experimental.MemoryAllocatorFunc( + func(_, max uint64) experimental.LinearMemory { + return &windowsLinearMemory{max: max} + })) +} + +// Reserve the address range once, committing zero-filled pages on demand. This +// avoids both copying the previous buffer and temporarily retaining two large +// Go allocations when memory.grow runs. The backing address never changes. +type windowsLinearMemory struct { + address uintptr + max uint64 + size uint64 + freed bool +} + +func (m *windowsLinearMemory) Reallocate(size uint64) []byte { + if m.freed || size > m.max || m.max > uint64(^uint(0)>>1) { + return nil + } + if size == 0 { + return []byte{} + } + if m.address == 0 { + address, err := windows.VirtualAlloc(0, uintptr(m.max), windows.MEM_RESERVE, windows.PAGE_READWRITE) + if err != nil { + panic(fmt.Errorf("ffmpeg: reserve WASM memory: %w", err)) + } + m.address = address + } + if size > m.size { + _, err := windows.VirtualAlloc(m.address, uintptr(size), windows.MEM_COMMIT, windows.PAGE_READWRITE) + if err != nil { + if m.size == 0 { + // Instantiation cannot proceed without the minimum memory. Wazero + // has not taken ownership yet, so release the reservation here. + m.Free() + panic(fmt.Errorf("ffmpeg: commit WASM memory: %w", err)) + } + return nil // memory.grow reports failure to the guest. + } + m.size = size + } + return unsafe.Slice((*byte)(unsafe.Pointer(m.address)), int(size)) +} + +func (m *windowsLinearMemory) Free() { + if m.address != 0 { + _ = windows.VirtualFree(m.address, 0, windows.MEM_RELEASE) + m.address = 0 + } + m.size = 0 + m.freed = true +} diff --git a/pkg/download/engine/ffmpeg/memory_windows_test.go b/pkg/download/engine/ffmpeg/memory_windows_test.go new file mode 100644 index 00000000..fd3e80a3 --- /dev/null +++ b/pkg/download/engine/ffmpeg/memory_windows_test.go @@ -0,0 +1,54 @@ +//go:build windows && (amd64 || arm64) + +package ffmpeg + +import ( + "testing" + "unsafe" + + "github.com/tetratelabs/wazero/experimental" + "golang.org/x/sys/windows" +) + +func TestWindowsLinearMemoryGrowthAndRelease(t *testing.T) { + const page = 65536 + m := &windowsLinearMemory{max: 4 * page} + defer m.Free() + first := m.Reallocate(page) + if len(first) != page { + t.Fatalf("initial allocation: %d", len(first)) + } + first[0], first[page-1] = 17, 29 + address := m.address + grown := m.Reallocate(3 * page) + if unsafe.SliceData(first) != unsafe.SliceData(grown) || grown[0] != 17 || grown[page-1] != 29 { + t.Fatal("growing memory moved or lost existing bytes") + } + for _, b := range grown[page:] { + if b != 0 { + t.Fatal("newly committed memory is not zero-filled") + } + } + if m.Reallocate(5*page) != nil || m.address != address || m.size != 3*page { + t.Fatal("failed growth changed the existing allocation") + } + m.Free() + m.Free() + if m.address != 0 || m.Reallocate(page) != nil { + t.Fatal("freed memory can still be used") + } + var info windows.MemoryBasicInformation + if err := windows.VirtualQuery(address, &info, unsafe.Sizeof(info)); err != nil { + t.Fatal(err) + } + const memFree = 0x10000 // MEMORY_BASIC_INFORMATION.State: MEM_FREE. + if info.State != memFree { + t.Fatalf("pages remain reserved after Free: state=%x", info.State) + } +} + +func TestWindowsLinearMemoryModuleLifecycle(t *testing.T) { + testLinearMemoryModuleLifecycle(t, func(_, max uint64) experimental.LinearMemory { + return &windowsLinearMemory{max: max} + }) +} diff --git a/pkg/download/engine/ffmpeg/runtime.go b/pkg/download/engine/ffmpeg/runtime.go index 5b60cc36..5762b39b 100644 --- a/pkg/download/engine/ffmpeg/runtime.go +++ b/pkg/download/engine/ffmpeg/runtime.go @@ -151,7 +151,9 @@ func RunAcquired(ctx context.Context, video, audio Input, out io.Writer, format files := &inputFS{files: map[string]Input{"video": video, "audio": audio}} fsconfig := wazero.NewFSConfig().(sysfs.FSConfig).WithSysFSMount(files, "/inputs") logs := &tailLog{} - rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{ + runCtx, freeMemory := withLinearMemory(ctx) + defer freeMemory() + rc, err := wasm.Run(runCtx, shared.runtime, shared.module, wasm.Args{ Name: "ffmpeg", Args: args, Stdout: out, Stderr: logs, Config: func(c wazero.ModuleConfig) wazero.ModuleConfig { return c.WithFSConfig(fsconfig) }, }) diff --git a/ui/flutter/windows/runner/CMakeLists.txt b/ui/flutter/windows/runner/CMakeLists.txt index 394917c0..1a9a19c9 100644 --- a/ui/flutter/windows/runner/CMakeLists.txt +++ b/ui/flutter/windows/runner/CMakeLists.txt @@ -7,6 +7,7 @@ project(runner LANGUAGES CXX) # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 + "crash_handler.cpp" "flutter_window.cpp" "main.cpp" "utils.cpp" @@ -34,6 +35,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_link_libraries(${BINARY_NAME} PRIVATE "dbghelp.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. diff --git a/ui/flutter/windows/runner/CRASH_DIAGNOSTICS.md b/ui/flutter/windows/runner/CRASH_DIAGNOSTICS.md new file mode 100644 index 00000000..42d45ec8 --- /dev/null +++ b/ui/flutter/windows/runner/CRASH_DIAGNOSTICS.md @@ -0,0 +1,31 @@ +# Windows native crash diagnostics + +The runner installs diagnostics before starting Flutter and plugins. Files are +written beside the executable under `storage/logs`, matching Windows app storage: + +- `native-crash.log`: UTC timestamp, app version, process/thread, exception code, + faulting module/address/offset, and minidump success or the Windows error code. +- `native-crash.dmp`: the most recent native crash, including thread stacks, + loaded modules and unloaded module information. +- `crash.log`: Go runtime crash output, kept separately from native diagnostics. + +Logs are appended and existing dumps survive normal restarts. A subsequent native +crash replaces the previous dump. Dump generation is best effort, particularly +for stack exhaustion, heap corruption or unavailable disk space. Process kills +and fail-fast termination can bypass exception handlers. + +The top-level unhandled filter covers native exceptions on process threads. An +SEH boundary inside the runner's window callback records the original escaping exception +before Windows wraps a window callback failure as `0xC000041D`. A vectored handler +also observes that fatal callback status, since it can bypass the unhandled +filter. All handlers continue exception processing; they do not attempt recovery +or log ordinary handled first-chance exceptions. + +On Windows, verify a release build by exercising tray clicks, Explorer restart, +system font changes and child-window open/close. In a disposable test build, +raise an unhandled access violation inside a window callback and on a worker +thread, then raise `0xC000041D`; check that the log and dump are written and +survive relaunch. A debugger may intercept these before application handlers. +Open the dump with WinDbg and matching runner/Flutter/plugin PDBs to locate the +faulting stack. Minidumps can contain application data; inspect them before +sharing. diff --git a/ui/flutter/windows/runner/crash_handler.cpp b/ui/flutter/windows/runner/crash_handler.cpp new file mode 100644 index 00000000..a6048449 --- /dev/null +++ b/ui/flutter/windows/runner/crash_handler.cpp @@ -0,0 +1,119 @@ +#include "crash_handler.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +HANDLE crash_log = INVALID_HANDLE_VALUE; +std::wstring dump_path; +volatile LONG recording = 0; +LPTOP_LEVEL_EXCEPTION_FILTER previous_filter = nullptr; + +void WriteLog(const char* text) { + if (crash_log == INVALID_HANDLE_VALUE) return; + DWORD written = 0; + WriteFile(crash_log, text, static_cast(strlen(text)), &written, + nullptr); + FlushFileBuffers(crash_log); +} + +LONG WINAPI UnhandledException(EXCEPTION_POINTERS* exception) { + RecordNativeCrash(exception); + return previous_filter ? previous_filter(exception) + : EXCEPTION_CONTINUE_SEARCH; +} + +LONG CALLBACK FatalCallbackException(EXCEPTION_POINTERS* exception) { + // Windows can terminate on this callback exception without invoking the + // process's unhandled filter. Ignore ordinary first-chance exceptions (Go, + // Dart and plugins may handle those) and observe only this fatal status. + if (exception->ExceptionRecord->ExceptionCode == 0xC000041D) { + RecordNativeCrash(exception); + } + return EXCEPTION_CONTINUE_SEARCH; +} + +} // namespace + +LONG RecordNativeCrash(EXCEPTION_POINTERS* exception) { + if (!exception || InterlockedCompareExchange(&recording, 1, 0) != 0) { + return EXCEPTION_CONTINUE_SEARCH; + } + + SYSTEMTIME time; + GetSystemTime(&time); + const auto* record = exception->ExceptionRecord; + HMODULE module = nullptr; + wchar_t module_path[MAX_PATH] = L"unknown"; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(record->ExceptionAddress), + &module); + if (module) GetModuleFileNameW(module, module_path, MAX_PATH); + + char line[2048] = {}; + StringCchPrintfA( + line, ARRAYSIZE(line), + "\r\n%04u-%02u-%02uT%02u:%02u:%02uZ Gopeed %s pid=%lu tid=%lu " + "exception=0x%08lX address=%p module=%ls offset=0x%llX\r\n", + time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, + time.wSecond, FLUTTER_VERSION, GetCurrentProcessId(), GetCurrentThreadId(), + record->ExceptionCode, record->ExceptionAddress, module_path, + static_cast( + reinterpret_cast(record->ExceptionAddress) - + reinterpret_cast(module))); + WriteLog(line); + + HANDLE dump = CreateFileW(dump_path.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (dump != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION info = {}; + info.ThreadId = GetCurrentThreadId(); + info.ExceptionPointers = exception; + info.ClientPointers = FALSE; + const BOOL ok = MiniDumpWriteDump( + GetCurrentProcess(), GetCurrentProcessId(), dump, + static_cast(MiniDumpNormal | MiniDumpWithThreadInfo | + MiniDumpWithUnloadedModules), + &info, nullptr, nullptr); + const DWORD error = ok ? ERROR_SUCCESS : GetLastError(); + FlushFileBuffers(dump); + CloseHandle(dump); + StringCchPrintfA(line, ARRAYSIZE(line), "minidump success=%d error=%lu\r\n", + ok, error); + WriteLog(line); + } else { + StringCchPrintfA(line, ARRAYSIZE(line), "minidump open error=%lu\r\n", + GetLastError()); + WriteLog(line); + } + + // Do not swallow an exception or resume a potentially corrupted process. + return EXCEPTION_CONTINUE_SEARCH; +} + +void InstallCrashHandler() { + // Match Dart's Windows storage path independently of the working directory. + wchar_t executable[32768] = {}; + const DWORD length = GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable)); + if (length == 0 || length >= ARRAYSIZE(executable)) return; + const auto directory = std::filesystem::path(executable).parent_path() / + L"storage" / L"logs"; + std::error_code error; + std::filesystem::create_directories(directory, error); + if (error) return; + + dump_path = (directory / L"native-crash.dmp").wstring(); + crash_log = CreateFileW((directory / L"native-crash.log").c_str(), + FILE_APPEND_DATA, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + previous_filter = SetUnhandledExceptionFilter(UnhandledException); + AddVectoredExceptionHandler(1, FatalCallbackException); +} diff --git a/ui/flutter/windows/runner/crash_handler.h b/ui/flutter/windows/runner/crash_handler.h new file mode 100644 index 00000000..fece59e6 --- /dev/null +++ b/ui/flutter/windows/runner/crash_handler.h @@ -0,0 +1,12 @@ +#ifndef RUNNER_CRASH_HANDLER_H_ +#define RUNNER_CRASH_HANDLER_H_ + +#include + +// Installs best-effort native diagnostics before Flutter or plugins start. +void InstallCrashHandler(); + +// Records the exception and continues normal Windows exception processing. +LONG RecordNativeCrash(EXCEPTION_POINTERS* exception); + +#endif // RUNNER_CRASH_HANDLER_H_ diff --git a/ui/flutter/windows/runner/flutter_window.cpp b/ui/flutter/windows/runner/flutter_window.cpp index 5f4b6e27..1df494aa 100644 --- a/ui/flutter/windows/runner/flutter_window.cpp +++ b/ui/flutter/windows/runner/flutter_window.cpp @@ -63,7 +63,9 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message, switch (message) { case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); + if (flutter_controller_ && flutter_controller_->engine()) { + flutter_controller_->engine()->ReloadSystemFonts(); + } break; } diff --git a/ui/flutter/windows/runner/main.cpp b/ui/flutter/windows/runner/main.cpp index 74a39e96..281d2fd3 100644 --- a/ui/flutter/windows/runner/main.cpp +++ b/ui/flutter/windows/runner/main.cpp @@ -5,6 +5,7 @@ #include #include "app_links/app_links_plugin_c_api.h" +#include "crash_handler.h" #include "flutter_window.h" #include "flutter/generated_plugin_registrant.h" #include "utils.h" @@ -43,6 +44,7 @@ bool IsMultiWindowLaunch(const std::vector& arguments) { int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + InstallCrashHandler(); // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { @@ -77,11 +79,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + BOOL message_result; + while ((message_result = ::GetMessage(&msg, nullptr, 0, 0)) > 0) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); - return EXIT_SUCCESS; + return message_result == -1 ? EXIT_FAILURE : static_cast(msg.wParam); } diff --git a/ui/flutter/windows/runner/win32_window.cpp b/ui/flutter/windows/runner/win32_window.cpp index 60608d0f..b2f72605 100644 --- a/ui/flutter/windows/runner/win32_window.cpp +++ b/ui/flutter/windows/runner/win32_window.cpp @@ -3,6 +3,7 @@ #include #include +#include "crash_handler.h" #include "resource.h" namespace { @@ -158,19 +159,26 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); + // Capture inside the callback, before user32 wraps or suppresses an escaping + // exception. Keep C++ objects requiring unwinding out of this SEH boundary. + __try { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); + } __except (RecordNativeCrash(GetExceptionInformation())) { + // The filter always continues the exception search; this is unreachable. + return 0; } - - return DefWindowProc(window, message, wparam, lparam); } LRESULT