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.android.internal.annotations.VisibleForTesting;
29import com.google.android.collect.Lists;
30
31import java.nio.charset.Charsets;
32import java.io.FileDescriptor;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.OutputStream;
36import java.io.PrintWriter;
37import java.util.ArrayList;
38import java.util.concurrent.atomic.AtomicInteger;
39import java.util.concurrent.ArrayBlockingQueue;
40import java.util.concurrent.BlockingQueue;
41import java.util.concurrent.TimeUnit;
42import java.util.LinkedList;
43
44/**
45 * Generic connector class for interfacing with a native daemon which uses the
46 * {@code libsysutils} FrameworkListener protocol.
47 */
48final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {
49    private static final boolean LOGD = false;
50
51    private final String TAG;
52
53    private String mSocket;
54    private OutputStream mOutputStream;
55    private LocalLog mLocalLog;
56
57    private final ResponseQueue mResponseQueue;
58
59    private INativeDaemonConnectorCallbacks mCallbacks;
60    private Handler mCallbackHandler;
61
62    private AtomicInteger mSequenceNumber;
63
64    private static final int DEFAULT_TIMEOUT = 1 * 60 * 1000; /* 1 minute */
65    private static final long WARN_EXECUTE_DELAY_MS = 500; /* .5 sec */
66
67    /** Lock held whenever communicating with native daemon. */
68    private final Object mDaemonLock = new Object();
69
70    private final int BUFFER_SIZE = 4096;
71
72    NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
73            int responseQueueSize, String logTag, int maxLogSize) {
74        mCallbacks = callbacks;
75        mSocket = socket;
76        mResponseQueue = new ResponseQueue(responseQueueSize);
77        mSequenceNumber = new AtomicInteger(0);
78        TAG = logTag != null ? logTag : "NativeDaemonConnector";
79        mLocalLog = new LocalLog(maxLogSize);
80    }
81
82    @Override
83    public void run() {
84        HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
85        thread.start();
86        mCallbackHandler = new Handler(thread.getLooper(), this);
87
88        while (true) {
89            try {
90                listenToSocket();
91            } catch (Exception e) {
92                loge("Error in NativeDaemonConnector: " + e);
93                SystemClock.sleep(5000);
94            }
95        }
96    }
97
98    @Override
99    public boolean handleMessage(Message msg) {
100        String event = (String) msg.obj;
101        try {
102            if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {
103                log(String.format("Unhandled event '%s'", event));
104            }
105        } catch (Exception e) {
106            loge("Error handling '" + event + "': " + e);
107        }
108        return true;
109    }
110
111    private void listenToSocket() throws IOException {
112        LocalSocket socket = null;
113
114        try {
115            socket = new LocalSocket();
116            LocalSocketAddress address = new LocalSocketAddress(mSocket,
117                    LocalSocketAddress.Namespace.RESERVED);
118
119            socket.connect(address);
120
121            InputStream inputStream = socket.getInputStream();
122            synchronized (mDaemonLock) {
123                mOutputStream = socket.getOutputStream();
124            }
125
126            mCallbacks.onDaemonConnected();
127
128            byte[] buffer = new byte[BUFFER_SIZE];
129            int start = 0;
130
131            while (true) {
132                int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
133                if (count < 0) {
134                    loge("got " + count + " reading with start = " + start);
135                    break;
136                }
137
138                // Add our starting point to the count and reset the start.
139                count += start;
140                start = 0;
141
142                for (int i = 0; i < count; i++) {
143                    if (buffer[i] == 0) {
144                        final String rawEvent = new String(
145                                buffer, start, i - start, Charsets.UTF_8);
146                        log("RCV <- {" + rawEvent + "}");
147
148                        try {
149                            final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
150                                    rawEvent);
151                            if (event.isClassUnsolicited()) {
152                                // TODO: migrate to sending NativeDaemonEvent instances
153                                mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
154                                        event.getCode(), event.getRawEvent()));
155                            } else {
156                                mResponseQueue.add(event.getCmdNumber(), event);
157                            }
158                        } catch (IllegalArgumentException e) {
159                            log("Problem parsing message: " + rawEvent + " - " + e);
160                        }
161
162                        start = i + 1;
163                    }
164                }
165                if (start == 0) {
166                    final String rawEvent = new String(buffer, start, count, Charsets.UTF_8);
167                    log("RCV incomplete <- {" + rawEvent + "}");
168                }
169
170                // We should end at the amount we read. If not, compact then
171                // buffer and read again.
172                if (start != count) {
173                    final int remaining = BUFFER_SIZE - start;
174                    System.arraycopy(buffer, start, buffer, 0, remaining);
175                    start = remaining;
176                } else {
177                    start = 0;
178                }
179            }
180        } catch (IOException ex) {
181            loge("Communications error: " + ex);
182            throw ex;
183        } finally {
184            synchronized (mDaemonLock) {
185                if (mOutputStream != null) {
186                    try {
187                        loge("closing stream for " + mSocket);
188                        mOutputStream.close();
189                    } catch (IOException e) {
190                        loge("Failed closing output stream: " + e);
191                    }
192                    mOutputStream = null;
193                }
194            }
195
196            try {
197                if (socket != null) {
198                    socket.close();
199                }
200            } catch (IOException ex) {
201                loge("Failed closing socket: " + ex);
202            }
203        }
204    }
205
206    /**
207     * Make command for daemon, escaping arguments as needed.
208     */
209    private void makeCommand(StringBuilder builder, String cmd, Object... args)
210            throws NativeDaemonConnectorException {
211        // TODO: eventually enforce that cmd doesn't contain arguments
212        if (cmd.indexOf('\0') >= 0) {
213            throw new IllegalArgumentException("unexpected command: " + cmd);
214        }
215
216        builder.append(cmd);
217        for (Object arg : args) {
218            final String argString = String.valueOf(arg);
219            if (argString.indexOf('\0') >= 0) {
220                throw new IllegalArgumentException("unexpected argument: " + arg);
221            }
222
223            builder.append(' ');
224            appendEscaped(builder, argString);
225        }
226    }
227
228    /**
229     * Issue the given command to the native daemon and return a single expected
230     * response.
231     *
232     * @throws NativeDaemonConnectorException when problem communicating with
233     *             native daemon, or if the response matches
234     *             {@link NativeDaemonEvent#isClassClientError()} or
235     *             {@link NativeDaemonEvent#isClassServerError()}.
236     */
237    public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
238        return execute(cmd.mCmd, cmd.mArguments.toArray());
239    }
240
241    /**
242     * Issue the given command to the native daemon and return a single expected
243     * response.
244     *
245     * @throws NativeDaemonConnectorException when problem communicating with
246     *             native daemon, or if the response matches
247     *             {@link NativeDaemonEvent#isClassClientError()} or
248     *             {@link NativeDaemonEvent#isClassServerError()}.
249     */
250    public NativeDaemonEvent execute(String cmd, Object... args)
251            throws NativeDaemonConnectorException {
252        final NativeDaemonEvent[] events = executeForList(cmd, args);
253        if (events.length != 1) {
254            throw new NativeDaemonConnectorException(
255                    "Expected exactly one response, but received " + events.length);
256        }
257        return events[0];
258    }
259
260    /**
261     * Issue the given command to the native daemon and return any
262     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
263     * final terminal response.
264     *
265     * @throws NativeDaemonConnectorException when problem communicating with
266     *             native daemon, or if the response matches
267     *             {@link NativeDaemonEvent#isClassClientError()} or
268     *             {@link NativeDaemonEvent#isClassServerError()}.
269     */
270    public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
271        return executeForList(cmd.mCmd, cmd.mArguments.toArray());
272    }
273
274    /**
275     * Issue the given command to the native daemon and return any
276     * {@link NativeDaemonEvent#isClassContinue()} responses, including the
277     * final terminal response.
278     *
279     * @throws NativeDaemonConnectorException when problem communicating with
280     *             native daemon, or if the response matches
281     *             {@link NativeDaemonEvent#isClassClientError()} or
282     *             {@link NativeDaemonEvent#isClassServerError()}.
283     */
284    public NativeDaemonEvent[] executeForList(String cmd, Object... args)
285            throws NativeDaemonConnectorException {
286            return execute(DEFAULT_TIMEOUT, cmd, args);
287    }
288
289    /**
290     * Issue the given command to the native daemon and return any
291     * {@linke NativeDaemonEvent@isClassContinue()} responses, including the
292     * final terminal response.  Note that the timeout does not count time in
293     * deep sleep.
294     *
295     * @throws NativeDaemonConnectorException when problem communicating with
296     *             native daemon, or if the response matches
297     *             {@link NativeDaemonEvent#isClassClientError()} or
298     *             {@link NativeDaemonEvent#isClassServerError()}.
299     */
300    public NativeDaemonEvent[] execute(int timeout, String cmd, Object... args)
301            throws NativeDaemonConnectorException {
302        final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
303
304        final int sequenceNumber = mSequenceNumber.incrementAndGet();
305        final StringBuilder cmdBuilder =
306                new StringBuilder(Integer.toString(sequenceNumber)).append(' ');
307        final long startTime = SystemClock.elapsedRealtime();
308
309        makeCommand(cmdBuilder, cmd, args);
310
311        final String logCmd = cmdBuilder.toString(); /* includes cmdNum, cmd, args */
312        log("SND -> {" + logCmd + "}");
313
314        cmdBuilder.append('\0');
315        final String sentCmd = cmdBuilder.toString(); /* logCmd + \0 */
316
317        synchronized (mDaemonLock) {
318            if (mOutputStream == null) {
319                throw new NativeDaemonConnectorException("missing output stream");
320            } else {
321                try {
322                    mOutputStream.write(sentCmd.getBytes(Charsets.UTF_8));
323                } catch (IOException e) {
324                    throw new NativeDaemonConnectorException("problem sending command", e);
325                }
326            }
327        }
328
329        NativeDaemonEvent event = null;
330        do {
331            event = mResponseQueue.remove(sequenceNumber, timeout, sentCmd);
332            if (event == null) {
333                loge("timed-out waiting for response to " + logCmd);
334                throw new NativeDaemonFailureException(logCmd, event);
335            }
336            log("RMV <- {" + event + "}");
337            events.add(event);
338        } while (event.isClassContinue());
339
340        final long endTime = SystemClock.elapsedRealtime();
341        if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
342            loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
343        }
344
345        if (event.isClassClientError()) {
346            throw new NativeDaemonArgumentException(logCmd, event);
347        }
348        if (event.isClassServerError()) {
349            throw new NativeDaemonFailureException(logCmd, event);
350        }
351
352        return events.toArray(new NativeDaemonEvent[events.size()]);
353    }
354
355    /**
356     * Issue a command to the native daemon and return the raw responses.
357     *
358     * @deprecated callers should move to {@link #execute(String, Object...)}
359     *             which returns parsed {@link NativeDaemonEvent}.
360     */
361    @Deprecated
362    public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException {
363        final ArrayList<String> rawEvents = Lists.newArrayList();
364        final NativeDaemonEvent[] events = executeForList(cmd);
365        for (NativeDaemonEvent event : events) {
366            rawEvents.add(event.getRawEvent());
367        }
368        return rawEvents;
369    }
370
371    /**
372     * Issues a list command and returns the cooked list of all
373     * {@link NativeDaemonEvent#getMessage()} which match requested code.
374     */
375    @Deprecated
376    public String[] doListCommand(String cmd, int expectedCode)
377            throws NativeDaemonConnectorException {
378        final ArrayList<String> list = Lists.newArrayList();
379
380        final NativeDaemonEvent[] events = executeForList(cmd);
381        for (int i = 0; i < events.length - 1; i++) {
382            final NativeDaemonEvent event = events[i];
383            final int code = event.getCode();
384            if (code == expectedCode) {
385                list.add(event.getMessage());
386            } else {
387                throw new NativeDaemonConnectorException(
388                        "unexpected list response " + code + " instead of " + expectedCode);
389            }
390        }
391
392        final NativeDaemonEvent finalEvent = events[events.length - 1];
393        if (!finalEvent.isClassOk()) {
394            throw new NativeDaemonConnectorException("unexpected final event: " + finalEvent);
395        }
396
397        return list.toArray(new String[list.size()]);
398    }
399
400    /**
401     * Append the given argument to {@link StringBuilder}, escaping as needed,
402     * and surrounding with quotes when it contains spaces.
403     */
404    @VisibleForTesting
405    static void appendEscaped(StringBuilder builder, String arg) {
406        final boolean hasSpaces = arg.indexOf(' ') >= 0;
407        if (hasSpaces) {
408            builder.append('"');
409        }
410
411        final int length = arg.length();
412        for (int i = 0; i < length; i++) {
413            final char c = arg.charAt(i);
414
415            if (c == '"') {
416                builder.append("\\\"");
417            } else if (c == '\\') {
418                builder.append("\\\\");
419            } else {
420                builder.append(c);
421            }
422        }
423
424        if (hasSpaces) {
425            builder.append('"');
426        }
427    }
428
429    private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
430        public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
431            super(command, event);
432        }
433
434        @Override
435        public IllegalArgumentException rethrowAsParcelableException() {
436            throw new IllegalArgumentException(getMessage(), this);
437        }
438    }
439
440    private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
441        public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
442            super(command, event);
443        }
444    }
445
446    /**
447     * Command builder that handles argument list building.
448     */
449    public static class Command {
450        private String mCmd;
451        private ArrayList<Object> mArguments = Lists.newArrayList();
452
453        public Command(String cmd, Object... args) {
454            mCmd = cmd;
455            for (Object arg : args) {
456                appendArg(arg);
457            }
458        }
459
460        public Command appendArg(Object arg) {
461            mArguments.add(arg);
462            return this;
463        }
464    }
465
466    /** {@inheritDoc} */
467    public void monitor() {
468        synchronized (mDaemonLock) { }
469    }
470
471    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
472        mLocalLog.dump(fd, pw, args);
473        pw.println();
474        mResponseQueue.dump(fd, pw, args);
475    }
476
477    private void log(String logstring) {
478        if (LOGD) Slog.d(TAG, logstring);
479        mLocalLog.log(logstring);
480    }
481
482    private void loge(String logstring) {
483        Slog.e(TAG, logstring);
484        mLocalLog.log(logstring);
485    }
486
487    private static class ResponseQueue {
488
489        private static class PendingCmd {
490            public int cmdNum;
491            public BlockingQueue<NativeDaemonEvent> responses =
492                    new ArrayBlockingQueue<NativeDaemonEvent>(10);
493            public String request;
494
495            // The availableResponseCount member is used to track when we can remove this
496            // instance from the ResponseQueue.
497            // This is used under the protection of a sync of the mPendingCmds object.
498            // A positive value means we've had more writers retreive this object while
499            // a negative value means we've had more readers.  When we've had an equal number
500            // (it goes to zero) we can remove this object from the mPendingCmds list.
501            // Note that we may have more responses for this command (and more readers
502            // coming), but that would result in a new PendingCmd instance being created
503            // and added with the same cmdNum.
504            // Also note that when this goes to zero it just means a parity of readers and
505            // writers have retrieved this object - not that they are done using it.  The
506            // responses queue may well have more responses yet to be read or may get more
507            // responses added to it.  But all those readers/writers have retreived and
508            // hold references to this instance already so it can be removed from
509            // mPendingCmds queue.
510            public int availableResponseCount;
511            public PendingCmd(int c, String r) {cmdNum = c; request = r;}
512        }
513
514        private final LinkedList<PendingCmd> mPendingCmds;
515        private int mMaxCount;
516
517        ResponseQueue(int maxCount) {
518            mPendingCmds = new LinkedList<PendingCmd>();
519            mMaxCount = maxCount;
520        }
521
522        public void add(int cmdNum, NativeDaemonEvent response) {
523            PendingCmd found = null;
524            synchronized (mPendingCmds) {
525                for (PendingCmd pendingCmd : mPendingCmds) {
526                    if (pendingCmd.cmdNum == cmdNum) {
527                        found = pendingCmd;
528                        break;
529                    }
530                }
531                if (found == null) {
532                    // didn't find it - make sure our queue isn't too big before adding
533                    while (mPendingCmds.size() >= mMaxCount) {
534                        Slog.e("NativeDaemonConnector.ResponseQueue",
535                                "more buffered than allowed: " + mPendingCmds.size() +
536                                " >= " + mMaxCount);
537                        // let any waiter timeout waiting for this
538                        PendingCmd pendingCmd = mPendingCmds.remove();
539                        Slog.e("NativeDaemonConnector.ResponseQueue",
540                                "Removing request: " + pendingCmd.request + " (" +
541                                pendingCmd.cmdNum + ")");
542                    }
543                    found = new PendingCmd(cmdNum, null);
544                    mPendingCmds.add(found);
545                }
546                found.availableResponseCount++;
547                // if a matching remove call has already retrieved this we can remove this
548                // instance from our list
549                if (found.availableResponseCount == 0) mPendingCmds.remove(found);
550            }
551            try {
552                found.responses.put(response);
553            } catch (InterruptedException e) { }
554        }
555
556        // note that the timeout does not count time in deep sleep.  If you don't want
557        // the device to sleep, hold a wakelock
558        public NativeDaemonEvent remove(int cmdNum, int timeoutMs, String origCmd) {
559            PendingCmd found = null;
560            synchronized (mPendingCmds) {
561                for (PendingCmd pendingCmd : mPendingCmds) {
562                    if (pendingCmd.cmdNum == cmdNum) {
563                        found = pendingCmd;
564                        break;
565                    }
566                }
567                if (found == null) {
568                    found = new PendingCmd(cmdNum, origCmd);
569                    mPendingCmds.add(found);
570                }
571                found.availableResponseCount--;
572                // if a matching add call has already retrieved this we can remove this
573                // instance from our list
574                if (found.availableResponseCount == 0) mPendingCmds.remove(found);
575            }
576            NativeDaemonEvent result = null;
577            try {
578                result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS);
579            } catch (InterruptedException e) {}
580            if (result == null) {
581                Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response");
582            }
583            return result;
584        }
585
586        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
587            pw.println("Pending requests:");
588            synchronized (mPendingCmds) {
589                for (PendingCmd pendingCmd : mPendingCmds) {
590                    pw.println("  Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.request);
591                }
592            }
593        }
594    }
595}
596