1 /* 2 * Copyright (c) 2000, 2011 QNX Software Systems and others. 3 * All rights reserved. This program and the accompanying materials 4 * are made available under the terms of the Eclipse Public License v1.0 5 * which accompanies this distribution, and is available at 6 * http://www.eclipse.org/legal/epl-v10.html 7 */ 8 package com.pty4j.unix; 9 10 import com.google.common.base.MoreObjects; 11 import com.pty4j.PtyProcess; 12 import com.pty4j.PtyProcessOptions; 13 import com.pty4j.WinSize; 14 import com.pty4j.util.PtyUtil; 15 import org.jetbrains.annotations.NotNull; 16 import org.jetbrains.annotations.Nullable; 17 18 import java.io.IOException; 19 import java.io.InputStream; 20 import java.io.OutputStream; 21 import java.util.Arrays; 22 23 public class UnixPtyProcess extends PtyProcess { 24 private static final int NOOP = 0; 25 26 // Signals with portable numbers (https://en.wikipedia.org/wiki/Signal_(IPC)#POSIX_signals) 27 private static final int SIGHUP = 1; 28 private static final int SIGKILL = 9; 29 private static final int SIGTERM = 15; 30 31 private static final int ENOTTY = 25; // Not a typewriter 32 33 private int pid = 0; 34 private int myExitCode; 35 private boolean isDone; 36 private OutputStream out; 37 private InputStream in; 38 private InputStream err; 39 private final Pty myPty; 40 private final Pty myErrPty; 41 42 @Deprecated UnixPtyProcess(String[] cmdarray, String[] envp, String dir, Pty pty, Pty errPty)43 public UnixPtyProcess(String[] cmdarray, String[] envp, String dir, Pty pty, Pty errPty) throws IOException { 44 if (dir == null) { 45 dir = "."; 46 } 47 if (pty == null) { 48 throw new IOException("pty cannot be null"); 49 } 50 myPty = pty; 51 myErrPty = errPty; 52 execInPty(cmdarray, envp, dir, pty, errPty, null, null); 53 } 54 UnixPtyProcess(@otNull PtyProcessOptions options, boolean consoleMode)55 public UnixPtyProcess(@NotNull PtyProcessOptions options, boolean consoleMode) throws IOException { 56 myPty = new Pty(consoleMode, options.isUnixOpenTtyToPreserveOutputAfterTermination()); 57 myErrPty = options.isRedirectErrorStream() ? null : (consoleMode ? new Pty() : null); 58 String dir = MoreObjects.firstNonNull(options.getDirectory(), "."); 59 execInPty(options.getCommand(), PtyUtil.toStringArray(options.getEnvironment()), dir, myPty, myErrPty, 60 options.getInitialColumns(), options.getInitialRows()); 61 } 62 getPty()63 public Pty getPty() { 64 return myPty; 65 } 66 67 @Override finalize()68 protected void finalize() throws Throwable { 69 closeUnusedStreams(); 70 super.finalize(); 71 } 72 73 /** 74 * See java.lang.Process#getInputStream (); The client is responsible for closing the stream explicitly. 75 */ 76 @Override getInputStream()77 public synchronized InputStream getInputStream() { 78 if (null == in) { 79 in = myPty.getInputStream(); 80 } 81 return in; 82 } 83 84 /** 85 * See java.lang.Process#getOutputStream (); The client is responsible for closing the stream explicitly. 86 */ 87 @Override getOutputStream()88 public synchronized OutputStream getOutputStream() { 89 if (null == out) { 90 out = myPty.getOutputStream(); 91 } 92 return out; 93 } 94 95 /** 96 * See java.lang.Process#getErrorStream (); The client is responsible for closing the stream explicitly. 97 */ 98 @Override getErrorStream()99 public synchronized InputStream getErrorStream() { 100 if (null == err) { 101 if (myErrPty == null || !myPty.isConsole()) { 102 // If Pty is used and it's not in "Console" mode, then stderr is redirected to the Pty's output stream. 103 // Therefore, return a dummy stream for error stream. 104 err = new InputStream() { 105 @Override 106 public int read() { 107 return -1; 108 } 109 }; 110 } 111 else { 112 err = myErrPty.getInputStream(); 113 } 114 } 115 return err; 116 } 117 118 @Override waitFor()119 public synchronized int waitFor() throws InterruptedException { 120 while (!isDone) { 121 wait(); 122 } 123 return myExitCode; 124 } 125 126 /** 127 * See java.lang.Process#exitValue (); 128 */ 129 @Override exitValue()130 public synchronized int exitValue() { 131 if (!isDone) { 132 throw new IllegalThreadStateException("process hasn't exited"); 133 } 134 return myExitCode; 135 } 136 137 /** 138 * See java.lang.Process#destroy (); 139 * <p/> 140 * Clients are responsible for explicitly closing any streams that they have requested through getErrorStream(), 141 * getInputStream() or getOutputStream() 142 */ 143 @Override destroy()144 public synchronized void destroy() { 145 Pty.raise(pid, SIGTERM); 146 closeUnusedStreams(); 147 } 148 149 @Override destroyForcibly()150 public synchronized Process destroyForcibly() { 151 Pty.raise(pid, SIGKILL); 152 closeUnusedStreams(); 153 return this; 154 } 155 156 @Override isRunning()157 public boolean isRunning() { 158 return Pty.raise(pid, NOOP) == 0; 159 } 160 hangup()161 public int hangup() { 162 return Pty.raise(pid, SIGHUP); 163 } 164 execInPty(String[] command, String[] environment, String workingDirectory, Pty pty, Pty errPty, @Nullable Integer initialColumns, @Nullable Integer initialRows)165 private void execInPty(String[] command, String[] environment, String workingDirectory, Pty pty, Pty errPty, 166 @Nullable Integer initialColumns, 167 @Nullable Integer initialRows) throws IOException { 168 String cmd = command[0]; 169 SecurityManager s = System.getSecurityManager(); 170 if (s != null) { 171 s.checkExec(cmd); 172 } 173 if (environment == null) { 174 environment = new String[0]; 175 } 176 final String slaveName = pty.getSlaveName(); 177 final int masterFD = pty.getMasterFD(); 178 final String errSlaveName = errPty == null ? null : errPty.getSlaveName(); 179 final int errMasterFD = errPty == null ? -1 : errPty.getMasterFD(); 180 final boolean console = pty.isConsole(); 181 // int fdm = pty.get 182 Reaper reaper = new Reaper(command, environment, workingDirectory, slaveName, masterFD, errSlaveName, errMasterFD, console); 183 184 reaper.setDaemon(true); 185 reaper.start(); 186 // Wait until the subprocess is started or error. 187 synchronized (this) { 188 while (pid == 0) { 189 try { 190 wait(); 191 } 192 catch (InterruptedException e) { 193 Thread.currentThread().interrupt(); 194 } 195 } 196 197 boolean init = Boolean.getBoolean("unix.pty.init") || initialColumns != null || initialRows != null; 198 if (init) { 199 int cols = initialColumns != null ? initialColumns : Integer.getInteger("unix.pty.cols", 80); 200 int rows = initialRows != null ? initialRows : Integer.getInteger("unix.pty.rows", 25); 201 WinSize size = new WinSize(cols, rows); 202 203 // On OSX, there is a race condition with pty initialization 204 // If we call com.pty4j.unix.Pty.setTerminalSize(com.pty4j.WinSize) too early, we can get ENOTTY 205 for (int attempt = 0; attempt < 1000; attempt++) { 206 try { 207 myPty.setWindowSize(size, this); 208 break; 209 } 210 catch (UnixPtyException e) { 211 if (e.getErrno() != ENOTTY) { 212 break; 213 } 214 } 215 } 216 } 217 } 218 if (pid == -1) { 219 throw new IOException("Exec_tty error:" + reaper.getErrorMessage(), reaper.getException()); 220 } 221 } 222 223 /** 224 * Close the streams on this side. 225 * <p/> 226 * We only close the streams that were 227 * never used by any client. 228 * So, if the stream was not created yet, 229 * we create it ourselves and close it 230 * right away, so as to release the pipe. 231 * Note that even if the stream was never 232 * created, the pipe has been allocated in 233 * native code, so we need to create the 234 * stream and explicitly close it. 235 * <p/> 236 * We don't close streams the clients have 237 * created because we don't know when the 238 * client will be finished using them. 239 * It is up to the client to close those 240 * streams. 241 * <p/> 242 * But 345164 243 */ closeUnusedStreams()244 private synchronized void closeUnusedStreams() { 245 try { 246 if (null == err) { 247 getErrorStream().close(); 248 } 249 } 250 catch (IOException e) { 251 } 252 try { 253 if (null == in) { 254 getInputStream().close(); 255 } 256 } 257 catch (IOException e) { 258 } 259 try { 260 if (null == out) { 261 getOutputStream().close(); 262 } 263 } 264 catch (IOException e) { 265 } 266 } 267 exec(String[] cmd, String[] envp, String dirname, String slaveName, int masterFD, String errSlaveName, int errMasterFD, boolean console)268 int exec(String[] cmd, String[] envp, String dirname, String slaveName, int masterFD, 269 String errSlaveName, int errMasterFD, boolean console) throws IOException { 270 int pid = -1; 271 272 if (cmd == null) { 273 return pid; 274 } 275 276 if (envp == null) { 277 return pid; 278 } 279 280 return PtyHelpers.execPty(cmd[0], cmd, envp, dirname, slaveName, masterFD, errSlaveName, errMasterFD, console); 281 } 282 283 @Override setWinSize(WinSize winSize)284 public void setWinSize(WinSize winSize) { 285 try { 286 myPty.setWindowSize(winSize, this); 287 } 288 catch (UnixPtyException e) { 289 throw new IllegalStateException(e); 290 } 291 if (myErrPty != null) { 292 try { 293 myErrPty.setWindowSize(winSize, this); 294 } 295 catch (UnixPtyException e) { 296 throw new IllegalStateException(e); 297 } 298 } 299 } 300 301 @Override getWinSize()302 public @NotNull WinSize getWinSize() throws IOException { 303 return myPty.getWinSize(this); 304 } 305 306 @Override getPid()307 public int getPid() { 308 return pid; 309 } 310 311 // Spawn a thread to handle the forking and waiting. 312 // We do it this way because on linux the SIGCHLD is send to the one thread. So do the forking and then wait in the 313 // same thread. 314 class Reaper extends Thread { 315 private String[] myCommand; 316 private String[] myEnv; 317 private String myDir; 318 private String mySlaveName; 319 private int myMasterFD; 320 private String myErrSlaveName; 321 private int myErrMasterFD; 322 private boolean myConsole; 323 volatile Throwable myException; 324 Reaper(String[] command, String[] environment, String workingDirectory, String slaveName, int masterFD, String errSlaveName, int errMasterFD, boolean console)325 public Reaper(String[] command, String[] environment, String workingDirectory, String slaveName, int masterFD, String errSlaveName, 326 int errMasterFD, boolean console) { 327 super("PtyProcess Reaper for " + Arrays.toString(command)); 328 myCommand = command; 329 myEnv = environment; 330 myDir = workingDirectory; 331 mySlaveName = slaveName; 332 myMasterFD = masterFD; 333 myErrSlaveName = errSlaveName; 334 myErrMasterFD = errMasterFD; 335 myConsole = console; 336 myException = null; 337 } 338 execute(String[] cmd, String[] env, String dir)339 int execute(String[] cmd, String[] env, String dir) throws IOException { 340 return exec(cmd, env, dir, mySlaveName, myMasterFD, myErrSlaveName, myErrMasterFD, myConsole); 341 } 342 343 @Override run()344 public void run() { 345 try { 346 pid = execute(myCommand, myEnv, myDir); 347 } 348 catch (Exception e) { 349 pid = -1; 350 myException = e; 351 } 352 // Tell spawner that the process started. 353 synchronized (UnixPtyProcess.this) { 354 UnixPtyProcess.this.notifyAll(); 355 } 356 if (pid != -1) { 357 // Sync with spawner and notify when done. 358 myExitCode = PtyHelpers.getPtyExecutor().waitForProcessExitAndGetExitCode(pid); 359 synchronized (UnixPtyProcess.this) { 360 isDone = true; 361 UnixPtyProcess.this.notifyAll(); 362 } 363 myPty.breakRead(); 364 if (myErrPty != null) myErrPty.breakRead(); 365 } 366 } 367 getErrorMessage()368 public String getErrorMessage() { 369 return myException != null ? myException.getMessage() : "Unknown reason"; 370 } 371 372 @Nullable getException()373 public Throwable getException() { 374 return myException; 375 } 376 } 377 } 378