Files
frameworks_base/services/java/com/android/server/NativeDaemonConnector.java
Robert Greenwalt 470007f69a Make NDC not block forever and handle restart.
Still possible for netd to get wedged but system won't die because of it.
Leads the way to having forking netd - then only individual commands would
wedge, promoting stability.

bug:5864209
bug:6019952
Change-Id: I43e4c5072863b8b812d4fe24d30d92eb1a11651a
2012-02-29 16:53:27 -08:00

587 lines
21 KiB
Java

/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.util.LocalLog;
import android.util.Slog;
import com.google.android.collect.Lists;
import java.nio.charset.Charsets;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.LinkedList;
/**
* Generic connector class for interfacing with a native daemon which uses the
* {@code libsysutils} FrameworkListener protocol.
*/
final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {
private static final boolean LOGD = false;
private final String TAG;
private String mSocket;
private OutputStream mOutputStream;
private LocalLog mLocalLog;
private final ResponseQueue mResponseQueue;
private INativeDaemonConnectorCallbacks mCallbacks;
private Handler mCallbackHandler;
private AtomicInteger mSequenceNumber;
private static final int DEFAULT_TIMEOUT = 1 * 60 * 1000; /* 1 minute */
/** Lock held whenever communicating with native daemon. */
private final Object mDaemonLock = new Object();
private final int BUFFER_SIZE = 4096;
NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
int responseQueueSize, String logTag, int maxLogSize) {
mCallbacks = callbacks;
mSocket = socket;
mResponseQueue = new ResponseQueue(responseQueueSize);
mSequenceNumber = new AtomicInteger(0);
TAG = logTag != null ? logTag : "NativeDaemonConnector";
mLocalLog = new LocalLog(maxLogSize);
}
@Override
public void run() {
HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
thread.start();
mCallbackHandler = new Handler(thread.getLooper(), this);
while (true) {
try {
listenToSocket();
} catch (Exception e) {
loge("Error in NativeDaemonConnector: " + e);
SystemClock.sleep(5000);
}
}
}
@Override
public boolean handleMessage(Message msg) {
String event = (String) msg.obj;
try {
if (!mCallbacks.onEvent(msg.what, event, event.split(" "))) {
log(String.format("Unhandled event '%s'", event));
}
} catch (Exception e) {
loge("Error handling '" + event + "': " + e);
}
return true;
}
private void listenToSocket() throws IOException {
LocalSocket socket = null;
try {
socket = new LocalSocket();
LocalSocketAddress address = new LocalSocketAddress(mSocket,
LocalSocketAddress.Namespace.RESERVED);
socket.connect(address);
InputStream inputStream = socket.getInputStream();
synchronized (mDaemonLock) {
mOutputStream = socket.getOutputStream();
}
mCallbacks.onDaemonConnected();
byte[] buffer = new byte[BUFFER_SIZE];
int start = 0;
while (true) {
int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
if (count < 0) {
loge("got " + count + " reading with start = " + start);
break;
}
// Add our starting point to the count and reset the start.
count += start;
start = 0;
for (int i = 0; i < count; i++) {
if (buffer[i] == 0) {
final String rawEvent = new String(
buffer, start, i - start, Charsets.UTF_8);
log("RCV <- {" + rawEvent + "}");
try {
final NativeDaemonEvent event = NativeDaemonEvent.parseRawEvent(
rawEvent);
if (event.isClassUnsolicited()) {
// TODO: migrate to sending NativeDaemonEvent instances
mCallbackHandler.sendMessage(mCallbackHandler.obtainMessage(
event.getCode(), event.getRawEvent()));
} else {
mResponseQueue.add(event.getCmdNumber(), event);
}
} catch (IllegalArgumentException e) {
log("Problem parsing message: " + rawEvent + " - " + e);
}
start = i + 1;
}
}
if (start == 0) {
final String rawEvent = new String(buffer, start, count, Charsets.UTF_8);
log("RCV incomplete <- {" + rawEvent + "}");
}
// We should end at the amount we read. If not, compact then
// buffer and read again.
if (start != count) {
final int remaining = BUFFER_SIZE - start;
System.arraycopy(buffer, start, buffer, 0, remaining);
start = remaining;
} else {
start = 0;
}
}
} catch (IOException ex) {
loge("Communications error: " + ex);
throw ex;
} finally {
synchronized (mDaemonLock) {
if (mOutputStream != null) {
try {
loge("closing stream for " + mSocket);
mOutputStream.close();
} catch (IOException e) {
loge("Failed closing output stream: " + e);
}
mOutputStream = null;
}
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException ex) {
loge("Failed closing socket: " + ex);
}
}
}
/**
* Make command for daemon, escaping arguments as needed.
*
* @return the final command.
*/
private StringBuilder makeCommand(String cmd, Object... args)
throws NativeDaemonConnectorException {
// TODO: eventually enforce that cmd doesn't contain arguments
if (cmd.indexOf('\0') >= 0) {
throw new IllegalArgumentException("unexpected command: " + cmd);
}
final StringBuilder builder = new StringBuilder(cmd);
for (Object arg : args) {
final String argString = String.valueOf(arg);
if (argString.indexOf('\0') >= 0) {
throw new IllegalArgumentException("unexpected argument: " + arg);
}
builder.append(' ');
appendEscaped(builder, argString);
}
return builder;
}
private int sendCommand(StringBuilder builder)
throws NativeDaemonConnectorException {
int sequenceNumber = mSequenceNumber.incrementAndGet();
builder.insert(0, Integer.toString(sequenceNumber) + " ");
log("SND -> {" + builder.toString() + "}");
builder.append('\0');
synchronized (mDaemonLock) {
if (mOutputStream == null) {
throw new NativeDaemonConnectorException("missing output stream");
} else {
try {
mOutputStream.write(builder.toString().getBytes(Charsets.UTF_8));
} catch (IOException e) {
throw new NativeDaemonConnectorException("problem sending command", e);
}
}
}
return sequenceNumber;
}
/**
* Issue the given command to the native daemon and return a single expected
* response.
*
* @throws NativeDaemonConnectorException when problem communicating with
* native daemon, or if the response matches
* {@link NativeDaemonEvent#isClassClientError()} or
* {@link NativeDaemonEvent#isClassServerError()}.
*/
public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
return execute(cmd.mCmd, cmd.mArguments.toArray());
}
/**
* Issue the given command to the native daemon and return a single expected
* response.
*
* @throws NativeDaemonConnectorException when problem communicating with
* native daemon, or if the response matches
* {@link NativeDaemonEvent#isClassClientError()} or
* {@link NativeDaemonEvent#isClassServerError()}.
*/
public NativeDaemonEvent execute(String cmd, Object... args)
throws NativeDaemonConnectorException {
final NativeDaemonEvent[] events = executeForList(cmd, args);
if (events.length != 1) {
throw new NativeDaemonConnectorException(
"Expected exactly one response, but received " + events.length);
}
return events[0];
}
/**
* Issue the given command to the native daemon and return any
* {@link NativeDaemonEvent#isClassContinue()} responses, including the
* final terminal response.
*
* @throws NativeDaemonConnectorException when problem communicating with
* native daemon, or if the response matches
* {@link NativeDaemonEvent#isClassClientError()} or
* {@link NativeDaemonEvent#isClassServerError()}.
*/
public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
return executeForList(cmd.mCmd, cmd.mArguments.toArray());
}
/**
* Issue the given command to the native daemon and return any
* {@link NativeDaemonEvent#isClassContinue()} responses, including the
* final terminal response.
*
* @throws NativeDaemonConnectorException when problem communicating with
* native daemon, or if the response matches
* {@link NativeDaemonEvent#isClassClientError()} or
* {@link NativeDaemonEvent#isClassServerError()}.
*/
public NativeDaemonEvent[] executeForList(String cmd, Object... args)
throws NativeDaemonConnectorException {
return execute(DEFAULT_TIMEOUT, cmd, args);
}
/**
* Issue the given command to the native daemon and return any
* {@linke NativeDaemonEvent@isClassContinue()} responses, including the
* final terminal response. Note that the timeout does not count time in
* deep sleep.
*
* @throws NativeDaemonConnectorException when problem communicating with
* native daemon, or if the response matches
* {@link NativeDaemonEvent#isClassClientError()} or
* {@link NativeDaemonEvent#isClassServerError()}.
*/
public NativeDaemonEvent[] execute(int timeout, String cmd, Object... args)
throws NativeDaemonConnectorException {
final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
final StringBuilder sentCommand = makeCommand(cmd, args);
final int cmdNumber = sendCommand(sentCommand);
NativeDaemonEvent event = null;
cmd = sentCommand.toString();
do {
event = mResponseQueue.remove(cmdNumber, timeout, cmd);
if (event == null) {
loge("timed-out waiting for response to " + cmdNumber + " " + cmd);
throw new NativeDaemonFailureException(cmd, event);
}
events.add(event);
} while (event.isClassContinue());
if (event.isClassClientError()) {
throw new NativeDaemonArgumentException(cmd, event);
}
if (event.isClassServerError()) {
throw new NativeDaemonFailureException(cmd, event);
}
return events.toArray(new NativeDaemonEvent[events.size()]);
}
/**
* Issue a command to the native daemon and return the raw responses.
*
* @deprecated callers should move to {@link #execute(String, Object...)}
* which returns parsed {@link NativeDaemonEvent}.
*/
@Deprecated
public ArrayList<String> doCommand(String cmd) throws NativeDaemonConnectorException {
final ArrayList<String> rawEvents = Lists.newArrayList();
final NativeDaemonEvent[] events = executeForList(cmd);
for (NativeDaemonEvent event : events) {
rawEvents.add(event.getRawEvent());
}
return rawEvents;
}
/**
* Issues a list command and returns the cooked list of all
* {@link NativeDaemonEvent#getMessage()} which match requested code.
*/
@Deprecated
public String[] doListCommand(String cmd, int expectedCode)
throws NativeDaemonConnectorException {
final ArrayList<String> list = Lists.newArrayList();
final NativeDaemonEvent[] events = executeForList(cmd);
for (int i = 0; i < events.length - 1; i++) {
final NativeDaemonEvent event = events[i];
final int code = event.getCode();
if (code == expectedCode) {
list.add(event.getMessage());
} else {
throw new NativeDaemonConnectorException(
"unexpected list response " + code + " instead of " + expectedCode);
}
}
final NativeDaemonEvent finalEvent = events[events.length - 1];
if (!finalEvent.isClassOk()) {
throw new NativeDaemonConnectorException("unexpected final event: " + finalEvent);
}
return list.toArray(new String[list.size()]);
}
/**
* Append the given argument to {@link StringBuilder}, escaping as needed,
* and surrounding with quotes when it contains spaces.
*/
// @VisibleForTesting
static void appendEscaped(StringBuilder builder, String arg) {
final boolean hasSpaces = arg.indexOf(' ') >= 0;
if (hasSpaces) {
builder.append('"');
}
final int length = arg.length();
for (int i = 0; i < length; i++) {
final char c = arg.charAt(i);
if (c == '"') {
builder.append("\\\"");
} else if (c == '\\') {
builder.append("\\\\");
} else {
builder.append(c);
}
}
if (hasSpaces) {
builder.append('"');
}
}
private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
super(command, event);
}
@Override
public IllegalArgumentException rethrowAsParcelableException() {
throw new IllegalArgumentException(getMessage(), this);
}
}
private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
super(command, event);
}
}
/**
* Command builder that handles argument list building.
*/
public static class Command {
private String mCmd;
private ArrayList<Object> mArguments = Lists.newArrayList();
public Command(String cmd, Object... args) {
mCmd = cmd;
for (Object arg : args) {
appendArg(arg);
}
}
public Command appendArg(Object arg) {
mArguments.add(arg);
return this;
}
}
/** {@inheritDoc} */
public void monitor() {
synchronized (mDaemonLock) { }
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mLocalLog.dump(fd, pw, args);
pw.println();
mResponseQueue.dump(fd, pw, args);
}
private void log(String logstring) {
if (LOGD) Slog.d(TAG, logstring);
mLocalLog.log(logstring);
}
private void loge(String logstring) {
Slog.e(TAG, logstring);
mLocalLog.log(logstring);
}
private static class ResponseQueue {
private static class Response {
public int cmdNum;
public LinkedList<NativeDaemonEvent> responses = new LinkedList<NativeDaemonEvent>();
public String request;
public Response(int c, String r) {cmdNum = c; request = r;}
}
private final LinkedList<Response> mResponses;
private int mMaxCount;
ResponseQueue(int maxCount) {
mResponses = new LinkedList<Response>();
mMaxCount = maxCount;
}
public void add(int cmdNum, NativeDaemonEvent response) {
Response found = null;
synchronized (mResponses) {
for (Response r : mResponses) {
if (r.cmdNum == cmdNum) {
found = r;
break;
}
}
if (found == null) {
// didn't find it - make sure our queue isn't too big before adding
// another..
while (mResponses.size() >= mMaxCount) {
Slog.e("NativeDaemonConnector.ResponseQueue",
"more buffered than allowed: " + mResponses.size() +
" >= " + mMaxCount);
// let any waiter timeout waiting for this
Response r = mResponses.remove();
Slog.e("NativeDaemonConnector.ResponseQueue",
"Removing request: " + r.request + " (" + r.cmdNum + ")");
}
found = new Response(cmdNum, null);
mResponses.add(found);
}
found.responses.add(response);
}
synchronized (found) {
found.notify();
}
}
// note that the timeout does not count time in deep sleep. If you don't want
// the device to sleep, hold a wakelock
public NativeDaemonEvent remove(int cmdNum, int timeoutMs, String origCmd) {
long endTime = SystemClock.uptimeMillis() + timeoutMs;
long nowTime;
Response found = null;
while (true) {
synchronized (mResponses) {
for (Response response : mResponses) {
if (response.cmdNum == cmdNum) {
found = response;
// how many response fragments are left
switch (response.responses.size()) {
case 0: // haven't got any - must wait
break;
case 1: // last one - remove this from the master list
mResponses.remove(response); // fall through
default: // take one and move on
response.request = origCmd;
return response.responses.remove();
}
}
}
nowTime = SystemClock.uptimeMillis();
if (endTime <= nowTime) {
Slog.e("NativeDaemonConnector.ResponseQueue",
"Timeout waiting for response");
return null;
}
/* pre-allocate so we have something unique to wait on */
if (found == null) {
found = new Response(cmdNum, origCmd);
mResponses.add(found);
}
}
try {
synchronized (found) {
found.wait(endTime - nowTime);
}
} catch (InterruptedException e) {
// loop around to check if we're done or if it's time to stop waiting
}
}
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Pending requests:");
synchronized (mResponses) {
for (Response response : mResponses) {
pw.println(" Cmd " + response.cmdNum + " - " + response.request);
}
}
}
}
}