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