blob: e74bfe3731c4ed056e836e587b5885135ea1f0a8 [file] [log] [blame]
/*
* Copyright (C) 2017 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.fakeadbserver;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.fakeadbserver.devicecommandhandlers.DeviceCommandHandler;
import com.android.fakeadbserver.hostcommandhandlers.HostCommandHandler;
import com.android.fakeadbserver.shellcommandhandlers.ShellCommandHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
final class ConnectionHandler implements Runnable {
// The ADB protocol allows certain commands to address a single existing device on a/any
// transport. The following are commands that don't rely on wildcards on a transport level.
private static final Set<String> NON_WILDCARD_TRANSPORT_DEVICE_COMMANDS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(
"version", "kill", "devices", "devices-l", "track-devices", "emulator",
"transport", "transport-usb", "transport-local", "transport-any"
)));
@NonNull
private final FakeAdbServer mServer;
@NonNull
private final Socket mSocket;
@NonNull
private final Map<String, Supplier<HostCommandHandler>> mHostCommandHandlers;
@NonNull
private final Map<String, Supplier<DeviceCommandHandler>> mDeviceCommandHandlers;
@NonNull
private final Map<String, Supplier<ShellCommandHandler>> mShellCommandHandlers;
ConnectionHandler(@NonNull FakeAdbServer server, @NonNull Socket socket,
@NonNull Map<String, Supplier<HostCommandHandler>> hostCommandHandlers,
@NonNull Map<String, Supplier<DeviceCommandHandler>> deviceCommandHandlers,
@NonNull Map<String, Supplier<ShellCommandHandler>> shellCommandHandlers) {
mServer = server;
mSocket = socket;
mHostCommandHandlers = hostCommandHandlers;
mDeviceCommandHandlers = deviceCommandHandlers;
mShellCommandHandlers = shellCommandHandlers;
}
@Override
public void run() {
boolean keepRunning = true;
DeviceState targetDevice = null;
try {
while (keepRunning) {
if (targetDevice == null) {
HostRequest request = parseHostRequest();
if (request == null) {
// Something went wrong with the request, and parseHostRequest already
// sent a failure message with the context.
return;
}
if (request.mCommand.startsWith("transport")) {
targetDevice = request.mTargetDevice;
sendOkay();
} else if (mHostCommandHandlers.containsKey(request.mCommand)) {
keepRunning = mHostCommandHandlers.get(request.mCommand).get()
.invoke(mServer, mSocket, request.mTargetDevice,
request.mArguments);
} else {
sendFailWithReason(
"Unimplemented host command received: " + request.mCommand);
}
} else {
Request request = parseDeviceRequest();
if (request == null) {
// Something went wrong with the request, and parseDeviceRequest already
// sent a failure message with the context.
return;
}
if (request.mCommand.equals("shell")) {
String[] splitShellString = request.mArguments.split(" ", 2);
if (mShellCommandHandlers.containsKey(splitShellString[0])) {
if (!mShellCommandHandlers.get(splitShellString[0]).get()
.invoke(mServer, mSocket, targetDevice,
splitShellString.length > 1 ? splitShellString[1]
: null)) {
return;
}
} else {
sendFailWithReason(
"Unimplemented shell command received: " + request.mCommand);
}
} else if (mDeviceCommandHandlers.containsKey(request.mCommand)) {
if (!mDeviceCommandHandlers.get(request.mCommand).get()
.invoke(mServer, mSocket, targetDevice, request.mArguments)) {
return;
}
} else {
sendFailWithReason(
"Unimplemented device command received: " + request.mCommand);
}
}
}
} catch (RuntimeException e) {
sendFailWithReason("Bad request received: " + e.toString());
} catch (IOException ignored) {
sendFailWithReason("IOException occurred when processing request.");
} finally {
try {
mSocket.close();
} catch (IOException ignored) {
}
}
}
@Nullable
private HostRequest parseHostRequest() throws IOException {
byte[] lengthString = new byte[4];
readFully(lengthString);
int requestLength = Integer.parseInt(new String(lengthString), 16);
assert requestLength > "host:".length();
byte[] payloadBytes = new byte[requestLength];
readFully(payloadBytes);
String payload = new String(payloadBytes, US_ASCII);
String[] splitPayload = payload.split(":", 2);
if (splitPayload.length < 2) {
sendFailWithReason("Invalid host command: " + payload);
return null;
}
DeviceState device = null;
switch (splitPayload[0]) {
case "host":
String[] transportSplit = splitPayload[1].split(":");
String command = transportSplit[0];
if (!NON_WILDCARD_TRANSPORT_DEVICE_COMMANDS.contains(command)) {
device =
findAnyDeviceWithProtocol(
Arrays.asList(
DeviceState.HostConnectionType.LOCAL,
DeviceState.HostConnectionType.USB));
if (device == null) {
return null;
}
} else if (command.startsWith("transport")) {
switch (command) {
case "transport":
device = findDeviceWithSerial(transportSplit[1]);
break;
case "transport-usb":
device =
findAnyDeviceWithProtocol(
Collections.singletonList(
DeviceState.HostConnectionType.USB));
break;
case "transport-local":
device =
findAnyDeviceWithProtocol(
Collections.singletonList(
DeviceState.HostConnectionType.LOCAL));
break;
case "transport-any":
device =
findAnyDeviceWithProtocol(
Arrays.asList(
DeviceState.HostConnectionType.LOCAL,
DeviceState.HostConnectionType.USB));
break;
default:
sendFailWithReason("Invalid command specified in payload: " + payload);
return null;
}
if (device == null) {
return null;
}
}
break;
case "host-usb":
device =
findAnyDeviceWithProtocol(
Collections.singletonList(DeviceState.HostConnectionType.USB));
if (device == null) {
return null;
}
break;
case "host-local":
device =
findAnyDeviceWithProtocol(
Collections.singletonList(DeviceState.HostConnectionType.LOCAL));
if (device == null) {
return null;
}
break;
case "host-serial":
splitPayload = splitPayload[1].split(":", 2);
String serial = splitPayload[0];
device = findDeviceWithSerial(serial);
break;
default:
sendFailWithReason("Invalid transport specified in payload: " + payload);
return null;
}
splitPayload = splitPayload[1].split(":", 2);
return new HostRequest(device, splitPayload[0],
splitPayload.length > 1 ? splitPayload[1] : "");
}
@Nullable
private DeviceState findAnyDeviceWithProtocol(
@NonNull List<DeviceState.HostConnectionType> acceptibleConnectionTypes) {
List<DeviceState> deviceList;
try {
deviceList = mServer.getDeviceListCopy().get();
} catch (ExecutionException | InterruptedException ignored) {
sendFailWithReason("Internal server failure while processing command.");
return null;
}
DeviceState foundDevice = null;
for (DeviceState device : deviceList) {
if (!acceptibleConnectionTypes.contains(device.getHostConnectionType())) {
continue;
}
if (foundDevice == null) {
foundDevice = device;
} else {
sendFailWithReason("More than one device on the USB bus. Please specify which.");
return null;
}
}
if (foundDevice == null) {
sendFailWithReason("No devices available on the USB bus.");
return null;
}
return foundDevice;
}
@Nullable
private DeviceState findDeviceWithSerial(@NonNull String serial) {
try {
List<DeviceState> devices = mServer.getDeviceListCopy().get();
Optional<DeviceState> streamResult = devices.stream()
.filter(streamDevice -> serial.equals(streamDevice.getDeviceId()))
.findAny();
if (!streamResult.isPresent()) {
sendFailWithReason("No device with serial: " + serial + " is connected.");
return null;
}
return streamResult.get();
} catch (InterruptedException | ExecutionException e) {
sendFailWithReason("Internal server error while retrieving device list.");
return null;
}
}
@Nullable
private Request parseDeviceRequest() throws IOException {
byte[] lengthString = new byte[4];
readFully(lengthString);
int requestLength = Integer.parseInt(new String(lengthString), 16);
byte[] payloadBytes = new byte[requestLength];
readFully(payloadBytes);
String payload = new String(payloadBytes, US_ASCII);
// The track-jdwp packet comes without a trailing colon, so we need to special-case it
String[] splitPayload =
payload.equals("track-jdwp") ? new String[] {payload, ""} : payload.split(":", 2);
if (splitPayload.length < 2) {
sendFailWithReason("Invalid host command: " + payload);
return null;
}
return new Request(splitPayload[0], splitPayload[1]);
}
private void readFully(@NonNull byte[] buffer) throws IOException {
int bytesRead = 0;
while (bytesRead < buffer.length) {
bytesRead += mSocket.getInputStream()
.read(buffer, bytesRead, buffer.length - bytesRead);
}
}
private void sendOkay() throws IOException {
OutputStream stream = mSocket.getOutputStream();
stream.write("OKAY".getBytes(US_ASCII));
}
private void sendFailWithReason(@NonNull String reason) {
try {
OutputStream stream = mSocket.getOutputStream();
stream.write("FAIL".getBytes(US_ASCII));
byte[] reasonBytes = reason.getBytes(UTF_8);
assert reasonBytes.length < 65536;
stream.write(String.format("%x4d", reason.length()).getBytes(US_ASCII));
stream.write(reasonBytes);
stream.flush();
} catch (IOException ignored) {
}
}
private static class Request {
@NonNull
protected String mCommand;
@NonNull
protected String mArguments;
private Request(@NonNull String command, @NonNull String arguments) {
mCommand = command;
mArguments = arguments;
}
}
private static class HostRequest extends Request {
@Nullable
protected DeviceState mTargetDevice;
private HostRequest(@Nullable DeviceState targetDevice, @NonNull String command,
@NonNull String arguments) {
super(command, arguments);
mTargetDevice = targetDevice;
}
}
}