We had a gap in sync coverage between doing a check and waiting and a matching gap between setting a condition and notifying. It was possible to get context switched just so and have the notify hit before the waiter had started waiting. bug:6492166 Change-Id: Idc876cf85b35902a79fae932547957ed5ef00e4f
595 lines
22 KiB
Java
595 lines
22 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.concurrent.ArrayBlockingQueue;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.TimeUnit;
|
|
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 */
|
|
private static final long WARN_EXECUTE_DELAY_MS = 500; /* .5 sec */
|
|
|
|
/** 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, NativeDaemonEvent.unescapeArgs(event))) {
|
|
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.
|
|
*/
|
|
private void makeCommand(StringBuilder builder, 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);
|
|
}
|
|
|
|
builder.append(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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 int sequenceNumber = mSequenceNumber.incrementAndGet();
|
|
final StringBuilder cmdBuilder =
|
|
new StringBuilder(Integer.toString(sequenceNumber)).append(' ');
|
|
final long startTime = SystemClock.elapsedRealtime();
|
|
|
|
makeCommand(cmdBuilder, cmd, args);
|
|
|
|
final String logCmd = cmdBuilder.toString(); /* includes cmdNum, cmd, args */
|
|
log("SND -> {" + logCmd + "}");
|
|
|
|
cmdBuilder.append('\0');
|
|
final String sentCmd = cmdBuilder.toString(); /* logCmd + \0 */
|
|
|
|
synchronized (mDaemonLock) {
|
|
if (mOutputStream == null) {
|
|
throw new NativeDaemonConnectorException("missing output stream");
|
|
} else {
|
|
try {
|
|
mOutputStream.write(sentCmd.getBytes(Charsets.UTF_8));
|
|
} catch (IOException e) {
|
|
throw new NativeDaemonConnectorException("problem sending command", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
NativeDaemonEvent event = null;
|
|
do {
|
|
event = mResponseQueue.remove(sequenceNumber, timeout, sentCmd);
|
|
if (event == null) {
|
|
loge("timed-out waiting for response to " + logCmd);
|
|
throw new NativeDaemonFailureException(logCmd, event);
|
|
}
|
|
log("RMV <- {" + event + "}");
|
|
events.add(event);
|
|
} while (event.isClassContinue());
|
|
|
|
final long endTime = SystemClock.elapsedRealtime();
|
|
if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
|
|
loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
|
|
}
|
|
|
|
if (event.isClassClientError()) {
|
|
throw new NativeDaemonArgumentException(logCmd, event);
|
|
}
|
|
if (event.isClassServerError()) {
|
|
throw new NativeDaemonFailureException(logCmd, 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 PendingCmd {
|
|
public int cmdNum;
|
|
public BlockingQueue<NativeDaemonEvent> responses =
|
|
new ArrayBlockingQueue<NativeDaemonEvent>(10);
|
|
public String request;
|
|
|
|
// The availableResponseCount member is used to track when we can remove this
|
|
// instance from the ResponseQueue.
|
|
// This is used under the protection of a sync of the mPendingCmds object.
|
|
// A positive value means we've had more writers retreive this object while
|
|
// a negative value means we've had more readers. When we've had an equal number
|
|
// (it goes to zero) we can remove this object from the mPendingCmds list.
|
|
// Note that we may have more responses for this command (and more readers
|
|
// coming), but that would result in a new PendingCmd instance being created
|
|
// and added with the same cmdNum.
|
|
// Also note that when this goes to zero it just means a parity of readers and
|
|
// writers have retrieved this object - not that they are done using it. The
|
|
// responses queue may well have more responses yet to be read or may get more
|
|
// responses added to it. But all those readers/writers have retreived and
|
|
// hold references to this instance already so it can be removed from
|
|
// mPendingCmds queue.
|
|
public int availableResponseCount;
|
|
public PendingCmd(int c, String r) {cmdNum = c; request = r;}
|
|
}
|
|
|
|
private final LinkedList<PendingCmd> mPendingCmds;
|
|
private int mMaxCount;
|
|
|
|
ResponseQueue(int maxCount) {
|
|
mPendingCmds = new LinkedList<PendingCmd>();
|
|
mMaxCount = maxCount;
|
|
}
|
|
|
|
public void add(int cmdNum, NativeDaemonEvent response) {
|
|
PendingCmd found = null;
|
|
synchronized (mPendingCmds) {
|
|
for (PendingCmd pendingCmd : mPendingCmds) {
|
|
if (pendingCmd.cmdNum == cmdNum) {
|
|
found = pendingCmd;
|
|
break;
|
|
}
|
|
}
|
|
if (found == null) {
|
|
// didn't find it - make sure our queue isn't too big before adding
|
|
while (mPendingCmds.size() >= mMaxCount) {
|
|
Slog.e("NativeDaemonConnector.ResponseQueue",
|
|
"more buffered than allowed: " + mPendingCmds.size() +
|
|
" >= " + mMaxCount);
|
|
// let any waiter timeout waiting for this
|
|
PendingCmd pendingCmd = mPendingCmds.remove();
|
|
Slog.e("NativeDaemonConnector.ResponseQueue",
|
|
"Removing request: " + pendingCmd.request + " (" +
|
|
pendingCmd.cmdNum + ")");
|
|
}
|
|
found = new PendingCmd(cmdNum, null);
|
|
mPendingCmds.add(found);
|
|
}
|
|
found.availableResponseCount++;
|
|
// if a matching remove call has already retrieved this we can remove this
|
|
// instance from our list
|
|
if (found.availableResponseCount == 0) mPendingCmds.remove(found);
|
|
}
|
|
try {
|
|
found.responses.put(response);
|
|
} catch (InterruptedException e) { }
|
|
}
|
|
|
|
// 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) {
|
|
PendingCmd found = null;
|
|
synchronized (mPendingCmds) {
|
|
for (PendingCmd pendingCmd : mPendingCmds) {
|
|
if (pendingCmd.cmdNum == cmdNum) {
|
|
found = pendingCmd;
|
|
break;
|
|
}
|
|
}
|
|
if (found == null) {
|
|
found = new PendingCmd(cmdNum, origCmd);
|
|
mPendingCmds.add(found);
|
|
}
|
|
found.availableResponseCount--;
|
|
// if a matching add call has already retrieved this we can remove this
|
|
// instance from our list
|
|
if (found.availableResponseCount == 0) mPendingCmds.remove(found);
|
|
}
|
|
NativeDaemonEvent result = null;
|
|
try {
|
|
result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS);
|
|
} catch (InterruptedException e) {}
|
|
if (result == null) {
|
|
Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
|
pw.println("Pending requests:");
|
|
synchronized (mPendingCmds) {
|
|
for (PendingCmd pendingCmd : mPendingCmds) {
|
|
pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.request);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|