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