mirror of https://github.com/coder/coder.git
268 lines
6.0 KiB
Go
268 lines
6.0 KiB
Go
package cliui
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID) error {
|
|
return ProvisionerJob(ctx, writer, ProvisionerJobOptions{
|
|
Fetch: func() (codersdk.ProvisionerJob, error) {
|
|
build, err := client.WorkspaceBuild(ctx, build)
|
|
return build.Job, err
|
|
},
|
|
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
|
|
return client.WorkspaceBuildLogsAfter(ctx, build, 0)
|
|
},
|
|
})
|
|
}
|
|
|
|
type ProvisionerJobOptions struct {
|
|
Fetch func() (codersdk.ProvisionerJob, error)
|
|
Cancel func() error
|
|
Logs func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error)
|
|
|
|
FetchInterval time.Duration
|
|
// Verbose determines whether debug and trace logs will be shown.
|
|
Verbose bool
|
|
// Silent determines whether log output will be shown unless there is an
|
|
// error.
|
|
Silent bool
|
|
}
|
|
|
|
type ProvisionerJobError struct {
|
|
Message string
|
|
Code codersdk.JobErrorCode
|
|
}
|
|
|
|
var _ error = new(ProvisionerJobError)
|
|
|
|
func (err *ProvisionerJobError) Error() string {
|
|
return err.Message
|
|
}
|
|
|
|
// ProvisionerJob renders a provisioner job with interactive cancellation.
|
|
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
|
|
if opts.FetchInterval == 0 {
|
|
opts.FetchInterval = time.Second
|
|
}
|
|
ctx, cancelFunc := context.WithCancel(ctx)
|
|
defer cancelFunc()
|
|
|
|
var (
|
|
currentStage = "Queued"
|
|
currentStageStartedAt = time.Now().UTC()
|
|
|
|
errChan = make(chan error, 1)
|
|
job codersdk.ProvisionerJob
|
|
jobMutex sync.Mutex
|
|
)
|
|
|
|
sw := &stageWriter{w: writer, verbose: opts.Verbose, silentLogs: opts.Silent}
|
|
|
|
printStage := func() {
|
|
sw.Start(currentStage)
|
|
}
|
|
|
|
updateStage := func(stage string, startedAt time.Time) {
|
|
if currentStage != "" {
|
|
duration := startedAt.Sub(currentStageStartedAt)
|
|
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
|
|
sw.Fail(currentStage, duration)
|
|
} else {
|
|
sw.Complete(currentStage, duration)
|
|
}
|
|
}
|
|
if stage == "" {
|
|
return
|
|
}
|
|
currentStage = stage
|
|
currentStageStartedAt = startedAt
|
|
printStage()
|
|
}
|
|
|
|
updateJob := func() {
|
|
var err error
|
|
jobMutex.Lock()
|
|
defer jobMutex.Unlock()
|
|
job, err = opts.Fetch()
|
|
if err != nil {
|
|
errChan <- xerrors.Errorf("fetch: %w", err)
|
|
return
|
|
}
|
|
if job.StartedAt == nil {
|
|
return
|
|
}
|
|
if currentStage != "Queued" {
|
|
// If another stage is already running, there's no need
|
|
// for us to notify the user we're running!
|
|
return
|
|
}
|
|
updateStage("Running", *job.StartedAt)
|
|
}
|
|
|
|
if opts.Cancel != nil {
|
|
// Handles ctrl+c to cancel a job.
|
|
stopChan := make(chan os.Signal, 1)
|
|
signal.Notify(stopChan, os.Interrupt)
|
|
go func() {
|
|
defer signal.Stop(stopChan)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case _, ok := <-stopChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
_, _ = fmt.Fprintf(writer, DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
|
|
err := opts.Cancel()
|
|
if err != nil {
|
|
errChan <- xerrors.Errorf("cancel: %w", err)
|
|
return
|
|
}
|
|
updateJob()
|
|
}()
|
|
}
|
|
|
|
// The initial stage needs to print after the signal handler has been registered.
|
|
printStage()
|
|
updateJob()
|
|
|
|
logs, closer, err := opts.Logs()
|
|
if err != nil {
|
|
return xerrors.Errorf("begin streaming logs: %w", err)
|
|
}
|
|
defer closer.Close()
|
|
|
|
ticker := time.NewTicker(opts.FetchInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case err = <-errChan:
|
|
sw.Fail(currentStage, time.Since(currentStageStartedAt))
|
|
return err
|
|
case <-ctx.Done():
|
|
sw.Fail(currentStage, time.Since(currentStageStartedAt))
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
updateJob()
|
|
case log, ok := <-logs:
|
|
if !ok {
|
|
updateJob()
|
|
jobMutex.Lock()
|
|
if job.CompletedAt != nil {
|
|
updateStage("", *job.CompletedAt)
|
|
}
|
|
switch job.Status {
|
|
case codersdk.ProvisionerJobCanceled:
|
|
jobMutex.Unlock()
|
|
return Canceled
|
|
case codersdk.ProvisionerJobSucceeded:
|
|
jobMutex.Unlock()
|
|
return nil
|
|
case codersdk.ProvisionerJobFailed:
|
|
}
|
|
err = &ProvisionerJobError{
|
|
Message: job.Error,
|
|
Code: job.ErrorCode,
|
|
}
|
|
sw.Fail(currentStage, time.Since(currentStageStartedAt))
|
|
jobMutex.Unlock()
|
|
return err
|
|
}
|
|
|
|
jobMutex.Lock()
|
|
if log.Stage != currentStage && log.Stage != "" {
|
|
updateStage(log.Stage, log.CreatedAt)
|
|
jobMutex.Unlock()
|
|
continue
|
|
}
|
|
sw.Log(log.CreatedAt, log.Level, log.Output)
|
|
jobMutex.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
type stageWriter struct {
|
|
w io.Writer
|
|
verbose bool
|
|
silentLogs bool
|
|
logBuf bytes.Buffer
|
|
}
|
|
|
|
func (s *stageWriter) Start(stage string) {
|
|
_, _ = fmt.Fprintf(s.w, "==> ⧗ %s\n", stage)
|
|
}
|
|
|
|
func (s *stageWriter) Complete(stage string, duration time.Duration) {
|
|
s.end(stage, duration, true)
|
|
}
|
|
|
|
func (s *stageWriter) Fail(stage string, duration time.Duration) {
|
|
s.flushLogs()
|
|
s.end(stage, duration, false)
|
|
}
|
|
|
|
//nolint:revive
|
|
func (s *stageWriter) end(stage string, duration time.Duration, ok bool) {
|
|
s.logBuf.Reset()
|
|
|
|
mark := "✔"
|
|
if !ok {
|
|
mark = "✘"
|
|
}
|
|
if duration < 0 {
|
|
duration = 0
|
|
}
|
|
_, _ = fmt.Fprintf(s.w, "=== %s %s [%dms]\n", mark, stage, duration.Milliseconds())
|
|
}
|
|
|
|
func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line string) {
|
|
w := s.w
|
|
if s.silentLogs {
|
|
w = &s.logBuf
|
|
}
|
|
|
|
render := func(s ...string) string { return strings.Join(s, " ") }
|
|
|
|
var lines []string
|
|
if !createdAt.IsZero() {
|
|
lines = append(lines, createdAt.Local().Format("2006-01-02 15:04:05.000Z07:00"))
|
|
}
|
|
lines = append(lines, line)
|
|
|
|
switch level {
|
|
case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
|
|
if !s.verbose {
|
|
return
|
|
}
|
|
render = DefaultStyles.Placeholder.Render
|
|
case codersdk.LogLevelError:
|
|
render = DefaultStyles.Error.Render
|
|
case codersdk.LogLevelWarn:
|
|
render = DefaultStyles.Warn.Render
|
|
case codersdk.LogLevelInfo:
|
|
}
|
|
_, _ = fmt.Fprintf(w, "%s\n", render(lines...))
|
|
}
|
|
|
|
func (s *stageWriter) flushLogs() {
|
|
if s.silentLogs {
|
|
_, _ = io.Copy(s.w, &s.logBuf)
|
|
}
|
|
s.logBuf.Reset()
|
|
}
|