// Package job выполняет длинные операции (установка сервера, обновление модов, // бэкап) по одной за раз, чтобы они не мешали друг другу. package job import ( "context" "errors" "sync" "time" ) // ErrBusy возвращается, когда другая задача ещё выполняется. var ErrBusy = errors.New("уже выполняется другая задача") // Status — что менеджер задач показывает веб-панели. type Status struct { Name string `json:"name"` Running bool `json:"running"` StartedAt int64 `json:"started_at"` FinishedAt int64 `json:"finished_at"` Error string `json:"error"` } // Func — тело задачи. Логирование идёт через log, отмена — через ctx. type Func func(ctx context.Context, log func(string)) error // Manager допускает только одну активную задачу. type Manager struct { logf func(stream, text string) mu sync.Mutex status Status cancel context.CancelFunc } // New создаёт менеджер задач; logf получает весь вывод задач. func New(logf func(stream, text string)) *Manager { return &Manager{logf: logf} } // Start запускает задачу в фоне. Если что-то уже выполняется — ErrBusy. func (m *Manager) Start(name string, fn Func) error { m.mu.Lock() if m.status.Running { m.mu.Unlock() return ErrBusy } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.status = Status{Name: name, Running: true, StartedAt: time.Now().Unix()} m.mu.Unlock() m.logf("task", "▶ "+name) go func() { defer cancel() err := fn(ctx, func(line string) { m.logf("task", line) }) m.mu.Lock() m.status.Running = false m.status.FinishedAt = time.Now().Unix() if err != nil { m.status.Error = err.Error() } m.mu.Unlock() if err != nil { m.logf("task", "✖ "+name+": "+err.Error()) return } m.logf("task", "✔ "+name+" — готово") }() return nil } // Cancel просит текущую задачу завершиться. func (m *Manager) Cancel() { m.mu.Lock() cancel := m.cancel running := m.status.Running m.mu.Unlock() if running && cancel != nil { cancel() } } // Status отдаёт состояние последней (или текущей) задачи. func (m *Manager) Status() Status { m.mu.Lock() defer m.mu.Unlock() return m.status } // Busy сообщает, занят ли менеджер. func (m *Manager) Busy() bool { m.mu.Lock() defer m.mu.Unlock() return m.status.Running }