Compare commits

...

1 Commits

Author SHA1 Message Date
bean 0a85a0b0b5 RD-213 implementation http proxy 2 years ago
  1. 1
      .gitignore
  2. 12
      README.md
  3. 1
      build.gradle.kts
  4. 8
      gradle/libs.versions.toml
  5. 10
      src/main/kotlin/tv/anypoint/ApplicationProperties.kt
  6. 3
      src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt
  7. 3
      src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt
  8. 36
      src/main/kotlin/tv/anypoint/dsl/Dsl.kt
  9. 13
      src/main/kotlin/tv/anypoint/dsl/TestCaseConfig.kt
  10. 11
      src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt
  11. 12
      src/main/kotlin/tv/anypoint/dsl/model/Log.kt
  12. 8
      src/main/kotlin/tv/anypoint/dsl/model/LogInfo.kt
  13. 4
      src/main/kotlin/tv/anypoint/dsl/model/Recording.kt
  14. 14
      src/main/kotlin/tv/anypoint/dsl/model/Tc.kt
  15. 5
      src/main/kotlin/tv/anypoint/dsl/model/TcContext.kt
  16. 9
      src/main/kotlin/tv/anypoint/dsl/model/TcFailEvent.kt
  17. 3
      src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt
  18. 11
      src/main/kotlin/tv/anypoint/dsl/service/AdbTransmitter.kt
  19. 72
      src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt
  20. 41
      src/main/kotlin/tv/anypoint/proxy/adapter/AssignAdapter.kt
  21. 34
      src/main/kotlin/tv/anypoint/proxy/adapter/AuthAdapter.kt
  22. 30
      src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt
  23. 32
      src/main/kotlin/tv/anypoint/proxy/config/ProxyApplicationConfig.kt
  24. 2
      src/main/kotlin/tv/anypoint/proxy/model/adb/ChangeCommand.kt
  25. 4
      src/main/kotlin/tv/anypoint/proxy/model/adb/ExtraKey.kt
  26. 4
      src/main/kotlin/tv/anypoint/proxy/model/adb/IntentAction.kt
  27. 5
      src/main/kotlin/tv/anypoint/proxy/model/agent/AuthRequest.kt
  28. 15
      src/main/kotlin/tv/anypoint/proxy/model/agent/AuthResponse.kt
  29. 2
      src/main/kotlin/tv/anypoint/proxy/model/agent/Cue.kt
  30. 2
      src/main/kotlin/tv/anypoint/proxy/model/agent/CueOwner.kt
  31. 4
      src/main/kotlin/tv/anypoint/proxy/model/agent/PlayType.kt
  32. 7
      src/main/kotlin/tv/anypoint/proxy/model/agent/PlayerStateCheckParam.kt
  33. 5
      src/main/kotlin/tv/anypoint/proxy/model/agent/ProgramProviderChannel.kt
  34. 2
      src/main/kotlin/tv/anypoint/proxy/model/agent/StateChangeLog.kt
  35. 7
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Ad.kt
  36. 6
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdRequest.kt
  37. 5
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdsResponse.kt
  38. 2
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdsSyncRequest.kt
  39. 6
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Asset.kt
  40. 3
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AssetConvertResponse.kt
  41. 5
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Click.kt
  42. 4
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/ProgramPlacement.kt
  43. 5
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/TargetAd.kt
  44. 3
      src/main/kotlin/tv/anypoint/proxy/model/agent/ad/VastResponse.kt
  45. 6
      src/main/kotlin/tv/anypoint/proxy/model/push/PushCommandType.kt
  46. 42
      src/main/kotlin/tv/anypoint/proxy/service/LogcatService.kt
  47. 98
      src/main/kotlin/tv/anypoint/proxy/service/ProxyDeviceService.kt
  48. 10
      src/main/kotlin/tv/anypoint/proxy/service/PushServerInterface.kt
  49. 21
      src/main/kotlin/tv/anypoint/proxy/service/PushServerService.kt
  50. 54
      src/main/kotlin/tv/anypoint/proxy/service/StbService.kt
  51. 40
      src/main/kotlin/tv/anypoint/proxy/shell/ShellController.kt
  52. 22
      src/main/kotlin/tv/anypoint/proxy/util/JadbDeviceSerialUtil.kt
  53. 4
      src/main/kotlin/tv/anypoint/proxy/util/NetworkUtil.kt
  54. 88
      src/main/kotlin/tv/anypoint/proxy/util/StbUtil.kt
  55. 3
      src/main/kotlin/tv/anypoint/proxy/util/StringUtil.kt
  56. 55
      src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt
  57. 18
      src/main/kotlin/tv/anypoint/proxy/web/TestController.kt
  58. 12
      src/main/kotlin/tv/anypoint/tc/Base1.kt
  59. 28
      src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt
  60. 6
      src/main/resources/application.yaml
  61. 15
      src/test/kotlin/Test.kt
  62. 4
      src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt
  63. 51
      src/test/kotlin/tv/anypoint/dsl/DslKtTest.kt
  64. 44
      src/test/kotlin/tv/anypoint/proxy/adapter/AuthAdapterTest.kt
  65. 15
      src/test/kotlin/tv/anypoint/proxy/service/LogcatServiceTest.kt

1
.gitignore vendored

@ -38,3 +38,4 @@ out/
### Kotlin ###
.kotlin
/android-qa.log

12
README.md

@ -14,8 +14,13 @@
## 실행 방법
1. STB 대여 (아래 사항들 확인 필요함)
- STB 가 접속되는 AP 이름과 비밀번호
- STB 이 접속되는 AP 이름과 비밀번호 (QA 에 문의)
- STB IP, adb 포트 번호
- ip:
1. 리모콘으로 번호 입력 `*7899#`
2. 기기정보
3. IP 설정
- port: 대부분 `8888` skb bko-uh600/bhx-uh600 의 경우 `5815`
1. STB 설치
1. 컴퓨터의 USB 에 캡쳐보드를 꼽고 캡쳐보드와 STB output 과 연결
1. 컴퓨터에 obs-studio 설치 및 실행
@ -30,6 +35,11 @@
1. application.yml 에서 anypoint.android-qa.stb 하위 정보들을 STB 의 정보로 수정
1. obs-studio 종료 후 android-qa 애플리케이션 실행 (obs-studio 종료해야 정상 작동됨)
## logcat 확인
```
서버를 띄우면 stb 에 알아서 붙으므로
```
## TC 수행 순서
1. adb 를 사용하여 STB 이 프록시 서버를 바라보도록 변경

1
build.gradle.kts

@ -22,7 +22,6 @@ repositories {
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")

8
gradle/libs.versions.toml

@ -9,6 +9,9 @@ kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core",
kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version = "2.0.11" }
thymeleaf = { module = "org.thymeleaf:thymeleaf", version = "3.1.2.RELEASE" }
thymeleaf-spring5 = { module = "org.thymeleaf:thymeleaf-spring5", version = "3.1.2.RELEASE" }
spring-shell = { module = "org.springframework.shell:spring-shell-starter", version = "3.2.3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version = "2.11.0" }
retrofix-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version = "2.11.0" }
kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version = "5.8.1" }
kotest-property = { module = "io.kotest:kotest-property", version = "5.8.1" }
@ -22,7 +25,10 @@ deps = [
"kotlin-logging",
"thymeleaf",
"thymeleaf-spring5",
"kotlinx-coroutines"
"kotlinx-coroutines",
"spring-shell",
"retrofit",
"retrofix-kotlinx-serialization"
]
test-deps = [
"kotest-runner",

10
src/main/kotlin/tv/anypoint/ApplicationProperties.kt

@ -19,11 +19,17 @@ class ApplicationProperties {
class Stb {
lateinit var ip: String
var port: Int = 5555
/**
* 보통 8888
* bko-uh600 / bhx-uh600
* port : 5815
*/
var port: Int = 8888
}
class Endpoints {
lateinit var auth: String
lateinit var assign: String
}
}
}

3
src/main/kotlin/tv/anypoint/domain/agent/ad/AssetConvertResponse.kt

@ -1,3 +0,0 @@
package tv.anypoint.domain.agent.ad
data class AssetConvertResponse(val id: Long)

3
src/main/kotlin/tv/anypoint/domain/agent/ad/VastResponse.kt

@ -1,3 +0,0 @@
package tv.anypoint.domain.agent.ad
data class VastResponse(val id: Long)

36
src/main/kotlin/tv/anypoint/dsl/Dsl.kt

@ -1,10 +1,16 @@
package tv.anypoint.dsl
import tv.anypoint.domain.adb.ExtraKey
import tv.anypoint.domain.adb.IntentAction
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import tv.anypoint.proxy.model.adb.ExtraKey
import tv.anypoint.proxy.model.adb.IntentAction
import tv.anypoint.dsl.handler.HttpHandler
import tv.anypoint.dsl.model.Log
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.service.TestCase
import java.io.BufferedReader
import java.io.FileReader
import java.io.LineNumberReader
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeoutException
@ -17,31 +23,37 @@ inline fun <reified T> http(
block: HttpHandler<T>.() -> Unit
) = HttpHandler<T>().also { block(it) }
data class Adb(
val a: IntentAction,
val es: Map<ExtraKey, String>
)
inline fun adb(
fun adb(
a: IntentAction,
es: Map<ExtraKey, String>
) {
val adb = Adb(a, es)
val aStr = "-a $a"
val esStr: String = es.entries.joinToString(" ") { "--es ${it.key.string} ${it.value}" }
println("[ADB] adb shell am broadcast $aStr $esStr")
}
fun TestCase.expected(
fun Log.expected(
expectedLog: String,
timeout: Long = 10_000L
) {
val log = this
val startedAt = LocalDateTime.now()
val reader = LineNumberReader(BufferedReader(FileReader(log.logcatFilePath)))
while (true) {
val now = LocalDateTime.now()
if (ChronoUnit.MILLIS.between(startedAt, now) > timeout) {
throw TimeoutException("failed to find log. expectedLog: $expectedLog, absoluteFilePath: ${this.tc.logInfo.absoluteFilePath}")
throw TimeoutException("failed to find log. expectedLog: $expectedLog")
}
reader.lineNumber = log.cursor
var line: String?
while ((reader.readLine().also { line = it }) != null) {
if (line!!.contains(expectedLog)) {
return
}
log.cursor = reader.lineNumber
}
// TODO: tc 로그에서 expectedLog 찾기 tc.logInfo.cursor ~ 마지막 라인까지의 로그 중에 찾으면 됨
Thread.sleep(100)
}
}

13
src/main/kotlin/tv/anypoint/dsl/TestCaseConfig.kt

@ -0,0 +1,13 @@
package tv.anypoint.dsl
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope
import tv.anypoint.dsl.model.TcContext
@Configuration
class TestCaseConfig {
@Bean
@Scope("singleton")
fun tcContext(): TcContext = TcContext()
}

11
src/main/kotlin/tv/anypoint/dsl/exception/HttpValidationException.kt

@ -3,10 +3,9 @@ package tv.anypoint.dsl.exception
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.proxy.model.agent.ad.AssetConvertResponse
import tv.anypoint.proxy.model.agent.ad.VastResponse
class HttpValidationException(
type: HttpValidationExceptionType,
@ -27,11 +26,11 @@ inline fun <reified T> httpValidationError(
response: T
): HttpValidationException = HttpValidationException(
type = when (T::class) {
AuthResponse::class -> HttpValidationExceptionType.AUTH
tv.anypoint.proxy.model.agent.AuthResponse::class -> HttpValidationExceptionType.AUTH
AdsResponse::class -> HttpValidationExceptionType.ADS
VastResponse::class -> HttpValidationExceptionType.VAST
AssetConvertResponse::class -> HttpValidationExceptionType.ASSET_CONVERT
else -> HttpValidationExceptionType.NONE
},
response = Json.encodeToJsonElement(response)
)
)

12
src/main/kotlin/tv/anypoint/dsl/model/Log.kt

@ -0,0 +1,12 @@
package tv.anypoint.dsl.model
import kotlinx.serialization.Serializable
@Serializable
class Log(
val logcatFilePath: String
) {
var startLine: Int = 0
var cursor: Int = 0
var lastLine: Int = 0
}

8
src/main/kotlin/tv/anypoint/dsl/model/LogInfo.kt

@ -1,8 +0,0 @@
package tv.anypoint.dsl.model
class LogInfo(
val absoluteFilePath: String
) {
var cursor: Long = 0
var lastLine: Long = 0
}

4
src/main/kotlin/tv/anypoint/dsl/model/RecordingInfo.kt → src/main/kotlin/tv/anypoint/dsl/model/Recording.kt

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
data class RecordingInfo(
data class Recording(
val absoluteFilePath: String
) {
@Contextual
@ -25,4 +25,4 @@ data class RecordingInfo(
}
}
}

14
src/main/kotlin/tv/anypoint/dsl/model/Tc.kt

@ -1,11 +1,11 @@
package tv.anypoint.dsl.model
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
import tv.anypoint.dsl.handler.HttpHandler
import tv.anypoint.dsl.http
import tv.anypoint.proxy.model.agent.AuthResponse
import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.proxy.model.agent.ad.AssetConvertResponse
import tv.anypoint.proxy.model.agent.ad.VastResponse
import java.time.LocalDateTime
class Tc {
@ -26,6 +26,6 @@ class Tc {
lateinit var finishedAt: LocalDateTime
var result: Boolean? = null
lateinit var logInfo: LogInfo
var recordingInfo: RecordingInfo? = null
}
lateinit var log: Log
var recording: Recording? = null
}

5
src/main/kotlin/tv/anypoint/dsl/model/TcContext.kt

@ -0,0 +1,5 @@
package tv.anypoint.dsl.model
class TcContext {
var tc: Tc = Tc()
}

9
src/main/kotlin/tv/anypoint/dsl/model/TcFailEvent.kt

@ -0,0 +1,9 @@
package tv.anypoint.dsl.model
import kotlinx.serialization.Serializable
@Serializable
data class TcFailEvent(
val tcName: String,
val errorMessage: String
)

3
src/main/kotlin/tv/anypoint/dsl/serialization/JsonConfiguration.kt

@ -11,9 +11,10 @@ import java.time.LocalDateTime
class JsonConfiguration {
@Bean
fun json(): Json = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
contextual(LocalDate::class, LocalDateSerializer())
contextual(LocalDateTime::class, LocalDateTimeSerializer())
}
}
}
}

11
src/main/kotlin/tv/anypoint/dsl/service/AdbTransmitter.kt

@ -1,11 +0,0 @@
package tv.anypoint.dsl.service
import org.springframework.stereotype.Service
import tv.anypoint.dsl.Adb
@Service
class AdbTransmitter {
fun transmit(req: Adb) {
}
}

72
src/main/kotlin/tv/anypoint/dsl/service/TestCase.kt

@ -2,15 +2,11 @@ package tv.anypoint.dsl.service
import mu.KLogging
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import tv.anypoint.ApplicationProperties
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.VastResponse
import tv.anypoint.dsl.exception.httpValidationError
import tv.anypoint.dsl.model.LogInfo
import tv.anypoint.dsl.model.RecordingInfo
import tv.anypoint.dsl.model.Log
import tv.anypoint.dsl.model.Recording
import tv.anypoint.dsl.model.Tc
import java.time.LocalDateTime
@ -25,14 +21,13 @@ abstract class TestCase {
@Autowired
private lateinit var applicationProperties: ApplicationProperties
@Autowired
private lateinit var adbTransmitter: AdbTransmitter
@Autowired
private lateinit var captureBoardRecorder: CaptureBoardRecorder
fun executeTest() {
startToDumpLog()
startRecording(tc)
tc = init()
logger.info("[${tc.number}] starting...")
@ -45,12 +40,6 @@ abstract class TestCase {
// TODO
}
auth()
ads()
vast()
assetConvert()
startRecording(tc)
test()
stopRecording(tc)
@ -67,7 +56,7 @@ abstract class TestCase {
}
private fun startToDumpLog() {
tc.logInfo = LogInfo("${applicationProperties.fileRoot}/${tc.number}.log")
tc.log = Log("${applicationProperties.fileRoot}/${tc.number}.log")
// TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 ${tc.number}.log 파일 새로 만들어서 적재 시작
}
@ -75,57 +64,22 @@ abstract class TestCase {
// TODO main.log >> ${tc.number}.log 적재 종료
}
private fun auth() {
// TODO: auth
val actualResponse = AuthResponse(0)
val response = tc.auth.convert(actualResponse)
if (!tc.auth.validate(response)) {
throw httpValidationError(response)
}
tc.authResponse = tc.auth.convert(response)
}
@EventListener
fun failed() {
private fun ads() {
// TODO: auth
val actualResponse = AdsResponse()
val response = tc.ads.convert(actualResponse)
if (!tc.ads.validate(response)) {
throw httpValidationError(response)
}
tc.adsResponse = tc.ads.convert(response)
}
private fun vast() {
// TODO: auth
val actualResponse = VastResponse(0)
val response = tc.vast.convert(actualResponse)
if (!tc.vast.validate(response)) {
throw httpValidationError(response)
}
tc.vastResponse = tc.vast.convert(response)
}
private fun assetConvert() {
// TODO: auth
val actualResponse = AssetConvertResponse(0)
val response = tc.assetConvert.convert(actualResponse)
if (!tc.assetConvert.validate(response)) {
throw httpValidationError(response)
}
tc.assetConvertResponse = tc.assetConvert.convert(response)
}
private fun startRecording(tc: Tc) {
tc.recordingInfo = RecordingInfo("${applicationProperties.fileRoot}/${tc.number}.mp4")
logger.debug("start to record. info: {}", tc.recordingInfo)
tc.recording = Recording("${applicationProperties.fileRoot}/${tc.number}.mp4")
logger.debug("start to record. info: {}", tc.recording)
// TODO
}
private fun stopRecording(tc: Tc) {
tc.recordingInfo!!.finish(1000L)
tc.recording!!.finish(1000L)
// TODO
logger.debug("finished to record. info: {}", tc.recordingInfo)
logger.debug("finished to record. info: {}", tc.recording)
}
companion object : KLogging()
}
}

41
src/main/kotlin/tv/anypoint/proxy/adapter/AssignAdapter.kt

@ -0,0 +1,41 @@
package tv.anypoint.proxy.adapter
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Query
import tv.anypoint.ApplicationProperties
import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.proxy.model.agent.ad.AdsSyncRequest
interface AssignAdapter {
@POST("/v3/device/ads")
fun ads(
@Query("deviceId") deviceId: Long,
@Query("freeStorage") freeStorage: Long = 0,
@Query("usedStorage") usedStorage: Long = 0
): Call<AdsResponse>
@POST("/v3/device/sync/ads")
fun syncAds(
@Body request: AdsSyncRequest
)
}
@Configuration
class AssignAdapterConfiguration {
@Bean
fun assignAdapter(json: Json, applicationProperties: ApplicationProperties): AssignAdapter {
val retrofit = Retrofit.Builder()
.baseUrl(applicationProperties.endpoints.assign)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
return retrofit.create(AssignAdapter::class.java)
}
}

34
src/main/kotlin/tv/anypoint/proxy/adapter/AuthAdapter.kt

@ -0,0 +1,34 @@
package tv.anypoint.proxy.adapter
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
import tv.anypoint.ApplicationProperties
import tv.anypoint.proxy.model.agent.AuthRequest
import tv.anypoint.proxy.model.agent.AuthResponse
interface AuthAdapter {
@POST("/v3/device/auth")
fun auth(
@Body request: AuthRequest
): Call<AuthResponse>
}
@Configuration
class AuthAdapterConfiguration {
@Bean
fun authAdapter(json: Json, applicationProperties: ApplicationProperties): AuthAdapter {
val retrofit = Retrofit.Builder()
.baseUrl(applicationProperties.endpoints.auth)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
return retrofit.create(AuthAdapter::class.java)
}
}

30
src/main/kotlin/tv/anypoint/proxy/adapter/DeviceV3Adapter.kt

@ -1,30 +0,0 @@
package tv.anypoint.proxy.adapter
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import tv.anypoint.domain.agent.AuthRequest
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsSyncRequest
@FeignClient(
name = "DeviceV3Adapter",
path = "/v3/device"
)
interface DeviceV3Adapter {
@PostMapping("/auth")
fun auth(@RequestBody body: AuthRequest): AuthResponse
@PostMapping("/ads")
fun ads(
@RequestParam(required = true) deviceId: Long,
@RequestParam("freeStorage", required = true, defaultValue = "0") freeStorage: Long = 0,
@RequestParam("usedStorage", required = true, defaultValue = "0") usedStorage: Long = 0
): AdsResponse
@PostMapping("/sync/ads")
fun syncAds(@RequestBody request: AdsSyncRequest)
}

32
src/main/kotlin/tv/anypoint/proxy/config/ProxyApplicationConfig.kt

@ -0,0 +1,32 @@
package tv.anypoint.proxy.config
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mu.KLogging
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import se.vidstige.jadb.JadbDevice
import tv.anypoint.ApplicationProperties
import tv.anypoint.proxy.util.JadbDeviceSerialUtil
import tv.anypoint.proxy.util.NetworkUtil
import tv.anypoint.proxy.util.getDevice
import java.io.File
@Configuration
class ProxyApplicationConfig(
private val applicationProperties: ApplicationProperties
) {
@Bean
fun jadbDevice(): JadbDevice = getDevice(
stbIp = applicationProperties.stb.ip,
stbPort = applicationProperties.stb.port
)
@Bean
fun serverIp(jadbDevice: JadbDevice): String = NetworkUtil.getPrivateIpV4(
JadbDeviceSerialUtil.serialToAddress(jadbDevice.serial)
?: throw RuntimeException("Can't get server ip address")
)
companion object : KLogging()
}

2
src/main/kotlin/tv/anypoint/domain/adb/ChangeCommand.kt → src/main/kotlin/tv/anypoint/proxy/model/adb/ChangeCommand.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.adb
package tv.anypoint.proxy.model.adb
enum class ChangeCommand {
CHANGE_API_ENDPOINT,

4
src/main/kotlin/tv/anypoint/domain/adb/ExtraKey.kt → src/main/kotlin/tv/anypoint/proxy/model/adb/ExtraKey.kt

@ -1,8 +1,8 @@
package tv.anypoint.domain.adb
package tv.anypoint.proxy.model.adb
enum class ExtraKey(val string: String) {
EXTRA_JSON("EXTRA_JSON"),
CHANGE_COMMAND("change.command"),
API_ENDPOINT("api.endpoint"),
TEST_DEVICE("test.device")
}
}

4
src/main/kotlin/tv/anypoint/domain/adb/IntentAction.kt → src/main/kotlin/tv/anypoint/proxy/model/adb/IntentAction.kt

@ -1,5 +1,5 @@
package tv.anypoint.domain.adb
package tv.anypoint.proxy.model.adb
enum class IntentAction(val string: String) {
CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY")
}
}

5
src/main/kotlin/tv/anypoint/domain/agent/AuthRequest.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/AuthRequest.kt

@ -1,5 +1,8 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
import kotlinx.serialization.Serializable
@Serializable
data class AuthRequest(
var fingerPrint: String = "",
val soId: Int = 0,

15
src/main/kotlin/tv/anypoint/domain/agent/AuthResponse.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/AuthResponse.kt

@ -1,7 +1,9 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
import kotlinx.serialization.Serializable
import java.util.concurrent.TimeUnit
@Serializable
data class AuthResponse(
var deviceId: Long = 0,
var deviceTypeId: Int = 0,
@ -25,10 +27,11 @@ data class AuthResponse(
fun authorization() = "Anypoint ${accessToken ?: ""}"
fun monitoringInterval() = monitoringInterval.let {
if (it < MINIMUM_MONITOR_INTERVAL) DEFAULT_MONITOR_PERIOD else it
if (it < tv.anypoint.proxy.model.agent.AuthResponse.Companion.MINIMUM_MONITOR_INTERVAL) tv.anypoint.proxy.model.agent.AuthResponse.Companion.DEFAULT_MONITOR_PERIOD else it
}
}
@Serializable
data class Endpoints(
var auth: String = "",
var requestAds: String = "",
@ -54,6 +57,7 @@ enum class VideoMediaType {
}
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
@Serializable
data class AdConfig(
var maxDownloadBandwidth: Long = 150 * 1024,
var maxLazyDownloadBandwidth: Long = 1024 * 1024,
@ -68,8 +72,8 @@ data class AdConfig(
var remnantTimeThreshold: Int = 2_000,
var maxEndAdPlaytime: Int = 15_000,
var transitionDelay: Int = 0,
var videoPlayMode: VideoPlayMode = VideoPlayMode.PARALLEL,
var videoMediaType: VideoMediaType = VideoMediaType.CONCATENATING,
var videoPlayMode: tv.anypoint.proxy.model.agent.VideoPlayMode = tv.anypoint.proxy.model.agent.VideoPlayMode.PARALLEL,
var videoMediaType: tv.anypoint.proxy.model.agent.VideoMediaType = tv.anypoint.proxy.model.agent.VideoMediaType.CONCATENATING,
var startDelay: Int = 0,
var stopDelay: Int = 0,
var startRenderDelay: Int = 0,
@ -102,6 +106,7 @@ data class AdConfig(
)
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
@Serializable
data class KidWatermark(
var imageUrl: String = "",
var crc: Long = 0,
@ -109,4 +114,4 @@ data class KidWatermark(
var top: Int = 0,
var width: Int = 0,
var height: Int = 0
)
)

2
src/main/kotlin/tv/anypoint/domain/agent/Cue.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/Cue.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
data class Cue(
var id: Long = 0,

2
src/main/kotlin/tv/anypoint/domain/agent/CueOwner.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/CueOwner.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
enum class CueOwner {
PP,

4
src/main/kotlin/tv/anypoint/domain/agent/PlayType.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/PlayType.kt

@ -1,7 +1,7 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
enum class PlayType {
DNP,
LAZY_DNP,
STREAMING
}
}

7
src/main/kotlin/tv/anypoint/domain/agent/PlayerStateCheckParam.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/PlayerStateCheckParam.kt

@ -1,6 +1,9 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
import kotlinx.serialization.Serializable
@Serializable
data class PlayerStateCheckParam(
val playerStateCheckDuration: Int = 1500,
val maxPlayerInvalidateInterval: Int = 300
)
)

5
src/main/kotlin/tv/anypoint/domain/agent/ProgramProviderChannel.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ProgramProviderChannel.kt

@ -1,5 +1,8 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
import kotlinx.serialization.Serializable
@Serializable
data class ProgramProviderChannel(
val id: Int,
val delay: Int = 0,

2
src/main/kotlin/tv/anypoint/domain/agent/StateChangeLog.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/StateChangeLog.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.agent
package tv.anypoint.proxy.model.agent
data class StateChangeLog(
val soId: Int = 0,

7
src/main/kotlin/tv/anypoint/domain/agent/ad/Ad.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Ad.kt

@ -1,5 +1,8 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import kotlinx.serialization.Serializable
@Serializable
data class Ad(
val id: Long,
val adRequest: AdRequest? = null,
@ -25,4 +28,4 @@ enum class CampaignType {
HOUSE,
BUFFER,
ENDING
}
}

6
src/main/kotlin/tv/anypoint/domain/agent/ad/AdRequest.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdRequest.kt

@ -1,6 +1,7 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import tv.anypoint.domain.agent.PlayType
import kotlinx.serialization.Serializable
import tv.anypoint.proxy.model.agent.PlayType
enum class EncodingType {
@ -14,6 +15,7 @@ enum class ExtAdProtocol {
GOOGLE_AD
}
@Serializable
data class AdRequest(
var url: String,
val updateInterval: Int = 0,

5
src/main/kotlin/tv/anypoint/domain/agent/ad/AdsResponse.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdsResponse.kt

@ -1,5 +1,8 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import kotlinx.serialization.Serializable
@Serializable
data class AdsResponse(
val status: AdListResponseStatus = AdListResponseStatus.OK,
val ads: List<Ad> = listOf(),

2
src/main/kotlin/tv/anypoint/domain/agent/ad/AdsSyncRequest.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AdsSyncRequest.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
data class AdsSyncRequest(
val deviceId: Long,

6
src/main/kotlin/tv/anypoint/domain/agent/ad/Asset.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Asset.kt

@ -1,7 +1,9 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import tv.anypoint.domain.agent.PlayType
import kotlinx.serialization.Serializable
import tv.anypoint.proxy.model.agent.PlayType
@Serializable
data class Asset(
var assetId: Long = 0L,
var crc: String = "0",

3
src/main/kotlin/tv/anypoint/proxy/model/agent/ad/AssetConvertResponse.kt

@ -0,0 +1,3 @@
package tv.anypoint.proxy.model.agent.ad
data class AssetConvertResponse(val id: Long)

5
src/main/kotlin/tv/anypoint/domain/agent/ad/Click.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/Click.kt

@ -1,5 +1,8 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import kotlinx.serialization.Serializable
@Serializable
data class Click(
val targetType: String,
val targetValue: String,

4
src/main/kotlin/tv/anypoint/domain/agent/ad/ProgramPlacement.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/ProgramPlacement.kt

@ -1,4 +1,4 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
data class ProgramPlacement(
val id: Int,
@ -18,4 +18,4 @@ data class PlacementUniqueKey(
enum class MediaType {
SO, PP
}
}

5
src/main/kotlin/tv/anypoint/domain/agent/ad/TargetAd.kt → src/main/kotlin/tv/anypoint/proxy/model/agent/ad/TargetAd.kt

@ -1,6 +1,9 @@
package tv.anypoint.domain.agent.ad
package tv.anypoint.proxy.model.agent.ad
import kotlinx.serialization.Serializable
@Serializable
data class TargetAd(
val hours: String? = null,
val days: String? = null,

3
src/main/kotlin/tv/anypoint/proxy/model/agent/ad/VastResponse.kt

@ -0,0 +1,3 @@
package tv.anypoint.proxy.model.agent.ad
data class VastResponse(val id: Long)

6
src/main/kotlin/tv/anypoint/proxy/model/push/PushCommandType.kt

@ -0,0 +1,6 @@
package tv.anypoint.proxy.model.push
enum class PushCommandType(val string: String) {
UPDATE_ASSET_COMMAND("UpdateAssetCommand"),
UPDATE_SYSTEM_INFO("UpdateSystemInfo")
}

42
src/main/kotlin/tv/anypoint/proxy/service/LogcatService.kt

@ -0,0 +1,42 @@
package tv.anypoint.proxy.service
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mu.KLogging
import org.springframework.stereotype.Service
import tv.anypoint.ApplicationProperties
import java.io.File
import java.util.concurrent.TimeUnit
@Service
class LogcatService(
private val applicationProperties: ApplicationProperties
) {
private var dumpProcess: Process? = null
fun startDump() {
val filePath = "${applicationProperties.fileRoot}/all.log"
GlobalScope.launch {
logger.info("start to save logcat. path: $filePath")
launch {
File(filePath).delete()
ProcessBuilder("/bin/sh", "-c", "adb logcat -c").start()
val processBuilder = ProcessBuilder("/bin/sh", "-c", "adb logcat -s AnypointAD")
processBuilder.redirectOutput(File(filePath))
val process = processBuilder.start()
process.onExit().thenAcceptAsync {
logger.info("finished to save logcat. exitValue: ${it.exitValue()}, path: $filePath")
}
dumpProcess = process
Thread.sleep(TimeUnit.HOURS.toMillis(2))
}
}
}
fun stopDump() {
dumpProcess?.destroy()
}
companion object : KLogging()
}

98
src/main/kotlin/tv/anypoint/proxy/service/ProxyDeviceService.kt

@ -0,0 +1,98 @@
package tv.anypoint.proxy.service
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import mu.KLogging
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.stereotype.Service
import tv.anypoint.ApplicationProperties
import tv.anypoint.dsl.exception.httpValidationError
import tv.anypoint.dsl.model.TcContext
import tv.anypoint.proxy.adapter.AssignAdapter
import tv.anypoint.proxy.adapter.AuthAdapter
import tv.anypoint.proxy.model.agent.AuthRequest
import tv.anypoint.proxy.model.agent.AuthResponse
import tv.anypoint.proxy.model.agent.Endpoints
import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.proxy.model.agent.ad.AdsSyncRequest
@Service
class ProxyDeviceService(
private val json: Json,
private val serverIp: String,
private val serverProperties: ServerProperties,
private val applicationProperties: ApplicationProperties,
private val authAdapter: AuthAdapter,
private val assignAdapter: AssignAdapter,
private val tcContext: TcContext,
private val logcatService: LogcatService
) {
fun auth(request: AuthRequest): AuthResponse {
return try {
val authResponse = authAdapter.auth(
request = request
).execute().body()!!
authResponse.endpoints.redirectToProxyServer(
serverIp = serverIp,
serverPort = serverProperties.port,
pushServerPort = applicationProperties.pushServerPort
)
if (!tcContext.tc.auth.validate(authResponse)) {
throw httpValidationError(authResponse)
}
val response = tcContext.tc.auth.convert(authResponse)
tcContext.tc.authResponse = response
logger.info("auth response: ${json.encodeToString(response)}")
if (tcContext.tc.reboot) {
logcatService.startDump()
// TODO: logcat 에서 `auth response body` 를 기다린 다음에 adb ADS_SYNC 호출
} else {
// TODO: adb ADS_SYNC 호출
}
response
} catch(e: Exception) {
logger.error("failed to auth.", e)
throw RuntimeException("failed to auth.")
}
}
private fun Endpoints.redirectToProxyServer(
serverIp: String,
serverPort: Int,
pushServerPort: Int
) {
val serverEndpoint = "http://$serverIp:$serverPort"
val pushServerEndpoint = "$serverIp:$pushServerPort"
this.auth = serverEndpoint
this.requestAds = serverEndpoint
this.adSyncResult = serverEndpoint
this.appLog = serverEndpoint
this.event = serverEndpoint
// this.pushServers = listOf(pushServerEndpoint)
this.ntpServers = emptyList()
this.stateLog = serverEndpoint
this.impressionLog = serverEndpoint
this.assetRequest = serverEndpoint
this.proxyAdLog = serverEndpoint
}
fun ads(deviceId: Long, freeStorage: Long?, usedStorage: Long?): AdsResponse {
val adsResponse = assignAdapter.ads(
deviceId = deviceId,
freeStorage = freeStorage ?: 0,
usedStorage = usedStorage ?: 0
).execute().body()!!
val response = tcContext.tc.ads.convert(adsResponse)
tcContext.tc.adsResponse = response
logger.info("ads response: ${json.encodeToString(response)}")
return response
}
fun adSync(request: AdsSyncRequest) {
// nothing to do.
}
companion object : KLogging()
}

10
src/main/kotlin/tv/anypoint/proxy/service/PushServerInterface.kt

@ -0,0 +1,10 @@
package tv.anypoint.proxy.service
import tv.anypoint.proxy.model.agent.Cue
import tv.anypoint.proxy.model.push.PushCommandType
interface PushServerInterface {
fun sendCommand(commandType: PushCommandType): Boolean
fun sendCue(cue: Cue): Boolean
}

21
src/main/kotlin/tv/anypoint/proxy/service/PushServerService.kt

@ -0,0 +1,21 @@
package tv.anypoint.proxy.service
import org.springframework.stereotype.Service
import tv.anypoint.ApplicationProperties
import tv.anypoint.proxy.model.agent.Cue
import tv.anypoint.proxy.model.push.PushCommandType
@Service
class PushServerService(
private val applicationProperties: ApplicationProperties
) : PushServerInterface {
override fun sendCommand(commandType: PushCommandType): Boolean {
applicationProperties.pushServerPort
TODO("Not Implemented")
}
override fun sendCue(cue: Cue): Boolean {
applicationProperties.pushServerPort
TODO("Not Implemented")
}
}

54
src/main/kotlin/tv/anypoint/proxy/service/StbService.kt

@ -0,0 +1,54 @@
package tv.anypoint.proxy.service
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.stereotype.Service
import se.vidstige.jadb.JadbDevice
import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.model.TcContext
import tv.anypoint.proxy.model.push.PushCommandType
import tv.anypoint.proxy.shell.ShellController
import tv.anypoint.proxy.util.changeAuthEndpoint
import tv.anypoint.proxy.util.reboot
import tv.anypoint.proxy.util.sendNumberInput
@Service
class StbService(
private val serverIp: String,
private val jadbDevice: JadbDevice,
private val tcContext: TcContext,
private val serverProperties: ServerProperties,
private val logcatService: LogcatService,
private val pushServerService: PushServerService
) {
fun start(
restartStb: Boolean,
serverIp: String = this.serverIp,
serverPort: Int = serverProperties.port
) {
logcatService.startDump()
tcContext.tc = Tc().apply {
this.number = "MANUAL"
this.reboot = restartStb
}
changeAuthEndpoint(serverIp = serverIp, serverPort = serverPort)
if (restartStb) {
jadbDevice.reboot()
} else {
pushServerService.sendCommand(PushCommandType.UPDATE_SYSTEM_INFO)
}
}
private fun changeAuthEndpoint(
serverIp: String,
serverPort: Int
) {
jadbDevice.changeAuthEndpoint(serverIp = serverIp, serverPort = serverPort)
}
fun changeChannelNumber(channelNumber: String) {
channelNumber.forEach { c ->
jadbDevice.sendNumberInput(c)
}
ShellController.logger.info("changed ch number: $channelNumber")
}
}

40
src/main/kotlin/tv/anypoint/proxy/shell/ShellController.kt

@ -0,0 +1,40 @@
package tv.anypoint.proxy.shell
import mu.KLogging
import org.springframework.shell.standard.ShellComponent
import org.springframework.shell.standard.ShellMethod
import tv.anypoint.proxy.service.StbService
@ShellComponent
class ShellController(
private val stbService: StbService
) {
/**
* 채널 변경
*/
@ShellMethod
fun ch(channelNumber: String) {
stbService.changeChannelNumber(channelNumber)
}
/**
* STB 접속
* ip, port 지정하지 않을 경우 설정된 STB 접속
*/
@ShellMethod
fun start(
restart: Boolean = false,
ip: String? = null,
port: Int? = null
) {
when {
ip == null && port == null -> stbService.start(restartStb = restart)
ip != null && port != null -> stbService.start(restartStb = restart, serverIp = ip, serverPort = port)
ip != null && port == null -> stbService.start(restartStb = restart, serverIp = ip)
ip == null && port != null -> stbService.start(restartStb = restart, serverPort = port)
}
}
companion object : KLogging()
}

22
src/main/kotlin/tv/anypoint/proxy/util/JadbDeviceSerialUtil.kt

@ -0,0 +1,22 @@
package tv.anypoint.proxy.util
import java.net.Inet4Address
import java.net.InetSocketAddress
object JadbDeviceSerialUtil {
private val serialRegex = Regex("(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d{1,5})")
fun addressToId(hostAddress: String, port: Int) = "$hostAddress:$port"
fun addressToId(socketAddress: InetSocketAddress) = addressToId(socketAddress.address.hostAddress, socketAddress.port)
fun serialToSocketAddress(serial: String) : InetSocketAddress? {
val matchResult = serialRegex.matchEntire(serial) ?: return null
val address = Inet4Address.getByName(matchResult.groupValues[1])
val port = Integer.parseInt(matchResult.groupValues[2])
return InetSocketAddress(address, port)
}
fun serialToAddress(serial: String) = serialToSocketAddress(serial)?.address
}

4
src/main/kotlin/tv/anypoint/proxy/service/NetworkUtil.kt → src/main/kotlin/tv/anypoint/proxy/util/NetworkUtil.kt

@ -1,4 +1,4 @@
package tv.anypoint.proxy.service
package tv.anypoint.proxy.util
import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.*
@ -10,7 +10,7 @@ import java.util.*
object NetworkUtil {
fun getPrivateIpV4(setTopBoxAddress: InetAddress): String = getPrivateIpV4List()
.chooseSimilarIp(setTopBoxAddress)?.hostAddress
?: throw RuntimeException()
?: throw RuntimeException("셋톱과 공유하는 private IP가 없습니다")
/**
* Gives the private IP of this device

88
src/main/kotlin/tv/anypoint/proxy/util/StbUtil.kt

@ -0,0 +1,88 @@
package tv.anypoint.proxy.util
import org.apache.commons.io.IOUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import tv.anypoint.proxy.model.adb.ChangeCommand
import tv.anypoint.proxy.model.adb.ExtraKey
import tv.anypoint.proxy.model.adb.IntentAction
import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets
private val logger: Logger = LoggerFactory.getLogger("tv.anypoint.proxy.util.StbUtilKt")
fun getDevice(stbIp: String, stbPort: Int): JadbDevice {
try {
val socketAddress = InetSocketAddress(stbIp, stbPort)
val jadbc = JadbConnection()
try {
jadbc.connectToTcpDevice(socketAddress)
} catch (e: Exception) {
logger.error("Can't connect adb to $socketAddress, Reason -> ${e.message}")
}
return jadbc.devices.firstOrNull { it.serial in JadbDeviceSerialUtil.addressToId(socketAddress) }
?: throw RuntimeException("adb server not running!")
} catch (e: Exception) {
throw RuntimeException("adb server not running!", e)
}
}
/**
* Sends broadcast command to the device to change the auth endpoint to the server.
*/
fun JadbDevice.changeAuthEndpoint(
serverIp: String,
serverPort: Int
) {
val proxyEndpoint = "http://$serverIp:$serverPort"
executeShellDebug(
a = tv.anypoint.proxy.model.adb.IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(
tv.anypoint.proxy.model.adb.ExtraKey.CHANGE_COMMAND to tv.anypoint.proxy.model.adb.ChangeCommand.CHANGE_API_ENDPOINT.name,
tv.anypoint.proxy.model.adb.ExtraKey.API_ENDPOINT to proxyEndpoint
)
)
logger.info("changed auth endpoint to proxy endpoint $proxyEndpoint")
}
fun JadbDevice.executeShellDebug(a: tv.anypoint.proxy.model.adb.IntentAction, es: Map<tv.anypoint.proxy.model.adb.ExtraKey, String>) {
val shellContentBuilder = StringBuilder()
shellContentBuilder.append("am broadcast ")
shellContentBuilder.append("-a ${a.string} ")
es.forEach { (k, v) ->
shellContentBuilder.append("--es ${k.string} $v ")
}
this.executeShellDebug(shellContentBuilder.toString())
}
/**
* Sends number as an input to the device, just like
* pressing a number button in the remote controller.
*/
fun JadbDevice.sendNumberInput(numberButton: Char) {
if (numberButton < '0' || numberButton > '9') return
val shellContent = "input keyevent KEYCODE_$numberButton"
this.executeShellDebug(shellContent)
}
/**
* Sends shell to the device
*/
fun JadbDevice.executeShellDebug(content: String) {
logger.info("[[ cmd --> {} ]] {}", this.serial, content)
val inputStream = this.executeShell(content)
val commandResult = IOUtils.toString(inputStream, StandardCharsets.UTF_8)
for (line in commandResult.split('\n')) {
logger.info("[[ cmd <-- {} ]] {}", this.serial, line.removeNewLines())
}
}
/**
* Sends reboot command to the device.
*/
fun JadbDevice.reboot() {
executeShellDebug("reboot")
}

3
src/main/kotlin/tv/anypoint/proxy/util/StringUtil.kt

@ -0,0 +1,3 @@
package tv.anypoint.proxy.util
fun String.removeNewLines() = this.replace("\n", "")

55
src/main/kotlin/tv/anypoint/proxy/web/DeviceController.kt

@ -1,27 +1,27 @@
package tv.anypoint.proxy.web
import jakarta.servlet.http.HttpServletRequest
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import mu.KLogging
import org.springframework.web.bind.annotation.*
import tv.anypoint.domain.agent.AuthRequest
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.domain.agent.StateChangeLog
import tv.anypoint.domain.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsSyncRequest
import tv.anypoint.proxy.model.agent.AuthRequest
import tv.anypoint.proxy.model.agent.AuthResponse
import tv.anypoint.proxy.model.agent.StateChangeLog
import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.proxy.model.agent.ad.AdsSyncRequest
import tv.anypoint.proxy.service.ProxyDeviceService
@RestController
@RequestMapping("/v3/device")
class DeviceController {
class DeviceController(
private val json: Json,
private val proxyDeviceService: ProxyDeviceService
) {
@PostMapping("/auth")
fun authorize(request: HttpServletRequest, @RequestBody body: AuthRequest): AuthResponse {
logger.info("POST /v3/device/auth -d $body")
// request.getIpAddress()
val authResponse: AuthResponse = TODO("authRequestHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request), body)")
authResponse.adConfig.maxDownloadBandwidth = 1000 * 1024 * 1024
authResponse.adConfig.maxLazyDownloadBandwidth = 1000 * 1024 * 1024
return authResponse
fun authorize(@RequestBody request: AuthRequest): AuthResponse {
logger.info("POST /v3/device/auth -d ${json.encodeToString(request)}")
return proxyDeviceService.auth(request)
}
@GetMapping("/ads")
@ -31,31 +31,34 @@ class DeviceController {
@RequestParam("usedStorage", required = false, defaultValue = "0") usedStorage: Long?
): AdsResponse {
logger.info("GET /v3/device/ads?deviceId=$deviceId&freeStorage=$freeStorage&usedStorage=$usedStorage")
// return adsRequestHandlerService.handleRequest(deviceId, freeStorage, usedStorage)
return AdsResponse()
return proxyDeviceService.ads(
deviceId = deviceId,
freeStorage = freeStorage,
usedStorage = usedStorage
)
}
@PostMapping("/sync/ads")
fun adSync(request: HttpServletRequest, @RequestBody body: AdsSyncRequest) {
logger.info("POST /v3/device/sync/ads -d $body")
// deviceAdSyncHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request))
fun adSync(@RequestBody request: AdsSyncRequest) {
logger.info("POST /v3/device/sync/ads -d $request")
proxyDeviceService.adSync(request)
}
@PostMapping("/state-logs")
fun stateLogs(@RequestBody body: List<StateChangeLog>) {
logger.info("POST /v3/device/state-logs -d $body")
fun stateLogs(@RequestBody request: List<StateChangeLog>) {
logger.info("POST /v3/device/state-logs -d $request")
}
@PostMapping("/event")
@ResponseBody
fun deviceEvent(request: HttpServletRequest, @RequestBody body: String) {
logger.info("POST /v3/device/event -d $body")
fun deviceEvent(@RequestBody request: String) {
logger.info("POST /v3/device/event -d $request")
}
@PostMapping("/impression-logs")
@ResponseBody
fun impressionLogs(request: HttpServletRequest, @RequestBody body: String) {
logger.info("POST /v3/device/ssion-logs -d $body")
fun impressionLogs(@RequestBody request: String) {
logger.info("POST /v3/device/ssion-logs -d $request")
}
companion object : KLogging()

18
src/main/kotlin/tv/anypoint/proxy/web/TestController.kt

@ -0,0 +1,18 @@
package tv.anypoint.proxy.web
import mu.KLogging
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RequestMapping("/test")
@RestController
class TestController {
@GetMapping
fun testGet(): Boolean {
logger.info("GET /test")
return true
}
companion object : KLogging()
}

12
src/main/kotlin/tv/anypoint/tc/Base1.kt

@ -2,8 +2,6 @@ package tv.anypoint.tc
import mu.KLogging
import org.springframework.stereotype.Component
import tv.anypoint.domain.adb.ExtraKey
import tv.anypoint.domain.adb.IntentAction
import tv.anypoint.dsl.adb
import tv.anypoint.dsl.expected
import tv.anypoint.dsl.http
@ -23,18 +21,18 @@ class Base1 : TestCase() {
override fun test() {
adb(
a = IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "true")
a = tv.anypoint.proxy.model.adb.IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(tv.anypoint.proxy.model.adb.ExtraKey.TEST_DEVICE to "true")
)
expected("changed to test device")
tc.log.expected("changed to test device")
// AD Agent, AD SDK 통신규격상으로는 a 필수, es 는 선택사항
// https://dev-docs.anypoint.tv/books/ff-settop-ad-agent/page/ad-agent-ad-sdk
// 예외 사항이 존재할 지?
adb(
a = IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "false")
a = tv.anypoint.proxy.model.adb.IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(tv.anypoint.proxy.model.adb.ExtraKey.TEST_DEVICE to "false")
)
}

28
src/main/kotlin/tv/anypoint/tc/TestCaseStarter.kt

@ -1,19 +1,24 @@
package tv.anypoint.tc
import mu.KLogging
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service
import tv.anypoint.dsl.model.TcFailEvent
import tv.anypoint.dsl.service.TestCase
import tv.anypoint.proxy.service.StbService
@Service
class TestCaseStarter(
private val testCases: List<TestCase>
private val testCases: List<TestCase>,
private val stbService: StbService,
) {
fun testAll() {
// TODO: main.log 덤프 시작
startDumpLog()
testCases.forEach {
// [reboot] -> auth -> ads -> vast -> asset convert
stbService.start(it.tc.reboot)
it.executeTest()
}
println("| tc | result | startedAt | finishedAt | log file | recording file |")
@ -27,22 +32,17 @@ class TestCaseStarter(
if (it.tc.result!!) "PASS" else "FAIL",
it.tc.startedAt,
it.tc.finishedAt,
it.tc.logInfo.absoluteFilePath,
it.tc.recordingInfo?.absoluteFilePath ?: "-"
it.tc.log.logcatFilePath,
it.tc.recording?.absoluteFilePath ?: "-"
)
)
}
stopDumpLog()
}
fun startDumpLog() {
// TODO:
}
@EventListener
fun failed(event: TcFailEvent) {
fun stopDumpLog() {
// TODO
}
companion object : KLogging()
}
}

6
src/main/resources/application.yaml

@ -7,11 +7,13 @@ logging:
level:
root: INFO
tv.anypoint: DEBUG
server:
port: 8080
anypoint.android-qa:
stb:
ip: 192.168.0.1
ip: 192.168.15.139
port: 5555
file-root: /home/bean/dev/qa
file-root: /home/bean/dev/qaz
---

15
src/test/kotlin/Test.kt

@ -0,0 +1,15 @@
import java.io.BufferedReader
import java.io.FileReader
import java.io.IOException
import java.io.LineNumberReader
class Test {
@Throws(IOException::class)
fun test() {
val lnr = LineNumberReader(BufferedReader(FileReader("all.log")))
var line: String?
while ((lnr.readLine().also { line = it }) != null) {
println(line)
}
}
}

4
src/test/kotlin/tv/anypoint/androidqa/AuthResponseTest.kt

@ -6,7 +6,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.springframework.core.io.ClassPathResource
import tv.anypoint.domain.agent.AuthResponse
import tv.anypoint.proxy.model.agent.AuthResponse
@OptIn(ExperimentalSerializationApi::class)
class AuthResponseTest : BehaviorSpec() {
@ -15,7 +15,7 @@ class AuthResponseTest : BehaviorSpec() {
Given("auth-response.json file") {
val file = ClassPathResource("auth-response.json").file
When("deserialize") {
val actual = Json.decodeFromStream<AuthResponse>(file.inputStream())
val actual = Json.decodeFromStream<tv.anypoint.proxy.model.agent.AuthResponse>(file.inputStream())
Then("read values") {
actual.deviceId shouldBe 36986237L
}

51
src/test/kotlin/tv/anypoint/dsl/DslKtTest.kt

@ -0,0 +1,51 @@
package tv.anypoint.dsl
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.longs.shouldBeLessThanOrEqual
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import mu.KLogging
import tv.anypoint.dsl.model.Log
import java.io.File
import java.time.Duration
import java.time.LocalDateTime
class DslKtTest : FunSpec() {
init {
test("Log.expected()") {
// given
val file = File("./test.log")
val writer = file.writer()
val startedAt = LocalDateTime.now()
val test5 = Log("./test.log")
val test7 = Log("./test.log")
object : Thread() {
override fun run() {
(1..20).forEach {
logger.info("print test $it")
writer.write("test $it${System.lineSeparator()}")
writer.flush()
sleep(500)
}
}
}
.start()
// when
test5.expected("test 5")
test7.expected("test 7")
// then
test5.cursor shouldBe 4
test7.cursor shouldBe 6
Duration.between(startedAt, LocalDateTime.now()).seconds shouldBeLessThanOrEqual 10_000
}
}
companion object : KLogging()
}

44
src/test/kotlin/tv/anypoint/proxy/adapter/AuthAdapterTest.kt

@ -0,0 +1,44 @@
package tv.anypoint.proxy.adapter
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import mu.KLogging
import org.springframework.boot.test.context.SpringBootTest
import tv.anypoint.proxy.model.agent.AuthRequest
@SpringBootTest
class AuthAdapterTest(
private val authAdapter: AuthAdapter
) : BehaviorSpec() {
init {
Given("request") {
val request = AuthRequest(
fingerPrint = "",
soId = 1,
uuid = "43dd41a2-111b-48c8-b840-629978714f0b",
freeStorage = 2519707648,
usedStorage = 294611792,
cachedStorage = 203884308,
modelName = "BID-AI100",
firmwareBuildDate = "2024.04.02",
firmwareVer = "16.540.22",
fullFirmwareVer = "16.540.22-0000",
appVersion = "3.9.4-RC59_20230712",
platformAdId = "63c33b8c-f5d5-47b3-9e7d-f06342fb65fe",
sdkVersion = "2.0.1-RC12"
)
When("call auth API") {
val actual = authAdapter.auth(request)
.execute().body()
logger.info("actual: $actual")
Then("actual is not null") {
1 shouldBe 1
}
}
}
}
companion object : KLogging()
}

15
src/test/kotlin/tv/anypoint/proxy/service/LogcatServiceTest.kt

@ -0,0 +1,15 @@
package tv.anypoint.proxy.service
import io.kotest.core.spec.style.FunSpec
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class LogcatServiceTest(
logcatService: LogcatService
) : FunSpec({
test("start") {
logcatService.startDump()
Thread.sleep(5_000)
logcatService.stopDump()
}
})
Loading…
Cancel
Save