Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSUE-576: Allow complex ADB server commands execution #597

Merged
merged 14 commits into from
Jan 22, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.kaspersky.adbserver.commandtypes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General feedback.

I guess we can use Runtime::exec(String[] cmdarray) instead of Runtime::exec(String command) in CmdCommandPerformer.perform. Have a look at java.lang.Runtime, where both methods call the same method and command param in Runtime::exec(String command) is parsing into the array of strings.

I don't support introducing separate classes like ComplexAdbCommand. I think we need to add the new constructors like Command(command: String, arguments: List<String>) and the correspondent methods in AdbTerminal and etc.

I don't support syntaxis like (command: List<String>) and (command): String. It's confusing. Also, the most popular syntaxis in these cases is (command: String, arguments: List<String>). Yes, another command is an argument of the main command. Cases like sh -c "adb shell dumpsys deviceidle | grep mForceIdle" should be described in the documentation and examples.

import com.kaspersky.adbserver.common.api.Command
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also reflect all these cases in tests? I mean testing a single command, a command with arguments and a command with commands in arguments (complex commands with pipes and etc.).


data class AdbCommand(override val body: String) : Command(body)
data class AdbCommand(val command: String) : Command(command)
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.kaspersky.adbserver.commandtypes

import com.kaspersky.adbserver.common.api.Command

data class CmdCommand(override val body: String) : Command(body)
data class CmdCommand(val command: String) : Command(command)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kaspersky.adbserver.commandtypes

import com.kaspersky.adbserver.common.api.Command

data class ComplexAdbCommand(override val body: List<String>) : Command(body)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kaspersky.adbserver.commandtypes

import com.kaspersky.adbserver.common.api.Command

data class ComplexCmdCommand(override val body: List<String>) : Command(body)
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ import java.io.Serializable
/**
* Command to execute by AdbServer
*/
abstract class Command(open val body: String) : Serializable
abstract class Command : Serializable {
open val body: List<String>

constructor(body: List<String>) {
this.body = body
}

constructor(body: String) {
this.body = listOf(body)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.kaspersky.adbserver.device
import com.kaspersky.adbserver.common.api.CommandResult
import com.kaspersky.adbserver.commandtypes.AdbCommand
import com.kaspersky.adbserver.commandtypes.CmdCommand
import com.kaspersky.adbserver.commandtypes.ComplexAdbCommand
import com.kaspersky.adbserver.commandtypes.ComplexCmdCommand
import com.kaspersky.adbserver.common.log.LoggerFactory
import com.kaspersky.adbserver.common.log.logger.LogLevel
import com.kaspersky.adbserver.common.log.logger.Logger
Expand All @@ -27,10 +29,32 @@ object AdbTerminal {
AdbCommand(command)
) ?: throw IllegalStateException("Please first of all call [connect] method to establish a connection")

/**
* Allows more control over how arguments are parsed. Each list element is a command or an argument used as is.
* Refer to the https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html
*
* @param command the list of the commands and arguments
* Please first of all call [connect] method to establish a connection
*/
fun executeAdb(command: List<String>): CommandResult = device?.fulfill(
ComplexAdbCommand(command)
) ?: throw IllegalStateException("Please first of all call [connect] method to establish a connection")

/**
* Please first of all call [connect] method to establish a connection
*/
fun executeCmd(command: String): CommandResult = device?.fulfill(
CmdCommand(command)
) ?: throw IllegalStateException("Please first of all call [connect] method to establish a connection")

/**
* Allows more control over how arguments are parsed. Each list element is a command or an argument used as is.
* Refer to the https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html
*
* @param command the list of the commands and arguments
* Please first of all call [connect] method to establish a connection
*/
fun executeCmd(command: List<String>): CommandResult = device?.fulfill(
ComplexCmdCommand(command)
) ?: throw IllegalStateException("Please first of all call [connect] method to establish a connection")
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,39 @@ internal class CmdCommandPerformer(
process.destroy()
}
}

/**
* Be aware it's a synchronous method
*/
fun perform(command: List<String>): CommandResult {
val serviceInfo = "The command was executed on desktop=$desktopName"
val process = ProcessBuilder(command).start()
try {
if (process.waitFor(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
val exitCode = process.exitValue()
return if (exitCode != 0) {
val error = "exitCode=$exitCode, message=${process.errorStream.bufferedReader().readText()}"
CommandResult(
status = ExecutorResultStatus.FAILURE,
description = error,
serviceInfo = serviceInfo
)
} else {
val success = "exitCode=$exitCode, message=${process.inputStream.bufferedReader().readText()}"
CommandResult(
status = ExecutorResultStatus.SUCCESS,
description = success,
serviceInfo = serviceInfo
)
}
}
return CommandResult(
status = ExecutorResultStatus.TIMEOUT,
description = "Command execution timeout ($EXECUTION_TIMEOUT_SECONDS sec) overhead",
serviceInfo = serviceInfo
)
} finally {
process.destroy()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.kaspersky.adbserver.common.api.CommandExecutor
import com.kaspersky.adbserver.common.api.CommandResult
import com.kaspersky.adbserver.commandtypes.AdbCommand
import com.kaspersky.adbserver.commandtypes.CmdCommand
import com.kaspersky.adbserver.commandtypes.ComplexAdbCommand
import com.kaspersky.adbserver.commandtypes.ComplexCmdCommand
import com.kaspersky.adbserver.common.log.logger.Logger
import java.lang.UnsupportedOperationException

Expand All @@ -18,12 +20,32 @@ internal class CommandExecutorImpl(

override fun execute(command: Command): CommandResult {
return when (command) {
is CmdCommand -> cmdCommandPerformer.perform(command.body)
is CmdCommand -> cmdCommandPerformer.perform(command.command)
is ComplexCmdCommand -> cmdCommandPerformer.perform(command.body)

is AdbCommand -> {
val adbCommand = "$adbPath ${ adbServerPort?.let { "-P $adbServerPort " } ?: "" }-s $deviceName ${command.body}"
val adbCommand = "$adbPath ${adbServerPort?.let { "-P $adbServerPort " } ?: ""}-s $deviceName ${command.command}"
logger.d("The created adbCommand=$adbCommand")
cmdCommandPerformer.perform(adbCommand)
}

is ComplexAdbCommand -> {

val adbCommand = buildList {
add(adbPath)
adbServerPort?.let {
add("-P")
add(adbServerPort)
}
add("-s")
add(deviceName)

addAll(command.body)
}
logger.d("The created complex adbCommand=$adbCommand")
cmdCommandPerformer.perform(adbCommand)
}

else -> throw UnsupportedOperationException("The command=$command is unsupported command")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ interface AdbServer {
*/
fun performCmd(vararg commands: String): List<String>

/**
* Performs shell commands blocking current thread. Allows more control over how arguments are parsed.
matzuk marked this conversation as resolved.
Show resolved Hide resolved
* Each list element is used as is. Refer to the https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please unify your comments. Somewhere you write class and method name, somewhere you provide a link.

*
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
fun performCmd(command: List<String>): String

/**
* Performs adb commands blocking current thread.
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
Expand All @@ -38,6 +52,20 @@ interface AdbServer {
*/
fun performAdb(vararg commands: String): List<String>

/**
* Performs adb commands blocking current thread. Allows more control over how arguments are parsed.
* Each list element is used as is. Refer to the https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html.
*
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
fun performAdb(command: List<String>): String

/**
* Performs shell commands blocking current thread.
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
Expand All @@ -50,6 +78,20 @@ interface AdbServer {
*/
fun performShell(vararg commands: String): List<String>

/**
* Performs shell commands blocking current thread. Allows more control over how arguments are parsed.
* Each list element is used as is. Refer to the https://docs.oracle.com/javase/8/docs/api/java/lang/ProcessBuilder.html.
*
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
fun performShell(command: List<String>): String

/**
* Disconnect from AdbServer.
* The method is called by Kaspresso after each test.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,58 +30,41 @@ class AdbServerImpl(
return AdbTerminal
}

/**
* Executes shell commands blocking current thread.
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
override fun performCmd(vararg commands: String): List<String> {
return perform(commands) {
adbTerminal.executeCmd(it)
}
}

/**
* Performs adb commands blocking current thread.
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
override fun performCmd(command: List<String>): String {
return performComplex(command, adbTerminal::executeCmd)
}

override fun performAdb(vararg commands: String): List<String> {
return perform(commands) {
adbTerminal.executeAdb(it)
}
}

/**
* Performs shell commands blocking current thread.
* Please be aware! If any command that is in @param commands failed then AdbServerException will be thrown
*
* Required Permissions: INTERNET.
*
* @param commands commands to execute.
* @throws AdbServerException if a result status of any command in @param commands is Failed
* @return list of answers of commands' execution
*/
override fun performAdb(command: List<String>): String {
return performComplex(command, adbTerminal::executeAdb)
}

override fun performShell(vararg commands: String): List<String> {
return perform(commands) {
adbTerminal.executeAdb("shell $it")
}
}

/**
* Disconnect from AdbServer.
* The method is called by Kaspresso after each test.
*/
override fun performShell(command: List<String>): String {
return performComplex(command) { complexCommand: List<String> ->
adbTerminal.executeAdb(buildList {
add("shell")
addAll(complexCommand)
})
}
}

override fun disconnectServer() {
if (connected) {
adbTerminal.disconnect()
Expand All @@ -95,16 +78,27 @@ class AdbServerImpl(
logger.i("AdbServer. The command to execute=$command")
}
.map { command -> command to executor.invoke(command) }
.onEach { (command, result) ->
logger.i("AdbServer. The command=$command was performed with result=$result")
}
.onEach { (command, result) ->
if (result.status == ExecutorResultStatus.FAILURE) {
throw AdbServerException("AdbServer. The command=$command was performed with failed result=$result")
}
if (result.status == ExecutorResultStatus.TIMEOUT) {
throw AdbServerException(
"""
.onEach { (command, result) -> logCommandResult(command, result) }
.map { (_, result) -> result.description }
.toList()
}

private fun performComplex(command: List<String>, executor: (List<String>) -> CommandResult): String {
logger.i("AdbServer. The complex command to execute=$command")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arguments missed

val result = executor.invoke(command)
logCommandResult(command.toString(), result)

return result.description
}

private fun logCommandResult(command: String, result: CommandResult) {
logger.i("AdbServer. The command=$command was performed with result=$result")
if (result.status == ExecutorResultStatus.FAILURE) {
throw AdbServerException("AdbServer. The command=$command was performed with failed result=$result")
}
if (result.status == ExecutorResultStatus.TIMEOUT) {
throw AdbServerException(
"""

AdbServer. The command=$command was performed with timeout exception.
There are two possible reasons:
Expand All @@ -122,10 +116,7 @@ class AdbServerImpl(
c. Start 'adbserver-desktop.jar' with the command in Terminal - 'java -jar /Users/yuri.gagarin/Desktop/adbserver-desktop.jar

""".trimIndent()
)
}
}
.map { (_, result) -> result.description }
.toList()
)
}
}
}
Loading