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