NativeDaemonConnector.java revision f0be1d89bf1cf5592ea1786d837f4f2329bdf66d
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.Handler; 22import android.os.HandlerThread; 23import android.os.Message; 24import android.os.SystemClock; 25import android.util.LocalLog; 26import android.util.Slog; 27 28import com.google.android.collect.Lists; 29 30import java.nio.charset.Charsets; 31import java.io.FileDescriptor; 32import java.io.IOException; 33import java.io.InputStream; 34import java.io.OutputStream; 35import java.io.PrintWriter; 36import java.util.ArrayList; 37import java.util.concurrent.BlockingQueue; 38import java.util.concurrent.LinkedBlockingQueue; 39 40/** 41 * Generic connector class for interfacing with a native daemon which uses the 42 * {@code libsysutils} FrameworkListener protocol. 43 */ 44final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor { 45 private static final boolean LOGD = false; 46 47 private final String TAG; 48 49 private String mSocket; 50 private OutputStream mOutputStream; 51 private LocalLog mLocalLog; 52 53 private final BlockingQueue<NativeDaemonEvent> mResponseQueue; 54 55 private INativeDaemonConnectorCallbacks mCallbacks; 56 private Handler mCallbackHandler; 57 58 /** Lock held whenever communicating with native daemon. */ 59 private final Object mDaemonLock = new Object(); 60 61 private final int BUFFER_SIZE = 4096; 62 63 NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket, 64 int responseQueueSize, String logTag, int maxLogSize) { 65 mCallbacks = callbacks; 66 mSocket = socket; 67 mResponseQueue = new LinkedBlockingQueue<NativeDaemonEvent>(responseQueueSize); 68 TAG = logTag != null ? logTag : "NativeDaemonConnector"; 69 mLocalLog = new LocalLog(maxLogSize); 70 } 71 72 @Override 73 public void run() { 74 HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); 75 thread.start(); 76 mCallbackHandler = new Handler(thread.getLooper(), this); 77 78 while (true) { 79 try { 80 listenToSocket(); 81 } catch (Exception e) { 82 Slog.e(TAG, "Error in NativeDaemonConnector", e); 83 SystemClock.sleep(5000); 84 } 85 } 86 } 87 88 @Override 89 public boolean handleMessage(Message msg) { 90 String event = (String) msg.obj; 91 try { 92 if (!mCallbacks.onEvent(msg.what, event, event.split(" "))) { 93 Slog.w(TAG, String.format( 94 "Unhandled event '%s'", event)); 95 } 96 } catch (Exception e) { 97 Slog.e(TAG, String.format( 98 "Error handling '%s'", event), e); 99 } 100 return true; 101 } 102 103 private void listenToSocket() throws IOException { 104 LocalSocket socket = null; 105 106 try { 107 socket = new LocalSocket(); 108 LocalSocketAddress address = new LocalSocketAddress(mSocket, 109 LocalSocketAddress.Namespace.RESERVED); 110 111 socket.connect(address); 112 113 InputStream inputStream = socket.getInputStream(); 114 mOutputStream = socket.getOutputStream(); 115 116 mCallbacks.onDaemonConnected(); 117 118 byte[] buffer = new byte[BUFFER_SIZE]; 119 int start = 0; 120 121 while (true) { 122 int count = inputStream.read(buffer, start, BUFFER_SIZE - start); 123 if (count < 0) break; 124 125 // Add our starting point to the count and reset the start. 126 count += start; 127 start = 0; 128 129 for (int i = 0; i < count; i++) { 130 if (buffer[i] == 0) { 131 final String rawEvent = new String( 132 buffer, start, i - start, Charsets.UTF_8); 133 log("RCV <- {" + rawEvent + "}"); 134 135 try { 136 final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent( 137 rawEvent); 138 if (event.isClassUnsolicited()) { 139 // TODO: migrate to sending NativeDaemonEvent instances 140 mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage( 141 event.getCode(), event.getRawEvent())); 142 } else { 143 try { 144 mResponseQueue.put(event); 145 } catch (InterruptedException ex) { 146 Slog.e(TAG, "Failed to put response onto queue: " + ex); 147 } 148 } 149 } catch (IllegalArgumentException e) { 150 Slog.w(TAG, "Problem parsing message: " + rawEvent, e); 151 } 152 153 start = i + 1; 154 } 155 } 156 if (start == 0) { 157 final String rawEvent = new String(buffer, start, count, Charsets.UTF_8); 158 log("RCV incomplete <- {" + rawEvent + "}"); 159 } 160 161 // We should end at the amount we read. If not, compact then 162 // buffer and read again. 163 if (start != count) { 164 final int remaining = BUFFER_SIZE - start; 165 System.arraycopy(buffer, start, buffer, 0, remaining); 166 start = remaining; 167 } else { 168 start = 0; 169 } 170 } 171 } catch (IOException ex) { 172 Slog.e(TAG, "Communications error", ex); 173 throw ex; 174 } finally { 175 synchronized (mDaemonLock) { 176 if (mOutputStream != null) { 177 try { 178 mOutputStream.close(); 179 } catch (IOException e) { 180 Slog.w(TAG, "Failed closing output stream", e); 181 } 182 mOutputStream = null; 183 } 184 } 185 186 try { 187 if (socket != null) { 188 socket.close(); 189 } 190 } catch (IOException ex) { 191 Slog.w(TAG, "Failed closing socket", ex); 192 } 193 } 194 } 195 196 /** 197 * Send command to daemon, escaping arguments as needed. 198 * 199 * @return the final command issued. 200 */ 201 private String sendCommandLocked(String cmd, Object... args) 202 throws NativeDaemonConnectorException { 203 // TODO: eventually enforce that cmd doesn't contain arguments 204 if (cmd.indexOf('\0') >= 0) { 205 throw new IllegalArgumentException("unexpected command: " + cmd); 206 } 207 208 final StringBuilder builder = new StringBuilder(cmd); 209 for (Object arg : args) { 210 final String argString = String.valueOf(arg); 211 if (argString.indexOf('\0') >= 0) { 212 throw new IllegalArgumentException("unexpected argument: " + arg); 213 } 214 215 builder.append(' '); 216 appendEscaped(builder, argString); 217 } 218 219 final String unterminated = builder.toString(); 220 log("SND -> {" + unterminated + "}"); 221 222 builder.append('\0'); 223 224 if (mOutputStream == null) { 225 throw new NativeDaemonConnectorException("missing output stream"); 226 } else { 227 try { 228 mOutputStream.write(builder.toString().getBytes(Charsets.UTF_8)); 229 } catch (IOException e) { 230 throw new NativeDaemonConnectorException("problem sending command", e); 231 } 232 } 233 234 return unterminated; 235 } 236 237 /** 238 * Issue the given command to the native daemon and return a single expected 239 * response. 240 * 241 * @throws NativeDaemonConnectorException when problem communicating with 242 * native daemon, or if the response matches 243 * {@link NativeDaemonEvent#isClassClientError()} or 244 * {@link NativeDaemonEvent#isClassServerError()}. 245 */ 246 public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException { 247 return execute(cmd.mCmd, cmd.mArguments.toArray()); 248 } 249 250 /** 251 * Issue the given command to the native daemon and return a single expected 252 * response. 253 * 254 * @throws NativeDaemonConnectorException when problem communicating with 255 * native daemon, or if the response matches 256 * {@link NativeDaemonEvent#isClassClientError()} or 257 * {@link NativeDaemonEvent#isClassServerError()}. 258 */ 259 public NativeDaemonEvent execute(String cmd, Object... args) 260 throws NativeDaemonConnectorException { 261 final NativeDaemonEvent[] events = executeForList(cmd, args); 262 if (events.length != 1) { 263 throw new NativeDaemonConnectorException( 264 "Expected exactly one response, but received " + events.length); 265 } 266 return events[0]; 267 } 268 269 /** 270 * Issue the given command to the native daemon and return any 271 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 272 * final terminal response. 273 * 274 * @throws NativeDaemonConnectorException when problem communicating with 275 * native daemon, or if the response matches 276 * {@link NativeDaemonEvent#isClassClientError()} or 277 * {@link NativeDaemonEvent#isClassServerError()}. 278 */ 279 public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException { 280 return executeForList(cmd.mCmd, cmd.mArguments.toArray()); 281 } 282 283 /** 284 * Issue the given command to the native daemon and return any 285 * {@link NativeDaemonEvent#isClassContinue()} responses, including the 286 * final terminal response. 287 * 288 * @throws NativeDaemonConnectorException when problem communicating with 289 * native daemon, or if the response matches 290 * {@link NativeDaemonEvent#isClassClientError()} or 291 * {@link NativeDaemonEvent#isClassServerError()}. 292 */ 293 public NativeDaemonEvent[] executeForList(String cmd, Object... args) 294 throws NativeDaemonConnectorException { 295 synchronized (mDaemonLock) { 296 return executeLocked(cmd, args); 297 } 298 } 299 300 private NativeDaemonEvent[] executeLocked(String cmd, Object... args) 301 throws NativeDaemonConnectorException { 302 final ArrayList<NativeDaemonEvent> events = Lists.newArrayList(); 303 304 while (mResponseQueue.size() > 0) { 305 try { 306 log("ignoring {" + mResponseQueue.take() + "}"); 307 } catch (Exception e) {} 308 } 309 310 final String sentCommand = sendCommandLocked(cmd, args); 311 312 NativeDaemonEvent event = null; 313 do { 314 try { 315 event = mResponseQueue.take(); 316 } catch (InterruptedException e) { 317 Slog.w(TAG, "interrupted waiting for event line"); 318 continue; 319 } 320 events.add(event); 321 } while (event.isClassContinue()); 322 323 if (event.isClassClientError()) { 324 throw new NativeDaemonArgumentException(sentCommand, event); 325 } 326 if (event.isClassServerError()) { 327 throw new NativeDaemonFailureException(sentCommand, event); 328 } 329 330 return events.toArray(new NativeDaemonEvent[events.size()]); 331 } 332 333 /** 334 * Issue a command to the native daemon and return the raw responses. 335 * 336 * @deprecated callers should move to {@link #execute(String, Object...)} 337 * which returns parsed {@link NativeDaemonEvent}. 338 */ 339 @Deprecated 340 public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException { 341 final ArrayList<String> rawEvents = Lists.newArrayList(); 342 final NativeDaemonEvent[] events = executeForList(cmd); 343 for (NativeDaemonEvent event : events) { 344 rawEvents.add(event.getRawEvent()); 345 } 346 return rawEvents; 347 } 348 349 /** 350 * Issues a list command and returns the cooked list of all 351 * {@link NativeDaemonEvent#getMessage()} which match requested code. 352 */ 353 @Deprecated 354 public String[] doListCommand(String cmd, int expectedCode) 355 throws NativeDaemonConnectorException { 356 final ArrayList<String> list = Lists.newArrayList(); 357 358 final NativeDaemonEvent[] events = executeForList(cmd); 359 for (int i = 0; i < events.length - 1; i++) { 360 final NativeDaemonEvent event = events[i]; 361 final int code = event.getCode(); 362 if (code == expectedCode) { 363 list.add(event.getMessage()); 364 } else { 365 throw new NativeDaemonConnectorException( 366 "unexpected list response " + code + " instead of " + expectedCode); 367 } 368 } 369 370 final NativeDaemonEvent finalEvent = events[events.length - 1]; 371 if (!finalEvent.isClassOk()) { 372 throw new NativeDaemonConnectorException("unexpected final event: " + finalEvent); 373 } 374 375 return list.toArray(new String[list.size()]); 376 } 377 378 /** 379 * Append the given argument to {@link StringBuilder}, escaping as needed, 380 * and surrounding with quotes when it contains spaces. 381 */ 382 // @VisibleForTesting 383 static void appendEscaped(StringBuilder builder, String arg) { 384 final boolean hasSpaces = arg.indexOf(' ') >= 0; 385 if (hasSpaces) { 386 builder.append('"'); 387 } 388 389 final int length = arg.length(); 390 for (int i = 0; i < length; i++) { 391 final char c = arg.charAt(i); 392 393 if (c == '"') { 394 builder.append("\\\""); 395 } else if (c == '\\') { 396 builder.append("\\\\"); 397 } else { 398 builder.append(c); 399 } 400 } 401 402 if (hasSpaces) { 403 builder.append('"'); 404 } 405 } 406 407 private static class NativeDaemonArgumentException extends NativeDaemonConnectorException { 408 public NativeDaemonArgumentException(String command, NativeDaemonEvent event) { 409 super(command, event); 410 } 411 412 @Override 413 public IllegalArgumentException rethrowAsParcelableException() { 414 throw new IllegalArgumentException(getMessage(), this); 415 } 416 } 417 418 private static class NativeDaemonFailureException extends NativeDaemonConnectorException { 419 public NativeDaemonFailureException(String command, NativeDaemonEvent event) { 420 super(command, event); 421 } 422 } 423 424 /** 425 * Command builder that handles argument list building. 426 */ 427 public static class Command { 428 private String mCmd; 429 private ArrayList<Object> mArguments = Lists.newArrayList(); 430 431 public Command(String cmd, Object... args) { 432 mCmd = cmd; 433 for (Object arg : args) { 434 appendArg(arg); 435 } 436 } 437 438 public Command appendArg(Object arg) { 439 mArguments.add(arg); 440 return this; 441 } 442 } 443 444 /** {@inheritDoc} */ 445 public void monitor() { 446 synchronized (mDaemonLock) { } 447 } 448 449 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 450 mLocalLog.dump(fd, pw, args); 451 } 452 453 private void log(String logstring) { 454 if (LOGD) Slog.d(TAG, logstring); 455 mLocalLog.log(logstring); 456 } 457} 458