Add optional web UI mirroring --monitor (LISTEN_UI, off by default)
A single self-contained page polls /api/status once a second and renders the same lines as the terminal monitor (downstreams with models/VRAM and busy markers, lock, queue, GPU stats); the u/r buttons and keys trigger update-now and reload-env through the same rate-limited command handler the control pipe uses. Unauthenticated by design — the sample config tells users to keep it on localhost.
This commit is contained in:
+34
-15
@@ -567,6 +567,7 @@ func run(ctx context.Context, cfg config.Config, log *slog.Logger, logOut io.Wri
|
||||
"version", version,
|
||||
"listen_ollama", cfg.ListenOllama,
|
||||
"listen_comfy", cfg.ListenComfy,
|
||||
"listen_ui", orDisabled(cfg.ListenUI),
|
||||
"ollama_url", orDisabled(cfg.OllamaURL),
|
||||
"comfy_url", orDisabled(cfg.ComfyURL),
|
||||
"unload_timeout", cfg.UnloadTimeout,
|
||||
@@ -808,13 +809,24 @@ func run(ctx context.Context, cfg config.Config, log *slog.Logger, logOut io.Wri
|
||||
}
|
||||
}
|
||||
|
||||
// The control channel (status for --monitor, update-now and reload-env
|
||||
// triggers) is served whenever running as a service, independent of
|
||||
// AUTO_UPDATE.
|
||||
// The command handler backs both the local control channel (served
|
||||
// whenever running as a service) and the optional web UI (LISTEN_UI),
|
||||
// independent of AUTO_UPDATE.
|
||||
handler := controlHandler(ctx, u, exePath, applyStaged,
|
||||
statusProvider(cfg, lk, comfySup, health, started, gw, ollamaClient),
|
||||
reloadHandler(cfg, configPath, restartWhenIdle))
|
||||
if isService {
|
||||
serveControl(ctx, log, u, exePath, applyStaged,
|
||||
statusProvider(cfg, lk, comfySup, health, started, gw, ollamaClient),
|
||||
reloadHandler(cfg, configPath, restartWhenIdle))
|
||||
serveControl(ctx, log, handler)
|
||||
}
|
||||
if cfg.ListenUI != "" {
|
||||
ln, err := net.Listen("tcp", cfg.ListenUI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen ui on %s: %w", cfg.ListenUI, err)
|
||||
}
|
||||
uiSrv := &http.Server{Addr: cfg.ListenUI, Handler: uiHandler(handler)}
|
||||
servers = append(servers, uiSrv)
|
||||
log.Warn("listening", "consumer", "ui", "addr", cfg.ListenUI)
|
||||
go func() { errCh <- uiSrv.Serve(ln) }()
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -1024,17 +1036,18 @@ func updateLoop(ctx context.Context, interval time.Duration, log *slog.Logger, u
|
||||
}
|
||||
}
|
||||
|
||||
// serveControl opens the local control channel (named pipe on Windows,
|
||||
// unix socket on Linux) so unprivileged local users can query status
|
||||
// (--monitor), trigger an update check (--force-update/--update-now) and
|
||||
// poke a config reload (--reload-env) without admin rights. The update
|
||||
// payload is signature-verified regardless of who asks; update triggers
|
||||
// are rate-limited to one per minute so the channel cannot be used to spam
|
||||
// restarts. u is nil when AUTO_UPDATE=false.
|
||||
func serveControl(ctx context.Context, log *slog.Logger, u *update.Updater, exePath string, applyStaged func(to string), status func() string, reload func() string) {
|
||||
// controlHandler builds the command handler shared by the control channel
|
||||
// (named pipe / unix socket) and the web UI's action endpoints: status for
|
||||
// --monitor / the UI, an update check (--force-update/--update-now) and a
|
||||
// config reload (--reload-env), all safe for unprivileged local users. The
|
||||
// update payload is signature-verified regardless of who asks; update
|
||||
// triggers are rate-limited to one per minute and reloads to one per two
|
||||
// seconds so neither can be used to spam restarts or disk churn. u is nil
|
||||
// when AUTO_UPDATE=false.
|
||||
func controlHandler(ctx context.Context, u *update.Updater, exePath string, applyStaged func(to string), status func() string, reload func() string) control.Handler {
|
||||
var mu sync.Mutex
|
||||
var lastTrigger, lastReload time.Time
|
||||
h := func(cmd string) string {
|
||||
return func(cmd string) string {
|
||||
switch cmd {
|
||||
case control.CmdStatus:
|
||||
return "OK " + status()
|
||||
@@ -1075,6 +1088,12 @@ func serveControl(ctx context.Context, log *slog.Logger, u *update.Updater, exeP
|
||||
applyStaged(to)
|
||||
return "OK updated from " + version + " to " + to + "; the service restarts once the GPU is idle"
|
||||
}
|
||||
}
|
||||
|
||||
// serveControl opens the local control channel (named pipe on Windows,
|
||||
// unix socket on Linux) for unprivileged local users (--monitor,
|
||||
// --force-update/--update-now, --reload-env).
|
||||
func serveControl(ctx context.Context, log *slog.Logger, h control.Handler) {
|
||||
if err := control.Serve(ctx, h, log); err != nil {
|
||||
log.Warn("control channel disabled", "err", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user