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