• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket, int responseQueueSize, String logTag, int maxLogSize)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
run()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
handleMessage(Message msg)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 
listenToSocket()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      */
makeCommand(StringBuilder builder, String cmd, Object... args)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      */
execute(Command cmd)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      */
execute(String cmd, Object... args)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      */
executeForList(Command cmd)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      */
executeForList(String cmd, Object... args)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      */
execute(int timeout, String cmd, Object... args)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
doCommand(String cmd)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
doListCommand(String cmd, int expectedCode)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
appendEscaped(StringBuilder builder, String arg)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 {
NativeDaemonArgumentException(String command, NativeDaemonEvent event)426         public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
427             super(command, event);
428         }
429 
430         @Override
rethrowAsParcelableException()431         public IllegalArgumentException rethrowAsParcelableException() {
432             throw new IllegalArgumentException(getMessage(), this);
433         }
434     }
435 
436     private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
NativeDaemonFailureException(String command, NativeDaemonEvent event)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 
Command(String cmd, Object... args)449         public Command(String cmd, Object... args) {
450             mCmd = cmd;
451             for (Object arg : args) {
452                 appendArg(arg);
453             }
454         }
455 
appendArg(Object arg)456         public Command appendArg(Object arg) {
457             mArguments.add(arg);
458             return this;
459         }
460     }
461 
462     /** {@inheritDoc} */
monitor()463     public void monitor() {
464         synchronized (mDaemonLock) { }
465     }
466 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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 
log(String logstring)473     private void log(String logstring) {
474         if (LOGD) Slog.d(TAG, logstring);
475         mLocalLog.log(logstring);
476     }
477 
loge(String logstring)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;
Response(int c, String r)489             public Response(int c, String r) {cmdNum = c; request = r;}
490         }
491 
492         private final LinkedList<Response> mResponses;
493         private int mMaxCount;
494 
ResponseQueue(int maxCount)495         ResponseQueue(int maxCount) {
496             mResponses = new LinkedList<Response>();
497             mMaxCount = maxCount;
498         }
499 
add(int cmdNum, NativeDaemonEvent response)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
remove(int cmdNum, int timeoutMs, String origCmd)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 
dump(FileDescriptor fd, PrintWriter pw, String[] args)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