Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another attempt of support pty on windows with ConPTY api #155

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

photostorm
Copy link

@photostorm photostorm commented Jul 25, 2022

This is an another attempt to support pseudo-terminal on windows using ConPty API (should resolve issue #95)

This pull request does introduce major api change for Start method (*os.File -> Pty),

I have tested this code with remote terminal application that I wrote. It would be great to get some additional testing with other use cases.

@pete-woods
Copy link

This looks very promising, I will try it out to see if it works in our system

@pete-woods
Copy link

I just tried this out, and works perfectly for my use case (an SSH server that must work on UNIX and Windows) 👍

@mjudeikis
Copy link

Trying this now. Would be really nice if this works on go 1.18 too :) considering repo is still pointing to 1.13 :D

@pete-woods
Copy link

It worked on Go 1.18 for me

@mjudeikis
Copy link

mjudeikis commented Jul 28, 2022 via email

@pete-woods
Copy link

pete-woods commented Jul 28, 2022

@creack if you have time to look at this, it would be most appreciated, I found the API break to be extremely unintrusive (in that I didn't need to change any code)

@creack
Copy link
Owner

creack commented Jul 28, 2022 via email

@pete-woods
Copy link

minions-please

@mjudeikis
Copy link

image
Please help to remove wild forks!

@gabemarshall
Copy link

Worked for me on 1.18 and 1.17.1, thanks @photostorm 👏👏👏

@pete-woods
Copy link

@creack Is there anything we can do to help with this PR getting reviewed? Windows support is a really big deal for a bunch of users of your library.

@mohammed90
Copy link

I'm happy this PR exists. However, it introduces a breaking change: *os.File -> Pty. The func Start and StartWithAttrs return *os.File currently, but the PR changes this to type Pty.

@kr
Copy link
Collaborator

kr commented Oct 23, 2022

FWIW I agree there should be a high bar to justify incompatible changes to the interface. It's not immediately clear to me why that would be necessary to add Windows support.

@photostorm
Copy link
Author

There should be high bar to justify incompatible changes to the interface. Due to the return type of Start, I do not see a way to pass the handles. I think this API change would make it possible for other systems in future to be added.

@kr
Copy link
Collaborator

kr commented Oct 24, 2022

Would it be possible to use a Unix socket on Windows instead of a pair of pipes? Each end of a socket is full-duplex, which might mean the exported interface can be a single os.File object.

https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/

@photostorm
Copy link
Author

photostorm commented Oct 24, 2022

ok, I will look into it. Thanks for your feedback. @kr

@photostorm
Copy link
Author

@kr I do not think I can used that. there is window handle that I need access to also.

@kr
Copy link
Collaborator

kr commented Oct 26, 2022

Ultimately we will have to defer to @creack, it's his library.

My feeling is, it might be worth breaking the API, if that's absolutely necessary to support Windows. Even in that case, I think any new interface should undergo careful design review (which I could participate in).

But we haven't exhausted all the strategies to maintain compatibility. For instance, maybe the window handle could be stored on the side in a global lookup table, and the *os.File pointer or a file descriptor or something could be used to look up the window handle when it's needed.

var (
	windowHandles = map[*os.File]windows.Handle{}
	windowHandlesMu sync.Mutex
)

Choosing a suitable value to use as the map key could be tricky. The fd might not work if the client code calls dup or something. I don't have great knowledge of the Windows API and its semantics so I don't know offhand if there's a good value to use. Is there a stable way to identify a pty in Windows? Maybe something available via os.FileInfo.Sys?

Also, this would probably need to use a finalizer to delete map entries.

What do you think?

@pete-woods
Copy link

That seems like a plausible strategy to me, but without input from @creack I don't thing we can realistically progress

@sabattle
Copy link

@creack have you gotten the chance to review this PR? My team could definitely use this. At the moment we are relying on @photostorm's fork (which has worked perfectly so far)

@pete-woods
Copy link

Great, if we're going the v2 route 🎉

@creack
Copy link
Owner

creack commented Oct 30, 2023

@photostorm I can't get Setsize to work with this PR, when using it, it sets cols/rows to 1, regardless of the value I attempt to set.

This results in InheritSize not working either and any children spawn up to have either size 0/0 or 1/1.

Any idea?

Note that before calling Setsize, Getsize returns the expected value.

Sample code:

package main

import (
        "log"

        pty "github.com/creack/pty/v2"
)

func assertSize(ptm pty.Pty, expected *pty.Winsize) {
        childSize, err := pty.GetsizeFull(ptm)
        if err != nil {
                log.Fatalf("Error getting the child size: %s.", err)
        }
        if childSize.Cols != expected.Cols || childSize.Rows != expected.Rows {
                log.Fatalf("Unexpected ptm size. %v != %v.", childSize, expected)
        }
}

func main() {
        a, _, err := pty.Open()
        if err != nil {
                panic(err)
        }

        if err := pty.Setsize(a, &pty.Winsize{Rows: 10, Cols: 10}); err != nil {
                log.Fatalf("Error setsize: %s.", err)
        }

        assertSize(a, &pty.Winsize{Rows: 10, Cols: 10})

        log.Printf("Success.")
}
go get github.com/creack/pty/v2@photostorm-master-creack
GOOS=windows go build -o /tmp/foo.exe

Result

2023/10/30 11:32:52 [0] Unexpected ptm size. &{1 1 0 0} != &{10 10 0 0}.

@creack
Copy link
Owner

creack commented Nov 1, 2023

Actually, taking a step back, I can't even get write/read to work. I may be doing something wrong though, would you have any example showing a working behavior?

When I try to write or read from the tty, I always get an "already closed" error. When reading on the pty, it blocks forever regardless of what I write.

package main

import (
        "log"

        pty "github.com/creack/pty/v2"
)

func main() {
        ptmA, _, err := pty.Open()
        if err != nil {
                panic(err)
        }

        if _, err := ptmA.Write([]byte("hello\n")); err != nil {
                panic(err)
        }

        bufA := make([]byte, 5)
        if _, err := ptmA.Read(bufA); err != nil {
                panic(err)
        }

        if string(bufA) != "hello" {
                log.Fatalf("Unexpected bufA: %q.", bufA)
        }
}

@photostorm
Copy link
Author

I will take a look when I get some time later this week.

@creack
Copy link
Owner

creack commented Nov 1, 2023

Minor-ish issue: closing the pty twice causes a heap violation resulting in the parent process to die (exit status 0xc0000374).

The case happens often when dealing with network where multiple thing can cause the server to want to kill the process / close the pty.

package main

import (
	"os/exec"
	"runtime"

	"github.com/creack/pty/v2"
)

func main() {
	binName := "sh"
	if runtime.GOOS == "windows" {
		binName = "cmd"
	}
	cmd := exec.Command(binName)
	ptm, err := pty.Start(cmd)
	if err != nil {
		panic(err)
	}
	_ = ptm.Close()
	_ = ptm.Close() // Causes exit 0xc0000374.
	println("not reached on windows")
}

Note that it also happens when closing only once but when the process is already gone.

package main

import (
	"os/exec"
	"runtime"
	"time"

	"github.com/creack/pty/v2"
)

func main() {
	binName := "sh"
	if runtime.GOOS == "windows" {
		binName = "cmd"
	}
	cmd := exec.Command(binName)
	ptm, err := pty.Start(cmd)
	if err != nil {
		panic(err)
	}

	_, _ = ptm.WriteString("exit\r\n")
	time.Sleep(200e6) // Small delay to let the process die.

	_ = ptm.Close()   // Causes exit 0xc0000374.
	println("not reached on windows")
}

@creack
Copy link
Owner

creack commented Nov 1, 2023

Another issue is that I can't get a shell to properly work :(, it seems to be working at first, but as soon as I reach ~50% of the terminal, the cursor starts to jump around randomly.

https://asciinema.org/a/uNCRfSYmuuwYqoqu9Q4a7SEOn

A regular cmd or pwsh without the lib wrapping it works as expected.

EDIT: Now that I post this, I realize it is likely related to the SetSize issue mention before, thinks it has the wrong size, it moves back the cursor to where it thinks is the last line. I need to run, I'll dig more into this later.

@creack
Copy link
Owner

creack commented Nov 2, 2023

confirmed, when manually setting the size, it works fine, with one caveat, still having an issue with MaxPhysicalWindowSize though, I can't figure out how to change this value.

Some programs (like vim) use that as upper bound, which results in the wrong size.

Example open/close vim:

PS C:\> $Host.UI.RawUI

ForegroundColor       : Gray
BackgroundColor       : Black
CursorPosition        : 0,11
WindowPosition        : 0,0
CursorSize            : 25
BufferSize            : 292,69
WindowSize            : 292,69
MaxWindowSize         : 292,69
MaxPhysicalWindowSize : 1008,45
KeyAvailable          : True
WindowTitle           : Administrator: C:\Program Files\PowerShell\7\pwsh.exe


PS C:\> vim
PS C:\> $Host.UI.RawUI

ForegroundColor       : Gray
BackgroundColor       : Black
CursorPosition        : 0,27
WindowPosition        : 0,0
CursorSize            : 25
BufferSize            : 292,45
WindowSize            : 292,45
MaxWindowSize         : 292,45
MaxPhysicalWindowSize : 1008,45
KeyAvailable          : True
WindowTitle           : Administrator: C:\Program Files\PowerShell\7\pwsh.exe

@creack
Copy link
Owner

creack commented Nov 9, 2023

As OpenSSH suffers from the same MaxPhysicalWindowSize issue, once we fix the crash on .Close() and when we have a reliable way to set size, we can merge.

It would be nice if we could get a working example from windows to windows as well. For now, I only have been able to get something working when using OSX terminal/iTerm2. Using VSCode terminal, everything breaks, using windows console, everything breaks.

@creack creack mentioned this pull request Dec 25, 2023
@photostorm
Copy link
Author

photostorm commented Dec 30, 2023

I have a fix for the close issue. I am currently investigating sizing behavior. It is interesting that the setSize function seems to report a success, but when using the GetConsoleScreenBufferInfo method, it reports an invalid handle error.

@pete-woods
Copy link

I have a fix for the close issue. I am currently investigating sizing behavior. It is interesting that the setSize function seems to report a success, but when using the GetConsoleScreenBufferInfo method, it reports an invalid handle error.

Thankyou for continuing to take time on this issue

@adrivn
Copy link

adrivn commented Mar 13, 2024

Any updates? Thank you for dedicating your time to this issue

@photostorm
Copy link
Author

Any updates? Thank you for dedicating your time to this issue

Right now, I'm still working on fixing the invalid handle issue with get size. It seemed I was using the wrong type of handle for the console screen buffer info. It should be the stdout handle. However, that also seems to be reporting an invalid handle. so I just been researching to try to figure out the correct way of handling the situation. Any help would be appreciation.

theclapp and others added 3 commits March 28, 2024 09:39
pty_windows.go closed consoleW and consoleR in the initial open(), with
the comment "These pipes can be closed here without any worry."

This isn't actually true.

If you interact with the terminal via Go code (i.e. don't immediately
fork a new process, but use (for example) github.com/mvdan/sh), you need
those pipes to stay open, and to close them yourself.
Don't close consoleR & consoleW in Windows
@pete-woods
Copy link

Thankyou for continuing to plug away at this

@iyzyi
Copy link

iyzyi commented Apr 24, 2024

I don't mean to disrupt the discussion about this PR, but perhaps you could take a look at aiopty/pty/conpty/conpty_windows.go and aiopty/term/term_windows.go.

@kcmvp
Copy link

kcmvp commented May 20, 2024

thank you all very much for implementation tty on windows!
any update on this?

@creack
Copy link
Owner

creack commented May 22, 2024

Sorry for the delay, I have been swamped recently, I'll take a look as soon as I can.

@pidgeon777
Copy link

Great news! 👍

@creack creack self-requested a review May 30, 2024 14:11
@creack
Copy link
Owner

creack commented Jun 4, 2024

Getting an error when running the tests (go1.22.3)

.\io_test.go:33:32: cannot use int(ptmx.Fd()) (value of type int) as syscall.Handle value in argument to syscall.SetNonblock
.\io_test.go:69:32: cannot use int(ptmx.Fd()) (value of type int) as syscall.Handle value in argument to syscall.SetNonblock 

@creack
Copy link
Owner

creack commented Jun 4, 2024

When commenting out the SetDeadline section which cause the build to fail, I get test errors:

--- FAIL: TestSetsize (0.01s)e.
    doc_test.go:102: Unexpected Getsize X result after Setsize: 1 != 0.
    doc_test.go:103: Unexpected Getsize Y result after Setsize: 1 != 0.
    doc_test.go:104: Unexpected Getsize Rows result after Setsize: 2 != 1.
    doc_test.go:105: Unexpected Getsize Cols result after Setsize: 2 != 1.
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
--- FAIL: TestOpen (0.01s)
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
--- FAIL: TestGetsizeFull (0.02s)
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
--- FAIL: TestOpenByName (0.02s)
    doc_test.go:38: Failed to open tty file: open |0: The filename, directory name, or volume label syntax is incorrect..
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
--- FAIL: TestName (0.02s)
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
--- FAIL: TestGetsize (0.02s)
    helpers_test.go:24: Unexpected error from pty Close: Access is denied..
exit status 0xc0000374
FAIL    github.com/creack/pty/v2        0.357s

Running the tests as Administrator yields the same result, including Access is denied.

@photostorm
Copy link
Author

I still working on fixing invalid handle with get size, but just been busy with work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet