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