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 }