All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.shaft.cli.TerminalActions Maven / Gradle / Ivy

Go to download

SHAFT is a unified test automation engine. Powered by best-in-class frameworks, SHAFT provides a wizard-like syntax to drive your automation efficiently, maximize your ROI, and minimize your learning curve. Stop reinventing the wheel. Upgrade now!

There is a newer version: 8.2.20240402
Show newest version
package com.shaft.cli;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.shaft.driver.SHAFT;
import com.shaft.tools.io.ReportManager;
import com.shaft.tools.io.internal.FailureReporter;
import com.shaft.tools.io.internal.ReportHelper;
import com.shaft.tools.io.internal.ReportManagerHelper;
import org.apache.commons.lang3.SystemUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("unused")
public class TerminalActions {
    private String sshHostName = "";
    private String sshUsername;
    private String sshKeyFileFolderName;
    private String sshKeyFileName;
    private int sshPortNumber = 22;

    private String dockerName = "";
    private String dockerUsername;

    private boolean asynchronous = false;
    private boolean verbose = false;

    /**
     * This constructor is used for local terminal actions.
     */
    public TerminalActions() {
    }

    /**
     * This constructor is used for local terminal actions.
     *
     * @param asynchronous true for asynchronous execution of commands in a separate thread
     */
    public TerminalActions(boolean asynchronous) {
        this.asynchronous = asynchronous;
    }

    private TerminalActions(boolean asynchronous, boolean verbose) {
        this.asynchronous = asynchronous;
        this.verbose = verbose;
    }

    /**
     * This constructor is used for local terminal actions inside a docker.
     *
     * @param dockerName     the name of the docker instance that you want to
     *                       execute the terminal command inside
     * @param dockerUsername the username which will be used to access the docker
     *                       instance. Must have the access/privilege to execute the
     *                       terminal command
     */
    public TerminalActions(String dockerName, String dockerUsername) {
        this.dockerName = dockerName;
        this.dockerUsername = dockerUsername;
    }

    /**
     * This constructor is used for remote terminal actions.
     *
     * @param sshHostName          the IP address or host name for the remote
     *                             machine you want to execute the terminal command
     *                             on.
     * @param sshPortNumber        the port that's used for the SSH service on the
     *                             target machine. Default is 22.
     * @param sshUsername          the username which will be used to access the
     *                             target machine via ssh. Must have the
     *                             access/privilege to execute the terminal command
     * @param sshKeyFileFolderName the directory that holds the ssh key file
     *                             (usually it's somewhere in the test data of the
     *                             current project)
     * @param sshKeyFileName       the name of the ssh key file
     */
    public TerminalActions(String sshHostName, int sshPortNumber, String sshUsername, String sshKeyFileFolderName,
                           String sshKeyFileName) {
        this.sshHostName = sshHostName;
        this.sshPortNumber = sshPortNumber;
        this.sshUsername = sshUsername;
        this.sshKeyFileFolderName = sshKeyFileFolderName;
        this.sshKeyFileName = sshKeyFileName;
    }

    /**
     * This constructor is used for remote terminal actions inside a docker.
     *
     * @param sshHostName          the IP address or host name for the remote
     *                             machine you want to execute the terminal command
     *                             on.
     * @param sshPortNumber        the port that's used for the SSH service on the
     *                             target machine. Default is 22.
     * @param sshUsername          the username which will be used to access the
     *                             target machine via ssh. Must have the
     *                             access/privilege to execute the terminal command
     * @param sshKeyFileFolderName the directory that holds the ssh key file
     *                             (usually it's somewhere in the test data of the
     *                             current project)
     * @param sshKeyFileName       the name of the ssh key file
     * @param dockerName           the name of the docker instance that you want to
     *                             execute the terminal command inside
     * @param dockerUsername       the username which will be used to access the
     *                             docker instance. Must have the access/privilege
     *                             to execute the terminal command
     */
    public TerminalActions(String sshHostName, int sshPortNumber, String sshUsername, String sshKeyFileFolderName,
                           String sshKeyFileName, String dockerName, String dockerUsername) {
        this.sshHostName = sshHostName;
        this.sshPortNumber = sshPortNumber;
        this.sshUsername = sshUsername;
        this.sshKeyFileFolderName = sshKeyFileFolderName;
        this.sshKeyFileName = sshKeyFileName;
        this.dockerName = dockerName;
        this.dockerUsername = dockerUsername;
    }

    public static TerminalActions getInstance() {
        return new TerminalActions();
    }

    public static TerminalActions getInstance(boolean asynchronous) {
        return new TerminalActions(asynchronous);
    }

    public static TerminalActions getInstance(boolean asynchronous, boolean verbose) {
        return new TerminalActions(asynchronous, verbose);
    }

    private static String reportActionResult(String actionName, String testData, String log, Boolean passFailStatus, Exception... rootCauseException) {
        actionName = actionName.substring(0, 1).toUpperCase() + actionName.substring(1);
        String message;
        if (Boolean.TRUE.equals(passFailStatus)) {
            message = "Terminal Action \"" + actionName + "\" successfully performed.";
        } else {
            message = "Terminal Action \"" + actionName + "\" failed.";
        }

        List> attachments = new ArrayList<>();
        if (testData != null && testData.length() >= 500) {
            List actualValueAttachment = Arrays.asList("Terminal Action Test Data - " + actionName,
                    "Actual Value", testData);
            attachments.add(actualValueAttachment);
        } else if (testData != null && !testData.isEmpty()) {
            message = message + " With the following test data \"" + testData + "\".";
        }

        if (log != null && !log.trim().equals("")) {
            attachments.add(Arrays.asList("Terminal Action Actual Result", "Command Log", log));
        }

        if (rootCauseException != null && rootCauseException.length >= 1) {
            List actualValueAttachment = Arrays.asList("Terminal Action Exception - " + actionName,
                    "Stacktrace", ReportManagerHelper.formatStackTraceToLogEntry(rootCauseException[0]));
            attachments.add(actualValueAttachment);
        }

        if (!attachments.equals(new ArrayList<>())) {
            ReportManagerHelper.log(message, attachments);
        } else {
            ReportManager.log(message);
        }

        return message;
    }

    public boolean isRemoteTerminal() {
        return !sshHostName.equals("");
    }

    public boolean isDockerizedTerminal() {
        return !dockerName.equals("");
    }

    public String performTerminalCommands(List commands) {
        var internalCommands = commands;

        // Build long command and refactor for dockerized execution if needed
        String longCommand = buildLongCommand(internalCommands);

        if (internalCommands.size() == 1) {
            if (internalCommands.get(0).contains(" && ")) {
                internalCommands = List.of(internalCommands.get(0).split(" && "));
            } else if (internalCommands.get(0).contains(" ; ")) {
                internalCommands = List.of(internalCommands.get(0).split(" ; "));
            }
        }

        // Perform command
        List exitLogs = isRemoteTerminal() ? executeRemoteCommand(internalCommands, longCommand) : executeLocalCommand(internalCommands, longCommand);

        String log = exitLogs.get(0);
        String exitStatus = exitLogs.get(1);

        // Prepare final log message
        StringBuilder reportMessage = new StringBuilder();
        if (!sshHostName.equals("")) {
            reportMessage.append("Host Name: \"").append(sshHostName).append("\"");
            reportMessage.append(" | SSH Port Number: \"").append(sshPortNumber).append("\"");
            reportMessage.append(" | SSH Username: \"").append(sshUsername).append("\"");
        } else {
            reportMessage.append("Host Name: \"" + "localHost" + "\"");
        }
        if (sshKeyFileName != null && !sshKeyFileName.equals("")) {
            reportMessage.append(" | Key File: \"").append(sshKeyFileFolderName).append(sshKeyFileName).append("\"");
        }
        reportMessage.append(" | Command: \"").append(longCommand).append("\"");
        reportMessage.append(" | Exit Status: \"").append(exitStatus).append("\"");

        if (log != null) {
            passAction(reportMessage.toString(), log);
            return log;
        } else {
            return "";
        }
    }

    public String performTerminalCommand(String command) {
        return performTerminalCommands(Collections.singletonList(command));
    }

    public String getSshHostName() {
        return sshHostName;
    }

    public String getSshUsername() {
        return sshUsername;
    }

    public String getSshKeyFileFolderName() {
        return sshKeyFileFolderName;
    }

    public String getSshKeyFileName() {
        return sshKeyFileName;
    }

    public int getSshPortNumber() {
        return sshPortNumber;
    }

    public String getDockerName() {
        return dockerName;
    }

    public String getDockerUsername() {
        return dockerUsername;
    }

    private void passAction(String actionName, String testData, String log) {
        reportActionResult(actionName, testData, log, true);
    }

    private void passAction(String testData, String log) {
        String actionName = Thread.currentThread().getStackTrace()[2].getMethodName();
        passAction(actionName, testData, log);
    }

    private void failAction(String actionName, String testData, Exception... rootCauseException) {
        String message = reportActionResult(actionName, testData, null, false, rootCauseException);
        FailureReporter.fail(TerminalActions.class, message, rootCauseException[0]);
    }

    private void failAction(String testData, Exception... rootCauseException) {
        String actionName = Thread.currentThread().getStackTrace()[2].getMethodName();
        failAction(actionName, testData, rootCauseException);
    }

    private Session createSSHsession() {
        Session session = null;
        String testData = sshHostName + ", " + sshPortNumber + ", " + sshUsername + ", " + sshKeyFileFolderName + ", "
                + sshKeyFileName;
        try {
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            JSch jsch = new JSch();
            if (sshKeyFileName != null && !sshKeyFileName.equals("")) {
                jsch.addIdentity(FileActions.getInstance().getAbsolutePath(sshKeyFileFolderName, sshKeyFileName));
            }
            session = jsch.getSession(sshUsername, sshHostName, sshPortNumber);
            session.setConfig(config);
            session.connect();
            ReportManager.logDiscrete("Successfully created SSH Session.");
        } catch (JSchException rootCauseException) {
            failAction(testData, rootCauseException);
        }
        return session;
    }

    private String buildLongCommand(List commands) {
        StringBuilder command = new StringBuilder();
        // build long command
        for (Iterator i = commands.iterator(); i.hasNext(); ) {
            if (command.length() == 0) {
                command.append(i.next());
            } else {
                command.append(" && ").append(i.next());
            }
        }

        // refactor long command for dockerized execution
        if (isDockerizedTerminal()) {
            command.insert(0, "docker exec -u " + dockerUsername + " -i " + dockerName + " timeout "
                    + SHAFT.Properties.timeouts.dockerCommandTimeout() + " sh -c '");
            command.append("'");
        }
        return command.toString();
    }

    private List executeLocalCommand(List commands, String longCommand) {
        StringBuilder logs = new StringBuilder();
        StringBuilder exitStatuses = new StringBuilder();
        // local execution
//        ReportManager.logDiscrete("Attempting to execute the following command locally. Command: \"" + longCommand + "\"");
        boolean isWindows = SystemUtils.IS_OS_WINDOWS;
        String directory;
        LinkedList internalCommands;
        if (commands.size() > 1 && commands.get(0).startsWith("cd ")) {
            directory = commands.get(0).replace("cd ", "");
            internalCommands = new LinkedList<>(commands);
            internalCommands.remove(0);
        } else {
            directory = System.getProperty("user.dir");
            internalCommands = new LinkedList<>(commands);
        }

        ReportHelper.disableLogging();
        FileActions.getInstance().createFolder(directory.replace("\"", ""));
        ReportHelper.enableLogging();

        String finalDirectory = directory;
        internalCommands.forEach(command -> {
            command = command.contains(".bat") && !command.contains(".\\") ? ".\\" + command : command;
            ReportManager.logDiscrete("Executing: \"" + command + "\" locally.");
            try {
                ProcessBuilder pb = new ProcessBuilder();
                pb.directory(new File(finalDirectory));

                // https://stackoverflow.com/a/10954450/12912100
                if (isWindows) {
                    if (asynchronous && verbose) {
                        pb.command("powershell.exe", "Start-Process powershell.exe '-NoExit -WindowStyle Minimized \"[Console]::Title = ''SHAFT_Engine''; " + command + "\"'");
                    } else {
                        pb.command("powershell.exe", "-Command", command);
                    }
                } else {
                    pb.command("sh", "-c", command);
                }
                if (!asynchronous) {
                    pb.redirectErrorStream(true);
                    Process localProcess = pb.start();
                    // output logs
                    String line;
                    InputStreamReader isr = new InputStreamReader(localProcess.getInputStream());
                    BufferedReader rdr = new BufferedReader(isr);
                    while ((line = rdr.readLine()) != null) {
                        if (Boolean.TRUE.equals(verbose)) {
                            ReportManager.logDiscrete(line);
                        }
                        logs.append(line);
                        logs.append("\n");
                    }
                    isr = new InputStreamReader(localProcess.getErrorStream());
                    rdr = new BufferedReader(isr);
                    while ((line = rdr.readLine()) != null) {
                        if (Boolean.TRUE.equals(verbose)) {
                            ReportManager.logDiscrete(line);
                        }
                        logs.append("\n");
                        logs.append(line);
                    }
                    // Wait for the process to complete
                    localProcess.waitFor(SHAFT.Properties.timeouts.shellSessionTimeout(), TimeUnit.MINUTES);
                    // Retrieve the exit status of the executed command and destroy open sessions
                    exitStatuses.append(localProcess.exitValue());
                } else {
                    exitStatuses.append("asynchronous");
                    ScheduledExecutorService asynchronousProcessExecution = Executors.newScheduledThreadPool(1);
                    asynchronousProcessExecution.schedule(() -> {
                        try {
                            pb.start();
                            asynchronousProcessExecution.shutdown();
                        } catch (Throwable throwable) {
                            asynchronousProcessExecution.shutdownNow();
                        }
                    }, 0, TimeUnit.SECONDS);
                    if (!asynchronousProcessExecution.awaitTermination(SHAFT.Properties.timeouts.shellSessionTimeout(), TimeUnit.MINUTES)) {
                        asynchronousProcessExecution.shutdownNow();
                    }
                }
            } catch (IOException | InterruptedException exception) {
                failAction(longCommand, exception);
            }
        });
        return Arrays.asList(logs.toString().trim(), exitStatuses.toString());
    }

    private List executeRemoteCommand(List commands, String longCommand) {
        StringBuilder logs = new StringBuilder();
        StringBuilder exitStatuses = new StringBuilder();
        int sessionTimeout = Integer.parseInt(String.valueOf(SHAFT.Properties.timeouts.shellSessionTimeout() * 1000));
        // remote execution
        ReportManager.logDiscrete(
                "Attempting to perform the following command remotely. Command: \"" + longCommand + "\"");
        Session remoteSession = createSSHsession();
        if (remoteSession != null) {
            try {
                remoteSession.setTimeout(sessionTimeout);
                ChannelExec remoteChannelExecutor = (ChannelExec) remoteSession.openChannel("exec");
                remoteChannelExecutor.setCommand(longCommand);
                remoteChannelExecutor.connect();

                // Capture logs and close readers
                BufferedReader reader = new BufferedReader(new InputStreamReader(remoteChannelExecutor.getInputStream()));
                BufferedReader errorReader = new BufferedReader(new InputStreamReader(remoteChannelExecutor.getErrStream()));
                logs.append(readConsoleLogs(reader));
                logs.append(readConsoleLogs(errorReader));

                // Retrieve the exit status of the executed command and destroy open sessions
                exitStatuses.append(remoteChannelExecutor.getExitStatus());
                remoteSession.disconnect();
            } catch (JSchException | IOException exception) {
                failAction(longCommand, exception);
            }
        }
        return Arrays.asList(logs.toString(), exitStatuses.toString());
    }

    private String readConsoleLogs(BufferedReader reader) throws IOException {
        StringBuilder logBuilder = new StringBuilder();
        if (reader != null) {
            String logLine;
            while ((logLine = reader.readLine()) != null) {
                if (logBuilder.length() == 0) {
                    logBuilder.append(logLine);
                } else {
                    logBuilder.append(System.lineSeparator()).append(logLine);
                }
            }
            reader.close();
        }
        return logBuilder.toString();
    }
}