From d002ff69414b422138379e5b93f696078bf78531 Mon Sep 17 00:00:00 2001 From: foxyz Date: Fri, 9 Oct 2026 18:53:07 +0300 Subject: [PATCH] Linux debug: hot restart no longer crashes with live mpv players Hot restart frees the old isolate's NativeCallables while mpv handles stay alive; media_kit then quits them on the next start and mpv calls the dead wakeup callback (SIGSEGV on the mpv 'core' thread). A debug-only runner hook clears every live handle's wakeup callback on the engine's on-pre-engine-restart signal. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/linux/runner/CMakeLists.txt | 2 + .../desktop/linux/runner/hot_restart_guard.cc | 69 +++++++++++++++++++ apps/desktop/linux/runner/hot_restart_guard.h | 13 ++++ apps/desktop/linux/runner/my_application.cc | 2 + docs/DEVELOPMENT.md | 15 +++- 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/linux/runner/hot_restart_guard.cc create mode 100644 apps/desktop/linux/runner/hot_restart_guard.h diff --git a/apps/desktop/linux/runner/CMakeLists.txt b/apps/desktop/linux/runner/CMakeLists.txt index e97dabc..730c6f4 100644 --- a/apps/desktop/linux/runner/CMakeLists.txt +++ b/apps/desktop/linux/runner/CMakeLists.txt @@ -9,6 +9,7 @@ project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} "main.cc" "my_application.cc" + "hot_restart_guard.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) @@ -22,5 +23,6 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/apps/desktop/linux/runner/hot_restart_guard.cc b/apps/desktop/linux/runner/hot_restart_guard.cc new file mode 100644 index 0000000..c8e4b7a --- /dev/null +++ b/apps/desktop/linux/runner/hot_restart_guard.cc @@ -0,0 +1,69 @@ +#include "hot_restart_guard.h" + +#ifdef NDEBUG + +void hot_restart_guard_attach(FlView* view) {} + +#else + +#include +#include +#include + +namespace { + +// Mirrors media_kit's NativeReferenceHolder (debug only): a calloc'd buffer +// of kReferenceBufferSize intptr slots holding live mpv_handle addresses, +// its address written in decimal to /. +constexpr int kReferenceBufferSize = 512; +constexpr char kFilePrefix[] = "com.alexmercerind.media_kit.NativeReferenceHolder."; + +using SetWakeupCallback = void (*)(void* ctx, void (*cb)(void*), void* d); + +SetWakeupCallback find_set_wakeup_callback() { + static const char* const kNames[] = {"libmpv.so.2", "libmpv.so", "libmpv.so.1"}; + for (const char* name : kNames) { + void* lib = dlopen(name, RTLD_NOW | RTLD_NOLOAD); + if (lib == nullptr) continue; + auto fn = reinterpret_cast( + dlsym(lib, "mpv_set_wakeup_callback")); + dlclose(lib); // Drops only the reference NOLOAD added. + if (fn != nullptr) return fn; + } + return nullptr; +} + +// Runs on the engine's UI thread before the old isolate shuts down, so the +// buffer is not being written concurrently. Clearing the callback takes the +// same lock mpv calls it under: once this returns, mpv never calls it again. +// The handles stay in the buffer: media_kit's startup in the new isolate +// still sends them "quit". +void on_pre_engine_restart(FlEngine* engine, gpointer user_data) { + g_autofree gchar* name = g_strdup_printf("%s%d", kFilePrefix, getpid()); + g_autofree gchar* path = g_build_filename(g_get_tmp_dir(), name, nullptr); + g_autofree gchar* contents = nullptr; + if (!g_file_get_contents(path, &contents, nullptr, nullptr)) return; + const guint64 address = g_ascii_strtoull(contents, nullptr, 10); + if (address == 0) return; + const SetWakeupCallback set_wakeup = find_set_wakeup_callback(); + if (set_wakeup == nullptr) return; + const auto* slots = reinterpret_cast(address); + int cleared = 0; + for (int i = 0; i < kReferenceBufferSize; i++) { + if (slots[i] == 0) continue; + set_wakeup(reinterpret_cast(slots[i]), nullptr, nullptr); + cleared++; + } + if (cleared > 0) { + g_message("hot restart: unhooked %d mpv wakeup callback(s)", cleared); + } +} + +} // namespace + +void hot_restart_guard_attach(FlView* view) { + g_signal_connect(fl_view_get_engine(view), "on-pre-engine-restart", + G_CALLBACK(on_pre_engine_restart), nullptr); +} + +#endif diff --git a/apps/desktop/linux/runner/hot_restart_guard.h b/apps/desktop/linux/runner/hot_restart_guard.h new file mode 100644 index 0000000..e876dec --- /dev/null +++ b/apps/desktop/linux/runner/hot_restart_guard.h @@ -0,0 +1,13 @@ +#ifndef RUNNER_HOT_RESTART_GUARD_H_ +#define RUNNER_HOT_RESTART_GUARD_H_ + +#include + +// Debug builds: before a hot restart tears the Dart isolate down, unhooks +// the libmpv wakeup callbacks that media_kit's players point at that +// isolate's NativeCallables. Without this, the next mpv event on a leftover +// player calls into freed code and the process dies (SIGSEGV on mpv's +// "core" thread). No-op in profile/release. +void hot_restart_guard_attach(FlView* view); + +#endif // RUNNER_HOT_RESTART_GUARD_H_ diff --git a/apps/desktop/linux/runner/my_application.cc b/apps/desktop/linux/runner/my_application.cc index ce91967..90877d0 100644 --- a/apps/desktop/linux/runner/my_application.cc +++ b/apps/desktop/linux/runner/my_application.cc @@ -6,6 +6,7 @@ #endif #include "flutter/generated_plugin_registrant.h" +#include "hot_restart_guard.h" struct _MyApplication { GtkApplication parent_instance; @@ -87,6 +88,7 @@ static void my_application_activate(GApplication* application) { gtk_widget_realize(GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + hot_restart_guard_attach(view); gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index b30bc9d..b357bbe 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -108,7 +108,10 @@ attaches to spawned isolates too (it shows as `server`). Its log lines are printed with a `[server]` prefix. Hot reload reloads the server isolate's code too, but its state (members, clock, sockets) survives until hosting restarts; changes to startup code (`SyncServer.start`, routes) need a new -Host. Without the app: `dart run packages/server/bin/server.dart`. +Host. Hot restart kills the server isolate (flutter_tools kills every +non-UI isolate first), so a `[server] server crashed: the server stopped +unexpectedly` line just before it is expected. Without the app: +`dart run packages/server/bin/server.dart`. ## Gotchas @@ -125,6 +128,16 @@ Host. Without the app: `dart run packages/server/bin/server.dart`. - media_kit has no public `Player.setProperty`; the code reaches mpv through a dynamic call on `player.platform`. Cache size goes through `PlayerConfiguration(bufferSize:)`. +- Hot restart with any media_kit `Player` alive (session, thumbnails, sfx) + used to SIGSEGV on mpv's `core` thread: each mpv handle's wakeup callback + is a `NativeCallable` of the old isolate, freed by the restart, and media_kit + then sends the leftover handles `quit`, which fires it. The Linux runner + (`linux/runner/hot_restart_guard.cc`, debug builds only) clears those + callbacks on the engine's `on-pre-engine-restart` signal, reading the + handles from media_kit's debug-only `NativeReferenceHolder` buffer (address + in `$TMPDIR/com.alexmercerind.media_kit.NativeReferenceHolder.`). Old + players are leaked, not destroyed, until the process exits. Re-check this + after a media_kit upgrade. - mpv reconnect flags: comma form works, colon form is rejected. Re-verify through `setProperty` if touched. - Overlays that are hidden but mounted keep focus (Space falls into a hidden