NativeDaemonConnector.java revision d66f3e584ba9e5c9b104b111305edceedd23fcb0
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.Build; 22import android.os.Handler; 23import android.os.Looper; 24import android.os.Message; 25import android.os.PowerManager; 26import android.os.SystemClock; 27import android.util.LocalLog; 28import android.util.Slog; 29 30import com.android.internal.annotations.VisibleForTesting; 31import com.google.android.collect.Lists; 32 33import java.io.FileDescriptor; 34import java.io.IOException; 35import java.io.InputStream; 36import java.io.OutputStream; 37import java.io.PrintWriter; 38import java.nio.charset.StandardCharsets; 39import java.util.ArrayList; 40import java.util.concurrent.atomic.AtomicInteger; 41import java.util.concurrent.ArrayBlockingQueue; 42import java.util.concurrent.BlockingQueue; 43import java.util.concurrent.TimeUnit; 44import java.util.LinkedList; 45 46/** 47 * Generic connector class for interfacing with a native daemon which uses the 48 * {@code libsysutils} FrameworkListener protocol. 49 */ 50final 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 log("RCV <- {" + rawEvent + "}"); 180 181 boolean releaseWl = false; 182 try { 183 final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent( 184 rawEvent); 185 if (event.isClassUnsolicited()) { 186 // TODO: migrate to sending NativeDaemonEvent instances 187 if (mCallbacks.onCheckHoldWakeLock(event.getCode()) 188 && mWakeLock != null) { 189 mWakeLock.acquire(); 190 releaseWl = true; 191 } 192 if (mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage( 193 event.getCode(), event.getRawEvent()))) { 194 releaseWl = false; 195 } 196 } else { 197 mResponseQueue.add(event.getCmdNumber(), event); 198 } 199 } catch (IllegalArgumentException e) { 200 log("Problem parsing message: " + rawEvent + " - " + e); 201 } finally { 202 if (releaseWl) { 203 mWakeLock.acquire(); 204 } 205 } 206 207 start = i + 1; 208 } 209 } 210 if (start == 0) { 211 final String rawEvent = new String(buffer, start, count, StandardCharsets.UTF_8); 212 log("RCV incomplete <- {" + rawEvent + "}"); 213 } 214 215 // We should end at the amount we read. If not, compact then 216 // buffer and read again. 217 if (start != count) { 218 final int remaining = BUFFER_SIZE - start; 219 System.arraycopy(buffer, start, buffer, 0, remaining); 220 start = remaining; 221 } else { 222 start = 0; 223 } 224 } 225 } catch (IOException ex) { 226 loge("Communications error: " + ex); 227 throw ex; 228 } finally { 229 synchronized (mDaemonLock) { 230 if (mOutputStream != null) { 231 try { 232 loge("closing stream for " + mSocket); 233 mOutputStream.close(); 234 } catch (IOException e) { 235 loge("Failed closing output stream: " + e); 236 } 237 mOutputStream = null; 238 } 239 } 240 241 try { 242 if (socket != null) { 243 socket.close(); 244 } 245 } catch (IOException ex) { 246 loge("Failed closing socket: " + ex); 247 } 248 } 249 } 250 251 /** 252 * Wrapper around argument that indicates it's sensitive and shouldn't be 253 * logged. 254 */ 255 public static class SensitiveArg { 256 private final Object mArg; 257 258 public SensitiveArg(Object arg) { 259 mArg = arg; 260 } 261 262 @Override 263 public String toString() { 264 return String.valueOf(mArg); 265 } 266 } 267 268 /** 269 * Make command for daemon, escaping arguments as needed. 270 */ 271 @VisibleForTesting 272 static void makeCommand(StringBuilder rawBuilder, StringBuilder logBuilder, int sequenceNumber, 273 String cmd, Object... args) { 274 if (cmd.indexOf('\0') >= 0) { 275 throw new IllegalArgumentException("Unexpected command: " + cmd); 276 } 277 if (cmd.indexOf(' ') >= 0) { 278 throw new IllegalArgumentException("Arguments must be separate from command"); 279 } 280 281 rawBuilder.append(sequenceNumber).append(' ').append(cmd); 282 logBuilder.append(sequenceNumber).append(' ').append(cmd); 283 for (Object arg : args) { 284 final String argString = String.valueOf(arg); 285 if (argString.indexOf('\0') >= 0) { 286 throw new IllegalArgumentException("Unexpected argument: " + arg); 287 } 288 289 rawBuilder.append(' '); 290 logBuilder.append(' '); 291 292 appendEscaped(rawBuilder, argString); 293 if (arg instanceof SensitiveArg) { 294 logBuilder.append("[scrubbed]"); 295 } else { 296 appendEscaped(logBuilder, argString); 297 } 298 } 299 300 rawBuilder.append('\0'); 301 } 302 303 /** 304 * Issue the given command to the native daemon and return a single expected 305 * response. 306 * 307 * @throws NativeDaemonConnectorException when problem communicating with 308 * native daemon, or if the response matches 309 * {@link NativeDaemonEvent#isClassClientError()} or 310 * {@link NativeDaemonEvent#isClassServerError()}. 311 */ 312 public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException { 313 return execute(cmd.mCmd, cmd.mArguments.toArray()); 314 } 315 316 /** 317 * Issue the given command to the native daemon and return a single expected 318 * response. Any arguments must be separated from base command so they can 319 * be properly escaped. 320 * 321 * @throws NativeDaemonConnectorException when problem communicating with 322 * native daemon, or if the response matches 323 * {@link NativeDaemonEvent#isClassClientError()} or 324 * {@link NativeDaemonEvent#isClassServerError()}. 325 */ 326 public NativeDaemonEvent execute(String cmd, Object... args) 327 throws NativeDaemonConnectorException { 328 final NativeDaemonEvent[] events = executeForList(cmd, args); 329 if (events.length != 1) { 330 throw new NativeDaemonConnectorException( 331 "Expected exactly one response, but received " + events.length); 332 } 333 return events[0]; 334 } 335 336 /** 337 * Issue the given command to the native daemon and return any 338 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 339 * final terminal response. 340 * 341 * @throws NativeDaemonConnectorException when problem communicating with 342 * native daemon, or if the response matches 343 * {@link NativeDaemonEvent#isClassClientError()} or 344 * {@link NativeDaemonEvent#isClassServerError()}. 345 */ 346 public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException { 347 return executeForList(cmd.mCmd, cmd.mArguments.toArray()); 348 } 349 350 /** 351 * Issue the given command to the native daemon and return any 352 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 353 * final terminal response. Any arguments must be separated from base 354 * command so they can be properly escaped. 355 * 356 * @throws NativeDaemonConnectorException when problem communicating with 357 * native daemon, or if the response matches 358 * {@link NativeDaemonEvent#isClassClientError()} or 359 * {@link NativeDaemonEvent#isClassServerError()}. 360 */ 361 public NativeDaemonEvent[] executeForList(String cmd, Object... args) 362 throws NativeDaemonConnectorException { 363 return execute(DEFAULT_TIMEOUT, cmd, args); 364 } 365 366 /** 367 * Issue the given command to the native daemon and return any {@linke 368 * NativeDaemonEvent@isClassContinue()} responses, including the final 369 * terminal response. Note that the timeout does not count time in deep 370 * sleep. Any arguments must be separated from base command so they can be 371 * properly escaped. 372 * 373 * @throws NativeDaemonConnectorException when problem communicating with 374 * native daemon, or if the response matches 375 * {@link NativeDaemonEvent#isClassClientError()} or 376 * {@link NativeDaemonEvent#isClassServerError()}. 377 */ 378 public NativeDaemonEvent[] execute(int timeout, String cmd, Object... args) 379 throws NativeDaemonConnectorException { 380 final long startTime = SystemClock.elapsedRealtime(); 381 382 final ArrayList<NativeDaemonEvent> events = Lists.newArrayList(); 383 384 final StringBuilder rawBuilder = new StringBuilder(); 385 final StringBuilder logBuilder = new StringBuilder(); 386 final int sequenceNumber = mSequenceNumber.incrementAndGet(); 387 388 makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args); 389 390 final String rawCmd = rawBuilder.toString(); 391 final String logCmd = logBuilder.toString(); 392 393 log("SND -> {" + logCmd + "}"); 394 395 synchronized (mDaemonLock) { 396 if (mOutputStream == null) { 397 throw new NativeDaemonConnectorException("missing output stream"); 398 } else { 399 try { 400 mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8)); 401 } catch (IOException e) { 402 throw new NativeDaemonConnectorException("problem sending command", e); 403 } 404 } 405 } 406 407 NativeDaemonEvent event = null; 408 do { 409 event = mResponseQueue.remove(sequenceNumber, timeout, logCmd); 410 if (event == null) { 411 loge("timed-out waiting for response to " + logCmd); 412 throw new NativeDaemonFailureException(logCmd, event); 413 } 414 if (VDBG) log("RMV <- {" + event + "}"); 415 events.add(event); 416 } while (event.isClassContinue()); 417 418 final long endTime = SystemClock.elapsedRealtime(); 419 if (endTime - startTime > WARN_EXECUTE_DELAY_MS) { 420 loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)"); 421 } 422 423 if (event.isClassClientError()) { 424 throw new NativeDaemonArgumentException(logCmd, event); 425 } 426 if (event.isClassServerError()) { 427 throw new NativeDaemonFailureException(logCmd, event); 428 } 429 430 return events.toArray(new NativeDaemonEvent[events.size()]); 431 } 432 433 /** 434 * Append the given argument to {@link StringBuilder}, escaping as needed, 435 * and surrounding with quotes when it contains spaces. 436 */ 437 @VisibleForTesting 438 static void appendEscaped(StringBuilder builder, String arg) { 439 final boolean hasSpaces = arg.indexOf(' ') >= 0; 440 if (hasSpaces) { 441 builder.append('"'); 442 } 443 444 final int length = arg.length(); 445 for (int i = 0; i < length; i++) { 446 final char c = arg.charAt(i); 447 448 if (c == '"') { 449 builder.append("\\\""); 450 } else if (c == '\\') { 451 builder.append("\\\\"); 452 } else { 453 builder.append(c); 454 } 455 } 456 457 if (hasSpaces) { 458 builder.append('"'); 459 } 460 } 461 462 private static class NativeDaemonArgumentException extends NativeDaemonConnectorException { 463 public NativeDaemonArgumentException(String command, NativeDaemonEvent event) { 464 super(command, event); 465 } 466 467 @Override 468 public IllegalArgumentException rethrowAsParcelableException() { 469 throw new IllegalArgumentException(getMessage(), this); 470 } 471 } 472 473 private static class NativeDaemonFailureException extends NativeDaemonConnectorException { 474 public NativeDaemonFailureException(String command, NativeDaemonEvent event) { 475 super(command, event); 476 } 477 } 478 479 /** 480 * Command builder that handles argument list building. Any arguments must 481 * be separated from base command so they can be properly escaped. 482 */ 483 public static class Command { 484 private String mCmd; 485 private ArrayList<Object> mArguments = Lists.newArrayList(); 486 487 public Command(String cmd, Object... args) { 488 mCmd = cmd; 489 for (Object arg : args) { 490 appendArg(arg); 491 } 492 } 493 494 public Command appendArg(Object arg) { 495 mArguments.add(arg); 496 return this; 497 } 498 } 499 500 /** {@inheritDoc} */ 501 public void monitor() { 502 synchronized (mDaemonLock) { } 503 } 504 505 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 506 mLocalLog.dump(fd, pw, args); 507 pw.println(); 508 mResponseQueue.dump(fd, pw, args); 509 } 510 511 private void log(String logstring) { 512 if (LOGD) Slog.d(TAG, logstring); 513 mLocalLog.log(logstring); 514 } 515 516 private void loge(String logstring) { 517 Slog.e(TAG, logstring); 518 mLocalLog.log(logstring); 519 } 520 521 private static class ResponseQueue { 522 523 private static class PendingCmd { 524 public final int cmdNum; 525 public final String logCmd; 526 527 public BlockingQueue<NativeDaemonEvent> responses = 528 new ArrayBlockingQueue<NativeDaemonEvent>(10); 529 530 // The availableResponseCount member is used to track when we can remove this 531 // instance from the ResponseQueue. 532 // This is used under the protection of a sync of the mPendingCmds object. 533 // A positive value means we've had more writers retreive this object while 534 // a negative value means we've had more readers. When we've had an equal number 535 // (it goes to zero) we can remove this object from the mPendingCmds list. 536 // Note that we may have more responses for this command (and more readers 537 // coming), but that would result in a new PendingCmd instance being created 538 // and added with the same cmdNum. 539 // Also note that when this goes to zero it just means a parity of readers and 540 // writers have retrieved this object - not that they are done using it. The 541 // responses queue may well have more responses yet to be read or may get more 542 // responses added to it. But all those readers/writers have retreived and 543 // hold references to this instance already so it can be removed from 544 // mPendingCmds queue. 545 public int availableResponseCount; 546 547 public PendingCmd(int cmdNum, String logCmd) { 548 this.cmdNum = cmdNum; 549 this.logCmd = logCmd; 550 } 551 } 552 553 private final LinkedList<PendingCmd> mPendingCmds; 554 private int mMaxCount; 555 556 ResponseQueue(int maxCount) { 557 mPendingCmds = new LinkedList<PendingCmd>(); 558 mMaxCount = maxCount; 559 } 560 561 public void add(int cmdNum, NativeDaemonEvent response) { 562 PendingCmd found = null; 563 synchronized (mPendingCmds) { 564 for (PendingCmd pendingCmd : mPendingCmds) { 565 if (pendingCmd.cmdNum == cmdNum) { 566 found = pendingCmd; 567 break; 568 } 569 } 570 if (found == null) { 571 // didn't find it - make sure our queue isn't too big before adding 572 while (mPendingCmds.size() >= mMaxCount) { 573 Slog.e("NativeDaemonConnector.ResponseQueue", 574 "more buffered than allowed: " + mPendingCmds.size() + 575 " >= " + mMaxCount); 576 // let any waiter timeout waiting for this 577 PendingCmd pendingCmd = mPendingCmds.remove(); 578 Slog.e("NativeDaemonConnector.ResponseQueue", 579 "Removing request: " + pendingCmd.logCmd + " (" + 580 pendingCmd.cmdNum + ")"); 581 } 582 found = new PendingCmd(cmdNum, null); 583 mPendingCmds.add(found); 584 } 585 found.availableResponseCount++; 586 // if a matching remove call has already retrieved this we can remove this 587 // instance from our list 588 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 589 } 590 try { 591 found.responses.put(response); 592 } catch (InterruptedException e) { } 593 } 594 595 // note that the timeout does not count time in deep sleep. If you don't want 596 // the device to sleep, hold a wakelock 597 public NativeDaemonEvent remove(int cmdNum, int timeoutMs, String logCmd) { 598 PendingCmd found = null; 599 synchronized (mPendingCmds) { 600 for (PendingCmd pendingCmd : mPendingCmds) { 601 if (pendingCmd.cmdNum == cmdNum) { 602 found = pendingCmd; 603 break; 604 } 605 } 606 if (found == null) { 607 found = new PendingCmd(cmdNum, logCmd); 608 mPendingCmds.add(found); 609 } 610 found.availableResponseCount--; 611 // if a matching add call has already retrieved this we can remove this 612 // instance from our list 613 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 614 } 615 NativeDaemonEvent result = null; 616 try { 617 result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS); 618 } catch (InterruptedException e) {} 619 if (result == null) { 620 Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response"); 621 } 622 return result; 623 } 624 625 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 626 pw.println("Pending requests:"); 627 synchronized (mPendingCmds) { 628 for (PendingCmd pendingCmd : mPendingCmds) { 629 pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd); 630 } 631 } 632 } 633 } 634} 635