org.ballerinalang.test.context.ServerInstance.java Source code

Java tutorial

Introduction

Here is the source code for org.ballerinalang.test.context.ServerInstance.java

Source

/*
*  Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
*  WSO2 Inc. licenses this file to you under the Apache License,
*  Version 2.0 (the "License"); you may not use this file except
*  in compliance with the License.
*  You may obtain a copy of the License at
*
*  http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing,
*  software distributed under the License is distributed on an
*  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
*  KIND, either express or implied.  See the License for the
*  specific language governing permissions and limitations
*  under the License.
*/
package org.ballerinalang.test.context;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.mina.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

/**
 * This class hold the server information and manage the a server instance.
 */
public class ServerInstance implements Server {
    private static final Logger log = LoggerFactory.getLogger(ServerInstance.class);
    private String serverHome;
    private String serverDistribution;
    private String[] args;
    private Map<String, String> envProperties;
    private Process process;
    private ServerLogReader serverInfoLogReader;
    private ServerLogReader serverErrorLogReader;
    private boolean isServerRunning;
    private int httpServerPort = 9099; //Constant.DEFAULT_HTTP_PORT;
    private ConcurrentHashSet<LogLeecher> tmpLeechers = new ConcurrentHashSet<>();

    /**
     * The parent directory which the ballerina runtime will be extracted to.
     */
    private String extractDir;

    public ServerInstance(String serverDistributionPath) throws BallerinaTestException {
        this.serverDistribution = serverDistributionPath;

        initialize();
    }

    public ServerInstance(String serverDistributionPath, int serverHttpPort) throws BallerinaTestException {
        this.serverDistribution = serverDistributionPath;
        this.httpServerPort = serverHttpPort;

        initialize();
    }

    /**
     * Method to start Ballerina server given the port and bal file.
     *
     * @param port In which server starts.
     * @return ballerinaServer      Started server instance.
     * @throws BallerinaTestException If any exception is thrown when starting the ballerina server
     */
    public static ServerInstance initBallerinaServer(int port) throws BallerinaTestException {
        String serverZipPath = System.getProperty(Constant.SYSTEM_PROP_SERVER_ZIP);
        ServerInstance ballerinaServer = new ServerInstance(serverZipPath, port);

        return ballerinaServer;
    }

    /**
     * Method to start Ballerina server in default port 9092 with given bal file.
     *
     * @return ballerinaServer      Started server instance.
     * @throws BallerinaTestException If any exception is thrown when starting the ballerina server
     */
    public static ServerInstance initBallerinaServer() throws BallerinaTestException {
        int defaultPort = Constant.DEFAULT_HTTP_PORT;
        String serverZipPath = System.getProperty(Constant.SYSTEM_PROP_SERVER_ZIP);
        ServerInstance ballerinaServer = new ServerInstance(serverZipPath, defaultPort);

        return ballerinaServer;
    }

    public void startBallerinaServer(String balFile) throws BallerinaTestException {
        String[] args = { balFile };
        setArguments(args);

        startServer();
    }

    public void startBallerinaServer(String balFile, Map<String, String> envProperties)
            throws BallerinaTestException {
        String[] args = { balFile };
        setArguments(args);
        setEnvProperties(envProperties);

        startServer();
    }

    public void startBallerinaServer(String balFile, String[] args) throws BallerinaTestException {
        String[] newArgs = { balFile };
        newArgs = ArrayUtils.addAll(args, newArgs);
        setArguments(newArgs);

        startServer();
    }

    /**
     * Start the server pointing to the ballerina.conf path.
     *
     * @param balFile           ballerina file path
     * @param ballerinaConfPath ballerina.conf file path
     * @throws BallerinaTestException if an error occurs while starting the server
     */
    public void startBallerinaServerWithConfigPath(String balFile, String ballerinaConfPath)
            throws BallerinaTestException {
        String balConfigPathArg = "--config ";
        String balConfigPathVal = ballerinaConfPath;
        String[] args = { balConfigPathArg, balConfigPathVal, balFile };
        setArguments(args);

        startServer();
    }

    /**
     * Start a server instance y extracting a server zip distribution.
     *
     * @throws BallerinaTestException if server start fails
     */
    @Override
    public void startServer() throws BallerinaTestException {

        if (args == null | args.length == 0) {
            throw new IllegalArgumentException("No Argument provided for server startup.");
        }

        Utils.checkPortAvailability(httpServerPort);

        log.info("Starting server..");

        startServer(args, envProperties);

        serverInfoLogReader = new ServerLogReader("inputStream", process.getInputStream());
        tmpLeechers.forEach(leacher -> serverInfoLogReader.addLeecher(leacher));
        serverInfoLogReader.start();
        serverErrorLogReader = new ServerLogReader("errorStream", process.getErrorStream());
        serverErrorLogReader.start();
        log.info("Waiting for port " + httpServerPort + " to open");
        Utils.waitForPort(httpServerPort, 1000 * 60 * 2, false, "localhost");
        log.info("Server Started Successfully.");
        isServerRunning = true;
    }

    /**
     * Initialize the server instance with properties.
     *
     * @throws BallerinaTestException when an exception is thrown while initializing the server
     */
    private void initialize() throws BallerinaTestException {
        if (serverHome == null) {
            setUpServerHome(serverDistribution);
            log.info("Server Home " + serverHome);
            configServer();
        }
    }

    /**
     * Stop the server instance which is started by start method.
     *
     * @throws BallerinaTestException if service stop fails
     */
    @Override
    public void stopServer() throws BallerinaTestException {
        log.info("Stopping server..");
        if (process != null) {
            String pid;
            try {
                pid = getServerPID();
                if (Utils.getOSName().toLowerCase(Locale.ENGLISH).contains("windows")) {
                    Process killServer = Runtime.getRuntime().exec("TASKKILL -F /PID " + pid);
                    log.info(readProcessInputStream(killServer.getInputStream()));
                    killServer.waitFor(15, TimeUnit.SECONDS);
                    killServer.destroy();
                } else {
                    Process killServer = Runtime.getRuntime().exec("kill -9 " + pid);
                    killServer.waitFor(15, TimeUnit.SECONDS);
                    killServer.destroy();
                }
            } catch (IOException e) {
                log.error("Error getting process id for the server in port - " + httpServerPort + " error - "
                        + e.getMessage(), e);
                throw new BallerinaTestException("Error while getting the server process id", e);
            } catch (InterruptedException e) {
                log.error("Error stopping the server in port - " + httpServerPort + " error - " + e.getMessage(),
                        e);
                throw new BallerinaTestException("Error waiting for services to stop", e);
            }
            process.destroy();
            serverInfoLogReader.stop();
            serverErrorLogReader.stop();
            process = null;
            //wait until port to close
            Utils.waitForPortToClosed(httpServerPort, 30000);
            log.info("Server Stopped Successfully");

            deleteWorkDir();
        }
    }

    /**
     * Restart the server instance.
     *
     * @throws BallerinaTestException if the services could not be started
     */
    @Override
    public void restartServer() throws BallerinaTestException {
        log.info("Restarting Server...");
        stopServer();
        startServer();
        log.info("Server Restarted Successfully");
    }

    /**
     * Run main with args.
     *
     * @param args string arguments
     * @throws BallerinaTestException if the main could not be started
     */
    public void runMain(String[] args) throws BallerinaTestException {
        runMain(args, null, "run", serverHome);
    }

    /**
     * Run main with args.
     *
     * @param args string arguments
     * @throws BallerinaTestException if the main could not be started
     */
    public void runMain(String[] args, String[] envVariables, String command) throws BallerinaTestException {
        runMain(args, envVariables, command, serverHome);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void runMain(String[] args, String[] envVariables, String command, String dirPath)
            throws BallerinaTestException {
        initialize();
        File commandDir = new File(dirPath);
        Process process;

        try {
            process = executeProcess(args, envVariables, command, commandDir);
            serverInfoLogReader = new ServerLogReader("inputStream", process.getInputStream());
            tmpLeechers.forEach(leacher -> serverInfoLogReader.addLeecher(leacher));
            serverInfoLogReader.start();
            serverErrorLogReader = new ServerLogReader("errorStream", process.getErrorStream());
            serverErrorLogReader.start();

            process.waitFor();
            deleteWorkDir();
        } catch (IOException e) {
            throw new BallerinaTestException("Error executing ballerina", e);
        } catch (InterruptedException e) {
            throw new BallerinaTestException("Error waiting for execution to finish", e);
        }
    }

    /**
     * Run command with client options.
     *
     * @param args         client arguments
     * @param options      options
     * @param envVariables environment variables
     * @param command      command name
     * @param dir          working directory name
     * @throws BallerinaTestException
     */
    public void runMainWithClientOptions(String[] args, String[] options, String[] envVariables, String command,
            String dir) throws BallerinaTestException {
        initialize();
        File commandDir = new File(dir);
        Process process;

        try {
            process = executeProcess(args, envVariables, command, commandDir);
            // Wait until the options are prompted
            Thread.sleep(3000);
            writeClientOptionsToProcess(options, process);
            deleteWorkDir();
        } catch (IOException e) {
            throw new BallerinaTestException("Error executing ballerina", e);
        } catch (InterruptedException ignore) {
        }
    }

    /**
     * Execute process.
     *
     * @param args         client arguments
     * @param envVariables environment variables
     * @param command      command name
     * @param commandDir   working directory
     * @return process executed
     * @throws IOException
     */
    private Process executeProcess(String[] args, String[] envVariables, String command, File commandDir)
            throws IOException {
        String scriptName = Constant.BALLERINA_SERVER_SCRIPT_NAME;
        String[] cmdArray;
        Process process;
        if (Utils.getOSName().toLowerCase(Locale.ENGLISH).contains("windows")) {
            cmdArray = new String[] { "cmd.exe", "/c",
                    serverHome + File.separator + "bin" + File.separator + scriptName + ".bat", command };
            String[] cmdArgs = Stream.concat(Arrays.stream(cmdArray), Arrays.stream(args)).toArray(String[]::new);
            process = Runtime.getRuntime().exec(cmdArgs, envVariables, commandDir);

        } else {
            cmdArray = new String[] { "bash", serverHome + File.separator + "bin/" + scriptName, command };
            String[] cmdArgs = Stream.concat(Arrays.stream(cmdArray), Arrays.stream(args)).toArray(String[]::new);
            process = Runtime.getRuntime().exec(cmdArgs, envVariables, commandDir);
        }
        return process;
    }

    /**
     * Write client options to process.
     *
     * @param options client options
     * @param process process executed
     * @throws IOException
     */
    private void writeClientOptionsToProcess(String[] options, Process process) throws IOException {
        OutputStream stdin = process.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin));

        for (String arguments : options) {
            writer.write(arguments);
        }
        writer.flush();
        writer.close();
    }

    /**
     * Checking whether server instance is up and running.
     *
     * @return true if the server is up and running
     */
    @Override
    public boolean isRunning() {
        return isServerRunning;
    }

    /**
     * setting the list of command line argument while server startup.
     *
     * @param args list of service files
     */
    public void setArguments(String[] args) {
        this.args = args;
    }

    private void setEnvProperties(Map<String, String> envProperties) {
        this.envProperties = envProperties;
    }

    /**
     * to change the server configuration if required. This method can be overriding when initialising
     * the object of this class.
     *
     * @throws BallerinaTestException if configuring server failed
     */
    protected void configServer() throws BallerinaTestException {
    }

    /**
     * Return server home path.
     *
     * @return absolute path of the server location
     */
    public String getServerHome() {
        return serverHome;
    }

    /**
     * Return the service URL.
     *
     * @param servicePath - http url of the given service
     * @return The service URL
     */
    public String getServiceURLHttp(String servicePath) {
        return "http://localhost:" + httpServerPort + "/" + servicePath;
    }

    /**
     * Add a Leecher which is going to listen to an expected text.
     *
     * @param leecher The Leecher instance
     */
    public void addLogLeecher(LogLeecher leecher) {
        if (serverInfoLogReader == null) {
            tmpLeechers.add(leecher);
            return;
        }
        serverInfoLogReader.addLeecher(leecher);
    }

    /**
     * Unzip carbon zip file and return the carbon home. Based on the coverage configuration
     * in automation.xml.
     * This method will inject jacoco agent to the carbon server startup scripts.
     *
     * @param serverZipFile - Carbon zip file, which should be specified in test module pom
     * @throws BallerinaTestException if setting up the server fails
     */
    private void setUpServerHome(String serverZipFile) throws BallerinaTestException {
        if (process != null) { // An instance of the server is running
            return;
        }
        int indexOfZip = serverZipFile.lastIndexOf(".zip");
        if (indexOfZip == -1) {
            throw new IllegalArgumentException(serverZipFile + " is not a zip file");
        }
        String fileSeparator = (File.separator.equals("\\")) ? "\\" : "/";
        if (fileSeparator.equals("\\")) {
            serverZipFile = serverZipFile.replace("/", "\\");
        }
        String extractedCarbonDir = serverZipFile.substring(serverZipFile.lastIndexOf(fileSeparator) + 1,
                indexOfZip);
        String baseDir = (System.getProperty(Constant.SYSTEM_PROP_BASE_DIR, ".")) + File.separator + "target";

        extractDir = new File(baseDir).getAbsolutePath() + File.separator + "ballerinatmp"
                + System.currentTimeMillis();

        log.info("Extracting ballerina zip file.. ");

        try {
            Utils.extractFile(serverZipFile, extractDir);

            this.serverHome = extractDir + File.separator + extractedCarbonDir;
        } catch (IOException e) {
            throw new BallerinaTestException("Error extracting server zip file", e);
        }
    }

    /**
     * Executing the sh or bat file to start the server.
     *
     * @param args - command line arguments to pass when executing the sh or bat file
     * @param envProperties - environmental properties to be appended to the environment
     *
     * @throws BallerinaTestException if starting services failed
     */
    private void startServer(String[] args, Map<String, String> envProperties) throws BallerinaTestException {
        String scriptName = Constant.BALLERINA_SERVER_SCRIPT_NAME;
        String[] cmdArray;
        File commandDir = new File(serverHome);
        try {
            if (Utils.getOSName().toLowerCase(Locale.ENGLISH).contains("windows")) {
                commandDir = new File(serverHome + File.separator + "bin");
                cmdArray = new String[] { "cmd.exe", "/c", scriptName + ".bat", "run" };

            } else {
                cmdArray = new String[] { "bash", "bin/" + scriptName, "run" };
            }
            String[] cmdArgs = Stream.concat(Arrays.stream(cmdArray), Arrays.stream(args)).toArray(String[]::new);
            ProcessBuilder processBuilder = new ProcessBuilder(cmdArgs).directory(commandDir);
            if (envProperties != null) {
                Map<String, String> env = processBuilder.environment();
                for (Map.Entry<String, String> entry : envProperties.entrySet()) {
                    env.put(entry.getKey(), entry.getValue());
                }
            }
            process = processBuilder.start();
        } catch (IOException e) {
            throw new BallerinaTestException("Error starting services", e);
        }
    }

    /**
     * reading the server process id.
     *
     * @return process id
     * @throws BallerinaTestException if pid could not be retrieved
     */
    private String getServerPID() throws BallerinaTestException {
        String pid = null;
        if (Utils.getOSName().toLowerCase(Locale.ENGLISH).contains("windows")) {
            //reading the process id from netstat
            Process tmp;
            try {
                tmp = Runtime.getRuntime().exec("netstat -a -n -o");
            } catch (IOException e) {
                throw new BallerinaTestException("Error retrieving netstat data", e);
            }

            String outPut = readProcessInputStream(tmp.getInputStream());
            String[] lines = outPut.split("\r\n");
            for (String line : lines) {
                String[] column = line.trim().split("\\s+");
                if (column.length < 5) {
                    continue;
                }
                if (column[1].contains(":" + httpServerPort) && column[3].contains("LISTENING")) {
                    log.info(line);
                    pid = column[4];
                    break;
                }
            }
            tmp.destroy();
        } else {

            //reading the process id from ss
            Process tmp = null;
            try {
                String[] cmd = { "bash", "-c",
                        "ss -ltnp \'sport = :" + httpServerPort + "\' | grep LISTEN | awk \'{print $6}\'" };
                tmp = Runtime.getRuntime().exec(cmd);
                String outPut = readProcessInputStream(tmp.getInputStream());
                log.info("Output of the PID extraction command : " + outPut);
                /* The output of ss command is "users:(("java",pid=24522,fd=161))" in latest ss versions
                 But in older versions the output is users:(("java",23165,116))
                 TODO : Improve this OS dependent logic */
                if (outPut.contains("pid=")) {
                    pid = outPut.split("pid=")[1].split(",")[0];
                } else {
                    pid = outPut.split(",")[1];
                }

            } catch (Exception e) {
                log.warn("Error occurred while extracting the PID with ss " + e.getMessage());
                // If ss command fails trying with lsof. MacOS doesn't have ss by default
                pid = getPidWithLsof(httpServerPort);
            } finally {
                if (tmp != null) {
                    tmp.destroy();
                }
            }
        }
        log.info("Server process id in " + Utils.getOSName() + " : " + pid);
        return pid;
    }

    /**
     * Reading output from input stream.
     *
     * @param inputStream input steam of a process
     * @return the output string generated by java process
     */
    private String readProcessInputStream(InputStream inputStream) {
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        StringBuilder stringBuilder = new StringBuilder();
        try {
            inputStreamReader = new InputStreamReader(inputStream, Charset.defaultCharset());
            bufferedReader = new BufferedReader(inputStreamReader);
            int x;
            while ((x = bufferedReader.read()) != -1) {
                stringBuilder.append((char) x);
            }
        } catch (Exception ex) {
            log.error("Error reading process id", ex);
        } finally {
            if (inputStreamReader != null) {
                try {
                    inputStream.close();
                    inputStreamReader.close();
                } catch (IOException e) {
                    log.error("Error occurred while closing stream: " + e.getMessage(), e);
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    log.error("Error occurred while closing stream: " + e.getMessage(), e);
                }
            }
        }
        return stringBuilder.toString();
    }

    /**
     * Delete the working directory with the extracted ballerina instance to cleanup data after execution is complete.
     */
    private void deleteWorkDir() {
        File workDir = new File(extractDir);
        Utils.deleteFolder(workDir);
    }

    /**
     * This method returns the pid of the service which is using the provided port.
     *
     * @param httpServerPort port of the service running
     * @return the pid of the service
     * @throws BallerinaTestException if pid could not be retrieved
     */
    private String getPidWithLsof(int httpServerPort) throws BallerinaTestException {
        String pid;
        Process tmp = null;
        try {
            String[] cmd = { "bash", "-c",
                    "lsof -Pi tcp:" + httpServerPort + " | grep LISTEN | awk \'{print $2}\'" };
            tmp = Runtime.getRuntime().exec(cmd);
            pid = readProcessInputStream(tmp.getInputStream());

        } catch (Exception err) {
            throw new BallerinaTestException("Error retrieving the PID : ", err);
        } finally {
            if (tmp != null) {
                tmp.destroy();
            }
        }
        return pid;
    }
}