Home | History | Annotate | Download | only in server
      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 
     17 package com.android.server;
     18 
     19 import android.net.LocalSocket;
     20 import android.net.LocalSocketAddress;
     21 import android.os.Build;
     22 import android.os.Handler;
     23 import android.os.Looper;
     24 import android.os.Message;
     25 import android.os.PowerManager;
     26 import android.os.SystemClock;
     27 import android.util.LocalLog;
     28 import android.util.Slog;
     29 
     30 import com.android.internal.annotations.VisibleForTesting;
     31 import com.google.android.collect.Lists;
     32 
     33 import java.io.FileDescriptor;
     34 import java.io.IOException;
     35 import java.io.InputStream;
     36 import java.io.OutputStream;
     37 import java.io.PrintWriter;
     38 import java.nio.charset.StandardCharsets;
     39 import java.util.ArrayList;
     40 import java.util.concurrent.atomic.AtomicInteger;
     41 import java.util.concurrent.ArrayBlockingQueue;
     42 import java.util.concurrent.BlockingQueue;
     43 import java.util.concurrent.TimeUnit;
     44 import java.util.LinkedList;
     45 
     46 /**
     47  * Generic connector class for interfacing with a native daemon which uses the
     48  * {@code libsysutils} FrameworkListener protocol.
     49  */
     50 final 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