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
17package vogar.monitor;
18
19import com.google.gson.JsonElement;
20import com.google.gson.JsonObject;
21import java.io.BufferedInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.InputStreamReader;
25import java.net.ConnectException;
26import java.net.Socket;
27import java.net.SocketException;
28import java.nio.charset.Charset;
29import vogar.Log;
30import vogar.Outcome;
31import vogar.Result;
32import vogar.util.IoUtils;
33
34/**
35 * Connects to a target process to monitor its action using XML over raw
36 * sockets.
37 */
38public 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
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     */
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     */
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
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     */
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         */
157        void start(String outcomeName, String runnerClass);
158
159        /**
160         * Receive a completed outcome.
161         */
162        void finish(Outcome outcome);
163
164        /**
165         * Receive partial output from an action being executed.
166         */
167        void output(String outcomeName, String output);
168
169        /**
170         * Receive a string to print immediately
171         */
172        void print(String string);
173    }
174}
175