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