1 package com.pty4j.windows;
2 
3 import com.pty4j.WinSize;
4 import com.pty4j.util.PtyUtil;
5 import com.sun.jna.*;
6 import com.sun.jna.platform.win32.Kernel32;
7 import com.sun.jna.platform.win32.WinBase;
8 import com.sun.jna.platform.win32.WinNT;
9 import com.sun.jna.ptr.IntByReference;
10 import com.sun.jna.ptr.PointerByReference;
11 import org.apache.log4j.Logger;
12 import org.jetbrains.annotations.NotNull;
13 import org.jetbrains.annotations.Nullable;
14 
15 import java.io.File;
16 import java.io.IOException;
17 
18 import static com.sun.jna.platform.win32.WinBase.INFINITE;
19 import static com.sun.jna.platform.win32.WinNT.GENERIC_READ;
20 import static com.sun.jna.platform.win32.WinNT.GENERIC_WRITE;
21 
22 /**
23  * @author traff
24  */
25 public class WinPty {
26 
27   private static final Logger LOG = Logger.getLogger(WinPty.class);
28 
29   private static final boolean DEFAULT_MIN_INITIAL_TERMINAL_WINDOW_HEIGHT =
30     !Boolean.getBoolean("disable.minimal.initial.terminal.window.height");
31 
32   private Pointer myWinpty;
33 
34   private WinNT.HANDLE myProcess;
35   private NamedPipe myConinPipe;
36   private NamedPipe myConoutPipe;
37   private NamedPipe myConerrPipe;
38 
39   private boolean myChildExited = false;
40   private int myStatus = -1;
41   private boolean myClosed = false;
42   private WinSize myLastWinSize;
43 
44   private int openInputStreamCount = 0;
45 
WinPty(@otNull String cmdline, @Nullable String cwd, @NotNull String env, boolean consoleMode, @Nullable Integer initialColumns, @Nullable Integer initialRows, boolean enableAnsiColor)46   WinPty(@NotNull String cmdline,
47          @Nullable String cwd,
48          @NotNull String env,
49          boolean consoleMode,
50          @Nullable Integer initialColumns,
51          @Nullable Integer initialRows,
52          boolean enableAnsiColor) throws WinPtyException, IOException {
53     int cols = initialColumns != null ? initialColumns : Integer.getInteger("win.pty.cols", 80);
54     int rows = getInitialRows(initialRows);
55     IntByReference errCode = new IntByReference();
56     PointerByReference errPtr = new PointerByReference(null);
57     Pointer agentCfg = null;
58     Pointer spawnCfg = null;
59     Pointer winpty = null;
60     WinNT.HANDLEByReference processHandle = new WinNT.HANDLEByReference();
61     NamedPipe coninPipe = null;
62     NamedPipe conoutPipe = null;
63     NamedPipe conerrPipe = null;
64 
65     try {
66       // Configure the winpty agent.
67       long agentFlags = 0;
68       if (consoleMode) {
69         agentFlags = WinPtyLib.WINPTY_FLAG_CONERR | WinPtyLib.WINPTY_FLAG_PLAIN_OUTPUT;
70         if (enableAnsiColor) {
71           agentFlags |= WinPtyLib.WINPTY_FLAG_COLOR_ESCAPES;
72         }
73       }
74       agentCfg = INSTANCE.winpty_config_new(agentFlags, null);
75       if (agentCfg == null) {
76         throw new WinPtyException("winpty agent cfg is null");
77       }
78       INSTANCE.winpty_config_set_initial_size(agentCfg, cols, rows);
79       myLastWinSize = new WinSize(cols, rows);
80 
81       // Start the agent.
82       winpty = INSTANCE.winpty_open(agentCfg, errPtr);
83       if (winpty == null) {
84         WString errMsg = INSTANCE.winpty_error_msg(errPtr.getValue());
85         String errorMessage = errMsg.toString();
86         if ("ConnectNamedPipe failed: Windows error 232".equals(errorMessage)) {
87           errorMessage += "\n" + suggestFixForError232();
88         }
89         throw new WinPtyException("Error starting winpty: " + errorMessage);
90       }
91 
92       // Connect the pipes.  These calls return immediately (i.e. they don't block).
93       coninPipe = NamedPipe.connectToServer(INSTANCE.winpty_conin_name(winpty).toString(), GENERIC_WRITE);
94       conoutPipe = NamedPipe.connectToServer(INSTANCE.winpty_conout_name(winpty).toString(), GENERIC_READ);
95       if (consoleMode) {
96         conerrPipe = NamedPipe.connectToServer(INSTANCE.winpty_conerr_name(winpty).toString(), GENERIC_READ);
97       }
98 
99       for (int i = 0; i < 5; i++) {
100         boolean result = INSTANCE.winpty_set_size(winpty, cols, rows, null);
101         if (!result) {
102           LOG.info("Cannot resize to workaround extra newlines issue");
103           break;
104         }
105         try {
106           Thread.sleep(10);
107         }
108         catch (InterruptedException e) {
109           e.printStackTrace();
110         }
111       }
112 
113       // Spawn a child process.
114       spawnCfg = INSTANCE.winpty_spawn_config_new(
115           WinPtyLib.WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN |
116               WinPtyLib.WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN,
117           null,
118           toWString(cmdline),
119           toWString(cwd),
120           toWString(env),
121           null);
122       if (spawnCfg == null) {
123         throw new WinPtyException("winpty spawn cfg is null");
124       }
125       if (!INSTANCE.winpty_spawn(winpty, spawnCfg, processHandle, null, errCode, errPtr)) {
126         WString errMsg = INSTANCE.winpty_error_msg(errPtr.getValue());
127         throw new WinPtyException("Error running process: " + errMsg.toString() + ". Code " + errCode.getValue());
128       }
129 
130       // Success!  Save the values we want and let the `finally` block clean up the rest.
131 
132       myWinpty = winpty;
133       myProcess = processHandle.getValue();
134       myConinPipe = coninPipe;
135       myConoutPipe = conoutPipe;
136       myConerrPipe = conerrPipe;
137       openInputStreamCount = consoleMode ? 2 : 1;
138 
139       // Designate a thread to wait for the process to exit.
140       Thread waitForExit = new WaitForExitThread();
141       waitForExit.setDaemon(true);
142       waitForExit.start();
143 
144       winpty = null;
145       processHandle.setValue(null);
146       coninPipe = conoutPipe = conerrPipe = null;
147 
148     } finally {
149       INSTANCE.winpty_error_free(errPtr.getValue());
150       INSTANCE.winpty_config_free(agentCfg);
151       INSTANCE.winpty_spawn_config_free(spawnCfg);
152       INSTANCE.winpty_free(winpty);
153       if (processHandle.getValue() != null) {
154         Kernel32.INSTANCE.CloseHandle(processHandle.getValue());
155       }
156       closeNamedPipeQuietly(coninPipe);
157       closeNamedPipeQuietly(conoutPipe);
158       closeNamedPipeQuietly(conerrPipe);
159     }
160   }
161 
162   @NotNull
suggestFixForError232()163   private static String suggestFixForError232() {
164     try {
165       File dllFile = new File(getLibraryPath());
166       File exeFile = new File(dllFile.getParentFile(), "winpty-agent.exe");
167       return "This error can occur due to antivirus blocking winpty from creating a pty. Please exclude the following files in your antivirus:\n" +
168              " - " + exeFile.getAbsolutePath() + "\n" +
169              " - " + dllFile.getAbsolutePath();
170     }
171     catch (Exception e) {
172       return e.getMessage();
173     }
174   }
175 
getInitialRows(@ullable Integer initialRows)176   private int getInitialRows(@Nullable Integer initialRows) {
177     if (initialRows != null) {
178       return initialRows;
179     }
180     Integer rows = Integer.getInteger("win.pty.rows");
181     if (rows != null) {
182       return rows;
183     }
184     try {
185       WindowsVersion.getVersion();
186       // workaround for https://github.com/Microsoft/console/issues/270
187       return DEFAULT_MIN_INITIAL_TERMINAL_WINDOW_HEIGHT ? 1 : 25;
188     }
189     catch (Exception e) {
190       e.printStackTrace();
191       return 25;
192     }
193   }
194 
closeNamedPipeQuietly(NamedPipe pipe)195   private static void closeNamedPipeQuietly(NamedPipe pipe) {
196     try {
197       if (pipe != null) {
198         pipe.close();
199       }
200     } catch (IOException e) {
201     }
202   }
203 
toWString(String string)204   private static WString toWString(String string) {
205     return string == null ? null : new WString(string);
206   }
207 
setWinSize(@otNull WinSize winSize)208   synchronized void setWinSize(@NotNull WinSize winSize) throws IOException {
209     if (myClosed) {
210       throw new IOException("Unable to set window size: closed=" + myClosed + ", winSize=" + winSize);
211     }
212     boolean result = INSTANCE.winpty_set_size(myWinpty, winSize.getColumns(), winSize.getRows(), null);
213     if (result) {
214       myLastWinSize = new WinSize(winSize.getColumns(), winSize.getRows());
215     }
216   }
217 
getWinSize()218   synchronized @NotNull WinSize getWinSize() throws IOException {
219     // The implementation might be improved after https://github.com/rprichard/winpty/issues/153
220     WinSize lastWinSize = myLastWinSize;
221     if (myClosed || lastWinSize == null) {
222       throw new IOException("Unable to get window size: closed=" + myClosed + ", lastWinSize=" + lastWinSize);
223     }
224     return new WinSize(lastWinSize.getColumns(), lastWinSize.getRows());
225   }
226 
decrementOpenInputStreamCount()227   synchronized void decrementOpenInputStreamCount() {
228     openInputStreamCount--;
229     if (openInputStreamCount == 0) {
230       close();
231     }
232   }
233 
234   // Close the winpty_t object, which disconnects libwinpty from the winpty
235   // agent process.  The agent will then close the hidden console, killing
236   // everything attached to it.
close()237   synchronized void close() {
238     // This function can be called from WinPty.finalize, so its member fields
239     // may have already been finalized.  The JNA Pointer class has no finalizer,
240     // so it's safe to use, and the various JNA Library objects are static, so
241     // they won't ever be collected.
242     if (myClosed) {
243       return;
244     }
245     INSTANCE.winpty_free(myWinpty);
246     myWinpty = null;
247     myClosed = true;
248     closeUnusedProcessHandle();
249   }
250 
closeUnusedProcessHandle()251   private synchronized void closeUnusedProcessHandle() {
252     // Keep the process handle open until both conditions are met:
253     //  1. The process has exited.
254     //  2. We have disconnected from the agent, by closing the winpty_t
255     //     object.
256     // As long as the process handle is open, Windows will not reuse the child
257     // process' PID.
258     // https://blogs.msdn.microsoft.com/oldnewthing/20110107-00/?p=11803
259     if (myClosed && myChildExited && myProcess != null) {
260       Kernel32.INSTANCE.CloseHandle(myProcess);
261       myProcess = null;
262     }
263   }
264 
265   // Returns true if the child process is still running.  The winpty_t and
266   // WinPty objects may be closed/freed either before or after the child
267   // process exits.
isRunning()268   synchronized boolean isRunning() {
269     return !myChildExited;
270   }
271 
272   // Waits for the child process to exit.
waitFor()273   synchronized int waitFor() throws InterruptedException {
274     while (!myChildExited) {
275       wait();
276     }
277     return myStatus;
278   }
279 
getChildProcessId()280   synchronized int getChildProcessId() {
281     if (myClosed) {
282       return -1;
283     }
284     return Kernel32.INSTANCE.GetProcessId(myProcess);
285   }
286 
exitValue()287   synchronized int exitValue() {
288     if (!myChildExited) {
289       throw new IllegalThreadStateException("Process not Terminated");
290     }
291     return myStatus;
292   }
293 
294   @Override
finalize()295   protected void finalize() throws Throwable {
296     close();
297     super.finalize();
298   }
299 
getInputPipe()300   NamedPipe getInputPipe() {
301     return myConoutPipe;
302   }
303 
getOutputPipe()304   NamedPipe getOutputPipe() {
305     return myConinPipe;
306   }
307 
getErrorPipe()308   NamedPipe getErrorPipe() {
309     return myConerrPipe;
310   }
311 
312   @Nullable
getWorkingDirectory()313   String getWorkingDirectory() throws IOException {
314     if (myClosed) {
315       return null;
316     }
317     int bufferLength = 1024;
318     Pointer buffer = new Memory(Native.WCHAR_SIZE * bufferLength);
319     PointerByReference errPtr = new PointerByReference();
320     try {
321       int result = INSTANCE.winpty_get_current_directory(myWinpty, bufferLength, buffer, errPtr);
322       if (result > 0) {
323         return buffer.getWideString(0);
324       }
325       WString message = INSTANCE.winpty_error_msg(errPtr.getValue());
326       int code = INSTANCE.winpty_error_code(errPtr.getValue());
327       throw new IOException("winpty_get_current_directory failed, code: " + code + ", message: " + message);
328     }
329     finally {
330       INSTANCE.winpty_error_free(errPtr.getValue());
331     }
332   }
333 
getConsoleProcessList()334   int getConsoleProcessList() throws IOException {
335     if (myClosed) {
336       return 0;
337     }
338     int MAX_COUNT = 64;
339     Pointer buffer = new Memory(Native.LONG_SIZE * MAX_COUNT);
340     PointerByReference errPtr = new PointerByReference();
341     try {
342       int actualProcessCount = INSTANCE.winpty_get_console_process_list(myWinpty, buffer, MAX_COUNT, errPtr);
343       if (actualProcessCount == 0) {
344         WString message = INSTANCE.winpty_error_msg(errPtr.getValue());
345         int code = INSTANCE.winpty_error_code(errPtr.getValue());
346         throw new IOException("winpty_get_console_process_list failed, code: " + code + ", message: " + message);
347       }
348       // use buffer.getIntArray(0, actualProcessCount); to get actual PIDs
349       return actualProcessCount;
350     }
351     finally {
352       INSTANCE.winpty_error_free(errPtr.getValue());
353     }
354   }
355 
356   // It is mostly possible to avoid using this thread; instead, the above
357   // methods could call WaitForSingleObject themselves, using either a 0 or
358   // INFINITE timeout as appropriate.  It is tricky, though, because we need
359   // to avoid closing the process handle as long as any threads are waiting on
360   // it, but we can't do an INFINITE wait inside a synchronized method.  It
361   // could be done using an extra reference count, or by using DuplicateHandle
362   // for INFINITE waits.
363   private class WaitForExitThread extends Thread {
364     private IntByReference myStatusByRef = new IntByReference(-1);
365 
366     @Override
run()367     public void run() {
368       Kernel32.INSTANCE.WaitForSingleObject(myProcess, INFINITE);
369       Kernel32.INSTANCE.GetExitCodeProcess(myProcess, myStatusByRef);
370       synchronized (WinPty.this) {
371         WinPty.this.myChildExited = true;
372         WinPty.this.myStatus = myStatusByRef.getValue();
373         closeUnusedProcessHandle();
374         WinPty.this.notifyAll();
375       }
376     }
377   }
378 
379   static final Kern32 KERNEL32 = Native.loadLibrary("kernel32", Kern32.class);
380 
381   interface Kern32 extends Library {
PeekNamedPipe(WinNT.HANDLE hFile, Pointer lpBuffer, int nBufferSize, IntByReference lpBytesRead, IntByReference lpTotalBytesAvail, IntByReference lpBytesLeftThisMessage)382     boolean PeekNamedPipe(WinNT.HANDLE hFile,
383                           Pointer lpBuffer,
384                           int nBufferSize,
385                           IntByReference lpBytesRead,
386                           IntByReference lpTotalBytesAvail,
387                           IntByReference lpBytesLeftThisMessage);
388 
ReadFile(WinNT.HANDLE file, Pointer buf, int len, IntByReference actual, Pointer over)389     boolean ReadFile(WinNT.HANDLE file, Pointer buf, int len, IntByReference actual, Pointer over);
390 
WriteFile(WinNT.HANDLE file, Pointer buf, int len, IntByReference actual, Pointer over)391     boolean WriteFile(WinNT.HANDLE file, Pointer buf, int len, IntByReference actual, Pointer over);
392 
GetOverlappedResult(WinNT.HANDLE file, Pointer over, IntByReference actual, boolean wait)393     boolean GetOverlappedResult(WinNT.HANDLE file, Pointer over, IntByReference actual, boolean wait);
394 
CreateNamedPipeA(String lpName, int dwOpenMode, int dwPipeMode, int nMaxInstances, int nOutBufferSize, int nInBufferSize, int nDefaultTimeout, WinBase.SECURITY_ATTRIBUTES securityAttributes)395     WinNT.HANDLE CreateNamedPipeA(String lpName,
396                                   int dwOpenMode,
397                                   int dwPipeMode,
398                                   int nMaxInstances,
399                                   int nOutBufferSize,
400                                   int nInBufferSize,
401                                   int nDefaultTimeout,
402                                   WinBase.SECURITY_ATTRIBUTES securityAttributes);
403 
ConnectNamedPipe(WinNT.HANDLE hNamedPipe, WinBase.OVERLAPPED overlapped)404     boolean ConnectNamedPipe(WinNT.HANDLE hNamedPipe, WinBase.OVERLAPPED overlapped);
405 
CloseHandle(WinNT.HANDLE hObject)406     boolean CloseHandle(WinNT.HANDLE hObject);
407 
CreateEventA(WinBase.SECURITY_ATTRIBUTES lpEventAttributes, boolean bManualReset, boolean bInitialState, String lpName)408     WinNT.HANDLE CreateEventA(WinBase.SECURITY_ATTRIBUTES lpEventAttributes, boolean bManualReset, boolean bInitialState, String lpName);
409 
GetLastError()410     int GetLastError();
411 
WaitForSingleObject(WinNT.HANDLE hHandle, int dwMilliseconds)412     int WaitForSingleObject(WinNT.HANDLE hHandle, int dwMilliseconds);
413 
CancelIo(WinNT.HANDLE hFile)414     boolean CancelIo(WinNT.HANDLE hFile);
415 
GetCurrentProcessId()416     int GetCurrentProcessId();
417   }
418 
419   private static WinPtyLib INSTANCE = Native.loadLibrary(getLibraryPath(), WinPtyLib.class);
420 
getLibraryPath()421   private static String getLibraryPath() {
422     try {
423       return PtyUtil.resolveNativeLibrary().getAbsolutePath();
424     }
425     catch (Exception e) {
426       throw new IllegalStateException("Couldn't detect jar containing folder", e);
427     }
428   }
429 
430   private interface WinPtyLib extends Library {
431     /*
432      * winpty API.
433      */
434 
435     long WINPTY_FLAG_CONERR = 1;
436     long WINPTY_FLAG_PLAIN_OUTPUT = 2;
437     long WINPTY_FLAG_COLOR_ESCAPES = 4;
438 
439     long WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN = 1;
440     long WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN = 2;
441 
winpty_error_code(Pointer err)442     int winpty_error_code(Pointer err);
winpty_error_msg(Pointer err)443     WString winpty_error_msg(Pointer err);
winpty_error_free(Pointer err)444     void winpty_error_free(Pointer err);
445 
winpty_config_new(long flags, PointerByReference err)446     Pointer winpty_config_new(long flags, PointerByReference err);
winpty_config_free(Pointer cfg)447     void winpty_config_free(Pointer cfg);
winpty_config_set_initial_size(Pointer cfg, int cols, int rows)448     void winpty_config_set_initial_size(Pointer cfg, int cols, int rows);
winpty_open(Pointer cfg, PointerByReference err)449     Pointer winpty_open(Pointer cfg, PointerByReference err);
450 
winpty_conin_name(Pointer wp)451     WString winpty_conin_name(Pointer wp);
winpty_conout_name(Pointer wp)452     WString winpty_conout_name(Pointer wp);
winpty_conerr_name(Pointer wp)453     WString winpty_conerr_name(Pointer wp);
454 
winpty_spawn_config_new(long flags, WString appname, WString cmdline, WString cwd, WString env, PointerByReference err)455     Pointer winpty_spawn_config_new(long flags,
456                                     WString appname,
457                                     WString cmdline,
458                                     WString cwd,
459                                     WString env,
460                                     PointerByReference err);
461 
winpty_spawn_config_free(Pointer cfg)462     void winpty_spawn_config_free(Pointer cfg);
463 
winpty_spawn(Pointer winpty, Pointer cfg, WinNT.HANDLEByReference process_handle, WinNT.HANDLEByReference thread_handle, IntByReference create_process_error, PointerByReference err)464     boolean winpty_spawn(Pointer winpty,
465                          Pointer cfg,
466                          WinNT.HANDLEByReference process_handle,
467                          WinNT.HANDLEByReference thread_handle,
468                          IntByReference create_process_error,
469                          PointerByReference err);
470 
winpty_set_size(Pointer winpty, int cols, int rows, PointerByReference err)471     boolean winpty_set_size(Pointer winpty, int cols, int rows, PointerByReference err);
472 
winpty_get_console_process_list(Pointer winpty, Pointer processList, int processCount, PointerByReference err)473     int winpty_get_console_process_list(Pointer winpty,
474                                         Pointer processList,
475                                         int processCount,
476                                         PointerByReference err);
477 
winpty_get_current_directory(Pointer winpty, int nBufferLength, Pointer lpBuffer, PointerByReference err)478     int winpty_get_current_directory(Pointer winpty,
479                                      int nBufferLength,
480                                      Pointer lpBuffer,
481                                      PointerByReference err);
winpty_free(Pointer winpty)482     void winpty_free(Pointer winpty);
483   }
484 }
485