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

Add Kaspresso plugin #417

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ check_android_task:
Xvfb :99 -screen 0 1000x1000x16 &
sleep 5
startxfce4
start_adb_server_background_script:
java -jar artifacts/adbserver-desktop.jar || true
accept_licenses_script:
echo yes | sdkmanager --licenses
install_emulator_script:
Expand All @@ -43,7 +41,7 @@ check_android_task:
-no-snapshot
# -no-window is not used here because the flag somehow affects the "GPS" test. actual for API=30
assemble_instrumented_tests_script:
./gradlew assembleDebugAndroidTest
cd samples && ./gradlew assembleDebugAndroidTest
wait_for_avd_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 3; done; input keyevent 82'
configure_avd_script: |
Expand All @@ -53,7 +51,7 @@ check_android_task:
adb shell settings put secure spell_checker_enabled 0
adb shell settings put secure show_ime_with_hard_keyboard 1
run_tests_script:
./gradlew connectedDebugAndroidTest
cd samples && ./gradlew connectedDebugAndroidTest
# After we do "adb root" connection is closed for a moment. So first attempt to pull screenshots usually fails
# That's why we make 5 attempts to pull folders
always:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kaspersky.adbserver.common.log

import com.kaspersky.adbserver.common.log.filterlog.FullLoggerOptimiser
import com.kaspersky.adbserver.common.log.fulllogger.FullLogger
import com.kaspersky.adbserver.common.log.fulllogger.FullLoggerSystemImpl
import com.kaspersky.adbserver.common.log.logger.DesktopLogger
import com.kaspersky.adbserver.common.log.logger.LogLevel
Expand All @@ -12,8 +13,12 @@ import com.kaspersky.adbserver.common.log.logger.LoggerImpl
*/
object LoggerFactory {

fun getDesktopLogger(logLevel: LogLevel, desktopName: String): DesktopLogger {
val logger = getCommonLogger(logLevel, desktopName)
fun getDesktopLogger(
logLevel: LogLevel,
desktopName: String,
fullLogger: FullLogger = FullLoggerSystemImpl(logLevel, desktopName, null)
): DesktopLogger {
val logger = getCommonLogger(logLevel, desktopName, fullLogger = fullLogger)
return DesktopLogger(logger, logLevel, desktopName)
}

Expand All @@ -26,8 +31,12 @@ object LoggerFactory {
fun getDeviceLogger(logLevel: LogLevel): Logger =
getCommonLogger(logLevel)

private fun getCommonLogger(logLevel: LogLevel, desktopName: String? = null, deviceName: String? = null): Logger {
val fullLogger = FullLoggerSystemImpl(logLevel, desktopName, deviceName)
private fun getCommonLogger(
logLevel: LogLevel,
desktopName: String? = null,
deviceName: String? = null,
fullLogger: FullLogger = FullLoggerSystemImpl(logLevel, desktopName, deviceName)
): Logger {
val fullLoggerWrapper =
if (logLevel == LogLevel.DEBUG) FullLoggerOptimiser(originalFullLogger = fullLogger, generateLogs = true) else fullLogger
return LoggerImpl(fullLoggerWrapper)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.kaspersky.adbserver.common.log.fulllogger

import com.kaspersky.adbserver.common.log.logger.LogLevel

internal interface FullLogger {
interface FullLogger {

fun log(
logLevel: LogLevel? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kaspersky.adbserver.desktop

import com.kaspersky.adbserver.common.api.CommandResult
import java.nio.file.Path

/**
* @param adbPath - path to adb binary
*/
class AdbCommandPerformer(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
class AdbCommandPerformer(
class AdbCommandExecutor(

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I found CmdCommandPerformer exists already

private val adbPath: Path,
private val cmdCommandPerformer: CmdCommandPerformer,
) {

/**
* Be aware it's a synchronous method
* @param command - adb command without path to adb binaries
*/
fun perform(command: String): CommandResult {
return cmdCommandPerformer.perform("$adbPath $command")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package com.kaspersky.adbserver.desktop

import com.kaspersky.adbserver.common.api.CommandResult
import com.kaspersky.adbserver.common.api.ExecutorResultStatus
import java.io.File
import java.nio.file.Path
import java.util.concurrent.TimeUnit

internal class CmdCommandPerformer(
private val desktopName: String
/**
* @param workingDir - working directory used to execute any cmd command if null when use default process working directory
*/
class CmdCommandPerformer(
private val desktopName: String,
private val workingDir: Path? = null
) {

companion object {
Expand All @@ -17,7 +23,8 @@ internal class CmdCommandPerformer(
*/
fun perform(command: String): CommandResult {
val serviceInfo = "The command was executed on desktop=$desktopName"
val process = Runtime.getRuntime().exec(command)
val workingDir = workingDir?.toFile() ?: File(".")
val process = Runtime.getRuntime().exec(command, emptyArray(), workingDir)
try {
if (process.waitFor(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
val exitCode = process.exitValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.lang.UnsupportedOperationException

internal class CommandExecutorImpl(
private val cmdCommandPerformer: CmdCommandPerformer,
private val adbCommandPerformer: AdbCommandPerformer,
private val deviceName: String,
private val adbServerPort: String?,
private val logger: Logger
Expand All @@ -19,9 +20,9 @@ internal class CommandExecutorImpl(
return when (command) {
is CmdCommand -> cmdCommandPerformer.perform(command.body)
is AdbCommand -> {
val adbCommand = "adb ${ adbServerPort?.let { "-P $adbServerPort " } ?: "" }-s $deviceName ${command.body}"
logger.d("The created adbCommand=$adbCommand")
cmdCommandPerformer.perform(adbCommand)
val adbCommand = "${ adbServerPort?.let { "-P $adbServerPort " } ?: "" }-s $deviceName ${command.body}"
logger.d("The created adbCommand=adb $adbCommand")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be cool to print out actual adb path since it's configurable. Otherwise it may mislead someone during adb related issues debugging

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this good idea. I'm add print adb path to log at server startup.

adbCommandPerformer.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 @@ -3,31 +3,66 @@ package com.kaspersky.adbserver.desktop
import com.kaspersky.adbserver.common.api.ExecutorResultStatus
import com.kaspersky.adbserver.common.log.LoggerFactory
import com.kaspersky.adbserver.common.log.logger.DesktopLogger
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import kotlin.concurrent.thread

internal class Desktop(
class Desktop(
private val cmdCommandPerformer: CmdCommandPerformer,
private val adbCommandPerformer: AdbCommandPerformer,
private val presetEmulators: List<String>,
private val adbServerPort: String?,
private val logger: DesktopLogger
) {

companion object {
private const val PAUSE_MS = 500L
private val DEVICE_PATTERN = Pattern.compile("^([a-zA-Z0-9\\-:.]+)(\\s+)(device)")
}

private val devices: MutableCollection<DeviceMirror> = mutableListOf()
private var isRunning = AtomicBoolean(false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it accessed from multiple threads?


fun startDevicesObserving() {
/**
* Start Desktop server.
* Blocking current thread while server working
* @throws IllegalStateException - if server already running
*/
fun startDevicesObservingSync() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I would just mention that this method is blocking in documentation as you did in AdbCommandPerformer::perform. I think blocking behavior is an expected one. Another hint is that there's startDevicesObservingAsync

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, I'm add documentation to this methods

if (!isRunning.compareAndSet(false, true)) error("Desktop already running")
startDevicesObservingInternal()
}

/**
* Start Desktop server asynchronously
* @throws IllegalStateException - if server already running
*/
fun startDevicesObservingAsync() {
if (!isRunning.compareAndSet(false, true)) error("Desktop already running")
thread {
startDevicesObservingInternal()
}
}

/**
* Stop Desktop server
* @throws IllegalStateException - if server already stopped
*/
fun stopDevicesObserving() {
if (!isRunning.compareAndSet(true, false)) error("Desktop already stopped")
}

private fun startDevicesObservingInternal() {
logger.d("start")
while (true) {
while (isRunning.get()) {
val namesOfAttachedDevicesByAdb = getAttachedDevicesByAdb()
namesOfAttachedDevicesByAdb.forEach { deviceName ->
if (devices.find { client -> client.deviceName == deviceName } == null) {
logger.i("New device has been found: $deviceName. Initialize connection to the device...")
val deviceMirror =
DeviceMirror.create(
cmdCommandPerformer,
adbCommandPerformer,
deviceName,
adbServerPort,
LoggerFactory.getDesktopLoggerReflectingDevice(logger, deviceName)
Expand All @@ -47,18 +82,22 @@ internal class Desktop(
}
Thread.sleep(PAUSE_MS)
}

devices.forEach { client ->
client.stopConnectionToDevice()
}
devices.clear()
}

private fun getAttachedDevicesByAdb(): List<String> {
val pattern = Pattern.compile("^([a-zA-Z0-9\\-:.]+)(\\s+)(device)")
val commandResult = cmdCommandPerformer.perform("adb devices")
val commandResult = adbCommandPerformer.perform("devices")
if (commandResult.status != ExecutorResultStatus.SUCCESS) {
return emptyList()
}
val adbDevicesCommandResult: String = commandResult.description
return adbDevicesCommandResult.lines()
.asSequence()
.map { pattern.matcher(it) }
.map { DEVICE_PATTERN.matcher(it) }
.filter { matcher -> matcher.find() }
.map { matcher -> matcher.group(1) }
.filter { foundEmulator -> presetEmulators.isEmpty() || presetEmulators.contains(foundEmulator) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class DeviceMirror private constructor(

fun create(
cmdCommandPerformer: CmdCommandPerformer,
adbCommandPerformer: AdbCommandPerformer,
deviceName: String,
adbServerPort: String?,
logger: Logger
Expand All @@ -30,6 +31,7 @@ internal class DeviceMirror private constructor(
DesktopDeviceSocketConnectionFactory.getSockets(DesktopDeviceSocketConnectionType.FORWARD)
val commandExecutor = CommandExecutorImpl(
cmdCommandPerformer,
adbCommandPerformer,
deviceName,
adbServerPort,
logger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.delimiter
import java.io.File
import java.lang.management.ManagementFactory
import java.nio.file.Path
import kotlin.system.exitProcess

private const val DESKTOP = "Desktop-"
private const val ERROR_EXIT_CODE = -1

internal fun main(args: Array<String>) {
val parser = ArgParser("Adb Server")
Expand All @@ -34,25 +38,52 @@ internal fun main(args: Array<String>) {
description = "Logs Level"
).default(LogLevel.INFO)

val adbPathFromArguments by parser.option(
type = ArgType.String,
fullName = "adbPath",
description = "Path to adb binary, if not set when use 'adb' from process environment"
)

parser.parse(args)

val desktopName = getDesktopName()
val desktopLogger = LoggerFactory.getDesktopLogger(logLevel, desktopName)

desktopLogger.i("Desktop started with arguments: emulators=$emulators, adbServerPort=$port")
val adbPath = adbPathFromArguments?.let { Path.of(it) } ?: findAdbAtPath()

if (adbPath == null) {
desktopLogger.e("Adb path not passed via arguments. Adb not found at path variable. Aborting...")
exitProcess(ERROR_EXIT_CODE)
}

desktopLogger.i("Desktop started with arguments: emulators=$emulators, adbServerPort=$port, adbPath=$adbPath")

val cmdCommandPerformer = CmdCommandPerformer(desktopName)
val adbCommandPerformer = AdbCommandPerformer(adbPath, cmdCommandPerformer)
val desktop = Desktop(
cmdCommandPerformer = cmdCommandPerformer,
adbCommandPerformer = adbCommandPerformer,
presetEmulators = emulators,
adbServerPort = port,
logger = desktopLogger
)
desktop.startDevicesObserving()
desktop.startDevicesObservingSync()
}

private fun getDesktopName(): String {
val processName = ManagementFactory.getRuntimeMXBean().name
val pid = processName.split("@".toRegex()).toTypedArray()[0].toLong()
return DESKTOP + pid
}

/**
* Search for adb executable at environment PATH variable
*/
private fun findAdbAtPath(): Path? {
return System.getenv("PATH")
.split(":")
.asSequence()
.map { File(it) }
.flatMap { it.listFiles()?.toList() ?: emptyList() }
.firstOrNull { it.nameWithoutExtension == "adb" }?.toPath()
}
2 changes: 0 additions & 2 deletions build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
enableFeaturePreview("VERSION_CATALOGS")

rootProject.name = "build-logic"

include("android")
Expand Down
File renamed without changes.
File renamed without changes.
21 changes: 21 additions & 0 deletions kaspresso-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
`kotlin-dsl`
`java-gradle-plugin`
}

dependencies {
implementation(libs.androidPlugin)
implementation(projects.adbServer.adbserverCommon)
implementation(projects.adbServer.adbserverDesktop)
}

// TODO setup maven publication
group = "com.kaspersky.kaspresso"
gradlePlugin {
plugins {
create("SignerPlugin") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a SignerPlugin

id = "com.kaspersky.kaspresso"
implementationClass = "com.kaspersky.kaspresso.plugin.KaspressoPlugin"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.kaspersky.kaspresso.plugin

import com.kaspersky.adbserver.common.log.LoggerFactory
import com.kaspersky.adbserver.common.log.logger.LogLevel
import com.kaspersky.adbserver.desktop.AdbCommandPerformer
import com.kaspersky.adbserver.desktop.CmdCommandPerformer
import com.kaspersky.adbserver.desktop.Desktop
import org.gradle.api.logging.Logger
import java.nio.file.Path

internal class DesktopServerHolder(private val logger: Logger) {
companion object {
private const val DESKTOP_NAME = "kaspresso-plugin-adb-server"
}

private var desktop: Desktop? = null

@Synchronized
fun start(workingDirectory: Path, adbPath: Path) {
check(desktop == null) { "Desktop already started" }

logger.debug("Starting Desktop server. workingDir=$workingDirectory, adbPath=$adbPath")

val cmdCommandPerformer = CmdCommandPerformer(DESKTOP_NAME, workingDirectory)
val adbCommandPerformer = AdbCommandPerformer(adbPath, cmdCommandPerformer)
val logger = LoggerFactory.getDesktopLogger(LogLevel.VERBOSE, DESKTOP_NAME, GradleFullLogger(logger))
desktop = Desktop(
cmdCommandPerformer = cmdCommandPerformer,
adbCommandPerformer = adbCommandPerformer,
presetEmulators = emptyList(),
adbServerPort = null,
logger = logger
)
.apply { startDevicesObservingAsync() }
}

@Synchronized
fun stop() {
check(desktop != null) { "Desktop not started" }
desktop!!.stopDevicesObserving()
desktop = null
}
}
Loading