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