View Javadoc

1   package org.apache.helix;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.UnsupportedEncodingException;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.concurrent.TimeoutException;
33  
34  import org.apache.log4j.Logger;
35  
36  public class ExternalCommand
37  {
38    public static final String MODULE = ExternalCommand.class.getName();
39    public static final Logger LOG = Logger.getLogger(MODULE);
40  
41    private final ProcessBuilder _processBuilder;
42  
43    private Process _process;
44    private InputReader _out;
45    private InputReader _err;
46  
47    private static class InputReader extends Thread
48    {
49      private static final int BUFFER_SIZE = 2048;
50  
51      private final InputStream _in;
52      private final ByteArrayOutputStream _out;
53      private boolean _running = false;
54  
55      InputReader(InputStream in)
56      {
57        _in = in;
58        _out = new ByteArrayOutputStream();
59      }
60  
61      @Override
62      public void run()
63      {
64        _running = true;
65  
66        byte[] buf = new byte[BUFFER_SIZE];
67        int n = 0;
68        try
69        {
70          while((n = _in.read(buf)) != -1)
71            _out.write(buf, 0, n);
72        }
73        catch(IOException e)
74        {
75          LOG.error("error while reading external command", e);
76        }
77  
78        _running = false;
79      }
80  
81      public byte[] getOutput()
82      {
83        if(_running)
84          throw new IllegalStateException("wait for process to be completed");
85  
86        return _out.toByteArray();
87      }
88    }
89    /**
90  * Constructor */
91    public ExternalCommand(ProcessBuilder processBuilder)
92    {
93      _processBuilder = processBuilder;
94    }
95  
96    /**
97  * After creating the command, you have to start it...
98  *
99  * @throws IOException
100 */
101   public void start() throws IOException
102   {
103     _process = _processBuilder.start();
104     _out = new InputReader(new BufferedInputStream(_process.getInputStream()));
105     _err = new InputReader(new BufferedInputStream(_process.getErrorStream()));
106 
107     _out.start();
108     _err.start();
109   }
110 
111   /**
112 * @see ProcessBuilder
113 */
114   public Map<String, String> getEnvironment()
115   {
116     return _processBuilder.environment();
117   }
118 
119   /**
120 * @see ProcessBuilder
121 */
122   public File getWorkingDirectory()
123   {
124     return _processBuilder.directory();
125   }
126 
127   /**
128 * @see ProcessBuilder
129 */
130   public void setWorkingDirectory(File directory)
131   {
132     _processBuilder.directory(directory);
133   }
134 
135   /**
136 * @see ProcessBuilder
137 */
138   public boolean getRedirectErrorStream()
139   {
140     return _processBuilder.redirectErrorStream();
141   }
142 
143   /**
144 * @see ProcessBuilder
145 */
146   public void setRedirectErrorStream(boolean redirectErrorStream)
147   {
148     _processBuilder.redirectErrorStream(redirectErrorStream);
149   }
150 
151   public byte[] getOutput() throws InterruptedException
152   {
153     waitFor();
154     return _out.getOutput();
155   }
156 
157   public byte[] getError() throws InterruptedException
158   {
159     waitFor();
160     return _err.getOutput();
161   }
162 
163   /**
164 * Returns the output as a string.
165 *
166 * @param encoding
167 * @return encoded string
168 * @throws InterruptedException
169 * @throws UnsupportedEncodingException
170 */
171   public String getStringOutput(String encoding) throws InterruptedException,
172                                                         UnsupportedEncodingException
173   {
174     return new String(getOutput(), encoding);
175   }
176 
177   /**
178 * Returns the output as a string. Uses encoding "UTF-8".
179 *
180 * @return utf8 encoded string
181 * @throws InterruptedException
182 */
183   public String getStringOutput() throws InterruptedException
184   {
185     try
186     {
187       return getStringOutput("UTF-8");
188     }
189     catch(UnsupportedEncodingException e)
190     {
191       // should not happen
192       throw new RuntimeException(e);
193     }
194   }
195 
196   /**
197 * Returns the error as a string.
198 *
199 * @param encoding
200 * @return error as string
201 * @throws InterruptedException
202 * @throws UnsupportedEncodingException
203 */
204   public String getStringError(String encoding) throws InterruptedException,
205                                                        UnsupportedEncodingException
206   {
207     return new String(getError(), encoding);
208   }
209 
210   /**
211 * Returns the error as a string. Uses encoding "UTF-8".
212 *
213 * @return error as string
214 * @throws InterruptedException
215 */
216   public String getStringError() throws InterruptedException
217   {
218     try
219     {
220       return getStringError("UTF-8");
221     }
222     catch(UnsupportedEncodingException e)
223     {
224       // should not happen
225       throw new RuntimeException(e);
226     }
227   }
228 
229   /**
230 * Properly waits until everything is complete: joins on the thread that
231 * reads the output, joins on the thread that reads the error and finally
232 * wait for the process to be finished.
233 * @return the status code of the process.
234 *
235 * @throws InterruptedException
236 */
237   public int waitFor() throws InterruptedException
238   {
239     if(_process == null)
240       throw new IllegalStateException("you must call start first");
241 
242     _out.join();
243     _err.join();
244     return _process.waitFor();
245   }
246 
247   /**
248 * Properly waits until everything is complete: joins on the thread that
249 * reads the output, joins on the thread that reads the error and finally
250 * wait for the process to be finished.
251 * If the process has not completed before the timeout, throws a
252 * {@link TimeoutException}
253 * @return the status code of the process.
254 *
255 * @throws TimeoutException
256 * @throws InterruptedException
257 */
258   public int waitFor(long timeout) throws InterruptedException, TimeoutException
259   {
260     if(_process == null)
261       throw new IllegalStateException("you must call start first");
262 
263 //    Chronos c = new Chronos();
264     _out.join(timeout);
265 //    timeout -= c.tick();
266     if (timeout <= 0)
267       throw new TimeoutException("Wait timed out");
268     _err.join(timeout);
269 //    timeout -= c.tick();
270     if (timeout <= 0)
271       throw new TimeoutException("Wait timed out");
272 
273     // there is no timeout in this API, not much we can do here
274     // waiting on the other two threads should give us some safety
275     return _process.waitFor();
276   }
277 
278   public int exitValue()
279   {
280     if(_process == null)
281       throw new IllegalStateException("you must call start first");
282 
283     return _process.exitValue();
284   }
285 
286   public void destroy()
287   {
288     if(_process == null)
289       throw new IllegalStateException("you must call start first");
290 
291     _process.destroy();
292   }
293 
294   /**
295 * Creates an external process from the command. It is not started and you have to call
296 * start on it!
297 *
298 * @param commands the command to execute
299 * @return the process */
300   public static ExternalCommand create(String... commands)
301   {
302     ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
303     return ec;
304   }
305 
306   /**
307 * Creates an external process from the command. It is not started and you have to call
308 * start on it!
309 *
310 * @param commands the command to execute
311 * @return the process */
312   public static ExternalCommand create(List<String> commands)
313   {
314     ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
315     return ec;
316   }
317 
318   /**
319 * Creates an external process from the command. The command is executed.
320 *
321 * @param commands the commands to execute
322 * @return the process
323 * @throws IOException if there is an error */
324   public static ExternalCommand start(String... commands) throws IOException
325   {
326     ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
327     ec.start();
328     return ec;
329   }
330 
331   /**
332 * Executes the external command in the given working directory and waits for it to be
333 * finished.
334 *
335 * @param workingDirectory the root directory from where to run the command
336 * @param command the command to execute (should be relative to the working directory
337 * @param args the arguments to the command
338 * @return the process */
339   public static ExternalCommand execute(File workingDirectory,
340                                         String command,
341                                         String... args)
342       throws IOException, InterruptedException
343   {
344     try
345     {
346       return executeWithTimeout(workingDirectory, command, 0, args);
347     }
348     catch (TimeoutException e)
349     {
350       // Can't happen!
351       throw new IllegalStateException(MODULE + ".execute: Unexpected timeout occurred!");
352     }
353   }
354 
355 /**
356 * Executes the external command in the given working directory and waits (until timeout
357 * is elapsed) for it to be finished.
358 *
359 * @param workingDirectory
360 * the root directory from where to run the command
361 * @param command
362 * the command to execute (should be relative to the working directory
363 * @param timeout
364 * the maximum amount of time to wait for this external command (in ms). If
365 * this value is less than or equal to 0, timeout is ignored
366 * @param args
367 * the arguments to the command
368 * @return the process
369 */
370   public static ExternalCommand executeWithTimeout(File workingDirectory,
371                                                    String command,
372                                                    long timeout,
373                                                    String... args)
374       throws IOException, InterruptedException, TimeoutException
375   {
376     List<String> arguments = new ArrayList<String>(args.length + 1);
377 
378     arguments.add(new File(workingDirectory, command).getAbsolutePath());
379     arguments.addAll(Arrays.asList(args));
380 
381     ExternalCommand cmd = ExternalCommand.create(arguments);
382 
383     cmd.setWorkingDirectory(workingDirectory);
384 
385     cmd.setRedirectErrorStream(true);
386 
387     cmd.start();
388 
389     /* Use timeout if it is a valid value! */
390     if (timeout <= 0)
391       cmd.waitFor();
392     else
393       cmd.waitFor(timeout);
394 
395     if (LOG.isDebugEnabled())
396       LOG.debug(cmd.getStringOutput());
397 
398     return cmd;
399   }
400 }