001 /** 002 * Copyright (c) 2010 Yahoo! Inc. All rights reserved. 003 * Licensed under the Apache License, Version 2.0 (the "License"); 004 * you may not use this file except in compliance with the License. 005 * 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 010 * distributed under the License is distributed on an "AS IS" BASIS, 011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 * See the License for the specific language governing permissions and 013 * limitations under the License. See accompanying LICENSE file. 014 */ 015 package org.apache.oozie.action.ssh; 016 017 import java.io.BufferedReader; 018 import java.io.File; 019 import java.io.FileWriter; 020 import java.io.IOException; 021 import java.io.InputStreamReader; 022 import java.util.List; 023 import java.util.concurrent.Callable; 024 025 import org.apache.oozie.client.WorkflowAction; 026 import org.apache.oozie.client.OozieClient; 027 import org.apache.oozie.client.WorkflowAction.Status; 028 import org.apache.oozie.action.ActionExecutor; 029 import org.apache.oozie.action.ActionExecutorException; 030 import org.apache.oozie.service.CallbackService; 031 import org.apache.oozie.servlet.CallbackServlet; 032 import org.apache.oozie.service.Services; 033 import org.apache.oozie.util.IOUtils; 034 import org.apache.oozie.util.PropertiesUtils; 035 import org.apache.oozie.util.XLog; 036 import org.apache.oozie.util.XmlUtils; 037 import org.jdom.Element; 038 import org.jdom.JDOMException; 039 import org.jdom.Namespace; 040 041 /** 042 * Ssh action executor. <p/> <ul> <li>Execute the shell commands on the remote host</li> <li>Copies the base and wrapper 043 * scripts on to the remote location</li> <li>Base script is used to run the command on the remote host</li> <li>Wrapper 044 * script is used to check the status of the submitted command</li> <li>handles the submission failures</li> </ul> 045 */ 046 public class SshActionExecutor extends ActionExecutor { 047 public static final String ACTION_TYPE = "ssh"; 048 049 /** 050 * Configuration parameter which specifies whether the specified ssh user is allowed, or has to be the job user. 051 */ 052 public static final String CONF_SSH_ALLOW_USER_AT_HOST = CONF_PREFIX + "ssh.allow.user.at.host"; 053 054 protected static final String SSH_COMMAND_OPTIONS = 055 "-o PasswordAuthentication=no -o KbdInteractiveDevices=no -o StrictHostKeyChecking=no -o ConnectTimeout=20 "; 056 057 protected static final String SSH_COMMAND_BASE = "ssh " + SSH_COMMAND_OPTIONS; 058 protected static final String SCP_COMMAND_BASE = "scp " + SSH_COMMAND_OPTIONS; 059 060 public static final String ERR_SETUP_FAILED = "SETUP_FAILED"; 061 public static final String ERR_EXECUTION_FAILED = "EXECUTION_FAILED"; 062 public static final String ERR_UNKNOWN_ERROR = "UNKOWN_ERROR"; 063 public static final String ERR_COULD_NOT_CONNECT = "COULD_NOT_CONNECT"; 064 public static final String ERR_HOST_RESOLUTION = "COULD_NOT_RESOLVE_HOST"; 065 public static final String ERR_FNF = "FNF"; 066 public static final String ERR_AUTH_FAILED = "AUTH_FAILED"; 067 public static final String ERR_NO_EXEC_PERM = "NO_EXEC_PERM"; 068 public static final String ERR_USER_MISMATCH = "ERR_USER_MISMATCH"; 069 public static final String ERR_EXCEDE_LEN = "ERR_OUTPUT_EXCEED_MAX_LEN"; 070 071 public static final String DELETE_TMP_DIR = "oozie.action.ssh.delete.remote.tmp.dir"; 072 073 public static final String HTTP_COMMAND = "oozie.action.ssh.http.command"; 074 075 public static final String HTTP_COMMAND_OPTIONS = "oozie.action.ssh.http.command.post.options"; 076 077 private static final String EXT_STATUS_VAR = "#status"; 078 079 private static int maxLen; 080 private static boolean allowSshUserAtHost; 081 082 protected SshActionExecutor() { 083 super(ACTION_TYPE); 084 } 085 086 /** 087 * Initialize Action. 088 */ 089 @Override 090 public void initActionType() { 091 super.initActionType(); 092 maxLen = getOozieConf().getInt(CallbackServlet.CONF_MAX_DATA_LEN, 2 * 1024); 093 allowSshUserAtHost = getOozieConf().getBoolean(CONF_SSH_ALLOW_USER_AT_HOST, true); 094 registerError(InterruptedException.class.getName(), ActionExecutorException.ErrorType.ERROR, "SH001"); 095 registerError(JDOMException.class.getName(), ActionExecutorException.ErrorType.ERROR, "SH002"); 096 initSshScripts(); 097 } 098 099 /** 100 * Check ssh action status. 101 * 102 * @param context action execution context. 103 * @param action action object. 104 */ 105 @Override 106 public void check(Context context, WorkflowAction action) throws ActionExecutorException { 107 Status status = getActionStatus(context, action); 108 boolean captureOutput = false; 109 try { 110 Element eConf = XmlUtils.parseXml(action.getConf()); 111 Namespace ns = eConf.getNamespace(); 112 captureOutput = eConf.getChild("capture-output", ns) != null; 113 } 114 catch (JDOMException ex) { 115 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "ERR_XML_PARSE_FAILED", 116 "unknown error", ex); 117 } 118 XLog log = XLog.getLog(getClass()); 119 log.debug("Capture Output: {0}", captureOutput); 120 if (status == Status.OK) { 121 if (captureOutput) { 122 String outFile = getRemoteFileName(context, action, "stdout", false, true); 123 String dataCommand = SSH_COMMAND_BASE + action.getTrackerUri() + " cat " + outFile; 124 log.debug("Ssh command [{0}]", dataCommand); 125 try { 126 Process process = Runtime.getRuntime().exec(dataCommand.split("\\s")); 127 StringBuffer buffer = new StringBuffer(); 128 boolean overflow = false; 129 drainBuffers(process, buffer, null, maxLen); 130 if (buffer.length() > maxLen) { 131 overflow = true; 132 } 133 if (overflow) { 134 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, 135 "ERR_OUTPUT_EXCEED_MAX_LEN", "unknown error"); 136 } 137 context.setExecutionData(status.toString(), PropertiesUtils.stringToProperties(buffer.toString())); 138 } 139 catch (Exception ex) { 140 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "ERR_UNKNOWN_ERROR", 141 "unknown error", ex); 142 } 143 } 144 else { 145 context.setExecutionData(status.toString(), null); 146 } 147 } 148 else { 149 if (status == Status.ERROR) { 150 context.setExecutionData(status.toString(), null); 151 } 152 else { 153 context.setExternalStatus(status.toString()); 154 } 155 } 156 } 157 158 /** 159 * Kill ssh action. 160 * 161 * @param context action execution context. 162 * @param action object. 163 */ 164 @Override 165 public void kill(Context context, WorkflowAction action) throws ActionExecutorException { 166 String command = "ssh " + action.getTrackerUri() + " kill -KILL " + action.getExternalId(); 167 int returnValue = getReturnValue(command); 168 if (returnValue != 0) { 169 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "FAILED_TO_KILL", XLog.format( 170 "Unable to kill process {0} on {1}", action.getExternalId(), action.getTrackerUri())); 171 } 172 context.setEndData(WorkflowAction.Status.KILLED, "ERROR"); 173 } 174 175 /** 176 * Start the ssh action execution. 177 * 178 * @param context action execution context. 179 * @param action action object. 180 */ 181 @SuppressWarnings("unchecked") 182 @Override 183 public void start(final Context context, final WorkflowAction action) throws ActionExecutorException { 184 XLog log = XLog.getLog(getClass()); 185 log.info("start() begins"); 186 String confStr = action.getConf(); 187 Element conf; 188 try { 189 conf = XmlUtils.parseXml(confStr); 190 } 191 catch (Exception ex) { 192 throw convertException(ex); 193 } 194 Namespace nameSpace = conf.getNamespace(); 195 Element hostElement = conf.getChild("host", nameSpace); 196 String hostString = hostElement.getValue().trim(); 197 hostString = prepareUserHost(hostString, context); 198 final String host = hostString; 199 final String dirLocation = execute(new Callable<String>() { 200 public String call() throws Exception { 201 return setupRemote(host, context, action); 202 } 203 204 }); 205 206 String runningPid = execute(new Callable<String>() { 207 public String call() throws Exception { 208 return checkIfRunning(host, context, action); 209 } 210 }); 211 String pid = ""; 212 213 if (runningPid == null) { 214 final Element commandElement = conf.getChild("command", nameSpace); 215 final boolean ignoreOutput = conf.getChild("capture-output", nameSpace) == null; 216 217 if (commandElement != null) { 218 List<Element> argsList = conf.getChildren("args", nameSpace); 219 StringBuilder args = new StringBuilder(""); 220 if ((argsList != null) && (argsList.size() > 0)) { 221 for (Element argsElement : argsList) { 222 args = args.append(argsElement.getValue()).append(" "); 223 } 224 args.setLength(args.length() - 1); 225 } 226 final String argsString = args.toString(); 227 final String recoveryId = context.getRecoveryId(); 228 pid = execute(new Callable<String>() { 229 230 @Override 231 public String call() throws Exception { 232 return doExecute(host, dirLocation, commandElement.getValue(), argsString, ignoreOutput, 233 action, recoveryId); 234 } 235 236 }); 237 } 238 context.setStartData(pid, host, host); 239 } 240 else { 241 pid = runningPid; 242 context.setStartData(pid, host, host); 243 check(context, action); 244 } 245 log.info("start() ends"); 246 } 247 248 private String checkIfRunning(String host, final Context context, final WorkflowAction action) { 249 String pid = null; 250 String outFile = getRemoteFileName(context, action, "pid", false, false); 251 String getOutputCmd = SSH_COMMAND_BASE + host + " cat " + outFile; 252 try { 253 Process process = Runtime.getRuntime().exec(getOutputCmd.split("\\s")); 254 StringBuffer buffer = new StringBuffer(); 255 drainBuffers(process, buffer, null, maxLen); 256 pid = getFirstLine(buffer); 257 258 if (Long.valueOf(pid) > 0) { 259 return pid; 260 } 261 else { 262 return null; 263 } 264 } 265 catch (Exception e) { 266 return null; 267 } 268 } 269 270 /** 271 * Get remote host working location. 272 * 273 * @param context action execution context 274 * @param action Action 275 * @param fileExtension Extension to be added to file name 276 * @param dirOnly Get the Directory only 277 * @param useExtId Flag to use external ID in the path 278 * @return remote host file name/Directory. 279 */ 280 public String getRemoteFileName(Context context, WorkflowAction action, String fileExtension, boolean dirOnly, 281 boolean useExtId) { 282 String path = getActionDirPath(context.getWorkflow().getId(), action, ACTION_TYPE, false) + "/"; 283 if (dirOnly) { 284 return path; 285 } 286 if (useExtId) { 287 path = path + action.getExternalId() + "."; 288 } 289 path = path + context.getRecoveryId() + "." + fileExtension; 290 return path; 291 } 292 293 /** 294 * Utility method to execute command. 295 * 296 * @param command Command to execute as String. 297 * @return exit status of the execution. 298 * @throws IOException if process exits with status nonzero. 299 * @throws InterruptedException if process does not run properly. 300 */ 301 public int executeCommand(String command) throws IOException, InterruptedException { 302 Runtime runtime = Runtime.getRuntime(); 303 Process p = runtime.exec(command.split("\\s")); 304 305 StringBuffer errorBuffer = new StringBuffer(); 306 int exitValue = drainBuffers(p, null, errorBuffer, maxLen); 307 308 String error = null; 309 if (exitValue != 0) { 310 error = getTruncatedString(errorBuffer); 311 throw new IOException(XLog.format("Not able to perform operation [{0}]", command) + " | " + "ErrorStream: " 312 + error); 313 } 314 return exitValue; 315 } 316 317 /** 318 * Do ssh action execution setup on remote host. 319 * 320 * @param host host name. 321 * @param context action execution context. 322 * @param action action object. 323 * @return remote host working directory. 324 * @throws IOException thrown if failed to setup. 325 * @throws InterruptedException thrown if any interruption happens. 326 */ 327 protected String setupRemote(String host, Context context, WorkflowAction action) throws IOException, InterruptedException { 328 XLog log = XLog.getLog(getClass()); 329 log.info("Attempting to copy ssh base scripts to remote host [{0}]", host); 330 String localDirLocation = Services.get().getRuntimeDir() + "/ssh"; 331 if (localDirLocation.endsWith("/")) { 332 localDirLocation = localDirLocation.substring(0, localDirLocation.length() - 1); 333 } 334 File file = new File(localDirLocation + "/ssh-base.sh"); 335 if (!file.exists()) { 336 throw new IOException("Required Local file " + file.getAbsolutePath() + " not present."); 337 } 338 file = new File(localDirLocation + "/ssh-wrapper.sh"); 339 if (!file.exists()) { 340 throw new IOException("Required Local file " + file.getAbsolutePath() + " not present."); 341 } 342 String remoteDirLocation = getRemoteFileName(context, action, null, true, true); 343 String command = XLog.format("{0}{1} mkdir -p {2} ", SSH_COMMAND_BASE, host, remoteDirLocation).toString(); 344 executeCommand(command); 345 command = XLog.format("{0}{1}/ssh-base.sh {2}/ssh-wrapper.sh {3}:{4}", SCP_COMMAND_BASE, localDirLocation, 346 localDirLocation, host, remoteDirLocation); 347 executeCommand(command); 348 command = XLog.format("{0}{1} chmod +x {2}ssh-base.sh {3}ssh-wrapper.sh ", SSH_COMMAND_BASE, host, 349 remoteDirLocation, remoteDirLocation); 350 executeCommand(command); 351 return remoteDirLocation; 352 } 353 354 /** 355 * Execute the ssh command. 356 * 357 * @param host hostname. 358 * @param dirLocation location of the base and wrapper scripts. 359 * @param cmnd command to be executed. 360 * @param args command arguments. 361 * @param ignoreOutput ignore output option. 362 * @param action action object. 363 * @param recoveryId action id + run number to enable recovery in rerun 364 * @return process id of the running command. 365 * @throws IOException thrown if failed to run the command. 366 * @throws InterruptedException thrown if any interruption happens. 367 */ 368 protected String doExecute(String host, String dirLocation, String cmnd, String args, boolean ignoreOutput, 369 WorkflowAction action, String recoveryId) throws IOException, InterruptedException { 370 XLog log = XLog.getLog(getClass()); 371 Runtime runtime = Runtime.getRuntime(); 372 String callbackPost = ignoreOutput ? "_" : getOozieConf().get(HTTP_COMMAND_OPTIONS).replace(" ", "%%%"); 373 // TODO check 374 String callBackUrl = Services.get().get(CallbackService.class) 375 .createCallBackUrl(action.getId(), EXT_STATUS_VAR); 376 String command = XLog.format("{0}{1} {2}ssh-base.sh {3} \"{4}\" \"{5}\" {6} {7} {8} ", SSH_COMMAND_BASE, host, 377 dirLocation, getOozieConf().get(HTTP_COMMAND), callBackUrl, callbackPost, recoveryId, cmnd, args) 378 .toString(); 379 log.trace("Executing ssh command [{0}]", command); 380 Process p = runtime.exec(command.split("\\s")); 381 String pid = ""; 382 383 StringBuffer inputBuffer = new StringBuffer(); 384 StringBuffer errorBuffer = new StringBuffer(); 385 int exitValue = drainBuffers(p, inputBuffer, errorBuffer, maxLen); 386 387 pid = getFirstLine(inputBuffer); 388 389 String error = null; 390 if (exitValue != 0) { 391 error = getTruncatedString(errorBuffer); 392 throw new IOException(XLog.format("Not able to execute ssh-base.sh on {0}", host) + " | " + "ErrorStream: " 393 + error); 394 } 395 return pid; 396 } 397 398 /** 399 * End action execution. 400 * 401 * @param context action execution context. 402 * @param action action object. 403 * @throws ActionExecutorException thrown if action end execution fails. 404 */ 405 public void end(final Context context, final WorkflowAction action) throws ActionExecutorException { 406 if (action.getExternalStatus().equals("OK")) { 407 context.setEndData(WorkflowAction.Status.OK, WorkflowAction.Status.OK.toString()); 408 } 409 else { 410 context.setEndData(WorkflowAction.Status.ERROR, WorkflowAction.Status.ERROR.toString()); 411 } 412 boolean deleteTmpDir = getOozieConf().getBoolean(DELETE_TMP_DIR, true); 413 if (deleteTmpDir) { 414 String tmpDir = getRemoteFileName(context, action, null, true, false); 415 String removeTmpDirCmd = SSH_COMMAND_BASE + action.getTrackerUri() + " rm -rf " + tmpDir; 416 int retVal = getReturnValue(removeTmpDirCmd); 417 if (retVal != 0) { 418 XLog.getLog(getClass()).warn("Cannot delete temp dir {0}", tmpDir); 419 } 420 } 421 } 422 423 /** 424 * Get the return value of a process. 425 * 426 * @param command command to be executed. 427 * @return zero if execution is successful and any non zero value for failure. 428 * @throws ActionExecutorException 429 */ 430 private int getReturnValue(String command) throws ActionExecutorException { 431 int returnValue; 432 Process ps = null; 433 try { 434 ps = Runtime.getRuntime().exec(command.split("\\s")); 435 returnValue = drainBuffers(ps, null, null, 0); 436 } 437 catch (IOException e) { 438 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, "FAILED_OPERATION", XLog.format( 439 "Not able to perform operation {0}", command), e); 440 } 441 finally { 442 ps.destroy(); 443 } 444 return returnValue; 445 } 446 447 /** 448 * Copy the ssh base and wrapper scripts to the local directory. 449 */ 450 private void initSshScripts() { 451 String dirLocation = Services.get().getRuntimeDir() + "/ssh"; 452 File path = new File(dirLocation); 453 if (!path.mkdirs()) { 454 throw new RuntimeException(XLog.format("Not able to create required directory {0}", dirLocation)); 455 } 456 try { 457 IOUtils.copyCharStream(IOUtils.getResourceAsReader("ssh-base.sh", -1), new FileWriter(dirLocation 458 + "/ssh-base.sh")); 459 IOUtils.copyCharStream(IOUtils.getResourceAsReader("ssh-wrapper.sh", -1), new FileWriter(dirLocation 460 + "/ssh-wrapper.sh")); 461 } 462 catch (IOException ie) { 463 throw new RuntimeException(XLog.format("Not able to copy required scripts file to {0} " 464 + "for SshActionHandler", dirLocation)); 465 } 466 } 467 468 /** 469 * Get action status. 470 * 471 * @param action action object. 472 * @return status of the action(RUNNING/OK/ERROR). 473 * @throws ActionExecutorException thrown if there is any error in getting status. 474 */ 475 protected Status getActionStatus(Context context, WorkflowAction action) throws ActionExecutorException { 476 String command = SSH_COMMAND_BASE + action.getTrackerUri() + " ps -p " + action.getExternalId(); 477 Status aStatus; 478 int returnValue = getReturnValue(command); 479 if (returnValue == 0) { 480 aStatus = Status.RUNNING; 481 } 482 else { 483 String outFile = getRemoteFileName(context, action, "error", false, true); 484 String checkErrorCmd = SSH_COMMAND_BASE + action.getTrackerUri() + " ls " + outFile; 485 int retVal = getReturnValue(checkErrorCmd); 486 if (retVal == 0) { 487 aStatus = Status.ERROR; 488 } 489 else { 490 aStatus = Status.OK; 491 } 492 } 493 return aStatus; 494 } 495 496 /** 497 * Execute the callable. 498 * 499 * @param callable required callable. 500 * @throws ActionExecutorException thrown if there is any error in command execution. 501 */ 502 private <T> T execute(Callable<T> callable) throws ActionExecutorException { 503 XLog log = XLog.getLog(getClass()); 504 try { 505 return callable.call(); 506 } 507 catch (IOException ex) { 508 log.warn("Error while executing ssh EXECUTION"); 509 String errorMessage = ex.getMessage(); 510 if (null == errorMessage) { // Unknown IOException 511 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, ERR_UNKNOWN_ERROR, ex 512 .getMessage(), ex); 513 } // Host Resolution Issues 514 else { 515 if (errorMessage.contains("Could not resolve hostname") || 516 errorMessage.contains("service not known")) { 517 throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_HOST_RESOLUTION, ex 518 .getMessage(), ex); 519 } // Connection Timeout. Host temporarily down. 520 else { 521 if (errorMessage.contains("timed out")) { 522 throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_COULD_NOT_CONNECT, 523 ex.getMessage(), ex); 524 }// Local ssh-base or ssh-wrapper missing 525 else { 526 if (errorMessage.contains("Required Local file")) { 527 throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_FNF, 528 ex.getMessage(), ex); // local_FNF 529 }// Required oozie bash scripts missing, after the copy was 530 // successful 531 else { 532 if (errorMessage.contains("No such file or directory") 533 && (errorMessage.contains("ssh-base") || errorMessage.contains("ssh-wrapper"))) { 534 throw new ActionExecutorException(ActionExecutorException.ErrorType.TRANSIENT, ERR_FNF, 535 ex.getMessage(), ex); // remote 536 // FNF 537 } // Required application execution binary missing (either 538 // caught by ssh-wrapper 539 else { 540 if (errorMessage.contains("command not found")) { 541 throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT, ERR_FNF, ex 542 .getMessage(), ex); // remote 543 // FNF 544 } // Permission denied while connecting 545 else { 546 if (errorMessage.contains("Permission denied")) { 547 throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT, ERR_AUTH_FAILED, ex 548 .getMessage(), ex); 549 } // Permission denied while executing 550 else { 551 if (errorMessage.contains(": Permission denied")) { 552 throw new ActionExecutorException(ActionExecutorException.ErrorType.NON_TRANSIENT, ERR_NO_EXEC_PERM, ex 553 .getMessage(), ex); 554 } 555 else { 556 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, ERR_UNKNOWN_ERROR, ex 557 .getMessage(), ex); 558 } 559 } 560 } 561 } 562 } 563 } 564 } 565 } 566 } // Any other type of exception 567 catch (Exception ex) { 568 throw convertException(ex); 569 } 570 } 571 572 /** 573 * Checks whether the system is configured to always use the oozie user for ssh, and injects the user if required. 574 * 575 * @param host the host string. 576 * @param context the execution context. 577 * @return the modified host string with a user parameter added on if required. 578 * @throws ActionExecutorException in case the flag to use the oozie user is turned on and there is a mismatch 579 * between the user specified in the host and the oozie user. 580 */ 581 private String prepareUserHost(String host, Context context) throws ActionExecutorException { 582 String oozieUser = context.getProtoActionConf().get(OozieClient.USER_NAME); 583 if (allowSshUserAtHost) { 584 if (!host.contains("@")) { 585 host = oozieUser + "@" + host; 586 } 587 } 588 else { 589 if (host.contains("@")) { 590 if (!host.toLowerCase().startsWith(oozieUser + "@")) { 591 throw new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, ERR_USER_MISMATCH, 592 XLog.format("user mismatch between oozie user [{0}] and ssh host [{1}]", oozieUser, host)); 593 } 594 } 595 else { 596 host = oozieUser + "@" + host; 597 } 598 } 599 return host; 600 } 601 602 public boolean isCompleted(String externalStatus) { 603 return true; 604 } 605 606 /** 607 * Truncate the string to max length. 608 * 609 * @param strBuffer 610 * @return truncated string string 611 */ 612 private String getTruncatedString(StringBuffer strBuffer) { 613 614 if (strBuffer.length() <= maxLen) { 615 return strBuffer.toString(); 616 } 617 else { 618 return strBuffer.substring(0, maxLen); 619 } 620 } 621 622 /** 623 * Drains the inputStream and errorStream of the Process being executed. The contents of the streams are stored if a 624 * buffer is provided for the stream. 625 * 626 * @param p The Process instance. 627 * @param inputBuffer The buffer into which STDOUT is to be read. Can be null if only draining is required. 628 * @param errorBuffer The buffer into which STDERR is to be read. Can be null if only draining is required. 629 * @param maxLength The maximum data length to be stored in these buffers. This is an indicative value, and the 630 * store content may exceed this length. 631 * @return the exit value of the process. 632 * @throws IOException 633 */ 634 private int drainBuffers(Process p, StringBuffer inputBuffer, StringBuffer errorBuffer, int maxLength) 635 throws IOException { 636 int exitValue = -1; 637 BufferedReader ir = new BufferedReader(new InputStreamReader(p.getInputStream())); 638 BufferedReader er = new BufferedReader(new InputStreamReader(p.getErrorStream())); 639 640 int inBytesRead = 0; 641 int errBytesRead = 0; 642 643 boolean processEnded = false; 644 645 try { 646 while (!processEnded) { 647 try { 648 exitValue = p.exitValue(); 649 processEnded = true; 650 } 651 catch (IllegalThreadStateException ex) { 652 // Continue to drain. 653 } 654 655 inBytesRead += drainBuffer(ir, inputBuffer, maxLength, inBytesRead, processEnded); 656 errBytesRead += drainBuffer(er, errorBuffer, maxLength, errBytesRead, processEnded); 657 } 658 } 659 finally { 660 ir.close(); 661 er.close(); 662 } 663 return exitValue; 664 } 665 666 /** 667 * Reads the contents of a stream and stores them into the provided buffer. 668 * 669 * @param br The stream to be read. 670 * @param storageBuf The buffer into which the contents of the stream are to be stored. 671 * @param maxLength The maximum number of bytes to be stored in the buffer. An indicative value and may be 672 * exceeded. 673 * @param bytesRead The number of bytes read from this stream to date. 674 * @param readAll If true, the stream is drained while their is data available in it. Otherwise, only a single chunk 675 * of data is read, irrespective of how much is available. 676 * @return 677 * @throws IOException 678 */ 679 private int drainBuffer(BufferedReader br, StringBuffer storageBuf, int maxLength, int bytesRead, boolean readAll) 680 throws IOException { 681 int bReadSession = 0; 682 if (br.ready()) { 683 char[] buf = new char[1024]; 684 do { 685 int bReadCurrent = br.read(buf, 0, 1024); 686 if (storageBuf != null && bytesRead < maxLength) { 687 storageBuf.append(buf, 0, bReadCurrent); 688 } 689 bReadSession += bReadCurrent; 690 } while (br.ready() && readAll); 691 } 692 return bReadSession; 693 } 694 695 /** 696 * Returns the first line from a StringBuffer, recognized by the new line character \n. 697 * 698 * @param buffer The StringBuffer from which the first line is required. 699 * @return The first line of the buffer. 700 */ 701 private String getFirstLine(StringBuffer buffer) { 702 int newLineIndex = 0; 703 newLineIndex = buffer.indexOf("\n"); 704 if (newLineIndex == -1) { 705 return buffer.toString(); 706 } 707 else { 708 return buffer.substring(0, newLineIndex); 709 } 710 } 711 }