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                         // 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