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    }