toolbox/src/cmd/utils.go

518 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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)
}
}
}()
}