Skip to content

Commit 4bb7597

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 4bb7597

28 files changed

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

0 commit comments

Comments
 (0)