-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds a progressbar interface and implementations: 1. pbProgressBar based on github.com/cheggaaa/pb/v3 2. debugProgressBar to just print the osbuild/images status 3. nullProgressBar for tests etc to just silence the progress
- Loading branch information
Showing
3 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package progress | ||
|
||
type ( | ||
PbProgressBar = pbProgressBar | ||
DebugProgressBar = debugProgressBar | ||
NullProgressBar = nullProgressBar | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
package progress | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
|
||
"github.com/cheggaaa/pb/v3" | ||
|
||
"github.com/osbuild/images/pkg/osbuild" | ||
) | ||
|
||
// ProgressBar is an interfacs for progress reporting when there is | ||
// an arbitrary amount of sub-progress information (like osbuild) | ||
type ProgressBar interface { | ||
// SetProgress sets the progress details at the given "level". | ||
// Levels should start with "0" and increase as the nesting | ||
// gets deeper. | ||
SetProgress(level int, msg string, done int, total int) error | ||
// The high-level message that is displayed in a spinner | ||
// (e.g. "Building image foo") | ||
SetPulseMsg(fmt string, args ...interface{}) | ||
// A high level message with the last high level status | ||
// (e.g. "Started downloading") | ||
SetMessage(fmt string, args ...interface{}) | ||
Start() error | ||
Stop() error | ||
} | ||
|
||
// New creates a new progressbar based on the requested type | ||
func New(progress string) ProgressBar { | ||
switch progress { | ||
case "", "null": | ||
return NewNullProgressBar() | ||
case "text": | ||
return NewProgressBar() | ||
case "debug": | ||
return NewDebugProgressBar() | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
type pbProgressBar struct { | ||
spinnerPb *pb.ProgressBar | ||
msgPb *pb.ProgressBar | ||
subLevelPbs []*pb.ProgressBar | ||
|
||
pool *pb.Pool | ||
startedMain bool | ||
startedSub bool | ||
} | ||
|
||
// NewProgressBar creates a new default pb3 based progressbar suitable for | ||
// most terminals. | ||
func NewProgressBar() ProgressBar { | ||
ppb := &pbProgressBar{} | ||
ppb.spinnerPb = pb.New(0) | ||
ppb.spinnerPb.SetTemplate(`[{{ (cycle . "|" "/" "-" "\\") }}] {{ string . "spinnerMsg" }}`) | ||
ppb.msgPb = pb.New(0) | ||
ppb.msgPb.SetTemplate(`Message: {{ string . "msg" }}`) | ||
return ppb | ||
} | ||
|
||
func (ppb *pbProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { | ||
// auto-add as needed, requires sublevels to get added in order | ||
// i.e. adding 0 and then 2 will fail | ||
switch { | ||
case subLevel == len(ppb.subLevelPbs): | ||
apb := pb.New(0) | ||
ppb.subLevelPbs = append(ppb.subLevelPbs, apb) | ||
progressBarTmpl := `[{{ counters . }}] {{ string . "prefix" }} {{ bar .}} {{ percent . }}` | ||
apb.SetTemplateString(progressBarTmpl) | ||
ppb.pool.Add(apb) | ||
case subLevel > len(ppb.subLevelPbs): | ||
return fmt.Errorf("sublevel added out of order, have %v sublevels but want level %v", len(ppb.subLevelPbs), subLevel) | ||
} | ||
apb := ppb.subLevelPbs[subLevel] | ||
apb.SetTotal(int64(total) + 1) | ||
apb.SetCurrent(int64(done) + 1) | ||
if msg != "" { | ||
apb.Set("prefix", msg) | ||
} | ||
return nil | ||
} | ||
|
||
func shorten(msg string) string { | ||
msg = strings.Replace(msg, "\n", " ", -1) | ||
// XXX: make this smarter | ||
if len(msg) > 60 { | ||
return msg[:60] + "..." | ||
} | ||
return msg | ||
} | ||
|
||
func (ppb *pbProgressBar) SetPulseMsg(msg string, args ...interface{}) { | ||
ppb.spinnerPb.Set("spinnerMsg", shorten(fmt.Sprintf(msg, args...))) | ||
} | ||
|
||
func (ppb *pbProgressBar) SetMessage(msg string, args ...interface{}) { | ||
ppb.msgPb.Set("msg", shorten(fmt.Sprintf(msg, args...))) | ||
} | ||
|
||
func (ppb *pbProgressBar) Start() (err error) { | ||
ppb.pool, err = pb.StartPool(ppb.spinnerPb, ppb.msgPb) | ||
if err != nil { | ||
return fmt.Errorf("progress bar failed: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (ppb *pbProgressBar) Stop() (err error) { | ||
return ppb.pool.Stop() | ||
} | ||
|
||
type nullProgressBar struct { | ||
w io.Writer | ||
} | ||
|
||
// NewNullProgressBar starts a new "empty" progressbar that will just | ||
// do nothing. | ||
func NewNullProgressBar() ProgressBar { | ||
np := &nullProgressBar{w: os.Stderr} | ||
return np | ||
} | ||
|
||
func (np *nullProgressBar) SetPulseMsg(msg string, args ...interface{}) { | ||
} | ||
|
||
func (np *nullProgressBar) SetMessage(msg string, args ...interface{}) { | ||
} | ||
|
||
func (np *nullProgressBar) Start() (err error) { | ||
return nil | ||
} | ||
|
||
func (np *nullProgressBar) Stop() (err error) { | ||
return nil | ||
} | ||
|
||
func (np *nullProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { | ||
return nil | ||
} | ||
|
||
type debugProgressBar struct { | ||
w io.Writer | ||
} | ||
|
||
// NewDebugProgressBar will create a progressbar aimed to debug the | ||
// lower level osbuild/images message. It will never clear the screen | ||
// so "glitches/weird" messages from the lower-layers can be inspected | ||
// easier. | ||
func NewDebugProgressBar() ProgressBar { | ||
np := &debugProgressBar{w: os.Stderr} | ||
return np | ||
} | ||
|
||
func (np *debugProgressBar) SetPulseMsg(msg string, args ...interface{}) { | ||
fmt.Fprintf(np.w, msg, args...) | ||
fmt.Fprintf(np.w, "\n") | ||
} | ||
|
||
func (np *debugProgressBar) SetMessage(msg string, args ...interface{}) { | ||
fmt.Fprintf(np.w, msg, args...) | ||
fmt.Fprintf(np.w, "\n") | ||
} | ||
|
||
func (np *debugProgressBar) Start() (err error) { | ||
fmt.Fprintf(np.w, "Start\n") | ||
return nil | ||
} | ||
|
||
func (np *debugProgressBar) Stop() (err error) { | ||
fmt.Fprintf(np.w, "Stop\n") | ||
return nil | ||
} | ||
|
||
func (np *debugProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { | ||
fmt.Fprintf(np.w, "%s[%v / %v], %s", strings.Repeat(" ", subLevel), done, total, msg) | ||
fmt.Fprintf(np.w, "\n") | ||
return nil | ||
} | ||
|
||
// XXX: merge variant back into images/pkg/osbuild/osbuild-exec.go | ||
func RunOSBuild(pb ProgressBar, manifest []byte, store, outputDirectory string, exports, extraEnv []string) error { | ||
switch pb.(type) { | ||
case *pbProgressBar, *debugProgressBar: | ||
return runOSBuildNew(pb, manifest, store, outputDirectory, exports, extraEnv) | ||
default: | ||
return runOSBuildOld(pb, manifest, store, outputDirectory, exports, extraEnv) | ||
} | ||
} | ||
|
||
func runOSBuildOld(pb ProgressBar, manifest []byte, store, outputDirectory string, exports, extraEnv []string) error { | ||
_, err := osbuild.RunOSBuild(manifest, store, outputDirectory, exports, nil, extraEnv, false, os.Stderr) | ||
return err | ||
} | ||
|
||
func runOSBuildNew(pb ProgressBar, manifest []byte, store, outputDirectory string, exports, extraEnv []string) error { | ||
rp, wp, err := os.Pipe() | ||
if err != nil { | ||
return fmt.Errorf("cannot create pipe for osbuild: %w", err) | ||
} | ||
defer rp.Close() | ||
defer wp.Close() | ||
|
||
cmd := exec.Command( | ||
"osbuild", | ||
"--store", store, | ||
"--output-directory", outputDirectory, | ||
"--monitor=JSONSeqMonitor", | ||
"--monitor-fd=3", | ||
"-", | ||
) | ||
for _, export := range exports { | ||
cmd.Args = append(cmd.Args, "--export", export) | ||
} | ||
|
||
cmd.Env = append(os.Environ(), extraEnv...) | ||
cmd.Stdin = bytes.NewBuffer(manifest) | ||
cmd.Stderr = os.Stderr | ||
// we could use "--json" here and would get the build-result | ||
// exported here | ||
cmd.Stdout = nil | ||
cmd.ExtraFiles = []*os.File{wp} | ||
|
||
osbuildStatus := osbuild.NewStatusScanner(rp) | ||
if err := cmd.Start(); err != nil { | ||
return fmt.Errorf("error starting osbuild: %v", err) | ||
} | ||
wp.Close() | ||
|
||
var tracesMsgs []string | ||
for { | ||
st, err := osbuildStatus.Status() | ||
if err != nil { | ||
return fmt.Errorf("error reading osbuild status: %w", err) | ||
} | ||
if st == nil { | ||
break | ||
} | ||
i := 0 | ||
for p := st.Progress; p != nil; p = p.SubProgress { | ||
// XXX: osbuild gives us bad progress messages | ||
pb.SetProgress(i, p.Message, p.Done, p.Total) | ||
i++ | ||
} | ||
// keep the messages/traces for better error reporting | ||
if st.Message != "" { | ||
tracesMsgs = append(tracesMsgs, st.Message) | ||
} | ||
if st.Trace != "" { | ||
tracesMsgs = append(tracesMsgs, st.Trace) | ||
} | ||
// forward to user | ||
if st.Message != "" { | ||
pb.SetMessage(st.Message) | ||
} | ||
} | ||
|
||
if err := cmd.Wait(); err != nil { | ||
return fmt.Errorf("error running osbuild: %w\nLog:\n%s", err, strings.Join(tracesMsgs, "\n")) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package progress_test | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/osbuild/bootc-image-builder/bib/internal/progress" | ||
) | ||
|
||
func TestProgressNew(t *testing.T) { | ||
for _, tc := range []struct { | ||
Type string | ||
Expected interface{} | ||
}{ | ||
{"text", &progress.PbProgressBar{}}, | ||
{"debug", &progress.DebugProgressBar{}}, | ||
{"null", &progress.NullProgressBar{}}, | ||
{"bad", nil}, | ||
} { | ||
pb := progress.New(tc.Type) | ||
assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.Expected), fmt.Sprintf("[%v] %T not the expected %T", tc.Type, pb, tc.Expected)) | ||
} | ||
} |