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