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

com.saucelabs.ci.sauceconnect.SauceConnectFourManager Maven / Gradle / Ivy

package com.saucelabs.ci.sauceconnect;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.logging.console.ConsoleLogger;

import java.io.*;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.logging.Level;

/**
 * Handles launching Sauce Connect v4 (binary executable).
 *
 * @author Ross Rowe
 */
public class SauceConnectFourManager extends AbstractSauceTunnelManager implements SauceTunnelManager {

    /**
     * Represents the operating system-specific Sauce Connect binary.
     */
    public enum OperatingSystem {

        OSX(OSX_DIR, OSX_FILE, UNIX_TEMP_DIR),
        WINDOWS(WINDOWS_DIR, WINDOWS_FILE, WINDOWS_TEMP_DIR, "bin" + File.separator + "sc.exe"),
        LINUX(LINUX_DIR, LINUX_FILE, UNIX_TEMP_DIR),
        LINUX32(LINUX32_DIR, LINUX32_FILE, UNIX_TEMP_DIR);
        private final String directory;
        private final String fileName;
        private final String executable;
        private final String tempDirectory;

        OperatingSystem(String directory, String fileName, String tempDirectory, String executable) {
            this.directory = directory;
            this.fileName = fileName;
            this.executable = executable;
            this.tempDirectory = tempDirectory;
        }

        OperatingSystem(String directory, String fileName, String tempDirectory) {
            this(directory, fileName, tempDirectory, "bin/sc");
        }

        public static OperatingSystem getOperatingSystem() {
            String os = System.getProperty("os.name").toLowerCase();
            if (isWindows(os)) {
                return WINDOWS;
            } else if (isMac(os)) {
                return OSX;
            } else if (isUnix(os)) {
                //check to see if we are on 64 bit
                if (is64BitLinux()) {
                    return LINUX;
                } else {
                    return LINUX32;
                }
            }
            return null;
        }

        /**
         * Executes 'uname -a', if the result of the command contains '64', then return true
         *
         * @return boolean indicating whether OS is 64-bit
         */
        private static boolean is64BitLinux() {
            BufferedReader reader = null;
            try {
                Runtime runtime = Runtime.getRuntime();
                Process process = runtime.exec("uname -a");
                process.waitFor();
                reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                StringBuilder builder = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    builder.append(line);
                }
                return builder.toString().contains("64");
            } catch (Exception e) {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e1) {
                        //ignore
                    }
                }
            }
            return false;
        }

        private static boolean isWindows(String os) {
            return (os.indexOf("win") >= 0);
        }

        private static boolean isMac(String os) {
            return (os.indexOf("mac") >= 0);
        }

        private static boolean isUnix(String os) {
            return (os.indexOf("nux") >= 0);
        }

        private String getDirectory() {
            return directory;
        }

        private String getFileName() {
            return fileName;
        }

        public String getExecutable() {
            return executable;
        }


        public String getDefaultSauceConnectLogDirectory() {
            return tempDirectory;
        }
    }


    private static final String UNIX_TEMP_DIR = "/tmp";

    private static final String WINDOWS_TEMP_DIR = System.getProperty("java.io.tmpdir");
    /**
     * Output from Sauce Connect process which indicates that it has been started.
     */
    private static final String SAUCE_CONNECT_4_STARTED = "Sauce Connect is up, you may start your tests";

    public static final String CURRENT_SC_VERSION = "4.3.14";
    public static final String SAUCE_CONNECT_4 = "sc-" + CURRENT_SC_VERSION;

    private static final String OSX_DIR = SAUCE_CONNECT_4 + "-osx";
    private static final String WINDOWS_DIR = SAUCE_CONNECT_4 + "-win32";
    private static final String LINUX_DIR = SAUCE_CONNECT_4 + "-linux";
    private static final String LINUX32_DIR = SAUCE_CONNECT_4 + "-linux32";

    private static final String BASE_FILE_NAME = SAUCE_CONNECT_4 + "-";
    private static final String LINUX_FILE = BASE_FILE_NAME + "linux.tar.gz";
    private static final String LINUX32_FILE = BASE_FILE_NAME + "linux32.tar.gz";
    private static final String OSX_FILE = BASE_FILE_NAME + "osx.zip";
    private static final String WINDOWS_FILE = BASE_FILE_NAME + "win32.zip";

    /**
     * Constructs a new instance with quiet mode disabled.
     */
    public SauceConnectFourManager() {
        this(false);
    }

    /**
     * Constructs a new instance.
     *
     * @param quietMode indicates whether Sauce Connect output should be suppressed
     */
    public SauceConnectFourManager(boolean quietMode) {
        super(quietMode);
    }

    /**
     * @param username         name of the user which launched Sauce Connect
     * @param apiKey           api key corresponding to the user
     * @param port             port which Sauce Connect should be launched on
     * @param sauceConnectJar  File which contains the Sauce Connect executables (typically the CI plugin Jar file)
     * @param options          the command line options used to launch Sauce Connect
     * @param printStream      the output stream to send log messages
     * @param sauceConnectPath if defined, Sauce Connect will be launched from the specified path and won't be extracted from the jar file
     * @return new ProcessBuilder instance which will launch Sauce Connect
     * @throws SauceConnectException thrown if an error occurs extracting the Sauce Connect binary from the CI jar file
     */
    @Override
    protected Process prepAndCreateProcess(String username, String apiKey, int port, File sauceConnectJar, String options, PrintStream printStream, String sauceConnectPath) throws SauceConnectException {

        //find zip file to extract
        try {
            String[] args;
            File unzipDirectory;
            if (sauceConnectPath == null || sauceConnectPath.equals("")) {
                File workingDirectory = null;
                if (sauceConnectJar != null && sauceConnectJar.exists()) {
                    workingDirectory = sauceConnectJar.getParentFile();
                }
                if (workingDirectory == null) {
                    workingDirectory = new File(getSauceConnectWorkingDirectory());
                }
                if (!workingDirectory.canWrite()) {
                    throw new SauceConnectException("Can't write to " + workingDirectory.getAbsolutePath() + ", please check the directory permissions");
                }
                OperatingSystem operatingSystem = OperatingSystem.getOperatingSystem();
                if (operatingSystem == null) {
                    //TODO log an error
                    return null;
                }

                unzipDirectory = getUnzipDir(workingDirectory, operatingSystem);
                File binPath = new File(unzipDirectory, operatingSystem.getExecutable());
                if (!binPath.exists()) {
                    unzipDirectory = extractZipFile(workingDirectory, operatingSystem);
                } else {
                    logMessage(printStream, binPath + " already exists, so not extracting");
                }
                //although we are setting the working directory, we need to specify the full path to the exe
                args = new String[]{binPath.getPath()};
            } else {
                File sauceConnectBinary = new File(sauceConnectPath);
                if (!sauceConnectBinary.exists()) {
                    throw new SauceConnectException(sauceConnectPath + "doesn't exist, please check the location");
                }
                unzipDirectory = sauceConnectBinary.getParentFile();
                args = new String[]{sauceConnectBinary.getPath()};
            }

            args = generateSauceConnectArgs(args, username, apiKey, port, options);

            julLogger.log(Level.INFO, "Launching Sauce Connect " + getCurrentVersion() + " " + Arrays.toString(args));
            return createProcess(args, unzipDirectory);
        } catch (IOException e) {
            throw new SauceConnectException(e);
        }
    }

    /**
     * @param args     the initial Sauce Connect command line args
     * @param username name of the user which launched Sauce Connect
     * @param apiKey   the access key for the Sauce user
     * @param port     the port that Sauce Connect should be launched on
     * @param options  command line args specified by the user
     * @return String array representing the command line args to be used to launch Sauce Connect
     */
    protected String[] generateSauceConnectArgs(String[] args, String username, String apiKey, int port, String options) {

        args = addElement(args, "-u");
        args = addElement(args, username);
        args = addElement(args, "-k");
        args = addElement(args, apiKey);
        args = addElement(args, "-P");
        args = addElement(args, String.valueOf(port));
        if (StringUtils.isNotBlank(options)) {
            args = addElement(args, options);
        }
        return args;
    }

    /**
     * @param workingDirectory the destination directory
     * @param operatingSystem  represents the current operating system
     * @return the directory containing the extracted files
     * @throws IOException thrown if an error occurs extracting the files
     */
    public File extractZipFile(File workingDirectory, OperatingSystem operatingSystem) throws IOException {

        File zipFile = extractFile(workingDirectory, operatingSystem.getFileName());
        if (operatingSystem.equals(OperatingSystem.OSX) | operatingSystem.equals(OperatingSystem.WINDOWS)) {
            unzipFile(zipFile, workingDirectory);
        } else if (operatingSystem.equals(OperatingSystem.LINUX) || operatingSystem.equals(OperatingSystem.LINUX32)) {
            untarGzFile(zipFile, workingDirectory);
        }
        return getUnzipDir(workingDirectory, operatingSystem);
    }

    private File getUnzipDir(File workingDirectory, OperatingSystem operatingSystem) {
        return new File(workingDirectory, operatingSystem.getDirectory());
    }

    /**
     * @param zipFile     the compressed file to extract
     * @param destination the destination directory
     */
    private void untarGzFile(File zipFile, File destination) {
        //remove tar file if it exists first
        File tarFile = new File(zipFile.getParentFile(), zipFile.getName().replaceAll(".gz", ""));
        if (tarFile.exists()) {
            tarFile.delete();
        }

        final TarGZipUnArchiver unArchiver = new TarGZipUnArchiver();
        unArchiver.enableLogging(new ConsoleLogger(Logger.LEVEL_DEBUG, "Sauce"));
        unArchiver.setSourceFile(zipFile);
        unArchiver.setDestDirectory(destination);
        unArchiver.extract();

    }

    /**
     * @param zipFile     the compressed file to extract
     * @param destination the destination directory
     */
    private void unzipFile(File zipFile, File destination) {
        final ZipUnArchiver unArchiver = new ZipUnArchiver();
        unArchiver.enableLogging(new ConsoleLogger(Logger.LEVEL_DEBUG, "Sauce"));
        unArchiver.setSourceFile(zipFile);
        unArchiver.setDestDirectory(destination);
        unArchiver.extract();
    }

    /**
     * @param workingDirectory the destination directory
     * @param fileName         the name of the file to extract
     * @return the directory containing the extracted files
     * @throws IOException thrown if an error occurs extracting the files
     */
    private File extractFile(File workingDirectory, String fileName) throws IOException {
        InputStream inputStream = null;
        FileOutputStream outputStream = null;

        File destination;

        try {
            inputStream = getClass().getClassLoader().getResourceAsStream(fileName);
            //copy input stream to a file
            destination = new File(workingDirectory, fileName);
            //remove file if it already exists
            if (destination.exists()) {
                destination.delete();
            }
            outputStream = new FileOutputStream(destination);
            IOUtils.copy(inputStream, outputStream);
            outputStream.flush();
        } catch (IOException e) {
            throw e;
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return destination;
    }

    /**
     * {@inheritDoc}
     */
    protected String getSauceStartedMessage() {
        return SAUCE_CONNECT_4_STARTED;
    }

    @Override
    protected String getCurrentVersion() {
        return CURRENT_SC_VERSION;
    }

    /**
     * Attempts to find the Sauce Connect log file.  If the --logfile argument has been specified, then
     * use that location, otherwise look at the operating system/tunnel identifer to determine the location.
     *
     * @param options the Sauce Connect command line options, can be null
     *
     * @return File representing the Sauce Connect log file, can be null
     */
    @Override
    public File getSauceConnectLogFile(String options) {

        //Has --logfile arg been specified
        String logfile = getLogfile(options);
        if (logfile != null) {

            File sauceConnectLogFile = new File(logfile);
            if (sauceConnectLogFile.exists()) {
                return sauceConnectLogFile;
            } else {
                return null;
            }
        }

        //otherwise, try to work out location
        String fileName = "sc.log";
        File logFileDirectory = new File(OperatingSystem.getOperatingSystem().getDefaultSauceConnectLogDirectory());

        //has --tunnel-identifer been specified?
        String tunnelIdentifier = getTunnelIdentifier(options, null);
        if (tunnelIdentifier != null) {
            fileName = MessageFormat.format("sc-{0}.log", tunnelIdentifier);
        }
        File sauceConnectLogFile = new File(logFileDirectory, fileName);
        if (!sauceConnectLogFile.exists()) {
            //try working directory
            sauceConnectLogFile = new File(getSauceConnectWorkingDirectory(), fileName);
            if (!sauceConnectLogFile.exists()) {
                return null;
            }
        }
        return sauceConnectLogFile;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy