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

hudson.plugins.android_emulator.AndroidEmulator Maven / Gradle / Ivy

The newest version!
package hudson.plugins.android_emulator;


import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import hudson.Launcher.ProcStarter;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.TaskListener;
import hudson.remoting.Callable;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.util.ArgumentListBuilder;
import hudson.util.FormValidation;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.net.Socket;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.json.JSONObject;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jvnet.hudson.plugins.port_allocator.PortAllocationManager;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

public class AndroidEmulator extends BuildWrapper implements Serializable {

    private static final long serialVersionUID = 1L;

    /** Duration by which the emulator should start being available via adb. */
    private static final int ADB_CONNECT_TIMEOUT_MS = 60 * 1000;

    /** Duration by which emulator booting should normally complete. */
    private static final int BOOT_COMPLETE_TIMEOUT_MS = 120 * 1000;

    private DescriptorImpl descriptor;

    private final String avdName;
    private final String osVersion;
    private final String screenDensity;
    private final String screenResolution;
    private final String deviceLocale;
    private final String sdCardSize;
    private final boolean wipeData;
    private final boolean showWindow;
    private final String commandLineOptions;
    private final int startupDelay;

    @DataBoundConstructor
    public AndroidEmulator(String avdName, String osVersion, String screenDensity,
            String screenResolution, String deviceLocale, String sdCardSize, boolean wipeData,
            boolean showWindow, String commandLineOptions, int startupDelay) {
        this.avdName = avdName;
        this.osVersion = osVersion;
        this.screenDensity = screenDensity;
        this.screenResolution = screenResolution;
        this.deviceLocale = deviceLocale;
        this.sdCardSize = sdCardSize;
        this.wipeData = wipeData;
        this.showWindow = showWindow;
        this.commandLineOptions = commandLineOptions;
        this.startupDelay = Math.abs(startupDelay);
    }

    public boolean getUseNamedEmulator() {
        return avdName != null;
    }

    public String getOsVersion() {
        return osVersion;
    }

    public String getAvdName() {
        return avdName;
    }

    public String getScreenDensity() {
        return screenDensity;
    }

    public String getScreenResolution() {
        return screenResolution;
    }

    public String getDeviceLocale() {
        return deviceLocale;
    }

    public String getSdCardSize() {
        return sdCardSize;
    }

    public boolean shouldWipeData() {
        return wipeData;
    }

    public boolean shouldShowWindow() {
        return showWindow;
    }

    public String getCommandLineOptions() {
        return commandLineOptions;
    }

    public int getStartupDelay() {
        return startupDelay;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener)
            throws IOException, InterruptedException {
        final PrintStream logger = listener.getLogger();
        if (descriptor == null) {
            descriptor = Hudson.getInstance().getDescriptorByType(DescriptorImpl.class);
        }

        // Substitute environment and build variables into config
        final EnvVars localVars = Computer.currentComputer().getEnvironment();
        final EnvVars envVars = new EnvVars(localVars);
        envVars.putAll(build.getEnvironment(listener));
        final Map buildVars = build.getBuildVariables();

        // Device properties
        String avdName = expandVariables(envVars, buildVars, this.avdName);
        String osVersion = expandVariables(envVars, buildVars, this.osVersion);
        String screenDensity = expandVariables(envVars, buildVars, this.screenDensity);
        String screenResolution = expandVariables(envVars, buildVars, this.screenResolution);
        String deviceLocale = expandVariables(envVars, buildVars, this.deviceLocale);
        String sdCardSize = expandVariables(envVars, buildVars, this.sdCardSize);

        // Emulator properties
        String commandLineOptions = expandVariables(envVars, buildVars, this.commandLineOptions);

        // SDK location
        String androidHome = expandVariables(envVars, buildVars, descriptor.androidHome);
        androidHome = discoverAndroidHome(launcher, localVars, androidHome);

        // Despite the nice inline checks and warnings when the user is editing the config,
        // these are not binding, so the user may have saved invalid configuration.
        // Here we check whether or not it's worth proceeding based on the saved values.
        String configError = isConfigValid(avdName, osVersion, screenDensity, screenResolution,
                deviceLocale, sdCardSize);
        if (configError != null) {
            log(logger, Messages.ERROR_MISCONFIGURED(configError));
            build.setResult(Result.NOT_BUILT);
            return null;
        }

        // Confirm that tools are available on PATH
        AndroidSdk androidSdk = new AndroidSdk(androidHome);
        if (androidHome == null) {
            if (!validateAndroidToolsInPath(launcher)) {
                log(logger, Messages.SDK_TOOLS_NOT_FOUND());
                build.setResult(Result.NOT_BUILT);
                return null;
            }
        } else {
            // Determine SDK type, if tools aren't on the PATH
            boolean usesPlatformTools = sdkUsesPlatformTools(launcher, androidSdk);
            androidSdk.setUsesPlatformTools(usesPlatformTools);
        }

        // Ok, everything looks good.. let's go
        String displayHome = androidSdk.hasKnownRoot() ? androidSdk.getSdkRoot() : Messages.USING_PATH();
        log(logger, Messages.USING_SDK(displayHome));
        EmulatorConfig emuConfig = EmulatorConfig.create(avdName, osVersion, screenDensity,
                screenResolution, deviceLocale, sdCardSize, wipeData, showWindow, commandLineOptions);

        return doSetUp(build, launcher, listener, androidSdk, emuConfig);
    }

    private Environment doSetUp(final AbstractBuild build, final Launcher launcher,
            final BuildListener listener, final AndroidSdk androidSdk, final EmulatorConfig emuConfig)
                throws IOException, InterruptedException {
        final PrintStream logger = listener.getLogger();
        final boolean isUnix = launcher.isUnix();

        // First ensure that emulator exists
        final Computer computer = Computer.currentComputer();
        final boolean emulatorAlreadyExists;
        try {
            Callable task = emuConfig.getEmulatorCreationTask(androidSdk, isUnix, listener);
            emulatorAlreadyExists = launcher.getChannel().call(task);
        } catch (EmulatorDiscoveryException ex) {
            log(logger, Messages.CANNOT_START_EMULATOR(ex.getMessage()));
            build.setResult(Result.FAILURE);
            return null;
        } catch (AndroidEmulatorException ex) {
            log(logger, Messages.COULD_NOT_CREATE_EMULATOR(ex.getMessage()));
            build.setResult(Result.NOT_BUILT);
            return null;
        }

        // Delay start up by the configured amount of time
        final int delaySecs = getStartupDelay();
        if (delaySecs > 0) {
            log(logger, Messages.DELAYING_START_UP(delaySecs));
            Thread.sleep(delaySecs * 1000);
        }

        // Use the Port Allocator plugin to reserve the two ports we need
        final PortAllocationManager portAllocator = PortAllocationManager.getManager(computer);
        final int userPort = portAllocator.allocateRandom(build, 0);
        final int adbPort = portAllocator.allocateRandom(build, 0);

        // Compile complete command for starting emulator
        final String avdArgs = emuConfig.getCommandArguments();
        String emulatorArgs = String.format("-ports %s,%s %s", userPort, adbPort, avdArgs);
        ArgumentListBuilder emulatorCmd = Utils.getToolCommand(androidSdk, isUnix, Tool.EMULATOR, emulatorArgs);

        // Start emulator process
        log(logger, Messages.STARTING_EMULATOR());
        if (emulatorAlreadyExists && emuConfig.shouldWipeData()) {
            log(logger, Messages.ERASING_EXISTING_EMULATOR_DATA());
        }
        final long bootTime = System.currentTimeMillis();
        final EnvVars buildEnvironment = build.getEnvironment(TaskListener.NULL);
        final ProcStarter procStarter = launcher.launch().stdout(logger).stderr(logger);
        final Proc emulatorProcess = procStarter.envs(buildEnvironment).cmds(emulatorCmd).start();

        // Wait for TCP socket to become available
        boolean socket = waitForSocket(launcher, adbPort, ADB_CONNECT_TIMEOUT_MS);
        if (!socket || !emulatorProcess.isAlive()) {
            log(logger, Messages.EMULATOR_DID_NOT_START());
            build.setResult(Result.NOT_BUILT);
            cleanUp(logger, portAllocator, emulatorProcess, adbPort, userPort);
            return null;
        }

        // Notify adb of our existence
        final String adbConnectArgs = "connect localhost:"+ adbPort;
        ArgumentListBuilder adbConnectCmd = Utils.getToolCommand(androidSdk, isUnix, Tool.ADB, adbConnectArgs);
        int result = procStarter.cmds(adbConnectCmd).stdout(new NullOutputStream()).start().join();
        if (result != 0) { // adb currently only ever returns 0!
            log(logger, Messages.CANNOT_CONNECT_TO_EMULATOR());
            build.setResult(Result.NOT_BUILT);
            cleanUp(logger, portAllocator, emulatorProcess, adbPort, userPort);
            return null;
        }

        // Start dumping logs to disk
        final File artifactsDir = build.getArtifactsDir();
        final FilePath logcatFile = build.getWorkspace().createTempFile("logcat_", ".log");
        final OutputStream logcatStream = logcatFile.write();
        final String logcatArgs = "-s localhost:"+ adbPort +" logcat -v time";
        ArgumentListBuilder logcatCmd = Utils.getToolCommand(androidSdk, isUnix, Tool.ADB, logcatArgs);
        final Proc logWriter = procStarter.cmds(logcatCmd).stdout(logcatStream).stderr(new NullOutputStream()).start();

        // Monitor device for boot completion signal
        log(logger, Messages.WAITING_FOR_BOOT_COMPLETION());
        int bootTimeout = BOOT_COMPLETE_TIMEOUT_MS;
        if (!emulatorAlreadyExists || emuConfig.shouldWipeData()) {
            bootTimeout *= 4;
        }
        boolean bootSucceeded = waitForBootCompletion(logger, launcher, androidSdk, adbPort, bootTimeout);
        if (!bootSucceeded) {
            log(logger, Messages.BOOT_COMPLETION_TIMED_OUT(bootTimeout / 1000));
            build.setResult(Result.NOT_BUILT);
            cleanUp(logger, launcher, androidSdk, portAllocator, emulatorProcess,
                    adbPort, userPort, logWriter, logcatFile, logcatStream, artifactsDir);
            return null;
        }

        // Unlock emulator by pressing the Menu key once, if required.
        // Upon first boot (and when the data is wiped) the emulator is already unlocked
        if (emulatorAlreadyExists && !wipeData) {
            // Even if the emulator has started, we generally need to wait longer before the lock
            // screen is up and ready to accept key presses.
            // The delay here is a function of boot time, i.e. relative to the slowness of the host
            final long bootDuration = System.currentTimeMillis() - bootTime;
            Thread.sleep(bootDuration / 4);

            log(logger, Messages.UNLOCKING_SCREEN());
            final String keyEventArgs = String.format("-s localhost:%d shell input keyevent %%d", adbPort);
            final String menuArgs = String.format(keyEventArgs, 82);
            ArgumentListBuilder menuCmd = Utils.getToolCommand(androidSdk, isUnix, Tool.ADB, menuArgs);
            procStarter.cmds(menuCmd).start().join();

            // If a named emulator already existed, it may not have been booted yet, so the screen
            // wouldn't be locked.  In this case, after pressing Menu, we press Back to compensate
            if (emuConfig.isNamedEmulator()) {
                final String backArgs = String.format(keyEventArgs, 4);
                ArgumentListBuilder backCmd = Utils.getToolCommand(androidSdk, isUnix, Tool.ADB, backArgs);
                procStarter.cmds(backCmd).start().join();
            }
        }

        // Done!
        final long bootCompleteTime = System.currentTimeMillis();
        log(logger, Messages.EMULATOR_IS_READY((bootCompleteTime - bootTime) / 1000));

        // Return wrapped environment
        return new Environment() {
            @Override
            public void buildEnvVars(Map env) {
                env.put("ANDROID_AVD_DEVICE", "localhost:"+ adbPort);
                env.put("ANDROID_AVD_ADB_PORT", Integer.toString(adbPort));
                env.put("ANDROID_AVD_USER_PORT", Integer.toString(userPort));
                env.put("ANDROID_AVD_NAME", emuConfig.getAvdName());
                if (!emuConfig.isNamedEmulator()) {
                    env.put("ANDROID_AVD_OS", emuConfig.getOsVersion().toString());
                    env.put("ANDROID_AVD_DENSITY", emuConfig.getScreenDensity().toString());
                    env.put("ANDROID_AVD_RESOLUTION", emuConfig.getScreenResolution().toString());
                    env.put("ANDROID_AVD_SKIN", emuConfig.getScreenResolution().getSkinName());
                    env.put("ANDROID_AVD_LOCALE", emuConfig.getDeviceLocale());
                }
            }

            @Override
            @SuppressWarnings("unchecked")
            public boolean tearDown(AbstractBuild build, BuildListener listener)
                    throws IOException, InterruptedException {
                cleanUp(logger, launcher, androidSdk, portAllocator, emulatorProcess,
                        adbPort, userPort, logWriter, logcatFile, logcatStream, artifactsDir);

                return true;
            }
        };
    }

    /** Helper method for writing to the build log in a consistent manner. */
    synchronized static void log(final PrintStream logger, final String message) {
        log(logger, message, false);
    }

    /** Helper method for writing to the build log in a consistent manner. */
    synchronized static void log(final PrintStream logger, String message, boolean indent) {
        if (indent) {
            message = '\t' + message.replace("\n", "\n\t");
        } else {
            logger.print("[android] ");
        }
        logger.println(message);
    }

    /**
     * Called when this wrapper needs to exit, so we need to clean up some processes etc.
     *
     * @param logger The build logger.
     * @param portAllocator The port allocator used.
     * @param emulatorProcess The Android emulator process.
     * @param adbPort The ADB port used by the emulator.
     * @param userPort The user port used by the emulator.
     */
    private void cleanUp(PrintStream logger, PortAllocationManager portAllocator,
            Proc emulatorProcess, int adbPort, int userPort) throws IOException, InterruptedException {
        cleanUp(logger, null, null, portAllocator, emulatorProcess, adbPort, userPort, null, null, null, null);
    }

    /**
     * Called when this wrapper needs to exit, so we need to clean up some processes etc.
     *
     * @param logger The build logger.
     * @param launcher The launcher for the remote node.
     * @param androidSdk The Android SDK being used.
     * @param portAllocator The port allocator used.
     * @param emulatorProcess The Android emulator process.
     * @param adbPort The ADB port used by the emulator.
     * @param userPort The user port used by the emulator.
     * @param logcatProcess The adb logcat process.
     * @param logcatFile The file the logcat output is being written to.
     * @param logcatStream The stream the logcat output is being written to.
     * @param artifactsDir The directory where build artifacts should go.
     */
    private void cleanUp(PrintStream logger, Launcher launcher, AndroidSdk androidSdk,
            PortAllocationManager portAllocator, Proc emulatorProcess, int adbPort,
            int userPort, Proc logcatProcess, FilePath logcatFile,
            OutputStream logcatStream, File artifactsDir)
                throws IOException, InterruptedException {
        // FIXME: Sometimes on Windows neither the emulator.exe nor the adb.exe processes die.
        //        Launcher.kill(EnvVars) does not appear to help either.
        //        This is (a) inconsistent; (b) very annoying.

        // Disconnect emulator from adb, if it's running
        if (launcher != null) {
            final String args = "disconnect localhost:"+ adbPort;
            ArgumentListBuilder adbDisconnectCmd = Utils.getToolCommand(androidSdk, launcher.isUnix(), Tool.ADB, args);
            final ProcStarter procStarter = launcher.launch().stderr(logger);
            procStarter.cmds(adbDisconnectCmd).stdout(new NullOutputStream()).start().join();
        }

        // Stop emulator process
        log(logger, Messages.STOPPING_EMULATOR());
        if (logcatProcess != null) {
            sendEmulatorCommand(launcher, logger, userPort, "kill");
            if (logcatProcess.isAlive()) {
                logcatProcess.kill();
            }
            logcatStream.close();

            // Archive the logs
            if (logcatFile.length() != 0) {
                log(logger, Messages.ARCHIVING_LOG());
                logcatFile.copyTo(new FilePath(artifactsDir).child("logcat.txt"));
            }
            logcatFile.delete();
        }

        // Ensure the process is dead and free up the TCP ports
        if (emulatorProcess.isAlive()) {
            emulatorProcess.kill();
        }
        portAllocator.free(adbPort);
        portAllocator.free(userPort);
    }

    /**
     * Expands the variable in the given string to its value in the environment variables available
     * to this build.  The Hudson-specific build variables for this build are then substituted.
     *
     * @param envVars  Map of the environment variables.
     * @param buildVars  Map of the build-specific variables.
     * @param token  The token which may or may not contain variables in the format ${foo}.
     * @return  The given token, with applicable variable expansions done.
     */
    private String expandVariables(EnvVars envVars, Map buildVars,
            String token) {
        String result = Util.fixEmptyAndTrim(token);
        if (result != null) {
            result = Util.replaceMacro(Util.replaceMacro(result, envVars), buildVars);
        }
        return result;
    }

    /**
     * Validates this instance's configuration.
     *
     * @return A human-readable error message, or null if the config is valid.
     */
    private String isConfigValid(String avdName, String osVersion, String screenDensity,
            String screenResolution, String deviceLocale, String sdCardSize) {
        if (getUseNamedEmulator()) {
            ValidationResult result = descriptor.doCheckAvdName(avdName, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
        } else {
            ValidationResult result = descriptor.doCheckOsVersion(osVersion, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
            result = descriptor.doCheckScreenDensity(screenDensity, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
            result = descriptor.doCheckScreenResolution(screenResolution, null, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
            result = descriptor.doCheckDeviceLocale(deviceLocale, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
            result = descriptor.doCheckSdCardSize(sdCardSize, false);
            if (result.isFatal()) {
                return result.getMessage();
            }
        }

        return null;
    }

    /**
     * Tries to validate the given Android SDK root directory; otherwise tries to
     * locate a copy of the SDK by checking for common environment variables.
     *
     * @param launcher The launcher for the remote node.
     * @param envVars Environment variables for the build.
     * @param androidHome The (variable-expanded) SDK root given in global config.
     * @return Either a discovered SDK path or, if all else fails, the given androidHome value.
     */
    private String discoverAndroidHome(final Launcher launcher, final EnvVars envVars,
            final String androidHome) {
        Callable task = new Callable() {
            public String call() throws InterruptedException {
                // Verify existence of provided value
                if (validateHomeDir(androidHome)) {
                    return androidHome;
                }

                // Check for common environment variables
                String[] keys = { "ANDROID_SDK_ROOT", "ANDROID_SDK_HOME",
                                  "ANDROID_HOME", "ANDROID_SDK" };
                for (String key : keys) {
                    String home = envVars.get(key);
                    if (validateHomeDir(home)) {
                        return home;
                    }
                }

                // Give up
                return null;
            }

            private boolean validateHomeDir(String dir) {
                if (Util.fixEmptyAndTrim(dir) == null) {
                    return false;
                }
                return !descriptor.doCheckAndroidHome(new File(dir), false).isFatal();
            }

            private static final long serialVersionUID = 1L;
        };

        String result = androidHome;
        try {
            result = launcher.getChannel().call(task);
        } catch (InterruptedException e) {
            // Ignore; will return default value
        } catch (IOException e) {
            // Ignore; will return default value
        }
        return result;
    }

    /**
     * Validates whether the required SDK tools can be found on the PATH.
     *
     * @param launcher The launcher for the remote node.
     * @return {@code true} if all the required tools are available.
     */
    private boolean validateAndroidToolsInPath(Launcher launcher) {
        final boolean isUnix = launcher.isUnix();

        Callable task = new Callable() {
            public Boolean call() throws IOException {
                // Get list of required tools when working from PATH
                Tool[] tools = { Tool.ADB, Tool.EMULATOR };

                // Examine each directory specified by the PATH environment variable.
                int toolCount = 0;
                String[] paths = System.getenv("PATH").split(File.pathSeparator);
                for (String path : paths) {
                    File toolsDirectory = new File(path);
                    if (toolsDirectory.isDirectory()) {
                        for (Tool tool : tools) {
                            String executable = tool.getExecutable(isUnix);
                            if (new File(toolsDirectory, executable).exists()) {
                                toolCount++;
                            }
                        }
                    }
                    if (toolCount == tools.length) {
                        return true;
                    }
                }

                return false;
            }
            private static final long serialVersionUID = 1L;
        };

        try {
            return launcher.getChannel().call(task);
        } catch (IOException e) {
            // Ignore
        } catch (InterruptedException e) {
            // Ignore
        }
        return false;
    }

    /**
     * Determines where platform tools are stored for the given SDK instance.
     *
     * @param launcher The launcher for the remote node.
     * @param androidHome The SDK we are going to use.
     */
    private boolean sdkUsesPlatformTools(Launcher launcher, final AndroidSdk androidSdk) {
        Callable task = new Callable() {
            public Boolean call() throws IOException {
                File toolsDirectory = new File(androidSdk.getSdkRoot(), "platform-tools");
                return toolsDirectory.isDirectory();
            }
            private static final long serialVersionUID = 1L;
        };

        try {
            return launcher.getChannel().call(task);
        } catch (IOException e) {
            // Ignore
        } catch (InterruptedException e) {
            // Ignore
        }

        return false;
    }

    /**
     * Waits for a socket on the remote machine's localhost to become available, or times out.
     *
     * @param launcher The launcher for the remote node.
     * @param port The port to try and connect to.
     * @param timeout How long to keep trying (in milliseconds) before giving up.
     * @return true if the socket was available, false if we timed-out.
     */
    private boolean waitForSocket(Launcher launcher, int port, int timeout) {
        try {
            LocalPortOpenTask task = new LocalPortOpenTask(port, timeout);
            return launcher.getChannel().call(task);
        } catch (InterruptedException ex) {
            // Ignore
        } catch (IOException e) {
            // Ignore
        }

        return false;
    }

    /**
     * Checks whether the emulator running on the given port has finished booting yet, or times out.
     *
     * @param logger The build logger.
     * @param launcher The launcher for the remote node.
     * @param androidHome The Android SDK root.
     * @param port The emulator's ADB port.
     * @param timeout How long to keep trying (in milliseconds) before giving up.
     * @return true if the emulator has booted, false if we timed-out.
     */
    private boolean waitForBootCompletion(final PrintStream logger, final Launcher launcher,
            final AndroidSdk androidSdk, final int port, final int timeout) {
        long start = System.currentTimeMillis();
        int sleep = timeout / (int) Math.sqrt(timeout / 1000);

        final String serialNo = "localhost:"+ port;
        final String args = "-s "+ serialNo +" shell getprop dev.bootcomplete";
        ArgumentListBuilder cmd = Utils.getToolCommand(androidSdk, launcher.isUnix(), Tool.ADB, args);

        try {
            while (System.currentTimeMillis() < start + timeout) {
                ByteArrayOutputStream stream = new ByteArrayOutputStream(4);

                // Run "getprop"
                launcher.launch().cmds(cmd).stdout(stream).start().join();

                // Check output
                String result = stream.toString().trim();
                if (result.equals("1")) {
                    return true;
                }

                // Otherwise continue...
                Thread.sleep(sleep);
            }
        } catch (InterruptedException ex) {
            log(logger, Messages.INTERRUPTED_DURING_BOOT_COMPLETION());
        } catch (IOException ex) {
            log(logger, Messages.COULD_NOT_CHECK_BOOT_COMPLETION());
            ex.printStackTrace(logger);
        }

        return false;
    }

    /**
     * Sends a user command to the running emulator via its telnet interface.
     *
     * @param logger The build logger.
     * @param launcher The launcher for the remote node.
     * @param port The emulator's telnet port.
     * @param command The command to execute on the emulator's telnet interface.
     * @return Whether sending the command succeeded.
     */
    private boolean sendEmulatorCommand(final Launcher launcher, final PrintStream logger,
            final int port, final String command) {
        Callable task = new Callable() {
            @Override
            public Boolean call() throws IOException {
                Socket socket = null;
                BufferedReader in = null;
                PrintWriter out = null;
                try {
                    socket = new Socket("127.0.0.1", port);
                    out = new PrintWriter(socket.getOutputStream(), true);
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    if (in.readLine() == null) {
                        return false;
                    }

                    out.write(command);
                    out.write("\r\n");
                } finally {
                    try {
                        out.close();
                        in.close();
                        socket.close();
                    } catch (Exception e) {
                        // Buh
                    }
                }

                return true;
            }

            private static final long serialVersionUID = 1L;
        };

        boolean result = false;
        try {
            result = launcher.getChannel().call(task);
        } catch (Exception e) {
            log(logger, String.format("Failed to execute emulator command '%s': %s", command, e));
        }

        return result;
    }

    @Extension(ordinal=-100) // Negative ordinal makes us execute after other wrappers (i.e. Xvnc)
    public static final class DescriptorImpl extends BuildWrapperDescriptor implements Serializable {

        private static final long serialVersionUID = 1L;

        // From hudson.Util.VARIABLE
        private static final String VARIABLE_REGEX = "\\$([A-Za-z0-9_]+|\\{[A-Za-z0-9_]+\\}|\\$)";

        /**
         * The Android SDK home directory.  Can include variables, e.g. ${ANDROID_HOME}.
         * 

If null, we will just assume the required commands are on the PATH.

*/ public String androidHome; public DescriptorImpl() { super(AndroidEmulator.class); load(); } @Override public String getDisplayName() { return Messages.JOB_DESCRIPTION(); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { req.bindParameters(this, "android-emulator."); save(); return true; } @Override public BuildWrapper newInstance(StaplerRequest req, JSONObject formData) throws FormException { String avdName = null; String osVersion = null; String screenDensity = null; String screenResolution = null; String deviceLocale = null; String sdCardSize = null; boolean wipeData = false; boolean showWindow = true; String commandLineOptions = null; int startupDelay = 0; JSONObject emulatorData = formData.getJSONObject("useNamed"); String useNamedValue = emulatorData.getString("value"); if (Boolean.parseBoolean(useNamedValue)) { avdName = Util.fixEmptyAndTrim(emulatorData.getString("avdName")); } else { osVersion = Util.fixEmptyAndTrim(emulatorData.getString("osVersion")); screenDensity = Util.fixEmptyAndTrim(emulatorData.getString("screenDensity")); screenResolution = Util.fixEmptyAndTrim(emulatorData.getString("screenResolution")); deviceLocale = Util.fixEmptyAndTrim(emulatorData.getString("deviceLocale")); sdCardSize = Util.fixEmptyAndTrim(emulatorData.getString("sdCardSize")); if (sdCardSize != null) { sdCardSize = sdCardSize.toUpperCase().replaceAll("[ B]", ""); } } wipeData = formData.getBoolean("wipeData"); showWindow = formData.getBoolean("showWindow"); commandLineOptions = formData.getString("commandLineOptions"); try { startupDelay = Integer.parseInt(formData.getString("startupDelay")); } catch (NumberFormatException e) {} return new AndroidEmulator(avdName, osVersion, screenDensity, screenResolution, deviceLocale, sdCardSize, wipeData, showWindow, commandLineOptions, startupDelay); } @Override public String getHelpFile() { return "/plugin/android-emulator/help-buildConfig.html"; } @Override public boolean isApplicable(AbstractProject item) { return true; } /** Used in config.jelly: Lists the OS versions available. */ public AndroidPlatform[] getAndroidVersions() { return AndroidPlatform.PRESETS; } /** Used in config.jelly: Lists the screen densities available. */ public ScreenDensity[] getDeviceDensities() { return ScreenDensity.PRESETS; } /** Used in config.jelly: Lists the screen resolutions available. */ public ScreenResolution[] getDeviceResolutions() { return ScreenResolution.PRESETS; } /** Used in config.jelly: Lists the locales available. */ public String[] getEmulatorLocales() { return Constants.EMULATOR_LOCALES; } public FormValidation doCheckAvdName(@QueryParameter String value) { return doCheckAvdName(value, true).getFormValidation(); } private ValidationResult doCheckAvdName(String avdName, boolean allowVariables) { if (avdName == null || avdName.equals("")) { return ValidationResult.error(Messages.AVD_NAME_REQUIRED()); } String regex = Constants.REGEX_AVD_NAME; if (allowVariables) { regex = "(("+ Constants.REGEX_AVD_NAME +")*("+ VARIABLE_REGEX +")*)+"; } if (!avdName.matches(regex)) { return ValidationResult.error(Messages.INVALID_AVD_NAME()); } return ValidationResult.ok(); } public FormValidation doCheckOsVersion(@QueryParameter String value) { return doCheckOsVersion(value, true).getFormValidation(); } private ValidationResult doCheckOsVersion(String osVersion, boolean allowVariables) { if (osVersion == null || osVersion.equals("")) { return ValidationResult.error(Messages.OS_VERSION_REQUIRED()); } if (!allowVariables && osVersion.matches(VARIABLE_REGEX)) { return ValidationResult.error(Messages.INVALID_OS_VERSION()); } return ValidationResult.ok(); } public FormValidation doCheckScreenDensity(@QueryParameter String value) { return doCheckScreenDensity(value, true).getFormValidation(); } private ValidationResult doCheckScreenDensity(String density, boolean allowVariables) { if (density == null || density.equals("")) { return ValidationResult.error(Messages.SCREEN_DENSITY_REQUIRED()); } String regex = Constants.REGEX_SCREEN_DENSITY; if (allowVariables) { regex += "|"+ VARIABLE_REGEX; } if (!density.matches(regex)) { return ValidationResult.error(Messages.SCREEN_DENSITY_NOT_NUMERIC()); } return ValidationResult.ok(); } public FormValidation doCheckScreenResolution(@QueryParameter String value, @QueryParameter String density) { return doCheckScreenResolution(value, density, true).getFormValidation(); } private ValidationResult doCheckScreenResolution(String resolution, String density, boolean allowVariables) { if (resolution == null || resolution.equals("")) { return ValidationResult.error(Messages.SCREEN_RESOLUTION_REQUIRED()); } String regex = Constants.REGEX_SCREEN_RESOLUTION_FULL; if (allowVariables) { regex += "|"+ VARIABLE_REGEX; } if (!resolution.matches(regex)) { return ValidationResult.error(Messages.INVALID_RESOLUTION_FORMAT()); } // Check for shenanigans ScreenResolution resolutionValue = ScreenResolution.valueOf(resolution); ScreenDensity densityValue = ScreenDensity.valueOf(density); if (resolutionValue != null && densityValue != null && !resolutionValue.isCustomResolution() && !densityValue.isCustomDensity()) { boolean densityFound = false; for (ScreenDensity okDensity : resolutionValue.getApplicableDensities()) { if (okDensity.equals(densityValue)) { densityFound = true; break; } } if (!densityFound) { return ValidationResult.warning(Messages.SUSPECT_RESOLUTION(resolution, densityValue)); } } return ValidationResult.ok(); } public FormValidation doCheckDeviceLocale(@QueryParameter String value) { return doCheckDeviceLocale(value, true).getFormValidation(); } private ValidationResult doCheckDeviceLocale(String locale, boolean allowVariables) { if (locale == null || locale.equals("")) { return ValidationResult.warning(Messages.DEFAULT_LOCALE_WARNING(Constants.DEFAULT_LOCALE)); } String regex = Constants.REGEX_LOCALE; if (allowVariables) { regex += "|"+ VARIABLE_REGEX; } if (!locale.matches(regex)) { return ValidationResult.error(Messages.LOCALE_FORMAT_WARNING()); } return ValidationResult.ok(); } public FormValidation doCheckSdCardSize(@QueryParameter String value) { return doCheckSdCardSize(value, true).getFormValidation(); } private ValidationResult doCheckSdCardSize(String sdCardSize, boolean allowVariables) { if (sdCardSize == null || sdCardSize.equals("")) { // No value, no SD card is created return ValidationResult.ok(); } String regex = Constants.REGEX_SD_CARD_SIZE; if (allowVariables) { regex += "|"+ VARIABLE_REGEX; } if (!sdCardSize.matches(regex)) { return ValidationResult.error(Messages.INVALID_SD_CARD_SIZE()); } // Validate size of SD card: New AVD requires at least 9MB Matcher matcher = Pattern.compile(Constants.REGEX_SD_CARD_SIZE).matcher(sdCardSize); if (matcher.matches()) { long bytes = Long.parseLong(matcher.group(1)); if (matcher.group(2).toUpperCase().equals("M")) { // Convert to KB bytes *= 1024; } bytes *= 1024L; if (bytes < (9 * 1024 * 1024)) { return ValidationResult.error(Messages.SD_CARD_SIZE_TOO_SMALL()); } } return ValidationResult.ok(); } public FormValidation doCheckAndroidHome(@QueryParameter File value) { return doCheckAndroidHome(value, true).getFormValidation(); } private ValidationResult doCheckAndroidHome(File sdkRoot, boolean fromWebConfig) { // This can be used to check the existence of a file on the server, so needs to be protected if (fromWebConfig && !Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) { return ValidationResult.ok(); } // Check the utter basics if (fromWebConfig && (sdkRoot == null || sdkRoot.getPath().equals(""))) { return ValidationResult.ok(); } if (!sdkRoot.isDirectory()) { if (fromWebConfig && sdkRoot.getPath().matches(".*("+ VARIABLE_REGEX +").*")) { return ValidationResult.ok(); } return ValidationResult.error(Messages.INVALID_DIRECTORY()); } // We'll be using items from the tools and platforms directories. // Ignore that "platform-tools" may also be required for newer SDKs, // as we'll check for the presence of the individual tools in a moment final String[] sdkDirectories = { "tools", "platforms" }; for (String dirName : sdkDirectories) { File dir = new File(sdkRoot, dirName); if (!dir.exists() || !dir.isDirectory()) { return ValidationResult.error(Messages.INVALID_SDK_DIRECTORY()); } } // Search the possible tool directories to ensure the tools exist int toolsFound = 0; final String[] toolDirectories = { "tools", "platform-tools" }; for (String dir : toolDirectories) { File toolsDir = new File(sdkRoot, dir); if (!toolsDir.isDirectory()) { continue; } for (String executable : Tool.getAllExecutableVariants()) { File toolPath = new File(toolsDir, executable); if (toolPath.exists() && toolPath.isFile()) { toolsFound++; } } } if (toolsFound < Tool.values().length) { return ValidationResult.errorWithMarkup(Messages.REQUIRED_SDK_TOOLS_NOT_FOUND()); } // Give the user a nice warning (not error) if they've not downloaded any platforms yet File platformsDir = new File(sdkRoot, "platforms"); if (platformsDir.list().length == 0) { return ValidationResult.warning(Messages.SDK_PLATFORMS_EMPTY()); } return ValidationResult.ok(); } } /** Task that will block until it can either connect to a port on localhost, or it times-out. */ private static final class LocalPortOpenTask implements Callable { private static final long serialVersionUID = 1L; private final int port; private final int timeout; /** * @param port The local TCP port to attempt to connect to. * @param timeout How long to keep trying (in milliseconds) before giving up. */ public LocalPortOpenTask(int port, int timeout) { this.port = port; this.timeout = timeout; } public Boolean call() throws InterruptedException { final long start = System.currentTimeMillis(); while (System.currentTimeMillis() < start + timeout) { try { Socket socket = new Socket("127.0.0.1", port); socket.getOutputStream(); socket.close(); return true; } catch (IOException ex) { // Ignore } Thread.sleep(1000); } return false; } } /** The Java equivalent of /dev/null. */ private static final class NullOutputStream extends OutputStream { @Override public void write(int b) throws IOException { // La la la } @Override public void write(byte[] b) throws IOException { // I can't hear you } @Override public void write(byte[] b, int off, int len) throws IOException { // Nope, still can't hear you } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy