Compare commits

...

2 Commits

Author SHA1 Message Date
bean 9cfd685614 RD-214 save all.log 2 years ago
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. 35
      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. 8
      src/main/kotlin/tv/anypoint/dsl/model/LogInfo.kt
  12. 12
      src/main/kotlin/tv/anypoint/dsl/model/Logcat.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. 46
      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)

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

@ -1,10 +1,13 @@
package tv.anypoint.dsl
import tv.anypoint.domain.adb.ExtraKey
import tv.anypoint.domain.adb.IntentAction
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.Logcat
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 +20,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 Logcat.expected(
expectedLog: String,
timeout: Long = 10_000L
) {
val logcat = this
val startedAt = LocalDateTime.now()
val reader = LineNumberReader(BufferedReader(FileReader(logcat.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 = logcat.cursor
var line: String?
while ((reader.readLine().also { line = it }) != null) {
if (line!!.contains(expectedLog)) {
return
}
logcat.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)
)
)

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
}

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

@ -0,0 +1,12 @@
package tv.anypoint.dsl.model
import kotlinx.serialization.Serializable
@Serializable
class Logcat(
val logcatFilePath: String
) {
var startLine: Int = 0
var cursor: Int = 0
var lastLine: Int = 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 logcat: Logcat
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.Logcat
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.logcat = Logcat("${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.logcat.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.logcat.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
}

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

@ -0,0 +1,46 @@
package tv.anypoint.dsl
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.longs.shouldBeLessThanOrEqual
import io.kotest.matchers.shouldBe
import mu.KLogging
import tv.anypoint.dsl.model.Logcat
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 = Logcat("./test.log")
val test7 = Logcat("./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