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.JsonObject; 20 import java.io.BufferedInputStream; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.InputStreamReader; 24 import java.net.ConnectException; 25 import java.net.Socket; 26 import java.net.SocketException; 27 import java.nio.charset.Charset; 28 import vogar.Log; 29 import vogar.Outcome; 30 import vogar.Result; 31 import vogar.util.IoUtils; 32 33 /** 34 * Connects to a target process to monitor its action using XML over raw 35 * sockets. 36 */ 37 public final class HostMonitor { 38 private static final Charset UTF8 = Charset.forName("UTF-8"); 39 40 private Log log; 41 private Handler handler; 42 private final String marker = "//00xx"; 43 44 public HostMonitor(Log log, Handler handler) { 45 this.log = log; 46 this.handler = handler; 47 } 48 49 /** 50 * Returns true if the target process completed normally. 51 */ 52 public boolean attach(int port) throws IOException { 53 for (int attempt = 0; true; attempt++) { 54 Socket socket = null; 55 try { 56 socket = new Socket("localhost", port); 57 InputStream in = new BufferedInputStream(socket.getInputStream()); 58 if (checkStream(in)) { 59 log.verbose("action monitor connected to " + socket.getRemoteSocketAddress()); 60 return followStream(in); 61 } 62 } catch (ConnectException ignored) { 63 } catch (SocketException ignored) { 64 } finally { 65 IoUtils.closeQuietly(socket); 66 } 67 68 log.verbose("connection " + attempt + " to localhost:" 69 + port + " failed; retrying in 1s"); 70 try { 71 Thread.sleep(1000); 72 } catch (InterruptedException ignored) { 73 } 74 } 75 } 76 77 /** 78 * Somewhere between the host and client process, broken socket connections 79 * are being accepted. Before we try to do any work on such a connection, 80 * check it to make sure it's not dead! 81 * 82 * TODO: file a bug (against adb?) for this 83 */ 84 private boolean checkStream(InputStream in) throws IOException { 85 in.mark(1); 86 if (in.read() == -1) { 87 return false; 88 } else { 89 in.reset(); 90 return true; 91 } 92 } 93 94 public boolean followStream(InputStream in) throws IOException { 95 return followProcess(new InterleavedReader(marker, new InputStreamReader(in, UTF8))); 96 } 97 98 /** 99 * Our wire format is a mix of strings and the JSON values like the following: 100 * 101 * {"outcome"="java.util.FormatterMain"} 102 * {"result"="SUCCESS"} 103 * {"outcome"="java.util.FormatterTest#testBar" runner="vogar.target.junit.JUnitRunner"} 104 * {"result"="SUCCESS"} 105 * {"completedNormally"=true} 106 */ 107 private boolean followProcess(InterleavedReader reader) throws IOException { 108 String currentOutcome = null; 109 StringBuilder output = new StringBuilder(); 110 boolean completedNormally = false; 111 112 Object o; 113 while ((o = reader.read()) != null) { 114 if (o instanceof String) { 115 String text = (String) o; 116 if (currentOutcome != null) { 117 output.append(text); 118 handler.output(currentOutcome, text); 119 } else { 120 handler.print(text); 121 } 122 } else if (o instanceof JsonObject) { 123 JsonObject jsonObject = (JsonObject) o; 124 if (jsonObject.get("outcome") != null) { 125 currentOutcome = jsonObject.get("outcome").getAsString(); 126 handler.output(currentOutcome, ""); 127 handler.start(currentOutcome); 128 } else if (jsonObject.get("result") != null) { 129 Result currentResult = Result.valueOf(jsonObject.get("result").getAsString()); 130 handler.finish(new Outcome(currentOutcome, currentResult, output.toString())); 131 output.delete(0, output.length()); 132 currentOutcome = null; 133 } else if (jsonObject.get("completedNormally") != null) { 134 completedNormally = jsonObject.get("completedNormally").getAsBoolean(); 135 } 136 } else { 137 throw new IllegalStateException("Unexpected object: " + o); 138 } 139 } 140 141 return completedNormally; 142 } 143 144 145 /** 146 * Handles updates on the outcomes of a target process. 147 */ 148 public interface Handler { 149 150 /** 151 * Receive notification that an outcome is pending. 152 */ 153 void start(String outcomeName); 154 155 /** 156 * Receive a completed outcome. 157 */ 158 void finish(Outcome outcome); 159 160 /** 161 * Receive partial output from an action being executed. 162 */ 163 void output(String outcomeName, String output); 164 165 /** 166 * Receive a string to print immediately 167 */ 168 void print(String string); 169 } 170 } 171