518 lines
12 KiB
Go
518 lines
12 KiB
Go
/*
|
||
* Copyright © 2020 – 2024 Red Hat Inc.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
package cmd
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"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
|
||
// user
|
||
//
|
||
// Expected answers are: "yes", "y", "no", "n"
|
||
//
|
||
// Answers are internally converted to lower case.
|
||
//
|
||
// The default answer is "no" ([y/N])
|
||
func askForConfirmation(prompt string) bool {
|
||
var retVal bool
|
||
|
||
ctx := context.Background()
|
||
retValCh, errCh := askForConfirmationAsync(ctx, prompt, nil)
|
||
|
||
select {
|
||
case val := <-retValCh:
|
||
retVal = val
|
||
case err := <-errCh:
|
||
logrus.Debugf("Failed to ask for confirmation: %s", err)
|
||
retVal = false
|
||
}
|
||
|
||
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 discardInputAsync(ctx context.Context) (<-chan int, <-chan error) {
|
||
retValCh := make(chan int, 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() {
|
||
var total int
|
||
|
||
for {
|
||
pollFn := func(errPoll error, pollFDs []unix.PollFd) error {
|
||
if len(pollFDs) != 1 {
|
||
panic("unexpected number of file descriptors")
|
||
}
|
||
|
||
if errPoll != nil && !errors.Is(errPoll, context.Canceled) {
|
||
return errPoll
|
||
}
|
||
|
||
if pollFDs[0].Revents&unix.POLLIN != 0 {
|
||
logrus.Debug("Returned from /dev/stdin: POLLIN")
|
||
|
||
buffer := make([]byte, bytes.MinRead)
|
||
n, err := os.Stdin.Read(buffer)
|
||
total += n
|
||
|
||
if errPoll != nil {
|
||
return errPoll
|
||
} else if err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
if pollFDs[0].Revents&unix.POLLHUP != 0 {
|
||
logrus.Debug("Returned from /dev/stdin: POLLHUP")
|
||
|
||
if errPoll != nil {
|
||
return errPoll
|
||
}
|
||
|
||
return errHUP
|
||
}
|
||
|
||
if pollFDs[0].Revents&unix.POLLNVAL != 0 {
|
||
logrus.Debug("Returned from /dev/stdin: POLLNVAL")
|
||
|
||
if errPoll != nil {
|
||
return errPoll
|
||
}
|
||
|
||
return errClosed
|
||
}
|
||
|
||
if errPoll != nil {
|
||
return errPoll
|
||
}
|
||
|
||
return errContinue
|
||
}
|
||
|
||
stdinFD := int32(os.Stdin.Fd())
|
||
|
||
err := poll(pollFn, int32(eventFD), stdinFD)
|
||
if err != nil {
|
||
retValCh <- total
|
||
errCh <- err
|
||
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)
|
||
fmt.Fprintf(&builder, "Use the 'create' command to create a Toolbx.\n")
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorDistroWithoutRelease(distro string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "option '--release' is needed\n")
|
||
fmt.Fprintf(&builder, "Distribution %s doesn't match the host.\n", distro)
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorInvalidContainer(containerArg string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '%s'\n", containerArg)
|
||
fmt.Fprintf(&builder, "Container names must match '%s'.\n", utils.ContainerNameRegexp)
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorInvalidDistro(distro string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '--distro'\n")
|
||
fmt.Fprintf(&builder, "Distribution %s is unsupported.\n", distro)
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorInvalidImageForContainerName(container string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '--image'\n")
|
||
fmt.Fprintf(&builder, "Container name %s generated from image is invalid.\n", container)
|
||
fmt.Fprintf(&builder, "Container names must match '%s'.\n", utils.ContainerNameRegexp)
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorInvalidImageWithoutBasename() error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '--image'\n")
|
||
fmt.Fprintf(&builder, "Images must have basenames.\n")
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func createErrorInvalidRelease(hint string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '--release'\n")
|
||
fmt.Fprintf(&builder, "%s\n", hint)
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func getUsageForCommonCommands() string {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "create Create a new Toolbx container\n")
|
||
fmt.Fprintf(&builder, "enter Enter an existing Toolbx container\n")
|
||
fmt.Fprintf(&builder, "list List all existing Toolbx containers and images\n")
|
||
|
||
usage := builder.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,
|
||
) {
|
||
container, image, release, err := utils.ResolveContainerAndImageNames(container,
|
||
distroCLI,
|
||
imageCLI,
|
||
releaseCLI)
|
||
|
||
if err != nil {
|
||
var errContainer *utils.ContainerError
|
||
var errDistro *utils.DistroError
|
||
var errImage *utils.ImageError
|
||
var errParseRelease *utils.ParseReleaseError
|
||
|
||
if errors.As(err, &errContainer) {
|
||
if errors.Is(err, utils.ErrContainerNameInvalid) {
|
||
if containerArg == "" {
|
||
panicMsg := fmt.Sprintf("unexpected %T without containerArg: %s", err, err)
|
||
panic(panicMsg)
|
||
}
|
||
|
||
err := createErrorInvalidContainer(containerArg)
|
||
return "", "", "", err
|
||
} else if errors.Is(err, utils.ErrContainerNameFromImageInvalid) {
|
||
err := createErrorInvalidImageForContainerName(errContainer.Container)
|
||
return "", "", "", err
|
||
} else {
|
||
panicMsg := fmt.Sprintf("unexpected %T: %s", err, err)
|
||
panic(panicMsg)
|
||
}
|
||
} else if errors.As(err, &errDistro) {
|
||
if errors.Is(err, utils.ErrDistroUnsupported) {
|
||
err := createErrorInvalidDistro(errDistro.Distro)
|
||
return "", "", "", err
|
||
} else if errors.Is(err, utils.ErrDistroWithoutRelease) {
|
||
err := createErrorDistroWithoutRelease(errDistro.Distro)
|
||
return "", "", "", err
|
||
} else {
|
||
panicMsg := fmt.Sprintf("unexpected %T: %s", err, err)
|
||
panic(panicMsg)
|
||
}
|
||
} else if errors.As(err, &errImage) {
|
||
if errors.Is(err, utils.ErrImageWithoutBasename) {
|
||
err := createErrorInvalidImageWithoutBasename()
|
||
return "", "", "", err
|
||
} else {
|
||
panicMsg := fmt.Sprintf("unexpected %T: %s", err, err)
|
||
panic(panicMsg)
|
||
}
|
||
} else if errors.As(err, &errParseRelease) {
|
||
err := createErrorInvalidRelease(errParseRelease.Hint)
|
||
return "", "", "", err
|
||
} else {
|
||
return "", "", "", err
|
||
}
|
||
}
|
||
|
||
return container, image, release, nil
|
||
}
|
||
|
||
// showManual tries to open the specified manual page using man on stdout
|
||
func showManual(manual string) error {
|
||
manBinary, err := exec.LookPath("man")
|
||
if err != nil {
|
||
if errors.Is(err, exec.ErrNotFound) {
|
||
fmt.Printf("toolbox - Tool for containerized command line environments on Linux\n")
|
||
fmt.Printf("\n")
|
||
fmt.Printf("Common commands are:\n")
|
||
|
||
usage := getUsageForCommonCommands()
|
||
fmt.Printf("%s", usage)
|
||
|
||
fmt.Printf("\n")
|
||
fmt.Printf("Go to https://github.com/containers/toolbox for further information.\n")
|
||
return nil
|
||
}
|
||
|
||
return errors.New("failed to look up man(1)")
|
||
}
|
||
|
||
manualArgs := []string{"man", manual}
|
||
env := os.Environ()
|
||
|
||
stderrFd := os.Stderr.Fd()
|
||
stderrFdInt := int(stderrFd)
|
||
stdoutFd := os.Stdout.Fd()
|
||
stdoutFdInt := int(stdoutFd)
|
||
if err := syscall.Dup3(stdoutFdInt, stderrFdInt, 0); err != nil {
|
||
return errors.New("failed to redirect standard error to standard output")
|
||
}
|
||
|
||
if err := syscall.Exec(manBinary, manualArgs, env); err != nil {
|
||
return errors.New("failed to invoke man(1)")
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
}()
|
||
}
|