Skip to content

Commit 4de06b3

Browse files
committed
cmd/utils: Add an asynchronous cancellable version of askForConfirmation
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. #752 #1263
1 parent 5d068f7 commit 4de06b3

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed

src/cmd/utils.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,25 @@ package cmd
1818

1919
import (
2020
"bufio"
21+
"context"
22+
"encoding/binary"
2123
"errors"
2224
"fmt"
25+
"io"
2326
"os"
2427
"os/exec"
2528
"strings"
2629
"syscall"
2730

2831
"github.com/containers/toolbox/pkg/utils"
32+
"github.com/sirupsen/logrus"
33+
"golang.org/x/sys/unix"
34+
)
35+
36+
var (
37+
errClosed = errors.New("closed")
38+
39+
errHUP = errors.New("HUP")
2940
)
3041

3142
// askForConfirmation prints prompt to stdout and waits for response from the
@@ -67,6 +78,158 @@ func askForConfirmation(prompt string) bool {
6778
return retVal
6879
}
6980

81+
func askForConfirmationAsyncScan(doneFD int32) (string, error) {
82+
fileFD := int32(os.Stdin.Fd())
83+
84+
pollFDs := []unix.PollFd{
85+
{
86+
Fd: doneFD,
87+
Events: unix.POLLIN,
88+
Revents: 0,
89+
},
90+
{
91+
Fd: fileFD,
92+
Events: unix.POLLIN,
93+
Revents: 0,
94+
},
95+
}
96+
97+
for {
98+
if _, err := unix.Poll(pollFDs, -1); err != nil {
99+
if errors.Is(err, unix.EINTR) {
100+
logrus.Debugf("Failed to poll(2): %s: ignoring", err)
101+
continue
102+
}
103+
104+
return "", fmt.Errorf("poll(2) failed: %w", err)
105+
}
106+
107+
logrus.Debugf("Returned events from poll(2): 0x%x, 0x%x\n", pollFDs[0].Revents, pollFDs[1].Revents)
108+
109+
if pollFDs[0].Revents&unix.POLLIN != 0 {
110+
logrus.Debug("Returned from eventfd: POLLIN")
111+
112+
for {
113+
buffer := make([]byte, 8)
114+
if n, err := unix.Read(int(doneFD), buffer); n != len(buffer) || err != nil {
115+
break
116+
}
117+
}
118+
119+
return "", context.Canceled
120+
}
121+
122+
if pollFDs[0].Revents&unix.POLLNVAL != 0 {
123+
logrus.Debug("Returned from eventfd: POLLNVAL")
124+
return "", context.Canceled
125+
}
126+
127+
if pollFDs[1].Revents&unix.POLLIN != 0 {
128+
logrus.Debug("Returned from /dev/stdin: POLLIN")
129+
130+
scanner := bufio.NewScanner(os.Stdin)
131+
scanner.Split(bufio.ScanLines)
132+
133+
if !scanner.Scan() {
134+
if err := scanner.Err(); err != nil {
135+
return "", err
136+
} else {
137+
return "", io.EOF
138+
}
139+
}
140+
141+
response := scanner.Text()
142+
return response, nil
143+
}
144+
145+
if pollFDs[1].Revents&unix.POLLHUP != 0 {
146+
logrus.Debug("Returned from /dev/stdin: POLLHUP")
147+
return "", errHUP
148+
}
149+
150+
if pollFDs[1].Revents&unix.POLLNVAL != 0 {
151+
logrus.Debug("Returned from /dev/stdin: POLLNVAL")
152+
return "", errClosed
153+
}
154+
}
155+
}
156+
157+
func askForConfirmationAsync(ctx context.Context, prompt string, restoreCursor bool) (<-chan bool, <-chan error) {
158+
retValCh := make(chan bool)
159+
errCh := make(chan error)
160+
161+
done := ctx.Done()
162+
doneFD := -1
163+
if done != nil {
164+
eventfd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
165+
if err != nil {
166+
errCh <- fmt.Errorf("eventfd(2) failed: %w", err)
167+
return retValCh, errCh
168+
}
169+
170+
doneFD = eventfd
171+
}
172+
173+
go func() {
174+
for {
175+
fmt.Printf("%s ", prompt)
176+
if restoreCursor {
177+
fmt.Printf("\033[u")
178+
restoreCursor = false
179+
}
180+
181+
response, err := askForConfirmationAsyncScan(int32(doneFD))
182+
if err != nil {
183+
errCh <- err
184+
break
185+
}
186+
187+
if response == "" {
188+
response = "n"
189+
} else {
190+
response = strings.ToLower(response)
191+
}
192+
193+
if response == "no" || response == "n" {
194+
retValCh <- false
195+
break
196+
} else if response == "yes" || response == "y" {
197+
retValCh <- true
198+
break
199+
}
200+
}
201+
}()
202+
203+
go func() {
204+
if done == nil {
205+
return
206+
}
207+
208+
defer unix.Close(doneFD)
209+
210+
select {
211+
case <-done:
212+
// A varint-encoded uint64 takes a maximum of 10 bytes,
213+
// as defined by binary.MaxVarintLen64. However, 1
214+
// byte is enough to encode the number 1. See:
215+
// https://protobuf.dev/programming-guides/encoding/
216+
//
217+
// An eventfd(2) file descriptor expects a uint64 to be
218+
// given to write(2). Luckily, a varint-encoded number
219+
// 1 happens to work.
220+
buffer := make([]byte, 8)
221+
binary.PutUvarint(buffer, 1)
222+
223+
if _, err := unix.Write(doneFD, buffer); err != nil {
224+
panicMsg := fmt.Sprintf("write(2) to eventfd failed: %s", err)
225+
panic(panicMsg)
226+
}
227+
}
228+
}()
229+
230+
return retValCh, errCh
231+
}
232+
70233
func createErrorContainerNotFound(container string) error {
71234
var builder strings.Builder
72235
fmt.Fprintf(&builder, "container %s not found\n", container)

0 commit comments

Comments
 (0)