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