1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package vogar.monitor; 18 19 import com.google.gson.JsonElement; 20 import com.google.gson.JsonObject; 21 import java.io.BufferedInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.InputStreamReader; 25 import java.net.ConnectException; 26 import java.net.Socket; 27 import java.net.SocketException; 28 import java.nio.charset.Charset; 29 import vogar.Log; 30 import vogar.Outcome; 31 import vogar.Result; 32 import vogar.util.IoUtils; 33 34 /** 35 * Connects to a target process to monitor its action using XML over raw 36 * sockets. 37 */ 38 public final class HostMonitor { 39 private static final Charset UTF8 = Charset.forName("UTF-8"); 40 41 private Log log; 42 private Handler handler; 43 private final String marker = "//00xx"; 44 HostMonitor(Log log, Handler handler)45 public HostMonitor(Log log, Handler handler) { 46 this.log = log; 47 this.handler = handler; 48 } 49 50 /** 51 * Returns true if the target process completed normally. 52 */ attach(int port)53 public boolean attach(int port) throws IOException { 54 for (int attempt = 0; true; attempt++) { 55 Socket socket = null; 56 try { 57 socket = new Socket("localhost", port); 58 InputStream in = new BufferedInputStream(socket.getInputStream()); 59 if (checkStream(in)) { 60 log.verbose("action monitor connected to " + socket.getRemoteSocketAddress()); 61 return followStream(in); 62 } 63 } catch (ConnectException ignored) { 64 } catch (SocketException ignored) { 65 } finally { 66 IoUtils.closeQuietly(socket); 67 } 68 69 log.verbose("connection " + attempt + " to localhost:" 70 + port + " failed; retrying in 1s"); 71 try { 72 Thread.sleep(1000); 73 } catch (InterruptedException ignored) { 74 } 75 } 76 } 77 78 /** 79 * Somewhere between the host and client process, broken socket connections 80 * are being accepted. Before we try to do any work on such a connection, 81 * check it to make sure it's not dead! 82 * 83 * TODO: file a bug (against adb?) for this 84 */ checkStream(InputStream in)85 private boolean checkStream(InputStream in) throws IOException { 86 in.mark(1); 87 if (in.read() == -1) { 88 return false; 89 } else { 90 in.reset(); 91 return true; 92 } 93 } 94 followStream(InputStream in)95 public boolean followStream(InputStream in) throws IOException { 96 return followProcess(new InterleavedReader(marker, new InputStreamReader(in, UTF8))); 97 } 98 99 /** 100 * Our wire format is a mix of strings and the JSON values like the following: 101 * 102 * {"outcome"="java.util.FormatterMain"} 103 * {"result"="SUCCESS"} 104 * {"outcome"="java.util.FormatterTest#testBar" runner="vogar.target.junit.JUnitRunner"} 105 * {"result"="SUCCESS"} 106 * {"completedNormally"=true} 107 */ followProcess(InterleavedReader reader)108 private boolean followProcess(InterleavedReader reader) throws IOException { 109 String currentOutcome = null; 110 StringBuilder output = new StringBuilder(); 111 boolean completedNormally = false; 112 113 Object o; 114 while ((o = reader.read()) != null) { 115 if (o instanceof String) { 116 String text = (String) o; 117 if (currentOutcome != null) { 118 output.append(text); 119 handler.output(currentOutcome, text); 120 } else { 121 handler.print(text); 122 } 123 } else if (o instanceof JsonObject) { 124 JsonObject jsonObject = (JsonObject) o; 125 if (jsonObject.get("outcome") != null) { 126 currentOutcome = jsonObject.get("outcome").getAsString(); 127 handler.output(currentOutcome, ""); 128 JsonElement runner = jsonObject.get("runner"); 129 String runnerClass = runner != null ? runner.getAsString() : null; 130 handler.start(currentOutcome, runnerClass); 131 } else if (jsonObject.get("result") != null) { 132 Result currentResult = Result.valueOf(jsonObject.get("result").getAsString()); 133 handler.finish(new Outcome(currentOutcome, currentResult, output.toString())); 134 output.delete(0, output.length()); 135 currentOutcome = null; 136 } else if (jsonObject.get("completedNormally") != null) { 137 completedNormally = jsonObject.get("completedNormally").getAsBoolean(); 138 } 139 } else { 140 throw new IllegalStateException("Unexpected object: " + o); 141 } 142 } 143 144 return completedNormally; 145 } 146 147 148 /** 149 * Handles updates on the outcomes of a target process. 150 */ 151 public interface Handler { 152 153 /** 154 * @param runnerClass can be null, indicating nothing is actually being run. This will 155 * happen in the event of an impending error. 156 */ start(String outcomeName, String runnerClass)157 void start(String outcomeName, String runnerClass); 158 159 /** 160 * Receive a completed outcome. 161 */ finish(Outcome outcome)162 void finish(Outcome outcome); 163 164 /** 165 * Receive partial output from an action being executed. 166 */ output(String outcomeName, String output)167 void output(String outcomeName, String output); 168 169 /** 170 * Receive a string to print immediately 171 */ print(String string)172 void print(String string); 173 } 174 } 175