Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/maintainer/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ These are mistakes already made here; each was silent rather than loud, which is

**`internal/daemon` must not depend on a cloud package.** What an engine should serve is `inference.DeployConfig`, in `internal/inference` — a leaf that imports only the standard library. Both the daemon and the cloud control plane speak it, so neither has to import the other to be described: a daemon is handed one over its control API, and `internal/remote` persists one against an environment. The dependency used to run the other way, `internal/daemon` importing `internal/remote` for the type and for a config-directory helper that only forwarded to `internal/config.Dir`, which made the package that knows nothing about AWS depend on the package that is nothing but AWS. Keep new shared vocabulary in `internal/inference` only where it describes an engine's workload and needs nothing of ours to express; anything cloud-shaped — `IsInstanceType` and the rest of the EC2 vocabulary — stays in `internal/remote`.

**A captured engine's stdout is a pseudo-terminal, and the normaliser above it is a line model, not a screen emulator.** llama.cpp's download bar prints nothing when its stdout is not a terminal, and the download happens in-process before the HTTP listener comes up — there is no status API to scrape — so the capture (the daemon and the serve view) presents the engine's stdout as a PTY: the only way the progress reaches the engine log at all. `ptylog.go` turns the terminal stream into the log's lines as a column, and three of its rules are easy to break by "simplifying". The engine's up-down cursor dance is *part of each redraw* — the bar's state is drawn between a cursor-up and a cursor-back-down, the cursor parked on an anchor line — so a cursor move must move the drawing between the column's lines and never end a line: committing on a move records every state as a "final" one and resets the dedup, which is how the first design put the whole download, state for state, in the log. A state's replacement is lazy — a carriage return settles the state drawn before it, and the line's content is cleared only by the next text byte — so `S\r\r\n`, the PTY's ONLCR turning a lone `\n` into a CRLF, commits `S` intact. And the tick at the interval's half-rate runs on *every* line of the column, because a bar's last state outlives the engine's move off its line: the download finishes, the engine goes quiet on stdout, and the log still owes it. Because the engine's stdout is now a terminal, the capture branch sets `NO_COLOR=1` in the engine's environment: llama.cpp routes its log lines to stderr and colours them by whether it sees a terminal — *stdout* among them — and the normaliser never sees the stderr path, so without it escapes would reach the log file. The bar draws no colour, so nothing is lost; the forwarding paths (piped `serve`, off-terminal `serve --api`) set nothing and must stay byte-for-byte what the engine wrote — a test pins the raw `\r` surviving. Where no pseudo-terminal can be opened — Windows, a restricted environment — the fallback writes stdout to the log file directly, no worse than the capture did before, and the engine is told `NO_COLOR` the same way; a test stands the opening in for with a failure to keep that branch alive. The pump always drains — the throttle delays the log's writes, never the reads, so the engine can never wedge on a full terminal — and the log file closes only after the pump's final record, never under it.

## Dashboard (`fleet_dashboard.go` and friends)

A few Bubble Tea/lipgloss specifics that are easy to break by "simplifying":
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/teatest v0.0.0-20260816001655-68d539dca504
github.com/creack/pty v1.1.24
github.com/godbus/dbus/v5 v5.2.2
github.com/muesli/termenv v0.16.0
github.com/spf13/cobra v1.10.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
45 changes: 45 additions & 0 deletions internal/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,51 @@ func TestSupervisorCleanExitIsStopped(t *testing.T) {
}
}

func TestStartRefusesAnEmptyCommand(t *testing.T) {
s := NewSupervisor(filepath.Join(t.TempDir(), "engine.log"))
if err := s.Start(nil); err == nil {
t.Fatal("starting with no engine command succeeded")
}
if state, _, _ := s.Status(); state != StateIdle {
t.Errorf("state = %s, want idle", state)
}
// And a wait on a supervisor that never started is already over.
if err := s.Wait(); err != nil {
t.Errorf("Wait = %v, want nil, on a supervisor that never started", err)
}
}

func TestStartRefusesAnUnreachableLogPath(t *testing.T) {
dir := t.TempDir()

// A file where the log's directory must be: the directory cannot be
// made, and the start must fail saying nothing ran.
blocker := filepath.Join(dir, "in the way")
if err := os.WriteFile(blocker, nil, 0o600); err != nil {
t.Fatal(err)
}
s := NewSupervisor(filepath.Join(blocker, "engine.log"))
if err := s.Start([]string{"/bin/sh"}); err == nil {
t.Fatal("starting with an unreachable log directory succeeded")
}
if state, _, _ := s.Status(); state != StateIdle {
t.Errorf("state = %s, want idle", state)
}

// A directory where the log's file must be: the file cannot be opened.
logDir := filepath.Join(dir, "engine.log")
if err := os.Mkdir(logDir, 0o700); err != nil {
t.Fatal(err)
}
s = NewSupervisor(logDir)
if err := s.Start([]string{"/bin/sh"}); err == nil {
t.Fatal("starting with a directory for the log succeeded")
}
if state, _, _ := s.Status(); state != StateIdle {
t.Errorf("state = %s, want idle", state)
}
}

func TestSupervisorStopEscalatesToKill(t *testing.T) {
s := NewSupervisor(filepath.Join(t.TempDir(), "engine.log"))
s.Grace = 100 * time.Millisecond
Expand Down
36 changes: 36 additions & 0 deletions internal/daemon/pty_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build !windows

package daemon

import (
"os"

"github.com/creack/pty"
)

// ptyWindow is the window a captured engine's pseudo-terminal reports. Wide
// enough that an engine sizes its terminal output for an ordinary screen — a
// download bar among them — and narrow enough that the recorded lines stay
// compact in the view's pane and for the log's other readers.
var ptyWindow = &pty.Winsize{Rows: 50, Cols: 80}

// attachPTY opens a pseudo-terminal for a captured engine's stdout: the
// master is what the supervisor reads, the slave is what the engine's stdout
// holds. An error means no pseudo-terminal could be opened — the unsupported
// platform among them — and the capture falls back to the log file. It is a
// variable so a test can stand in for the opening and exercise that
// fallback.
var attachPTY = openPTY

func openPTY() (master, slave *os.File, err error) {
master, slave, err = pty.Open()
if err != nil {
return nil, nil, err
}
if err := pty.Setsize(master, ptyWindow); err != nil {
master.Close()
slave.Close()
return nil, nil, err
}
return master, slave, nil
}
14 changes: 14 additions & 0 deletions internal/daemon/pty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build windows

package daemon

import (
"errors"
"os"
)

// attachPTY has no pseudo-terminal on Windows: the capture falls back to the
// log file, no worse than it did before the pseudo-terminal existed.
var attachPTY = func() (*os.File, *os.File, error) {
return nil, nil, errors.New("no pseudo-terminal on this platform")
}
Loading
Loading