Skip to content

Commit aa48a0c

Browse files
committed
cmd/create: Don't block user interaction while fetching the image size
It takes 'skopeo inspect' a few seconds to fetch the image size from the remote registry, and while that happens the user can't interact with the image download prompt: $ toolbox create Image required to create toolbox container. <wait for a few seconds> Download registry.fedoraproject.org/fedora-toolbox:39 (359.8MB)? [y/N]: This feels awkward because it's not clear to the user what's going on during those few seconds. Moreover, while knowing the image size can be convenient at times, for example when disk space and network bandwidth are limited, it's not always important. It will be better if 'skopeo inspect' ran in the background, while waiting for the user to respond to the image download prompt, and once the image size has been fetched, the prompt can be updated to include it. So, initially: $ toolbox create Image required to create toolbox container. Download registry.fedoraproject.org/fedora-toolbox:39 (...)? [y/N]: ... and then once the size is available: $ toolbox create Image required to create toolbox container. Download registry.fedoraproject.org/fedora-toolbox:39 (359.8MB)? [y/N]: If skopeo(1) is missing or too old, then the prompt can continue without the size, as it did before: $ toolbox create Image required to create toolbox container. Download registry.fedoraproject.org/fedora-toolbox:39 [y/N]: #752 #1263
1 parent 8401092 commit aa48a0c

File tree

3 files changed

+156
-16
lines changed

3 files changed

+156
-16
lines changed

.github/workflows/ubuntu-tests.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
fish \
4545
gcc \
4646
go-md2man \
47-
golang \
47+
golang-1.20 \
4848
meson \
4949
ninja-build \
5050
openssl \
@@ -54,6 +54,9 @@ jobs:
5454
systemd \
5555
udisks2
5656
57+
- name: Set up PATH for Go 1.20
58+
run: echo "export PATH=/usr/lib/go-1.20/bin:\$PATH" >> $GITHUB_ENV
59+
5760
- name: Checkout Bats
5861
uses: actions/checkout@v3
5962
with:

src/cmd/create.go

Lines changed: 151 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -576,8 +576,7 @@ func getFullyQualifiedImageFromRepoTags(image string) (string, error) {
576576
return imageFull, nil
577577
}
578578

579-
func getImageSizeFromRegistry(imageFull string) (string, error) {
580-
ctx := context.Background()
579+
func getImageSizeFromRegistry(ctx context.Context, imageFull string) (string, error) {
581580
image, err := skopeo.Inspect(ctx, imageFull)
582581
if err != nil {
583582
return "", err
@@ -601,6 +600,23 @@ func getImageSizeFromRegistry(imageFull string) (string, error) {
601600
return imageSizeHuman, nil
602601
}
603602

603+
func getImageSizeFromRegistryAsync(ctx context.Context, imageFull string) (<-chan string, <-chan error) {
604+
retValCh := make(chan string)
605+
errCh := make(chan error)
606+
607+
go func() {
608+
imageSize, err := getImageSizeFromRegistry(ctx, imageFull)
609+
if err != nil {
610+
errCh <- err
611+
return
612+
}
613+
614+
retValCh <- imageSize
615+
}()
616+
617+
return retValCh, errCh
618+
}
619+
604620
func getServiceSocket(serviceName string, unitName string) (string, error) {
605621
logrus.Debugf("Resolving path to the %s socket", serviceName)
606622

@@ -713,18 +729,7 @@ func pullImage(image, release, authFile string) (bool, error) {
713729
}
714730

715731
if promptForDownload {
716-
fmt.Println("Image required to create toolbox container.")
717-
718-
var prompt string
719-
720-
if imageSize, err := getImageSizeFromRegistry(imageFull); err != nil {
721-
logrus.Debugf("Getting image size failed: %s", err)
722-
prompt = fmt.Sprintf("Download %s? [y/N]:", imageFull)
723-
} else {
724-
prompt = fmt.Sprintf("Download %s (%s)? [y/N]:", imageFull, imageSize)
725-
}
726-
727-
shouldPullImage = askForConfirmation(prompt)
732+
shouldPullImage = showPromptForDownload(imageFull)
728733
}
729734

730735
if !shouldPullImage {
@@ -756,6 +761,138 @@ func pullImage(image, release, authFile string) (bool, error) {
756761
return true, nil
757762
}
758763

764+
func showPromptForDownload(imageFull string) bool {
765+
fmt.Println("Image required to create toolbox container.")
766+
767+
prompt := fmt.Sprintf("Download %s ( ... MB)? [y/N]:", imageFull)
768+
769+
parentCtx := context.Background()
770+
askCtx, askCancel := context.WithCancelCause(parentCtx)
771+
defer askCancel(errors.New("clean-up"))
772+
773+
askCh, askErrCh := askForConfirmationAsync(askCtx, prompt, false)
774+
775+
imageSizeCtx, imageSizeCancel := context.WithCancelCause(parentCtx)
776+
defer imageSizeCancel(errors.New("clean-up"))
777+
778+
imageSizeCh, imageSizeErrCh := getImageSizeFromRegistryAsync(imageSizeCtx, imageFull)
779+
780+
var imageSize string
781+
var shouldPullImage bool
782+
783+
select {
784+
case val := <-askCh:
785+
shouldPullImage = val
786+
cause := fmt.Errorf("%w: received confirmation without image size", context.Canceled)
787+
imageSizeCancel(cause)
788+
case err := <-askErrCh:
789+
shouldPullImage = false
790+
cause := fmt.Errorf("failed to ask for confirmation without image size: %w", err)
791+
imageSizeCancel(cause)
792+
case val := <-imageSizeCh:
793+
imageSize = val
794+
cause := fmt.Errorf("%w: received image size", context.Canceled)
795+
askCancel(cause)
796+
case err := <-imageSizeErrCh:
797+
cause := fmt.Errorf("failed to get image size: %w", err)
798+
askCancel(cause)
799+
}
800+
801+
if imageSizeCtx.Err() != nil && askCtx.Err() == nil {
802+
cause := context.Cause(imageSizeCtx)
803+
logrus.Debugf("Show prompt for download: image size canceled: %s", cause)
804+
return shouldPullImage
805+
}
806+
807+
var done bool
808+
809+
if imageSizeCtx.Err() == nil && askCtx.Err() != nil {
810+
select {
811+
case val := <-askCh:
812+
logrus.Debugf("Show prompt for download: received pending confirmation without image size")
813+
shouldPullImage = val
814+
done = true
815+
case err := <-askErrCh:
816+
logrus.Debugf("Show prompt for download: failed to ask for confirmation without image size: %s",
817+
err)
818+
}
819+
} else {
820+
panic("code should not be reached")
821+
}
822+
823+
cause := context.Cause(askCtx)
824+
logrus.Debugf("Show prompt for download: ask canceled: %s", cause)
825+
826+
if done {
827+
return shouldPullImage
828+
}
829+
830+
var restoreCursor bool
831+
832+
if errors.Is(cause, context.Canceled) {
833+
imageSizeLen := len(imageSize)
834+
var padding1 int
835+
var padding2 int
836+
var padding3 int
837+
var padding4 int
838+
839+
if imageSizeLen < 7 {
840+
padding4 = 7 - imageSizeLen
841+
}
842+
843+
if padding4 > 1 {
844+
padding3 = padding4 - 1
845+
padding4 = 1
846+
}
847+
848+
if padding3 > 1 {
849+
padding2 = padding3 - 1
850+
padding3 = 1
851+
}
852+
853+
if padding2 > 1 {
854+
padding1 = padding2 - 1
855+
padding2 = 1
856+
}
857+
858+
prompt = fmt.Sprintf("Download %s (%*s%s%*s)? %*s[y/N]:%*s",
859+
imageFull,
860+
padding1, "",
861+
imageSize,
862+
padding2, "",
863+
padding3, "",
864+
padding4, "")
865+
866+
// Save the cursor position.
867+
fmt.Printf("\033[s")
868+
869+
restoreCursor = true
870+
} else {
871+
prompt = fmt.Sprintf("Download %s? [y/N]:", imageFull)
872+
873+
// Delete entire line regardless of cursor position.
874+
fmt.Printf("\033[2K")
875+
}
876+
877+
fmt.Printf("\r")
878+
879+
askAgainCtx, askAgainCancel := context.WithCancelCause(parentCtx)
880+
defer askAgainCancel(errors.New("clean-up"))
881+
882+
askAgainCh, askAgainErrCh := askForConfirmationAsync(askAgainCtx, prompt, restoreCursor)
883+
884+
select {
885+
case val := <-askAgainCh:
886+
logrus.Debug("Show prompt for download: received confirmation with image size")
887+
shouldPullImage = val
888+
case err := <-askAgainErrCh:
889+
logrus.Debugf("Show prompt for download: failed to ask for confirmation with image size: %s", err)
890+
shouldPullImage = false
891+
}
892+
893+
return shouldPullImage
894+
}
895+
759896
// systemdNeedsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped
760897
func systemdNeedsEscape(i int, b byte) bool {
761898
// Escape everything that is not a-z-A-Z-0-9

src/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/containers/toolbox
22

3-
go 1.14
3+
go 1.20
44

55
require (
66
github.com/HarryMichal/go-version v1.0.1

0 commit comments

Comments
 (0)