fix: local tui mode routes console logging to a file and disables download progress bars so stray output cannot scroll the TUI
This commit is contained in:
@@ -2,8 +2,11 @@ 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.
|
||||
# application starts it and crashes the VM. Local tui mode additionally
|
||||
# silences all other terminal writers (console logging goes to a file,
|
||||
# model-download progress bars are disabled) — the TUI owns the terminal
|
||||
# and every stray line scrolls its rendering up one row. Runtime config is
|
||||
# the only hook that runs before dependency applications start.
|
||||
BDS.Server.prepare_boot_env()
|
||||
|
||||
if config_env() == :prod do
|
||||
|
||||
@@ -67,15 +67,71 @@ defmodule BDS.Server do
|
||||
calls into no-ops, so the dependency starts inert and the TUI fallback
|
||||
(issue #33) can actually boot.
|
||||
|
||||
In tui mode the app additionally owns the launching terminal, so every
|
||||
other terminal writer is silenced: the default (console) logger handler
|
||||
is replaced with a rotating file handler at `tui_log_file/0`, and
|
||||
Bumblebee's model-download progress bar (raw stdout writes) is disabled.
|
||||
Each stray line would otherwise scroll the TUI's alternate screen up one
|
||||
row and corrupt the rendering. Desktop and server modes keep logging to
|
||||
the terminal, and SSH-served TUI sessions are unaffected either way —
|
||||
their cells travel over the SSH channel, away from the server console.
|
||||
|
||||
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
|
||||
@spec prepare_boot_env(:desktop | :server | :tui, (-> :ok)) :: :desktop | :server | :tui
|
||||
def prepare_boot_env(mode \\ effective_mode(), redirect_logs \\ &redirect_logging_to_file/0) do
|
||||
if mode != :desktop, do: System.put_env("NO_WX", "1")
|
||||
|
||||
if mode == :tui do
|
||||
Application.put_env(:bumblebee, :progress_bar_enabled, false)
|
||||
redirect_logs.()
|
||||
end
|
||||
|
||||
mode
|
||||
end
|
||||
|
||||
@doc """
|
||||
Log file that replaces console logging in local tui mode: a
|
||||
machine-specific, regenerable artifact, so it lives in the private app
|
||||
dir (PrivateArtifactsLiveInOsAppDir). Overridable via `:bds, :tui_log_file`.
|
||||
"""
|
||||
@spec tui_log_file() :: Path.t()
|
||||
def tui_log_file do
|
||||
Application.get_env(:bds, :tui_log_file) ||
|
||||
Path.join([BDS.Projects.private_dir(), "logs", "bds.log"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Replaces the default (console) logger handler with a rotating file
|
||||
handler writing to `tui_log_file/0`, keeping the configured level,
|
||||
filters, and Elixir formatter. See `prepare_boot_env/2`.
|
||||
"""
|
||||
@spec redirect_logging_to_file() :: :ok
|
||||
def redirect_logging_to_file do
|
||||
log_file = tui_log_file()
|
||||
File.mkdir_p!(Path.dirname(log_file))
|
||||
|
||||
with {:ok, handler} <- :logger.get_handler_config(:default) do
|
||||
:ok = :logger.remove_handler(:default)
|
||||
|
||||
:ok =
|
||||
:logger.add_handler(
|
||||
:default,
|
||||
:logger_std_h,
|
||||
handler
|
||||
|> Map.take([:level, :filters, :filter_default, :formatter])
|
||||
|> Map.put(:config, %{
|
||||
file: String.to_charlist(log_file),
|
||||
max_no_bytes: 10_000_000,
|
||||
max_no_files: 5
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Whether the HTTP endpoint requires the desktop webview auth token. Only
|
||||
desktop mode does: in server/tui mode the endpoint stays loopback-only
|
||||
|
||||
@@ -41,7 +41,13 @@ rule ModeResolution {
|
||||
-- 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.
|
||||
-- dependency starts inert before any BDS supervisor runs. In tui
|
||||
-- mode prepare_boot_env also silences every other terminal writer —
|
||||
-- the default logger handler moves to a rotating file in the private
|
||||
-- app dir and Bumblebee's model-download progress bar is disabled —
|
||||
-- because the TUI owns the terminal and each stray console line
|
||||
-- scrolls the alternate screen up one row. Desktop/server modes and
|
||||
-- SSH-served TUI sessions keep normal console logging.
|
||||
ensures: BootMode.created(kind: mode)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,18 +88,90 @@ defmodule BDS.ServerTest do
|
||||
end
|
||||
|
||||
test "non-desktop modes disable wx so the :desktop dep boots inert" do
|
||||
assert Server.prepare_boot_env(:tui) == :tui
|
||||
assert Server.prepare_boot_env(:tui, fn -> :ok end) == :tui
|
||||
assert System.get_env("NO_WX") == "1"
|
||||
|
||||
System.delete_env("NO_WX")
|
||||
assert Server.prepare_boot_env(:server) == :server
|
||||
assert Server.prepare_boot_env(:server, fn -> :ok end) == :server
|
||||
assert System.get_env("NO_WX") == "1"
|
||||
end
|
||||
|
||||
test "desktop mode leaves wx enabled" do
|
||||
assert Server.prepare_boot_env(:desktop) == :desktop
|
||||
assert Server.prepare_boot_env(:desktop, fn -> :ok end) == :desktop
|
||||
assert System.get_env("NO_WX") == nil
|
||||
end
|
||||
|
||||
test "tui mode silences terminal writers; desktop and server modes keep them" do
|
||||
original = Application.get_env(:bumblebee, :progress_bar_enabled)
|
||||
|
||||
on_exit(fn ->
|
||||
if original == nil do
|
||||
Application.delete_env(:bumblebee, :progress_bar_enabled)
|
||||
else
|
||||
Application.put_env(:bumblebee, :progress_bar_enabled, original)
|
||||
end
|
||||
end)
|
||||
|
||||
parent = self()
|
||||
|
||||
assert Server.prepare_boot_env(:tui, fn -> send(parent, :redirected) end) == :tui
|
||||
assert Application.get_env(:bumblebee, :progress_bar_enabled) == false
|
||||
assert_received :redirected
|
||||
|
||||
Server.prepare_boot_env(:desktop, fn -> send(parent, :redirected) end)
|
||||
Server.prepare_boot_env(:server, fn -> send(parent, :redirected) end)
|
||||
refute_received :redirected
|
||||
end
|
||||
end
|
||||
|
||||
describe "tui logging redirect" do
|
||||
setup do
|
||||
dir = Path.join(System.tmp_dir!(), "bds-tui-log-#{System.unique_integer([:positive])}")
|
||||
log_file = Path.join(dir, "bds.log")
|
||||
Application.put_env(:bds, :tui_log_file, log_file)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.delete_env(:bds, :tui_log_file)
|
||||
File.rm_rf(dir)
|
||||
end)
|
||||
|
||||
{:ok, log_file: log_file}
|
||||
end
|
||||
|
||||
test "tui_log_file defaults to the private app dir and honors the override", %{
|
||||
log_file: log_file
|
||||
} do
|
||||
assert Server.tui_log_file() == log_file
|
||||
|
||||
Application.delete_env(:bds, :tui_log_file)
|
||||
assert Server.tui_log_file() == Path.join([BDS.Projects.private_dir(), "logs", "bds.log"])
|
||||
end
|
||||
|
||||
test "redirect_logging_to_file swaps the default handler to a rotating file handler", %{
|
||||
log_file: log_file
|
||||
} do
|
||||
{:ok, original} = :logger.get_handler_config(:default)
|
||||
|
||||
on_exit(fn ->
|
||||
_ = :logger.remove_handler(:default)
|
||||
|
||||
:ok =
|
||||
:logger.add_handler(
|
||||
:default,
|
||||
original.module,
|
||||
Map.take(original, [:level, :filters, :filter_default, :formatter, :config])
|
||||
)
|
||||
end)
|
||||
|
||||
assert Server.redirect_logging_to_file() == :ok
|
||||
|
||||
{:ok, handler} = :logger.get_handler_config(:default)
|
||||
assert handler.module == :logger_std_h
|
||||
assert handler.config.file == String.to_charlist(log_file)
|
||||
assert handler.config.max_no_bytes > 0
|
||||
# The swap must keep the Elixir log formatter, not fall back to OTP's.
|
||||
assert handler.formatter == original.formatter
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_ssh_dir!/1" do
|
||||
|
||||
Reference in New Issue
Block a user