creack / pty

PTY interface for Go
https://pkg.go.dev/github.com/creack/pty?tab=doc
MIT License
1.68k stars 234 forks source link

SetDeadline not working on ptmx #162

Closed gcrtnst closed 11 months ago

gcrtnst commented 1 year ago

Run the following code:

package main

import (
    "fmt"
    "os"
    "time"

    "github.com/creack/pty"
)

func main() {
    ptmx, pts, err := pty.Open()
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: open: %v\n", err)
        return
    }
    defer ptmx.Close()
    defer pts.Close()

    err = ptmx.SetDeadline(time.Now().Add(5 * time.Second))
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: set deadline: %v\n", err)
        return
    }

    n, err := ptmx.Read(make([]byte, 1))
    fmt.Printf("n = %d\n", n)
    fmt.Printf("err = %v\n", err)
}

Expected behavior: The code exits after 5 seconds and the output following

n = 0
err = read /dev/ptmx: i/o timeout

Actual behavior: The code does not exit forever, it blocks on the ptmx.Read call and does not output anything to the stdout/stderr.

The cause of the issue is that "github.com/creack/pty" package calls (*os.File).Fd internally, which disables the SetDeadline method on Unix systems. Using (*os.File).SyscallConn instead may solve this issue.

This other issue posted on StackOverflow may have the same cause.

I reproduced this issue on Linux, but it may happen on other platforms.

sio commented 1 year ago

@gcrtnst, thank you for the detailed report! I have encountered a similar problem with blocking Read() and your mention of side effects in (*os.File).Fd() had saved me a significant amount of time.

I have submitted a PR (#167) to fix this problem, but until it gets merged you may be interested in a workaround. You can manually reset file descriptor to non-blocking mode:

--- issue162/demo-fail.go
+++ issue162/demo-ok.go
@@ -3,6 +3,7 @@
 import (
    "fmt"
    "os"
+   "syscall"
    "time"

    "github.com/creack/pty"
@@ -17,6 +18,11 @@
    defer ptmx.Close()
    defer pts.Close()

+   err = syscall.SetNonblock(int(ptmx.Fd()), true)
+   if err != nil {
+       fmt.Fprintf(os.Stderr, "failed to unblock ptmx: %v\n", err)
+   }
+
    err = ptmx.SetDeadline(time.Now().Add(5 * time.Second))
    if err != nil {
        fmt.Fprintf(os.Stderr, "error: set deadline: %v\n", err)

Of course, this will only work until you make another call to Fd() through pty library, though it's often good enough for my use cases.

creack commented 11 months ago

Closed with #167