fix: set NO_WX before dependency boot and detect headless sessions per platform so the TUI fallback actually works (issue #33)
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
# Headless boots (BDS_MODE=server/tui, or desktop mode without a graphical
|
||||||
|
# display — issue #33) must neuter wx before the :desktop dependency
|
||||||
|
# application starts it and crashes the VM. Runtime config is the only hook
|
||||||
|
# that runs before dependency applications start.
|
||||||
|
BDS.Server.prepare_boot_env()
|
||||||
|
|
||||||
if config_env() == :prod do
|
if config_env() == :prod do
|
||||||
database_path =
|
database_path =
|
||||||
System.get_env("BDS_DATABASE_PATH") ||
|
System.get_env("BDS_DATABASE_PATH") ||
|
||||||
|
|||||||
@@ -30,27 +30,52 @@ defmodule BDS.Server do
|
|||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
The mode the app actually boots in: the resolved `BDS_MODE`, except that
|
The mode the app actually boots in: the resolved `BDS_MODE`, except that
|
||||||
desktop mode falls back to the TUI when no graphical display is available
|
desktop mode falls back to the TUI when no graphical session is available
|
||||||
(issue #33). Starting the app on a display-less Linux server then still
|
(issue #33). Starting the app anywhere then still opens a UI — graphical
|
||||||
opens a UI — the terminal one — instead of crashing wx.
|
when possible, the terminal one otherwise.
|
||||||
|
|
||||||
Only non-Darwin unix systems fall back: they need `DISPLAY` or
|
Per platform: non-Darwin unix needs `DISPLAY` or `WAYLAND_DISPLAY` to run
|
||||||
`WAYLAND_DISPLAY` to run wx, while macOS and Windows always have a
|
wx — that check also covers plain ssh sessions, while `ssh -X` with a
|
||||||
graphical session for a launched app.
|
forwarded display keeps the GUI. macOS never sets `DISPLAY`, so there an
|
||||||
|
ssh session (`SSH_CONNECTION`/`SSH_CLIENT`/`SSH_TTY`) is the headless
|
||||||
|
signal and a local launch always has a graphical session. Windows always
|
||||||
|
boots the GUI.
|
||||||
"""
|
"""
|
||||||
@spec effective_mode(:desktop | :server | :tui, {atom(), atom()}, %{
|
@spec effective_mode(:desktop | :server | :tui, {atom(), atom()}, %{
|
||||||
optional(String.t()) => String.t()
|
optional(String.t()) => String.t()
|
||||||
}) :: :desktop | :server | :tui
|
}) :: :desktop | :server | :tui
|
||||||
def effective_mode(mode \\ mode(), os_type \\ :os.type(), env \\ System.get_env())
|
def effective_mode(mode \\ mode(), os_type \\ :os.type(), env \\ System.get_env())
|
||||||
|
|
||||||
def effective_mode(:desktop, {:unix, os}, env) when os != :darwin do
|
def effective_mode(:desktop, {:unix, :darwin}, env) do
|
||||||
if Enum.any?(["DISPLAY", "WAYLAND_DISPLAY"], &(env[&1] not in [nil, ""])),
|
if env_any?(env, ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"]), do: :tui, else: :desktop
|
||||||
do: :desktop,
|
end
|
||||||
else: :tui
|
|
||||||
|
def effective_mode(:desktop, {:unix, _os}, env) do
|
||||||
|
if env_any?(env, ["DISPLAY", "WAYLAND_DISPLAY"]), do: :desktop, else: :tui
|
||||||
end
|
end
|
||||||
|
|
||||||
def effective_mode(mode, _os_type, _env), do: mode
|
def effective_mode(mode, _os_type, _env), do: mode
|
||||||
|
|
||||||
|
defp env_any?(env, vars), do: Enum.any?(vars, &(env[&1] not in [nil, ""]))
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Prepares the OS environment for the given effective boot mode and returns
|
||||||
|
it. The `:desktop` dependency application boots wx inside its own
|
||||||
|
`Application.start/2` — before any BDS supervisor runs — and that wx load
|
||||||
|
crashes the whole VM on a display-less system. For every non-desktop mode
|
||||||
|
this sets `NO_WX=1`, elixir-desktop's own switch that turns all of its wx
|
||||||
|
calls into no-ops, so the dependency starts inert and the TUI fallback
|
||||||
|
(issue #33) can actually boot.
|
||||||
|
|
||||||
|
Called from `config/runtime.exs`: runtime config is the only hook that
|
||||||
|
runs before dependency applications start, in both `mix run` and releases.
|
||||||
|
"""
|
||||||
|
@spec prepare_boot_env(:desktop | :server | :tui) :: :desktop | :server | :tui
|
||||||
|
def prepare_boot_env(mode \\ effective_mode()) do
|
||||||
|
if mode != :desktop, do: System.put_env("NO_WX", "1")
|
||||||
|
mode
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Whether the HTTP endpoint requires the desktop webview auth token. Only
|
Whether the HTTP endpoint requires the desktop webview auth token. Only
|
||||||
desktop mode does: in server/tui mode the endpoint stays loopback-only
|
desktop mode does: in server/tui mode the endpoint stays loopback-only
|
||||||
|
|||||||
@@ -31,11 +31,17 @@ surface ServerRuntimeSurface {
|
|||||||
rule ModeResolution {
|
rule ModeResolution {
|
||||||
when: ApplicationStarted(mode)
|
when: ApplicationStarted(mode)
|
||||||
-- BDS_MODE environment variable decides the mode once per boot.
|
-- BDS_MODE environment variable decides the mode once per boot.
|
||||||
-- Fallback (issue #33): resolved desktop mode on a non-Darwin unix
|
-- Fallback (issue #33): resolved desktop mode boots as tui when no
|
||||||
-- system without DISPLAY/WAYLAND_DISPLAY boots as tui instead, so a
|
-- graphical session is available, so the app always opens a UI
|
||||||
-- display-less Linux server still opens a UI rather than crashing wx;
|
-- rather than crashing wx; a log line records the switch. Headless
|
||||||
-- a log line records the switch. Explicit server/tui modes and
|
-- means: non-Darwin unix without DISPLAY/WAYLAND_DISPLAY (plain ssh
|
||||||
-- macOS/Windows are never rewritten.
|
-- included; ssh -X with a forwarded display keeps the GUI), or macOS
|
||||||
|
-- inside an ssh session (SSH_CONNECTION/SSH_CLIENT/SSH_TTY — macOS
|
||||||
|
-- never sets DISPLAY). Explicit server/tui modes and Windows are
|
||||||
|
-- never rewritten. Because the :desktop dependency boots wx in its
|
||||||
|
-- own application start, config/runtime.exs sets NO_WX=1 for every
|
||||||
|
-- non-desktop effective mode (BDS.Server.prepare_boot_env) so the
|
||||||
|
-- dependency starts inert before any BDS supervisor runs.
|
||||||
ensures: BootMode.created(kind: mode)
|
ensures: BootMode.created(kind: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,35 @@ defmodule BDS.ServerTest do
|
|||||||
assert Server.effective_mode(:desktop, {:unix, :freebsd}, %{"WAYLAND_DISPLAY" => ""}) == :tui
|
assert Server.effective_mode(:desktop, {:unix, :freebsd}, %{"WAYLAND_DISPLAY" => ""}) == :tui
|
||||||
end
|
end
|
||||||
|
|
||||||
test "macOS and Windows never fall back" do
|
test "macOS keeps the GUI locally but falls back to the TUI over ssh" do
|
||||||
|
# macOS has no DISPLAY variable; a local launch always has a
|
||||||
|
# graphical session, an ssh session never does.
|
||||||
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{}) == :desktop
|
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{}) == :desktop
|
||||||
|
|
||||||
|
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{
|
||||||
|
"SSH_CONNECTION" => "10.0.0.5 52000 10.0.0.9 22"
|
||||||
|
}) == :tui
|
||||||
|
|
||||||
|
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{"SSH_TTY" => "/dev/ttys003"}) ==
|
||||||
|
:tui
|
||||||
|
|
||||||
|
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{"SSH_CLIENT" => "10.0.0.5"}) ==
|
||||||
|
:tui
|
||||||
|
|
||||||
|
assert Server.effective_mode(:desktop, {:unix, :darwin}, %{"SSH_CONNECTION" => ""}) ==
|
||||||
|
:desktop
|
||||||
|
end
|
||||||
|
|
||||||
|
test "linux over ssh with a forwarded display keeps the GUI" do
|
||||||
|
env = %{"SSH_CONNECTION" => "10.0.0.5 52000 10.0.0.9 22", "DISPLAY" => "localhost:10.0"}
|
||||||
|
assert Server.effective_mode(:desktop, {:unix, :linux}, env) == :desktop
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Windows never falls back" do
|
||||||
assert Server.effective_mode(:desktop, {:win32, :nt}, %{}) == :desktop
|
assert Server.effective_mode(:desktop, {:win32, :nt}, %{}) == :desktop
|
||||||
|
|
||||||
|
assert Server.effective_mode(:desktop, {:win32, :nt}, %{"SSH_CONNECTION" => "10.0.0.5"}) ==
|
||||||
|
:desktop
|
||||||
end
|
end
|
||||||
|
|
||||||
test "explicit server and tui modes are untouched" do
|
test "explicit server and tui modes are untouched" do
|
||||||
@@ -49,6 +75,33 @@ defmodule BDS.ServerTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "prepare_boot_env/1" do
|
||||||
|
setup do
|
||||||
|
original = System.get_env("NO_WX")
|
||||||
|
System.delete_env("NO_WX")
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
if original, do: System.put_env("NO_WX", original), else: System.delete_env("NO_WX")
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-desktop modes disable wx so the :desktop dep boots inert" do
|
||||||
|
assert Server.prepare_boot_env(:tui) == :tui
|
||||||
|
assert System.get_env("NO_WX") == "1"
|
||||||
|
|
||||||
|
System.delete_env("NO_WX")
|
||||||
|
assert Server.prepare_boot_env(:server) == :server
|
||||||
|
assert System.get_env("NO_WX") == "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "desktop mode leaves wx enabled" do
|
||||||
|
assert Server.prepare_boot_env(:desktop) == :desktop
|
||||||
|
assert System.get_env("NO_WX") == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "ensure_ssh_dir!/1" do
|
describe "ensure_ssh_dir!/1" do
|
||||||
setup do
|
setup do
|
||||||
dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}")
|
dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}")
|
||||||
|
|||||||
Reference in New Issue
Block a user