Close a reader to quit a loop without closing its source

1,096 views
Skip to first unread message

mhhcbon

unread,
Aug 31, 2016, 7:42:53 AM8/31/16
to golang-nuts
Hi,

I have a small program to read stdin and execute some commands accordingly.

It s composed of a for loop which waits for data to read on the source.

I found out i could quit the loop by closing its source and catching err on ReadString calls to skip the iteration.

Qs are,
- is there a cleaner and cheap way to handle reader close ? Or in my case properly get away from the loop.
- Aside from the previous, How can i use err to detect a closed resource ? The error i get is "read /dev/stdin: bad file descriptor"
I mean something like described in Dave C last talk. In practical terms, how to get an identifier for the err, how to compare it.


The specific part of the code looks likes this,

func StdinRead (cmds map[string]func([]string), unhandled *func([]string)) {
  reader
:= bufio.NewReader(os.Stdin)
 
for {
    text
, err := reader.ReadString('\n')
   
if err!=nil {
      fmt
.Printf("%+v\n", err)
     
continue
   
}
    text
= strings.TrimSpace(text)
    args
:= strings.Split(text, " ")
   
if len(args)<1 {
     
continue
   
}
   
if fn, ok := cmds[args[0]]; ok {
      fn
(args[1:])
   
} else if unhandled!=nil {
      h
:= *unhandled
      h
(args)
   
} else {
      fmt
.Printf("unhandled: '%s' %v\n", args[0], args[1:])
   
}
 
}
}

func
StdinQuit () {
  fmt
.Println("Quiting the app, see you!")
  os
.Stdin.Close() // force close stdin, otherwise the process hangs in the next for loop
  time
.Sleep(time.Second * 200)
}


The full one file reproducible example is,
package main

import(
 
"fmt"
 
"bufio"
 
"strings"
 
"os"
 
"os/signal"
 
"syscall"
 
"time"
)

func main
() {
 
EOApp := Quiter{}
  go
EOApp.Read()


  cmds
:= make(map[string] func([]string))

  cmds
["hello"] = func (s []string) {
    fmt
.Println("hello buddy!")
 
}

  cmds
["serve"] = func (s []string) {
   
// fmt.Println("hello buddy!")
 
}
  cmds
["ping"] = func (s []string) {
   
// fmt.Println("hello buddy!")
 
}

  unhandled
:= func (s []string) {
   
if s[0]!="" {
      fmt
.Printf("nop, i don t know this command: '%s' %v\n", s[0], s[1:])
   
}
 
}



 
EOApp.Register(StdinQuit)
 
StdinRead(cmds, &unhandled)

}

func
StdinRead (cmds map[string]func([]string), unhandled *func([]string)) {
  reader
:= bufio.NewReader(os.Stdin)
 
for {
    text
, err := reader.ReadString('\n')
   
if err!=nil {
      fmt
.Printf("%+v\n", err)
     
continue
   
}
    text
= strings.TrimSpace(text)
    args
:= strings.Split(text, " ")
   
if len(args)<1 {
     
continue
   
}
   
if fn, ok := cmds[args[0]]; ok {
      fn
(args[1:])
   
} else if unhandled!=nil {
      h
:= *unhandled
      h
(args)
   
} else {
      fmt
.Printf("unhandled: '%s' %v\n", args[0], args[1:])
   
}
 
}
}

func
StdinQuit () {
  fmt
.Println("Quiting the app, see you!")
  os
.Stdin.Close() // force close stdin, otherwise the process hangs in the next for loop
  time
.Sleep(time.Second * 200)
}


type
Quiter struct {
 
Components []func()
}

func
(q *Quiter) Register (component func()) int {
  q
.Components = append(q.Components, component)
 
return len(q.Components)-1
}

func
(q *Quiter) exitComponents ()  {
 
for _, component := range q.Components {
    component
()
 
}
}

func
(q *Quiter) Read () {
  c
:= make(chan os.Signal)
  signal
.Notify(c, os.Interrupt, syscall.SIGTERM)
  signalSent
:= 0
 
for {
   
select {
     
case <-c:
       
if signalSent <1 {
         go func
() {
           q
.exitComponents()
           os
.Exit(1)
         
}()
       
} else {
         fmt
.Println("Force shutdown")
         os
.Exit(1)
       
}
       signalSent
++
   
}
 
}
}


BTW, do i need to make a unbuffered chan of len 2 in Quiter.Read func to catch both signals appropriately ?
I don t really get the implications of the doc when it says,

Package signal will not block sending to c: the caller must ensure that c has sufficient buffer space to keep up with the expected signal rate. For a channel used for notification of just one signal value, a buffer of size 1 is sufficient.



thks

Dave Cheney

unread,
Aug 31, 2016, 7:48:12 AM8/31/16
to golang-nuts
Unfortunately POSIX does not guarantee that close from one thread will unblock another.

adon...@google.com

unread,
Aug 31, 2016, 5:58:49 PM8/31/16
to golang-nuts
On Wednesday, 31 August 2016 07:48:12 UTC-4, Dave Cheney wrote:
Unfortunately POSIX does not guarantee that close from one thread will unblock another.

To read from a file without waiting longer than a specified time, you need to use the POSIX 'select' system call, which you can find at syscall.Select.  (It was the inspiration for the Go select statement, but whereas Go's select multiplexes channels, POSIX's select multiplexes files.)  The select system call is quite a pain to use in Go; you might want to take a look at https://github.com/creack/goselect.

Aram Hăvărneanu

unread,
Sep 1, 2016, 7:12:59 AM9/1/16
to adon...@google.com, golang-nuts
On Wed, Aug 31, 2016 at 11:58 PM, adonovan via golang-nuts
<golan...@googlegroups.com> wrote:
>
> [the POSIX 'select' system call] was the inspiration for the Go
> select statement, but whereas Go's select multiplexes channels,
> POSIX's select multiplexes files.

I can't claim any authority over this, but believe it is wrong. The
control structure appeared in Tony Hoare's communicating sequential
processes, and it is present in Go's predecessors developed at Bell
Labs. It's present (at least) in Squeak (although that used the
original CSP, not the revised version), Newsqueak, Alef and Limbo.

--
Aram Hăvărneanu

adon...@google.com

unread,
Sep 1, 2016, 2:15:41 PM9/1/16
to golang-nuts, adon...@google.com
On Thursday, 1 September 2016 07:12:59 UTC-4, Aram Hăvărneanu wrote:
On Wed, Aug 31, 2016 at 11:58 PM, adonovan via golang-nuts
<golan...@googlegroups.com> wrote:
>
> [the POSIX 'select' system call] was the inspiration for the Go
> select statement, but whereas Go's select multiplexes channels,
> POSIX's select multiplexes files.

I can't claim any authority over this, but believe it is wrong. The
control structure appeared in Tony Hoare's communicating sequential
processes [...]

You're absolutely right.  I meant merely that the name "select" comes from UNIX.  Now I wonder if the authors of Berkeley UNIX were inspired by CSP...

mhhcbon

unread,
Sep 2, 2016, 6:21:12 AM9/2/16
to golang-nuts, adon...@google.com
I dig into the repo you mentioned, TBHonnest, im not that wise, and it remains unclear.
For the few i know, select is the approach to take, but the API is unclear.

Not sure if it s possible to come up with something as straight as `NewReader(os.Stdin).Block(false).Read()`

Alan Donovan

unread,
Sep 2, 2016, 10:30:21 AM9/2/16
to mhhcbon, golang-nuts
On 2 September 2016 at 06:21, mhhcbon <cpasmabo...@gmail.com> wrote:
I dig into the repo you mentioned, TBHonnest, im not that wise, and it remains unclear.
For the few i know, select is the approach to take, but the API is unclear.

Not sure if it s possible to come up with something as straight as `NewReader(os.Stdin).Block(false).Read()`

As I said, the select system call is a pain to use in Go---not least because the macros FD_SET et al seem to be missing.

Another approach is to make the stdin file descriptor non-blocking, so that attempting to read from it will fail immediately if there is no data available.

Admittedly, neither of these approaches is straightforward.  A more typical Go approach is to start the read in its own goroutine and let it run to completion, even if no data ever arrives.  Of course, while simple to program, this is not ideal if you need to cancel the read after the timeout has expired.

mhhcbon

unread,
Sep 2, 2016, 2:28:21 PM9/2/16
to golang-nuts, cpasmabo...@gmail.com, adon...@google.com
thanks a lot !! It is very much appreciated.

The for loop is very clever.
Reply all
Reply to author
Forward
0 new messages