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