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