fix: fixed shutdown race
This commit is contained in:
@@ -61,9 +61,26 @@ defmodule BDS.Desktop.Shutdown do
|
|||||||
|
|
||||||
def command_menu_selected(_event, _command_event), do: :ok
|
def command_menu_selected(_event, _command_event), do: :ok
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Terminate the OS process directly with SIGKILL.
|
||||||
|
|
||||||
|
`Desktop.Window.quit/0` routes through `System.halt/1`, which calls the libc
|
||||||
|
`exit()` and runs the wxWidgets C++ static destructors on the way out. On
|
||||||
|
macOS that races the still-running wx event loop on the main thread and
|
||||||
|
segfaults (`wxMenu::~wxMenu` vs `wxAppBase::ProcessIdle`). A SIGKILL is a
|
||||||
|
kernel-level termination that skips those destructors entirely, so the app
|
||||||
|
exits cleanly without producing a crash report.
|
||||||
|
"""
|
||||||
|
@spec quit() :: :ok
|
||||||
|
def quit do
|
||||||
|
kill_heart()
|
||||||
|
kill_beam()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
defp start_shutdown_task do
|
defp start_shutdown_task do
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
MainWindow.persist_now()
|
persist_safely()
|
||||||
maybe_hide_window()
|
maybe_hide_window()
|
||||||
Process.sleep(50)
|
Process.sleep(50)
|
||||||
quit_module().quit()
|
quit_module().quit()
|
||||||
@@ -72,6 +89,57 @@ defmodule BDS.Desktop.Shutdown do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp persist_safely do
|
||||||
|
MainWindow.persist_now()
|
||||||
|
:ok
|
||||||
|
rescue
|
||||||
|
_error -> :ok
|
||||||
|
catch
|
||||||
|
:exit, _reason -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# heart, when present, would relaunch the app after we kill the BEAM, so it
|
||||||
|
# has to be terminated first. When the app is started without heart (e.g. via
|
||||||
|
# `mix`) there is nothing to do here.
|
||||||
|
defp kill_heart do
|
||||||
|
with heart when is_pid(heart) <- Process.whereis(:heart),
|
||||||
|
{:links, links} <- Process.info(heart, :links),
|
||||||
|
port when is_port(port) <- Enum.find(links, &is_port/1),
|
||||||
|
{:os_pid, heart_pid} <- Port.info(port, :os_pid) do
|
||||||
|
os_kill(heart_pid)
|
||||||
|
else
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
_error -> :ok
|
||||||
|
catch
|
||||||
|
:exit, _reason -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp kill_beam do
|
||||||
|
os_kill(:os.getpid())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp os_kill(os_pid) do
|
||||||
|
os_kill_fun().(os_pid)
|
||||||
|
:ok
|
||||||
|
rescue
|
||||||
|
_error -> :ok
|
||||||
|
catch
|
||||||
|
:exit, _reason -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp os_kill_fun do
|
||||||
|
Application.get_env(:bds, :desktop_os_kill_fun, &__MODULE__.hard_kill/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec hard_kill(charlist() | integer() | String.t()) :: :ok
|
||||||
|
def hard_kill(os_pid) do
|
||||||
|
System.cmd("kill", ["-9", to_string(os_pid)], stderr_to_stdout: true)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_hide_window do
|
defp maybe_hide_window do
|
||||||
module = window_module()
|
module = window_module()
|
||||||
|
|
||||||
@@ -86,8 +154,10 @@ defmodule BDS.Desktop.Shutdown do
|
|||||||
:exit, _reason -> :ok
|
:exit, _reason -> :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp quit_module do
|
@doc false
|
||||||
Application.get_env(:bds, :desktop_window_quit_module, Window)
|
@spec quit_module() :: module()
|
||||||
|
def quit_module do
|
||||||
|
Application.get_env(:bds, :desktop_window_quit_module, __MODULE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp window_module do
|
defp window_module do
|
||||||
|
|||||||
Binary file not shown.
@@ -256,6 +256,53 @@ defmodule BDS.DesktopTest do
|
|||||||
assert_receive :window_quit_requested
|
assert_receive :window_quit_requested
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "the app owns final termination instead of delegating to Desktop.Window/System.halt" do
|
||||||
|
# Desktop.Window.quit/0 routes through System.halt/1, which runs the wx C++
|
||||||
|
# static destructors on exit and crashes on macOS. The app-owned shutdown
|
||||||
|
# must terminate the OS process directly with SIGKILL instead.
|
||||||
|
assert BDS.Desktop.Shutdown.quit_module() == BDS.Desktop.Shutdown
|
||||||
|
end
|
||||||
|
|
||||||
|
test "hard quit terminates the BEAM OS process with SIGKILL rather than libc exit" do
|
||||||
|
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
|
||||||
|
test_pid = self()
|
||||||
|
|
||||||
|
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
|
||||||
|
send(test_pid, {:os_killed, to_string(os_pid)})
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
on_exit(fn -> restore_env(:desktop_os_kill_fun, previous_kill) end)
|
||||||
|
|
||||||
|
assert :ok = BDS.Desktop.Shutdown.quit()
|
||||||
|
assert_receive {:os_killed, beam_pid}
|
||||||
|
assert beam_pid == to_string(:os.getpid())
|
||||||
|
end
|
||||||
|
|
||||||
|
test "hard quit kills heart before itself so the app is not relaunched" do
|
||||||
|
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
|
||||||
|
test_pid = self()
|
||||||
|
|
||||||
|
{:ok, fake_heart} =
|
||||||
|
Agent.start(fn -> :ok end, name: :heart)
|
||||||
|
|
||||||
|
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
|
||||||
|
send(test_pid, {:os_killed, to_string(os_pid)})
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
restore_env(:desktop_os_kill_fun, previous_kill)
|
||||||
|
if Process.alive?(fake_heart), do: Agent.stop(fake_heart)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert :ok = BDS.Desktop.Shutdown.quit()
|
||||||
|
# heart has no linked port here, so only the BEAM itself is killed, and the
|
||||||
|
# call must not crash while inspecting the heart process.
|
||||||
|
assert_receive {:os_killed, beam_pid}
|
||||||
|
assert beam_pid == to_string(:os.getpid())
|
||||||
|
end
|
||||||
|
|
||||||
test "desktop root html is a LiveView shell and references the generated asset entrypoints" do
|
test "desktop root html is a LiveView shell and references the generated asset entrypoints" do
|
||||||
conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
|
conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
|
||||||
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
|
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
|
||||||
|
|||||||
Reference in New Issue
Block a user