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