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