summaryrefslogtreecommitdiff
path: root/fakeadbserver/src/main/java/com/android/fakeadbserver/shellcommandhandlers/ShellHandler.kt
blob: 8afb17337cebd541397211bfa4ff157c94b7b6ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
 * Copyright (C) 2021 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.shellcommandhandlers

import com.android.fakeadbserver.DeviceState
import com.android.fakeadbserver.FakeAdbServer
import com.android.fakeadbserver.ShellProtocolType
import com.android.fakeadbserver.devicecommandhandlers.DeviceCommandHandler
import com.android.fakeadbserver.services.ShellCommandOutput
import com.google.common.base.Charsets
import java.net.Socket

/**
 * ShellHandler is a pre-supplied convenience construct to plug in and handle general shell
 * commands. This reflects the "shell,v2:command" local service as stated in the ADB protocol.
 *
 * TODO: Rename to reflect this can handle both "shell" and "shell,v2" protocol.
 */
abstract class ShellHandler protected constructor(
    protected val shellProtocolType: ShellProtocolType
) : DeviceCommandHandler(shellProtocolType.command) {

    override fun accept(
        server: FakeAdbServer,
        socket: Socket,
        device: DeviceState,
        command: String,
        args: String
    ): Boolean {
        if (this.command != command) {
            return false
        }
        val split = args.trim().split(" ", limit = 2)
        val shellCommand = split[0]
        val shellCommandArgs = if (split.size > 1) split[1] else null
        if (shouldExecute(shellCommand, shellCommandArgs)) {
            val statusWriter = StatusWriter(socket)
            execute(
                server,
                statusWriter,
                shellProtocolType.createServiceOutput(socket, device),
                device,
                shellCommand,
                shellCommandArgs,
            )
            statusWriter.verifyStatusWritten()
            return true
        }
        return false
    }

    /**
     * Return true if the derived class will be able to act on this shell command
     * @param shellCommand Shell command, e.g. for "adb shell ls -l" [shellCommand] would be "ls"
     * @param shellCommandArgs Arguments for the command, e.g. for "adb shell ls -l" [shellCommandArgs] would be "-l"
     */
    abstract fun shouldExecute(
        shellCommand: String,
        shellCommandArgs: String?
    ): Boolean

    /**
     * This is the main execution method of the command.
     *
     * @param fakeAdbServer Fake ADB Server itself.
     * @param shellCommandOutput Shell protocol for standard in/out
     * @param device Target device for the command, if any.
     * @param shellCommand Shell command, e.g. for "adb shell ls -l" [shellCommand] would be "ls"
     * @param shellCommandArgs Arguments for the command, e.g. for "adb shell ls -l" [shellCommandArgs] would be "-l"
     */
    abstract fun execute(
      fakeAdbServer: FakeAdbServer,
      statusWriter: StatusWriter,
      shellCommandOutput: ShellCommandOutput,
      device: DeviceState,
      shellCommand: String,
      shellCommandArgs: String?
    )
}

class StatusWriter(val socket: Socket) {

    private var writeOkCalled = false
    private var writeFailCalled = false

    fun writeOk() {
        assert(!writeOkCalled)
        writeOkCalled = true
        socket.getOutputStream().write("OKAY".toByteArray(Charsets.UTF_8))
    }

    fun writeFail() {
        assert(!writeFailCalled)
        writeFailCalled = true
        socket.getOutputStream().write("FAIL".toByteArray(Charsets.UTF_8))
    }

    fun verifyStatusWritten() {
        assert(writeOkCalled != writeFailCalled) {
            "OKAY or FAIL message, but not both should be written to output stream"
        }
    }
}