/** * Original work provided by defunct 'autoandroid-1.0-rc5': http://code.google.com/p/autoandroid/ * New Derivative work required to repackage for wider distribution and continued development. * Copyright (C) SAS Institute * General Public License: http://www.opensource.org/licenses/gpl-license.php **/ package org.safs.android.auto.lib; import static java.util.Arrays.asList; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Vector; import java.util.regex.Pattern; import org.safs.tools.consoles.GenericProcessCapture; /** * Default usage:<br> * <pre> * new StartEmulator().run(args); * * The above starts a new emulator and waits up to the default {@value #bootCompletionTimeoutInSeconds} seconds detecting boot completion. * * Common Alternatives:<br> * * StartEmulator.setBootCompletionTimeout(150); * StartEmulator emu = new StartEmulator(); * emu.setOnlyIfRunning(true); * emu.setDoReaperThread(false); * emu.setDoSocketThread(false); * emu.setDoOpenSocket(false); * emu.setDoCloseSocket(false); * emu.run(emulatorArgs); * </pre> * @author canagl * */ public class StartEmulator { private final static Pattern FINISHED_BOOTING = Pattern.compile("(.*done scanning volume internal.*)|"+ "(.*Filesystem check completed.*)|"+ "(.*/PowerManagerService\\(.*\\): bootCompleted.*)"); private String serialNumber = null; private Process2 emulator = null; /** Set this System property "org.safs.android.start-emulator.destroy" to "true" to destroy any * emulator we launched. */ public static final String EMULATOR_DESTROY_PROPERTY = "org.safs.android.start-emulator.destroy"; /** System property "org.safs.android.start-emulator.destroyed" is set to "true" when we have * detected and attempted an emulator destroy request. */ public static final String EMULATOR_DESTROYED_PROPERTY = "org.safs.android.start-emulator.destroyed"; private boolean doReaperThread = true; private boolean doSocketThread = true; private boolean doCloseSocket = true; private boolean doOpenSocket = true; private boolean doOnlyIfNotRunning = false; private Appendable chainOut = null; /* * Default: 300 seconds (5 minutes). * Change to shorten or lengthen the timeout when waiting for boot completion. * Values less than 0 mean wait indefinitely. */ private static long bootCompletionTimeoutInSeconds = 300; /* * Default: 10 seconds. * Delay the issuance of boot completion detection to allow it to settle. */ private static long bootCompletionDetectedDelayInSeconds = 10; /** * This main entry point simply creates a new StartEmulator instance and passes * repackaged arguments to the run(onlyIfNotRunning, args) method. * @param args<br> * "--only-if-not-running" -- self explanatory.<br> * "noreaper" -- do not run reaper thread to force emulator termination.<br> * "nosocket" -- do not run socket thread to accept socket connections.<br> * "noopensocket" -- do not the socket at all.<br> * "noclosesocket" -- do not close the socket if opened.<br> * "noboottimeout" -- do not set a timeout for boot completion.<br> * "boottimeout <seconds> -- set boot completion timeout in seconds. Default is {@value #bootCompletionTimeoutInSeconds}<br> * all other args will be passed to the emulator instance.<br> * @throws IOException * @throws InterruptedException * @see #run(boolean, String...) */ public static void main(String [] args) throws IOException, InterruptedException { boolean onlyIfNotRunning = false; String anarg = null; StartEmulator em = new StartEmulator(); Vector realargs = new Vector(); if (args.length > 0){ for(int a=0;a<args.length;a++){ anarg = args[a]; if("--only-if-not-running".equalsIgnoreCase(anarg)) { em.setOnlyIfNotRunning(true); }else if("noreaper".equalsIgnoreCase(anarg)){ em.setDoReaperThread(false); }else if("nosocket".equalsIgnoreCase(anarg)){ em.setDoSocketThread(false); }else if("noopensocket".equalsIgnoreCase(anarg)){ em.setDoOpenSocket(false); }else if("noclosesocket".equalsIgnoreCase(anarg)){ em.setDoCloseSocket(false); }else if("noboottimeout".equalsIgnoreCase(anarg)){ StartEmulator.setBootCompletionTimeout(0); }else if("boottimeout".equalsIgnoreCase(anarg)){ try{ StartEmulator.setBootCompletionTimeout(Long.parseLong(args[++a])); }catch(Exception x){} }else{ realargs.add(anarg); } } args = (String[]) realargs.toArray(new String[0]); } em.run(args); } /** * Set the number of seconds we watch for boot completion before timing out the watch loop. * Default: {@value #bootCompletionTimeoutInSeconds} seconds. * Set 0 seconds for no timeout--wait indefinitely. * @see #watchLogUntilBooted() */ public static void setBootCompletionTimeout(long seconds){ if (seconds >= 0) bootCompletionTimeoutInSeconds = seconds; } /** * Set the number of seconds we delay issuing boot completion detected to allow it to settle. * Default: {@value #bootCompletionDetectedDelayInSeconds} seconds. * Set 0 seconds for no delay. * @see #watchLogUntilBooted() */ public static void setBootCompletionDetectedDelay(long seconds){ if (seconds >= 0) bootCompletionDetectedDelayInSeconds = seconds; } /** Set/Clear the flag to only launch an emulator on run() IF there is not one already running. * This is set FALSE by default. */ public void setOnlyIfNotRunning(boolean ifNotRunning) { doOnlyIfNotRunning = ifNotRunning;} /** Set/Clear the flag to run a monitor "reaper" thread to kill the emulator on command. * This is set TRUE by default. * <p> * The reaper thread monitors Java System.getProperty("org.safs.android.start-emulator.destroy"). * If this gets set to "true" then the emulator Process created/maintained here will * be destroyed. */ public void setDoReaperThread(boolean doReaper){doReaperThread = doReaper;} /** Set/Clear the flag to run a Sockets.accept() timeout thread which attempts to connect with * and report the TCP Port the emulator is starting on. * This is set TRUE by default. <br> * (I have not seen a scenario where this actually works--connecting and reporting the emulator * serial number to a receiver.) */ public void setDoSocketThread(boolean doSocket){doSocketThread = doSocket;} /** Set/Clear the flag to even attempt a Socket Server connection and temporarily bind to a TCP port * to report the emulator serial number. * This is set TRUE by default. <br> * (I have not seen a scenario where binding to the Socket has successfully connected to anything * and it may sometimes prevent abd from "seeing" the emulator--but this is just a guess.) */ public void setDoOpenSocket(boolean doSocket){doOpenSocket = doSocket;} /** Set/Clear the flag to Close the Socket Server connection after the Socket Thread has completed * with the attempts to report the emulator serial number to some remote receiver. * This is set TRUE by default. <br> * (I have not seen a scenario where binding to the Socket has successfully connected to anything * and it may sometimes prevent abd from "seeing" the emulator--but this is just a guess.) */ public void setDoCloseSocket(boolean doSocket){doCloseSocket = doSocket;} /** Setan Appendable sink to receive stdOut after we are finished using the stdOut Reader to monitor * the emulator for bootstrap completion. If no (optional) Appendable sink is provided, the stdOut is routed to * the dev/NULL sink once we are finished with it. */ public void setChainedStdOut(Appendable newOut){ chainOut = newOut;} /** * Retrieve the Process2 object owning/wrapping the emulator process. * Note, at least on Windows, the emulator process spawns multiple other processes and the emulator * that ultimately becomes visible is running in an entirely different process. * <p> * Thus, doing things like process.destroy() does NOT necessarily mean the emulator will actually * be affected. Although, it may free up resources no longer needed (and possibly interfering with) * the actual emulator and abd. * @return Process2 wrapper */ public Process2 getEmulatorProcess(){ return emulator;} public void SysOut(String out){ if(chainOut != null){ try{ chainOut.append(out+"\n"); }catch(Exception e){ System.out.println(out); } }else{ System.out.println(out); } } /** * Simply calls {@link #run(boolean, String...)} using the preset value for onlyIfNotRunning. * @param args emulator program args, not StartEmulator args. * @throws IOException * @throws InterruptedException */ public void run(String... args) throws IOException, InterruptedException{ run(doOnlyIfNotRunning, args); } /** * The primary routine used to start an emulator. The routine will exit without starting * an emulator if onlyIfNotRunning=true and it detects a running emulator via adb "devices". * <p> * if doOpenSocket is true(default) it will also launch and wait for up to 30 seconds for a * remote receiver to connect to the emulator remote console and receive the emulator serial number. * When that time is up, the connection is closed and the port is released (in theory). * <p> * Whatever emulator args are provided this routine also adds the following args: * <p> * <ul>-logcat *:v -report-console tcp:<SocketServer port></ul> * <p> * This routine also will block until it has detected the emulator has completed the boot process. * <p> * if doReaperThread is true(default) the routine will also spawn a separate thread to monitor the * System property {@link #EMULATOR_DESTROY_PROPERTY} for emulator destroy requests. However, because * the actual running emulator is one or more processes removed from this process this request usually * does not result in the emulator being shutdown. * * @param onlyIfNotRunning * @param args * @throws IOException * @throws InterruptedException */ public void run(boolean onlyIfNotRunning, String... args) throws IOException, InterruptedException { AndroidTools tools = AndroidTools.get(); if (onlyIfNotRunning) { StringBuffer devices = new StringBuffer(); tools.adb("devices").connectStdout(devices).discardStderr().waitForSuccess(); if (devices.toString().contains("emulator-")) return; } ServerSocket serverSocket = null; Thread socketThread = null; int port = 5554; boolean exhausted = false; boolean bound = false; try { //serverSocket = new ServerSocket(); //port = serverSocket.getLocalPort(); //start at 5554 and work up +2, until exhausting options if(doOpenSocket){ while(!bound && !exhausted){ try{ serverSocket = new ServerSocket(port); //port = 0; //serverSocket = new ServerSocket(); //serverSocket.bind(null); //port = serverSocket.getLocalPort(); bound = serverSocket.isBound(); }catch(IOException x){ //assuming "already be in use" SysOut("Local port "+ port +" may already be in use..."); exhausted = ((port += 2) > 5584); } } } List<String> emulatorArgs = new ArrayList<String>(asList(args)); emulatorArgs.addAll(asList("-logcat", "*:v", "-report-console", "tcp:" + port)); //emulatorArgs.addAll(asList("-report-console", "tcp:" + port)); emulator = tools.emulator(emulatorArgs).connectStderr(System.err); if(doOpenSocket && doSocketThread) { SysOut("Starting ServerSocket Socket Thread..."); socketThread = startSocketThread(serverSocket); }else{ SysOut("Bypassing ServerSocket Socket Thread..."); } if(bootCompletionTimeoutInSeconds > 0){ SysOut("Watching emulator for boot completion within "+ String.valueOf(bootCompletionTimeoutInSeconds) +" seconds."); }else{ SysOut("Watching emulator for boot completion indefinitely!"); } watchLogUntilBooted(); if(socketThread != null) { SysOut("Joining ServerSocket Socket Thread until ThreadDeath..."); socketThread.join(); } if(doReaperThread){ SysOut("Emulator: "+ serialNumber +", Starting Reaper Thread..."); startReaperThread(); }else{ SysOut("Emulator: "+ serialNumber +", Bypassing Reaper Thread..."); } } finally { if(socketThread != null) { SysOut("FINALLY Joining ServerSocket Socket Thread until ThreadDeath..."); try{socketThread.join();}catch(Exception x){} } if (doCloseSocket && serverSocket != null) { SysOut("Closing ServerSocket..."); try{ serverSocket.close();}catch(Exception x){} } } } /* * Attempt to accept a Socket connection on our ServerSocket for up to 30 seconds. * If a connection is made, get the emulator serial number to store for reference. * I have never seen this work. Unless this is something we are supposed to assign * to the emulator--which we've never done. */ private Thread startSocketThread(final ServerSocket serverSocket) { Thread thread = new Thread(new Runnable() { public void run() { Socket conn = null; InputStream in = null; if(serverSocket != null){ try { SysOut("Accepting Socket Connection for up to 30 seconds..."); serverSocket.setSoTimeout(30 * 1000); conn = serverSocket.accept(); SysOut("Socket Connection established."); in = conn.getInputStream(); StringBuilder serialNumber = new StringBuilder("emulator-"); int c = 0; while ((c = in.read()) != -1) { serialNumber.appendCodePoint(c); } StartEmulator.this.serialNumber = serialNumber.toString(); SysOut("Detected emulator serialno: "+ serialNumber.toString()); } catch (IOException e) { SysOut("SOCKET THREAD HANDLING IOEXCEPTION: "); e.printStackTrace(); } finally { try { if (in != null) in.close(); } catch (IOException e) {} try { if (conn != null) conn.close(); } catch (IOException e) {} } } } }); thread.setDaemon(true); thread.start(); return thread; } /* * connect emulator stdout to chainOut if chainOut exists. * Otherwise, tell the emulator to discard stdout. */ private void _chainEmulatorStdOut(){ if(chainOut != null) { emulator.connectStdout(chainOut); }else{ emulator.discardStdout(); } } /* * Attempts to run "adb shell dumpsys window policy" in order to detect the * presence of the main launcher window. */ private boolean launcherWindowDetected(int secsTimeout) throws IOException{ Process2 process = AndroidTools.get().adb("shell", "dumpsys", "window", "policy"); GenericProcessCapture monitor = new GenericProcessCapture(process.getProcess(),null, true, false); String line = null; boolean detected = false; final String LAUNCHER_CLASS = "com.android.launcher/com.android.launcher"; try{ process = process.waitFor(secsTimeout); Vector data = monitor.getData(); if(!data.isEmpty()){ for(Object item:data){ line = item.toString(); if(line.contains(LAUNCHER_CLASS)) { detected = true; break; } } } } catch(Exception x){ /* ignore: all bad */} monitor = null; process.destroy(); process = null; if(! detected) SysOut("Boot detection has not found Launcher Activity yet..."); return detected; } /* * Watches the emulator stdOut for an indication the emulator boot process * should be complete. Will wait/watch for boot completion up to the value of * bootCompletionTimeoutInSeconds. * @throws IOException * @see #bootCompletionTimeoutInSeconds * @see #bootCompletionDetectedDelayInSeconds */ private void watchLogUntilBooted() throws IOException, AndroidRuntimeException { _chainEmulatorStdOut(); long timeout = Long.MAX_VALUE; boolean detected = false; if (bootCompletionTimeoutInSeconds > 0) timeout = System.currentTimeMillis()+(bootCompletionTimeoutInSeconds * 1000); SysOut("Boot detection checking Window for Launcher Activity..."); while (! (System.currentTimeMillis() > timeout)) { if(launcherWindowDetected(10)){ SysOut("Emulator boot completion detected!"); if(StartEmulator.bootCompletionDetectedDelayInSeconds > 0) try{Thread.sleep(StartEmulator.bootCompletionDetectedDelayInSeconds *1000);}catch(Exception x){} return; } if(System.currentTimeMillis() > timeout) break; try{Thread.sleep(5000);}catch(Exception x){} } SysOut("*** TIMEOUT *** reached for emulator boot process completion."); throw new AndroidRuntimeException("Failed to detect emulator boot completion in timeout period."); } // private void watchLogUntilBooted() throws IOException, AndroidRuntimeException { // _chainEmulatorStdOut(); // long timeout = Long.MAX_VALUE; // boolean detected = false; // int online ; // int offline; // List devices; // if (bootCompletionTimeoutInSeconds > 0) // timeout = System.currentTimeMillis()+(bootCompletionTimeoutInSeconds * 1000); // while (! (System.currentTimeMillis() > timeout)) { // devices = DUtilities.getAttachedDevices(); // offline = 0; // online = 0; // if(devices.size()>0){ // for(Object device:devices){ // if(device.toString().toLowerCase().trim().endsWith("device")){ // online++; // }else{ // offline++; // } // } // if(online > 0 && offline == 0){ // SysOut("Emulator boot completion detected!"); // if(StartEmulator.bootCompletionDetectedDelayInSeconds > 0) // try{Thread.sleep(StartEmulator.bootCompletionDetectedDelayInSeconds *1000);}catch(Exception x){} // return; // } // } // if(System.currentTimeMillis() > timeout) break; // //try{Thread.sleep(2000);}catch(Exception x){} // } // SysOut("*** TIMEOUT *** reached for emulator boot process completion."); // throw new AndroidRuntimeException("Failed to detect emulator boot completion in timeout period."); // } /* * Thread that monitors System.getProperty(EMULATOR_DESTROY_PROPERTY). * If this gets set to "true" then the emulator Process created/maintained here will * be destroyed and EMULATOR_DESTROYED_PROPERTY will be set to "true". */ private void startReaperThread() { Thread thread = new Thread(new Runnable() { public void run() { SysOut("Monitoring Emulator shutdown requests..."); while (! "true".equalsIgnoreCase(System.getProperty(EMULATOR_DESTROYED_PROPERTY))) { if ("true".equals(System.getProperty(EMULATOR_DESTROY_PROPERTY))) { SysOut("Received Emulator shutdown requests..."); emulator.destroy(); System.setProperty(EMULATOR_DESTROY_PROPERTY, "false"); System.setProperty(EMULATOR_DESTROYED_PROPERTY, "true"); break; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } SysOut("Emulator Reaper Thread loop exiting as if DESTROYED."); } }); thread.setDaemon(true); thread.start(); } }