package webui import ( "sync" "time" ) // jobState holds the output lines and completion status of an async job. type jobState struct { lines []string done bool err string mu sync.Mutex subs []chan string cancel func() // optional cancel function; nil if job is not cancellable } // abort cancels the job if it has a cancel function and is not yet done. func (j *jobState) abort() bool { j.mu.Lock() defer j.mu.Unlock() if j.done || j.cancel == nil { return false } j.cancel() return true } func (j *jobState) append(line string) { j.mu.Lock() defer j.mu.Unlock() j.lines = append(j.lines, line) for _, ch := range j.subs { select { case ch <- line: default: } } } func (j *jobState) finish(errMsg string) { j.mu.Lock() defer j.mu.Unlock() j.done = true j.err = errMsg for _, ch := range j.subs { close(ch) } j.subs = nil } // subscribe returns a channel that receives all future lines. // Existing lines are returned first, then the channel streams new ones. func (j *jobState) subscribe() ([]string, <-chan string) { j.mu.Lock() defer j.mu.Unlock() existing := make([]string, len(j.lines)) copy(existing, j.lines) if j.done { return existing, nil } ch := make(chan string, 256) j.subs = append(j.subs, ch) return existing, ch } // jobManager manages async jobs identified by string IDs. type jobManager struct { mu sync.Mutex jobs map[string]*jobState } var globalJobs = &jobManager{jobs: make(map[string]*jobState)} func (m *jobManager) create(id string) *jobState { m.mu.Lock() defer m.mu.Unlock() j := &jobState{} m.jobs[id] = j // Schedule cleanup after 30 minutes go func() { time.Sleep(30 * time.Minute) m.mu.Lock() delete(m.jobs, id) m.mu.Unlock() }() return j } // isDone returns true if the job has finished (either successfully or with error). func (j *jobState) isDone() bool { j.mu.Lock() defer j.mu.Unlock() return j.done } func (m *jobManager) get(id string) (*jobState, bool) { m.mu.Lock() defer m.mu.Unlock() j, ok := m.jobs[id] return j, ok }