Remote TF refactor part one.
Clean up current remote manager protocol to use JSON encoded payloads, and
set the stage for splitting out client into a separate library.
Bug: 10919026
Change-Id: Ie6ecc8f6fa42c133a7e2b93861d74115ae8e4a64
diff --git a/.classpath b/.classpath
index b56c2ca..ae5e1f9 100644
--- a/.classpath
+++ b/.classpath
@@ -7,6 +7,7 @@
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jline-1.0_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/jline/src"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/junit/src"/>
+ <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/Android.mk b/Android.mk
index 51c4370..eedf707 100644
--- a/Android.mk
+++ b/Android.mk
@@ -26,7 +26,7 @@
LOCAL_MODULE := tradefed
LOCAL_MODULE_TAGS := optional
-LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 guavalib jline-1.0
+LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 guavalib jline-1.0 json-prebuilt
# emmalib is only a runtime dependency if generating code coverage reporters,
# not a compile time dependency
LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt emmalib tools-common-prebuilt
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 5dffe88..0f80fec 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -19,6 +19,8 @@
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.command.remote.RemoteClient;
+import com.android.tradefed.command.remote.RemoteManager;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.GlobalConfiguration;
@@ -830,7 +832,7 @@
CLog.d("connected to remote manager at %d", handoverPort);
// inform remote manager of the devices we are still using
for (String deviceInUse : getDeviceManager().getAllocatedDevices()) {
- if (!mRemoteClient.sendFilterDevice(deviceInUse)) {
+ if (!mRemoteClient.sendAllocateDevice(deviceInUse)) {
CLog.e("Failed to send command to remote manager");
return false;
}
@@ -874,7 +876,7 @@
// TODO: send freed device state too
if (mRemoteClient != null) {
try {
- mRemoteClient.sendUnfilterDevice(device.getSerialNumber());
+ mRemoteClient.sendFreeDevice(device.getSerialNumber());
} catch (IOException e) {
CLog.e("Failed to send unfilter device %s to remote manager",
device.getSerialNumber());
diff --git a/src/com/android/tradefed/command/RemoteClient.java b/src/com/android/tradefed/command/RemoteClient.java
deleted file mode 100644
index d81390c..0000000
--- a/src/com/android/tradefed/command/RemoteClient.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2011 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.tradefed.command;
-
-import com.android.tradefed.util.ArrayUtil;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-
-/**
- * Class for sending remote commands to another TF process via sockets.
- */
-public class RemoteClient {
-
- private final Socket mSocket;
- private final PrintWriter mWriter;
- private final BufferedReader mReader;
-
- /**
- * @param port
- * @throws IOException
- * @throws UnknownHostException
- */
- RemoteClient(int port) throws UnknownHostException, IOException {
- String hostName = InetAddress.getLocalHost().getHostName();
- mSocket = new Socket(hostName, port);
- mWriter = new PrintWriter(mSocket.getOutputStream(), true);
- mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
- }
-
- private synchronized boolean sendCommand(String... cmd) throws IOException {
- // TODO: use a more standard data protocol - such as Json
- mWriter.println(ArrayUtil.join(RemoteManager.DELIM, (Object[])cmd));
- String response = mReader.readLine();
- return response != null && Boolean.parseBoolean(response);
- }
-
- public static RemoteClient connect(int port) throws UnknownHostException, IOException {
- return new RemoteClient(port);
- }
-
- /**
- * Send a 'add this device to global ignore filter' command
- * @param serial
- * @throws IOException
- */
- public boolean sendFilterDevice(String serial) throws IOException {
- return sendCommand(RemoteManager.FILTER, serial);
- }
-
- /**
- * Send a 'remove this device from global ignore filter' command
- * @param serial
- * @throws IOException
- */
- public boolean sendUnfilterDevice(String serial) throws IOException {
- return sendCommand(RemoteManager.UNFILTER, serial);
- }
-
- /**
- * Send a 'add command' command.
- *
- * @param commandArgs
- */
- public boolean sendAddCommand(long totalTime, String... commandArgs) throws IOException {
- String[] fullList = ArrayUtil.buildArray(new String[] {RemoteManager.ADD_COMMAND,
- Long.toString(totalTime)}, commandArgs);
- return sendCommand(fullList);
- }
-
- /**
- * Send a 'close connection' command
- *
- * @throws IOException
- */
- public boolean sendClose() throws IOException {
- return sendCommand(RemoteManager.CLOSE);
- }
-
- /**
- * Close the connection to the {@link RemoteManager}.
- */
- public synchronized void close() {
- if (mSocket != null) {
- try {
- mSocket.close();
- } catch (IOException e) {
- // ignore
- }
- }
- if (mWriter != null) {
- mWriter.close();
- }
- }
-}
-
diff --git a/src/com/android/tradefed/command/remote/AddCommandOp.java b/src/com/android/tradefed/command/remote/AddCommandOp.java
new file mode 100644
index 0000000..268b115
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/AddCommandOp.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation for adding a command to the local tradefed scheduler.
+ */
+class AddCommandOp extends RemoteOperation {
+
+ private static final String COMMAND_ARGS = "commandArgs";
+ private static final String TIME = "time";
+ long mTotalTime;
+ String[] mCommandArgs;
+
+ AddCommandOp() {
+ this(0, new String[]{});
+ }
+
+ AddCommandOp(long totalTime, String... commandArgs) {
+ mTotalTime = totalTime;
+ mCommandArgs = commandArgs;
+ }
+
+ @Override
+ protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+ mTotalTime = json.getLong(TIME);
+ JSONArray jsonArgs = json.getJSONArray(COMMAND_ARGS);
+ mCommandArgs = new String[jsonArgs.length()];
+ for (int i=0; i < mCommandArgs.length; i++) {
+ mCommandArgs[i] = jsonArgs.getString(i);
+ }
+ }
+
+
+ @Override
+ protected OperationType getType() {
+ return OperationType.ADD_COMMAND;
+ }
+
+ @Override
+ protected void packIntoJson(JSONObject j) throws JSONException {
+ j.put(TIME, mTotalTime);
+ JSONArray jsonArgs = new JSONArray();
+ for (String arg : mCommandArgs) {
+ jsonArgs.put(arg);
+ }
+ j.put(COMMAND_ARGS, jsonArgs);
+ }
+}
diff --git a/src/com/android/tradefed/command/remote/AllocateDeviceOp.java b/src/com/android/tradefed/command/remote/AllocateDeviceOp.java
new file mode 100644
index 0000000..8f1a6e2
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/AllocateDeviceOp.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation for allocating a device.
+ * <p/>
+ * Currently used to mark the device as in use by another tradefed process in handover situation.
+ */
+class AllocateDeviceOp extends RemoteOperation {
+
+ private static final String SERIAL = "serial";
+ String mDeviceSerial;
+
+ AllocateDeviceOp(String serial) {
+ mDeviceSerial = serial;
+ }
+
+ AllocateDeviceOp() {
+ this(null);
+ }
+
+ @Override
+ protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+ mDeviceSerial = json.getString(SERIAL);
+ }
+
+
+ @Override
+ protected OperationType getType() {
+ return OperationType.ALLOCATE_DEVICE;
+ }
+
+ @Override
+ protected void packIntoJson(JSONObject j) throws JSONException {
+ j.put(SERIAL, mDeviceSerial);
+ }
+
+}
diff --git a/src/com/android/tradefed/command/remote/CloseOp.java b/src/com/android/tradefed/command/remote/CloseOp.java
new file mode 100644
index 0000000..4011388
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/CloseOp.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Remote operation that instructs the remote manager to shut down. Currently used when
+ * handover process is complete.
+ */
+class CloseOp extends RemoteOperation {
+
+ @Override
+ protected void unpackFromJson(JSONObject json) throws RemoteException {
+ // nothing to do
+ }
+
+ @Override
+ protected OperationType getType() {
+ return OperationType.CLOSE;
+ }
+
+ @Override
+ protected void packIntoJson(JSONObject j) throws JSONException {
+ // nothing to do
+ }
+}
diff --git a/src/com/android/tradefed/command/remote/DeviceTracker.java b/src/com/android/tradefed/command/remote/DeviceTracker.java
new file mode 100644
index 0000000..1702e40
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/DeviceTracker.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * Singleton class that tracks devices that have been remotely allocated.
+ */
+class DeviceTracker {
+
+ /**
+ * Use on demand holder idiom
+ * @see http://en.wikipedia.org/wiki/Singleton_pattern#The_solution_of_Bill_Pugh
+ */
+ private static class SingletonHolder {
+ public static final DeviceTracker cInstance = new DeviceTracker();
+ }
+
+ public static DeviceTracker getInstance() {
+ return SingletonHolder.cInstance;
+ }
+
+ // private constructor - don't allow instantiation
+ private DeviceTracker() {
+ }
+
+ // use Hashtable since its thread-safe
+ private Map<String, ITestDevice> mAllocatedDeviceMap = new Hashtable<String, ITestDevice>();
+
+ /**
+ * Mark given device as remotely allocated.
+ */
+ public void allocateDevice(ITestDevice d) {
+ mAllocatedDeviceMap.put(d.getSerialNumber(), d);
+ }
+
+ /**
+ * Mark given device serial as freed.
+ *
+ * @return the corresponding {@link ITestDevice} or <code>null</code> if device with given
+ * serial cannot be found
+ */
+ public ITestDevice freeDevice(String serial) {
+ return mAllocatedDeviceMap.remove(serial);
+ }
+
+ /**
+ * Mark all remotely allocated devices as freed.
+ *
+ * @return a {@link Collection} of all remotely allocated devices
+ */
+ public Collection<ITestDevice> freeAll() {
+ Collection<ITestDevice> devices = new ArrayList<ITestDevice>(mAllocatedDeviceMap.values());
+ mAllocatedDeviceMap.clear();
+ return devices;
+ }
+}
diff --git a/src/com/android/tradefed/command/remote/FreeDeviceOp.java b/src/com/android/tradefed/command/remote/FreeDeviceOp.java
new file mode 100644
index 0000000..c2f7054
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/FreeDeviceOp.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A remote operation for freeing a previously remotely allocated device.
+ */
+class FreeDeviceOp extends RemoteOperation {
+
+ private static final String SERIAL = "serial";
+
+ static final String ALL_DEVICES = "*";
+
+ String mDeviceSerial;
+
+ FreeDeviceOp(String serial) {
+ mDeviceSerial = serial;
+ }
+
+ FreeDeviceOp() {
+ this(null);
+ }
+
+ @Override
+ protected void unpackFromJson(JSONObject json) throws RemoteException, JSONException {
+ mDeviceSerial = json.getString(SERIAL);
+ }
+
+ @Override
+ protected OperationType getType() {
+ return OperationType.FREE_DEVICE;
+ }
+
+ @Override
+ protected void packIntoJson(JSONObject j) throws JSONException {
+ j.put(SERIAL, mDeviceSerial);
+ }
+}
diff --git a/src/com/android/tradefed/command/remote/RemoteClient.java b/src/com/android/tradefed/command/remote/RemoteClient.java
new file mode 100644
index 0000000..62fdab1
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/RemoteClient.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 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.tradefed.command.remote;
+
+import com.android.tradefed.command.remote.RemoteOperation.RemoteException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * Class for sending remote commands to another TF process.
+ * <p/>
+ * Currently uses JSON-encoded data sent via sockets.
+ */
+public class RemoteClient {
+
+ private final Socket mSocket;
+ private final PrintWriter mWriter;
+ private final BufferedReader mReader;
+
+ /**
+ * Initialize the {@RemoteClient}, and instruct it to connect to the given port on
+ * localhost.
+ *
+ * @param port the tcp/ip port number
+ * @throws IOException
+ * @throws UnknownHostException
+ */
+ RemoteClient(int port) throws UnknownHostException, IOException {
+ String hostName = InetAddress.getLocalHost().getHostName();
+ mSocket = new Socket(hostName, port);
+ mWriter = new PrintWriter(mSocket.getOutputStream(), true);
+ mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
+ }
+
+ /**
+ * Send the given command to the remote TF.
+ *
+ * @param cmd the {@link RemoteOperation} to send
+ * @return true if command was sent and processed successfully by remote TF
+ */
+ private synchronized boolean sendCommand(RemoteOperation cmd) {
+ try {
+ mWriter.println(cmd.pack());
+ String response = mReader.readLine();
+ return response != null && Boolean.parseBoolean(response);
+ } catch (RemoteException e) {
+ CLog.e("Failed to send remote commmand", e);
+ } catch (IOException e) {
+ CLog.e("Failed to send remote commmand", e);
+ }
+ return false;
+ }
+
+ /**
+ * Helper method to create a {@link RemoteClient} connected to given port
+ *
+ * @param port the tcp/ip port
+ * @return the {@link RemoteClient}
+ * @throws UnknownHostException
+ * @throws IOException
+ */
+ public static RemoteClient connect(int port) throws UnknownHostException, IOException {
+ return new RemoteClient(port);
+ }
+
+ /**
+ * Send a 'allocate device' command
+ *
+ * @param serial
+ * @throws IOException
+ */
+ public boolean sendAllocateDevice(String serial) throws IOException {
+ return sendCommand(new AllocateDeviceOp(serial));
+ }
+
+ /**
+ * Send a 'free previously allocated device' command
+ * @param serial
+ * @throws IOException
+ */
+ public boolean sendFreeDevice(String serial) throws IOException {
+ return sendCommand(new FreeDeviceOp(serial));
+ }
+
+ /**
+ * Send a 'add command' command.
+ *
+ * @param commandArgs
+ */
+ public boolean sendAddCommand(long totalTime, String... commandArgs) throws IOException {
+ return sendCommand(new AddCommandOp(totalTime, commandArgs));
+ }
+
+ /**
+ * Send a 'close connection' command
+ *
+ * @throws IOException
+ */
+ public boolean sendClose() throws IOException {
+ return sendCommand(new CloseOp());
+ }
+
+ /**
+ * Close the connection to the {@link RemoteManager}.
+ */
+ public synchronized void close() {
+ if (mSocket != null) {
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ StreamUtil.close(mWriter);
+ }
+}
+
diff --git a/src/com/android/tradefed/command/RemoteManager.java b/src/com/android/tradefed/command/remote/RemoteManager.java
similarity index 61%
rename from src/com/android/tradefed/command/RemoteManager.java
rename to src/com/android/tradefed/command/remote/RemoteManager.java
index a9363f4..9ca8b40 100644
--- a/src/com/android/tradefed/command/RemoteManager.java
+++ b/src/com/android/tradefed/command/remote/RemoteManager.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tradefed.command;
+package com.android.tradefed.command.remote;
-import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.command.ICommandScheduler;
+import com.android.tradefed.command.remote.RemoteOperation.RemoteException;
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
import com.android.tradefed.device.ITestDevice;
@@ -29,12 +30,9 @@
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
-import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.Map;
/**
- * Class that receives remote commands to add and remove devices from use via a socket.
+ * Class that receives {@link RemoteOperation}s via a socket.
* <p/>
* Currently accepts only one remote connection at one time, and processes incoming commands
* serially.
@@ -49,19 +47,10 @@
*/
public class RemoteManager extends Thread {
- // constants that define wire protocol between RemoteClient and RemoteManager
- static final String DELIM = ";";
- static final String FILTER = "filter";
- static final String UNFILTER = "unfilter";
- static final String ALL_DEVICES = "*";
- static final String CLOSE = "close";
- static final String ADD_COMMAND = "add_command";
-
private ServerSocket mServerSocket = null;
private boolean mCancel = false;
private final IDeviceManager mDeviceManager;
private final ICommandScheduler mScheduler;
- private Map<String, ITestDevice> mFilteredDeviceMap = new Hashtable<String, ITestDevice>();
/**
* Creates a {@link RemoteManager}.
@@ -130,7 +119,7 @@
clientSocket = serverSocket.accept();
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
out = new PrintWriter(clientSocket.getOutputStream(), true);
- processClientCommands(in, out);
+ processClientOperations(in, out);
} catch (IOException e) {
CLog.e("Failed to accept connection: %s", e);
} finally {
@@ -141,91 +130,84 @@
}
}
- private void processClientCommands(BufferedReader in, PrintWriter out) throws IOException {
+ private void processClientOperations(BufferedReader in, PrintWriter out) throws IOException {
String line = null;
while ((line = in.readLine()) != null && !mCancel) {
boolean result = false;
- String[] commandSegments = line.split(DELIM);
- String cmdType = commandSegments[0];
- if (FILTER.equals(cmdType)) {
- result = processFilterCommand(commandSegments);
- } else if (UNFILTER.equals(cmdType)) {
- result = processUnfilterCommand(commandSegments);
- } else if (CLOSE.equals(cmdType)) {
- cancel();
- result = true;
- } else if (ADD_COMMAND.equals(cmdType)) {
- result = processAddCommand(commandSegments);
+ RemoteOperation rc;
+ try {
+ rc = RemoteOperation.createRemoteOpFromString(line);
+ switch (rc.getType()) {
+ case ADD_COMMAND:
+ result = processAdd((AddCommandOp)rc);
+ break;
+ case CLOSE:
+ result = processClose((CloseOp)rc);
+ break;
+ case ALLOCATE_DEVICE:
+ result = processAllocate((AllocateDeviceOp)rc);
+ break;
+ case FREE_DEVICE:
+ result = processFree((FreeDeviceOp)rc);
+ break;
+ }
+ } catch (RemoteException e) {
+ CLog.e("Failed to handle remote command", e);
}
sendAck(result, out);
}
}
- private boolean processFilterCommand(final String[] commandSegments) {
- if (commandSegments.length < 2) {
- CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
- return false;
- }
- final String serial = commandSegments[1];
- ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(serial);
+ private boolean processAllocate(AllocateDeviceOp c) {
+ ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(c.mDeviceSerial);
if (allocatedDevice != null) {
- Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
- String.format("Allocating device %s that is still in use by remote tradefed",
- serial));
- mFilteredDeviceMap.put(serial, allocatedDevice);
+ CLog.logAndDisplay(LogLevel.INFO,
+ "Allocating device %s that is still in use by remote tradefed",
+ c.mDeviceSerial);
+ DeviceTracker.getInstance().allocateDevice(allocatedDevice);
return true;
- } else {
- CLog.e("Failed to allocate remote device %s", serial);
- return false;
}
+ CLog.e("Failed to allocate device %s", c.mDeviceSerial);
+ return false;
}
- private boolean processUnfilterCommand(final String[] commandSegments) {
- if (commandSegments.length < 2) {
- CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
- return false;
- }
- // TODO: consider making this synchronous, and sending ack back to client once allocated
- final String serial = commandSegments[1];
- if (ALL_DEVICES.equals(serial)) {
+ private boolean processFree(FreeDeviceOp c) {
+ if (FreeDeviceOp.ALL_DEVICES.equals(c.mDeviceSerial)) {
freeAllDevices();
return true;
} else {
- ITestDevice d = mFilteredDeviceMap.remove(serial);
+ ITestDevice d = DeviceTracker.getInstance().freeDevice(c.mDeviceSerial);
if (d != null) {
- Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
- String.format("Freeing device %s no longer in use by remote tradefed",
- serial));
+ CLog.logAndDisplay(LogLevel.INFO,
+ "Freeing device %s no longer in use by remote tradefed",
+ c.mDeviceSerial);
mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
return true;
} else {
- CLog.w("Could not find device to free %s", serial);
+ CLog.w("Could not find device to free %s", c.mDeviceSerial);
}
}
return false;
}
- private boolean processAddCommand(final String[] commandSegments) {
- if (commandSegments.length < 3) {
- CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments));
- return false;
- }
- long totalTime = Long.parseLong(commandSegments[1]);
- String[] cmdArgs = Arrays.copyOfRange(commandSegments, 2, commandSegments.length);
- Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
- String.format("Adding command '%s'", ArrayUtil.join(" ", (Object[])cmdArgs)));
- return mScheduler.addCommand(cmdArgs, totalTime);
+ boolean processAdd(AddCommandOp c) {
+ CLog.logAndDisplay(LogLevel.INFO, "Adding command '%s'", ArrayUtil.join(" ",
+ c.mCommandArgs));
+ return mScheduler.addCommand(c.mCommandArgs, c.mTotalTime);
+ }
+
+ private boolean processClose(CloseOp rc) {
+ cancel();
+ return true;
}
private void freeAllDevices() {
- for (ITestDevice d : mFilteredDeviceMap.values()) {
- Log.logAndDisplay(LogLevel.INFO, "RemoteManager",
- String.format("Freeing device %s no longer in use by remote tradefed",
- d.getSerialNumber()));
-
+ for (ITestDevice d : DeviceTracker.getInstance().freeAll()) {
+ CLog.logAndDisplay(LogLevel.INFO,
+ "Freeing device %s no longer in use by remote tradefed",
+ d.getSerialNumber());
mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
}
- mFilteredDeviceMap.clear();
}
private void sendAck(boolean result, PrintWriter out) {
@@ -238,7 +220,7 @@
public synchronized void cancel() {
if (!mCancel) {
mCancel = true;
- Log.logAndDisplay(LogLevel.INFO, "RemoteManager", "Closing remote manager");
+ CLog.logAndDisplay(LogLevel.INFO, "Closing remote manager");
}
}
diff --git a/src/com/android/tradefed/command/remote/RemoteOperation.java b/src/com/android/tradefed/command/remote/RemoteOperation.java
new file mode 100644
index 0000000..91f70f9
--- /dev/null
+++ b/src/com/android/tradefed/command/remote/RemoteOperation.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013 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.tradefed.command.remote;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates data for a remote operation sent over the wire.
+ */
+abstract class RemoteOperation {
+ private static final String TYPE = "type";
+ private static final String VERSION = "version";
+ static final int CURRENT_PROTOCOL_VERSION = 1;
+
+ @SuppressWarnings("serial")
+ static class RemoteException extends Exception {
+ RemoteException(Throwable t) {
+ super(t);
+ }
+
+ RemoteException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Represents all types of remote operations that can be performed
+ */
+ enum OperationType {
+ ALLOCATE_DEVICE, FREE_DEVICE, CLOSE, ADD_COMMAND
+ }
+
+ /**
+ * Create and populate a {@link RemoteOperation} from given data.
+ *
+ * @param data the data to parse
+ * @throws RemoteException
+ */
+ final static RemoteOperation createRemoteOpFromString(String data) throws RemoteException {
+ try {
+ JSONObject jsonData = new JSONObject(data);
+ int protocolVersion = jsonData.getInt(VERSION);
+ // to keep things simple for now, just barf when protocol version is unknown
+ if (protocolVersion != CURRENT_PROTOCOL_VERSION) {
+ throw new RemoteException(String.format(
+ "Remote operation has unknown version '%d'. Expected '%d'",
+ protocolVersion, CURRENT_PROTOCOL_VERSION));
+ }
+ OperationType op = OperationType.valueOf(jsonData.getString(TYPE));
+ RemoteOperation rc = null;
+ switch (op) {
+ case ALLOCATE_DEVICE:
+ rc = new AllocateDeviceOp();
+ break;
+ case FREE_DEVICE:
+ rc = new FreeDeviceOp();
+ break;
+ case CLOSE:
+ rc = new CloseOp();
+ break;
+ case ADD_COMMAND:
+ rc = new AddCommandOp();
+ break;
+ default:
+ throw new RemoteException(String.format("unknown remote command '%s'", data));
+
+ }
+ rc.unpackFromJson(jsonData);
+ return rc;
+ } catch (JSONException e) {
+ throw new RemoteException(e);
+ }
+ }
+
+ protected abstract OperationType getType();
+
+ /**
+ * Abstract method to allow sub-classes to parse additional data from payload.
+ *
+ * @param json
+ * @throws RemoteException, JSONException
+ */
+ protected abstract void unpackFromJson(JSONObject json) throws RemoteException, JSONException;
+
+ /**
+ * Return the RemoteCommand data in its wire protocol format
+ * @return
+ */
+ String pack() throws RemoteException {
+ JSONObject j = new JSONObject();
+ try {
+ j.put(VERSION, CURRENT_PROTOCOL_VERSION);
+ j.put(TYPE, getType().toString());
+ packIntoJson(j);
+ } catch (JSONException e) {
+ CLog.e("Failed to serialize RemoteOperation", e);
+ }
+ return j.toString();
+ }
+
+ /**
+ * Callback to add subclass specific data to the JSON object
+ * @param j
+ * @throws JSONException
+ */
+ protected abstract void packIntoJson(JSONObject j) throws JSONException;
+
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 60cdf55..d9476c1 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,7 +26,7 @@
import com.android.tradefed.command.CommandFileParserTest;
import com.android.tradefed.command.CommandSchedulerTest;
import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.RemoteManagerTest;
+import com.android.tradefed.command.remote.RemoteManagerTest;
import com.android.tradefed.config.ArgsOptionParserTest;
import com.android.tradefed.config.ConfigurationDefTest;
import com.android.tradefed.config.ConfigurationFactoryTest;
diff --git a/tests/src/com/android/tradefed/command/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/RemoteManagerTest.java
deleted file mode 100644
index ae3dfd4..0000000
--- a/tests/src/com/android/tradefed/command/RemoteManagerTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2011 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.tradefed.command;
-
-import com.android.tradefed.device.IDeviceManager;
-import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
-import com.android.tradefed.device.ITestDevice;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-/**
- * Unit tests for {@link RemoteManager}.
- */
-public class RemoteManagerTest extends TestCase {
-
- private IDeviceManager mMockDeviceManager;
- private RemoteManager mRemoteMgr;
- private RemoteClient mRemoteClient;
- private ICommandScheduler mMockScheduler;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mMockDeviceManager = EasyMock.createMock(IDeviceManager.class);
- mMockScheduler = EasyMock.createMock(ICommandScheduler.class);
- mRemoteMgr = new RemoteManager(mMockDeviceManager, mMockScheduler);
- }
-
- @Override
- protected void tearDown() throws Exception {
- if (mRemoteClient != null) {
- mRemoteClient.close();
- }
- if (mRemoteMgr != null) {
- mRemoteMgr.cancel();
- }
- super.tearDown();
- }
-
- /**
- * An integration test for client-manager interaction, that will filter, then unfilter a device.
- */
- public void testFilterUnfilter() throws Exception {
- ITestDevice device = EasyMock.createMock(ITestDevice.class);
- EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
- mMockDeviceManager.freeDevice(EasyMock.eq(device),
- EasyMock.eq(FreeDeviceState.AVAILABLE));
-
- EasyMock.replay(mMockDeviceManager, device);
- mRemoteMgr.start();
- int port = mRemoteMgr.getPort();
- assertTrue(port != -1);
- mRemoteClient = RemoteClient.connect(port);
- assertTrue(mRemoteClient.sendFilterDevice("serial"));
- assertTrue(mRemoteClient.sendUnfilterDevice("serial"));
- EasyMock.verify(mMockDeviceManager);
- }
-}
diff --git a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
new file mode 100644
index 0000000..32bce6f
--- /dev/null
+++ b/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 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.tradefed.command.remote;
+
+import com.android.tradefed.command.ICommandScheduler;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.IDeviceManager.FreeDeviceState;
+import com.android.tradefed.device.ITestDevice;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
+/**
+ * Unit tests for {@link RemoteManager}.
+ */
+public class RemoteManagerTest extends TestCase {
+
+ private IDeviceManager mMockDeviceManager;
+ private RemoteManager mRemoteMgr;
+ private RemoteClient mRemoteClient;
+ private ICommandScheduler mMockScheduler;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mMockDeviceManager = EasyMock.createMock(IDeviceManager.class);
+ mMockScheduler = EasyMock.createMock(ICommandScheduler.class);
+ mRemoteMgr = new RemoteManager(mMockDeviceManager, mMockScheduler);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mRemoteClient != null) {
+ mRemoteClient.close();
+ }
+ if (mRemoteMgr != null) {
+ mRemoteMgr.cancel();
+ }
+ super.tearDown();
+ }
+
+ /**
+ * An integration test for client-manager interaction, that will allocate, then free a device.
+ */
+ public void testAllocateFree() throws Exception {
+ ITestDevice device = EasyMock.createMock(ITestDevice.class);
+ EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+ EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+ mMockDeviceManager.freeDevice(EasyMock.eq(device),
+ EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+ EasyMock.replay(mMockDeviceManager, device);
+ mRemoteMgr.start();
+ int port = mRemoteMgr.getPort();
+ assertTrue(port != -1);
+ mRemoteClient = RemoteClient.connect(port);
+ assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+ assertTrue(mRemoteClient.sendFreeDevice("serial"));
+ EasyMock.verify(mMockDeviceManager);
+ }
+
+ /**
+ * An integration test for client-manager interaction, that will add a command
+ */
+ public void testAddCommand() throws Exception {
+ EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(new String[] {
+ "arg1", "arg2"
+ }), EasyMock.anyInt())).andReturn(true);
+
+ EasyMock.replay(mMockScheduler);
+ mRemoteMgr.start();
+ int port = mRemoteMgr.getPort();
+ assertTrue(port != -1);
+ mRemoteClient = RemoteClient.connect(port);
+ assertTrue(mRemoteClient.sendAddCommand(3, "arg1", "arg2"));
+ EasyMock.verify(mMockScheduler);
+ }
+
+ /**
+ * An integration test for client-manager interaction, that will allocate, then close the
+ * connection. Verifies that closing frees all devices.
+ */
+ public void testAllocateClose() throws Exception {
+ ITestDevice device = EasyMock.createMock(ITestDevice.class);
+ EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+ EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+ mMockDeviceManager.freeDevice(EasyMock.eq(device),
+ EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+ EasyMock.replay(mMockDeviceManager, device);
+ mRemoteMgr.start();
+ int port = mRemoteMgr.getPort();
+ assertTrue(port != -1);
+ mRemoteClient = RemoteClient.connect(port);
+ assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+ assertTrue(mRemoteClient.sendClose());
+ mRemoteClient.close();
+ mRemoteMgr.join();
+ EasyMock.verify(mMockDeviceManager);
+ }
+
+ /**
+ * An integration test for client-manager interaction, that will allocate, then frees all
+ * devices.
+ */
+ public void testAllocateFreeAll() throws Exception {
+ ITestDevice device = EasyMock.createMock(ITestDevice.class);
+ EasyMock.expect(device.getSerialNumber()).andStubReturn("serial");
+ EasyMock.expect(mMockDeviceManager.forceAllocateDevice("serial")).andReturn(device);
+ mMockDeviceManager.freeDevice(EasyMock.eq(device),
+ EasyMock.eq(FreeDeviceState.AVAILABLE));
+
+ EasyMock.replay(mMockDeviceManager, device);
+ mRemoteMgr.start();
+ int port = mRemoteMgr.getPort();
+ assertTrue(port != -1);
+ mRemoteClient = RemoteClient.connect(port);
+ assertTrue(mRemoteClient.sendAllocateDevice("serial"));
+ assertTrue(mRemoteClient.sendFreeDevice("*"));
+ EasyMock.verify(mMockDeviceManager);
+ }
+
+}