Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(progress): generic progress tracking #1524

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
36 changes: 12 additions & 24 deletions cmd/oras/internal/display/status/progress/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/cmd/oras/internal/display/status/console"
"oras.land/oras/internal/progress"
)

const (
Expand All @@ -34,24 +35,18 @@ const (

var errManagerStopped = errors.New("progress output manager has already been stopped")

// Manager is progress view master
type Manager interface {
Add() (*Messenger, error)
SendAndStop(desc ocispec.Descriptor, prompt string) error
Close() error
}

type manager struct {
status []*status
statusLock sync.RWMutex
console console.Console
updating sync.WaitGroup
renderDone chan struct{}
renderClosed chan struct{}
prompt map[progress.State]string
}

// NewManager initialized a new progress manager.
func NewManager(tty *os.File) (Manager, error) {
func NewManager(tty *os.File, prompt map[progress.State]string) (progress.Manager, error) {
c, err := console.NewConsole(tty)
if err != nil {
return nil, err
Expand All @@ -60,6 +55,7 @@ func NewManager(tty *os.File) (Manager, error) {
console: c,
renderDone: make(chan struct{}),
renderClosed: make(chan struct{}),
prompt: prompt,
}
m.start()
return m, nil
Expand Down Expand Up @@ -103,13 +99,13 @@ func (m *manager) render() {
}
}

// Add appends a new status with 2-line space for rendering.
func (m *manager) Add() (*Messenger, error) {
// Track appends a new status with 2-line space for rendering.
func (m *manager) Track(desc ocispec.Descriptor) (progress.Tracker, error) {
if m.closed() {
return nil, errManagerStopped
}

s := newStatus()
s := newStatus(desc)
m.statusLock.Lock()
m.status = append(m.status, s)
m.statusLock.Unlock()
Expand All @@ -119,18 +115,7 @@ func (m *manager) Add() (*Messenger, error) {
return m.statusChan(s), nil
}

// SendAndStop send message for descriptor and stop timing.
func (m *manager) SendAndStop(desc ocispec.Descriptor, prompt string) error {
messenger, err := m.Add()
if err != nil {
return err
}
messenger.Send(prompt, desc, desc.Size)
messenger.Stop()
return nil
}

func (m *manager) statusChan(s *status) *Messenger {
func (m *manager) statusChan(s *status) progress.Tracker {
ch := make(chan *status, BufferSize)
m.updating.Add(1)
go func() {
Expand All @@ -139,7 +124,10 @@ func (m *manager) statusChan(s *status) *Messenger {
s.update(newStatus)
}
}()
return &Messenger{ch: ch}
return &Messenger{
ch: ch,
prompt: m.prompt,
}
}

// Close stops all status and waits for updating and rendering.
Expand Down
3 changes: 2 additions & 1 deletion cmd/oras/internal/display/status/progress/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/cmd/oras/internal/display/status/console"
"oras.land/oras/internal/testutils"
)
Expand All @@ -41,7 +42,7 @@ func Test_manager_render(t *testing.T) {
}
height, _ := m.console.GetHeightWidth()
for i := 0; i < height; i++ {
if _, err := m.Add(); err != nil {
if _, err := m.Track(ocispec.Descriptor{}); err != nil {
t.Fatal(err)
}
}
Expand Down
87 changes: 34 additions & 53 deletions cmd/oras/internal/display/status/progress/messenger.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,48 @@

package progress

import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/cmd/oras/internal/display/status/progress/humanize"
"time"
)
import "oras.land/oras/internal/progress"

// Messenger is progress message channel.
type Messenger struct {
ch chan *status
closed bool
prompt map[progress.State]string
}

// Start initializes the messenger.
func (sm *Messenger) Start() {
if sm.ch == nil {
func (m *Messenger) Update(status progress.Status) error {
if status.State == progress.StateInitialized {
m.start()
}
m.send(m.prompt[status.State], status.Offset)
return nil
}

func (m *Messenger) Fail(err error) error {
m.ch <- fail(err)
return nil

Check warning on line 37 in cmd/oras/internal/display/status/progress/messenger.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/progress/messenger.go#L35-L37

Added lines #L35 - L37 were not covered by tests
}

func (m *Messenger) Close() error {
m.stop()
return nil
}

// start initializes the messenger.
func (m *Messenger) start() {
if m.ch == nil {
return
}
sm.ch <- startTiming()
m.ch <- startTiming()
}

// Send a status message for the specified descriptor.
func (sm *Messenger) Send(prompt string, descriptor ocispec.Descriptor, offset int64) {
// send a status message for the specified descriptor.
func (m *Messenger) send(prompt string, offset int64) {
for {
select {
case sm.ch <- newStatusMessage(prompt, descriptor, offset):
case m.ch <- newStatusMessage(prompt, offset):
return
case <-sm.ch:
case <-m.ch:
// purge the channel until successfully pushed
default:
// ch is nil
Expand All @@ -50,46 +65,12 @@
}
}

// Stop the messenger after sending a end message.
func (sm *Messenger) Stop() {
if sm.closed {
// stop the messenger after sending a end message.
func (m *Messenger) stop() {
if m.closed {
return
}
sm.ch <- endTiming()
close(sm.ch)
sm.closed = true
}

// newStatus generates a base empty status.
func newStatus() *status {
return &status{
offset: -1,
total: humanize.ToBytes(0),
speedWindow: newSpeedWindow(framePerSecond),
}
}

// newStatusMessage generates a status for messaging.
func newStatusMessage(prompt string, descriptor ocispec.Descriptor, offset int64) *status {
return &status{
prompt: prompt,
descriptor: descriptor,
offset: offset,
}
}

// startTiming creates start timing message.
func startTiming() *status {
return &status{
offset: -1,
startTime: time.Now(),
}
}

// endTiming creates end timing message.
func endTiming() *status {
return &status{
offset: -1,
endTime: time.Now(),
}
m.ch <- endTiming()
close(m.ch)
m.closed = true
}
21 changes: 11 additions & 10 deletions cmd/oras/internal/display/status/progress/messenger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ limitations under the License.
package progress

import (
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func Test_Messenger(t *testing.T) {
var msg *status
ch := make(chan *status, BufferSize)

messenger := &Messenger{ch: ch}

messenger.Start()
messenger.start()
select {
case msg = <-ch:
if msg.offset != -1 {
Expand All @@ -35,12 +37,11 @@ func Test_Messenger(t *testing.T) {
t.Error("Expected start message")
}

desc := v1.Descriptor{
Digest: "mouse",
Size: 100,
desc := ocispec.Descriptor{
Size: 100,
}
expected := int64(50)
messenger.Send("Reading", desc, expected)
messenger.send("Reading", expected)
select {
case msg = <-ch:
if msg.offset != expected {
Expand All @@ -53,8 +54,8 @@ func Test_Messenger(t *testing.T) {
t.Error("Expected status message")
}

messenger.Send("Reading", desc, expected)
messenger.Send("Read", desc, desc.Size)
messenger.send("Reading", expected)
messenger.send("Read", desc.Size)
select {
case msg = <-ch:
if msg.offset != desc.Size {
Expand All @@ -73,7 +74,7 @@ func Test_Messenger(t *testing.T) {
}

expected = int64(-1)
messenger.Stop()
messenger.stop()
select {
case msg = <-ch:
if msg.offset != expected {
Expand All @@ -83,7 +84,7 @@ func Test_Messenger(t *testing.T) {
t.Error("Expected END status message")
}

messenger.Stop()
messenger.stop()
select {
case msg = <-ch:
if msg != nil {
Expand Down
64 changes: 56 additions & 8 deletions cmd/oras/internal/display/status/progress/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@
spinnerColor = aec.LightYellowF
doneMarkColor = aec.LightGreenF
progressColor = aec.LightBlueB
failureColor = aec.LightRedF
)

// status is used as message to update progress view.
type status struct {
done bool // done is true when the end time is set
err error
prompt string
descriptor ocispec.Descriptor
offset int64
Expand All @@ -56,6 +58,47 @@
lock sync.Mutex
}

// newStatus generates a base empty status.
func newStatus(desc ocispec.Descriptor) *status {
return &status{
descriptor: desc,
offset: -1,
total: humanize.ToBytes(desc.Size),
speedWindow: newSpeedWindow(framePerSecond),
}
}

// newStatusMessage generates a status for messaging.
func newStatusMessage(prompt string, offset int64) *status {
return &status{
prompt: prompt,
offset: offset,
}
}

// startTiming creates start timing message.
func startTiming() *status {
return &status{
offset: -1,
startTime: time.Now(),
}
}

// endTiming creates end timing message.
func endTiming() *status {
return &status{
offset: -1,
endTime: time.Now(),
}
}

func fail(err error) *status {
return &status{
err: err,
offset: -1,
}

Check warning on line 99 in cmd/oras/internal/display/status/progress/status.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/status/progress/status.go#L95-L99

Added lines #L95 - L99 were not covered by tests
}

func (s *status) isZero() bool {
return s.offset < 0 && s.startTime.IsZero() && s.endTime.IsZero()
}
Expand Down Expand Up @@ -106,9 +149,13 @@
lenBar := int(percent * barLength)
bar := fmt.Sprintf("[%s%s]", progressColor.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar))
speed := s.calculateSpeed()
left = fmt.Sprintf("%s %s(%*s/s) %s %s",
spinnerColor.Apply(string(s.mark.symbol())),
bar, speedLength, speed, s.prompt, name)
var mark string
if s.err == nil {
mark = spinnerColor.Apply(string(s.mark.symbol()))
} else {
mark = failureColor.Apply("✗")
}
left = fmt.Sprintf("%s %s(%*s/s) %s %s", mark, bar, speedLength, speed, s.prompt, name)
// bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7
lenLeft = barLength + speedLength + 7
} else {
Expand Down Expand Up @@ -165,12 +212,11 @@
s.lock.Lock()
defer s.lock.Unlock()

if n.err != nil {
s.err = n.err
}
if n.offset >= 0 {
s.offset = n.offset
if n.descriptor.Size != s.descriptor.Size {
s.total = humanize.ToBytes(n.descriptor.Size)
}
s.descriptor = n.descriptor
}
if n.prompt != "" {
s.prompt = n.prompt
Expand All @@ -181,6 +227,8 @@
}
if !n.endTime.IsZero() {
s.endTime = n.endTime
s.done = true
if s.err == nil {
s.done = true
}
}
}
Loading
Loading