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
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
248
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
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
315 continue;
316 }
317
318 buffer.rewind();
319 if (readMe.read(buffer) == -1) {
320
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
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 }