View Javadoc

1   package expectj;
2   
3   
4   import java.io.BufferedWriter;
5   import java.io.IOException;
6   import java.io.OutputStreamWriter;
7   import java.nio.ByteBuffer;
8   import java.nio.channels.Channels;
9   import java.nio.channels.Pipe;
10  import java.nio.channels.SelectionKey;
11  import java.nio.channels.Selector;
12  import java.util.Date;
13  
14  import org.apache.commons.logging.Log;
15  import org.apache.commons.logging.LogFactory;
16  
17  /***
18   * This class is used for talking to processes / ports. This will also interact
19   * with the process to read and write to it.
20   *
21   * @author	Sachin Shekar Shetty
22   */
23  public class Spawn {
24      /***
25       * Log messages go here.
26       */
27      private final static Log LOG = LogFactory.getLog(ProcessSpawn.class);
28  
29      /*** Default time out for expect commands */
30      private long m_lDefaultTimeOutSeconds = -1;
31  
32      /***
33       * Buffered wrapper stream for slave's stdin.
34       */
35      private BufferedWriter toStdin = null;
36  
37      /***
38       * This is what we're actually talking to.
39       */
40      private SpawnableHelper slave = null;
41  
42      /***
43       * Turns false on timeout.
44       */
45      private volatile boolean continueReading = true;
46  
47      /***
48       * Pumps data from stdin to the spawn's stdin.
49       */
50      private StreamPiper interactIn = null;
51  
52      /***
53       * Pumps data from the spawn's stdout to stdout.
54       */
55      private StreamPiper interactOut = null;
56  
57      /***
58       * Pumps data from the spawn's stderr to stderr.
59       */
60      private StreamPiper interactErr = null;
61  
62      /***
63       * Wait for data from spawn's stdout.
64       */
65      private Selector stdoutSelector;
66  
67      /***
68       * Wait for data from spawn's stderr.
69       */
70      private Selector stderrSelector;
71  
72      /***
73       * This object will be notified on timer timeout or when the spawn we're
74       * waiting for closes.
75       */
76      private final Object doneWaitingForClose = new Object();
77  
78      /***
79       * Constructor
80       *
81       * @param spawn This is what we'll control.
82       * @param lDefaultTimeOutSeconds Default timeout for expect commands
83       * @throws IOException on trouble launching the spawn
84       */
85      Spawn(Spawnable spawn, long lDefaultTimeOutSeconds) throws IOException {
86          if (lDefaultTimeOutSeconds < -1) {
87              throw new IllegalArgumentException("Timeout must be >= -1, was "
88                                                 + lDefaultTimeOutSeconds);
89          }
90          m_lDefaultTimeOutSeconds = lDefaultTimeOutSeconds;
91  
92          slave = new SpawnableHelper(spawn, lDefaultTimeOutSeconds);
93          slave.start();
94          LOG.debug("Spawned Process: " + spawn);
95  
96          if (slave.getStdin() != null) {
97              toStdin =
98                  new BufferedWriter(new OutputStreamWriter(slave.getStdin()));
99          }
100 
101         stdoutSelector = Selector.open();
102         slave.getStdoutChannel().register(stdoutSelector, SelectionKey.OP_READ);
103         if (slave.getStderrChannel() != null) {
104             stderrSelector = Selector.open();
105             slave.getStderrChannel().register(stderrSelector, SelectionKey.OP_READ);
106         }
107     }
108 
109     /***
110      * This method is invoked by our {@link Timer} when the time-out occurs.
111      */
112     private synchronized void timerTimedOut() {
113         continueReading = false;
114         stdoutSelector.wakeup();
115         if (stderrSelector != null) {
116             stderrSelector.wakeup();
117         }
118         synchronized (doneWaitingForClose) {
119             doneWaitingForClose.notify();
120         }
121     }
122 
123     /***
124      * This method is invoked by our {@link Timer} when the timer thread
125      * receives an interrupted exception
126      * @param reason The reason for the interrupt
127      */
128     private void timerInterrupted(InterruptedException reason) {
129         timerTimedOut();
130     }
131 
132     /***
133      * Wait for a pattern to appear on standard out.
134      * @param pattern The case-insensitive substring to match against.
135      * @param timeOutSeconds The timeout in seconds before the match fails.
136      * @throws IOException on IO trouble waiting for pattern
137      * @throws TimeoutException on timeout waiting for pattern
138      */
139     public void expect(String pattern, long timeOutSeconds)
140     throws IOException, TimeoutException
141     {
142         expect(pattern, timeOutSeconds, stdoutSelector);
143     }
144 
145     /***
146      * Wait for the spawned process to finish.
147      * @param timeOutSeconds The number of seconds to wait before giving up, or
148      * -1 to wait forever.
149      * @throws ExpectJException if we're interrupted while waiting for the spawn
150      * to finish.
151      * @throws TimeoutException if the spawn didn't finish inside of the
152      * timeout.
153      * @see #expectClose()
154      */
155     public void expectClose(long timeOutSeconds)
156     throws TimeoutException, ExpectJException
157     {
158         if (timeOutSeconds < -1) {
159             throw new IllegalArgumentException("Timeout must be >= -1, was "
160                                                + timeOutSeconds);
161         }
162 
163         LOG.debug("Waiting for spawn to close connection...");
164         Timer tm = null;
165         slave.setCloseListener(new Spawnable.CloseListener() {
166             public void onClose() {
167                 synchronized (doneWaitingForClose) {
168                     doneWaitingForClose.notify();
169                 }
170             }
171         });
172         if (timeOutSeconds != -1 ) {
173             tm = new Timer(timeOutSeconds, new TimerEventListener() {
174                 public void timerTimedOut() {
175                     Spawn.this.timerTimedOut();
176                 }
177 
178                 public void timerInterrupted(InterruptedException reason) {
179                     Spawn.this.timerInterrupted(reason);
180                 }
181             });
182             tm.startTimer();
183         }
184         continueReading = true;
185         boolean closed = false;
186         synchronized (doneWaitingForClose) {
187             while(continueReading) {
188                 // Sleep if process is still running
189                 if (slave.isClosed()) {
190                     closed = true;
191                     break;
192                 } else {
193                     try {
194                         doneWaitingForClose.wait(500);
195                     } catch (InterruptedException e) {
196                         throw new ExpectJException("Interrupted waiting for spawn to finish",
197                                                    e);
198                     }
199                 }
200             }
201         }
202         if (tm != null) {
203             tm.close();
204         }
205         if (closed) {
206             LOG.debug("Connection to spawn closed, continueReading="
207                       + continueReading);
208         } else {
209             LOG.debug("Timed out waiting for spawn to close, continueReading="
210                       + continueReading);
211         }
212         if (tm != null) {
213             LOG.debug("Timer Status:" + tm.getStatus());
214         }
215         if (!continueReading) {
216             throw new TimeoutException("Timeout waiting for spawn to finish");
217         }
218 
219         freeResources();
220     }
221 
222     /***
223      * Free up system resources.
224      */
225     private void freeResources() {
226         try {
227             slave.close();
228             if (interactIn != null) {
229                 interactIn.stopProcessing();
230             }
231             if (interactOut != null) {
232                 interactOut.stopProcessing();
233             }
234             if (interactErr != null) {
235                 interactErr.stopProcessing();
236             }
237             if (stderrSelector != null) {
238                 stderrSelector.close();
239             }
240             if (stdoutSelector != null) {
241                 stdoutSelector.close();
242             }
243             if (toStdin != null) {
244                 toStdin.close();
245             }
246         } catch (IOException e) {
247             // Cleaning up is a best effort operation, failures are
248             // logged but otherwise accepted.
249             LOG.warn("Failed cleaning up after spawn done", e);
250         }
251     }
252 
253     /***
254      * Wait the default timeout for the spawned process to finish.
255      * @throws ExpectJException If something fails.
256      * @throws TimeoutException if the spawn didn't finish inside of the default
257      * timeout.
258      * @see #expectClose(long)
259      * @see ExpectJ#ExpectJ(long)
260      */
261     public void expectClose()
262     throws ExpectJException, TimeoutException
263     {
264         expectClose(m_lDefaultTimeOutSeconds);
265     }
266 
267     /***
268      * Workhorse of the expect() and expectErr() methods.
269      * @see #expect(String, long)
270      * @param pattern What to look for
271      * @param lTimeOutSeconds How long to look before giving up
272      * @param selector A selector covering only the channel we should read from
273      * @throws IOException on IO trouble waiting for pattern
274      * @throws TimeoutException on timeout waiting for pattern
275      */
276     private void expect(String pattern, long lTimeOutSeconds, Selector selector)
277     throws IOException, TimeoutException
278     {
279         if (lTimeOutSeconds < -1) {
280             throw new IllegalArgumentException("Timeout must be >= -1, was "
281                                                + lTimeOutSeconds);
282         }
283 
284         if (selector.keys().size() != 1) {
285             throw new IllegalArgumentException("Selector key set size must be 1, was "
286                                                + selector.keys().size());
287         }
288         // If this cast fails somebody gave us the wrong selector.
289         Pipe.SourceChannel readMe =
290             (Pipe.SourceChannel)((SelectionKey)(selector.keys().iterator().next())).channel();
291 
292         LOG.debug("Expecting '" + pattern + "'");
293         continueReading = true;
294         boolean found = false;
295         StringBuilder line = new StringBuilder();
296         Date runUntil = null;
297         if (lTimeOutSeconds > 0) {
298             runUntil = new Date(new Date().getTime() + lTimeOutSeconds * 1000);
299         }
300         ByteBuffer buffer = ByteBuffer.allocate(1024);
301         while(continueReading) {
302             if (runUntil == null) {
303                 selector.select();
304             } else {
305                 long msLeft = runUntil.getTime() - new Date().getTime();
306                 if (msLeft > 0) {
307                     selector.select(msLeft);
308                 } else {
309                     continueReading = false;
310                     break;
311                 }
312             }
313             if (selector.selectedKeys().size() == 0) {
314                 // Woke up with nothing selected, try again
315                 continue;
316             }
317 
318             buffer.rewind();
319             if (readMe.read(buffer) == -1) {
320                 // End of stream
321                 throw new IOException("End of stream reached, no match found");
322             }
323             buffer.rewind();
324             for (int i = 0; i < buffer.limit(); i++) {
325                 line.append((char)buffer.get(i));
326             }
327             if (line.toString().trim().toUpperCase().indexOf(pattern.toUpperCase()) != -1) {
328                 LOG.debug("Found match for " + pattern + ":" + line);
329                 found = true;
330                 break;
331             }
332             while (line.indexOf("\n") != -1) {
333                 line.delete(0, line.indexOf("\n") + 1);
334             }
335         }
336         if (found) {
337             LOG.debug("Match found, continueReading=" + continueReading);
338         } else {
339             LOG.debug("Timed out waiting for match, continueReading="
340                       + continueReading);
341         }
342         if (!continueReading) {
343             throw new TimeoutException("Timeout trying to match \"" + pattern + "\"");
344         }
345     }
346 
347     /***
348      * Wait for a pattern to appear on standard error.
349      * @see #expect(String, long)
350      * @param pattern The case-insensitive substring to match against.
351      * @param timeOutSeconds The timeout in seconds before the match fails.
352      * @throws TimeoutException on timeout waiting for pattern
353      * @throws IOException on IO trouble waiting for pattern
354      */
355     public void expectErr(String pattern, long timeOutSeconds)
356     throws IOException, TimeoutException
357     {
358         expect(pattern, timeOutSeconds, stderrSelector);
359     }
360 
361     /***
362      * Wait for a pattern to appear on standard out.
363      * @param pattern The case-insensitive substring to match against.
364      * @throws TimeoutException on timeout waiting for pattern
365      * @throws IOException on IO trouble waiting for pattern
366      */
367     public void expect(String pattern)
368     throws IOException, TimeoutException
369     {
370         expect(pattern, m_lDefaultTimeOutSeconds);
371     }
372 
373     /***
374      * Wait for a pattern to appear on standard error.
375      * @param pattern The case-insensitive substring to match against.
376      * @throws TimeoutException on timeout waiting for pattern
377      * @throws IOException on IO trouble waiting for pattern
378      * @see #expect(String)
379      */
380     public void expectErr(String pattern)
381     throws IOException, TimeoutException
382     {
383         expectErr(pattern, m_lDefaultTimeOutSeconds);
384     }
385 
386     /***
387      * This method can be use use to check the target process status
388      * before invoking {@link #send(String)}
389      * @return true if the process has already exited.
390      */
391     public boolean isClosed() {
392         return slave.isClosed();
393     }
394 
395     /***
396      * Retrieve the exit code of a finished process.
397      * @return the exit code of the process if the process has
398      * already exited.
399      * @throws ExpectJException if the spawn is still running.
400      */
401     public int getExitValue()
402     throws ExpectJException
403     {
404         return slave.getExitValue();
405     }
406 
407     /***
408      * Writes a string to the standard input of the spawned process.
409      *
410      * @param string The string to send.  Don't forget to terminate it with \n
411      * if you want it linefed.
412      * @throws IOException on IO trouble talking to spawn
413      */
414     public void send(String string)
415     throws IOException {
416         LOG.debug("Sending '" + string + "'");
417         toStdin.write(string);
418         toStdin.flush();
419     }
420 
421     /***
422      * Allows the user to interact with the spawned process.
423      */
424     public void interact() {
425         // FIXME: User input is echoed twice on the screen
426         interactIn = new StreamPiper(null,
427                                      System.in, slave.getStdin());
428         interactIn.start();
429         interactOut = new StreamPiper(null,
430                                       Channels.newInputStream(slave.getStdoutChannel()),
431                                       System.out);
432         interactOut.start();
433         interactErr = new StreamPiper(null,
434                                       Channels.newInputStream(slave.getStderrChannel()),
435                                       System.err);
436         interactErr.start();
437         slave.stopPipingToStandardOut();
438     }
439 
440     /***
441      * This method kills the process represented by SpawnedProcess object.
442      */
443     public void stop() {
444         slave.stop();
445 
446         freeResources();
447     }
448 
449     /***
450      * Returns everything that has been received on the spawn's stdout during
451      * this session.
452      *
453      * @return the available contents of Standard Out
454      */
455     public String getCurrentStandardOutContents() {
456         return slave.getCurrentStandardOutContents();
457     }
458 
459     /***
460      * Returns everything that has been received on the spawn's stderr during
461      * this session.
462      *
463      * @return the available contents of Standard Err
464      */
465     public String getCurrentStandardErrContents() {
466         return slave.getCurrentStandardErrContents();
467     }
468 }