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