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 long 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 return execute(DEFAULT_TIMEOUT, cmd, args); 341 } 342 343 public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args) 344 throws NativeDaemonConnectorException { 345 final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args); 346 if (events.length != 1) { 347 throw new NativeDaemonConnectorException( 348 "Expected exactly one response, but received " + events.length); 349 } 350 return events[0]; 351 } 352 353 /** 354 * Issue the given command to the native daemon and return any 355 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 356 * final terminal response. 357 * 358 * @throws NativeDaemonConnectorException when problem communicating with 359 * native daemon, or if the response matches 360 * {@link NativeDaemonEvent#isClassClientError()} or 361 * {@link NativeDaemonEvent#isClassServerError()}. 362 */ 363 public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException { 364 return executeForList(cmd.mCmd, cmd.mArguments.toArray()); 365 } 366 367 /** 368 * Issue the given command to the native daemon and return any 369 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 370 * final terminal response. Any arguments must be separated from base 371 * command so they can be 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[] executeForList(String cmd, Object... args) 379 throws NativeDaemonConnectorException { 380 return executeForList(DEFAULT_TIMEOUT, cmd, args); 381 } 382 383 /** 384 * Issue the given command to the native daemon and return any {@linke 385 * NativeDaemonEvent@isClassContinue()} responses, including the final 386 * terminal response. Note that the timeout does not count time in deep 387 * sleep. Any arguments must be separated from base command so they can be 388 * properly escaped. 389 * 390 * @throws NativeDaemonConnectorException when problem communicating with 391 * native daemon, or if the response matches 392 * {@link NativeDaemonEvent#isClassClientError()} or 393 * {@link NativeDaemonEvent#isClassServerError()}. 394 */ 395 public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args) 396 throws NativeDaemonConnectorException { 397 final long startTime = SystemClock.elapsedRealtime(); 398 399 final ArrayList<NativeDaemonEvent> events = Lists.newArrayList(); 400 401 final StringBuilder rawBuilder = new StringBuilder(); 402 final StringBuilder logBuilder = new StringBuilder(); 403 final int sequenceNumber = mSequenceNumber.incrementAndGet(); 404 405 makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args); 406 407 final String rawCmd = rawBuilder.toString(); 408 final String logCmd = logBuilder.toString(); 409 410 log("SND -> {" + logCmd + "}"); 411 412 synchronized (mDaemonLock) { 413 if (mOutputStream == null) { 414 throw new NativeDaemonConnectorException("missing output stream"); 415 } else { 416 try { 417 mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8)); 418 } catch (IOException e) { 419 throw new NativeDaemonConnectorException("problem sending command", e); 420 } 421 } 422 } 423 424 NativeDaemonEvent event = null; 425 do { 426 event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd); 427 if (event == null) { 428 loge("timed-out waiting for response to " + logCmd); 429 throw new NativeDaemonTimeoutException(logCmd, event); 430 } 431 if (VDBG) log("RMV <- {" + event + "}"); 432 events.add(event); 433 } while (event.isClassContinue()); 434 435 final long endTime = SystemClock.elapsedRealtime(); 436 if (endTime - startTime > WARN_EXECUTE_DELAY_MS) { 437 loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)"); 438 } 439 440 if (event.isClassClientError()) { 441 throw new NativeDaemonArgumentException(logCmd, event); 442 } 443 if (event.isClassServerError()) { 444 throw new NativeDaemonFailureException(logCmd, event); 445 } 446 447 return events.toArray(new NativeDaemonEvent[events.size()]); 448 } 449 450 /** 451 * Append the given argument to {@link StringBuilder}, escaping as needed, 452 * and surrounding with quotes when it contains spaces. 453 */ 454 @VisibleForTesting 455 static void appendEscaped(StringBuilder builder, String arg) { 456 final boolean hasSpaces = arg.indexOf(' ') >= 0; 457 if (hasSpaces) { 458 builder.append('"'); 459 } 460 461 final int length = arg.length(); 462 for (int i = 0; i < length; i++) { 463 final char c = arg.charAt(i); 464 465 if (c == '"') { 466 builder.append("\\\""); 467 } else if (c == '\\') { 468 builder.append("\\\\"); 469 } else { 470 builder.append(c); 471 } 472 } 473 474 if (hasSpaces) { 475 builder.append('"'); 476 } 477 } 478 479 private static class NativeDaemonArgumentException extends NativeDaemonConnectorException { 480 public NativeDaemonArgumentException(String command, NativeDaemonEvent event) { 481 super(command, event); 482 } 483 484 @Override 485 public IllegalArgumentException rethrowAsParcelableException() { 486 throw new IllegalArgumentException(getMessage(), this); 487 } 488 } 489 490 private static class NativeDaemonFailureException extends NativeDaemonConnectorException { 491 public NativeDaemonFailureException(String command, NativeDaemonEvent event) { 492 super(command, event); 493 } 494 } 495 496 /** 497 * Command builder that handles argument list building. Any arguments must 498 * be separated from base command so they can be properly escaped. 499 */ 500 public static class Command { 501 private String mCmd; 502 private ArrayList<Object> mArguments = Lists.newArrayList(); 503 504 public Command(String cmd, Object... args) { 505 mCmd = cmd; 506 for (Object arg : args) { 507 appendArg(arg); 508 } 509 } 510 511 public Command appendArg(Object arg) { 512 mArguments.add(arg); 513 return this; 514 } 515 } 516 517 /** {@inheritDoc} */ 518 public void monitor() { 519 synchronized (mDaemonLock) { } 520 } 521 522 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 523 mLocalLog.dump(fd, pw, args); 524 pw.println(); 525 mResponseQueue.dump(fd, pw, args); 526 } 527 528 private void log(String logstring) { 529 if (mDebug) Slog.d(TAG, logstring); 530 mLocalLog.log(logstring); 531 } 532 533 private void loge(String logstring) { 534 Slog.e(TAG, logstring); 535 mLocalLog.log(logstring); 536 } 537 538 private static class ResponseQueue { 539 540 private static class PendingCmd { 541 public final int cmdNum; 542 public final String logCmd; 543 544 public BlockingQueue<NativeDaemonEvent> responses = 545 new ArrayBlockingQueue<NativeDaemonEvent>(10); 546 547 // The availableResponseCount member is used to track when we can remove this 548 // instance from the ResponseQueue. 549 // This is used under the protection of a sync of the mPendingCmds object. 550 // A positive value means we've had more writers retreive this object while 551 // a negative value means we've had more readers. When we've had an equal number 552 // (it goes to zero) we can remove this object from the mPendingCmds list. 553 // Note that we may have more responses for this command (and more readers 554 // coming), but that would result in a new PendingCmd instance being created 555 // and added with the same cmdNum. 556 // Also note that when this goes to zero it just means a parity of readers and 557 // writers have retrieved this object - not that they are done using it. The 558 // responses queue may well have more responses yet to be read or may get more 559 // responses added to it. But all those readers/writers have retreived and 560 // hold references to this instance already so it can be removed from 561 // mPendingCmds queue. 562 public int availableResponseCount; 563 564 public PendingCmd(int cmdNum, String logCmd) { 565 this.cmdNum = cmdNum; 566 this.logCmd = logCmd; 567 } 568 } 569 570 private final LinkedList<PendingCmd> mPendingCmds; 571 private int mMaxCount; 572 573 ResponseQueue(int maxCount) { 574 mPendingCmds = new LinkedList<PendingCmd>(); 575 mMaxCount = maxCount; 576 } 577 578 public void add(int cmdNum, NativeDaemonEvent response) { 579 PendingCmd found = null; 580 synchronized (mPendingCmds) { 581 for (PendingCmd pendingCmd : mPendingCmds) { 582 if (pendingCmd.cmdNum == cmdNum) { 583 found = pendingCmd; 584 break; 585 } 586 } 587 if (found == null) { 588 // didn't find it - make sure our queue isn't too big before adding 589 while (mPendingCmds.size() >= mMaxCount) { 590 Slog.e("NativeDaemonConnector.ResponseQueue", 591 "more buffered than allowed: " + mPendingCmds.size() + 592 " >= " + mMaxCount); 593 // let any waiter timeout waiting for this 594 PendingCmd pendingCmd = mPendingCmds.remove(); 595 Slog.e("NativeDaemonConnector.ResponseQueue", 596 "Removing request: " + pendingCmd.logCmd + " (" + 597 pendingCmd.cmdNum + ")"); 598 } 599 found = new PendingCmd(cmdNum, null); 600 mPendingCmds.add(found); 601 } 602 found.availableResponseCount++; 603 // if a matching remove call has already retrieved this we can remove this 604 // instance from our list 605 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 606 } 607 try { 608 found.responses.put(response); 609 } catch (InterruptedException e) { } 610 } 611 612 // note that the timeout does not count time in deep sleep. If you don't want 613 // the device to sleep, hold a wakelock 614 public NativeDaemonEvent remove(int cmdNum, long timeoutMs, String logCmd) { 615 PendingCmd found = null; 616 synchronized (mPendingCmds) { 617 for (PendingCmd pendingCmd : mPendingCmds) { 618 if (pendingCmd.cmdNum == cmdNum) { 619 found = pendingCmd; 620 break; 621 } 622 } 623 if (found == null) { 624 found = new PendingCmd(cmdNum, logCmd); 625 mPendingCmds.add(found); 626 } 627 found.availableResponseCount--; 628 // if a matching add call has already retrieved this we can remove this 629 // instance from our list 630 if (found.availableResponseCount == 0) mPendingCmds.remove(found); 631 } 632 NativeDaemonEvent result = null; 633 try { 634 result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS); 635 } catch (InterruptedException e) {} 636 if (result == null) { 637 Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response"); 638 } 639 return result; 640 } 641 642 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 643 pw.println("Pending requests:"); 644 synchronized (mPendingCmds) { 645 for (PendingCmd pendingCmd : mPendingCmds) { 646 pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd); 647 } 648 } 649 } 650 } 651} 652