diff --git a/agent/pkg/engine/supervisor.go b/agent/pkg/engine/supervisor.go index 12d6d5a..a7df0c7 100644 --- a/agent/pkg/engine/supervisor.go +++ b/agent/pkg/engine/supervisor.go @@ -21,6 +21,8 @@ import ( "net/http" "os" "os/exec" + "regexp" + "strconv" "strings" "sync" "time" @@ -77,6 +79,7 @@ type Supervisor struct { restarts int cancel context.CancelFunc done chan struct{} + progress Progress } func New(opts Options) *Supervisor { @@ -110,6 +113,37 @@ func (s *Supervisor) Start() { go s.supervise(ctx, done) } +// Progress is what the engine is busy with before it answers - on first run, +// downloading ~275 MB of models. Empty once the engine is up. +type Progress struct { + What string `json:"what"` + Percent int `json:"percent"` +} + +var progressRe = regexp.MustCompile(`download: (.+?) (\d{1,3})%`) + +func (s *Supervisor) noteProgress(line string) { + m := progressRe.FindStringSubmatch(line) + if m == nil { + return + } + pct, _ := strconv.Atoi(m[2]) + s.mu.Lock() + if pct >= 100 { + s.progress = Progress{} + } else { + s.progress = Progress{What: m[1], Percent: pct} + } + s.mu.Unlock() +} + +// Progress reports the current first-run download, if any. +func (s *Supervisor) Progress() Progress { + s.mu.Lock() + defer s.mu.Unlock() + return s.progress +} + // Stop asks the engine to exit and waits for it. func (s *Supervisor) Stop() { s.mu.Lock() @@ -233,6 +267,7 @@ func (s *Supervisor) runOnce(ctx context.Context) error { for sc.Scan() { line := sc.Text() fmt.Fprintln(s.opts.LogWriter, line) + s.noteProgress(line) tailMu.Lock() tail = append(tail, line) if len(tail) > 12 { @@ -244,6 +279,9 @@ func (s *Supervisor) runOnce(ctx context.Context) error { s.setState(Running, nil) waitErr := cmd.Wait() + s.mu.Lock() + s.progress = Progress{} + s.mu.Unlock() <-pumped // A context cancel terminates the child through exec's own handling; the diff --git a/behavision/model_assets.py b/behavision/model_assets.py index 37c273c..29fecb3 100644 --- a/behavision/model_assets.py +++ b/behavision/model_assets.py @@ -35,6 +35,31 @@ _COPY_MAP = { } +def _fetch(url: str, dest: Path, label: str) -> None: + """Download with progress on stdout the supervisor can read. + + On first run this is minutes of nothing: the API is not up yet, so the + app cannot ask the engine what it is doing, and a shop PC that shows a + stopped engine for five minutes after install looks broken. The + supervisor watches for `download: