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