NativeDaemonConnector.java revision f0be1d89bf1cf5592ea1786d837f4f2329bdf66d
1/*
2 * Copyright (C) 2007 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 com.android.server;
18
19import android.net.LocalSocket;
20import android.net.LocalSocketAddress;
21import android.os.Handler;
22import android.os.HandlerThread;
23import android.os.Message;
24import android.os.SystemClock;
25import android.util.LocalLog;
26import android.util.Slog;
27
28import com.google.android.collect.Lists;
29
30import java.nio.charset.Charsets;
31import java.io.FileDescriptor;
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.PrintWriter;
36import java.util.ArrayList;
37import java.util.concurrent.BlockingQueue;
38import java.util.concurrent.LinkedBlockingQueue;
39
40/**
41 * Generic connector class for interfacing with a native daemon which uses the
42 * {@code libsysutils} FrameworkListener protocol.
43 */
44final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {
45    private static final boolean LOGD = false;
46
47    private final String TAG;
48
49    private String mSocket;
50    private OutputStream mOutputStream;
51    private LocalLog mLocalLog;
52
53    private final BlockingQueue<NativeDaemonEvent> mResponseQueue;
54
55    private INativeDaemonConnectorCallbacks mCallbacks;
56    private Handler mCallbackHandler;
57
58    /** Lock held whenever communicating with native daemon. */
59    private final Object mDaemonLock = new Object();
60
61    private final int BUFFER_SIZE = 4096;
62
63    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
64            int responseQueueSize, String logTag, int maxLogSize) {
65        mCallbacks = callbacks;
66        mSocket = socket;
67        mResponseQueue = new LinkedBlockingQueue<NativeDaemonEvent>(responseQueueSize);
68        TAG = logTag != null ? logTag : "NativeDaemonConnector";
69        mLocalLog = new LocalLog(maxLogSize);
70    }
71
72    @Override
73    public void run() {
74        HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
75        thread.start();
76        mCallbackHandler = new Handler(thread.getLooper(), this);
77
78        while (true) {
79            try {
80                listenToSocket();
81            } catch (Exception e) {
82                Slog.e(TAG, "Error in NativeDaemonConnector", e);
83                SystemClock.sleep(5000);
84            }
85        }
86    }
87
88    @Override
89    public boolean handleMessage(Message msg) {
90        String event = (String) msg.obj;
91        try {
92            if (!mCallbacks.onEvent(msg.what, event, event.split(" "))) {
93                Slog.w(TAG, String.format(
94                        "Unhandled event '%s'", event));
95            }
96        } catch (Exception e) {
97            Slog.e(TAG, String.format(
98                    "Error handling '%s'", event), e);
99        }
100        return true;
101    }
102
103    private void listenToSocket() throws IOException {
104        LocalSocket socket = null;
105
106        try {
107            socket = new LocalSocket();
108            LocalSocketAddress address = new LocalSocketAddress(mSocket,
109                    LocalSocketAddress.Namespace.RESERVED);
110
111            socket.connect(address);
112
113            InputStream inputStream = socket.getInputStream();
114            mOutputStream = socket.getOutputStream();
115
116            mCallbacks.onDaemonConnected();
117
118            byte[] buffer = new byte[BUFFER_SIZE];
119            int start = 0;
120
121            while (true) {
122                int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
123                if (count < 0) break;
124
125                // Add our starting point to the count and reset the start.
126                count += start;
127                start = 0;
128
129                for (int i = 0; i < count; i++) {
130                    if (buffer[i] == 0) {
131                        final String rawEvent = new String(
132                                buffer, start, i - start, Charsets.UTF_8);
133                        log("RCV <- {" + rawEvent + "}");
134
135                        try {
136                            final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
137                                    rawEvent);
138                            if (event.isClassUnsolicited()) {
139                                // TODO: migrate to sending NativeDaemonEvent instances
140                                mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
141                                        event.getCode(), event.getRawEvent()));
142                            } else {
143                                try {
144                                    mResponseQueue.put(event);
145                                } catch (InterruptedException ex) {
146                                    Slog.e(TAG, "Failed to put response onto queue: " + ex);
147                                }
148                            }
149                        } catch (IllegalArgumentException e) {
150                            Slog.w(TAG, "Problem parsing message: " + rawEvent, e);
151                        }
152
153                        start = i + 1;
154                    }
155                }
156                if (start == 0) {
157                    final String rawEvent = new String(buffer, start, count, Charsets.UTF_8);
158                    log("RCV incomplete <- {" + rawEvent + "}");
159                }
160
161                // We should end at the amount we read. If not, compact then
162                // buffer and read again.
163                if (start != count) {
164                    final int remaining = BUFFER_SIZE - start;
165                    System.arraycopy(buffer, start, buffer, 0, remaining);
166                    start = remaining;
167                } else {
168                    start = 0;
169                }
170            }
171        } catch (IOException ex) {
172            Slog.e(TAG, "Communications error", ex);
173            throw ex;
174        } finally {
175            synchronized (mDaemonLock) {
176                if (mOutputStream != null) {
177                    try {
178                        mOutputStream.close();
179                    } catch (IOException e) {
180                        Slog.w(TAG, "Failed closing output stream", e);
181                    }
182                    mOutputStream = null;
183                }
184            }
185
186            try {
187                if (socket != null) {
188                    socket.close();
189                }
190            } catch (IOException ex) {
191                Slog.w(TAG, "Failed closing socket", ex);
192            }
193        }
194    }
195
196    /**
197     * Send command to daemon, escaping arguments as needed.
198     *
199     * @return the final command issued.
200     */
201    private String sendCommandLocked(String cmd, Object... args)
202            throws NativeDaemonConnectorException {
203        // TODO: eventually enforce that cmd doesn't contain arguments
204        if (cmd.indexOf('\0') >= 0) {
205            throw new IllegalArgumentException("unexpected command: " + cmd);
206        }
207
208        final StringBuilder builder = new StringBuilder(cmd);
209        for (Object arg : args) {
210            final String argString = String.valueOf(arg);
211            if (argString.indexOf('\0') >= 0) {
212                throw new IllegalArgumentException("unexpected argument: " + arg);
213            }
214
215            builder.append(' ');
216            appendEscaped(builder, argString);
217        }
218
219        final String unterminated = builder.toString();
220        log("SND -> {" + unterminated + "}");
221
222        builder.append('\0');
223
224        if (mOutputStream == null) {
225            throw new NativeDaemonConnectorException("missing output stream");
226        } else {
227            try {
228                mOutputStream.write(builder.toString().getBytes(Charsets.UTF_8));
229            } catch (IOException e) {
230                throw new NativeDaemonConnectorException("problem sending command", e);
231            }
232        }
233
234        return unterminated;
235    }
236
237    /**
238     * Issue the given command to the native daemon and return a single expected
239     * response.
240     *
241     * @throws NativeDaemonConnectorException when problem communicating with
242     *             native daemon, or if the response matches
243     *             {@link NativeDaemonEvent#isClassClientError()} or
244     *             {@link NativeDaemonEvent#isClassServerError()}.
245     */
246    public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
247        return execute(cmd.mCmd, cmd.mArguments.toArray());
248    }
249
250    /**
251     * Issue the given command to the native daemon and return a single expected
252     * response.
253     *
254     * @throws NativeDaemonConnectorException when problem communicating with
255     *             native daemon, or if the response matches
256     *             {@link NativeDaemonEvent#isClassClientError()} or
257     *             {@link NativeDaemonEvent#isClassServerError()}.
258     */
259    public NativeDaemonEvent execute(String cmd, Object... args)
260            throws NativeDaemonConnectorException {
261        final NativeDaemonEvent[] events = executeForList(cmd, args);
262        if (events.length != 1) {
263            throw new NativeDaemonConnectorException(
264                    "Expected exactly one response, but received " + events.length);
265        }
266        return events[0];
267    }
268
269    /**
270     * Issue the given command to the native daemon and return any
271     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
272     * final terminal response.
273     *
274     * @throws NativeDaemonConnectorException when problem communicating with
275     *             native daemon, or if the response matches
276     *             {@link NativeDaemonEvent#isClassClientError()} or
277     *             {@link NativeDaemonEvent#isClassServerError()}.
278     */
279    public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
280        return executeForList(cmd.mCmd, cmd.mArguments.toArray());
281    }
282
283    /**
284     * Issue the given command to the native daemon and return any
285     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
286     * final terminal response.
287     *
288     * @throws NativeDaemonConnectorException when problem communicating with
289     *             native daemon, or if the response matches
290     *             {@link NativeDaemonEvent#isClassClientError()} or
291     *             {@link NativeDaemonEvent#isClassServerError()}.
292     */
293    public NativeDaemonEvent[] executeForList(String cmd, Object... args)
294            throws NativeDaemonConnectorException {
295        synchronized (mDaemonLock) {
296            return executeLocked(cmd, args);
297        }
298    }
299
300    private NativeDaemonEvent[] executeLocked(String cmd, Object... args)
301            throws NativeDaemonConnectorException {
302        final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
303
304        while (mResponseQueue.size() > 0) {
305            try {
306                log("ignoring {" + mResponseQueue.take() + "}");
307            } catch (Exception e) {}
308        }
309
310        final String sentCommand = sendCommandLocked(cmd, args);
311
312        NativeDaemonEvent event = null;
313        do {
314            try {
315                event = mResponseQueue.take();
316            } catch (InterruptedException e) {
317                Slog.w(TAG, "interrupted waiting for event line");
318                continue;
319            }
320            events.add(event);
321        } while (event.isClassContinue());
322
323        if (event.isClassClientError()) {
324            throw new NativeDaemonArgumentException(sentCommand, event);
325        }
326        if (event.isClassServerError()) {
327            throw new NativeDaemonFailureException(sentCommand, event);
328        }
329
330        return events.toArray(new NativeDaemonEvent[events.size()]);
331    }
332
333    /**
334     * Issue a command to the native daemon and return the raw responses.
335     *
336     * @deprecated callers should move to {@link #execute(String, Object...)}
337     *             which returns parsed {@link NativeDaemonEvent}.
338     */
339    @Deprecated
340    public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException {
341        final ArrayList<String> rawEvents = Lists.newArrayList();
342        final NativeDaemonEvent[] events = executeForList(cmd);
343        for (NativeDaemonEvent event : events) {
344            rawEvents.add(event.getRawEvent());
345        }
346        return rawEvents;
347    }
348
349    /**
350     * Issues a list command and returns the cooked list of all
351     * {@link NativeDaemonEvent#getMessage()} which match requested code.
352     */
353    @Deprecated
354    public String[] doListCommand(String cmd, int expectedCode)
355            throws NativeDaemonConnectorException {
356        final ArrayList<String> list = Lists.newArrayList();
357
358        final NativeDaemonEvent[] events = executeForList(cmd);
359        for (int i = 0; i < events.length - 1; i++) {
360            final NativeDaemonEvent event = events[i];
361            final int code = event.getCode();
362            if (code == expectedCode) {
363                list.add(event.getMessage());
364            } else {
365                throw new NativeDaemonConnectorException(
366                        "unexpected list response " + code + " instead of " + expectedCode);
367            }
368        }
369
370        final NativeDaemonEvent finalEvent = events[events.length - 1];
371        if (!finalEvent.isClassOk()) {
372            throw new NativeDaemonConnectorException("unexpected final event: " + finalEvent);
373        }
374
375        return list.toArray(new String[list.size()]);
376    }
377
378    /**
379     * Append the given argument to {@link StringBuilder}, escaping as needed,
380     * and surrounding with quotes when it contains spaces.
381     */
382    // @VisibleForTesting
383    static void appendEscaped(StringBuilder builder, String arg) {
384        final boolean hasSpaces = arg.indexOf(' ') >= 0;
385        if (hasSpaces) {
386            builder.append('"');
387        }
388
389        final int length = arg.length();
390        for (int i = 0; i < length; i++) {
391            final char c = arg.charAt(i);
392
393            if (c == '"') {
394                builder.append("\\\"");
395            } else if (c == '\\') {
396                builder.append("\\\\");
397            } else {
398                builder.append(c);
399            }
400        }
401
402        if (hasSpaces) {
403            builder.append('"');
404        }
405    }
406
407    private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
408        public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
409            super(command, event);
410        }
411
412        @Override
413        public IllegalArgumentException rethrowAsParcelableException() {
414            throw new IllegalArgumentException(getMessage(), this);
415        }
416    }
417
418    private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
419        public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
420            super(command, event);
421        }
422    }
423
424    /**
425     * Command builder that handles argument list building.
426     */
427    public static class Command {
428        private String mCmd;
429        private ArrayList<Object> mArguments = Lists.newArrayList();
430
431        public Command(String cmd, Object... args) {
432            mCmd = cmd;
433            for (Object arg : args) {
434                appendArg(arg);
435            }
436        }
437
438        public Command appendArg(Object arg) {
439            mArguments.add(arg);
440            return this;
441        }
442    }
443
444    /** {@inheritDoc} */
445    public void monitor() {
446        synchronized (mDaemonLock) { }
447    }
448
449    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
450        mLocalLog.dump(fd, pw, args);
451    }
452
453    private void log(String logstring) {
454        if (LOGD) Slog.d(TAG, logstring);
455        mLocalLog.log(logstring);
456    }
457}
458