org.jberet.support.io.OsCommandBatchlet.java Source code

Java tutorial

Introduction

Here is the source code for org.jberet.support.io.OsCommandBatchlet.java

Source

/*
 * Copyright (c) 2017 Red Hat, Inc. and/or its affiliates.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Cheng Fang - Initial API and implementation
 */

package org.jberet.support.io;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.batch.api.BatchProperty;
import javax.batch.api.Batchlet;
import javax.batch.runtime.context.StepContext;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteStreamHandler;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.ShutdownHookProcessDestroyer;
import org.jberet.support._private.SupportLogger;
import org.jberet.support._private.SupportMessages;

/**
 * This batchlet runs a native OS command in a sub-process asynchronously.
 * Main features supported include:
 * <ul>
 *     <li>specify command and arguments as a single line;
 *     <li>specify command and arguments as a comma-separated list of items, for easy handling of spaces in file paths;
 *     <li>custom working directory;
 *     <li>specify timeout as seconds so the OS commnad can timeout
 *     <li>the OS command process can be stopped;
 *     <li>passing custom environment variables;
 *     <li>map non-zero exit code from OS command process.
 * </ul>
 */
@Named
@Dependent
public class OsCommandBatchlet implements Batchlet {
    /**
     * Injected {@code StepContext}.
     */
    @Inject
    protected StepContext stepContext;

    /**
     * The OS command and its arguments as a single line.
     * <p>
     * For example,
     * <pre>
     * "diff out1.txt out2.txt"
     * "cp out1.txt /tmp"
     * </pre>
     *
     * Either this property or {@link #commandArray} property must be specified.
     * If both are present, this property takes precedence.
     * <p>
     * If a command argument contains whitespaces, the argument must be quoted
     * to prevent this argument being broken into multiple arguments.
     * Alternatively, {@link #commandArray} property can be used.
     *
     * @see #commandArray
     */
    @Inject
    @BatchProperty
    protected String commandLine;

    /**
     * The OS command and its arguments as a list of string values separated by comma (,).
     * <p>
     * For example,
     * <pre>
     * "diff, out1.txt, out2.txt"
     * "cp, out1.txt, /tmp"
     * </pre>
     *
     * Either this property or {@link #commandLine} property must be specified.
     * If both are present, {@link #commandLine} takes precedence.
     *
     * @see #commandLine
     */
    @Inject
    @BatchProperty
    protected List<String> commandArray;

    /**
     * The working directory for running the OS command. Optional property, and if not set,
     * it defaults to the current directory.
     */
    @Inject
    @BatchProperty
    protected File workingDir;

    /**
     * A comma-separated list of int numbers that signal the successful completion of the
     * OS command. Optional property, and if not set, it defaults to 0. If the OS command
     * process is known to return non-zero exit code upon successful execution,
     * this property must be set as such to mark the command execution as successful.
     * <p>
     * For example,
     * <pre>
     * "90, 91, 92"
     * </pre>
     */
    @Inject
    @BatchProperty
    protected int[] commandOkExitValues;

    /**
     * The timeout as number of seconds. After the {@code timeoutSeconds} elapses, and the
     * OS command still has not finished, its process will be destroyed, and the batch
     * job execution will be marked as failed.
     * <p>
     * Optional property, and if not set, it defaults to no timeout.
     */
    @Inject
    @BatchProperty
    protected long timeoutSeconds;

    /**
     * Custom environment variables to be used when running the OS command.
     * Optional property, and if not set, it defaults to using the environment
     * of parent process. If set, it will represent the entire environment in
     * the new process, and the parent environment will not be inherited.
     * <p>
     * For example,
     * <pre>
     * "LANG=UTF-8, TMPDIR=/tmp"
     * </pre>
     */
    @Inject
    @BatchProperty
    protected Map<String, String> environment;

    /**
     * Fully-qualified name of a class that implements {@code org.apache.commons.exec.ExecuteStreamHandler},
     * which handles the input and output of the subprocess.
     *
     * @see "org.apache.commons.exec.ExecuteStreamHandler"
     */
    @Inject
    @BatchProperty
    protected Class streamHandler;

    private ExecuteWatchdog watchdog;

    private volatile boolean isStopped;

    /**
     * {@inheritDoc}
     * <p>
     * This method runs the OS command.
     * If the command completes successfully, its process exit code is returned.
     * If there is exception while running the OS command, which may be
     * caused by timeout, the command being stopped, or other errors, the process
     * exit code is set as the step exit status, and the exception is thrown.
     *
     * @return the OS command process exit code
     *
     * @throws Exception upon errors
     */
    @Override
    public String process() throws Exception {
        final DefaultExecutor executor = new DefaultExecutor();
        final CommandLine commandLineObj;
        if (commandLine != null) {
            commandLineObj = CommandLine.parse(commandLine);
        } else {
            if (commandArray == null) {
                throw SupportMessages.MESSAGES.invalidReaderWriterProperty(null, null, "commandArray");
            } else if (commandArray.isEmpty()) {
                throw SupportMessages.MESSAGES.invalidReaderWriterProperty(null, commandArray.toString(),
                        "commandArray");
            }
            commandLineObj = new CommandLine(commandArray.get(0));
            final int len = commandArray.size();
            if (len > 1) {
                for (int i = 1; i < len; i++) {
                    commandLineObj.addArgument(commandArray.get(i));
                }
            }
        }

        if (workingDir != null) {
            executor.setWorkingDirectory(workingDir);
        }
        if (streamHandler != null) {
            executor.setStreamHandler((ExecuteStreamHandler) streamHandler.newInstance());
        }

        SupportLogger.LOGGER.runCommand(commandLineObj.getExecutable(),
                Arrays.toString(commandLineObj.getArguments()), executor.getWorkingDirectory().getAbsolutePath());

        if (commandOkExitValues != null) {
            executor.setExitValues(commandOkExitValues);
        }

        watchdog = new ExecuteWatchdog(
                timeoutSeconds > 0 ? timeoutSeconds * 1000 : ExecuteWatchdog.INFINITE_TIMEOUT);
        executor.setWatchdog(watchdog);

        executor.setProcessDestroyer(new ShutdownHookProcessDestroyer());
        final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
        executor.execute(commandLineObj, environment, resultHandler);
        resultHandler.waitFor();

        final ExecuteException exception = resultHandler.getException();
        if (exception != null) {
            stepContext.setExitStatus(String.valueOf(resultHandler.getExitValue()));
            if (!isStopped) {
                throw exception;
            } else {
                SupportLogger.LOGGER.warn("", exception);
            }
        }
        return String.valueOf(resultHandler.getExitValue());
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method tries to destroy the process running the OS command.
     *
     * @throws Exception upon errors
     */
    @Override
    public void stop() throws Exception {
        if (watchdog != null) {
            isStopped = true;
            watchdog.destroyProcess();
        }
    }
}