Home > database >  How to terminate a console input request when a new input is requested
How to terminate a console input request when a new input is requested

Time:04-08

I need to terminate an existing console input request when a new one is requested. The following code is an attempt to close an existing request using a channel but it does not seem to terminate the input request.

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "strings"
    "time"
)

func main() {
    go Confirm("you are a programmer, aint you?")
    time.Sleep(2 * time.Second)
    Confirm("do you love go?")
}

var cancelChannel chan struct{}

func Confirm(s string) bool {
    //check if channel type holds a value then close the channel to remove previous confirmation input
    if cancelChannel != nil {
        fmt.Println("channel to be closed")
        close(cancelChannel)
    }
    cancelChannel = make(chan struct{})
    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Printf("%s [y/n]: ", s)

        response, err := reader.ReadString('\n')
        if err != nil {
            log.Fatal(err)
        }

        response = strings.ToLower(strings.TrimSpace(response))

        if response == "y" || response == "yes" {
            return true
        } else if response == "n" || response == "no" {
            return false
        }

        if _, ok := <-cancelChannel; !ok {
            fmt.Println("channel closed")
            return false
        }

    }
}

CodePudding user response:

Go on and explore the simplicity offer by go https://pkg.go.dev/context#WithCancel

You can have a context that returning CancelFunc then you use context.WithCancel. And execute cancel func if you want to terminate.

This is the good practice way, you can also do a dirty os.Exit(0) in another case.

CodePudding user response:

As @JimB mentioned in comment you can't interrupt read on stdin although there is kinda shady trick how you can achieve it. It's possible to duplicate os.Stdin file descriptor using syscall (not recommended) and open it as non blocking file.

package main

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "io/fs"
    "io/ioutil"
    "os"
    "syscall"
    "time"
)

func setNonblock(f *os.File) error {
    c, err := f.SyscallConn()
    if err != nil {
        return err
    }

    var err2 error
    err = c.Control(func(fd uintptr) {
        err2 = syscall.SetNonblock(int(fd), true)
    })
    if err != nil {
        return err
    }
    return err2
}

func nonBlockingFile(f *os.File) (*os.File, error) {
    if err := setNonblock(f); err != nil {
        return nil, err
    }
    fd, err := syscall.Dup(int(f.Fd()))
    if err != nil {
        return nil, err
    }
    f2 := os.NewFile(uintptr(fd), f.Name())
    return f2, nil
}

func read(ctx context.Context, f *os.File) (io.Reader, error) {
    r, err := nonBlockingFile(f)
    if err != nil {
        return nil, err
    }
    go func() {
        defer r.Close()
        <-ctx.Done()
    }()
    buff := bytes.NewBuffer([]byte{})
    for {
        _, err := io.Copy(buff, r)
        if err != nil {
            if errors.Is(err, fs.ErrClosed) {
                break
            }
            panic(err)
        }
    }
    return buff, nil
}

func main() {
    ctx1, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(time.Second * 2)
        cancel()
        
    }()
    buf1, err := read(ctx1, os.Stdin)
    if err != nil {
        panic(err)
    }
    ctx2, _ := context.WithTimeout(context.Background(), time.Second*2)
    buf2, err := read(ctx2, os.Stdin)

    fmt.Println("buf1")
    fmt.Println(ioutil.ReadAll(buf1))
    fmt.Println("buf2")
    fmt.Println(ioutil.ReadAll(buf2))
}
  •  Tags:  
  • go
  • Related