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
|
# Headless boots (BDS_MODE=server/tui, or desktop mode without a graphical
|
||||||
# display — issue #33) must neuter wx before the :desktop dependency
|
# display — issue #33) must neuter wx before the :desktop dependency
|
||||||
# application starts it and crashes the VM. Runtime config is the only hook
|
# application starts it and crashes the VM. Local tui mode additionally
|
||||||
# that runs before dependency applications start.
|
# 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()
|
BDS.Server.prepare_boot_env()
|
||||||
|
|
||||||
if config_env() == :prod do
|
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
|
calls into no-ops, so the dependency starts inert and the TUI fallback
|
||||||
(issue #33) can actually boot.
|
(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
|
Called from `config/runtime.exs`: runtime config is the only hook that
|
||||||
runs before dependency applications start, in both `mix run` and releases.
|
runs before dependency applications start, in both `mix run` and releases.
|
||||||
"""
|
"""
|
||||||
@spec prepare_boot_env(:desktop | :server | :tui) :: :desktop | :server | :tui
|
@spec prepare_boot_env(:desktop | :server | :tui, (-> :ok)) :: :desktop | :server | :tui
|
||||||
def prepare_boot_env(mode \\ effective_mode()) do
|
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 != :desktop, do: System.put_env("NO_WX", "1")
|
||||||
|
|
||||||
|
if mode == :tui do
|
||||||
|
Application.put_env(:bumblebee, :progress_bar_enabled, false)
|
||||||
|
redirect_logs.()
|
||||||
|
end
|
||||||
|
|
||||||
mode
|
mode
|
||||||
end
|
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 """
|
@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
|
||||||
|
|||||||
@@ -41,7 +41,13 @@ rule ModeResolution {
|
|||||||
-- never rewritten. Because the :desktop dependency boots wx in its
|
-- never rewritten. Because the :desktop dependency boots wx in its
|
||||||
-- own application start, config/runtime.exs sets NO_WX=1 for every
|
-- own application start, config/runtime.exs sets NO_WX=1 for every
|
||||||
-- non-desktop effective mode (BDS.Server.prepare_boot_env) so the
|
-- 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)
|
ensures: BootMode.created(kind: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,18 +88,90 @@ defmodule BDS.ServerTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "non-desktop modes disable wx so the :desktop dep boots inert" do
|
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"
|
assert System.get_env("NO_WX") == "1"
|
||||||
|
|
||||||
System.delete_env("NO_WX")
|
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"
|
assert System.get_env("NO_WX") == "1"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "desktop mode leaves wx enabled" do
|
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
|
assert System.get_env("NO_WX") == nil
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "ensure_ssh_dir!/1" do
|
describe "ensure_ssh_dir!/1" do
|
||||||
|
|||||||
Reference in New Issue
Block a user