View Javadoc

1   /*
2   Copyright (C) 2000 - 2007 Grid Systems, S.A.
3   
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License, version 2, as
6   published by the Free Software Foundation.
7   
8   This program is distributed in the hope that it will be useful,
9   but WITHOUT ANY WARRANTY; without even the implied warranty of
10  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  GNU General Public License for more details.
12  
13  You should have received a copy of the GNU General Public License
14  along with this program; if not, write to the Free Software
15  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16  */
17  package com.gridsystems.utils;
18  
19  import java.io.File;
20  import java.io.FileOutputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.util.Enumeration;
25  import java.util.Formatter;
26  import java.util.HashSet;
27  import java.util.Map;
28  import java.util.Properties;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  
33  /**
34   * Cross-Platform command execution manager.
35   * <p>
36   * This class provides a configurable mechanism to invoke native commands
37   * in a cross-platform way.
38   *
39   * @author Rodrigo Ruiz
40   */
41  public final class NativeShell {
42  
43    /**
44     * Class logger.
45     */
46    private static final Log LOG = LogFactory.getLog(NativeShell.class);
47  
48    /**
49     * Buffer size.
50     */
51    private static final int BUF_SIZE = 4096;
52  
53    /**
54     * Command Alias map.
55     */
56    private static final FileProperties ALIAS;
57  
58    static {
59      Properties defaults = new Properties();
60      InputStream is = null;
61      try {
62        ClassLoader cl = Thread.currentThread().getContextClassLoader();
63        is = cl.getResourceAsStream("native-commands.properties");
64        defaults.load(is);
65      } catch (Exception e) {
66        LOG.error("Could not load native-commands defaults", e);
67      } finally {
68        FileUtils.close(is);
69      }
70  
71      File f = new File("native-commands.properties");
72      ALIAS = new FileProperties(f, defaults);
73    }
74  
75    /**
76     * Sets the command alias configuration file.
77     *
78     * @param f The file
79     */
80    public static void setConfigFile(File f) {
81      ALIAS.setFile(f);
82    }
83  
84    /**
85     * Environment.
86     */
87    private ProcessBuilder builder = new ProcessBuilder();
88  
89    /**
90     * Standard Output redirection.
91     */
92    private OutputStream stdOut;
93  
94    /**
95     * Standard Error redirection.
96     */
97    private OutputStream stdErr;
98  
99    /**
100    * Close StdOut on end flag.
101    */
102   private boolean closeStdOut;
103 
104   /**
105    * Close StdErr on end flag.
106    */
107   private boolean closeStdErr;
108 
109   /**
110    * Creates an instance.
111    */
112   public NativeShell() {
113   }
114 
115   /**
116    * Gets a platform dependent alias for the specified command.
117    *
118    * @param cmd The command to search for
119    * @param args Command parameters
120    * @return The found alias
121    */
122   public static String alias(String cmd, Object... args) {
123     String osName = SystemUtils.getOsName();
124     return doAlias(osName, cmd, args);
125   }
126 
127   /**
128    * Gets a platform dependent alias for the specified command.
129    *
130    * @param cmd    The command to search for
131    * @param osName The platform for which the command is expected
132    * @param args Command parameters
133    * @return The found alias
134    */
135   protected static String doAlias(String osName, String cmd, Object... args) {
136     String osPrefix = SystemUtils.getSimpleOsName(osName);
137     String osVersion = System.getProperty("os.version");
138 
139     String alias = ALIAS.getProperty(cmd + "_" + osPrefix + "_" + osVersion);
140     if (alias == null) {
141       alias = ALIAS.getProperty(cmd + "_" + osPrefix);
142       if (alias == null) {
143         alias = ALIAS.getProperty(cmd, "#null");
144       }
145     }
146 
147     if ("#null".equals(alias)) {
148       return null;
149     } else if (args.length == 0) {
150       return alias;
151     } else {
152       Formatter f = new Formatter();
153       return f.format(alias, args).toString();
154     }
155   }
156 
157   /**
158    * Gets a list of all commands this instance is aware of. The list includes
159    * commands that may be unsupported for the current platform. To get only
160    * those commands available in the local platform, use the method
161    * {@link #getAvailableCommands()} instead.
162    *
163    * @return A list of known command names
164    */
165   public static String[] getAllCommands() {
166     HashSet<String> commands = new HashSet<String>();
167     for (Enumeration< ? > names = ALIAS.propertyNames(); names.hasMoreElements();) {
168       String name = names.nextElement().toString();
169       int pos = name.indexOf('_');
170       String cmd = (pos == -1) ? name : name.substring(0, pos);
171       commands.add(cmd);
172     }
173 
174     return commands.toArray(new String[commands.size()]);
175   }
176 
177   /**
178    * Gets a list of all commands available in this platform.
179    *
180    * @return A list of available command names
181    */
182   public static String[] getAvailableCommands() {
183     String[] all = getAllCommands();
184     HashSet<String> available = new HashSet<String>();
185     for (String cmd : all) {
186       String alias = alias(cmd);
187       if (alias != null) {
188         available.add(cmd);
189       }
190     }
191 
192     return available.toArray(new String[available.size()]);
193   }
194 
195   /**
196    * Gets an array to be used for executing the given command in a
197    * platform dependent shell.
198    * <p>
199    * The command is converted in a platform dependent command by calling
200    * {@link #alias(String, Object...)}.
201    *
202    * @param cmd  The command to execute
203    * @param args The command arguments
204    * @return An array that can be used in an exec() call
205    */
206   public static String[] shell(String cmd, Object... args) {
207     String osName = SystemUtils.getOsName();
208     return doShell(osName, cmd, args);
209   }
210 
211   /**
212    * Gets an array to be used for executing the given command in a
213    * platform dependent shell.
214    * <p>
215    * The command is converted in a platform dependent command by calling
216    * {@link #alias(String, Object...)}.
217    *
218    * @param osName The OS name
219    * @param cmd    The command to execute
220    * @param args   The command arguments
221    * @return An array that can be used in an exec() call
222    */
223   protected static String[] doShell(String osName, String cmd, Object... args) {
224     String alias = alias(osName, cmd, args);
225     String shell = alias(osName, "shell");
226     String shopts = alias(osName, "shopts");
227 
228     if (alias == null || shell == null) {
229       return null;
230     } else if (shopts == null) {
231       return new String[] { shell, alias };
232     } else {
233       return new String[] { shell, shopts, alias };
234     }
235   }
236 
237   /**
238    * Gets the shell environment.
239    *
240    * @return The environment
241    */
242   public Map<String, String> getEnv() {
243     return this.builder.environment();
244   }
245 
246   /**
247    * Sets the working directory.
248    *
249    * @param dir The directory
250    */
251   public void setDirectory(File dir) {
252     this.builder.directory(dir);
253   }
254 
255   /**
256    * Gets the working directory.
257    *
258    * @return The directory
259    */
260   public File getDirectory() {
261     return this.builder.directory();
262   }
263 
264   /**
265    * Redirects the standard output to a file.
266    *
267    * @param f The file to redirect to
268    * @throws IOException If the file cannot be created
269    */
270   public void setStdOut(File f) throws IOException {
271     if (this.stdOut != null && this.closeStdOut) {
272       FileUtils.close(this.stdOut);
273     }
274 
275     if (f == null) {
276       this.stdOut = null;
277       this.closeStdOut = false;
278     } else {
279       File parent = f.getParentFile();
280       if (parent != null) {
281         parent.mkdirs();
282       }
283 
284       this.stdOut = new FileOutputStream(f);
285       this.closeStdOut = true;
286     }
287   }
288 
289   /**
290    * Redirects the standard output to a stream.
291    *
292    * @param os The stream to redirect to
293    */
294   public void setStdOut(OutputStream os) {
295     if (this.stdOut != null && this.closeStdOut) {
296       FileUtils.close(this.stdOut);
297     }
298 
299     this.stdOut = os;
300     this.closeStdOut = false;
301   }
302 
303   /**
304    * Redirects the standard error to a file.
305    *
306    * @param f The file to redirect to
307    * @throws IOException If the file cannot be created
308    */
309   public void setStdErr(File f) throws IOException {
310     if (this.stdErr != null && this.closeStdErr) {
311       FileUtils.close(this.stdErr);
312     }
313 
314     if (f == null) {
315       this.stdErr = null;
316       this.closeStdErr = false;
317     } else {
318       File parent = f.getParentFile();
319       if (parent != null) {
320         parent.mkdirs();
321       }
322 
323       this.stdErr = new FileOutputStream(f);
324       this.closeStdErr = true;
325     }
326   }
327 
328   /**
329    * Redirects the standard error to a stream.
330    *
331    * @param os The stream to redirect to
332    */
333   public void setStdErr(OutputStream os) {
334     if (this.stdErr != null && this.closeStdErr) {
335       FileUtils.close(this.stdErr);
336     }
337 
338     this.stdErr = os;
339     this.closeStdErr = false;
340   }
341 
342   /**
343    * Executes a native application.
344    *
345    * @param cmd  The command to execute
346    * @param args The command arguments
347    * @return null if nothing was executed, or the exit code
348    * @throws IOException If an error occurs during the execution
349    * @throws InterruptedException If the thread is interrupted
350    */
351   public Integer exec(String cmd, Object... args)
352     throws IOException, InterruptedException {
353     String osName = SystemUtils.getOsName();
354     return doExec(osName, cmd, args);
355   }
356 
357   /**
358    * Executes a native application.
359    *
360    * @param osName The OS name
361    * @param cmd    The command to execute
362    * @param args   The command arguments
363    * @return null if nothing was executed, or the exit code
364    * @throws IOException If an error occurs during the execution
365    * @throws InterruptedException If the thread is interrupted
366    */
367   protected Integer doExec(String osName, String cmd, Object... args)
368     throws IOException, InterruptedException {
369     String[] cmdline = doShell(osName, cmd, args);
370     if (cmdline == null) {
371       return null;
372     } else {
373       builder.command(cmdline);
374       Process p = builder.start();
375 
376       Pipe out;
377       Pipe err;
378       if (stdOut == null) {
379         out = null;
380       } else {
381         out = new Pipe(p.getInputStream(), stdOut, closeStdOut);
382         out.start();
383       }
384       if (stdErr == null) {
385         err = null;
386       } else {
387         err = new Pipe(p.getErrorStream(), stdErr, closeStdErr);
388         err.start();
389       }
390 
391       try {
392         int exitCode = p.waitFor();
393         if (out.getError() != null) {
394           throw out.getError();
395         } else if (err.getError() != null) {
396           throw err.getError();
397         } else {
398           return exitCode;
399         }
400       } catch (InterruptedException e) {
401         p.destroy(); // This closes the streams, cancelling the pipes
402         throw e;
403       }
404     }
405   }
406 
407   /**
408    * Pipe type.
409    */
410   private static class Pipe extends Thread {
411     /** Stream to read from. */
412     private InputStream is;
413     /** Stream to write to. */
414     private OutputStream os;
415     /** Error. */
416     private IOException error;
417     /** Close stream on end flag. */
418     private boolean closeOutput;
419 
420     /**
421      * Creates a new instance.
422      *
423      * @param is The stream to read from
424      * @param os The stream to write to
425      * @param closeOutput Whether to close the output stream or not
426      */
427     public Pipe(InputStream is, OutputStream os, boolean closeOutput) {
428       this.is = is;
429       this.os = os;
430       this.closeOutput = closeOutput;
431     }
432 
433     /**
434      * {@inheritDoc}
435      */
436     @Override public void run() {
437       try {
438         byte[] buf = new byte[BUF_SIZE];
439         int count = is.read(buf);
440         while (count != -1) {
441           os.write(buf, 0, count);
442           count = is.read(buf);
443         }
444         os.flush();
445       } catch (IOException e) {
446         this.error = e;
447       } finally {
448         FileUtils.close(is);
449         if (closeOutput) {
450           FileUtils.close(os);
451         }
452       }
453     }
454 
455     /**
456      * Gets the error, if any.
457      *
458      * @return The error
459      */
460     public IOException getError() {
461       return this.error;
462     }
463   }
464 }