Skip to content

Commit

Permalink
cmd/utils: Add an asynchronous cancellable version of askForConfirmation
Browse files Browse the repository at this point in the history
A subsequent commit will use this to ensure that the user can still
interact with the image download prompt while 'skopeo inspect' fetches
the image size from the remote registry.  Initially, the prompt will be
shown without the image size.  Once the size has been fetched, the older
prompt will be cancelled and a new one will be shown that includes the
size.

Even though this code is only expected to be used to read from the
standard input stream, when it's connected to a terminal device, the use
of poll(2) here was tested with FIFOs or named pipes and regular files
as well, in case they might be necessary in future.

An eventfd(2) file descriptor expects a 8-byte or 64-bit integer value
to be given to write(2) to increase its counter by that amount [1].  In
C, it could be phrased as:
  uint64_t one = 1;
  write (eventfd, &one, sizeof(one));

However, Go's wrapper for write(2) expects a sequence of bytes (ie.,
[]byte), and not an arbitrary memory address.  Therefore the 'binary'
package [3] is used to encode the integer into a byte sequence as a
varint.

Even though a varint-encoded 64-bit integer takes a maximum of 10
bytes, as defined by binary.MaxVarintLen64, 1 byte is enough to encode
the number 1 as an unsigned 64-bit integer [3].  That's enough to fit
into a byte sequence of length 8 to satisfy what an eventfd(2) file
descriptor expects.  Ultimately, it doesn't matter exactly what value
the receiving end assigns to the number given to write(2), as long as
it's not zero.

[1] https://man7.org/linux/man-pages/man2/eventfd.2.html

[2] https://pkg.go.dev/golang.org/x/sys/unix#Write

[3] https://protobuf.dev/programming-guides/encoding/

containers#752
containers#1263
  • Loading branch information
debarshiray committed Dec 15, 2023
1 parent 44664c2 commit f3c62d4
Showing 1 changed file with 196 additions and 0 deletions.
196 changes: 196 additions & 0 deletions src/cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,30 @@ package cmd

import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"syscall"

"github.com/containers/toolbox/pkg/utils"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

type askForConfirmationPreFunc func() error
type pollFunc func(error, []unix.PollFd) error

var (
errClosed = errors.New("closed")

errContinue = errors.New("continue")

errHUP = errors.New("HUP")
)

// askForConfirmation prints prompt to stdout and waits for response from the
Expand Down Expand Up @@ -67,6 +83,109 @@ func askForConfirmation(prompt string) bool {
return retVal
}

func askForConfirmationAsync(ctx context.Context,
prompt string,
askForConfirmationPreFn askForConfirmationPreFunc) (<-chan bool, <-chan error) {

retValCh := make(chan bool, 1)
errCh := make(chan error, 1)

done := ctx.Done()
eventFD := -1
if done != nil {
fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
if err != nil {
errCh <- fmt.Errorf("eventfd(2) failed: %w", err)
return retValCh, errCh
}

eventFD = fd
}

go func() {
for {
fmt.Printf("%s ", prompt)
if askForConfirmationPreFn != nil {
if err := askForConfirmationPreFn(); err != nil {
if errors.Is(err, errContinue) {
continue
}

errCh <- err
break
}
}

var response string

pollFn := func(errPoll error, pollFDs []unix.PollFd) error {
if len(pollFDs) != 1 {
panic("unexpected number of file descriptors")
}

if errPoll != nil {
return errPoll
}

if pollFDs[0].Revents&unix.POLLIN != 0 {
logrus.Debug("Returned from /dev/stdin: POLLIN")

scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanLines)

if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return err
} else {
return io.EOF
}
}

response = scanner.Text()
return nil
}

if pollFDs[0].Revents&unix.POLLHUP != 0 {
logrus.Debug("Returned from /dev/stdin: POLLHUP")
return errHUP
}

if pollFDs[0].Revents&unix.POLLNVAL != 0 {
logrus.Debug("Returned from /dev/stdin: POLLNVAL")
return errClosed
}

return errContinue
}

stdinFD := int32(os.Stdin.Fd())

err := poll(pollFn, int32(eventFD), stdinFD)
if err != nil {
errCh <- err
break
}

if response == "" {
response = "n"
} else {
response = strings.ToLower(response)
}

if response == "no" || response == "n" {
retValCh <- false
break
} else if response == "yes" || response == "y" {
retValCh <- true
break
}
}
}()

watchContextForEventFD(ctx, eventFD)
return retValCh, errCh
}

func createErrorContainerNotFound(container string) error {
var builder strings.Builder
fmt.Fprintf(&builder, "container %s not found\n", container)
Expand Down Expand Up @@ -148,6 +267,57 @@ func getUsageForCommonCommands() string {
return usage
}

func poll(pollFn pollFunc, eventFD int32, fds ...int32) error {
if len(fds) == 0 {
panic("file descriptors not specified")
}

pollFDs := []unix.PollFd{
{
Fd: eventFD,
Events: unix.POLLIN,
Revents: 0,
},
}

for _, fd := range fds {
pollFD := unix.PollFd{Fd: fd, Events: unix.POLLIN, Revents: 0}
pollFDs = append(pollFDs, pollFD)
}

for {
if _, err := unix.Poll(pollFDs, -1); err != nil {
if errors.Is(err, unix.EINTR) {
logrus.Debugf("Failed to poll(2): %s: ignoring", err)
continue
}

return fmt.Errorf("poll(2) failed: %w", err)
}

var err error

if pollFDs[0].Revents&unix.POLLIN != 0 {
logrus.Debug("Returned from eventfd: POLLIN")
err = context.Canceled

for {
buffer := make([]byte, 8)
if n, err := unix.Read(int(eventFD), buffer); n != len(buffer) || err != nil {
break
}
}
} else if pollFDs[0].Revents&unix.POLLNVAL != 0 {
logrus.Debug("Returned from eventfd: POLLNVAL")
err = context.Canceled
}

if err := pollFn(err, pollFDs[1:]); !errors.Is(err, errContinue) {
return err
}
}
}

func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string) (
string, string, string, error,
) {
Expand Down Expand Up @@ -245,3 +415,29 @@ func showManual(manual string) error {

return nil
}

func watchContextForEventFD(ctx context.Context, eventFD int) {
done := ctx.Done()
if done == nil {
return
}

if eventFD < 0 {
panic("invalid file descriptor for eventfd")
}

go func() {
defer unix.Close(eventFD)

select {
case <-done:
buffer := make([]byte, 8)
binary.PutUvarint(buffer, 1)

if _, err := unix.Write(eventFD, buffer); err != nil {
panicMsg := fmt.Sprintf("write(2) to eventfd failed: %s", err)
panic(panicMsg)
}
}
}()
}

0 comments on commit f3c62d4

Please sign in to comment.