rprichard / winpty

A Windows software package providing an interface similar to a Unix pty-master for communicating with Windows console programs.
MIT License
1.27k stars 164 forks source link

Using winpty with neovim #63

Open tarruda opened 8 years ago

tarruda commented 8 years ago

Hi, let me start with a bit of background:

libvterm is an abstract terminal emulator library without any system dependencies(implemented in pure C99 API). It exposes a structure that keeps track of terminal state and updates the screen when it receives terminal output(fed by the user).

One program that uses this library to implement an embedded terminal emulator is neovim. ATM neovim is only officially supported on UNIX, but some community members are porting it to windows.

When running under windows, neovim spawns a program with libuv, which in turns calls windows CreateProcess API to spawn and communicate with children, transfering bytes from/to their stdio into the abstract terminal emulator. While this doesn't work with native windows console programs, I wanted to try using winpty unix adapter to make neovim fully capable of running its terminal emulator on windows.

I have some questions about winpty:

rprichard commented 8 years ago

If neovim is using a terminal emulator, am I right in assuming that neovim is a graphical program rather than terminal/console?

The console.exe program is a Cygwin/MSYS program (as opposed to winpty.dll, winpty-agent.exe, and winpty-debugserver.exe, which are normal Windows binaries compiled with either MinGW or MSVC). From reading https://github.com/neovim/neovim/issues/1749, it seems clear that neovim is targeting normal Windows, so console.exe probably won't work as-is. (I suppose in principle, neovim.exe could redistribute parts of Cygwin, but even then, console.exe expects stdin/stdout to be ptys, so it will complain if neovim starts console.exe with ordinary Windows pipes.)

FWIW: console.exe detects window dimension changes using SIGWINCH signals, which of course don't exist on Windows, but do exist on Unix, so Cygwin emulates them somehow. console.exe calls winpty_set_size when it detects the resize ([1] [2]).

I suppose console.exe could be modified to do normal Windows I/O. The JOE integration I linked below did something like this.

It might make more sense to spawn winpty-agent.exe rather than console.exe. console.exe isn't doing much besides copying bytes between stdin/stdout and the agent's pipe(s).

Invoking console.exe is almost what you want, so maybe something like that ought to work. Communicating a changed terminal size seems to be the biggest sticking point. I can think of a few ways to do it, but they've all got issues of their own:

When Windows neovim spawns a child process, it needs to ensure that it doesn't create a visible console window. IIRC, both console.exe and winpty-agent.exe are marked CONSOLE subsystem, not WINDOWS subsystem. If neovim is WINDOWS, and it uses CreateProcess to spawn a CONSOLE program, then Windows will default to creating a new console (with a corresponding console window).

Also, if you do decide to link against winpty.dll, you might want to use the newer API, currently in the libwinpty-rewrite branch. The API on master has some problems. The biggest difference is that the old API had a winpty_get_data_pipe function returning an overlapped full-duplex named pipe handle, which required API clients to do asynchronous I/O. The new API instead has functions that return pipe names, which the API client can open using any method. e.g. neovim could use uv_pipe_connect. Unfortunately, this new API isn't quite done yet, which is why it's not merged into master.

I'm willing to help more with this, because my current project is figuring out how to make winpty easier to embed (as well as merging some changes from pty4j's winpty fork).

If it helps, winpty is integrated into these other projects:

rprichard commented 8 years ago

PostThreadMessage looks like it'd work, except that it needs the thread handle returned from CreateProcess, and libuv closes the thread handle. AFAICT, there is no practical way to reopen the thread handle.

tarruda commented 8 years ago

@rprichard thanks for the insight. FWIW there's a branch where @equalsraf tried winpty unix adapter and it returned 10, which I assume is because the adapter needs to be spawned through msys/cygwin API.

If neovim is using a terminal emulator, am I right in assuming that neovim is a graphical program rather than terminal/console?

More or less. ATM neovim is a text program, but it can also start in a daemon mode where arbitrary UI clients can connect and display its font grid(which is how the windows version works), but for winpty this is irrelevant since the terminal is inside a neovim window(which as far as the UI is concerned, is just another text window)

From your comment, I think there are two reasonable ways that neovim can use winpty:

What would be the way to go, considering that we want to spawn not only windows-native console programs such as cmd.exe/powershell.exe, but also msys programs like bash.exe(BTW do we need to use msys API in order to spawn bash.exe from neovim?)

I also have a few more questions about winpty:

I'm willing to help more with this, because my current project is figuring out how to make winpty easier to embed (as well as merging some changes from pty4j's winpty fork).

I appreciate this and would love to see winpty with an easier embed interface. If you are interested seeing how neovim manages processes and help with libwinpty integration, here's a short summary:

As I mentioned above, I think implementing a pty_process_windows.c module is the way to go but I'm not sure it is possible to pass the handles returned by libwinpty to libuv. Would you mind taking a look at pty_process.c to see how well its API could map to libwinpty?(it is a very short module, about 250 loc)

rprichard commented 8 years ago

FWIW there's a branch where @equalsraf tried winpty unix adapter and it returned 10, which I assume is because the adapter needs to be spawned through msys/cygwin API.

That sounds plausible. I would expect neovim to fail because console.exe would link to a DLL like cygwin1.dll or msys-2.0.dll, which wouldn't be in the PATH. (Or, if it were in PATH, it'd write input is not a tty or output is not a tty to stderr, then exit with 1.)

From your comment, I think there are two reasonable ways that neovim can use winpty: ...

I think either method you've listed would work. The second approach seems simpler at the moment. I think I'll take a closer look in the next several days and see what's involved in both approaches.

libwinpty transparently spawns winpty-agent.exe processes. It's careful to avoid leaking inheritable handles (by passing bInheritHandles=FALSE to CreateProcess), and it uses SW_HIDE and/or puts the agent process onto a background window station / desktop. The background window station is required on at least XP and Vista so that the polling technique doesn't interfere with visible console windows.

Windows itself only allows a process to be attached to one console at a time, so there's a separate winpty-agent.exe process for each console. Each console can have many processes attached, but typically the agent spawns only one parent process. (On the master branch, the agent never spawns more than one process.) The program using libwinpty can create multiple winpty_t instances, which would spawn multiple agent processes.

  • Would it be possible to adapt the existing console.exe so that it works when spawned from msys API(mintty) and from CreateProcess API? Clearly we'd need another way to control console dimensions, but this can be dealt with later.

I think console.exe can be adapted so that it works without msys (while providing some other way to change the terminal size). I think the mode switch would need to happen at compile-time rather than run-time.

  • Do you think it would be possible to build just libwinpty(which I assume doesn't depend on msys) with microsoft compilers?

Yes, libwinpty doesn't needs Cygwin or MSYS, and it's always compiled as a normal Windows program. MinGW produces normal Windows binaries, as does Microsoft Visual C++. While winpty's Makefile uses MinGW, there's also a src/winpty.gyp file that can generate MSVC project files for all of the binaries except for console.exe. To use it, download gyp and run <path-to-gyp>/gyp winpty.gyp.

  • You mentioned we could just spawn winpty-agent.exe and communicate with it. Does that mean that there's one agent per console process and we can just send/receive data to/from its stdio(as opposed to using some protocol or named pipes)? I ask this because if there's some protocol or special named pipe usage, then it is probably best to use libwinpty API to communicate with the agent right?(Please disregard if I didn't understand how winpty works)

Currently, the winpty-agent.exe protocol is:

"C:\cygwin\usr\local\bin\winpty-agent.exe" \\.\pipe\winpty-5956-1-control \\.\pipe\winpty-5956-1-data 165 62

The protocol in the libwinpty-rewrite branch is similar; the data' pipe is replaced withconinandconout` pipes, and the agent creates them rather than libwinpty.

In principle, though, winpty-agent.exe could instead communicate terminal data over stdio. The initial child process could be passed on the command-line. (Doing so would reduce the maximum command-line, though.)

As I mentioned above, I think implementing a pty_process_windows.c module is the way to go but I'm not sure it is possible to pass the handles returned by libwinpty to libuv. Would you mind taking a look at pty_process.c to see how well its API could map to libwinpty?(it is a very short module, about 250 loc)

Sure, I'll take a look in the next few days.

rprichard commented 8 years ago

I'm still planning to look at this, but it could be a few weeks.

tarruda commented 8 years ago

@rprichard take your time, I'm just grateful that you are willing to help us :smile:

rprichard commented 8 years ago

I prototyped a neovim<->winpty integration using winpty.dll. I'm not sure it's the right way to do the integration -- invoking something like console.exe might be better, but it's a start anyway.

It's at:

(The FindWinpty.cmake file I wrote expects to find winpty.dll/winpty.lib in winpty's build directory, so winpty should be built using Cygwin or MSYS. The winpty.dll file is not a Cygwin/MSYS binary, though -- it's compiled using MinGW. A more proper build system would involve third-party somehow and maybe do something with src/winpty.gyp?)

The most interesting file is: pty_process_win.c

It has a problem where it doesn't capture all of the child's output after it's exited, and it always invokes cmd.exe, ignoring the argv and quote_cmd fields in Process.

tarruda commented 8 years ago

I prototyped a neovim<->winpty integration using winpty.dll. I'm not sure it's the right way to do the integration -- invoking something like console.exe might be better, but it's a start anyway.

@rprichard you did much more than I could have hoped for, thank you!

What is the purpose of the neovim-demo tree? Did you have to make changes in order for winpty to work with neovim?(The diff from master is 6k lines so I couldn't tell).

A more proper build system would involve third-party somehow and maybe do something with src/winpty.gyp?)

I think so. Can winpty be built with microsoft compiler in case we want to generate a visual studio project? If so I might even port src/winpty.gyp to cmake so we can build without python.

The most interesting file is: pty_process_win.c

Nice! Did you manage to build this branch and/or test the :terminal command on windows?

It has a problem where it doesn't capture all of the child's output after it's exited, and it always invokes cmd.exe, ignoring the argv and quote_cmd fields in Process.

You already did more than enough, I'm going to dig into pty_process_win.c and make any necessary adjustments. Thanks again!

rprichard commented 8 years ago

What is the purpose of the neovim-demo tree? Did you have to make changes in order for winpty to work with neovim?(The diff from master is 6k lines so I couldn't tell).

At the moment, the neovim-demo branch is the same as the libwinpty-rewrite branch, which I was working on just before this issue was opened. There are a lot of changes on that branch relative to master -- it more-or-less rewrites winpty.dll with a different API. I think its API is an improvement over the master branch API, probably. That branch has many non-API-related changes, too -- e.g. the first commit in the branch switches from C++98 to C++11 in addition to replacing a huge amount of code. In the near-term, I'm thinking I'm going to cherry-pick parts of libwinpty-rewrite into master, then rebase libwinpty-rewrite.

A more proper build system would involve third-party somehow and maybe do something with src/winpty.gyp?)

I think so. Can winpty be built with microsoft compiler in case we want to generate a visual studio project? If so I might even port src/winpty.gyp to cmake so we can build without python.

Yes, winpty definitely works with MSVC. MSVC 2013 should be new enough. My impression is that neovim is using MSVC 2015, which is good because it has much better support for C++11/14. (I'm trying to keep winpty working with MSVC 2013 and above, but I'd prefer to require MSVC 2015.)

I'm not sure I'm interested in adding a cmake file, but obviously you're free to create one. If I added one to this repo, I'd probably remove the gyp file. I noticed that neovim depends on libuv, which also uses gyp. libuv has a vcbuild.bat file that downloads gyp. It clones master from https://chromium.googlesource.com/external/gyp.

The most interesting file is: pty_process_win.c

Nice! Did you manage to build this branch and/or test the :terminal command on windows?

Indeed! The :terminal command worked on Windows. Terminal resizing worked, too, IIRC.

equalsraf commented 8 years ago

This look awesome. Thanks @rprichard.

I cleaned up that FindWinpty.cmake a bit (its just as easy to drop the winpty files in .deps/usr) and it should be a bit more verbose now if it fails to find any of the files.

@tarruda we can write a bit of CMake to build in third-party, but I have already tested and I was able to link against the DLL from MSVC and MinGW builds, so maybe we can download binary builds instead of building from source. My work branch is here - https://github.com/equalsraf/neovim/commits/tb-staging-winpty (moved into neovim/neovim#810) - other than minor fixes there are no differences.

tarruda commented 8 years ago

@tarruda we can write a bit of CMake to build in third-party, but I have already tested and I was able to link against the DLL from MSVC and MinGW builds, so maybe we can download binary builds instead of building from source. My work branch is here - https://github.com/equalsraf/neovim/commits/tb-staging-winpty (moved into neovim/neovim#810) - other than minor fixes there are no differences.

@equalsraf great! I built rp-winpty branch locally in msys2 but neovim aborted when term: was invoked. It seems a libuv failed assertion was the cause. Since @rprichard managed to build and run locally, I assume this could be caused by building with mingw(I dont have visual studio installed so I couldnt test)

If you publish a prebuilt bundle of nvim + nvim-qt + winpty let me know, I've been wanting to run powershell in nvim for some time :)

equalsraf commented 8 years ago

The builds in 810 from appveyor already have neovim+winpty. https://ci.appveyor.com/project/equalsraf/neovim/branch/tb-mingw

You can get neovim-qt from its release page https://github.com/equalsraf/neovim-qt/releases

am11 commented 8 years ago

@rprichard, (bit off-topic; at the risk of it being inapplicable here) if we capture the device contexts of any Windows application via win32 API, we can get info about (and even modify) its graphical formation. I fiddled with this kind of thing once in Win2k days; captured a notepad's hDC to project its events onto another instance of notepad (mirroring all events including the user inputs). This was done as part of experimenting a custom remote desktop projection, in a server-client model. One thing I still remember is; as soon as the target application is minimized, the graphics handle goes haywire (all events go off). Although it is very low-level stuff and might not work seamlessly right off the bat (and worth the effort neither); just wanted to highlight the fact that with tooth and nail effort, it might be possible for consumers to build a resize event listener using win32 API without polling. Nonetheless, I think libcurses already provides SIGWINCH equivalent implementation on Windows.

rprichard commented 8 years ago

@am11 I'm not sure how device contexts (HDC) would alleviate polling. Can you refer to specific APIs? I'm guessing the idea is that winpty would somehow intercept drawing activity? The console is always hidden somehow. Currently it's always on a special window station / desktop.

The Console WinEvents API might allow winpty to stop polling for changes on Win7 and up. (https://github.com/rprichard/winpty/issues/46).

Nonetheless, I think libcurses already provides SIGWINCH equivalent implementation on Windows.

libcurses is designed for terminal/console programs. For such programs, it's easy to detect console size changes w/o polling. Just use ReadConsoleInput, looking for WINDOW_BUFFER_SIZE_RECORD records, with the ENABLE_WINDOW_INPUT mode flag enabled. I would guess that's how SIGWINCH is emulated on Windows.

am11 commented 8 years ago

@rprichard, I have that doodlebug project buried in some external hard drive. I will try to locate it and post more info to #46 if I find it. If memory serves; I think we were able to workaround polling by getting some semaphore about the target application's graphics state, and subscribed to some OS signal.

I'm guessing the idea is that winpty would somehow intercept drawing activity?

Yes, the idea was to be able to intercept graphics context of a PC game, capture its graphical state, encode it in a low bit-rate JPEG or likes and transmit it over the network (flash app in browser, where user's click coordinates were supposed to be captured and respond back to server for simulation). We just did some experimentations on notepad and later bailed out because of image encoding from raw graphics to optimal format + latency issues that we could not tackle at the time.

tarruda commented 8 years ago

@equalsraf thanks, just tested and it worked nicely :)

@rprichard I noticed that winpty works like a unix terminal program on "raw mode"(with little/no interpretation of bytes by the terminal driver). For example:

There might be others from this table that are not being handled(didnt test much). As a consequence, some keys like ctrl+c/ctrl+d still don't work(enter works because libvterm sends \r). To ensure it was not a problem on our end, I used the jobsend() function to put these sequences directly in winpty pipe.

Also tested how winpty handles large bursts of output and was very impressed: Even though winpty gathers data through polling, it is very unlikely to miss output. To test I spawned the python interactive interpreter and did the following:

Nevertheless, I don't think this will ever be an issue in real world usage, it still worked much better than I thought it would. You did an amazing work on this project, thanks again!

rprichard commented 8 years ago

@rprichard I noticed that winpty works like a unix terminal program on "raw mode"(with little/no interpretation of bytes by the terminal driver). For example:

I think that roughly describes winpty's behavior. It converts input data into INPUT_RECORD records, and line editing is all handled either by Windows itself or (sometimes) by the console application. If Windows is handling the line editing, then e.g. pressing F7 will bring up the history popup window.

  • newline characters are not interpreted as carriage return, which is the default behavior on UNIX
  • \x03(ETX) is not interpreted as ctrl+c

Ctrl-C is handled specially. If ENABLE_PROCESSED_INPUT is enabled, then Ctrl-C results in a call to GenerateConsoleCtrlEvent

  • \x04(EOT) is not interpreted as ctrl+d

    There might be others from this table that are not being handled(didnt test much). As a consequence, some keys like ctrl+c/ctrl+d still don't work(enter works because libvterm sends \r). To ensure it was not a problem on our end, I used the jobsend() function to put these sequences directly in winpty pipe.

The Ctrl-modified characters are handled in the same code path as ordinary characters. winpty is delegating all the work to VkKeyScan. In my experience, Ctrl-C and Ctrl-D have worked, so I'm not sure what issue you're hitting. (I just tested neovim-qt, and Ctrl-D seemed to work. There were other keys that didn't, like the function keys.) That same code path also handles '\x0A'/LF and '\x0D'/CR, but I'm not sure what's supposed to happen.

There is a different code path for backspace, which is handled by the "input map", which also handles all the keyboard escape sequences. The keyboard escape sequences are all listed in the DefaultInputMap.

Some tools for debugging keyboard handling:

Perhaps Ctrl-C is being mishandled, though, when the console is in unprocessed mode. In a normal console, --show-input for Ctrl-C is no different than Ctrl-A:

C:\rprichard\proj\winpty\winpty>build\winpty-agent.exe --show-input

Press any keys -- Ctrl-D exits

key: dn rpt=1 scn=29 LCtrl-CONTROL ch=0
key: dn rpt=1 scn=30 LCtrl-A ch=0x1
key: up rpt=1 scn=30 LCtrl-A ch=0x1
key: up rpt=1 scn=29 CONTROL ch=0
key: dn rpt=1 scn=29 LCtrl-CONTROL ch=0
key: dn rpt=1 scn=46 LCtrl-C ch=0x3
key: up rpt=1 scn=46 LCtrl-C ch=0x3
key: up rpt=1 scn=29 CONTROL ch=0

When using winpty, though, it's converted to VK_CANCEL:

$ build/console build/winpty-agent.exe --show-input

Press any keys -- Ctrl-D exits

key: dn rpt=1 scn=29 LCtrl-CONTROL ch=0
key: dn rpt=1 scn=30 LCtrl-A ch=0x1
key: up rpt=1 scn=30 LCtrl-A ch=0x1
key: up rpt=1 scn=29 CONTROL ch=0
key: dn rpt=1 scn=70 CANCEL ch=0x3
key: up rpt=1 scn=70 CANCEL ch=0x3
rprichard commented 8 years ago

winpty doesn't implement "backpressure", though I think it should.

Whenever a user is selecting text in a Windows console, ordinary writes to the console (e.g. WriteConsole / WriteFile) block, but low-level I/O can still occur (e.g. WriteConsoleOutput, ReadConsoleOutput). winpty exploits this behavior to prevent the console from scrolling while it's scraping the content. In theory, it could leave the console selection active if the winpty client isn't reading from the CONOUT pipe fast enough. I've been thinking about implementing this behavior for a while, but haven't gotten around to it.

Instead, IIRC, winpty has a dynamically-resizing output buffer, and if the winpty client doesn't read quickly enough, the buffer will grow arbitrarily large. That's unfortunate, because if a program is spewing output too quickly, and the user hits Ctrl-C to kill the program, output may continue spewing for a while.

rprichard commented 8 years ago

FWIW: the libwinpty-rewrite and neovim-demo branches in winpty are obsolete now. The master branch has a new API now that's almost identical to the one neovim used, so it should be easy to move over.

equalsraf commented 8 years ago

Looking at the msys2 releases, it seems the winpty.dll does not depend on msys-2.dll (which makes sense, since thats exactly what neovim needs for native builds). It might make sense to point that out in the release notes, or add another zip with just the lib and includes.

@rprichard I didn't get a chance to work this through in Neovim yet. But ideally I'd like to start a PR in https://github.com/Alexpux/MINGW-packages to have a libwinpty package.

rprichard commented 8 years ago

Yes, the winpty.dll, winpty.lib, and winpty-agent.exe files should be functionally equivalent across all of the packages, except for the 32-vs-64-bit difference. The code size varies quite a bit, and I think at least one package is using SJLJ exceptions.

I also wrote a ship/make_msvc_package.py script that builds a ZIP file containing winpty.dll, winpty.lib, and the header files, etc. I'm currently planning to use it for the upcoming 0.4 release. It produces ZIP files like this:

Path = winpty-0.4.0-msvc2015.zip
Type = zip
Physical Size = 2368213

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2016-06-14 05:18:55 D....            0            0  ia32
2016-06-14 05:18:55 D....            0            0  ia32\bin
2016-06-14 05:18:55 ....A       592384       205937  ia32\bin\winpty-agent.exe
2016-06-14 05:18:55 ....A       473600       159457  ia32\bin\winpty-debugserver.exe
2016-06-14 05:18:55 ....A       509440       171321  ia32\bin\winpty.dll
2016-06-14 05:18:55 D....            0            0  ia32\lib
2016-06-14 05:18:55 ....A         5564         1081  ia32\lib\winpty.lib
2016-06-14 05:17:57 D....            0            0  ia32_xp
2016-06-14 05:17:57 D....            0            0  ia32_xp\bin
2016-06-14 05:17:57 ....A       592384       205937  ia32_xp\bin\winpty-agent.exe
2016-06-14 05:17:57 ....A       473600       159455  ia32_xp\bin\winpty-debugserver.exe
2016-06-14 05:17:57 ....A       509440       171320  ia32_xp\bin\winpty.dll
2016-06-14 05:17:57 D....            0            0  ia32_xp\lib
2016-06-14 05:17:57 ....A         5564         1080  ia32_xp\lib\winpty.lib
2016-06-14 05:19:24 D....            0            0  include
2016-06-14 05:19:24 ....A         8885         3134  include\winpty.h
2016-06-14 05:19:24 ....A         5697         2297  include\winpty_constants.h
2016-06-14 05:19:24 ....A         1085          638  LICENSE
2016-06-14 05:19:24 ....A         5551         2324  README.md
2016-06-14 05:19:24 ....A         9783         3986  RELEASES.md
2016-06-14 05:19:24 D....            0            0  x64
2016-06-14 05:19:24 D....            0            0  x64\bin
2016-06-14 05:19:24 ....A       740864       244336  x64\bin\winpty-agent.exe
2016-06-14 05:19:24 ....A       592384       188421  x64\bin\winpty-debugserver.exe
2016-06-14 05:19:24 ....A       638464       203802  x64\bin\winpty.dll
2016-06-14 05:19:24 D....            0            0  x64\lib
2016-06-14 05:19:24 ....A         5482         1077  x64\lib\winpty.lib
2016-06-14 05:18:26 D....            0            0  x64_xp
2016-06-14 05:18:26 D....            0            0  x64_xp\bin
2016-06-14 05:18:26 ....A       740864       244336  x64_xp\bin\winpty-agent.exe
2016-06-14 05:18:26 ....A       592384       188421  x64_xp\bin\winpty-debugserver.exe
2016-06-14 05:18:26 ....A       638464       203800  x64_xp\bin\winpty.dll
2016-06-14 05:18:26 D....            0            0  x64_xp\lib
2016-06-14 05:18:26 ....A         5482         1077  x64_xp\lib\winpty.lib
------------------- ----- ------------ ------------  ------------------------
2016-06-14 05:19:24            7147365      2363237  21 files, 13 folders

Would that useful for neovim?

equalsraf commented 8 years ago

Would that useful for neovim?

Very. In fact this is very similar to the structure I had in place to fetch a prebuilt zip for winpty.