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