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