NativeDaemonConnector.java revision 4887789e44cdb16b042a35e8ec03983213e88ac6
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 final static boolean VDBG = false; 52 53 private final String TAG; 54 55 private String mSocket; 56 private OutputStream mOutputStream; 57 private LocalLog mLocalLog; 58 59 private volatile boolean mDebug = false; 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 /** 103 * Enable Set debugging mode, which causes messages to also be written to both 104 * {@link Slog} in addition to internal log. 105 */ 106 public void setDebug(boolean debug) { 107 mDebug = debug; 108 } 109 110 @Override 111 public void run() { 112 mCallbackHandler = new Handler(mLooper, this); 113 114 while (true) { 115 try { 116 listenToSocket(); 117 } catch (Exception e) { 118 loge("Error in NativeDaemonConnector: " + e); 119 SystemClock.sleep(5000); 120 } 121 } 122 } 123 124 @Override 125 public boolean handleMessage(Message msg) { 126 String event = (String) msg.obj; 127 try { 128 if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) { 129 log(String.format("Unhandled event '%s'", event)); 130 } 131 } catch (Exception e) { 132 loge("Error handling '" + event + "': " + e); 133 } finally { 134 if (mCallbacks.onCheckHoldWakeLock(msg.what) && mWakeLock != null) { 135 mWakeLock.release(); 136 } 137 } 138 return true; 139 } 140 141 private LocalSocketAddress determineSocketAddress() { 142 // If we're testing, set up a socket in a namespace that's accessible to test code. 143 // In order to ensure that unprivileged apps aren't able to impersonate native daemons on 144 // production devices, even if said native daemons ill-advisedly pick a socket name that 145 // starts with __test__, only allow this on debug builds. 146 if (mSocket.startsWith("__test__") && Build.IS_DEBUGGABLE) { 147 return new LocalSocketAddress(mSocket); 148 } else { 149 return new LocalSocketAddress(mSocket, LocalSocketAddress.Namespace.RESERVED); 150 } 151 } 152 153 private void listenToSocket() throws IOException { 154 LocalSocket socket = null; 155 156 try { 157 socket = new LocalSocket(); 158 LocalSocketAddress address = determineSocketAddress(); 159 160 socket.connect(address); 161 162 InputStream inputStream = socket.getInputStream(); 163 synchronized (mDaemonLock) { 164 mOutputStream = socket.getOutputStream(); 165 } 166 167 mCallbacks.onDaemonConnected(); 168 169 byte[] buffer = new byte[BUFFER_SIZE]; 170 int start = 0; 171 172 while (true) { 173 int count = inputStream.read(buffer, start, BUFFER_SIZE - start); 174 if (count < 0) { 175 loge("got " + count + " reading with start = " + start); 176 break; 177 } 178 179 // Add our starting point to the count and reset the start. 180 count += start; 181 start = 0; 182 183 for (int i = 0; i < count; i++) { 184 if (buffer[i] == 0) { 185 // Note - do not log this raw message since it may contain 186 // sensitive data 187 final String rawEvent = new String( 188 buffer, start, i - start, StandardCharsets.UTF_8); 189 190 boolean releaseWl = false; 191 try { 192 final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent( 193 rawEvent); 194 195 log("RCV <- {" + event + "}"); 196 197 if (event.isClassUnsolicited()) { 198 // TODO: migrate to sending NativeDaemonEvent instances 199 if (mCallbacks.onCheckHoldWakeLock(event.getCode()) 200 && mWakeLock != null) { 201 mWakeLock.acquire(); 202 releaseWl = true; 203 } 204 if (mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage( 205 event.getCode(), event.getRawEvent()))) { 206 releaseWl = false; 207 } 208 } else { 209 mResponseQueue.add(event.getCmdNumber(), event); 210 } 211 } catch (IllegalArgumentException e) { 212 log("Problem parsing message " + e); 213 } finally { 214 if (releaseWl) { 215 mWakeLock.acquire(); 216 } 217 } 218 219 start = i + 1; 220 } 221 } 222 223 if (start == 0) { 224 log("RCV incomplete"); 225 } 226 227 // We should end at the amount we read. If not, compact then 228 // buffer and read again. 229 if (start != count) { 230 final int remaining = BUFFER_SIZE - start; 231 System.arraycopy(buffer, start, buffer, 0, remaining); 232 start = remaining; 233 } else { 234 start = 0; 235 } 236 } 237 } catch (IOException ex) { 238 loge("Communications error: " + ex); 239 throw ex; 240 } finally { 241 synchronized (mDaemonLock) { 242 if (mOutputStream != null) { 243 try { 244 loge("closing stream for " + mSocket); 245 mOutputStream.close(); 246 } catch (IOException e) { 247 loge("Failed closing output stream: " + e); 248 } 249 mOutputStream = null; 250 } 251 } 252 253 try { 254 if (socket != null) { 255 socket.close(); 256 } 257 } catch (IOException ex) { 258 loge("Failed closing socket: " + ex); 259 } 260 } 261 } 262 263 /** 264 * Wrapper around argument that indicates it's sensitive and shouldn't be 265 * logged. 266 */ 267 public static class SensitiveArg { 268 private final Object mArg; 269 270 public SensitiveArg(Object arg) { 271 mArg = arg; 272 } 273 274 @Override 275 public String toString() { 276 return String.valueOf(mArg); 277 } 278 } 279 280 /** 281 * Make command for daemon, escaping arguments as needed. 282 */ 283 @VisibleForTesting 284 static void makeCommand(StringBuilder rawBuilder, StringBuilder logBuilder, int sequenceNumber, 285 String cmd, Object... args) { 286 if (cmd.indexOf('\0') >= 0) { 287 throw new IllegalArgumentException("Unexpected command: " + cmd); 288 } 289 if (cmd.indexOf(' ') >= 0) { 290 throw new IllegalArgumentException("Arguments must be separate from command"); 291 } 292 293 rawBuilder.append(sequenceNumber).append(' ').append(cmd); 294 logBuilder.append(sequenceNumber).append(' ').append(cmd); 295 for (Object arg : args) { 296 final String argString = String.valueOf(arg); 297 if (argString.indexOf('\0') >= 0) { 298 throw new IllegalArgumentException("Unexpected argument: " + arg); 299 } 300 301 rawBuilder.append(' '); 302 logBuilder.append(' '); 303 304 appendEscaped(rawBuilder, argString); 305 if (arg instanceof SensitiveArg) { 306 logBuilder.append("[scrubbed]"); 307 } else { 308 appendEscaped(logBuilder, argString); 309 } 310 } 311 312 rawBuilder.append('\0'); 313 } 314 315 /** 316 * Issue the given command to the native daemon and return a single expected 317 * response. 318 * 319 * @throws NativeDaemonConnectorException when problem communicating with 320 * native daemon, or if the response matches 321 * {@link NativeDaemonEvent#isClassClientError()} or 322 * {@link NativeDaemonEvent#isClassServerError()}. 323 */ 324 public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException { 325 return execute(cmd.mCmd, cmd.mArguments.toArray()); 326 } 327 328 /** 329 * Issue the given command to the native daemon and return a single expected 330 * response. Any arguments must be separated from base command so they can 331 * be properly escaped. 332 * 333 * @throws NativeDaemonConnectorException when problem communicating with 334 * native daemon, or if the response matches 335 * {@link NativeDaemonEvent#isClassClientError()} or 336 * {@link NativeDaemonEvent#isClassServerError()}. 337 */ 338 public NativeDaemonEvent execute(String cmd, Object... args) 339 throws NativeDaemonConnectorException { 340 final NativeDaemonEvent[] events = executeForList(cmd, args); 341 if (events.length != 1) { 342 throw new NativeDaemonConnectorException( 343 "Expected exactly one response, but received " + events.length); 344 } 345 return events[0]; 346 } 347 348 /** 349 * Issue the given command to the native daemon and return any 350 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 351 * final terminal response. 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(Command cmd) throws NativeDaemonConnectorException { 359 return executeForList(cmd.mCmd, cmd.mArguments.toArray()); 360 } 361 362 /** 363 * Issue the given command to the native daemon and return any 364 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 365 * final terminal response. Any arguments must be separated from base 366 * command so they can be properly escaped. 367 * 368 * @throws NativeDaemonConnectorException when problem communicating with 369 * native daemon, or if the response matches 370 * {@link NativeDaemonEvent#isClassClientError()} or 371 * {@link NativeDaemonEvent#isClassServerError()}. 372 */ 373 public NativeDaemonEvent[] executeForList(String cmd, Object... args) 374 throws NativeDaemonConnectorException { 375 return execute(DEFAULT_TIMEOUT, cmd, args); 376 } 377 378 /** 379 * Issue the given command to the native daemon and return any {@linke 380 * NativeDaemonEvent@isClassContinue()} responses, including the final 381 * terminal response. Note that the timeout does not count time in deep 382 * sleep. Any arguments must be separated from base command so they can be 383 * properly escaped. 384 * 385 * @throws NativeDaemonConnectorException when problem communicating with 386 * native daemon, or if the response matches 387 * {@link NativeDaemonEvent#isClassClientError()} or 388 * {@link NativeDaemonEvent#isClassServerError()}. 389 */ 390 public NativeDaemonEvent[] execute(int timeout, String cmd, Object... args) 391 throws NativeDaemonConnectorException { 392 final long startTime = SystemClock.elapsedRealtime(); 393 394 final ArrayList<NativeDaemonEvent> events = Lists.newArrayList(); 395 396 final StringBuilder rawBuilder = new StringBuilder(); 397 final StringBuilder logBuilder = new StringBuilder(); 398 final int sequenceNumber = mSequenceNumber.incrementAndGet(); 399 400 makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args); 401 402 final String rawCmd = rawBuilder.toString(); 403 final String logCmd = logBuilder.toString(); 404 405 log("SND -> {" + logCmd + "}"); 406 407 synchronized (mDaemonLock) { 408 if (mOutputStream == null) { 409 throw new NativeDaemonConnectorException("missing output stream"); 410 } else { 411 try { 412 mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8)); 413 } catch (IOException e) { 414 throw new NativeDaemonConnectorException("problem sending command", e); 415 } 416 } 417 } 418 419 NativeDaemonEvent event = null; 420 do { 421 event = mResponseQueue.remove(sequenceNumber, timeout, logCmd); 422 if (event == null) { 423 loge("timed-out waiting for response to " + logCmd); 424 throw new NativeDaemonFailureException(logCmd, event); 425 } 426 if (VDBG) log("RMV <- {" + event + "}"); 427 events.add(event); 428 } while (event.isClassContinue()); 429 430 final long endTime = SystemClock.elapsedRealtime(); 431 if (endTime - startTime > WARN_EXECUTE_DELAY_MS) { 432 loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)"); 433 } 434 435 if (event.isClassClientError()) { 436 throw new NativeDaemonArgumentException(logCmd, event); 437 } 438 if (event.isClassServerError()) { 439 throw new NativeDaemonFailureException(logCmd, event); 440 } 441 442 return events.toArray(new NativeDaemonEvent[events.size()]); 443 } 444 445 /** 446 * Append the given argument to {@link StringBuilder}, escaping as needed, 447 * and surrounding with quotes when it contains spaces. 448 */ 449 @VisibleForTesting 450 static void appendEscaped(StringBuilder builder, String arg) { 451 final boolean hasSpaces = arg.indexOf(' ') >= 0; 452 if (hasSpaces) { 453 builder.append('"'); 454 } 455 456 final int length = arg.length(); 457 for (int i = 0; i < length; i++) { 458 final char c = arg.charAt(i); 459 460 if (c == '"') { 461 builder.append("\\\""); 462 } else if (c == '\\') { 463 builder.append("\\\\"); 464 } else { 465 builder.append(c); 466 } 467 } 468 469 if (hasSpaces) { 470 builder.append('"'); 471 } 472 } 473 474 private static class NativeDaemonArgumentException extends NativeDaemonConnectorException { 475 public NativeDaemonArgumentException(String command, NativeDaemonEvent event) { 476 super(command, event); 477 } 478 479 @Override 480 public IllegalArgumentException rethrowAsParcelableException() { 481 throw new IllegalArgumentException(getMessage(), this); 482 } 483 } 484 485 private static class NativeDaemonFailureException extends NativeDaemonConnectorException { 486 public NativeDaemonFailureException(String command, NativeDaemonEvent event) { 487 super(command, event); 488 } 489 } 490 491 /** 492 * Command builder that handles argument list building. Any arguments must 493 * be separated from base command so they can be properly escaped. 494 */ 495 public static class Command { 496 private String mCmd; 497 private ArrayList<Object> mArguments = Lists.newArrayList(); 498 499 public Command(String cmd, Object... args) { 500 mCmd = cmd; 501 for (Object arg : args) { 502 appendArg(arg); 503 } 504 } 505 506 public Command appendArg(Object arg) { 507 mArguments.add(arg); 508 return this; 509 } 510 } 511 512 /** {@inheritDoc} */ 513 public void monitor() { 514 synchronized (mDaemonLock) { } 515 } 516 517 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 518 mLocalLog.dump(fd, pw, args); 519 pw.println(); 520 mResponseQueue.dump(fd, pw, args); 521 } 522 523 private void log(String logstring) { 524 if (mDebug) Slog.d(TAG, logstring); 525 mLocalLog.log(logstring); 526 } 527 528 private void loge(String logstring) { 529 Slog.e(TAG, logstring); 530 mLocalLog.log(logstring); 531 } 532 533 private static class ResponseQueue { 534 535 private static class PendingCmd { 536 public final int cmdNum; 537 public final String logCmd; 538 539 public BlockingQueue<NativeDaemonEvent> responses = 540 new ArrayBlockingQueue<NativeDaemonEvent>(10); 541 542 // The availableResponseCount member is used to track when we can remove this 543 // instance from the ResponseQueue. 544 // This is used under the protection of a sync of the mPendingCmds object. 545 // A positive value means we've had more writers retreive this object while 546 // a negative value means we've had more readers. When we've had an equal number 547 // (it goes to zero) we can remove this object from the mPendingCmds list. 548 // Note that we may have more responses for this command (and more readers 549 // coming), but that would result in a new PendingCmd instance being created 550 // and added with the same cmdNum. 551 // Also note that when this goes to zero it just means a parity of readers and 552 // writers have retrieved this object - not that they are done using it. The 553 // responses queue may well have more responses yet to be read or may get more 554 // responses added to it. But all those readers/writers have retreived and 555 // hold references to this instance already so it can be removed from 556 // mPendingCmds queue. 557 public int availableResponseCount; 558 559 public PendingCmd(int cmdNum, String logCmd) { 560 this.cmdNum = cmdNum; 561 this.logCmd = logCmd; 562 } 563 } 564 565 private final LinkedList<PendingCmd> mPendingCmds; 566 private int mMaxCount; 567 568 ResponseQueue(int maxCount) { 569 mPendingCmds = new LinkedList<PendingCmd>(); 570 mMaxCount = maxCount; 571 } 572 573 public void add(int cmdNum, NativeDaemonEvent response) { 574 PendingCmd found = null; 575 synchronized (mPendingCmds) { 576 for (PendingCmd pendingCmd : mPendingCmds) { 577 if (pendingCmd.cmdNum == cmdNum) { 578 found = pendingCmd; 579 break; 580 } 581 } 582 if (found == null) { 583 // didn't find it - make sure our queue isn't too big before adding 584 while (mPendingCmds.size() >= mMaxCount) { 585 Slog.e("NativeDaemonConnector.ResponseQueue", 586 "more buffered than allowed: " + mPendingCmds.size() + 587 " >= " + mMaxCount); 588 // let any waiter timeout waiting for this 589 PendingCmd pendingCmd = mPendingCmds.remove(); 590 Slog.e("NativeDaemonConnector.ResponseQueue", 591 "Removing request: " + pendingCmd.logCmd + " (" + 592 pendingCmd.cmdNum + ")"); 593 } 594 found = new PendingCmd(cmdNum, null); 595 mPendingCmds.add(found); 596 } 597 found.availableResponseCount++; 598 // if a matching remove call has already retrieved this we can remove this 599 // instance from our list 600 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 601 } 602 try { 603 found.responses.put(response); 604 } catch (InterruptedException e) { } 605 } 606 607 // note that the timeout does not count time in deep sleep. If you don't want 608 // the device to sleep, hold a wakelock 609 public NativeDaemonEvent remove(int cmdNum, int timeoutMs, String logCmd) { 610 PendingCmd found = null; 611 synchronized (mPendingCmds) { 612 for (PendingCmd pendingCmd : mPendingCmds) { 613 if (pendingCmd.cmdNum == cmdNum) { 614 found = pendingCmd; 615 break; 616 } 617 } 618 if (found == null) { 619 found = new PendingCmd(cmdNum, logCmd); 620 mPendingCmds.add(found); 621 } 622 found.availableResponseCount--; 623 // if a matching add call has already retrieved this we can remove this 624 // instance from our list 625 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 626 } 627 NativeDaemonEvent result = null; 628 try { 629 result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS); 630 } catch (InterruptedException e) {} 631 if (result == null) { 632 Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response"); 633 } 634 return result; 635 } 636 637 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 638 pw.println("Pending requests:"); 639 synchronized (mPendingCmds) { 640 for (PendingCmd pendingCmd : mPendingCmds) { 641 pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd); 642 } 643 } 644 } 645 } 646} 647