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