Skip to content
Advertisement

Getting a pipe status in Go

I’m unable to get a pipe state in Go (1.5).

While writing on a mkfifo created pipe, I try to get the state of this output pipe:

  • using the Write return status EPIPE
  • using the Write return status EPIPE and signal.Ignore on SIGPIPE (just in case)
  • using signal.Notify on SIGPIPE

I can see that:

  • EPIPE is never returned
  • when I use kill -13, the signal handler is called: “Got signal: broken pipe”
  • when I ctrl-c the reader, the signal handler is not called and my program exits with output: “signal: broken pipe”

Would you, please, indicate my error ?

// tee.go
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"

    sys "golang.org/x/sys/unix"
)

// wait for a signal and print it
func handleSignal(csig chan os.Signal) {
    for {
        fmt.Println("Wait signal")
        s := <-csig
        fmt.Println("Got signal:", s)
    }
}

func main() {
    csig := make(chan os.Signal, 1)

    // `kill -13` outputs "Got signal: broken pipe" => ok
    signal.Notify(csig, sys.SIGPIPE)

    // OR disable the previous `Notify` just to be sure ?
    // maybe it will help to get the EPIPE error status on `Write` ?
    //signal.Ignore(sys.SIGPIPE)

    go handleSignal(csig)

    // open previously created named pipe (`mkfifo /tmp/test`)
    pipe, _ := os.OpenFile("/tmp/test", os.O_WRONLY, 0)

    for {
        _, err := pipe.Write([]byte("foon"))
        if err == syscall.EPIPE {
            // never called => ko
            fmt.Println("EPIPE error")
        }
    }
}

Note: as a simple Go exercise, I try to implement a command which almost acts like tee -a <a_file> (print stdin to stdout and <a_file>) with the following specificity: non blocking write on a named pipe and optional reader.

Advertisement

Answer

The error returned is not a plain syscall.Error but instead wrapped within *os.PathError as illustrated with the following variation of your code:

package main

import (
    "fmt"
    "os"
    "syscall"
)

func main() {
    // open previously created named pipe (`mkfifo /tmp/test`)
    pipe, _ := os.OpenFile("/tmp/test", os.O_WRONLY, 0)
    for {
        n, err := pipe.Write([]byte("foon"))
        fmt.Printf("write: n=%v, err=(%T) %[2]vn", n, err)
        if err == syscall.EPIPE {
            fmt.Println("EPIPE error")
        } else if perr, ok := err.(*os.PathError); ok {
            fmt.Printf("op: %q; path=%q; err=(%T) %[3]qn",
                perr.Op, perr.Path, perr.Err)
            if perr.Err == syscall.EPIPE {
                fmt.Println("os.PathError.Err is EPIPE")
            }
        }
    }
}

Running this after doing mkfifo /tmp/test; head /tmp/test elsewhere gives me:

write: n=4, err=(<nil>) <nil>
[… repeated nine more times, as the head command reads ten lines …]
write: n=0, err=(*os.PathError) write /tmp/test: broken pipe
op: "write"; path="/tmp/test"; err=(syscall.Errno) "broken pipe"
os.PathError.Err is EPIPE
[… above three lines repeated nine more times …]
signal: broken pipe
Exit 1

After returning ten pipe errors on an individual file the Go runtine stops catching/blocking SIGPIPE and lets it through to your program killing it. I don’t believe the Go runtime lets you catch or ignore SIGPIPE as it uses that signal itself internally.

So two things: one, to look for syscall.EPIPE you need to check for *os.PathError as shown and two, don’t continue on when err != nil when you haven’t actually handled the error.

I don’t know the details of why Go handles SIGPIPE in this way; perhaps a search of Go’s bug tracker and/or the go-nuts list may help answer that. With Go 1.5’s additions to the os/signal package doing signal.Reset(syscall.SIGPIPE) (before any signal.Notify calls) changes this behaviour.

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement