001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.utils; 014 015import static org.apache.juneau.internal.IOUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.io.*; 019import java.lang.reflect.*; 020import java.util.*; 021import java.util.logging.*; 022 023import org.apache.juneau.internal.*; 024import org.apache.juneau.utils.IOPipe.*; 025 026/** 027 * Utility class for running operating system processes. 028 * 029 * <p> 030 * Similar to {@link java.lang.ProcessBuilder} but with additional features. 031 */ 032public class ProcBuilder { 033 034 private java.lang.ProcessBuilder pb = new java.lang.ProcessBuilder(); 035 private TeeWriter outWriters = new TeeWriter(), logWriters = new TeeWriter(); 036 private LineProcessor lp; 037 private Process p; 038 private int maxExitStatus = 0; 039 private boolean byLines; 040 private String divider = "--------------------------------------------------------------------------------"; 041 042 /** 043 * Creates a process builder with the specified arguments. 044 * 045 * <p> 046 * Equivalent to calling <code>ProcessBuilder.create().command(args);</code> 047 * 048 * @param args The command-line arguments. 049 * @return A new process builder. 050 */ 051 public static ProcBuilder create(Object...args) { 052 return new ProcBuilder().command(args); 053 } 054 055 /** 056 * Creates an empty process builder. 057 * 058 * @return A new process builder. 059 */ 060 public static ProcBuilder create() { 061 return new ProcBuilder().command(); 062 } 063 064 /** 065 * Command arguments. 066 * 067 * <p> 068 * Arguments can be collections or arrays and will be automatically expanded. 069 * 070 * @param args The command-line arguments. 071 * @return This object (for method chaining). 072 */ 073 public ProcBuilder command(Object...args) { 074 return commandIf(ANY, args); 075 } 076 077 /** 078 * Command arguments if the specified matcher matches. 079 * 080 * <p> 081 * Can be used for specifying OS-specific commands. 082 * 083 * <h5 class='section'>Example:</h5> 084 * <p class='bcode'> 085 * ProcBuilder pb = ProcBuilder 086 * .create() 087 * .commandIf(<jsf>WINDOWS</jsf>, <js>"cmd /c dir"</js>) 088 * .commandIf(<jsf>UNIX</jsf>, <js>"bash -c ls"</js>) 089 * .merge() 090 * .execute(); 091 * </p> 092 * 093 * @param m The matcher. 094 * @param args The command line arguments if matcher matches. 095 * @return This object (for method chaining). 096 */ 097 public ProcBuilder commandIf(Matcher m, Object...args) { 098 if (m.matches()) 099 pb.command(toList(args)); 100 return this; 101 } 102 103 /** 104 * Append to the command arguments. 105 * 106 * <p> 107 * Arguments can be collections or arrays and will be automatically expanded. 108 * 109 * @param args The command-line arguments. 110 * @return This object (for method chaining). 111 */ 112 public ProcBuilder append(Object...args) { 113 return appendIf(ANY, args); 114 } 115 116 /** 117 * Append to the command arguments if the specified matcher matches. 118 * 119 * <p> 120 * Arguments can be collections or arrays and will be automatically expanded. 121 * 122 * @param m The matcher. 123 * @param args The command line arguments if matcher matches. 124 * @return This object (for method chaining). 125 */ 126 public ProcBuilder appendIf(Matcher m, Object...args) { 127 if (m.matches()) 128 pb.command().addAll(toList(args)); 129 return this; 130 } 131 132 /** 133 * Merge STDOUT and STDERR into a single stream. 134 * 135 * @return This object (for method chaining). 136 */ 137 public ProcBuilder merge() { 138 pb.redirectErrorStream(true); 139 return this; 140 } 141 142 /** 143 * Use by-lines mode. 144 * 145 * <p> 146 * Flushes output after every line of input. 147 * 148 * @return This object (for method chaining). 149 */ 150 public ProcBuilder byLines() { 151 this.byLines = true; 152 return this; 153 } 154 155 /** 156 * Pipe output to the specified writer. 157 * 158 * <p> 159 * The method can be called multiple times to write to multiple writers. 160 * 161 * @param w The writer to pipe to. 162 * @param close Close the writer afterwards. 163 * @return This object (for method chaining). 164 */ 165 public ProcBuilder pipeTo(Writer w, boolean close) { 166 this.outWriters.add(w, close); 167 return this; 168 } 169 170 /** 171 * Pipe output to the specified writer, but don't close the writer. 172 * 173 * @param w The writer to pipe to. 174 * @return This object (for method chaining). 175 */ 176 public ProcBuilder pipeTo(Writer w) { 177 return pipeTo(w, false); 178 } 179 180 /** 181 * Pipe output to the specified writer, including the command and return code. 182 * 183 * <p> 184 * The method can be called multiple times to write to multiple writers. 185 * 186 * @param w The writer to pipe to. 187 * @param close Close the writer afterwards. 188 * @return This object (for method chaining). 189 */ 190 public ProcBuilder logTo(Writer w, boolean close) { 191 this.logWriters.add(w, close); 192 this.outWriters.add(w, close); 193 return this; 194 } 195 196 /** 197 * Pipe output to the specified writer, including the command and return code. 198 * 199 * <p> 200 * The method can be called multiple times to write to multiple writers. 201 * Don't close the writer afterwards. 202 * 203 * @param w The writer to pipe to. 204 * @return This object (for method chaining). 205 */ 206 public ProcBuilder logTo(Writer w) { 207 return logTo(w, false); 208 } 209 210 /** 211 * Pipe output to the specified writer, including the command and return code. 212 * The method can be called multiple times to write to multiple writers. 213 * 214 * @param level The log level. 215 * @param logger The logger to log to. 216 * @return This object (for method chaining). 217 */ 218 public ProcBuilder logTo(final Level level, final Logger logger) { 219 if (logger.isLoggable(level)) { 220 logTo(new StringWriter() { 221 private boolean isClosed; // Prevents messages from being written twice. 222 @Override /* Writer */ 223 public void close() { 224 if (! isClosed) 225 logger.log(level, this.toString()); 226 isClosed = true; 227 } 228 }, true); 229 } 230 return this; 231 } 232 233 /** 234 * Line processor to use to process/convert lines of output returned by the process. 235 * 236 * @param lp The new line processor. 237 * @return This object (for method chaining). 238 */ 239 public ProcBuilder lp(LineProcessor lp) { 240 this.lp = lp; 241 return this; 242 } 243 244 /** 245 * Append the specified environment variables to the process. 246 * 247 * @param env The new set of environment variables. 248 * @return This object (for method chaining). 249 */ 250 @SuppressWarnings({"rawtypes"}) 251 public ProcBuilder env(Map env) { 252 if (env != null) 253 for (Map.Entry e : (Set<Map.Entry>)env.entrySet()) 254 environment(e.getKey().toString(), e.getValue() == null ? null : e.getValue().toString()); 255 return this; 256 } 257 258 /** 259 * Append the specified environment variable. 260 * 261 * @param key The environment variable name. 262 * @param val The environment variable value. 263 * @return This object (for method chaining). 264 */ 265 public ProcBuilder environment(String key, String val) { 266 pb.environment().put(key, val); 267 return this; 268 } 269 270 /** 271 * Sets the directory where the command will be executed. 272 * 273 * @param directory The directory. 274 * @return This object (for method chaining). 275 */ 276 public ProcBuilder directory(File directory) { 277 pb.directory(directory); 278 return this; 279 } 280 281 /** 282 * Sets the maximum allowed return code on the process call. 283 * 284 * <p> 285 * If the return code exceeds this value, an IOException is returned on the {@link #run()} command. 286 * The default value is '0'. 287 * 288 * @param maxExitStatus The maximum exit status. 289 * @return This object (for method chaining). 290 */ 291 public ProcBuilder maxExitStatus(int maxExitStatus) { 292 this.maxExitStatus = maxExitStatus; 293 return this; 294 } 295 296 /** 297 * Run this command and pipes the output to the specified writer or output stream. 298 * 299 * @return The exit code from the process. 300 * @throws IOException 301 * @throws InterruptedException 302 */ 303 public int run() throws IOException, InterruptedException { 304 if (pb.command().size() == 0) 305 throw new IOException("No command specified in ProcBuilder."); 306 try { 307 logWriters.append(divider).append('\n').flush(); 308 logWriters.append(join(pb.command(), " ")).append('\n').flush(); 309 p = pb.start(); 310 IOPipe.create(p.getInputStream(), outWriters).lineProcessor(lp).byLines(byLines).run(); 311 int rc = p.waitFor(); 312 logWriters.append("Exit: ").append(String.valueOf(p.exitValue())).append('\n').flush(); 313 if (rc > maxExitStatus) 314 throw new IOException("Return code "+rc+" from command " + join(pb.command(), " ")); 315 return rc; 316 } finally { 317 close(); 318 } 319 } 320 321 /** 322 * Run this command and returns the output as a simple string. 323 * 324 * @return The output from the command. 325 * @throws IOException 326 * @throws InterruptedException 327 */ 328 public String getOutput() throws IOException, InterruptedException { 329 StringWriter sw = new StringWriter(); 330 pipeTo(sw).run(); 331 return sw.toString(); 332 } 333 334 /** 335 * Returns the output from this process as a {@link Scanner}. 336 * 337 * @return The output from the process as a Scanner object. 338 * @throws IOException 339 * @throws InterruptedException 340 */ 341 public Scanner getScanner() throws IOException, InterruptedException { 342 StringWriter sw = new StringWriter(); 343 pipeTo(sw, true); 344 run(); 345 return new Scanner(sw.toString()); 346 } 347 348 /** 349 * Destroys the underlying process. 350 * 351 * <p> 352 * This method is only needed if the {@link #getScanner()} method was used. 353 */ 354 private void close() { 355 closeQuietly(logWriters, outWriters); 356 if (p != null) 357 p.destroy(); 358 } 359 360 /** 361 * Specifies interface for defining OS-specific commands. 362 */ 363 public abstract static class Matcher { 364 abstract boolean matches(); 365 } 366 367 static final String OS = System.getProperty("os.name").toLowerCase(); 368 369 /** Operating system matcher: Any operating system. */ 370 public static final Matcher ANY = new Matcher() { 371 @Override boolean matches() { 372 return true; 373 } 374 }; 375 376 /** Operating system matcher: Any Windows system. */ 377 public static final Matcher WINDOWS = new Matcher() { 378 @Override boolean matches() { 379 return OS.indexOf("win") >= 0; 380 } 381 }; 382 383 /** Operating system matcher: Any Mac system. */ 384 public static final Matcher MAC = new Matcher() { 385 @Override boolean matches() { 386 return OS.indexOf("mac") >= 0; 387 } 388 }; 389 390 /** Operating system matcher: Any Unix or Linux system. */ 391 public static final Matcher UNIX = new Matcher() { 392 @Override boolean matches() { 393 return OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0; 394 } 395 }; 396 397 private static List<String> toList(Object...args) { 398 List<String> l = new LinkedList<>(); 399 for (Object o : args) { 400 if (o.getClass().isArray()) 401 for (int i = 0; i < Array.getLength(o); i++) 402 l.add(Array.get(o, i).toString()); 403 else if (o instanceof Collection) 404 for (Object o2 : (Collection<?>)o) 405 l.add(o2.toString()); 406 else 407 l.add(o.toString()); 408 } 409 return l; 410 } 411}