feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com> Co-authored-by: arturict <arturict@users.noreply.github.com> Co-authored-by: Thomas <thomas@ajnart.fr> Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com> Co-authored-by: Danura30082 <danura30082+github@gmail.com> Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com> Co-authored-by: ajnart <thomas@ajnart.fr> Co-authored-by: ajnart <thomas@ajnart.dev> Co-authored-by: chenjun <chenjun@foxmail.com> Co-authored-by: Wikibear <wikibear@users.noreply.github.com> Co-authored-by: Bergasha <bergasha@gmail.com> Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com> Co-authored-by: Lucas Araujo <lucas.lra@gmail.com> Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com> Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social> Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com> Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com> Co-authored-by: thrawnx <tight.bulb9384@fastmail.com> Co-authored-by: Chris <58918656+nordada@users.noreply.github.com> Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com> Co-authored-by: Yanis <naamaneyanis@gmail.com> Co-authored-by: Wikibear <me@sej.de> Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
This commit is contained in:
3286 files changed
+284386
-139176
No files matched your search
@@ -0,0 +1,360 @@
|
||||
// Package task runs the CLI's slow work — image pulls, local builds, container
|
||||
// actions — on background goroutines and exposes it as immutable snapshots.
|
||||
//
|
||||
// Nothing in this package blocks the caller. A Manager owns every running job,
|
||||
// coalesces their updates into a single wake-up channel, and hands the UI a
|
||||
// consistent view whenever it asks for one. That separation is what keeps the
|
||||
// terminal responsive while a fifteen minute Docker build streams output.
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Kind classifies a job so the UI can pick an icon and a detail renderer.
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
KindPull Kind = iota
|
||||
KindBuild
|
||||
KindAction
|
||||
)
|
||||
|
||||
var kindNames = map[Kind]string{
|
||||
KindPull: "pull",
|
||||
KindBuild: "build",
|
||||
KindAction: "action",
|
||||
}
|
||||
|
||||
func (k Kind) String() string {
|
||||
if name, found := kindNames[k]; found {
|
||||
return name
|
||||
}
|
||||
return "action"
|
||||
}
|
||||
|
||||
// State is the lifecycle stage of a job.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateRunning State = iota
|
||||
StateSucceeded
|
||||
StateFailed
|
||||
StateCanceled
|
||||
)
|
||||
|
||||
func (s State) Done() bool { return s != StateRunning }
|
||||
|
||||
var stateNames = map[State]string{
|
||||
StateRunning: "running",
|
||||
StateSucceeded: "succeeded",
|
||||
StateFailed: "failed",
|
||||
StateCanceled: "canceled",
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
if name, found := stateNames[s]; found {
|
||||
return name
|
||||
}
|
||||
return "canceled"
|
||||
}
|
||||
|
||||
// Step is one unit of progress inside a job: a Docker layer during a pull, or a
|
||||
// BuildKit vertex during a build.
|
||||
type Step struct {
|
||||
ID string
|
||||
Label string
|
||||
Status string
|
||||
Current int64
|
||||
Total int64
|
||||
Percent float64
|
||||
Done bool
|
||||
Failed bool
|
||||
Note string
|
||||
}
|
||||
|
||||
// Snapshot is an immutable view of a job. The UI only ever sees snapshots, so
|
||||
// it can render without holding a lock or racing the worker goroutine.
|
||||
type Snapshot struct {
|
||||
ID int
|
||||
Kind Kind
|
||||
Title string
|
||||
Target string
|
||||
Detail string
|
||||
State State
|
||||
Err error
|
||||
Percent float64
|
||||
Steps []Step
|
||||
Lines []string
|
||||
Started time.Time
|
||||
Ended time.Time
|
||||
Cancellable bool
|
||||
}
|
||||
|
||||
// Elapsed reports how long the job ran, or has been running.
|
||||
func (s Snapshot) Elapsed() time.Duration {
|
||||
if s.Ended.IsZero() {
|
||||
return time.Since(s.Started)
|
||||
}
|
||||
return s.Ended.Sub(s.Started)
|
||||
}
|
||||
|
||||
// Reporter is the handle a job body uses to publish progress. Every method is
|
||||
// safe to call from any goroutine and never blocks on the UI.
|
||||
type Reporter struct {
|
||||
task *Task
|
||||
}
|
||||
|
||||
// Log appends a line of output. Only the most recent lines are retained.
|
||||
func (r *Reporter) Log(line string) {
|
||||
r.task.mutate(func(t *Task) { t.lines = appendLine(t.lines, line) })
|
||||
}
|
||||
|
||||
// SetSteps replaces the job's progress breakdown.
|
||||
func (r *Reporter) SetSteps(steps []Step) {
|
||||
r.task.mutate(func(t *Task) { t.steps = steps })
|
||||
}
|
||||
|
||||
// SetPercent records overall progress in the range [0,1].
|
||||
func (r *Reporter) SetPercent(percent float64) {
|
||||
r.task.mutate(func(t *Task) { t.percent = clamp(percent) })
|
||||
}
|
||||
|
||||
// SetDetail replaces the single-line summary shown next to the job.
|
||||
func (r *Reporter) SetDetail(detail string) {
|
||||
r.task.mutate(func(t *Task) { t.detail = detail })
|
||||
}
|
||||
|
||||
// Lines returns the retained output, newest last.
|
||||
func (r *Reporter) Lines() []string {
|
||||
r.task.mu.Lock()
|
||||
defer r.task.mu.Unlock()
|
||||
return append([]string(nil), r.task.lines...)
|
||||
}
|
||||
|
||||
// Body is the work a job performs. Returning an error fails the job; returning
|
||||
// ctx.Err() after cancellation marks it canceled instead.
|
||||
type Body func(ctx context.Context, report *Reporter) error
|
||||
|
||||
// Task is a single background job.
|
||||
type Task struct {
|
||||
mu sync.Mutex
|
||||
manager *Manager
|
||||
|
||||
id int
|
||||
kind Kind
|
||||
title string
|
||||
target string
|
||||
detail string
|
||||
state State
|
||||
err error
|
||||
percent float64
|
||||
steps []Step
|
||||
lines []string
|
||||
started time.Time
|
||||
ended time.Time
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
const retainedLines = 500
|
||||
|
||||
func appendLine(lines []string, line string) []string {
|
||||
lines = append(lines, line)
|
||||
if len(lines) > retainedLines {
|
||||
lines = lines[len(lines)-retainedLines:]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func clamp(value float64) float64 {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 1 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (t *Task) mutate(apply func(*Task)) {
|
||||
t.mu.Lock()
|
||||
apply(t)
|
||||
t.mu.Unlock()
|
||||
t.manager.touch()
|
||||
}
|
||||
|
||||
func (t *Task) snapshot() Snapshot {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return Snapshot{
|
||||
ID: t.id,
|
||||
Kind: t.kind,
|
||||
Title: t.title,
|
||||
Target: t.target,
|
||||
Detail: t.detail,
|
||||
State: t.state,
|
||||
Err: t.err,
|
||||
Percent: t.percent,
|
||||
Steps: append([]Step(nil), t.steps...),
|
||||
Lines: append([]string(nil), t.lines...),
|
||||
Started: t.started,
|
||||
Ended: t.ended,
|
||||
Cancellable: t.cancel != nil && t.state == StateRunning,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the job's identifier, stable for the lifetime of the process.
|
||||
func (t *Task) ID() int { return t.id }
|
||||
|
||||
// Manager owns every background job and notifies a single listener when any of
|
||||
// them changes.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
tasks []*Task
|
||||
nextID int
|
||||
notify chan struct{}
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{notify: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
// Changed is a coalescing wake-up channel. It receives at most one pending
|
||||
// notification, so a job that emits thousands of progress updates per second
|
||||
// still costs the UI a single redraw per frame.
|
||||
func (m *Manager) Changed() <-chan struct{} { return m.notify }
|
||||
|
||||
func (m *Manager) touch() {
|
||||
select {
|
||||
case m.notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Start launches a job and returns immediately.
|
||||
func (m *Manager) Start(kind Kind, title, target string, body Body) *Task {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.mu.Lock()
|
||||
m.nextID++
|
||||
task := &Task{
|
||||
manager: m,
|
||||
id: m.nextID,
|
||||
kind: kind,
|
||||
title: title,
|
||||
target: target,
|
||||
state: StateRunning,
|
||||
started: time.Now(),
|
||||
cancel: cancel,
|
||||
}
|
||||
m.tasks = append(m.tasks, task)
|
||||
m.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
err := body(ctx, &Reporter{task: task})
|
||||
task.mu.Lock()
|
||||
task.ended = time.Now()
|
||||
task.cancel = nil
|
||||
switch {
|
||||
case ctx.Err() != nil:
|
||||
task.state = StateCanceled
|
||||
case err != nil:
|
||||
task.state, task.err = StateFailed, err
|
||||
default:
|
||||
task.state, task.percent = StateSucceeded, 1
|
||||
}
|
||||
task.mu.Unlock()
|
||||
cancel()
|
||||
m.touch()
|
||||
}()
|
||||
|
||||
m.touch()
|
||||
return task
|
||||
}
|
||||
|
||||
// Snapshots returns every job, oldest first.
|
||||
func (m *Manager) Snapshots() []Snapshot {
|
||||
m.mu.Lock()
|
||||
tasks := append([]*Task(nil), m.tasks...)
|
||||
m.mu.Unlock()
|
||||
snapshots := make([]Snapshot, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
snapshots = append(snapshots, task.snapshot())
|
||||
}
|
||||
return snapshots
|
||||
}
|
||||
|
||||
// Snapshot returns a single job by identifier.
|
||||
func (m *Manager) Snapshot(id int) (Snapshot, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, task := range m.tasks {
|
||||
if task.id == id {
|
||||
return task.snapshot(), true
|
||||
}
|
||||
}
|
||||
return Snapshot{}, false
|
||||
}
|
||||
|
||||
// Cancel asks a running job to stop.
|
||||
func (m *Manager) Cancel(id int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, task := range m.tasks {
|
||||
if task.id != id {
|
||||
continue
|
||||
}
|
||||
task.mu.Lock()
|
||||
cancel := task.cancel
|
||||
task.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CancelAll stops every running job, used when the program exits.
|
||||
func (m *Manager) CancelAll() {
|
||||
for _, snapshot := range m.Snapshots() {
|
||||
if snapshot.State == StateRunning {
|
||||
m.Cancel(snapshot.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Running counts jobs that have not finished.
|
||||
func (m *Manager) Running() int {
|
||||
count := 0
|
||||
for _, snapshot := range m.Snapshots() {
|
||||
if snapshot.State == StateRunning {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RunningFor reports whether a job is already working on the given target, so
|
||||
// the UI can refuse to start a duplicate build or pull.
|
||||
func (m *Manager) RunningFor(target string) (Snapshot, bool) {
|
||||
for _, snapshot := range m.Snapshots() {
|
||||
if snapshot.State == StateRunning && snapshot.Target == target {
|
||||
return snapshot, true
|
||||
}
|
||||
}
|
||||
return Snapshot{}, false
|
||||
}
|
||||
|
||||
// Prune drops finished jobs, keeping the history short.
|
||||
func (m *Manager) Prune() {
|
||||
m.mu.Lock()
|
||||
kept := m.tasks[:0]
|
||||
for _, task := range m.tasks {
|
||||
if task.snapshot().State == StateRunning {
|
||||
kept = append(kept, task)
|
||||
}
|
||||
}
|
||||
m.tasks = kept
|
||||
m.mu.Unlock()
|
||||
m.touch()
|
||||
}
|
||||
Reference in new issue
Block a user