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