001: /*
002: * GNetWatch
003: * Copyright 2006, 2007 Alexandre Fenyo
004: * gnetwatch@fenyo.net
005: *
006: * This file is part of GNetWatch.
007: *
008: * GNetWatch is free software; you can redistribute it and/or modify
009: * it under the terms of the GNU General Public License as published by
010: * the Free Software Foundation; either version 2 of the License, or
011: * (at your option) any later version.
012: *
013: * GNetWatch is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with GNetWatch; if not, write to the Free Software
020: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
021: */
022:
023: package net.fenyo.gnetwatch.actions;
024:
025: import org.apache.commons.logging.Log;
026: import org.apache.commons.logging.LogFactory;
027:
028: import java.io.*;
029:
030: /**
031: * Instances of this class launch and manage processes outside of the JVM.
032: * @author Alexandre Fenyo
033: * @version $Id: ExternalCommand.java,v 1.14 2007/03/03 00:38:20 fenyo Exp $
034: */
035:
036: public class ExternalCommand {
037: private static Log log = LogFactory.getLog(ExternalCommand.class);
038:
039: private boolean merge = false;
040:
041: final private String[] cmdLine;
042: final private String directory;
043:
044: final private StringBuffer sb = new StringBuffer();
045:
046: private Process process = null;
047: private BufferedReader reader = null;
048: private BufferedReader errReader = null;
049:
050: /**
051: * Creates an ExternalCommand instance and saves the command line.
052: * any thread.
053: * @param cmdLine command line.
054: * @param directory path to the current directory for the script to be launched.
055: */
056: public ExternalCommand(final String[] cmdLine,
057: final String directory) {
058: this .cmdLine = cmdLine;
059: this .directory = directory;
060: }
061:
062: /**
063: * Creates an ExternalCommand instance and saves the command line.
064: * any thread.
065: * @param cmdLine command line.
066: */
067: // any thread
068: public ExternalCommand(final String[] cmdLine) {
069: this .cmdLine = cmdLine;
070: this .directory = System.getProperty("java.io.tmpdir");
071: }
072:
073: /**
074: * Creates an ExternalCommand instance and saves the command line.
075: * any thread.
076: * @param cmdLine command line.
077: * @param merge merge standard output and standard error.
078: */
079: // any thread
080: public ExternalCommand(final String[] cmdLine, final boolean merge) {
081: this .merge = merge;
082: this .cmdLine = cmdLine;
083: this .directory = System.getProperty("java.io.tmpdir");
084: }
085:
086: /**
087: * Reads a line from the process output.
088: * @param r reader.
089: * @return line read.
090: * @throws IOException IO exception.
091: * @throws InterruptedException exception.
092: */
093: // data read is lost when interrupted
094: // returns null if EOF
095: // major feature: it never blocks the current thread while reading a stream
096: // On peut améliorer les perfs en gardant dans sb ce qui est lu et donc en lisant plusieurs caractères à la fois
097: // et en ne retournant que jusqu'au retour chariot.
098: // this private method must be called from synchronized methods
099: // any thread
100: private String readLine(Reader r) throws IOException,
101: InterruptedException {
102: sb.setLength(0);
103: while (!Thread.currentThread().isInterrupted()) {
104: if (r.ready()) {
105: final int ret = r.read();
106: if (ret == -1)
107: return sb.length() != 0 ? sb.toString() : null;
108: if (ret == '\n')
109: return sb.toString();
110: sb.append((char) ret);
111: } else {
112: try {
113: process.exitValue();
114: return sb.length() != 0 ? sb.toString() : null;
115: } catch (final IllegalThreadStateException ex) {
116: }
117: Thread.sleep(100);
118: }
119: }
120: log.info("readLine(): was interrupted");
121: throw new InterruptedException("readLine()");
122: }
123:
124: /**
125: * Reads the whole output.
126: * @return whole output.
127: * @throws InterruptedException exception.
128: */
129: // return null if IOException (EOF for instance)
130: // any thread
131: public synchronized String runStdoutStderr()
132: throws InterruptedException {
133: String retval = null;
134: try {
135: fork();
136: retval = readStdoutStderr();
137: } catch (final IOException ex) {
138: }
139: try {
140: end();
141: } catch (final IOException ex) {
142: }
143: return retval;
144: }
145:
146: /**
147: * Reads the whole standard output.
148: * @return whole standard output.
149: * @throws InterruptedException interrupted.
150: */
151: // return null if IOException (EOF for instance)
152: // any thread
153: public synchronized String runStdout() throws InterruptedException {
154: String retval = null;
155: try {
156: fork();
157: retval = readStdout();
158: } catch (final IOException ex) {
159: }
160: try {
161: end();
162: } catch (final IOException ex) {
163: }
164: return retval;
165: }
166:
167: /**
168: * Displays command line arguments.
169: * any thread.
170: * @param none.
171: * @return void.
172: */
173: public synchronized void logArgs() {
174: for (int x = 0; x < cmdLine.length; x++)
175: log.debug("arg " + x + ": " + cmdLine[x]);
176: }
177:
178: /**
179: * Launches a process but do not wait for its completion.
180: * any thread.
181: * @param none.
182: * @return void.
183: * @throws IOException i/o exception <bold>before</bold> reading the process output stream.
184: */
185: public synchronized void fork() throws IOException {
186: final ProcessBuilder pb = new ProcessBuilder(cmdLine);
187: pb.directory(new File(directory));
188: pb.redirectErrorStream(merge);
189: process = pb.start();
190:
191: if (process == null)
192: throw new IOException("null process");
193:
194: reader = new BufferedReader(new InputStreamReader(process
195: .getInputStream()));
196: errReader = new BufferedReader(new InputStreamReader(process
197: .getErrorStream()));
198: }
199:
200: /**
201: * Merges stdout and stderr.
202: * Waits for the end of the process output stream.
203: * Note that this method can return before the process completion.
204: * any thread.
205: * @param none.
206: * @return void.
207: * @throws IOException i/o exception <bold>while</bold> reading the process output stream.
208: */
209: public synchronized String readStdoutStderr() throws IOException,
210: InterruptedException {
211: StringBuffer output = new StringBuffer();
212: String str;
213: while ((str = readLine(reader)) != null) {
214: output.append(str);
215: output.append("\n");
216: }
217:
218: while ((str = readLine(errReader)) != null) {
219: output.append(str);
220: output.append("\n");
221: }
222:
223: // We can not return a StringBuffer since it could be modified by readOutput() running
224: // in another thread, even if we synchronize each method of ExternalCommand.
225: return output.toString();
226: }
227:
228: /**
229: * Reads stdout.
230: * Waits for the end of the process output stream.
231: * Note that this method can return before the process completion.
232: * any thread.
233: * @param none.
234: * @return void.
235: * @throws IOException i/o exception <bold>while</bold> reading the process output stream.
236: */
237: public synchronized String readStdout() throws IOException,
238: InterruptedException {
239: StringBuffer output = new StringBuffer();
240: String str;
241:
242: while ((str = readLine(reader)) != null) {
243: output.append(str);
244: output.append("\n");
245: }
246:
247: // We can not return a StringBuffer since it could be modified by readOutput() running
248: // in another thread, even if we synchronize each method of ExternalCommand.
249: return output.toString();
250: }
251:
252: /**
253: * Reads stderr.
254: * Waits for the closure of the process output stream.
255: * Note that this method can return before the process completion.
256: * any thread.
257: * @param none.
258: * @return void.
259: * @throws IOException i/o exception <bold>while</bold> reading the process output stream.
260: */
261: public synchronized String readStderr() throws IOException,
262: InterruptedException {
263: StringBuffer output = new StringBuffer();
264: String str;
265:
266: while ((str = readLine(errReader)) != null) {
267: output.append(str);
268: output.append("\n");
269: }
270:
271: // We can not return a StringBuffer since it could be modified by readOutput() running
272: // in another thread, even if we synchronize each method of ExternalCommand.
273: return output.toString();
274: }
275:
276: /**
277: * Reads one line of the stdout.
278: * any thread.
279: * @param none.
280: * @return void.
281: * @throws IOException i/o exception <bold>while</bold> reading the process output stream.
282: */
283: public synchronized String readLineStdout() throws IOException,
284: InterruptedException {
285: return readLine(reader);
286: }
287:
288: /**
289: * Reads one line of the stderr.
290: * any thread.
291: * @param none.
292: * @return void.
293: * @throws IOException i/o exception <bold>while</bold> reading the process output stream.
294: */
295: public synchronized String readLineStderr() throws IOException,
296: InterruptedException {
297: return readLine(errReader);
298: }
299:
300: /**
301: * Make sure the underlying file descriptors are closed, to avoid maintening
302: * unused resources in an application server JVM for instance.
303: * any thread.
304: * @param none.
305: * @return void.
306: * @throws IOException error while closing streams.
307: */
308: public void end() throws IOException {
309: // may be closing the readers is sufficient to get these streams closed
310: // we want the underlying file descriptors being close to avoid maintening
311: // unused resources in an application server JVM, so we explicitely close
312: // those streams.
313: if (process != null) {
314: kill();
315: synchronized (this ) {
316: if (process.getInputStream() != null)
317: process.getInputStream().close();
318: if (process.getOutputStream() != null)
319: process.getOutputStream().close();
320: if (process.getErrorStream() != null)
321: process.getErrorStream().close();
322: }
323: }
324: }
325:
326: /**
327: * Kills the process.
328: * We do not reset process to null since it can be used to get exit code.
329: * any thread.
330: * @param none.
331: * @return void.
332: */
333: private void kill() {
334: if (process != null) {
335: boolean terminated = false;
336: process.destroy();
337: while (terminated == false)
338: try {
339: process.waitFor();
340: terminated = true;
341: } catch (final InterruptedException ex) {
342: log.warn("Exception", ex);
343: }
344: }
345: }
346: }
|