Browse Source

RD-213 implementation http proxy

RD-214 save all.log
feature/RD-212
bean 2 years ago
parent
commit
0a85a0b0b5
  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 ###
.kotlin .kotlin
/android-qa.log

12
README.md

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

1
build.gradle.kts

@ -22,7 +22,6 @@ repositories {
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test") 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" } kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version = "2.0.11" }
thymeleaf = { module = "org.thymeleaf:thymeleaf", version = "3.1.2.RELEASE" } thymeleaf = { module = "org.thymeleaf:thymeleaf", version = "3.1.2.RELEASE" }
thymeleaf-spring5 = { module = "org.thymeleaf:thymeleaf-spring5", 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-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version = "5.8.1" }
kotest-property = { module = "io.kotest:kotest-property", version = "5.8.1" } kotest-property = { module = "io.kotest:kotest-property", version = "5.8.1" }
@ -22,7 +25,10 @@ deps = [
"kotlin-logging", "kotlin-logging",
"thymeleaf", "thymeleaf",
"thymeleaf-spring5", "thymeleaf-spring5",
"kotlinx-coroutines" "kotlinx-coroutines",
"spring-shell",
"retrofit",
"retrofix-kotlinx-serialization"
] ]
test-deps = [ test-deps = [
"kotest-runner", "kotest-runner",

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

@ -19,11 +19,17 @@ class ApplicationProperties {
class Stb { class Stb {
lateinit var ip: String lateinit var ip: String
var port: Int = 5555
/**
* 보통 8888
* bko-uh600 / bhx-uh600
* port : 5815
*/
var port: Int = 8888
} }
class Endpoints { class Endpoints {
lateinit var auth: String lateinit var auth: String
lateinit var assign: 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 package tv.anypoint.dsl
import tv.anypoint.domain.adb.ExtraKey import kotlinx.coroutines.delay
import tv.anypoint.domain.adb.IntentAction 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.handler.HttpHandler
import tv.anypoint.dsl.model.Log
import tv.anypoint.dsl.model.Tc import tv.anypoint.dsl.model.Tc
import tv.anypoint.dsl.service.TestCase 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.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
@ -17,31 +23,37 @@ inline fun <reified T> http(
block: HttpHandler<T>.() -> Unit block: HttpHandler<T>.() -> Unit
) = HttpHandler<T>().also { block(it) } ) = HttpHandler<T>().also { block(it) }
data class Adb( fun adb(
val a: IntentAction,
val es: Map<ExtraKey, String>
)
inline fun adb(
a: IntentAction, a: IntentAction,
es: Map<ExtraKey, String> es: Map<ExtraKey, String>
) { ) {
val adb = Adb(a, es)
val aStr = "-a $a" val aStr = "-a $a"
val esStr: String = es.entries.joinToString(" ") { "--es ${it.key.string} ${it.value}" } val esStr: String = es.entries.joinToString(" ") { "--es ${it.key.string} ${it.value}" }
println("[ADB] adb shell am broadcast $aStr $esStr") println("[ADB] adb shell am broadcast $aStr $esStr")
} }
fun TestCase.expected( fun Log.expected(
expectedLog: String, expectedLog: String,
timeout: Long = 10_000L timeout: Long = 10_000L
) { ) {
val log = this
val startedAt = LocalDateTime.now() val startedAt = LocalDateTime.now()
val reader = LineNumberReader(BufferedReader(FileReader(log.logcatFilePath)))
while (true) { while (true) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
if (ChronoUnit.MILLIS.between(startedAt, now) > timeout) { 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.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.encodeToJsonElement
import tv.anypoint.domain.agent.AuthResponse import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsResponse import tv.anypoint.proxy.model.agent.ad.AssetConvertResponse
import tv.anypoint.domain.agent.ad.AssetConvertResponse import tv.anypoint.proxy.model.agent.ad.VastResponse
import tv.anypoint.domain.agent.ad.VastResponse
class HttpValidationException( class HttpValidationException(
type: HttpValidationExceptionType, type: HttpValidationExceptionType,
@ -27,11 +26,11 @@ inline fun <reified T> httpValidationError(
response: T response: T
): HttpValidationException = HttpValidationException( ): HttpValidationException = HttpValidationException(
type = when (T::class) { type = when (T::class) {
AuthResponse::class -> HttpValidationExceptionType.AUTH tv.anypoint.proxy.model.agent.AuthResponse::class -> HttpValidationExceptionType.AUTH
AdsResponse::class -> HttpValidationExceptionType.ADS AdsResponse::class -> HttpValidationExceptionType.ADS
VastResponse::class -> HttpValidationExceptionType.VAST VastResponse::class -> HttpValidationExceptionType.VAST
AssetConvertResponse::class -> HttpValidationExceptionType.ASSET_CONVERT AssetConvertResponse::class -> HttpValidationExceptionType.ASSET_CONVERT
else -> HttpValidationExceptionType.NONE else -> HttpValidationExceptionType.NONE
}, },
response = Json.encodeToJsonElement(response) 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 import java.time.LocalDateTime
@Serializable @Serializable
data class RecordingInfo( data class Recording(
val absoluteFilePath: String val absoluteFilePath: String
) { ) {
@Contextual @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 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.handler.HttpHandler
import tv.anypoint.dsl.http 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 import java.time.LocalDateTime
class Tc { class Tc {
@ -26,6 +26,6 @@ class Tc {
lateinit var finishedAt: LocalDateTime lateinit var finishedAt: LocalDateTime
var result: Boolean? = null var result: Boolean? = null
lateinit var logInfo: LogInfo lateinit var log: Log
var recordingInfo: RecordingInfo? = null 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 { class JsonConfiguration {
@Bean @Bean
fun json(): Json = Json { fun json(): Json = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule { serializersModule = SerializersModule {
contextual(LocalDate::class, LocalDateSerializer()) contextual(LocalDate::class, LocalDateSerializer())
contextual(LocalDateTime::class, LocalDateTimeSerializer()) 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 mu.KLogging
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import tv.anypoint.ApplicationProperties import tv.anypoint.ApplicationProperties
import tv.anypoint.domain.agent.AuthResponse import tv.anypoint.dsl.model.Log
import tv.anypoint.domain.agent.ad.AdsResponse import tv.anypoint.dsl.model.Recording
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.Tc import tv.anypoint.dsl.model.Tc
import java.time.LocalDateTime import java.time.LocalDateTime
@ -25,14 +21,13 @@ abstract class TestCase {
@Autowired @Autowired
private lateinit var applicationProperties: ApplicationProperties private lateinit var applicationProperties: ApplicationProperties
@Autowired
private lateinit var adbTransmitter: AdbTransmitter
@Autowired @Autowired
private lateinit var captureBoardRecorder: CaptureBoardRecorder private lateinit var captureBoardRecorder: CaptureBoardRecorder
fun executeTest() { fun executeTest() {
startToDumpLog() startToDumpLog()
startRecording(tc)
tc = init() tc = init()
logger.info("[${tc.number}] starting...") logger.info("[${tc.number}] starting...")
@ -45,12 +40,6 @@ abstract class TestCase {
// TODO // TODO
} }
auth()
ads()
vast()
assetConvert()
startRecording(tc)
test() test()
stopRecording(tc) stopRecording(tc)
@ -67,7 +56,7 @@ abstract class TestCase {
} }
private fun startToDumpLog() { 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 파일 새로 만들어서 적재 시작 // TODO 이 함수 호출된 후 main.log 에서 쌓이는 로그를 ${tc.number}.log 파일 새로 만들어서 적재 시작
} }
@ -75,57 +64,22 @@ abstract class TestCase {
// TODO main.log >> ${tc.number}.log 적재 종료 // TODO main.log >> ${tc.number}.log 적재 종료
} }
private fun auth() { @EventListener
// TODO: auth fun failed() {
val actualResponse = AuthResponse(0)
val response = tc.auth.convert(actualResponse)
if (!tc.auth.validate(response)) {
throw httpValidationError(response)
}
tc.authResponse = tc.auth.convert(response)
}
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) { private fun startRecording(tc: Tc) {
tc.recordingInfo = RecordingInfo("${applicationProperties.fileRoot}/${tc.number}.mp4") tc.recording = Recording("${applicationProperties.fileRoot}/${tc.number}.mp4")
logger.debug("start to record. info: {}", tc.recordingInfo) logger.debug("start to record. info: {}", tc.recording)
// TODO // TODO
} }
private fun stopRecording(tc: Tc) { private fun stopRecording(tc: Tc) {
tc.recordingInfo!!.finish(1000L) tc.recording!!.finish(1000L)
// TODO // TODO
logger.debug("finished to record. info: {}", tc.recordingInfo) logger.debug("finished to record. info: {}", tc.recording)
} }
companion object : KLogging() 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 { enum class ChangeCommand {
CHANGE_API_ENDPOINT, 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) { enum class ExtraKey(val string: String) {
EXTRA_JSON("EXTRA_JSON"), EXTRA_JSON("EXTRA_JSON"),
CHANGE_COMMAND("change.command"), CHANGE_COMMAND("change.command"),
API_ENDPOINT("api.endpoint"), API_ENDPOINT("api.endpoint"),
TEST_DEVICE("test.device") 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) { enum class IntentAction(val string: String) {
CHANGE_TEST_PROPERTY("tv.anypoint.agent.app.CHANGE_TEST_PROPERTY") 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( data class AuthRequest(
var fingerPrint: String = "", var fingerPrint: String = "",
val soId: Int = 0, 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 import java.util.concurrent.TimeUnit
@Serializable
data class AuthResponse( data class AuthResponse(
var deviceId: Long = 0, var deviceId: Long = 0,
var deviceTypeId: Int = 0, var deviceTypeId: Int = 0,
@ -25,10 +27,11 @@ data class AuthResponse(
fun authorization() = "Anypoint ${accessToken ?: ""}" fun authorization() = "Anypoint ${accessToken ?: ""}"
fun monitoringInterval() = monitoringInterval.let { 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( data class Endpoints(
var auth: String = "", var auth: String = "",
var requestAds: String = "", var requestAds: String = "",
@ -54,6 +57,7 @@ enum class VideoMediaType {
} }
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함 // WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
@Serializable
data class AdConfig( data class AdConfig(
var maxDownloadBandwidth: Long = 150 * 1024, var maxDownloadBandwidth: Long = 150 * 1024,
var maxLazyDownloadBandwidth: Long = 1024 * 1024, var maxLazyDownloadBandwidth: Long = 1024 * 1024,
@ -68,8 +72,8 @@ data class AdConfig(
var remnantTimeThreshold: Int = 2_000, var remnantTimeThreshold: Int = 2_000,
var maxEndAdPlaytime: Int = 15_000, var maxEndAdPlaytime: Int = 15_000,
var transitionDelay: Int = 0, var transitionDelay: Int = 0,
var videoPlayMode: VideoPlayMode = VideoPlayMode.PARALLEL, var videoPlayMode: tv.anypoint.proxy.model.agent.VideoPlayMode = tv.anypoint.proxy.model.agent.VideoPlayMode.PARALLEL,
var videoMediaType: VideoMediaType = VideoMediaType.CONCATENATING, var videoMediaType: tv.anypoint.proxy.model.agent.VideoMediaType = tv.anypoint.proxy.model.agent.VideoMediaType.CONCATENATING,
var startDelay: Int = 0, var startDelay: Int = 0,
var stopDelay: Int = 0, var stopDelay: Int = 0,
var startRenderDelay: Int = 0, var startRenderDelay: Int = 0,
@ -102,6 +106,7 @@ data class AdConfig(
) )
// WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함 // WARNING: local db(data class Device)의 Embedded 엔티티이므로 프로퍼티 변경 시, db 버전 올려야 함
@Serializable
data class KidWatermark( data class KidWatermark(
var imageUrl: String = "", var imageUrl: String = "",
var crc: Long = 0, var crc: Long = 0,
@ -109,4 +114,4 @@ data class KidWatermark(
var top: Int = 0, var top: Int = 0,
var width: Int = 0, var width: Int = 0,
var height: 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( data class Cue(
var id: Long = 0, 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 { enum class CueOwner {
PP, 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 { enum class PlayType {
DNP, DNP,
LAZY_DNP, LAZY_DNP,
STREAMING 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( data class PlayerStateCheckParam(
val playerStateCheckDuration: Int = 1500, val playerStateCheckDuration: Int = 1500,
val maxPlayerInvalidateInterval: Int = 300 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( data class ProgramProviderChannel(
val id: Int, val id: Int,
val delay: Int = 0, 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( data class StateChangeLog(
val soId: Int = 0, 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( data class Ad(
val id: Long, val id: Long,
val adRequest: AdRequest? = null, val adRequest: AdRequest? = null,
@ -25,4 +28,4 @@ enum class CampaignType {
HOUSE, HOUSE,
BUFFER, BUFFER,
ENDING 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 { enum class EncodingType {
@ -14,6 +15,7 @@ enum class ExtAdProtocol {
GOOGLE_AD GOOGLE_AD
} }
@Serializable
data class AdRequest( data class AdRequest(
var url: String, var url: String,
val updateInterval: Int = 0, 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( data class AdsResponse(
val status: AdListResponseStatus = AdListResponseStatus.OK, val status: AdListResponseStatus = AdListResponseStatus.OK,
val ads: List<Ad> = listOf(), 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( data class AdsSyncRequest(
val deviceId: Long, 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( data class Asset(
var assetId: Long = 0L, var assetId: Long = 0L,
var crc: String = "0", 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( data class Click(
val targetType: String, val targetType: String,
val targetValue: 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( data class ProgramPlacement(
val id: Int, val id: Int,
@ -18,4 +18,4 @@ data class PlacementUniqueKey(
enum class MediaType { enum class MediaType {
SO, PP 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( data class TargetAd(
val hours: String? = null, val hours: String? = null,
val days: 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 jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -10,7 +10,7 @@ import java.util.*
object NetworkUtil { object NetworkUtil {
fun getPrivateIpV4(setTopBoxAddress: InetAddress): String = getPrivateIpV4List() fun getPrivateIpV4(setTopBoxAddress: InetAddress): String = getPrivateIpV4List()
.chooseSimilarIp(setTopBoxAddress)?.hostAddress .chooseSimilarIp(setTopBoxAddress)?.hostAddress
?: throw RuntimeException() ?: throw RuntimeException("셋톱과 공유하는 private IP가 없습니다")
/** /**
* Gives the private IP of this device * 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 package tv.anypoint.proxy.web
import jakarta.servlet.http.HttpServletRequest import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import mu.KLogging import mu.KLogging
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import tv.anypoint.domain.agent.AuthRequest import tv.anypoint.proxy.model.agent.AuthRequest
import tv.anypoint.domain.agent.AuthResponse import tv.anypoint.proxy.model.agent.AuthResponse
import tv.anypoint.domain.agent.StateChangeLog import tv.anypoint.proxy.model.agent.StateChangeLog
import tv.anypoint.domain.agent.ad.AdsResponse import tv.anypoint.proxy.model.agent.ad.AdsResponse
import tv.anypoint.domain.agent.ad.AdsSyncRequest import tv.anypoint.proxy.model.agent.ad.AdsSyncRequest
import tv.anypoint.proxy.service.ProxyDeviceService
@RestController @RestController
@RequestMapping("/v3/device") @RequestMapping("/v3/device")
class DeviceController { class DeviceController(
private val json: Json,
private val proxyDeviceService: ProxyDeviceService
) {
@PostMapping("/auth") @PostMapping("/auth")
fun authorize(request: HttpServletRequest, @RequestBody body: AuthRequest): AuthResponse { fun authorize(@RequestBody request: AuthRequest): AuthResponse {
logger.info("POST /v3/device/auth -d $body") logger.info("POST /v3/device/auth -d ${json.encodeToString(request)}")
return proxyDeviceService.auth(request)
// 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
} }
@GetMapping("/ads") @GetMapping("/ads")
@ -31,31 +31,34 @@ class DeviceController {
@RequestParam("usedStorage", required = false, defaultValue = "0") usedStorage: Long? @RequestParam("usedStorage", required = false, defaultValue = "0") usedStorage: Long?
): AdsResponse { ): AdsResponse {
logger.info("GET /v3/device/ads?deviceId=$deviceId&freeStorage=$freeStorage&usedStorage=$usedStorage") logger.info("GET /v3/device/ads?deviceId=$deviceId&freeStorage=$freeStorage&usedStorage=$usedStorage")
// return adsRequestHandlerService.handleRequest(deviceId, freeStorage, usedStorage) return proxyDeviceService.ads(
return AdsResponse() deviceId = deviceId,
freeStorage = freeStorage,
usedStorage = usedStorage
)
} }
@PostMapping("/sync/ads") @PostMapping("/sync/ads")
fun adSync(request: HttpServletRequest, @RequestBody body: AdsSyncRequest) { fun adSync(@RequestBody request: AdsSyncRequest) {
logger.info("POST /v3/device/sync/ads -d $body") logger.info("POST /v3/device/sync/ads -d $request")
// deviceAdSyncHandlerService.handleRequest(HttpRequestUtil.getIpAddress(request)) proxyDeviceService.adSync(request)
} }
@PostMapping("/state-logs") @PostMapping("/state-logs")
fun stateLogs(@RequestBody body: List<StateChangeLog>) { fun stateLogs(@RequestBody request: List<StateChangeLog>) {
logger.info("POST /v3/device/state-logs -d $body") logger.info("POST /v3/device/state-logs -d $request")
} }
@PostMapping("/event") @PostMapping("/event")
@ResponseBody @ResponseBody
fun deviceEvent(request: HttpServletRequest, @RequestBody body: String) { fun deviceEvent(@RequestBody request: String) {
logger.info("POST /v3/device/event -d $body") logger.info("POST /v3/device/event -d $request")
} }
@PostMapping("/impression-logs") @PostMapping("/impression-logs")
@ResponseBody @ResponseBody
fun impressionLogs(request: HttpServletRequest, @RequestBody body: String) { fun impressionLogs(@RequestBody request: String) {
logger.info("POST /v3/device/ssion-logs -d $body") logger.info("POST /v3/device/ssion-logs -d $request")
} }
companion object : KLogging() 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 mu.KLogging
import org.springframework.stereotype.Component 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.adb
import tv.anypoint.dsl.expected import tv.anypoint.dsl.expected
import tv.anypoint.dsl.http import tv.anypoint.dsl.http
@ -23,18 +21,18 @@ class Base1 : TestCase() {
override fun test() { override fun test() {
adb( adb(
a = IntentAction.CHANGE_TEST_PROPERTY, a = tv.anypoint.proxy.model.adb.IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "true") 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 는 선택사항 // AD Agent, AD SDK 통신규격상으로는 a 필수, es 는 선택사항
// https://dev-docs.anypoint.tv/books/ff-settop-ad-agent/page/ad-agent-ad-sdk // https://dev-docs.anypoint.tv/books/ff-settop-ad-agent/page/ad-agent-ad-sdk
// 예외 사항이 존재할 지? // 예외 사항이 존재할 지?
adb( adb(
a = IntentAction.CHANGE_TEST_PROPERTY, a = tv.anypoint.proxy.model.adb.IntentAction.CHANGE_TEST_PROPERTY,
es = mapOf(ExtraKey.TEST_DEVICE to "false") 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 package tv.anypoint.tc
import mu.KLogging import mu.KLogging
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import tv.anypoint.dsl.model.TcFailEvent
import tv.anypoint.dsl.service.TestCase import tv.anypoint.dsl.service.TestCase
import tv.anypoint.proxy.service.StbService
@Service @Service
class TestCaseStarter( class TestCaseStarter(
private val testCases: List<TestCase> private val testCases: List<TestCase>,
private val stbService: StbService,
) { ) {
fun testAll() { fun testAll() {
// TODO: main.log 덤프 시작
startDumpLog()
testCases.forEach { testCases.forEach {
// [reboot] -> auth -> ads -> vast -> asset convert
stbService.start(it.tc.reboot)
it.executeTest() it.executeTest()
} }
println("| tc | result | startedAt | finishedAt | log file | recording file |") println("| tc | result | startedAt | finishedAt | log file | recording file |")
@ -27,22 +32,17 @@ class TestCaseStarter(
if (it.tc.result!!) "PASS" else "FAIL", if (it.tc.result!!) "PASS" else "FAIL",
it.tc.startedAt, it.tc.startedAt,
it.tc.finishedAt, it.tc.finishedAt,
it.tc.logInfo.absoluteFilePath, it.tc.log.logcatFilePath,
it.tc.recordingInfo?.absoluteFilePath ?: "-" it.tc.recording?.absoluteFilePath ?: "-"
) )
) )
} }
stopDumpLog()
} }
fun startDumpLog() { @EventListener
// TODO: fun failed(event: TcFailEvent) {
}
fun stopDumpLog() {
// TODO
} }
companion object : KLogging() companion object : KLogging()
} }

6
src/main/resources/application.yaml

@ -7,11 +7,13 @@ logging:
level: level:
root: INFO root: INFO
tv.anypoint: DEBUG tv.anypoint: DEBUG
server:
port: 8080
anypoint.android-qa: anypoint.android-qa:
stb: stb:
ip: 192.168.0.1 ip: 192.168.15.139
port: 5555 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.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import tv.anypoint.domain.agent.AuthResponse import tv.anypoint.proxy.model.agent.AuthResponse
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
class AuthResponseTest : BehaviorSpec() { class AuthResponseTest : BehaviorSpec() {
@ -15,7 +15,7 @@ class AuthResponseTest : BehaviorSpec() {
Given("auth-response.json file") { Given("auth-response.json file") {
val file = ClassPathResource("auth-response.json").file val file = ClassPathResource("auth-response.json").file
When("deserialize") { When("deserialize") {
val actual = Json.decodeFromStream<AuthResponse>(file.inputStream()) val actual = Json.decodeFromStream<tv.anypoint.proxy.model.agent.AuthResponse>(file.inputStream())
Then("read values") { Then("read values") {
actual.deviceId shouldBe 36986237L 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