ExecuteExecutor.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */

package org.waarp.gateway.ftp.exec;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.waarp.common.command.exception.Reply421Exception;
import org.waarp.common.future.WaarpFuture;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpStringUtils;
import org.waarp.openr66.protocol.configuration.Configuration;

import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

/**
 * ExecuteExecutor class. The given argument will be executed after
 * replacements.
 *
 *
 * <br>
 * The following replacement are done dynamically before the command is
 * executed:<br>
 * - #BASEPATH# is replaced by the full path for the root of FTP Directory<br>
 * - #FILE# is replaced by the current file path relative to FTP Directory (so
 * #BASEPATH##FILE# is the full
 * path of the file)<br>
 * - #USER# is replaced by the username<br>
 * - #ACCOUNT# is replaced by the account<br>
 * - #COMMAND# is replaced by the command issued for the file<br>
 * - #SPECIALID# is replaced by the FTP id of the transfer (whatever in or
 * out)<br>
 * - #UUID# is replaced by a special UUID globally unique for the transfer, in
 * general to be placed in -info
 * part (for instance ##UUID## giving #uuid#)<br>
 */
public class ExecuteExecutor extends AbstractExecutor {
  private static final String EXCEPTION = "Exception: ";
  private static final String EXEC_IN_ERROR_WITH = "\n    Exec in error with ";
  private static final String CANNOT_EXECUTE_PRE_COMMAND =
      "Cannot execute Pre command";
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(ExecuteExecutor.class);
  private static final Pattern BLANK = WaarpStringUtils.BLANK;
  private final String[] args;
  private final String arg;
  private final WaarpFuture futureCompletion;
  private final long delay;

  /**
   * @param command
   * @param delay
   * @param futureCompletion
   */
  public ExecuteExecutor(final String command, final long delay,
                         final WaarpFuture futureCompletion) {
    args = BLANK.split(command);
    arg = command;
    this.futureCompletion = futureCompletion;
    this.delay = delay;
  }

  @Override
  public final void run() throws Reply421Exception {
    // Check if the execution will be done through LocalExec daemon
    if (AbstractExecutor.useLocalExec) {
      final LocalExecClient localExecClient = new LocalExecClient();
      if (localExecClient.connect()) {
        localExecClient.runOneCommand(arg, delay, futureCompletion);
        localExecClient.disconnect();
        return;
      } // else continue
    }
    // Execution is done internally
    final File exec = new File(args[0]);
    if (exec.isAbsolute() && !exec.canExecute()) {
      logger.error("Exec command is not executable: " + args[0]);
      throw new Reply421Exception("Pre Exec command is not executable");
    }
    final CommandLine commandLine = new CommandLine(args[0]);
    for (int i = 1; i < args.length; i++) {
      commandLine.addArgument(args[i]);
    }
    final DefaultExecutor defaultExecutor = new DefaultExecutor();
    final PumpStreamHandler pumpStreamHandler =
        new PumpStreamHandler(null, null);
    defaultExecutor.setStreamHandler(pumpStreamHandler);
    final int[] correctValues = { 0, 1 };
    defaultExecutor.setExitValues(correctValues);
    ExecuteWatchdog watchdog = null;
    if (delay > 0) {
      watchdog = new ExecuteWatchdog(delay);
      defaultExecutor.setWatchdog(watchdog);
    }
    int status;
    try {
      status = defaultExecutor.execute(commandLine);//NOSONAR
    } catch (final ExecuteException e) {
      if (e.getExitValue() == -559038737) {
        // Cannot run immediately so retry once
        try {
          Thread.sleep(Configuration.RETRYINMS);
        } catch (final InterruptedException e1) {//NOSONAR
          SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
        }
        try {
          status = defaultExecutor.execute(commandLine);//NOSONAR
        } catch (final ExecuteException e2) {
          try {
            pumpStreamHandler.stop();
          } catch (final IOException ignored) {
            // nothing
          }
          logger.error("System Exception: " + e.getMessage() +
                       "\n    Exec cannot execute command " + commandLine);
          throw new Reply421Exception(CANNOT_EXECUTE_PRE_COMMAND);
        } catch (final IOException e2) {
          try {
            pumpStreamHandler.stop();
          } catch (final IOException ignored) {
            // nothing
          }
          logger.error(
              EXCEPTION + e.getMessage() + EXEC_IN_ERROR_WITH + commandLine);
          throw new Reply421Exception(CANNOT_EXECUTE_PRE_COMMAND);
        }
        logger.info(
            "System Exception: {} but finally get the command executed {}",
            e.getMessage(), commandLine);
      } else {
        try {
          pumpStreamHandler.stop();
        } catch (final IOException ignored) {
          // nothing
        }
        logger.error(
            EXCEPTION + e.getMessage() + EXEC_IN_ERROR_WITH + commandLine);
        throw new Reply421Exception(CANNOT_EXECUTE_PRE_COMMAND);
      }
    } catch (final IOException e) {
      try {
        pumpStreamHandler.stop();
      } catch (final IOException ignored) {
        // nothing
      }
      logger.error(
          EXCEPTION + e.getMessage() + EXEC_IN_ERROR_WITH + commandLine);
      throw new Reply421Exception(CANNOT_EXECUTE_PRE_COMMAND);
    }
    try {
      pumpStreamHandler.stop();
    } catch (final IOException ignored) {
      // nothing
    }
    if (watchdog != null && watchdog.killedProcess()) {
      // kill by the watchdoc (time out)
      logger.error("Exec is in Time Out");
      status = -1;
    }
    if (status == 0) {
      futureCompletion.setSuccess();
      logger.info("Exec OK with {}", commandLine);
    } else if (status == 1) {
      logger.warn("Exec in warning with {}", commandLine);
      futureCompletion.setSuccess();
    } else {
      logger.debug("Status: {}{} Exec in error with {}", status,
                   status == -1? " Timeout" : "", commandLine);
      throw new Reply421Exception("Pre command executed in error");
    }
  }
}