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