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 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 18:53:07 +03:00
1 parent 151ec95289
commit d002ff6941
5 files changed
+100 -1

No files matched your search

+2
View File
@@ -9,6 +9,7 @@ project(runner LANGUAGES CXX)
add_executable(${BINARY_NAME} add_executable(${BINARY_NAME}
"main.cc" "main.cc"
"my_application.cc" "my_application.cc"
"hot_restart_guard.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.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. # Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 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}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
@@ -0,0 +1,69 @@
#include "hot_restart_guard.h"
#ifdef NDEBUG
void hot_restart_guard_attach(FlView* view) {}
#else
#include <dlfcn.h>
#include <stdint.h>
#include <unistd.h>
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 <tmp>/<kFilePrefix><pid>.
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<SetWakeupCallback>(
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<const intptr_t*>(address);
int cleared = 0;
for (int i = 0; i < kReferenceBufferSize; i++) {
if (slots[i] == 0) continue;
set_wakeup(reinterpret_cast<void*>(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
@@ -0,0 +1,13 @@
#ifndef RUNNER_HOT_RESTART_GUARD_H_
#define RUNNER_HOT_RESTART_GUARD_H_
#include <flutter_linux/flutter_linux.h>
// 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_
@@ -6,6 +6,7 @@
#endif #endif
#include "flutter/generated_plugin_registrant.h" #include "flutter/generated_plugin_registrant.h"
#include "hot_restart_guard.h"
struct _MyApplication { struct _MyApplication {
GtkApplication parent_instance; GtkApplication parent_instance;
@@ -87,6 +88,7 @@ static void my_application_activate(GApplication* application) {
gtk_widget_realize(GTK_WIDGET(view)); gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view));
hot_restart_guard_attach(view);
gtk_widget_grab_focus(GTK_WIDGET(view)); gtk_widget_grab_focus(GTK_WIDGET(view));
} }
+14 -1
View File
@@ -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 printed with a `[server]` prefix. Hot reload reloads the server isolate's
code too, but its state (members, clock, sockets) survives until hosting code too, but its state (members, clock, sockets) survives until hosting
restarts; changes to startup code (`SyncServer.start`, routes) need a new 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 ## 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 - media_kit has no public `Player.setProperty`; the code reaches mpv through a
dynamic call on `player.platform`. Cache size goes through dynamic call on `player.platform`. Cache size goes through
`PlayerConfiguration(bufferSize:)`. `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.<pid>`). 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 - mpv reconnect flags: comma form works, colon form is rejected. Re-verify
through `setProperty` if touched. through `setProperty` if touched.
- Overlays that are hidden but mounted keep focus (Space falls into a hidden - Overlays that are hidden but mounted keep focus (Space falls into a hidden