| /* |
| * 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; |
| } |
| } |
| } |