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