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:
1 parent
151ec95289
commit
d002ff6941
5 files changed
+100
-1
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user