Skip to content

Commit 048f29a

Browse files
committed
feat: Add stage to build logs
This adds a stage property to logs, and refactors the job logs cliui. It also adds tests to the cliui for build logs!
1 parent 4448ba2 commit 048f29a

29 files changed

+581
-262
lines changed

cli/cliui/cliui.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ func ValidateNotEmpty(s string) error {
2323
// Styles compose visual elements of the UI!
2424
var Styles = struct {
2525
Bold,
26+
Checkmark,
2627
Code,
28+
Crossmark,
2729
Field,
2830
Keyword,
2931
Paragraph,
@@ -36,7 +38,9 @@ var Styles = struct {
3638
Wrap lipgloss.Style
3739
}{
3840
Bold: lipgloss.NewStyle().Bold(true),
41+
Checkmark: defaultStyles.Checkmark,
3942
Code: defaultStyles.Code,
43+
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
4044
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
4145
Keyword: defaultStyles.Keyword,
4246
Paragraph: defaultStyles.Paragraph,

cli/cliui/job.go

Lines changed: 0 additions & 157 deletions
This file was deleted.

cli/cliui/provisionerjob.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cliui
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"sync"
9+
"time"
10+
11+
"github.com/spf13/cobra"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/coderd/database"
15+
"github.com/coder/coder/codersdk"
16+
)
17+
18+
type ProvisionerJobOptions struct {
19+
Fetch func() (codersdk.ProvisionerJob, error)
20+
Cancel func() error
21+
Logs func() (<-chan codersdk.ProvisionerJobLog, error)
22+
23+
FetchInterval time.Duration
24+
}
25+
26+
// ProvisionerJob renders a provisioner job with interactive cancellation.
27+
func ProvisionerJob(cmd *cobra.Command, opts ProvisionerJobOptions) error {
28+
if opts.FetchInterval == 0 {
29+
opts.FetchInterval = time.Second
30+
}
31+
32+
var (
33+
currentStage = "Queued"
34+
currentStageStartedAt = time.Now().UTC()
35+
didLogBetweenStage = false
36+
ctx, cancelFunc = context.WithCancel(cmd.Context())
37+
38+
errChan = make(chan error)
39+
job codersdk.ProvisionerJob
40+
jobMutex sync.Mutex
41+
)
42+
defer cancelFunc()
43+
44+
printStage := func() {
45+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
46+
}
47+
printStage()
48+
49+
updateStage := func(stage string, startedAt time.Time) {
50+
if currentStage != "" {
51+
prefix := ""
52+
if !didLogBetweenStage {
53+
prefix = "\033[1A\r"
54+
}
55+
mark := Styles.Checkmark
56+
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
57+
mark = Styles.Crossmark
58+
}
59+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
60+
}
61+
if stage == "" {
62+
return
63+
}
64+
currentStage = stage
65+
currentStageStartedAt = startedAt
66+
didLogBetweenStage = false
67+
printStage()
68+
}
69+
70+
updateJob := func() {
71+
var err error
72+
jobMutex.Lock()
73+
defer jobMutex.Unlock()
74+
job, err = opts.Fetch()
75+
if err != nil {
76+
errChan <- xerrors.Errorf("fetch: %w", err)
77+
return
78+
}
79+
if job.StartedAt == nil {
80+
return
81+
}
82+
if currentStage != "Queued" {
83+
// If another stage is already running, there's no need
84+
// for us to notify the user we're running!
85+
return
86+
}
87+
updateStage("Running", *job.StartedAt)
88+
}
89+
updateJob()
90+
91+
// Handles ctrl+c to cancel a job.
92+
stopChan := make(chan os.Signal, 1)
93+
defer signal.Stop(stopChan)
94+
go func() {
95+
signal.Notify(stopChan, os.Interrupt)
96+
select {
97+
case <-ctx.Done():
98+
return
99+
case _, ok := <-stopChan:
100+
if !ok {
101+
return
102+
}
103+
}
104+
// Stop listening for signals so another one kills it!
105+
signal.Stop(stopChan)
106+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling... wait for exit or data loss may occur!")+"\n\n")
107+
err := opts.Cancel()
108+
if err != nil {
109+
errChan <- xerrors.Errorf("cancel: %w", err)
110+
return
111+
}
112+
updateJob()
113+
}()
114+
115+
logs, err := opts.Logs()
116+
if err != nil {
117+
return xerrors.Errorf("logs: %w", err)
118+
}
119+
120+
ticker := time.NewTicker(opts.FetchInterval)
121+
for {
122+
select {
123+
case err = <-errChan:
124+
return err
125+
case <-ctx.Done():
126+
return ctx.Err()
127+
case <-ticker.C:
128+
updateJob()
129+
case log, ok := <-logs:
130+
if !ok {
131+
// The logs stream will end when the job does,
132+
// so it's safe to
133+
updateJob()
134+
jobMutex.Lock()
135+
if job.CompletedAt != nil {
136+
updateStage("", *job.CompletedAt)
137+
}
138+
switch job.Status {
139+
case codersdk.ProvisionerJobCanceled:
140+
jobMutex.Unlock()
141+
return Canceled
142+
case codersdk.ProvisionerJobSucceeded:
143+
jobMutex.Unlock()
144+
return nil
145+
case codersdk.ProvisionerJobFailed:
146+
}
147+
jobMutex.Unlock()
148+
return xerrors.New(job.Error)
149+
}
150+
output := ""
151+
switch log.Level {
152+
case database.LogLevelTrace, database.LogLevelDebug, database.LogLevelError:
153+
output = defaultStyles.Error.Render(log.Output)
154+
case database.LogLevelWarn:
155+
output = Styles.Warn.Render(log.Output)
156+
case database.LogLevelInfo:
157+
output = log.Output
158+
}
159+
if log.Stage != currentStage && log.Stage != "" {
160+
jobMutex.Lock()
161+
updateStage(log.Stage, log.CreatedAt)
162+
jobMutex.Unlock()
163+
continue
164+
}
165+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", Styles.Placeholder.Render(" "), output)
166+
didLogBetweenStage = true
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)