├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── DEVELOP.md ├── LICENSE ├── README.md ├── apkmanager.go ├── assets ├── index.html ├── remote.html ├── terminal.html └── terminal.ico ├── assets_dev.go ├── assets_generate.go ├── background.go ├── build-run-fg.sh ├── build-run.sh ├── build-x86.sh ├── cmdctrl ├── cmdctrl.go └── cmdctrl_test.go ├── dns.go ├── go.mod ├── go.sum ├── httpserver.go ├── hub.go ├── jsonrpc └── json2.go ├── logger └── logger.go ├── main.go ├── minitouch.go ├── minitouch_test.go ├── proto.go ├── pubsub └── pubsub.go ├── requirements.go ├── safetimer.go ├── safetimer_test.go ├── screenshot.go ├── subcmd └── curl.go ├── term_posix.go ├── term_windows.go ├── tunnelproxy.go ├── update.go ├── update_test.go ├── utils.go └── utils_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | 26 | - name: Run tests 27 | run: go test -v -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: goreleaser 3 | 4 | on: 5 | pull_request: 6 | push: 7 | # run only against tags 8 | tags: 9 | - "*" 10 | 11 | permissions: 12 | contents: write 13 | # packages: write 14 | # issues: write 15 | 16 | jobs: 17 | goreleaser: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: stable 28 | # More assembly might be required: Docker logins, GPG, etc. 29 | # It all depends on your needs. 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v5 32 | with: 33 | # either 'goreleaser' (default) or 'goreleaser-pro' 34 | distribution: goreleaser 35 | # 'latest', 'nightly', or a semver 36 | version: latest 37 | args: release --clean 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 41 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | *_vfsdata.go 16 | 17 | atx-agent 18 | testcmd/ 19 | 20 | *.tmp/ 21 | *.exe.old 22 | *.apk 23 | .DS_Store 24 | dist/ 25 | #go.sum 26 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - 3 | # https://github.com/golang/go/wiki/GoArm 4 | goos: 5 | - linux 6 | goarch: 7 | - amd64 8 | - arm 9 | - arm64 10 | - 386 11 | goarm: 12 | - 6 13 | - 7 14 | ignore: 15 | - goos: windows 16 | goarch: 386 17 | flags: -tags vfs 18 | hooks: 19 | pre: go generate 20 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Develop doc 2 | 需要Go版本 >= 1.11, 这个版本之后可以不用设置GOPATH变量了。 3 | 4 | 5 | ## 安装Go环境 6 | Mac上安装Go 7 | 8 | ```bash 9 | brew install go 10 | ``` 11 | 12 | ## 编译方法 13 | 编译参考: https://github.com/golang/go/wiki/GoArm 14 | 15 | ```bash 16 | # 下载代码 17 | git clone https://github.com/openatx/atx-agent 18 | cd atx-agent 19 | 20 | # 通过下面的命令就可以设置代理,方便国内用户。国外用户忽略 21 | export GOPROXY=https://goproxy.io 22 | 23 | # 使用go.mod管理依赖库 24 | export GO111MODULE=on 25 | 26 | # 将assets目录下的文件打包成go代码 27 | go get -v github.com/shurcooL/vfsgen # 不执行这个好像也没关系 28 | go generate 29 | 30 | # build for android binary 31 | GOOS=linux GOARCH=arm go build -tags vfs 32 | ``` 33 | 34 | ## 七牛 35 | 感谢ken提供的Qiniu镜像服务。默认qiniu服务器会去github上拉镜像,但由于近期(2020-03-20)镜像服务越来越不稳定,所以目前改为在travis服务器上直接推送到七牛CDN 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/atx-agent/0c30c9749ef662f3aec83db3a04f5c4f82e32d48/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atx-agent 2 | [![Build Status](https://travis-ci.org/openatx/atx-agent.svg?branch=master)](https://travis-ci.org/openatx/atx-agent) 3 | 4 | 这个项目的主要目的是为了屏蔽不同安卓机器的差异,然后开放出统一的HTTP接口供 [openatx/uiautomator2](https://github.com/openatx/uiautomator2)使用。项目最终会发布成一个二进制程序,运行在Android系统的后台。 5 | 6 | 这个项目是如何屏蔽不同机器的差异的呢?举个例子来说,截图这个操作,大概需要3次判断才行。 7 | 8 | 1. 先判断minicap是否安装可用,然后minicap截图。毕竟minicap截图速度最快 9 | 2. 使用uiautomator2提供的接口截图。(模拟器除外) 10 | 3. 使用screencap截图,然后根据屏幕的旋转调整旋转方向。(一般只有模拟器用这种方式截图) 11 | 12 | 正是Android手机不同的表现形式,才导致了需要这么多的判断。而atx-agent就是为了将这些操作帮你处理了。然后提供统一的HTTP接口(GET /screenshot)供你使用。 13 | 14 | # Develop 15 | 这个项目是用Go语言写成的。编译的时候的需要你有一点Go语言的基础。 16 | 更多内容查看 [DEVELOP.md](DEVELOP.md) 17 | 18 | # Installation 19 | 从下载以`linux_armv7.tar.gz`结尾的二进制包。绝大部分手机都是linux-arm架构的。 20 | 21 | 解压出`atx-agent`文件,然后打开控制台 22 | ```bash 23 | $ adb push atx-agent /data/local/tmp 24 | $ adb shell chmod 755 /data/local/tmp/atx-agent 25 | # launch atx-agent in daemon mode 26 | $ adb shell /data/local/tmp/atx-agent server -d 27 | 28 | # stop already running atx-agent and start daemon 29 | $ adb shell /data/local/tmp/atx-agent server -d --stop 30 | ``` 31 | 32 | 默认监听的端口是7912。 33 | 34 | # 常用接口 35 | 假设手机的地址是$DEVICE_URL (eg: `http://10.0.0.1:7912`) 36 | 37 | ## 获取手机截图 38 | ```bash 39 | # jpeg format image 40 | $ curl $DEVICE_URL/screenshot 41 | 42 | # 使用内置的uiautomator截图 43 | $ curl "$DEVICE_URL/screenshot/0?minicap=false" 44 | ``` 45 | 46 | ## 获取当前程序版本 47 | ```bash 48 | $ curl $DEVICE_URL/version 49 | # expect example: 0.0.2 50 | ``` 51 | 52 | ## 获取设备信息 53 | ```bash 54 | $ curl $DEVICE_URL/info 55 | { 56 | "udid": "bf755cab-ff:ff:ff:ff:ff:ff-SM901", 57 | "serial": "bf755cab", 58 | "brand": "SMARTISAN", 59 | "model": "SM901", 60 | "hwaddr": "ff:ff:ff:ff:ff:ff", 61 | "agentVersion": "dev" 62 | } 63 | ``` 64 | 65 | ## 获取Hierarchy 66 | 这个接口目前比较高级,当跟uiautomator通信失败的时候,它会在后台启动uiautomator这个服务,等它恢复正常了,在返回数据。 67 | 68 | ```bash 69 | $ curl $DEVICE_URL/dump/hierarchy 70 | { 71 | "jsonrpc":"2.0", 72 | "id":1559113464, 73 | "result": " ... hierarchy ..." 74 | } 75 | 76 | # 停止掉uiautomator 77 | $ curl -X DELETE $DEVICE_URL/uiautomator 78 | Success 79 | 80 | # 再次调用, 依然OK,只是可能要等个7~8s 81 | $ curl $DEVICE_URL/dump/hierarchy 82 | { 83 | "jsonrpc": "2.0", 84 | ... 85 | } 86 | ``` 87 | 88 | ## 安装应用 89 | ```bash 90 | $ curl -X POST -d url="http://some-host/some.apk" $DEVICE_URL/install 91 | # expect install id 92 | 2 93 | # get install progress 94 | $ curl -X GET $DEVICE_URL/install/1 95 | { 96 | "id": "2", 97 | "titalSize": 770571, 98 | "copiedSize": 770571, 99 | "message": "success installed" 100 | } 101 | ``` 102 | 103 | ## Shell命令 104 | ```bash 105 | $ curl -X POST -d command="pwd" $DEVICE_URL/shell 106 | { 107 | "output": "/", 108 | "error": null 109 | } 110 | ``` 111 | 112 | 后台Shell命令(可以在后台持续运行,不会被杀掉) 113 | 114 | ```bash 115 | $ curl -X POST -d command="pwd" $DEVICE_URL/shell/background 116 | { 117 | "success": true, 118 | } 119 | ``` 120 | 121 | ## Webview相关 122 | ```bash 123 | $ curl -X GET $DEVICE_URL/webviews 124 | [ 125 | "webview_devtools_remote_m6x_21074", 126 | "webview_devtools_remote_m6x_27681", 127 | "chrome_devtools_remote" 128 | ] 129 | ``` 130 | 131 | 132 | ## App信息获取 133 | ```bash 134 | # 获取所有运行的应用 135 | $ curl $DEVICE_URL/proc/list 136 | [ 137 | { 138 | "cmdline": ["/system/bin/adbd", "--root_seclabel=u:r:su:s0"], 139 | "name": "adbd", 140 | "pid": 16177 141 | }, 142 | { 143 | "cmdline": ["com.netease.cloudmusic"], 144 | "name": "com.netease.cloudmusic", 145 | "pid": 15532 146 | } 147 | ] 148 | 149 | # 获取应用的内存信息(数据仅供参考),单位KB,total代表应用的PSS 150 | $ curl $DEVICE_URL/proc/com.netease.cloudmusic/meminfo 151 | { 152 | "code": 17236, 153 | "graphics": 20740, 154 | "java heap": 22288, 155 | "native heap": 20576, 156 | "private other": 10632, 157 | "stack": 48, 158 | "system": 110925, 159 | "total": 202445, 160 | "total swap pss": 88534 161 | } 162 | 163 | # 获取应用以及其所有子进程的内存数据 164 | $ curl $DEVICE_URL/proc/com.netease.cloudmusic/meminfo/all 165 | { 166 | "com.netease.cloudmusic": { 167 | "code": 15952, 168 | "graphics": 19328, 169 | "java heap": 45488, 170 | "native heap": 20840, 171 | "private other": 4056, 172 | "stack": 956, 173 | "system": 18652, 174 | "total": 125272 175 | }, 176 | "com.netease.cloudmusic:browser": { 177 | "code": 848, 178 | "graphics": 12, 179 | "java heap": 6580, 180 | "native heap": 5428, 181 | "private other": 1592, 182 | "stack": 336, 183 | "system": 10603, 184 | "total": 25399 185 | } 186 | } 187 | ``` 188 | 189 | # 获取CPU信息 190 | 191 | 如果进程是多线程运行的话,且机器是多核的,返回的CPU Percent可能会大于100% 192 | 193 | ```bash 194 | curl $DEVICE_URL/proc//cpuinfo 195 | # success return 196 | { 197 | "pid": 1122, 198 | "user": 288138, 199 | "system": 73457, 200 | "percent": 50.0, 201 | "systemPercent": 88.372, 202 | "coreCount": 4, 203 | } 204 | # failure return 205 | 410 Gone, Or 500 Internal error 206 | ``` 207 | 208 | 209 | ## 下载文件 210 | ```bash 211 | $ curl $DEVICE_URL/raw/sdcard/tmp.txt 212 | ``` 213 | 214 | ## 上传文件 215 | ```bash 216 | # 上传到/sdcard目录下 (url以/结尾) 217 | $ curl -F "file=@somefile.txt" $DEVICE_URL/upload/sdcard/ 218 | 219 | # 上传到/sdcard/tmp.txt 220 | $ curl -F "file=@somefile.txt" $DEVICE_URL/upload/sdcard/tmp.txt 221 | ``` 222 | 223 | 上传目录(url必须以/结尾) 224 | 225 | ```bash 226 | $ curl -F file=@some.zip -F dir=true $DEVICE_URL/upload/sdcard/ 227 | ``` 228 | 229 | ## 获取文件和目录信息 230 | ```bash 231 | # 文件 232 | $ curl -X GET $DEVICE_URL/finfo/data/local/tmp/tmp.txt 233 | { 234 | "name": "tmp.txt", 235 | "path": "/data/local/tmp/tmp.txt", 236 | "isDirectory": false, 237 | "size": 15232, 238 | } 239 | 240 | # 目录 241 | $ curl -X GET $DEVICE_URL/finfo/data/local/tmp 242 | { 243 | "name": "tmp", 244 | "path": "/data/local/tmp", 245 | "isDirectory": true, 246 | "size": 8192, 247 | "files": [ 248 | { 249 | "name": "tmp.txt", 250 | "path": "/data/local/tmp/tmp.txt" 251 | "isDirectory": false, 252 | } 253 | ] 254 | } 255 | ``` 256 | 257 | 相当于将`some.zip`上传到手机,然后执行`unzip some.zip -d /sdcard`, 最后将`some.zip`删除 258 | 259 | ## 离线下载 260 | ```bash 261 | # 离线下载,返回ID 262 | $ curl -F url=https://.... -F filepath=/sdcard/some.txt -F mode=0644 $DEVICE_URL/download 263 | 1 264 | # 通过返回的ID查看下载状态 265 | $ curl $DEVICE_URL/download/1 266 | { 267 | "message": "downloading", 268 | "progress": { 269 | "totalSize": 15000, 270 | "copiedSize": 10000 271 | } 272 | } 273 | ``` 274 | 275 | ## uiautomator起停 276 | ```bash 277 | # 启动 278 | $ curl -X POST $DEVICE_URL/uiautomator 279 | Success 280 | 281 | # 停止 282 | $ curl -X DELETE $DEVICE_URL/uiautomator 283 | Success 284 | 285 | # 再次停止 286 | $ curl -X DELETE $DEVICE_URL/uiautomator 287 | Already stopped 288 | 289 | # 获取uiautomator状态 290 | $ curl $DEVICE/uiautomator 291 | { 292 | "running": true 293 | } 294 | ``` 295 | 296 | ## 启动应用 297 | ```bash 298 | # timeout 代表 am start -n 的超时时间 299 | # flags 默认为 -S -W 300 | $ http POST $DEVICE_URL/session/{com.cleanmaster.mguard_cn} timeout==10s flags=="-S" 301 | { 302 | "mainActivity": "com.keniu.security.main.MainActivity", 303 | "output": "Stopping: com.cleanmaster.mguard_cn\nStarting: Intent { cmp=com.cleanmaster.mguard_cn/com.keniu.security.main.MainActivity }\n", 304 | "success": true 305 | } 306 | ``` 307 | 308 | ## 获取包信息 309 | ```bash 310 | $ http GET $DEVICE_URL/packages/{packageName}/info 311 | { 312 | "success": true, 313 | "data": { 314 | "mainActivity": "com.github.uiautomator.MainActivity", 315 | "label": "ATX", 316 | "versionName": "1.1.7", 317 | "versionCode": 1001007, 318 | "size":1760809 319 | } 320 | } 321 | ``` 322 | 323 | 其中`size`单位为字节 324 | 325 | ## 获取包的图标 326 | ``` 327 | $ curl -XGET $DEVICE_URL/packages/{packageName}/icon 328 | # 返回包的图标文件 329 | # 失败的情况 status code != 200 330 | ``` 331 | 332 | ## 获取所有包的信息 333 | 该接口速度有点慢,大约需要3s。 334 | 335 | 原理是通过`pm list packages -3 -f`获取包的信息,然后在用`androidbinary`库对包进行解析 336 | 337 | ```bash 338 | $ http GET $DEVICE_URL/packages 339 | [ 340 | { 341 | "packageName": "com.github.uiautomator", 342 | "mainActivity": "com.github.uiautomator.MainActivity", 343 | "label": "ATX", 344 | "versionName": "1.1.7-2-361182f-dirty", 345 | "versionCode": 1001007, 346 | "size": 1639366 347 | }, 348 | { 349 | "packageName": "com.smartisanos.payment", 350 | "mainActivity": "", 351 | "label": "", 352 | "versionName": "1.1", 353 | "versionCode": 1, 354 | "size": 3910826 355 | }, 356 | ... 357 | ] 358 | ``` 359 | 360 | ## 调整uiautomator自动停止时间 (默认3分钟) 361 | ```bash 362 | $ curl -X POST 10.0.0.1:7912/newCommandTimeout --data 300 363 | { 364 | "success": true, 365 | "description":"newCommandTimeout updated to 5m0s" 366 | } 367 | ``` 368 | 369 | ## 程序自升级(暂时不能用了) 370 | 升级程序从gihub releases里面直接下载,升级完后自动重启 371 | 372 | 升级到最新版 373 | 374 | ```bash 375 | $ curl 10.0.0.1:7912/upgrade 376 | ``` 377 | 378 | 指定升级的版本 379 | 380 | ```bash 381 | $ curl "10.0.0.1:7912/upgrade?version=0.0.2" 382 | ``` 383 | 384 | ## 修复minicap, minitouch程序 385 | 386 | ```bash 387 | # Fix minicap 388 | $ curl -XPUT 10.0.0.1:7912/minicap 389 | 390 | # Fix minitouch 391 | $ curl -XPUT 10.0.0.1:7912/minitouch 392 | ``` 393 | 394 | ## 视频录制(不推荐用) 395 | 开始录制 396 | 397 | ```bash 398 | $ curl -X POST 10.0.0.1:7912/screenrecord 399 | ``` 400 | 401 | 停止录制并获取录制结果 402 | 403 | ```bash 404 | $ curl -X PUT 10.0.0.1:7912/screenrecord 405 | { 406 | "videos": [ 407 | "/sdcard/screenrecords/0.mp4", 408 | "/sdcard/screenrecords/1.mp4" 409 | ] 410 | } 411 | ``` 412 | 413 | 之后再下载到本地 414 | 415 | ```bash 416 | $ curl -X GET 10.0.0.1:7912/raw/sdcard/screenrecords/0.mp4 417 | ``` 418 | 419 | ## Minitouch操作方法 420 | 感谢 [openstf/minitouch](https://github.com/openstf/minitouch) 421 | 422 | Websocket连接 `$DEVICE_URL/minitouch`, 一行行的按照JSON的格式写入 423 | 424 | >注: 坐标原点始终是手机正放时候的左上角,使用者需要自己处理旋转的变化 425 | 426 | 请先详细阅读minitouch的[Usage](https://github.com/openstf/minitouch#usage)文档,再来看下面的部分 427 | 428 | - Touch Down 429 | 430 | 坐标(X: 50%, Y: 50%), index代表第几个手指, `pressure`是可选的。 431 | 432 | ```json 433 | {"operation": "d", "index": 0, "xP": 0.5, "yP": 0.5, "pressure": 50} 434 | ``` 435 | 436 | - Touch Commit 437 | 438 | ```json 439 | {"operation": "c"} 440 | ``` 441 | 442 | - Touch Move 443 | 444 | ```json 445 | {"operation": "m", "index": 0, "xP": 0.5, "yP": 0.5, "pressure": 50} 446 | ``` 447 | 448 | - Touch Up 449 | 450 | ```json 451 | {"operation": "u", "index": 0} 452 | ``` 453 | 454 | - 点击x:20%, y:20,滑动到x:40%, y:50% 455 | 456 | ```json 457 | {"operation": "d", "index": 0, "xP": 0.20, "yP": 0.20, "pressure": 50} 458 | {"operation": "c"} 459 | {"operation": "m", "index": 0, "xP": 0.40, "yP": 0.50, "pressure": 50} 460 | {"operation": "c"} 461 | {"operation": "u", "index": 0} 462 | {"operation": "c"} 463 | ``` 464 | 465 | # TODO 466 | 1. 目前安全性还是个问题,以后再想办法改善 467 | 2. 补全接口文档 468 | 3. 内置的网页adb shell的安全问题 469 | 470 | # Logs 471 | log path `/sdcard/atx-agent.log` 472 | 473 | ## TODO 474 | - [ ] 使用支持多线程下载的库 https://github.com/cavaliercoder/grab 475 | 476 | # LICENSE 477 | [MIT](LICENSE) 478 | -------------------------------------------------------------------------------- /apkmanager.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/shogo82148/androidbinary/apk" 11 | ) 12 | 13 | var canFixedInstallFails = map[string]bool{ 14 | "INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE": true, 15 | "INSTALL_FAILED_UPDATE_INCOMPATIBLE": true, 16 | "INSTALL_FAILED_VERSION_DOWNGRADE": true, 17 | } 18 | 19 | type APKManager struct { 20 | Path string 21 | packageName string 22 | mainActivity string 23 | } 24 | 25 | func (am *APKManager) PackageName() (string, error) { 26 | if am.packageName != "" { 27 | return am.packageName, nil 28 | } 29 | pkg, err := apk.OpenFile(am.Path) 30 | if err != nil { 31 | return "", errors.Wrap(err, "apk parse") 32 | } 33 | defer pkg.Close() 34 | am.packageName = pkg.PackageName() 35 | am.mainActivity, _ = pkg.MainActivity() 36 | 37 | return am.packageName, nil 38 | } 39 | 40 | func (am *APKManager) Install() error { 41 | sdk, _ := strconv.Atoi(getCachedProperty("ro.build.version.sdk")) 42 | cmds := []string{"pm", "install", "-d", "-r", am.Path} 43 | if sdk >= 23 { // android 6.0 44 | cmds = []string{"pm", "install", "-d", "-r", "-g", am.Path} 45 | } 46 | out, err := runShell(cmds...) 47 | if err != nil { 48 | matches := regexp.MustCompile(`Failure \[([\w_ ]+)\]`).FindStringSubmatch(string(out)) 49 | if len(matches) > 0 { 50 | return errors.Wrap(err, matches[0]) 51 | } 52 | return errors.Wrap(err, string(out)) 53 | } 54 | return nil 55 | } 56 | 57 | func (am *APKManager) ForceInstall() error { 58 | err := am.Install() 59 | if err == nil { 60 | return nil 61 | } 62 | errType := regexp.MustCompile(`INSTALL_FAILED_[\w_]+`).FindString(err.Error()) 63 | if !canFixedInstallFails[errType] { 64 | return err 65 | } 66 | log.Infof("install meet %v, try to uninstall", errType) 67 | packageName, err := am.PackageName() 68 | if err != nil { 69 | return errors.Wrap(err, "apk parse") 70 | } 71 | 72 | log.Infof("uninstall %s", packageName) 73 | runShell("pm", "uninstall", packageName) 74 | return am.Install() 75 | } 76 | 77 | type StartOptions struct { 78 | Stop bool 79 | Wait bool 80 | } 81 | 82 | func (am *APKManager) Start(opts StartOptions) error { 83 | packageName, err := am.PackageName() 84 | if err != nil { 85 | return err 86 | } 87 | if am.mainActivity == "" { 88 | return errors.New("parse MainActivity failed") 89 | } 90 | mainActivity := am.mainActivity 91 | if !strings.Contains(mainActivity, ".") { 92 | mainActivity = "." + mainActivity 93 | } 94 | _, err = runShellTimeout(30*time.Second, "am", "start", "-n", packageName+"/"+mainActivity) 95 | return err 96 | } 97 | 98 | func installAPK(path string) error { 99 | am := &APKManager{Path: path} 100 | return am.Install() 101 | } 102 | 103 | func forceInstallAPK(filepath string) error { 104 | am := &APKManager{Path: filepath} 105 | return am.ForceInstall() 106 | } 107 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ATX-Agent 9 | 10 | 11 | 12 |

Hello ATX-Agent

13 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/remote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Remote control 9 | 10 | 11 | 12 |
13 |

极简手机远程控制

14 |
15 | ReadOnly 16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /assets/terminal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | atx-agent terminal 10 | 11 | 12 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /assets/terminal.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/atx-agent/0c30c9749ef662f3aec83db3a04f5c4f82e32d48/assets/terminal.ico -------------------------------------------------------------------------------- /assets_dev.go: -------------------------------------------------------------------------------- 1 | // +build !vfs 2 | //go:generate go run assets_generate.go 3 | 4 | package main 5 | 6 | import "net/http" 7 | 8 | // Assets contains project assets. 9 | var Assets http.FileSystem = http.Dir("assets") 10 | -------------------------------------------------------------------------------- /assets_generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | "github.com/shurcooL/vfsgen" 10 | ) 11 | 12 | func main() { 13 | var fs http.FileSystem = http.Dir("assets") 14 | 15 | err := vfsgen.Generate(fs, vfsgen.Options{ 16 | PackageName: "main", 17 | BuildTags: "vfs", 18 | VariableName: "Assets", 19 | }) 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /background.go: -------------------------------------------------------------------------------- 1 | /* 2 | Handle offline download and apk install 3 | */ 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | "sync" 15 | "time" 16 | 17 | "github.com/DeanThompson/syncmap" 18 | "github.com/franela/goreq" 19 | ) 20 | 21 | const defaultDownloadTimeout = 2 * time.Hour 22 | 23 | var background = &Background{ 24 | sm: syncmap.New(), 25 | } 26 | 27 | type BackgroundState struct { 28 | Message string `json:"message"` 29 | Error string `json:"error"` 30 | Progress interface{} `json:"progress"` 31 | PackageName string `json:"packageName,omitempty"` 32 | Status string `json:"status"` 33 | err error 34 | wg sync.WaitGroup 35 | } 36 | 37 | type Background struct { 38 | sm *syncmap.SyncMap 39 | n int 40 | mu sync.Mutex 41 | // timer *SafeTimer 42 | } 43 | 44 | // Get return nil if not found 45 | func (b *Background) Get(key string) (status *BackgroundState) { 46 | value, ok := b.sm.Get(key) 47 | if !ok { 48 | return nil 49 | } 50 | status = value.(*BackgroundState) 51 | return status 52 | } 53 | 54 | func (b *Background) genKey() (key string, state *BackgroundState) { 55 | b.mu.Lock() 56 | defer b.mu.Unlock() 57 | b.n++ 58 | key = fmt.Sprintf("%d", b.n) 59 | state = &BackgroundState{} 60 | b.sm.Set(key, state) 61 | return 62 | } 63 | 64 | func (b *Background) HTTPDownload(urlStr string, dst string, mode os.FileMode) (key string) { 65 | key, state := b.genKey() 66 | state.wg.Add(1) 67 | go func() { 68 | defer time.AfterFunc(5*time.Minute, func() { 69 | b.sm.Delete(key) 70 | }) 71 | 72 | b.Get(key).Message = "downloading" 73 | if err := b.doHTTPDownload(key, urlStr, dst, mode); err != nil { 74 | b.Get(key).Message = "http download: " + err.Error() 75 | } else { 76 | b.Get(key).Message = "downloaded" 77 | } 78 | }() 79 | return 80 | } 81 | 82 | func (b *Background) Wait(key string) error { 83 | state := b.Get(key) 84 | if state == nil { 85 | return errors.New("not found key: " + key) 86 | } 87 | state.wg.Wait() 88 | return state.err 89 | } 90 | 91 | // Default download timeout 30 minutes 92 | func (b *Background) doHTTPDownload(key, urlStr, dst string, fileMode os.FileMode) (err error) { 93 | state := b.Get(key) 94 | if state == nil { 95 | panic("http download key invalid: " + key) 96 | } 97 | defer func() { 98 | state.err = err 99 | state.wg.Done() 100 | }() 101 | 102 | res, err := goreq.Request{ 103 | Uri: urlStr, 104 | MaxRedirects: 10, 105 | RedirectHeaders: true, 106 | }.Do() 107 | if err != nil { 108 | return 109 | } 110 | defer res.Body.Close() 111 | if res.StatusCode != http.StatusOK { 112 | body, err := res.Body.ToString() 113 | if err != nil && err == bytes.ErrTooLarge { 114 | return fmt.Errorf("Expected HTTP Status code: %d", res.StatusCode) 115 | } 116 | return errors.New(body) 117 | } 118 | 119 | // mkdir is not exists 120 | os.MkdirAll(filepath.Dir(dst), 0755) 121 | 122 | file, err := os.Create(dst) 123 | if err != nil { 124 | return 125 | } 126 | defer file.Close() 127 | 128 | var totalSize int 129 | fmt.Sscanf(res.Header.Get("Content-Length"), "%d", &totalSize) 130 | wrproxy := newDownloadProxy(file, totalSize) 131 | defer wrproxy.Done() 132 | b.Get(key).Progress = wrproxy 133 | 134 | // timeout here 135 | timer := time.AfterFunc(defaultDownloadTimeout, func() { 136 | res.Body.Close() 137 | }) 138 | defer timer.Stop() 139 | 140 | _, err = io.Copy(wrproxy, res.Body) 141 | if err != nil { 142 | return 143 | } 144 | if fileMode != 0 { 145 | os.Chmod(dst, fileMode) 146 | } 147 | return 148 | } 149 | 150 | type downloadProxy struct { 151 | canceled bool 152 | writer io.Writer 153 | TotalSize int `json:"totalSize"` 154 | CopiedSize int `json:"copiedSize"` 155 | Error string `json:"error,omitempty"` 156 | wg sync.WaitGroup 157 | } 158 | 159 | func newDownloadProxy(wr io.Writer, totalSize int) *downloadProxy { 160 | di := &downloadProxy{ 161 | writer: wr, 162 | TotalSize: totalSize, 163 | } 164 | di.wg.Add(1) 165 | return di 166 | } 167 | 168 | func (d *downloadProxy) Cancel() { 169 | d.canceled = true 170 | } 171 | 172 | func (d *downloadProxy) Write(data []byte) (int, error) { 173 | if d.canceled { 174 | return 0, errors.New("download proxy was canceled") 175 | } 176 | n, err := d.writer.Write(data) 177 | d.CopiedSize += n 178 | return n, err 179 | } 180 | 181 | // Should only call once 182 | func (d *downloadProxy) Done() { 183 | d.wg.Done() 184 | } 185 | 186 | func (d *downloadProxy) Wait() { 187 | d.wg.Wait() 188 | } 189 | -------------------------------------------------------------------------------- /build-run-fg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | 5 | ADB=$(which adb.exe) # for windows-linux 6 | 7 | set -ex 8 | ADB=${ADB:-"adb"} 9 | 10 | DEST="/data/local/tmp/atx-agent" 11 | 12 | echo "Build binary for arm ..." 13 | ABI=$(adb shell getprop ro.product.cpu.abi) 14 | 15 | GOARCH= 16 | case "$ABI" in 17 | arm64-v8a) 18 | GOARCH=arm64 19 | ;; 20 | *) 21 | GOARCH=arm 22 | ;; 23 | esac 24 | 25 | #GOOS=linux GOARCH=$GOARCH go build 26 | 27 | go generate 28 | GOOS=linux GOARCH=$GOARCH go build -tags vfs 29 | 30 | $ADB push atx-agent $DEST 31 | $ADB shell chmod 755 $DEST 32 | $ADB shell $DEST server --stop 33 | 34 | $ADB forward tcp:7912 tcp:7912 35 | 36 | # start server 37 | $ADB shell $DEST server "$@" 38 | -------------------------------------------------------------------------------- /build-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | 5 | ADB=$(which adb.exe) # for windows-linux 6 | 7 | set -ex 8 | ADB=${ADB:-"adb"} 9 | 10 | DEST="/data/local/tmp/atx-agent" 11 | 12 | echo "Build binary for arm ..." 13 | #GOOS=linux GOARCH=arm go build 14 | 15 | go generate 16 | GOOS=linux GOARCH=arm go build -tags vfs 17 | 18 | $ADB push atx-agent $DEST 19 | $ADB shell chmod 755 $DEST 20 | $ADB shell $DEST server --stop 21 | $ADB shell $DEST server -d "$@" 22 | 23 | $ADB forward tcp:7912 tcp:7912 24 | curl localhost:7912/wlan/ip 25 | -------------------------------------------------------------------------------- /build-x86.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | set -ex 5 | 6 | DEST="/data/local/tmp/atx-agent" 7 | 8 | echo "Build binary for x86(emulator) ..." 9 | GOOS=linux GOARCH=amd64 go build 10 | 11 | # go generate 12 | # GOOS=linux GOARCH=arm go build -tags vfs 13 | 14 | ADB="adb" 15 | $ADB push atx-agent $DEST 16 | $ADB shell chmod 755 $DEST 17 | $ADB shell $DEST server --stop 18 | $ADB shell $DEST server -d "$@" 19 | 20 | $ADB forward tcp:7912 tcp:7912 21 | curl localhost:7912/wlan/ip 22 | -------------------------------------------------------------------------------- /cmdctrl/cmdctrl.go: -------------------------------------------------------------------------------- 1 | // Like python-supervisor 2 | // Manager process start, stop, restart 3 | // Hope no bugs :) 4 | package cmdctrl 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | "strings" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/openatx/atx-agent/logger" 18 | ) 19 | 20 | var ( 21 | debug = true 22 | log = logger.Default 23 | 24 | ErrAlreadyRunning = errors.New("already running") 25 | ErrAlreadyStopped = errors.New("already stopped") 26 | ) 27 | 28 | func goFunc(f func() error) chan error { 29 | errC := make(chan error, 1) 30 | go func() { 31 | errC <- f() 32 | }() 33 | return errC 34 | } 35 | 36 | func shellPath() string { 37 | sh := os.Getenv("SHELL") 38 | if sh == "" { 39 | sh, err := exec.LookPath("sh") 40 | if err == nil { 41 | return sh 42 | } 43 | sh = "/system/bin/sh" 44 | } 45 | return sh 46 | } 47 | 48 | type CommandInfo struct { 49 | Environ []string 50 | Args []string 51 | ArgsFunc func() ([]string, error) // generate Args when args is dynamic 52 | MaxRetries int // 3 53 | NextLaunchWait time.Duration // 0.5s 54 | RecoverDuration time.Duration // 30s 55 | StopSignal os.Signal 56 | Shell bool 57 | 58 | OnStart func() error // if return non nil, cmd will not run 59 | OnStop func() 60 | 61 | Stderr io.Writer // nil 62 | Stdout io.Writer // nil 63 | Stdin io.Reader // nil 64 | } 65 | 66 | type CommandCtrl struct { 67 | rl sync.RWMutex 68 | cmds map[string]*processKeeper 69 | } 70 | 71 | func New() *CommandCtrl { 72 | return &CommandCtrl{ 73 | cmds: make(map[string]*processKeeper, 10), 74 | } 75 | } 76 | 77 | func (cc *CommandCtrl) Exists(name string) bool { 78 | cc.rl.RLock() 79 | defer cc.rl.RUnlock() 80 | _, ok := cc.cmds[name] 81 | return ok 82 | } 83 | 84 | func (cc *CommandCtrl) Add(name string, c CommandInfo) error { 85 | if len(c.Args) == 0 && c.ArgsFunc == nil { 86 | return errors.New("Args length must > 0") 87 | } 88 | if c.MaxRetries == 0 { 89 | c.MaxRetries = 3 90 | } 91 | if c.RecoverDuration == 0 { 92 | c.RecoverDuration = 30 * time.Second 93 | } 94 | if c.NextLaunchWait == 0 { 95 | c.NextLaunchWait = 500 * time.Millisecond 96 | } 97 | if c.StopSignal == nil { 98 | c.StopSignal = syscall.SIGTERM 99 | } 100 | 101 | cc.rl.Lock() 102 | defer cc.rl.Unlock() 103 | if _, exists := cc.cmds[name]; exists { 104 | return errors.New("name conflict: " + name) 105 | } 106 | cc.cmds[name] = &processKeeper{ 107 | name: name, 108 | cmdInfo: c, 109 | } 110 | return nil 111 | } 112 | 113 | func (cc *CommandCtrl) Start(name string) error { 114 | cc.rl.RLock() 115 | defer cc.rl.RUnlock() 116 | pkeeper, ok := cc.cmds[name] 117 | if !ok { 118 | return errors.New("cmdctl not found: " + name) 119 | } 120 | if pkeeper.cmdInfo.OnStart != nil { 121 | if err := pkeeper.cmdInfo.OnStart(); err != nil { 122 | return err 123 | } 124 | } 125 | return pkeeper.start() 126 | } 127 | 128 | // Stop send stop signal 129 | // Stop("demo") will quit immediately 130 | // Stop("demo", true) will quit until command really killed 131 | func (cc *CommandCtrl) Stop(name string, waits ...bool) error { 132 | cc.rl.RLock() 133 | defer cc.rl.RUnlock() 134 | pkeeper, ok := cc.cmds[name] 135 | if !ok { 136 | return errors.New("cmdctl not found: " + name) 137 | } 138 | wait := false 139 | if len(waits) > 0 { 140 | wait = waits[0] 141 | } 142 | return pkeeper.stop(wait) 143 | } 144 | 145 | // StopAll command and wait until all program quited 146 | func (cc *CommandCtrl) StopAll() { 147 | for _, pkeeper := range cc.cmds { 148 | pkeeper.stop(true) 149 | } 150 | } 151 | 152 | func (cc *CommandCtrl) Restart(name string) error { 153 | cc.Stop(name, true) 154 | return cc.Start(name) 155 | } 156 | 157 | // UpdateArgs func is not like exec.Command, the first argument name means cmdctl service name 158 | // the seconds argument args, should like "echo", "hello" 159 | // Example usage: 160 | // UpdateArgs("minitouch", "/data/local/tmp/minitouch", "-t", "1") 161 | func (cc *CommandCtrl) UpdateArgs(name string, args ...string) error { 162 | cc.rl.RLock() 163 | defer cc.rl.RUnlock() 164 | if len(args) <= 0 { 165 | return errors.New("Args length must > 0") 166 | } 167 | pkeeper, ok := cc.cmds[name] 168 | if !ok { 169 | return errors.New("cmdctl not found: " + name) 170 | } 171 | pkeeper.cmdInfo.Args = args 172 | log.Printf("cmd args: %v", pkeeper.cmdInfo.Args) 173 | if !pkeeper.keeping { 174 | return nil 175 | } 176 | return cc.Restart(name) 177 | } 178 | 179 | // Running return bool indicate if program is still running 180 | func (cc *CommandCtrl) Running(name string) bool { 181 | cc.rl.RLock() 182 | defer cc.rl.RUnlock() 183 | pkeeper, ok := cc.cmds[name] 184 | if !ok { 185 | return false 186 | } 187 | return pkeeper.keeping 188 | } 189 | 190 | // keep process running 191 | type processKeeper struct { 192 | name string 193 | mu sync.Mutex 194 | cmdInfo CommandInfo 195 | cmd *exec.Cmd 196 | retries int 197 | running bool 198 | keeping bool 199 | stopC chan bool 200 | runBeganAt time.Time 201 | donewg *sync.WaitGroup 202 | } 203 | 204 | // keep cmd running 205 | func (p *processKeeper) start() error { 206 | p.mu.Lock() 207 | if p.keeping { 208 | p.mu.Unlock() 209 | return ErrAlreadyRunning 210 | } 211 | p.keeping = true 212 | p.stopC = make(chan bool, 1) 213 | p.retries = 0 214 | p.donewg = &sync.WaitGroup{} 215 | p.donewg.Add(1) 216 | p.mu.Unlock() 217 | 218 | go func() { 219 | for { 220 | if p.retries < 0 { 221 | p.retries = 0 222 | } 223 | if p.retries > p.cmdInfo.MaxRetries { 224 | break 225 | } 226 | cmdArgs := p.cmdInfo.Args 227 | if p.cmdInfo.ArgsFunc != nil { 228 | var er error 229 | cmdArgs, er = p.cmdInfo.ArgsFunc() 230 | if er != nil { 231 | log.Printf("ArgsFunc error: %v", er) 232 | goto CMD_DONE 233 | } 234 | } 235 | log.Printf("[%s] Args: %v", p.name, cmdArgs) 236 | if p.cmdInfo.Shell { 237 | // simple but works fine 238 | cmdArgs = []string{shellPath(), "-c", strings.Join(cmdArgs, " ")} 239 | } 240 | p.cmd = exec.Command(cmdArgs[0], cmdArgs[1:]...) 241 | p.cmd.Env = append(os.Environ(), p.cmdInfo.Environ...) 242 | p.cmd.Stdin = p.cmdInfo.Stdin 243 | p.cmd.Stdout = p.cmdInfo.Stdout 244 | p.cmd.Stderr = p.cmdInfo.Stderr 245 | log.Printf("[%s] args: %v, env: %v", p.name, cmdArgs, p.cmdInfo.Environ) 246 | if err := p.cmd.Start(); err != nil { 247 | goto CMD_DONE 248 | } 249 | log.Printf("[%s] program pid: %d", p.name, p.cmd.Process.Pid) 250 | p.runBeganAt = time.Now() 251 | p.running = true 252 | cmdC := goFunc(p.cmd.Wait) 253 | select { 254 | case cmdErr := <-cmdC: 255 | if cmdErr != nil { 256 | log.Printf("[%s] cmd wait err: %v", p.name, cmdErr) 257 | } 258 | if time.Since(p.runBeganAt) > p.cmdInfo.RecoverDuration { 259 | p.retries -= 2 260 | } 261 | p.retries++ 262 | goto CMD_IDLE 263 | case <-p.stopC: 264 | p.terminate(cmdC) 265 | goto CMD_DONE 266 | } 267 | CMD_IDLE: 268 | log.Printf("[%s] idle for %v", p.name, p.cmdInfo.NextLaunchWait) 269 | p.running = false 270 | select { 271 | case <-p.stopC: 272 | goto CMD_DONE 273 | case <-time.After(p.cmdInfo.NextLaunchWait): 274 | // do nothing 275 | } 276 | } 277 | CMD_DONE: 278 | log.Printf("[%s] program finished", p.name) 279 | if p.cmdInfo.OnStop != nil { 280 | p.cmdInfo.OnStop() 281 | } 282 | p.mu.Lock() 283 | p.running = false 284 | p.keeping = false 285 | p.donewg.Done() 286 | p.mu.Unlock() 287 | }() 288 | return nil 289 | } 290 | 291 | // TODO: support kill by env, like jenkins 292 | func (p *processKeeper) terminate(cmdC chan error) { 293 | if runtime.GOOS == "windows" { 294 | if p.cmd.Process != nil { 295 | p.cmd.Process.Kill() 296 | } 297 | return 298 | } 299 | if p.cmd.Process != nil { 300 | p.cmd.Process.Signal(p.cmdInfo.StopSignal) 301 | } 302 | terminateWait := 3 * time.Second 303 | select { 304 | case <-cmdC: 305 | break 306 | case <-time.After(terminateWait): 307 | if p.cmd.Process != nil { 308 | p.cmd.Process.Kill() 309 | } 310 | } 311 | return 312 | } 313 | 314 | // stop cmd 315 | func (p *processKeeper) stop(wait bool) error { 316 | p.mu.Lock() 317 | if !p.keeping { 318 | p.mu.Unlock() 319 | return ErrAlreadyStopped 320 | } 321 | select { 322 | case p.stopC <- true: 323 | default: 324 | } 325 | donewg := p.donewg // keep a copy of sync.WaitGroup 326 | p.mu.Unlock() 327 | 328 | if wait { 329 | donewg.Wait() 330 | } 331 | return nil 332 | } 333 | -------------------------------------------------------------------------------- /cmdctrl/cmdctrl_test.go: -------------------------------------------------------------------------------- 1 | package cmdctrl 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func init() { 11 | debug = true 12 | } 13 | 14 | func TestProcessKeeperStartStop(t *testing.T) { 15 | cmdInfo := CommandInfo{ 16 | Args: []string{"sleep", "1"}, 17 | MaxRetries: 5, 18 | RecoverDuration: 2 * time.Second, 19 | NextLaunchWait: 1 * time.Second, 20 | } 21 | pkeeper := processKeeper{ 22 | cmdInfo: cmdInfo, 23 | } 24 | assert.Nil(t, pkeeper.start()) 25 | time.Sleep(500 * time.Millisecond) // 0.5s 26 | assert.True(t, pkeeper.keeping) 27 | assert.True(t, pkeeper.running) 28 | 29 | time.Sleep(1 * time.Second) // 1.5s 30 | assert.True(t, pkeeper.keeping) 31 | assert.False(t, pkeeper.running) 32 | 33 | time.Sleep(1500 * time.Millisecond) // 2.5s 34 | assert.True(t, pkeeper.keeping) 35 | assert.True(t, pkeeper.running) 36 | 37 | pkeeper.stop(true) 38 | assert.False(t, pkeeper.keeping) 39 | assert.False(t, pkeeper.running) 40 | 41 | // stop again 42 | assert.NotNil(t, pkeeper.stop(true)) 43 | assert.False(t, pkeeper.keeping) 44 | assert.False(t, pkeeper.running) 45 | 46 | assert.Nil(t, pkeeper.start()) 47 | assert.Nil(t, pkeeper.stop(false)) 48 | } 49 | 50 | func TestCommandCtrl(t *testing.T) { 51 | assert := assert.New(t) 52 | service := New() 53 | addErr := service.Add("mysleep", CommandInfo{ 54 | Args: []string{"sleep", "10"}, 55 | }) 56 | assert.Nil(addErr) 57 | assert.Nil(service.Start("mysleep")) 58 | assert.NotNil(service.Start("mysleep")) 59 | 60 | // duplicate add 61 | addErr = service.Add("mysleep", CommandInfo{ 62 | Args: []string{"sleep", "20"}, 63 | }) 64 | assert.NotNil(addErr) 65 | 66 | assert.Nil(service.UpdateArgs("mysleep", "sleep", "30")) 67 | assert.Equal(service.cmds["mysleep"].cmdInfo.Args, []string{"sleep", "30"}) 68 | assert.Nil(service.Stop("mysleep")) 69 | } 70 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type dnsSmartClient struct { 11 | dialer *net.Dialer 12 | } 13 | 14 | func newDnsSmartClient() *dnsSmartClient { 15 | return &dnsSmartClient{ 16 | dialer: &net.Dialer{ 17 | Timeout: 3 * time.Second, 18 | KeepAlive: 30 * time.Second, 19 | DualStack: true, 20 | }, 21 | } 22 | } 23 | func (c *dnsSmartClient) Dial(ctx context.Context, network, address string) (conn net.Conn, err error) { 24 | // net.dns1 might be ipv6, Issue https://github.com/openatx/atx-agent/issues/39 25 | dns1 := getProperty("net.dns1") 26 | if dns1 == "" || strings.Contains(dns1, ":") { 27 | // 国内DNS列表: https://www.zhihu.com/question/32229915 28 | dns1 = "114.114.114.114" 29 | } 30 | log.Println("dns resolve", dns1) 31 | return c.dialer.DialContext(ctx, "udp", dns1+":53") 32 | } 33 | 34 | func init() { 35 | net.DefaultResolver = &net.Resolver{ 36 | PreferGo: true, 37 | Dial: newDnsSmartClient().Dial, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/openatx/atx-agent 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.1 // indirect 5 | github.com/DeanThompson/syncmap v0.0.0-20170515023643-05cfe1984971 6 | github.com/alecthomas/kingpin v2.2.6+incompatible 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 9 | github.com/codeskyblue/goreq v0.0.0-20180831024223-49450746aaef 10 | github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 // indirect 11 | github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 12 | github.com/franela/goblin v0.0.0-20181003173013-ead4ad1d2727 // indirect 13 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 14 | github.com/getlantern/context v0.0.0-20181106182922-539649cc3118 // indirect 15 | github.com/getlantern/errors v0.0.0-20180829142810-e24b7f4ff7c7 // indirect 16 | github.com/getlantern/go-update v0.0.0-20170504001518-d7c3f1ac97f8 17 | github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5 // indirect 18 | github.com/getlantern/hex v0.0.0-20160523043825-083fba3033ad // indirect 19 | github.com/getlantern/hidden v0.0.0-20160523043807-d52a649ab33a // indirect 20 | github.com/getlantern/ops v0.0.0-20170904182230-37353306c908 // indirect 21 | github.com/go-stack/stack v1.8.0 // indirect 22 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect 23 | github.com/google/go-querystring v1.0.0 // indirect 24 | github.com/gorilla/mux v1.7.4 25 | github.com/gorilla/websocket v1.4.0 26 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect 27 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 28 | github.com/kr/binarydist v0.1.0 // indirect 29 | github.com/kr/pretty v0.1.0 // indirect 30 | github.com/kr/pty v1.1.8 31 | github.com/levigross/grequests v0.0.0-20190130132859-37c80f76a0da 32 | github.com/mholt/archiver v2.0.1-0.20171012052341-26cf5bb32d07+incompatible 33 | github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e 34 | github.com/nwaples/rardecode v1.0.0 // indirect 35 | github.com/onsi/gomega v1.5.0 // indirect 36 | github.com/openatx/androidutils v1.0.0 37 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 // indirect 38 | github.com/pierrec/lz4 v2.0.5+incompatible // indirect 39 | github.com/pkg/errors v0.8.1 40 | github.com/prometheus/procfs v0.0.2 41 | github.com/rs/cors v1.6.0 42 | github.com/sevlyar/go-daemon v0.1.4 43 | github.com/shogo82148/androidbinary v1.0.5 44 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect 45 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect 46 | github.com/sirupsen/logrus v1.6.0 47 | github.com/stretchr/testify v1.4.0 48 | github.com/ulikunitz/xz v0.5.5 // indirect 49 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect 50 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 51 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 52 | ) 53 | 54 | replace ( 55 | github.com/prometheus/procfs v0.0.2 => github.com/codeskyblue/procfs v0.0.0-20190614074311-71434f4ee4b7 56 | github.com/qiniu/log v0.0.0-20140728010919-a304a74568d6 => github.com/gobuild/log v1.0.0 57 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a => github.com/golang/net v0.0.0-20181114220301-adae6a3d119a 58 | ) 59 | 60 | go 1.13 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DeanThompson/syncmap v0.0.0-20170515023643-05cfe1984971 h1:P8qOH96Et86xkkgCNYcl5jHBo05+6KUY9CAmrkdBr64= 4 | github.com/DeanThompson/syncmap v0.0.0-20170515023643-05cfe1984971/go.mod h1:RGMdy7wvPQ2t5WjOOKhtLg58+TXZEK0vkmrdOtM4gO8= 5 | github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= 6 | github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 11 | github.com/codeskyblue/goreq v0.0.0-20180831024223-49450746aaef h1:sdL9ZNuTIsV2PBifPLBUvHNmWJG/ohPw04Y0mizsD8Y= 12 | github.com/codeskyblue/goreq v0.0.0-20180831024223-49450746aaef/go.mod h1:oLy8Ek90H3f0q7rfacBoiG5HXJo4fDKU800pciAk4ZQ= 13 | github.com/codeskyblue/procfs v0.0.0-20190614074311-71434f4ee4b7 h1:wi/PdMXlkX+w/rJ1hqSaNtfC8n/zQdZuvWg5/bbLlaw= 14 | github.com/codeskyblue/procfs v0.0.0-20190614074311-71434f4ee4b7/go.mod h1:HIAG9j7zWiDYOlZf9Dxme1azX/DMJ0qqv0IL4+rtEnw= 15 | github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= 16 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 h1:eX+pdPPlD279OWgdx7f6KqIRSONuK7egk+jDx7OM3Ac= 21 | github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76/go.mod h1:KjxHHirfLaw19iGT70HvVjHQsL1vq1SRQB4yOsAfy2s= 22 | github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY= 23 | github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU= 24 | github.com/franela/goblin v0.0.0-20181003173013-ead4ad1d2727 h1:eouy4stZdUKn7n98c1+rdUTxWMg+jvhP+oHt0K8fiug= 25 | github.com/franela/goblin v0.0.0-20181003173013-ead4ad1d2727/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 26 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= 27 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 28 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 29 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 30 | github.com/getlantern/context v0.0.0-20181106182922-539649cc3118 h1:n8rotJhMskm8bIQvNe1TIDta5JVJpH9g8QuHg2baJVs= 31 | github.com/getlantern/context v0.0.0-20181106182922-539649cc3118/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= 32 | github.com/getlantern/errors v0.0.0-20180829142810-e24b7f4ff7c7 h1:pKm0g6hKvbd09FUAfFdlGBV/1L1e2KnXsapRNR6Z5/E= 33 | github.com/getlantern/errors v0.0.0-20180829142810-e24b7f4ff7c7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= 34 | github.com/getlantern/go-update v0.0.0-20170504001518-d7c3f1ac97f8 h1:yexJQMPVopTGDKv9b+Zj2yxTgdXq0G8vHRktFqEIRBA= 35 | github.com/getlantern/go-update v0.0.0-20170504001518-d7c3f1ac97f8/go.mod h1:goroSTghTcnjKaR2C8ovKWy1lEvRNfqHrW/kRJNMek0= 36 | github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5 h1:Okd7vkn9CfIgDBj1ST/vtBTCfD/kxIhYD412K+FRKPc= 37 | github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5/go.mod h1:Vwx1Cg64gCdIalad44uvQsKZw6LsVczIKZrUBStEjVw= 38 | github.com/getlantern/hex v0.0.0-20160523043825-083fba3033ad h1:L/UatDVr6opOJnZdZnGwhFXjoIUwO6RHULxPyzb60L4= 39 | github.com/getlantern/hex v0.0.0-20160523043825-083fba3033ad/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= 40 | github.com/getlantern/hidden v0.0.0-20160523043807-d52a649ab33a h1:gF6WQx7V8q91bBC4MUWOuKnWjUcDDi27d1zMdB0hPnM= 41 | github.com/getlantern/hidden v0.0.0-20160523043807-d52a649ab33a/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= 42 | github.com/getlantern/ops v0.0.0-20170904182230-37353306c908 h1:PiLUBXrdcPfVqAQVqLGCfKVq/5KI3KsRowk2mJ7IuVk= 43 | github.com/getlantern/ops v0.0.0-20170904182230-37353306c908/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= 44 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 45 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 46 | github.com/golang/net v0.0.0-20181114220301-adae6a3d119a h1:wxhJMi186V9aI731PnbZnsQ1aE0hEeDc/Gf9tTNIssU= 47 | github.com/golang/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:98y8FxUyMjTdJ5eOj/8vzuiVO14/dkJ98NYhEPG8QGY= 48 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 49 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 50 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= 51 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 52 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 53 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 54 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 55 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 56 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 57 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 58 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 59 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 60 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 61 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= 62 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 63 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 64 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 65 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 66 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 67 | github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo= 68 | github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM= 69 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 70 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 71 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 72 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 73 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 74 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/levigross/grequests v0.0.0-20190130132859-37c80f76a0da h1:ixpx9UaTDElZrjbd9GeOVG4Deut0FFumoeel7PvVNm4= 77 | github.com/levigross/grequests v0.0.0-20190130132859-37c80f76a0da/go.mod h1:uCZIhROSrVmuF/BPYFPwDeiiQ6juSLp0kikFoEcNcEs= 78 | github.com/mholt/archiver v2.0.1-0.20171012052341-26cf5bb32d07+incompatible h1:rFTJTBrWyCqUtpGm5FRRGLB3usGujkJ4hgjcVT8KciE= 79 | github.com/mholt/archiver v2.0.1-0.20171012052341-26cf5bb32d07+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= 80 | github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e h1:Qa6dnn8DlasdXRnacluu8HzPts0S1I9zvvUPDbBnXFI= 81 | github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e/go.mod h1:waEya8ee1Ro/lgxpVhkJI4BVASzkm3UZqkx/cFJiYHM= 82 | github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= 83 | github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= 84 | github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= 85 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 86 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 87 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 88 | github.com/openatx/androidutils v1.0.0 h1:gYKFX/LqOf4LxyO7dZrNfGtPNaCaSNrniUHL06MPATQ= 89 | github.com/openatx/androidutils v1.0.0/go.mod h1:Pbja6rsE71OHQMhrK/tZm86fqB9Go8sXToi9CylrXEU= 90 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= 91 | github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= 92 | github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= 93 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 94 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 95 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= 99 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 100 | github.com/sevlyar/go-daemon v0.1.4 h1:Ayxp/9SNHwPBjV+kKbnHl2ch6rhxTu08jfkGkoxgULQ= 101 | github.com/sevlyar/go-daemon v0.1.4/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= 102 | github.com/shogo82148/androidbinary v1.0.5 h1:7afvcNw+vT84R0ugrL/u/DIrGYylC66yNvt0Y0j7rrM= 103 | github.com/shogo82148/androidbinary v1.0.5/go.mod h1:FzpR5bLAXR3VsAUG4BRCFaUm0WV6YD4Ldu+m05tr9Vk= 104 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= 105 | github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= 106 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg= 107 | github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg= 108 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 109 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 110 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 111 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 112 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 113 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 114 | github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= 115 | github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 116 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 117 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 119 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 122 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 124 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 125 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 126 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 127 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 129 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 130 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 131 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 132 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 133 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 134 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 135 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 136 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | -------------------------------------------------------------------------------- /httpserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "image/jpeg" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path" 15 | "path/filepath" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "syscall" 21 | "time" 22 | 23 | "github.com/openatx/atx-agent/jsonrpc" 24 | 25 | "github.com/gorilla/mux" 26 | "github.com/gorilla/websocket" 27 | "github.com/mholt/archiver" 28 | "github.com/openatx/androidutils" 29 | "github.com/openatx/atx-agent/cmdctrl" 30 | "github.com/prometheus/procfs" 31 | "github.com/rs/cors" 32 | ) 33 | 34 | type Server struct { 35 | // tunnel *TunnelProxy 36 | httpServer *http.Server 37 | } 38 | 39 | func NewServer() *Server { 40 | server := &Server{} 41 | server.initHTTPServer() 42 | return server 43 | } 44 | 45 | func (server *Server) initHTTPServer() { 46 | m := mux.NewRouter() 47 | 48 | m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 49 | renderHTML(w, "index.html") 50 | }) 51 | 52 | m.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 53 | io.WriteString(w, version) 54 | }) 55 | 56 | m.HandleFunc("/remote", func(w http.ResponseWriter, r *http.Request) { 57 | renderHTML(w, "remote.html") 58 | }) 59 | 60 | // jsonrpc client to call uiautomator 61 | rpcc := jsonrpc.NewClient("http://127.0.0.1:9008/jsonrpc/0") 62 | rpcc.ErrorCallback = func() error { 63 | service.Restart("uiautomator") 64 | // if !service.Running("uiautomator") { 65 | // service.Start("uiautomator") 66 | // } 67 | return nil 68 | } 69 | rpcc.ErrorFixTimeout = 40 * time.Second 70 | rpcc.ServerOK = func() bool { 71 | return service.Running("uiautomator") 72 | } 73 | 74 | m.HandleFunc("/newCommandTimeout", func(w http.ResponseWriter, r *http.Request) { 75 | var timeout int 76 | err := json.NewDecoder(r.Body).Decode(&timeout) // TODO: auto get rotation 77 | if err != nil { 78 | http.Error(w, "Empty payload", 400) // bad request 79 | return 80 | } 81 | cmdTimeout := time.Duration(timeout) * time.Second 82 | uiautomatorTimer.Reset(cmdTimeout) 83 | renderJSON(w, map[string]interface{}{ 84 | "success": true, 85 | "description": fmt.Sprintf("newCommandTimeout updated to %v", cmdTimeout), 86 | }) 87 | }).Methods("POST") 88 | 89 | // robust communicate with uiautomator 90 | // If the service is down, restart it and wait it recover 91 | m.HandleFunc("/dump/hierarchy", func(w http.ResponseWriter, r *http.Request) { 92 | if !service.Running("uiautomator") { 93 | xmlContent, err := dumpHierarchy() 94 | if err != nil { 95 | log.Println("Err:", err) 96 | http.Error(w, err.Error(), http.StatusInternalServerError) 97 | return 98 | } 99 | renderJSON(w, map[string]interface{}{ 100 | "jsonrpc": "2.0", 101 | "id": 1, 102 | "result": xmlContent, 103 | }) 104 | return 105 | } 106 | resp, err := rpcc.RobustCall("dumpWindowHierarchy", false) // false: no compress 107 | if err != nil { 108 | log.Println("Err:", err) 109 | http.Error(w, err.Error(), http.StatusInternalServerError) 110 | return 111 | } 112 | renderJSON(w, resp) 113 | }) 114 | 115 | m.HandleFunc("/proc/list", func(w http.ResponseWriter, r *http.Request) { 116 | ps, err := listAllProcs() 117 | if err != nil { 118 | http.Error(w, err.Error(), 500) 119 | return 120 | } 121 | renderJSON(w, ps) 122 | }) 123 | 124 | m.HandleFunc("/proc/{pkgname}/meminfo", func(w http.ResponseWriter, r *http.Request) { 125 | pkgname := mux.Vars(r)["pkgname"] 126 | info, err := parseMemoryInfo(pkgname) 127 | if err != nil { 128 | http.Error(w, err.Error(), http.StatusInternalServerError) 129 | return 130 | } 131 | renderJSON(w, info) 132 | }) 133 | 134 | m.HandleFunc("/proc/{pkgname}/meminfo/all", func(w http.ResponseWriter, r *http.Request) { 135 | pkgname := mux.Vars(r)["pkgname"] 136 | ps, err := listAllProcs() 137 | if err != nil { 138 | http.Error(w, err.Error(), 500) 139 | return 140 | } 141 | 142 | mems := make(map[string]map[string]int, 0) 143 | for _, p := range ps { 144 | if len(p.Cmdline) != 1 { 145 | continue 146 | } 147 | if p.Name == pkgname || strings.HasPrefix(p.Name, pkgname+":") { 148 | info, err := parseMemoryInfo(p.Name) 149 | if err != nil { 150 | continue 151 | } 152 | mems[p.Name] = info 153 | } 154 | } 155 | renderJSON(w, mems) 156 | }) 157 | 158 | // make(map[int][]int) 159 | m.HandleFunc("/proc/{pkgname}/cpuinfo", func(w http.ResponseWriter, r *http.Request) { 160 | pkgname := mux.Vars(r)["pkgname"] 161 | pid, err := pidOf(pkgname) 162 | if err != nil { 163 | http.Error(w, err.Error(), http.StatusGone) 164 | return 165 | } 166 | info, err := readCPUInfo(pid) 167 | if err != nil { 168 | http.Error(w, err.Error(), http.StatusInternalServerError) 169 | return 170 | } 171 | renderJSON(w, info) 172 | }) 173 | 174 | m.HandleFunc("/webviews", func(w http.ResponseWriter, r *http.Request) { 175 | netUnix, err := procfs.NewNetUnix() 176 | if err != nil { 177 | return 178 | } 179 | 180 | unixPaths := make(map[string]bool, 0) 181 | for _, row := range netUnix.Rows { 182 | if !strings.HasPrefix(row.Path, "@") { 183 | continue 184 | } 185 | if !strings.Contains(row.Path, "devtools_remote") { 186 | continue 187 | } 188 | unixPaths[row.Path[1:]] = true 189 | } 190 | socketPaths := make([]string, 0, len(unixPaths)) 191 | for key := range unixPaths { 192 | socketPaths = append(socketPaths, key) 193 | } 194 | renderJSON(w, socketPaths) 195 | }) 196 | 197 | m.HandleFunc("/webviews/{pkgname}", func(w http.ResponseWriter, r *http.Request) { 198 | packageName := mux.Vars(r)["pkgname"] 199 | netUnix, err := procfs.NewNetUnix() 200 | if err != nil { 201 | return 202 | } 203 | 204 | unixPaths := make(map[string]bool, 0) 205 | for _, row := range netUnix.Rows { 206 | if !strings.HasPrefix(row.Path, "@") { 207 | continue 208 | } 209 | if !strings.Contains(row.Path, "devtools_remote") { 210 | continue 211 | } 212 | unixPaths[row.Path[1:]] = true 213 | } 214 | 215 | result := make([]interface{}, 0) 216 | procs, err := findProcAll(packageName) 217 | for _, proc := range procs { 218 | cmdline, _ := proc.CmdLine() 219 | suffix := "_" + strconv.Itoa(proc.PID) 220 | 221 | for socketPath := range unixPaths { 222 | if strings.HasSuffix(socketPath, suffix) || 223 | (packageName == "com.android.browser" && socketPath == "chrome_devtools_remote") { 224 | result = append(result, map[string]interface{}{ 225 | "pid": proc.PID, 226 | "name": cmdline[0], 227 | "socketPath": socketPath, 228 | }) 229 | } 230 | } 231 | } 232 | renderJSON(w, result) 233 | }) 234 | 235 | m.HandleFunc("/pidof/{pkgname}", func(w http.ResponseWriter, r *http.Request) { 236 | pkgname := mux.Vars(r)["pkgname"] 237 | pid, err := pidOf(pkgname) 238 | if err != nil { 239 | http.Error(w, err.Error(), http.StatusGone) 240 | return 241 | } 242 | io.WriteString(w, strconv.Itoa(pid)) 243 | }) 244 | 245 | m.HandleFunc("/session/{pkgname}", func(w http.ResponseWriter, r *http.Request) { 246 | packageName := mux.Vars(r)["pkgname"] 247 | mainActivity, err := mainActivityOf(packageName) 248 | if err != nil { 249 | http.Error(w, err.Error(), http.StatusGone) // 410 250 | return 251 | } 252 | // Refs: https://stackoverflow.com/questions/12131555/leading-dot-in-androidname-really-required 253 | // MainActivity convert to .MainActivity 254 | // com.example.app.MainActivity keep same 255 | // app.MainActivity keep same 256 | // So only words not contains dot, need to add prefix "." 257 | if !strings.Contains(mainActivity, ".") { 258 | mainActivity = "." + mainActivity 259 | } 260 | 261 | flags := r.FormValue("flags") 262 | if flags == "" { 263 | flags = "-W -S" // W: wait launched, S: stop before started 264 | } 265 | timeout := r.FormValue("timeout") // supported value: 60s, 1m. 60 is invalid 266 | duration, err := time.ParseDuration(timeout) 267 | if err != nil { 268 | duration = 60 * time.Second 269 | } 270 | 271 | output, err := runShellTimeout(duration, "am", "start", flags, "-n", packageName+"/"+mainActivity) 272 | if err != nil { 273 | renderJSON(w, map[string]interface{}{ 274 | "success": false, 275 | "error": err.Error(), 276 | "output": string(output), 277 | "mainActivity": mainActivity, 278 | }) 279 | } else { 280 | renderJSON(w, map[string]interface{}{ 281 | "success": true, 282 | "mainActivity": mainActivity, 283 | "output": string(output), 284 | }) 285 | } 286 | }).Methods("POST") 287 | 288 | m.HandleFunc("/session/{pid:[0-9]+}:{pkgname}/{url:ping|jsonrpc/0}", func(w http.ResponseWriter, r *http.Request) { 289 | pkgname := mux.Vars(r)["pkgname"] 290 | pid, _ := strconv.Atoi(mux.Vars(r)["pid"]) 291 | 292 | proc, err := procfs.NewProc(pid) 293 | if err != nil { 294 | http.Error(w, err.Error(), http.StatusGone) // 410 295 | return 296 | } 297 | cmdline, _ := proc.CmdLine() 298 | if len(cmdline) != 1 || cmdline[0] != pkgname { 299 | http.Error(w, fmt.Sprintf("cmdline expect [%s] but got %v", pkgname, cmdline), http.StatusGone) 300 | return 301 | } 302 | r.URL.Path = "/" + mux.Vars(r)["url"] 303 | uiautomatorProxy.ServeHTTP(w, r) 304 | }) 305 | 306 | m.HandleFunc("/shell", func(w http.ResponseWriter, r *http.Request) { 307 | command := r.FormValue("command") 308 | if command == "" { 309 | command = r.FormValue("c") 310 | } 311 | timeoutSeconds := r.FormValue("timeout") 312 | if timeoutSeconds == "" { 313 | timeoutSeconds = "60" 314 | } 315 | seconds, err := strconv.Atoi(timeoutSeconds) 316 | if err != nil { 317 | http.Error(w, err.Error(), http.StatusBadRequest) 318 | return 319 | } 320 | c := Command{ 321 | Args: []string{command}, 322 | Shell: true, 323 | Timeout: time.Duration(seconds) * time.Second, 324 | } 325 | output, err := c.CombinedOutput() 326 | exitCode := cmdError2Code(err) 327 | renderJSON(w, map[string]interface{}{ 328 | "output": string(output), 329 | "exitCode": exitCode, 330 | "error": err, 331 | }) 332 | }).Methods("GET", "POST") 333 | 334 | // TODO(ssx): untested 335 | m.HandleFunc("/shell/background", func(w http.ResponseWriter, r *http.Request) { 336 | command := r.FormValue("command") 337 | if command == "" { 338 | command = r.FormValue("c") 339 | } 340 | c := Command{ 341 | Args: []string{command}, 342 | Shell: true, 343 | } 344 | pid, err := c.StartBackground() 345 | if err != nil { 346 | renderJSON(w, map[string]interface{}{ 347 | "success": false, 348 | "description": err.Error(), 349 | }) 350 | return 351 | } 352 | renderJSON(w, map[string]interface{}{ 353 | "success": true, 354 | "pid": pid, 355 | "description": fmt.Sprintf("Successfully started program: %v", command), 356 | }) 357 | }) 358 | 359 | m.HandleFunc("/shell/stream", func(w http.ResponseWriter, r *http.Request) { 360 | w.Header().Set("Content-Type", "application/octet-stream") 361 | command := r.FormValue("command") 362 | if command == "" { 363 | command = r.FormValue("c") 364 | } 365 | c := exec.Command("sh", "-c", command) 366 | 367 | httpWriter := newFakeWriter(func(data []byte) (int, error) { 368 | n, err := w.Write(data) 369 | if err == nil { 370 | if f, ok := w.(http.Flusher); ok { 371 | f.Flush() 372 | } 373 | } else { 374 | log.Println("Write error") 375 | } 376 | return n, err 377 | }) 378 | c.Stdout = httpWriter 379 | c.Stderr = httpWriter 380 | 381 | // wait until program quit 382 | cmdQuit := make(chan error, 0) 383 | go func() { 384 | cmdQuit <- c.Run() 385 | }() 386 | select { 387 | case <-httpWriter.Err: 388 | if c.Process != nil { 389 | c.Process.Signal(syscall.SIGTERM) 390 | } 391 | case <-cmdQuit: 392 | log.Println("command quit") 393 | } 394 | log.Println("program quit") 395 | }) 396 | 397 | m.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) { 398 | log.Println("stop all service") 399 | service.StopAll() 400 | log.Println("service stopped") 401 | io.WriteString(w, "Finished!") 402 | go func() { 403 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 404 | defer cancel() // The document says need to call cancel(), but I donot known why. 405 | server.httpServer.Shutdown(ctx) 406 | }() 407 | }) 408 | 409 | m.HandleFunc("/services/{name}", func(w http.ResponseWriter, r *http.Request) { 410 | name := mux.Vars(r)["name"] 411 | var resp map[string]interface{} 412 | if !service.Exists(name) { 413 | w.WriteHeader(400) // bad request 414 | renderJSON(w, map[string]interface{}{ 415 | "success": false, 416 | "description": fmt.Sprintf("service %s does not exist", strconv.Quote(name)), 417 | }) 418 | return 419 | } 420 | switch r.Method { 421 | case "GET": 422 | resp = map[string]interface{}{ 423 | "success": true, 424 | "running": service.Running(name), 425 | } 426 | case "POST": 427 | err := service.Start(name) 428 | switch err { 429 | case nil: 430 | resp = map[string]interface{}{ 431 | "success": true, 432 | "description": "successfully started", 433 | } 434 | case cmdctrl.ErrAlreadyRunning: 435 | resp = map[string]interface{}{ 436 | "success": true, 437 | "description": "already started", 438 | } 439 | default: 440 | resp = map[string]interface{}{ 441 | "success": false, 442 | "description": "failure on start: " + err.Error(), 443 | } 444 | } 445 | case "DELETE": 446 | err := service.Stop(name) 447 | switch err { 448 | case nil: 449 | resp = map[string]interface{}{ 450 | "success": true, 451 | "description": "successfully stopped", 452 | } 453 | case cmdctrl.ErrAlreadyStopped: 454 | resp = map[string]interface{}{ 455 | "success": true, 456 | "description": "already stopped", 457 | } 458 | default: 459 | resp = map[string]interface{}{ 460 | "success": false, 461 | "description": "failure on stop: " + err.Error(), 462 | } 463 | } 464 | default: 465 | resp = map[string]interface{}{ 466 | "success": false, 467 | "description": "invalid request method: " + r.Method, 468 | } 469 | } 470 | if ok, success := resp["success"].(bool); ok { 471 | if !success { 472 | w.WriteHeader(400) // bad request 473 | } 474 | } 475 | renderJSON(w, resp) 476 | }).Methods("GET", "POST", "DELETE") 477 | 478 | // Deprecated use /services/{name} instead 479 | m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) { 480 | err := service.Start("uiautomator") 481 | if err == nil { 482 | io.WriteString(w, "Successfully started") 483 | } else if err == cmdctrl.ErrAlreadyRunning { 484 | io.WriteString(w, "Already started") 485 | } else { 486 | http.Error(w, err.Error(), 500) 487 | } 488 | }).Methods("POST") 489 | 490 | // Deprecated 491 | m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) { 492 | err := service.Stop("uiautomator", true) // wait until program quit 493 | if err == nil { 494 | io.WriteString(w, "Successfully stopped") 495 | } else if err == cmdctrl.ErrAlreadyStopped { 496 | io.WriteString(w, "Already stopped") 497 | } else { 498 | http.Error(w, err.Error(), 500) 499 | } 500 | }).Methods("DELETE") 501 | 502 | // Deprecated 503 | m.HandleFunc("/uiautomator", func(w http.ResponseWriter, r *http.Request) { 504 | running := service.Running("uiautomator") 505 | renderJSON(w, map[string]interface{}{ 506 | "running": running, 507 | }) 508 | }).Methods("GET") 509 | 510 | m.HandleFunc("/raw/{filepath:.*}", func(w http.ResponseWriter, r *http.Request) { 511 | filepath := "/" + mux.Vars(r)["filepath"] 512 | http.ServeFile(w, r, filepath) 513 | }) 514 | 515 | m.HandleFunc("/finfo/{lpath:.*}", func(w http.ResponseWriter, r *http.Request) { 516 | lpath := "/" + mux.Vars(r)["lpath"] 517 | finfo, err := os.Stat(lpath) 518 | if err != nil { 519 | if os.IsNotExist(err) { 520 | http.Error(w, err.Error(), 404) 521 | } else { 522 | http.Error(w, err.Error(), 403) // forbidden 523 | } 524 | return 525 | } 526 | data := make(map[string]interface{}, 5) 527 | data["name"] = finfo.Name() 528 | data["path"] = lpath 529 | data["isDirectory"] = finfo.IsDir() 530 | data["size"] = finfo.Size() 531 | 532 | if finfo.IsDir() { 533 | files, err := ioutil.ReadDir(lpath) 534 | if err == nil { 535 | finfos := make([]map[string]interface{}, 0, 3) 536 | for _, f := range files { 537 | finfos = append(finfos, map[string]interface{}{ 538 | "name": f.Name(), 539 | "path": filepath.Join(lpath, f.Name()), 540 | "isDirectory": f.IsDir(), 541 | }) 542 | } 543 | data["files"] = finfos 544 | } 545 | } 546 | renderJSON(w, data) 547 | }) 548 | 549 | // keep ApkService always running 550 | // if no activity in 5min, then restart apk service 551 | const apkServiceTimeout = 5 * time.Minute 552 | apkServiceTimer := NewSafeTimer(apkServiceTimeout) 553 | go func() { 554 | for range apkServiceTimer.C { 555 | log.Println("startservice com.github.uiautomator/.Service") 556 | runShell("am", "startservice", "-n", "com.github.uiautomator/.Service") 557 | apkServiceTimer.Reset(apkServiceTimeout) 558 | } 559 | }() 560 | 561 | deviceInfo := getDeviceInfo() 562 | 563 | m.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) { 564 | w.Header().Set("Content-Type", "application/json") 565 | json.NewEncoder(w).Encode(deviceInfo) 566 | }) 567 | 568 | m.HandleFunc("/info/battery", func(w http.ResponseWriter, r *http.Request) { 569 | apkServiceTimer.Reset(apkServiceTimeout) 570 | deviceInfo.Battery.Update() 571 | // if err := server.tunnel.UpdateInfo(deviceInfo); err != nil { 572 | // io.WriteString(w, "Failure "+err.Error()) 573 | // return 574 | // } 575 | io.WriteString(w, "Success") 576 | }).Methods("POST") 577 | 578 | m.HandleFunc("/info/rotation", func(w http.ResponseWriter, r *http.Request) { 579 | apkServiceTimer.Reset(apkServiceTimeout) 580 | var direction int // 0,1,2,3 581 | err := json.NewDecoder(r.Body).Decode(&direction) // TODO: auto get rotation 582 | if err == nil { 583 | deviceRotation = direction * 90 584 | log.Println("rotation change received:", deviceRotation) 585 | } else { 586 | rotation, er := androidutils.Rotation() 587 | if er != nil { 588 | log.Println("rotation auto get err:", er) 589 | http.Error(w, "Failure", 500) 590 | return 591 | } 592 | deviceRotation = rotation 593 | } 594 | 595 | // Kill not controled minicap 596 | killed := false 597 | procWalk(func(proc procfs.Proc) { 598 | executable, _ := proc.Executable() 599 | if filepath.Base(executable) != "minicap" { 600 | return 601 | } 602 | stat, err := proc.NewStat() 603 | if err != nil || stat.PPID != 1 { // only not controled minicap need killed 604 | return 605 | } 606 | if p, err := os.FindProcess(proc.PID); err == nil { 607 | log.Println("Kill", executable) 608 | p.Kill() 609 | killed = true 610 | } 611 | }) 612 | if killed { 613 | service.Start("minicap") 614 | } 615 | 616 | // minicapHub.broadcast <- []byte("rotation " + strconv.Itoa(deviceRotation)) 617 | updateMinicapRotation(deviceRotation) 618 | rotationPublisher.Submit(deviceRotation) 619 | 620 | // APK Service will send rotation to atx-agent when rotation changes 621 | runShellTimeout(5*time.Second, "am", "startservice", "--user", "0", "-n", "com.github.uiautomator/.Service") 622 | renderJSON(w, map[string]int{ 623 | "rotation": deviceRotation, 624 | }) 625 | // fmt.Fprintf(w, "rotation change to %d", deviceRotation) 626 | }) 627 | 628 | /* 629 | # URLRules: 630 | # URLPath ends with / means directory, eg: $DEVICE_URL/upload/sdcard/ 631 | # The rest means file, eg: $DEVICE_URL/upload/sdcard/a.txt 632 | # 633 | # Upload a file to destination 634 | $ curl -X POST -F file=@file.txt -F mode=0755 $DEVICE_URL/upload/sdcard/a.txt 635 | 636 | # Upload a directory (file must be zip), URLPath must ends with / 637 | $ curl -X POST -F file=@dir.zip -F dir=true $DEVICE_URL/upload/sdcard/atx-stuffs/ 638 | */ 639 | m.HandleFunc("/upload/{target:.*}", func(w http.ResponseWriter, r *http.Request) { 640 | target := mux.Vars(r)["target"] 641 | if runtime.GOOS != "windows" { 642 | target = "/" + target 643 | } 644 | isDir := r.FormValue("dir") == "true" 645 | var fileMode os.FileMode 646 | if _, err := fmt.Sscanf(r.FormValue("mode"), "%o", &fileMode); !isDir && err != nil { 647 | log.Printf("invalid file mode: %s", r.FormValue("mode")) 648 | fileMode = 0644 649 | } // %o base 8 650 | 651 | file, header, err := r.FormFile("file") 652 | if err != nil { 653 | http.Error(w, err.Error(), http.StatusBadRequest) 654 | return 655 | } 656 | defer func() { 657 | file.Close() 658 | r.MultipartForm.RemoveAll() 659 | }() 660 | 661 | var targetDir = target 662 | if !isDir { 663 | if strings.HasSuffix(target, "/") { 664 | target = path.Join(target, header.Filename) 665 | } 666 | targetDir = filepath.Dir(target) 667 | } else { 668 | if !strings.HasSuffix(target, "/") { 669 | http.Error(w, "URLPath must endswith / if upload a directory", 400) 670 | return 671 | } 672 | } 673 | if _, err := os.Stat(targetDir); os.IsNotExist(err) { 674 | os.MkdirAll(targetDir, 0755) 675 | } 676 | 677 | if isDir { 678 | err = archiver.Zip.Read(file, target) 679 | } else { 680 | err = copyToFile(file, target) 681 | } 682 | 683 | if err != nil { 684 | http.Error(w, err.Error(), http.StatusInternalServerError) 685 | return 686 | } 687 | if !isDir && fileMode != 0 { 688 | os.Chmod(target, fileMode) 689 | } 690 | if fileInfo, err := os.Stat(target); err == nil { 691 | fileMode = fileInfo.Mode() 692 | } 693 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 694 | json.NewEncoder(w).Encode(map[string]interface{}{ 695 | "target": target, 696 | "isDir": isDir, 697 | "mode": fmt.Sprintf("0%o", fileMode), 698 | }) 699 | }) 700 | 701 | m.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) { 702 | dst := r.FormValue("filepath") 703 | url := r.FormValue("url") 704 | var fileMode os.FileMode 705 | if _, err := fmt.Sscanf(r.FormValue("mode"), "%o", &fileMode); err != nil { 706 | log.Printf("invalid file mode: %s", r.FormValue("mode")) 707 | fileMode = 0644 708 | } // %o base 8 709 | key := background.HTTPDownload(url, dst, fileMode) 710 | io.WriteString(w, key) 711 | }).Methods("POST") 712 | 713 | m.HandleFunc("/download/{key}", func(w http.ResponseWriter, r *http.Request) { 714 | key := mux.Vars(r)["key"] 715 | status := background.Get(key) 716 | w.Header().Set("Content-Type", "application/json") 717 | json.NewEncoder(w).Encode(status) 718 | }).Methods("GET") 719 | 720 | m.HandleFunc("/packages", func(w http.ResponseWriter, r *http.Request) { 721 | var url = r.FormValue("url") 722 | filepath := TempFileName("/sdcard/tmp", ".apk") 723 | key := background.HTTPDownload(url, filepath, 0644) 724 | go func() { 725 | defer os.Remove(filepath) // release sdcard space 726 | 727 | state := background.Get(key) 728 | state.Status = "downloading" 729 | if err := background.Wait(key); err != nil { 730 | log.Println("http download error") 731 | state.Error = err.Error() 732 | state.Status = "failure" 733 | state.Message = "http download error" 734 | return 735 | } 736 | 737 | state.Status = "installing" 738 | if err := forceInstallAPK(filepath); err != nil { 739 | state.Error = err.Error() 740 | state.Status = "failure" 741 | } else { 742 | state.Status = "success" 743 | } 744 | }() 745 | renderJSON(w, map[string]interface{}{ 746 | "success": true, 747 | "data": map[string]string{ 748 | "id": key, 749 | }, 750 | }) 751 | }).Methods("POST") 752 | 753 | // id: int 754 | m.HandleFunc("/packages/{id}", func(w http.ResponseWriter, r *http.Request) { 755 | id := mux.Vars(r)["id"] 756 | state := background.Get(id) 757 | w.Header().Set("Content-Type", "application/json") 758 | data, _ := json.Marshal(state.Progress) 759 | renderJSON(w, map[string]interface{}{ 760 | "success": true, 761 | "data": map[string]string{ 762 | "status": state.Status, 763 | "description": string(data), 764 | }, 765 | }) 766 | json.NewEncoder(w).Encode(state) 767 | }).Methods("GET") 768 | 769 | m.HandleFunc("/packages", func(w http.ResponseWriter, r *http.Request) { 770 | pkgs, err := listPackages() 771 | if err != nil { 772 | w.WriteHeader(500) 773 | renderJSON(w, map[string]interface{}{ 774 | "success": false, 775 | "description": err.Error(), 776 | }) 777 | return 778 | } 779 | renderJSON(w, pkgs) 780 | }).Methods("GET") 781 | 782 | m.HandleFunc("/packages/{pkgname}/info", func(w http.ResponseWriter, r *http.Request) { 783 | pkgname := mux.Vars(r)["pkgname"] 784 | info, err := readPackageInfo(pkgname) 785 | if err != nil { 786 | renderJSON(w, map[string]interface{}{ 787 | "success": false, 788 | "description": err.Error(), // "package " + strconv.Quote(pkgname) + " not found", 789 | }) 790 | return 791 | } 792 | renderJSON(w, map[string]interface{}{ 793 | "success": true, 794 | "data": info, 795 | }) 796 | }) 797 | 798 | m.HandleFunc("/packages/{pkgname}/icon", func(w http.ResponseWriter, r *http.Request) { 799 | pkgname := mux.Vars(r)["pkgname"] 800 | info, err := readPackageInfo(pkgname) 801 | if err != nil { 802 | http.Error(w, "package not found", 403) 803 | return 804 | } 805 | if info.Icon == nil { 806 | http.Error(w, "package not found", 400) 807 | return 808 | } 809 | w.Header().Set("Content-Type", "image/jpeg") 810 | jpeg.Encode(w, info.Icon, &jpeg.Options{Quality: 80}) 811 | }) 812 | 813 | // deprecated 814 | m.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) { 815 | var url = r.FormValue("url") 816 | var tmpdir = r.FormValue("tmpdir") 817 | if tmpdir == "" { 818 | tmpdir = "/data/local/tmp" 819 | } 820 | 821 | filepath := TempFileName(tmpdir, ".apk") 822 | key := background.HTTPDownload(url, filepath, 0644) 823 | go func() { 824 | defer os.Remove(filepath) // release sdcard space 825 | 826 | state := background.Get(key) 827 | state.Status = "downloading" 828 | if err := background.Wait(key); err != nil { 829 | log.Println("http download error") 830 | state.Error = err.Error() 831 | state.Message = "http download error" 832 | state.Status = "failure" 833 | return 834 | } 835 | 836 | state.Message = "installing" 837 | state.Status = "installing" 838 | if err := forceInstallAPK(filepath); err != nil { 839 | state.Error = err.Error() 840 | state.Message = "error install" 841 | state.Status = "failure" 842 | } else { 843 | state.Message = "success installed" 844 | state.Status = "success" 845 | } 846 | }() 847 | io.WriteString(w, key) 848 | }).Methods("POST") 849 | 850 | // deprecated 851 | m.HandleFunc("/install/{id}", func(w http.ResponseWriter, r *http.Request) { 852 | id := mux.Vars(r)["id"] 853 | state := background.Get(id) 854 | w.Header().Set("Content-Type", "application/json") 855 | json.NewEncoder(w).Encode(state) 856 | }).Methods("GET") 857 | 858 | // deprecated 859 | m.HandleFunc("/install/{id}", func(w http.ResponseWriter, r *http.Request) { 860 | id := mux.Vars(r)["id"] 861 | state := background.Get(id) 862 | if state.Progress != nil { 863 | if dproxy, ok := state.Progress.(*downloadProxy); ok { 864 | dproxy.Cancel() 865 | io.WriteString(w, "Cancelled") 866 | return 867 | } 868 | } 869 | io.WriteString(w, "Unable to canceled") 870 | }).Methods("DELETE") 871 | 872 | // fix minitouch 873 | m.HandleFunc("/minitouch", func(w http.ResponseWriter, r *http.Request) { 874 | if err := installMinitouch(); err == nil { 875 | log.Println("update minitouch success") 876 | io.WriteString(w, "Update minitouch success") 877 | } else { 878 | http.Error(w, err.Error(), http.StatusInternalServerError) 879 | } 880 | }).Methods("PUT") 881 | 882 | m.HandleFunc("/minitouch", func(w http.ResponseWriter, r *http.Request) { 883 | service.Stop("minitouch", true) 884 | io.WriteString(w, "minitouch stopped") 885 | }).Methods("DELETE") 886 | 887 | m.HandleFunc("/minitouch", singleFightNewerWebsocket(func(w http.ResponseWriter, r *http.Request, ws *websocket.Conn) { 888 | defer ws.Close() 889 | const wsWriteWait = 10 * time.Second 890 | wsWrite := func(messageType int, data []byte) error { 891 | ws.SetWriteDeadline(time.Now().Add(wsWriteWait)) 892 | return ws.WriteMessage(messageType, data) 893 | } 894 | wsWrite(websocket.TextMessage, []byte("start @minitouch service")) 895 | if err := service.Start("minitouch"); err != nil && err != cmdctrl.ErrAlreadyRunning { 896 | wsWrite(websocket.TextMessage, []byte("@minitouch service start failed: "+err.Error())) 897 | return 898 | } 899 | unixSocketPath := minitouchSocketPath 900 | wsWrite(websocket.TextMessage, []byte("dial unix:"+unixSocketPath)) 901 | log.Printf("minitouch connection: %v", r.RemoteAddr) 902 | retries := 0 903 | quitC := make(chan bool, 2) 904 | operC := make(chan TouchRequest, 10) 905 | defer func() { 906 | wsWrite(websocket.TextMessage, []byte(unixSocketPath+" websocket closed")) 907 | close(operC) 908 | }() 909 | go func() { 910 | for { 911 | if retries > 10 { 912 | log.Printf("unix %s connect failed", unixSocketPath) 913 | wsWrite(websocket.TextMessage, []byte(unixSocketPath+" listen timeout, possibly minitouch not installed")) 914 | ws.Close() 915 | break 916 | } 917 | conn, err := net.Dial("unix", unixSocketPath) 918 | if err != nil { 919 | retries++ 920 | log.Printf("dial %s error: %v, wait 0.5s", unixSocketPath, err) 921 | select { 922 | case <-quitC: 923 | return 924 | case <-time.After(500 * time.Millisecond): 925 | } 926 | continue 927 | } 928 | log.Printf("unix %s connected, accepting requests", unixSocketPath) 929 | retries = 0 // connected, reset retries 930 | err = drainTouchRequests(conn, operC) 931 | conn.Close() 932 | if err != nil { 933 | log.Println("drain touch requests err:", err) 934 | } else { 935 | log.Printf("unix %s disconnected", unixSocketPath) 936 | break // operC closed 937 | } 938 | } 939 | }() 940 | var touchRequest TouchRequest 941 | for { 942 | err := ws.ReadJSON(&touchRequest) 943 | if err != nil { 944 | log.Println("readJson err:", err) 945 | quitC <- true 946 | break 947 | } 948 | select { 949 | case operC <- touchRequest: 950 | case <-time.After(2 * time.Second): 951 | wsWrite(websocket.TextMessage, []byte("touch request buffer full")) 952 | } 953 | } 954 | })).Methods("GET") 955 | 956 | // fix minicap 957 | m.HandleFunc("/minicap", func(w http.ResponseWriter, r *http.Request) { 958 | if err := installMinicap(); err == nil { 959 | log.Println("update minicap success") 960 | io.WriteString(w, "Update minicap success") 961 | } else { 962 | http.Error(w, err.Error(), http.StatusInternalServerError) 963 | } 964 | }).Methods("PUT") 965 | 966 | minicapHandler := broadcastWebsocket() 967 | m.HandleFunc("/minicap/broadcast", minicapHandler).Methods("GET") 968 | m.HandleFunc("/minicap", minicapHandler).Methods("GET") 969 | 970 | // TODO(ssx): perfer to delete 971 | // FIXME(ssx): screenrecord is not good enough, need to change later 972 | var recordCmd *exec.Cmd 973 | var recordDone = make(chan bool, 1) 974 | var recordLock sync.Mutex 975 | var recordFolder = "/sdcard/screenrecords/" 976 | var recordRunning = false 977 | 978 | m.HandleFunc("/screenrecord", func(w http.ResponseWriter, r *http.Request) { 979 | recordLock.Lock() 980 | defer recordLock.Unlock() 981 | 982 | if recordCmd != nil { 983 | http.Error(w, "screenrecord not closed", 400) 984 | return 985 | } 986 | os.RemoveAll(recordFolder) 987 | os.MkdirAll(recordFolder, 0755) 988 | recordCmd = exec.Command("screenrecord", recordFolder+"0.mp4") 989 | if err := recordCmd.Start(); err != nil { 990 | http.Error(w, err.Error(), 500) 991 | return 992 | } 993 | recordRunning = true 994 | go func() { 995 | for i := 1; recordCmd.Wait() == nil && i <= 20 && recordRunning; i++ { // set limit, to prevent too many videos. max 1 hour 996 | recordCmd = exec.Command("screenrecord", recordFolder+strconv.Itoa(i)+".mp4") 997 | if err := recordCmd.Start(); err != nil { 998 | log.Println("screenrecord error:", err) 999 | break 1000 | } 1001 | } 1002 | recordDone <- true 1003 | }() 1004 | io.WriteString(w, "screenrecord started") 1005 | }).Methods("POST") 1006 | 1007 | m.HandleFunc("/screenrecord", func(w http.ResponseWriter, r *http.Request) { 1008 | recordLock.Lock() 1009 | defer recordLock.Unlock() 1010 | 1011 | recordRunning = false 1012 | if recordCmd != nil { 1013 | if recordCmd.Process != nil { 1014 | recordCmd.Process.Signal(os.Interrupt) 1015 | } 1016 | select { 1017 | case <-recordDone: 1018 | case <-time.After(5 * time.Second): 1019 | // force kill 1020 | exec.Command("pkill", "screenrecord").Run() 1021 | } 1022 | recordCmd = nil 1023 | } 1024 | w.Header().Set("Content-Type", "application/json") 1025 | files, _ := ioutil.ReadDir(recordFolder) 1026 | videos := []string{} 1027 | for i := 0; i < len(files); i++ { 1028 | videos = append(videos, fmt.Sprintf(recordFolder+"%d.mp4", i)) 1029 | } 1030 | json.NewEncoder(w).Encode(map[string]interface{}{ 1031 | "videos": videos, 1032 | }) 1033 | }).Methods("PUT") 1034 | 1035 | m.HandleFunc("/upgrade", func(w http.ResponseWriter, r *http.Request) { 1036 | ver := r.FormValue("version") 1037 | var err error 1038 | if ver == "" { 1039 | ver, err = getLatestVersion() 1040 | if err != nil { 1041 | http.Error(w, err.Error(), 500) 1042 | return 1043 | } 1044 | } 1045 | if ver == version { 1046 | io.WriteString(w, "current version is already "+version) 1047 | return 1048 | } 1049 | err = doUpdate(ver) 1050 | if err != nil { 1051 | http.Error(w, err.Error(), 500) 1052 | return 1053 | } 1054 | io.WriteString(w, "update finished, restarting") 1055 | go func() { 1056 | log.Printf("restarting server") 1057 | // TODO(ssx): runDaemon() 1058 | }() 1059 | }) 1060 | 1061 | m.HandleFunc("/term", func(w http.ResponseWriter, r *http.Request) { 1062 | if r.Header.Get("Upgrade") == "websocket" { 1063 | handleTerminalWebsocket(w, r) 1064 | return 1065 | } 1066 | renderHTML(w, "terminal.html") 1067 | }) 1068 | 1069 | screenshotIndex := -1 1070 | nextScreenshotFilename := func() string { 1071 | targetFolder := "/data/local/tmp/minicap-images" 1072 | if _, err := os.Stat(targetFolder); err != nil { 1073 | os.MkdirAll(targetFolder, 0755) 1074 | } 1075 | screenshotIndex = (screenshotIndex + 1) % 5 1076 | return filepath.Join(targetFolder, fmt.Sprintf("%d.jpg", screenshotIndex)) 1077 | } 1078 | 1079 | m.HandleFunc("/screenshot", func(w http.ResponseWriter, r *http.Request) { 1080 | targetURL := "/screenshot/0" 1081 | if r.URL.RawQuery != "" { 1082 | targetURL += "?" + r.URL.RawQuery 1083 | } 1084 | http.Redirect(w, r, targetURL, 302) 1085 | }).Methods("GET") 1086 | 1087 | m.Handle("/jsonrpc/0", uiautomatorProxy) 1088 | m.Handle("/ping", uiautomatorProxy) 1089 | m.HandleFunc("/screenshot/0", func(w http.ResponseWriter, r *http.Request) { 1090 | download := r.FormValue("download") 1091 | if download != "" { 1092 | w.Header().Set("Content-Disposition", "attachment; filename="+download) 1093 | } 1094 | 1095 | thumbnailSize := r.FormValue("thumbnail") 1096 | filename := nextScreenshotFilename() 1097 | 1098 | // android emulator use screencap 1099 | // then minicap when binary and .so exists 1100 | // then uiautomator when service(uiautomator) is running 1101 | // last screencap 1102 | 1103 | method := "screencap" 1104 | if getCachedProperty("ro.product.cpu.abi") == "x86" { // android emulator 1105 | method = "screencap" 1106 | } else if fileExists("/data/local/tmp/minicap") && fileExists("/data/local/tmp/minicap.so") && r.FormValue("minicap") != "false" && strings.ToLower(getCachedProperty("ro.product.manufacturer")) != "meizu" { 1107 | method = "minicap" 1108 | } else if service.Running("uiautomator") { 1109 | method = "uiautomator" 1110 | } 1111 | 1112 | var err error 1113 | switch method { 1114 | case "screencap": 1115 | err = screenshotWithScreencap(filename) 1116 | case "minicap": 1117 | err = screenshotWithMinicap(filename, thumbnailSize) 1118 | case "uiautomator": 1119 | uiautomatorProxy.ServeHTTP(w, r) 1120 | return 1121 | } 1122 | if err != nil && method != "screencap" { 1123 | method = "screencap" 1124 | err = screenshotWithScreencap(filename) 1125 | } 1126 | if err != nil { 1127 | http.Error(w, err.Error(), 500) 1128 | return 1129 | } 1130 | w.Header().Set("X-Screenshot-Method", method) 1131 | http.ServeFile(w, r, filename) 1132 | }) 1133 | 1134 | m.HandleFunc("/wlan/ip", func(w http.ResponseWriter, r *http.Request) { 1135 | itf, err := net.InterfaceByName("wlan0") 1136 | if err != nil { 1137 | http.Error(w, err.Error(), http.StatusInternalServerError) 1138 | return 1139 | } 1140 | addrs, err := itf.Addrs() 1141 | if err != nil { 1142 | http.Error(w, err.Error(), http.StatusInternalServerError) 1143 | return 1144 | } 1145 | for _, addr := range addrs { 1146 | if v, ok := addr.(*net.IPNet); ok { 1147 | io.WriteString(w, v.IP.String()) 1148 | } 1149 | return 1150 | } 1151 | http.Error(w, "wlan0 have no ip address", 500) 1152 | }) 1153 | m.Handle("/assets/{(.*)}", http.StripPrefix("/assets", http.FileServer(Assets))) 1154 | 1155 | var handler = cors.New(cors.Options{ 1156 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, 1157 | }).Handler(m) 1158 | // logHandler := handlers.LoggingHandler(os.Stdout, handler) 1159 | server.httpServer = &http.Server{Handler: handler} // url(/stop) need it. 1160 | } 1161 | 1162 | func (s *Server) Serve(lis net.Listener) error { 1163 | return s.httpServer.Serve(lis) 1164 | } 1165 | -------------------------------------------------------------------------------- /hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | type Hub struct { 15 | clients map[*Client]bool // Registered clients. 16 | broadcast chan []byte // Inbound messages from the clients. 17 | register chan *Client // Register requests from the clients. 18 | unregister chan *Client // Unregister requests from clients. 19 | } 20 | 21 | func newHub() *Hub { 22 | return &Hub{ 23 | broadcast: make(chan []byte, 10), 24 | register: make(chan *Client), 25 | unregister: make(chan *Client), 26 | clients: make(map[*Client]bool), 27 | } 28 | } 29 | 30 | func (h *Hub) _startTranslate(ctx context.Context) { 31 | h.broadcast <- []byte("welcome") 32 | if minicapSocketPath == "@minicap" { 33 | service.Start("minicap") 34 | } 35 | 36 | log.Printf("Receive images from %s", minicapSocketPath) 37 | retries := 0 38 | for { 39 | if retries > 10 { 40 | log.Printf("unix %s connect failed", minicapSocketPath) 41 | h.broadcast <- []byte("@minicapagent listen timeout") 42 | break 43 | } 44 | 45 | conn, err := net.Dial("unix", minicapSocketPath) 46 | if err != nil { 47 | retries++ 48 | log.Printf("dial %s err: %v, wait 0.5s", minicapSocketPath, err) 49 | select { 50 | case <-ctx.Done(): 51 | return 52 | case <-time.After(500 * time.Millisecond): 53 | } 54 | continue 55 | } 56 | 57 | retries = 0 // connected, reset retries 58 | if er := translateMinicap(conn, h.broadcast, ctx); er == nil { 59 | conn.Close() 60 | log.Println("transfer closed") 61 | break 62 | } else { 63 | conn.Close() 64 | log.Println("translateMinicap error:", er) //scrcpy read error, try to read again") 65 | } 66 | } 67 | } 68 | 69 | func (h *Hub) run() { 70 | var cancel context.CancelFunc 71 | var ctx context.Context 72 | 73 | for { 74 | select { 75 | case client := <-h.register: 76 | h.clients[client] = true 77 | log.Println("new broadcast client") 78 | h.broadcast <- []byte("rotation " + strconv.Itoa(deviceRotation)) 79 | if len(h.clients) == 1 { 80 | ctx, cancel = context.WithCancel(context.Background()) 81 | go h._startTranslate(ctx) 82 | } 83 | case client := <-h.unregister: 84 | if _, ok := h.clients[client]; ok { 85 | delete(h.clients, client) 86 | close(client.send) 87 | } 88 | if len(h.clients) == 0 { 89 | log.Println("All client quited, context stop minicap service") 90 | cancel() 91 | } 92 | case message := <-h.broadcast: 93 | for client := range h.clients { 94 | select { 95 | case client.send <- message: 96 | default: 97 | close(client.send) 98 | delete(h.clients, client) 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | // Client is a middleman between the websocket connection and the hub. 106 | type Client struct { 107 | hub *Hub 108 | conn *websocket.Conn // The websocket connection. 109 | send chan []byte // Buffered channel of outbound messages. 110 | } 111 | 112 | // writePump pumps messages from the hub to the websocket connection. 113 | // 114 | // A goroutine running writePump is started for each connection. The 115 | // application ensures that there is at most one writer to a connection by 116 | // executing all writes from this goroutine. 117 | func (c *Client) writePump() { 118 | ticker := time.NewTicker(time.Second * 10) 119 | defer func() { 120 | ticker.Stop() 121 | c.conn.Close() 122 | }() 123 | for { 124 | var err error 125 | select { 126 | case data, ok := <-c.send: 127 | c.conn.SetWriteDeadline(time.Now().Add(time.Second * 10)) 128 | if !ok { 129 | // The hub closed the channel. 130 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 131 | return 132 | } 133 | if string(data[:2]) == "\xff\xd8" || string(data[:4]) == "\x89PNG" { // jpg or png data 134 | err = c.conn.WriteMessage(websocket.BinaryMessage, data) 135 | } else { 136 | err = c.conn.WriteMessage(websocket.TextMessage, data) 137 | } 138 | case <-ticker.C: 139 | // err = c.conn.WriteMessage(websocket.PingMessage, nil) 140 | } 141 | if err != nil { 142 | log.Println(err) 143 | break 144 | } 145 | } 146 | } 147 | 148 | // readPump pumps messages from the websocket connection to the hub. 149 | // 150 | // The application runs readPump in a per-connection goroutine. The application 151 | // ensures that there is at most one reader on a connection by executing all 152 | // reads from this goroutine. 153 | func (c *Client) readPump() { 154 | defer func() { 155 | c.hub.unregister <- c 156 | c.conn.Close() 157 | }() 158 | // c.conn.SetReadLimit(maxMessageSize) 159 | // c.conn.SetReadDeadline(time.Now().Add(pongWait)) 160 | // c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 161 | for { 162 | _, message, err := c.conn.ReadMessage() 163 | if err != nil { 164 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 165 | log.Printf("error: %v", err) 166 | } 167 | break 168 | } 169 | log.Println("websocket recv message", string(message)) 170 | // message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 171 | // c.hub.broadcast <- message 172 | } 173 | } 174 | 175 | func broadcastWebsocket() func(http.ResponseWriter, *http.Request) { 176 | hub := newHub() 177 | go hub.run() // start read images from unix:@minicap 178 | 179 | return func(w http.ResponseWriter, r *http.Request) { 180 | conn, err := upgrader.Upgrade(w, r, nil) 181 | if err != nil { 182 | log.Println(err) 183 | return 184 | } 185 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} 186 | hub.register <- client 187 | 188 | done := make(chan bool) 189 | go client.writePump() 190 | go func() { 191 | client.readPump() 192 | done <- true 193 | }() 194 | go func() { 195 | ch := make(chan interface{}) 196 | rotationPublisher.Register(ch) 197 | defer rotationPublisher.Unregister(ch) 198 | for { 199 | select { 200 | case <-done: 201 | return 202 | case r := <-ch: 203 | hub.broadcast <- []byte(fmt.Sprintf("rotation %v", r)) 204 | } 205 | } 206 | }() 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /jsonrpc/json2.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/levigross/grequests" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type ErrorCode int 13 | 14 | const ( 15 | E_PARSE ErrorCode = -32700 16 | E_INVALID_REQ ErrorCode = -32600 17 | E_NO_METHOD ErrorCode = -32601 18 | E_BAD_PARAMS ErrorCode = -32602 19 | E_INTERNAL ErrorCode = -32603 20 | E_SERVER ErrorCode = -32000 21 | ) 22 | 23 | const JSONRPC_VERSION = "2.0" 24 | 25 | type Request struct { 26 | Version string `json:"jsonrpc"` 27 | ID int64 `json:"id"` 28 | Method string `json:"method"` 29 | Params interface{} `json:"params,omitempty"` 30 | } 31 | 32 | type Response struct { 33 | Version string `json:"jsonrpc"` 34 | ID int64 `json:"id"` 35 | Result *json.RawMessage `json:"result,omitempty"` 36 | Error *json.RawMessage `json:"error,omitempty"` 37 | } 38 | 39 | type RPCError struct { 40 | Code ErrorCode `json:"code"` 41 | Message string `json:"message"` 42 | Data interface{} `json:"data,omitempty"` 43 | } 44 | 45 | func (re *RPCError) Error() string { 46 | return fmt.Sprintf("code:%d message:%s data:%v", re.Code, re.Message, re.Data) 47 | } 48 | 49 | func NewRequest(method string, params ...interface{}) *Request { 50 | return &Request{ 51 | Version: JSONRPC_VERSION, 52 | ID: time.Now().Unix(), 53 | Method: method, 54 | Params: params, 55 | } 56 | } 57 | 58 | type Client struct { 59 | URL string 60 | Timeout time.Duration 61 | ErrorCallback func() error 62 | ErrorFixTimeout time.Duration 63 | ServerOK func() bool 64 | } 65 | 66 | func NewClient(url string) *Client { 67 | return &Client{ 68 | URL: url, 69 | Timeout: 60 * time.Second, 70 | } 71 | } 72 | 73 | func (r *Client) Call(method string, params ...interface{}) (resp *Response, err error) { 74 | // timeout maybe no needed 75 | gres, err := grequests.Post(r.URL, &grequests.RequestOptions{ 76 | RequestTimeout: r.Timeout, 77 | JSON: NewRequest(method, params...), 78 | }) 79 | if err != nil { 80 | return 81 | } 82 | if gres.Error != nil { 83 | err = gres.Error 84 | return 85 | } 86 | resp = new(Response) 87 | if err = gres.JSON(resp); err != nil { 88 | return 89 | } 90 | if resp.Error != nil { 91 | rpcErr := &RPCError{} 92 | if er := json.Unmarshal(*resp.Error, rpcErr); er != nil { 93 | err = &RPCError{ 94 | Code: E_SERVER, 95 | Message: string(*resp.Error), 96 | } 97 | return 98 | } 99 | err = rpcErr 100 | } 101 | return 102 | } 103 | 104 | func (r *Client) RobustCall(method string, params ...interface{}) (resp *Response, err error) { 105 | resp, err = r.Call(method, params...) 106 | if err == nil { 107 | return 108 | } 109 | if r.ErrorCallback == nil || r.ErrorCallback() != nil { 110 | return 111 | } 112 | 113 | start := time.Now() 114 | for { 115 | if time.Now().Sub(start) > r.ErrorFixTimeout { 116 | return 117 | } 118 | if r.ServerOK != nil && !r.ServerOK() { 119 | err = errors.New("jsonrpc server is down, auto-recover failed") 120 | return 121 | } 122 | time.Sleep(1 * time.Second) 123 | resp, err = r.Call(method, params...) 124 | if err == nil { 125 | return 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // import 4 | import ( 5 | 6 | // "github.com/qiniu/log" 7 | "github.com/sirupsen/logrus" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | ) 10 | 11 | var Default *logrus.Logger 12 | 13 | func init() { 14 | // Default = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile) 15 | Default = logrus.New() 16 | // logrus.Rep 17 | Default.SetLevel(logrus.DebugLevel) 18 | } 19 | 20 | func SetOutputFile(filename string) error { 21 | // f, err := os.Create(filename) 22 | // if err != nil { 23 | // return err 24 | // } 25 | Default.SetOutput(&lumberjack.Logger{ 26 | Filename: filename, 27 | MaxSize: 100, // megabytes 28 | MaxBackups: 3, 29 | MaxAge: 1, //days 30 | Compress: true, // disabled by default 31 | }) 32 | return nil 33 | // Default = log.New(out, "", log.LstdFlags|log.Lshortfile) 34 | // Default.SetOutputLevel(log.Ldebug) 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/binary" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | syslog "log" 13 | "net" 14 | "net/http" 15 | "net/http/httputil" 16 | "os" 17 | "os/exec" 18 | "os/signal" 19 | "os/user" 20 | "path/filepath" 21 | "strconv" 22 | "strings" 23 | "sync" 24 | "syscall" 25 | "time" 26 | 27 | "github.com/alecthomas/kingpin" 28 | "github.com/dustin/go-broadcast" 29 | "github.com/gorilla/websocket" 30 | "github.com/openatx/androidutils" 31 | "github.com/openatx/atx-agent/cmdctrl" 32 | "github.com/openatx/atx-agent/logger" 33 | "github.com/openatx/atx-agent/subcmd" 34 | "github.com/pkg/errors" 35 | "github.com/sevlyar/go-daemon" 36 | ) 37 | 38 | var ( 39 | service = cmdctrl.New() 40 | downManager = newDownloadManager() 41 | upgrader = websocket.Upgrader{ 42 | ReadBufferSize: 1024, 43 | WriteBufferSize: 1024, 44 | CheckOrigin: func(r *http.Request) bool { 45 | return true 46 | }, 47 | } 48 | 49 | version = "dev" 50 | owner = "openatx" 51 | repo = "atx-agent" 52 | listenAddr string 53 | daemonLogPath = "/sdcard/atx-agent.daemon.log" 54 | 55 | rotationPublisher = broadcast.NewBroadcaster(1) 56 | minicapSocketPath = "@minicap" 57 | minitouchSocketPath = "@minitouch" 58 | log = logger.Default 59 | ) 60 | 61 | const ( 62 | apkVersionCode = 4 63 | apkVersionName = "1.0.4" 64 | ) 65 | 66 | // singleFight for http request 67 | // - minicap 68 | // - minitouch 69 | var muxMutex = sync.Mutex{} 70 | var muxLocks = make(map[string]bool) 71 | var muxConns = make(map[string]*websocket.Conn) 72 | 73 | func singleFightWrap(handleFunc func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 74 | return func(w http.ResponseWriter, r *http.Request) { 75 | muxMutex.Lock() 76 | if _, ok := muxLocks[r.RequestURI]; ok { 77 | muxMutex.Unlock() 78 | log.Println("singlefight conflict", r.RequestURI) 79 | http.Error(w, "singlefight conflicts", http.StatusTooManyRequests) // code: 429 80 | return 81 | } 82 | muxLocks[r.RequestURI] = true 83 | muxMutex.Unlock() 84 | 85 | handleFunc(w, r) // handle requests 86 | 87 | muxMutex.Lock() 88 | delete(muxLocks, r.RequestURI) 89 | muxMutex.Unlock() 90 | } 91 | } 92 | 93 | func singleFightNewerWebsocket(handleFunc func(http.ResponseWriter, *http.Request, *websocket.Conn)) func(http.ResponseWriter, *http.Request) { 94 | return func(w http.ResponseWriter, r *http.Request) { 95 | muxMutex.Lock() 96 | if oldWs, ok := muxConns[r.RequestURI]; ok { 97 | oldWs.Close() 98 | delete(muxConns, r.RequestURI) 99 | } 100 | 101 | wsConn, err := upgrader.Upgrade(w, r, nil) 102 | if err != nil { 103 | http.Error(w, "websocket upgrade error", 500) 104 | muxMutex.Unlock() 105 | return 106 | } 107 | muxConns[r.RequestURI] = wsConn 108 | muxMutex.Unlock() 109 | 110 | handleFunc(w, r, wsConn) // handle request 111 | 112 | muxMutex.Lock() 113 | if muxConns[r.RequestURI] == wsConn { // release connection 114 | delete(muxConns, r.RequestURI) 115 | } 116 | muxMutex.Unlock() 117 | } 118 | } 119 | 120 | // Get preferred outbound ip of this machine 121 | func getOutboundIP() (ip net.IP, err error) { 122 | conn, err := net.Dial("udp", "8.8.8.8:80") 123 | if err != nil { 124 | return 125 | } 126 | defer conn.Close() 127 | 128 | localAddr := conn.LocalAddr().(*net.UDPAddr) 129 | return localAddr.IP, nil 130 | } 131 | 132 | func mustGetOoutboundIP() net.IP { 133 | ip, err := getOutboundIP() 134 | if err != nil { 135 | return net.ParseIP("127.0.0.1") 136 | // panic(err) 137 | } 138 | return ip 139 | } 140 | 141 | func renderJSON(w http.ResponseWriter, data interface{}) { 142 | js, err := json.Marshal(data) 143 | if err != nil { 144 | http.Error(w, err.Error(), http.StatusInternalServerError) 145 | return 146 | } 147 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 148 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(js))) 149 | w.Write(js) 150 | } 151 | 152 | func cmdError2Code(err error) int { 153 | if err == nil { 154 | return 0 155 | } 156 | if exiterr, ok := err.(*exec.ExitError); ok { 157 | // The program has exited with an exit code != 0 158 | 159 | // This works on both Unix and Windows. Although package 160 | // syscall is generally platform dependent, WaitStatus is 161 | // defined for both Unix and Windows and in both cases has 162 | // an ExitStatus() method with the same signature. 163 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 164 | return status.ExitStatus() 165 | } 166 | } 167 | return 128 168 | } 169 | 170 | func GoFunc(f func() error) chan error { 171 | ch := make(chan error) 172 | go func() { 173 | ch <- f() 174 | }() 175 | return ch 176 | } 177 | 178 | type MinicapInfo struct { 179 | Width int `json:"width"` 180 | Height int `json:"height"` 181 | Rotation int `json:"rotation"` 182 | Density float32 `json:"density"` 183 | } 184 | 185 | var ( 186 | deviceRotation int 187 | displayMaxWidthHeight = 800 188 | ) 189 | 190 | func updateMinicapRotation(rotation int) { 191 | running := service.Running("minicap") 192 | if running { 193 | service.Stop("minicap") 194 | killProcessByName("minicap") // kill not controlled minicap 195 | } 196 | devInfo := getDeviceInfo() 197 | width, height := devInfo.Display.Width, devInfo.Display.Height 198 | service.UpdateArgs("minicap", "/data/local/tmp/minicap", "-S", "-P", 199 | fmt.Sprintf("%dx%d@%dx%d/%d", width, height, displayMaxWidthHeight, displayMaxWidthHeight, rotation)) 200 | if running { 201 | service.Start("minicap") 202 | } 203 | } 204 | 205 | func checkUiautomatorInstalled() (ok bool) { 206 | pi, err := androidutils.StatPackage("com.github.uiautomator") 207 | if err != nil { 208 | return 209 | } 210 | if pi.Version.Code < apkVersionCode { 211 | return 212 | } 213 | _, err = androidutils.StatPackage("com.github.uiautomator.test") 214 | return err == nil 215 | } 216 | 217 | type DownloadManager struct { 218 | db map[string]*downloadProxy 219 | mu sync.Mutex 220 | n int 221 | } 222 | 223 | func newDownloadManager() *DownloadManager { 224 | return &DownloadManager{ 225 | db: make(map[string]*downloadProxy, 10), 226 | } 227 | } 228 | 229 | func (m *DownloadManager) Get(id string) *downloadProxy { 230 | m.mu.Lock() 231 | defer m.mu.Unlock() 232 | return m.db[id] 233 | } 234 | 235 | func (m *DownloadManager) Put(di *downloadProxy) (id string) { 236 | m.mu.Lock() 237 | defer m.mu.Unlock() 238 | m.n += 1 239 | id = strconv.Itoa(m.n) 240 | m.db[id] = di 241 | // di.Id = id 242 | return id 243 | } 244 | 245 | func (m *DownloadManager) Del(id string) { 246 | m.mu.Lock() 247 | defer m.mu.Unlock() 248 | delete(m.db, id) 249 | } 250 | 251 | func (m *DownloadManager) DelayDel(id string, sleep time.Duration) { 252 | go func() { 253 | time.Sleep(sleep) 254 | m.Del(id) 255 | }() 256 | } 257 | 258 | func currentUserName() string { 259 | if u, err := user.Current(); err == nil { 260 | return u.Name 261 | } 262 | if name := os.Getenv("USER"); name != "" { 263 | return name 264 | } 265 | output, err := exec.Command("whoami").Output() 266 | if err == nil { 267 | return strings.TrimSpace(string(output)) 268 | } 269 | return "" 270 | } 271 | 272 | func renderHTML(w http.ResponseWriter, filename string) { 273 | file, err := Assets.Open(filename) 274 | if err != nil { 275 | http.Error(w, "404 page not found", 404) 276 | return 277 | } 278 | content, _ := ioutil.ReadAll(file) 279 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 280 | w.Header().Set("Content-Length", strconv.Itoa(len(content))) 281 | w.Write(content) 282 | } 283 | 284 | var ( 285 | ErrJpegWrongFormat = errors.New("jpeg format error, not starts with 0xff,0xd8") 286 | 287 | // target, _ := url.Parse("http://127.0.0.1:9008") 288 | // uiautomatorProxy := httputil.NewSingleHostReverseProxy(target) 289 | 290 | uiautomatorTimer = NewSafeTimer(time.Hour * 3) 291 | 292 | uiautomatorProxy = &httputil.ReverseProxy{ 293 | Director: func(req *http.Request) { 294 | req.URL.RawQuery = "" // ignore http query 295 | req.URL.Scheme = "http" 296 | req.URL.Host = "127.0.0.1:9008" 297 | 298 | if req.URL.Path == "/jsonrpc/0" { 299 | uiautomatorTimer.Reset() 300 | } 301 | }, 302 | Transport: &http.Transport{ 303 | // Ref: https://golang.org/pkg/net/http/#RoundTripper 304 | Dial: func(network, addr string) (net.Conn, error) { 305 | conn, err := (&net.Dialer{ 306 | Timeout: 5 * time.Second, 307 | KeepAlive: 30 * time.Second, 308 | DualStack: true, 309 | }).Dial(network, addr) 310 | return conn, err 311 | }, 312 | MaxIdleConns: 100, 313 | IdleConnTimeout: 180 * time.Second, 314 | TLSHandshakeTimeout: 10 * time.Second, 315 | ExpectContinueTimeout: 1 * time.Second, 316 | }, 317 | } 318 | ) 319 | 320 | type errorBinaryReader struct { 321 | rd io.Reader 322 | err error 323 | } 324 | 325 | func (r *errorBinaryReader) ReadInto(datas ...interface{}) error { 326 | if r.err != nil { 327 | return r.err 328 | } 329 | for _, data := range datas { 330 | r.err = binary.Read(r.rd, binary.LittleEndian, data) 331 | if r.err != nil { 332 | return r.err 333 | } 334 | } 335 | return nil 336 | } 337 | 338 | // read from @minicap and send jpeg raw data to channel 339 | func translateMinicap(conn net.Conn, jpgC chan []byte, ctx context.Context) error { 340 | var pid, rw, rh, vw, vh uint32 341 | var version, unused, orientation, quirkFlag uint8 342 | rd := bufio.NewReader(conn) 343 | binRd := errorBinaryReader{rd: rd} 344 | err := binRd.ReadInto(&version, &unused, &pid, &rw, &rh, &vw, &vh, &orientation, &quirkFlag) 345 | if err != nil { 346 | return err 347 | } 348 | for { 349 | var size uint32 350 | if err = binRd.ReadInto(&size); err != nil { 351 | break 352 | } 353 | 354 | lr := &io.LimitedReader{R: rd, N: int64(size)} 355 | buf := bytes.NewBuffer(nil) 356 | _, err = io.Copy(buf, lr) 357 | if err != nil { 358 | break 359 | } 360 | if string(buf.Bytes()[:2]) != "\xff\xd8" { 361 | err = ErrJpegWrongFormat 362 | break 363 | } 364 | select { 365 | case jpgC <- buf.Bytes(): // Maybe should use buffer instead 366 | case <-ctx.Done(): 367 | return nil 368 | default: 369 | // TODO(ssx): image should not wait or it will stuck here 370 | } 371 | } 372 | return err 373 | } 374 | 375 | func runDaemon() (cntxt *daemon.Context) { 376 | cntxt = &daemon.Context{ // remove pid to prevent resource busy 377 | PidFilePerm: 0644, 378 | LogFilePerm: 0640, 379 | WorkDir: "./", 380 | Umask: 022, 381 | } 382 | // log might be no auth 383 | if f, err := os.OpenFile(daemonLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil { // |os.O_APPEND 384 | f.Close() 385 | cntxt.LogFileName = daemonLogPath 386 | } 387 | 388 | child, err := cntxt.Reborn() 389 | if err != nil { 390 | log.Fatal("Unale to run: ", err) 391 | } 392 | if child != nil { 393 | return nil // return nil indicate program run in parent 394 | } 395 | return cntxt 396 | } 397 | 398 | func setupLogrotate() { 399 | logger.SetOutputFile("/sdcard/atx-agent.log") 400 | } 401 | 402 | func stopSelf() { 403 | // kill previous daemon first 404 | log.Println("stop server self") 405 | 406 | listenPort, _ := strconv.Atoi(strings.Split(listenAddr, ":")[1]) 407 | client := http.Client{Timeout: 3 * time.Second} 408 | _, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/stop", listenPort)) 409 | if err == nil { 410 | log.Println("wait server stopped") 411 | time.Sleep(500 * time.Millisecond) // server will quit in 0.5s 412 | } else { 413 | log.Println("already stopped") 414 | } 415 | 416 | // to make sure stopped 417 | killAgentProcess() 418 | } 419 | 420 | func init() { 421 | syslog.SetFlags(syslog.Lshortfile | syslog.LstdFlags) 422 | 423 | // Set timezone. 424 | // 425 | // Note that Android zoneinfo is stored in /system/usr/share/zoneinfo, 426 | // but it is in some kind of packed TZiff file that we do not support 427 | // yet. To make it simple, we use FixedZone instead 428 | zones := map[string]int{ 429 | "Asia/Shanghai": 8, 430 | "CST": 8, // China Standard Time 431 | } 432 | tz := getCachedProperty("persist.sys.timezone") 433 | if tz != "" { 434 | offset, ok := zones[tz] 435 | if !ok { 436 | // get offset from date command, example date output: +0800\n 437 | output, _ := runShell("date", "+%z") 438 | if len(output) != 6 { 439 | return 440 | } 441 | offset, _ = strconv.Atoi(string(output[1:3])) 442 | if output[0] == '-' { 443 | offset *= -1 444 | } 445 | } 446 | time.Local = time.FixedZone(tz, offset*3600) 447 | } 448 | } 449 | 450 | // lazyInit will be called in func:main 451 | func lazyInit() { 452 | // watch rotation and send to rotatinPublisher 453 | go _watchRotation() 454 | if !isMinicapSupported() { 455 | minicapSocketPath = "@minicapagent" 456 | } 457 | 458 | if !fileExists("/data/local/tmp/minitouch") { 459 | minitouchSocketPath = "@minitouchagent" 460 | } else if sdk, _ := strconv.Atoi(getCachedProperty("ro.build.version.sdk")); sdk > 28 { // Android Q.. 461 | minitouchSocketPath = "@minitouchagent" 462 | } 463 | } 464 | 465 | func _watchRotation() { 466 | for { 467 | conn, err := net.Dial("unix", "@rotationagent") 468 | if err != nil { 469 | time.Sleep(2 * time.Second) 470 | continue 471 | } 472 | func() { 473 | defer conn.Close() 474 | scanner := bufio.NewScanner(conn) 475 | for scanner.Scan() { 476 | rotation, err := strconv.Atoi(scanner.Text()) 477 | if err != nil { 478 | continue 479 | } 480 | deviceRotation = rotation 481 | if minicapSocketPath == "@minicap" { 482 | updateMinicapRotation(deviceRotation) 483 | } 484 | rotationPublisher.Submit(rotation) 485 | log.Println("Rotation -->", rotation) 486 | } 487 | }() 488 | time.Sleep(1 * time.Second) 489 | } 490 | } 491 | 492 | func killAgentProcess() error { 493 | // kill process by process cmdline 494 | procs, err := listAllProcs() 495 | if err != nil { 496 | return err 497 | } 498 | for _, p := range procs { 499 | if os.Getpid() == p.Pid { 500 | // do not kill self 501 | continue 502 | } 503 | if len(p.Cmdline) >= 2 { 504 | // cmdline: /data/local/tmp/atx-agent server -d 505 | if filepath.Base(p.Cmdline[0]) == "atx-agent" && p.Cmdline[1] == "server" { 506 | log.Infof("kill running atx-agent (pid=%d)", p.Pid) 507 | p.Kill() 508 | } 509 | } 510 | } 511 | return nil 512 | } 513 | 514 | func main() { 515 | kingpin.Version(version) 516 | kingpin.CommandLine.HelpFlag.Short('h') 517 | kingpin.CommandLine.VersionFlag.Short('v') 518 | 519 | // CMD: curl 520 | cmdCurl := kingpin.Command("curl", "curl command") 521 | subcmd.RegisterCurl(cmdCurl) 522 | 523 | // CMD: server 524 | cmdServer := kingpin.Command("server", "start server") 525 | fDaemon := cmdServer.Flag("daemon", "daemon mode").Short('d').Bool() 526 | fStop := cmdServer.Flag("stop", "stop server").Bool() 527 | cmdServer.Flag("addr", "listen port").Default(":7912").StringVar(&listenAddr) // Create on 2017/09/12 528 | cmdServer.Flag("log", "log file path when in daemon mode").StringVar(&daemonLogPath) 529 | // fServerURL := cmdServer.Flag("server", "server url").Short('t').String() 530 | fNoUiautomator := cmdServer.Flag("nouia", "do not start uiautoamtor when start").Bool() 531 | 532 | // CMD: version 533 | kingpin.Command("version", "show version") 534 | 535 | // CMD: install 536 | cmdIns := kingpin.Command("install", "install apk") 537 | apkStart := cmdIns.Flag("start", "start when installed").Short('s').Bool() 538 | apkPath := cmdIns.Arg("apkPath", "apk path").Required().String() 539 | 540 | // CMD: info 541 | os.Setenv("COLUMNS", "160") 542 | 543 | kingpin.Command("info", "show device info") 544 | switch kingpin.Parse() { 545 | case "curl": 546 | subcmd.DoCurl() 547 | return 548 | case "version": 549 | println(version) 550 | return 551 | case "install": 552 | am := &APKManager{Path: *apkPath} 553 | if err := am.ForceInstall(); err != nil { 554 | log.Fatal(err) 555 | } 556 | if *apkStart { 557 | am.Start(StartOptions{}) 558 | } 559 | return 560 | case "info": 561 | data, _ := json.MarshalIndent(getDeviceInfo(), "", " ") 562 | println(string(data)) 563 | return 564 | case "server": 565 | // continue 566 | } 567 | 568 | if *fStop { 569 | stopSelf() 570 | if !*fDaemon { 571 | return 572 | } 573 | } 574 | 575 | // serverURL := *fServerURL 576 | // if serverURL != "" { 577 | // if !regexp.MustCompile(`https?://`).MatchString(serverURL) { 578 | // serverURL = "http://" + serverURL 579 | // } 580 | // u, err := url.Parse(serverURL) 581 | // if err != nil { 582 | // log.Fatal(err) 583 | // } 584 | // _ = u 585 | // } 586 | 587 | if _, err := os.Stat("/sdcard/tmp"); err != nil { 588 | os.MkdirAll("/sdcard/tmp", 0755) 589 | } 590 | os.Setenv("TMPDIR", "/sdcard/tmp") 591 | 592 | if *fDaemon { 593 | log.Println("run atx-agent in background") 594 | 595 | cntxt := runDaemon() 596 | if cntxt == nil { 597 | log.Printf("atx-agent listening on %v", listenAddr) 598 | return 599 | } 600 | defer cntxt.Release() 601 | log.Println("- - - - - - - - - - - - - - -") 602 | log.Println("daemon started") 603 | setupLogrotate() 604 | } 605 | 606 | log.Printf("atx-agent version %s\n", version) 607 | lazyInit() 608 | 609 | // show ip 610 | outIp, err := getOutboundIP() 611 | if err == nil { 612 | fmt.Printf("Device IP: %v\n", outIp) 613 | } else { 614 | fmt.Printf("Internet is not connected.") 615 | } 616 | 617 | listener, err := net.Listen("tcp", listenAddr) 618 | if err != nil { 619 | log.Fatal(err) 620 | } 621 | 622 | // minicap + minitouch 623 | devInfo := getDeviceInfo() 624 | 625 | width, height := devInfo.Display.Width, devInfo.Display.Height 626 | service.Add("minicap", cmdctrl.CommandInfo{ 627 | Environ: []string{"LD_LIBRARY_PATH=/data/local/tmp"}, 628 | Args: []string{"/data/local/tmp/minicap", "-S", "-P", 629 | fmt.Sprintf("%dx%d@%dx%d/0", width, height, displayMaxWidthHeight, displayMaxWidthHeight)}, 630 | }) 631 | 632 | service.Add("apkagent", cmdctrl.CommandInfo{ 633 | MaxRetries: 2, 634 | Shell: true, 635 | OnStart: func() error { 636 | log.Println("killProcessByName apk-agent.cli") 637 | killProcessByName("apkagent.cli") 638 | return nil 639 | }, 640 | ArgsFunc: func() ([]string, error) { 641 | packagePath, err := getPackagePath("com.github.uiautomator") 642 | if err != nil { 643 | return nil, err 644 | } 645 | return []string{"CLASSPATH=" + packagePath, "exec", "app_process", "/system/bin", "com.github.uiautomator.Console"}, nil 646 | }, 647 | }) 648 | 649 | service.Start("apkagent") 650 | 651 | service.Add("minitouch", cmdctrl.CommandInfo{ 652 | MaxRetries: 2, 653 | Args: []string{"/data/local/tmp/minitouch"}, 654 | Shell: true, 655 | }) 656 | 657 | // uiautomator 1.0 658 | service.Add("uiautomator-1.0", cmdctrl.CommandInfo{ 659 | Args: []string{"sh", "-c", 660 | "uiautomator runtest uiautomator-stub.jar bundle.jar -c com.github.uiautomatorstub.Stub"}, 661 | // Args: []string{"uiautomator", "runtest", "/data/local/tmp/uiautomator-stub.jar", "bundle.jar","-c", "com.github.uiautomatorstub.Stub"}, 662 | Stdout: os.Stdout, 663 | Stderr: os.Stderr, 664 | MaxRetries: 3, 665 | RecoverDuration: 30 * time.Second, 666 | StopSignal: os.Interrupt, 667 | OnStart: func() error { 668 | uiautomatorTimer.Reset() 669 | return nil 670 | }, 671 | OnStop: func() { 672 | uiautomatorTimer.Stop() 673 | }, 674 | }) 675 | 676 | // uiautomator 2.0 677 | service.Add("uiautomator", cmdctrl.CommandInfo{ 678 | Args: []string{"am", "instrument", "-w", "-r", 679 | "-e", "debug", "false", 680 | "-e", "class", "com.github.uiautomator.stub.Stub", 681 | "com.github.uiautomator.test/androidx.test.runner.AndroidJUnitRunner"}, // update for android-uiautomator-server.apk>=2.3.2 682 | //"com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner"}, 683 | Stdout: os.Stdout, 684 | Stderr: os.Stderr, 685 | MaxRetries: 1, // only once 686 | RecoverDuration: 30 * time.Second, 687 | StopSignal: os.Interrupt, 688 | OnStart: func() error { 689 | uiautomatorTimer.Reset() 690 | // log.Println("service uiautomator: startservice com.github.uiautomator/.Service") 691 | // runShell("am", "startservice", "-n", "com.github.uiautomator/.Service") 692 | return nil 693 | }, 694 | OnStop: func() { 695 | uiautomatorTimer.Stop() 696 | // log.Println("service uiautomator: stopservice com.github.uiautomator/.Service") 697 | // runShell("am", "stopservice", "-n", "com.github.uiautomator/.Service") 698 | // runShell("am", "force-stop", "com.github.uiautomator") 699 | }, 700 | }) 701 | 702 | // stop uiautomator when 3 minutes not requests 703 | go func() { 704 | for range uiautomatorTimer.C { 705 | log.Println("uiautomator has not activity for 3 minutes, closed") 706 | service.Stop("uiautomator") 707 | service.Stop("uiautomator-1.0") 708 | } 709 | }() 710 | 711 | if !*fNoUiautomator { 712 | if err := service.Start("uiautomator"); err != nil { 713 | log.Println("uiautomator start failed:", err) 714 | } 715 | } 716 | 717 | server := NewServer() 718 | 719 | sigc := make(chan os.Signal, 1) 720 | signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 721 | go func() { 722 | for sig := range sigc { 723 | needStop := false 724 | switch sig { 725 | case syscall.SIGTERM: 726 | needStop = true 727 | case syscall.SIGHUP: 728 | case syscall.SIGINT: 729 | if !*fDaemon { 730 | needStop = true 731 | } 732 | } 733 | if needStop { 734 | log.Println("Catch signal", sig) 735 | service.StopAll() 736 | server.httpServer.Shutdown(context.TODO()) 737 | return 738 | } 739 | log.Println("Ignore signal", sig) 740 | } 741 | }() 742 | 743 | service.Start("minitouch") 744 | 745 | // run server forever 746 | if err := server.Serve(listener); err != nil { 747 | log.Println("server quit:", err) 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /minitouch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net" 10 | ) 11 | 12 | type toucher struct { 13 | width, height int 14 | rotation int 15 | } 16 | 17 | type TouchRequest struct { 18 | Operation string `json:"operation"` // d, m, u 19 | Index int `json:"index"` 20 | PercentX float64 `json:"xP"` 21 | PercentY float64 `json:"yP"` 22 | Milliseconds int `json:"milliseconds"` 23 | Pressure float64 `json:"pressure"` 24 | } 25 | 26 | // coord(0, 0) is always left-top conner, no matter the rotation changes 27 | func drainTouchRequests(conn net.Conn, reqC chan TouchRequest) error { 28 | var maxX, maxY int 29 | var flag string 30 | var ver int 31 | var maxContacts, maxPressure int 32 | var pid int 33 | 34 | lineRd := lineFormatReader{bufrd: bufio.NewReader(conn)} 35 | lineRd.Scanf("%s %d", &flag, &ver) 36 | lineRd.Scanf("%s %d %d %d %d", &flag, &maxContacts, &maxX, &maxY, &maxPressure) 37 | if err := lineRd.Scanf("%s %d", &flag, &pid); err != nil { 38 | return err 39 | } 40 | 41 | log.Debugf("handle touch requests maxX:%d maxY:%d maxPressure:%d maxContacts:%d", maxX, maxY, maxPressure, maxContacts) 42 | go io.Copy(ioutil.Discard, conn) // ignore the rest output 43 | var posX, posY int 44 | for req := range reqC { 45 | var err error 46 | switch req.Operation { 47 | case "r": // reset 48 | _, err = conn.Write([]byte("r\n")) 49 | case "d": 50 | fallthrough 51 | case "m": 52 | posX = int(req.PercentX * float64(maxX)) 53 | posY = int(req.PercentY * float64(maxY)) 54 | pressure := int(req.Pressure * float64(maxPressure)) 55 | if pressure == 0 { 56 | pressure = maxPressure - 1 57 | } 58 | line := fmt.Sprintf("%s %d %d %d %d\n", req.Operation, req.Index, posX, posY, pressure) 59 | log.Debugf("write to @minitouch %v", line) 60 | _, err = conn.Write([]byte(line)) 61 | case "u": 62 | _, err = conn.Write([]byte(fmt.Sprintf("u %d\n", req.Index))) 63 | case "c": 64 | _, err = conn.Write([]byte("c\n")) 65 | case "w": 66 | _, err = conn.Write([]byte(fmt.Sprintf("w %d\n", req.Milliseconds))) 67 | default: 68 | err = errors.New("unsupported operation: " + req.Operation) 69 | } 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | type lineFormatReader struct { 78 | bufrd *bufio.Reader 79 | err error 80 | } 81 | 82 | func (r *lineFormatReader) Scanf(format string, args ...interface{}) error { 83 | if r.err != nil { 84 | return r.err 85 | } 86 | var line []byte 87 | line, _, r.err = r.bufrd.ReadLine() 88 | if r.err != nil { 89 | return r.err 90 | } 91 | _, r.err = fmt.Sscanf(string(line), format, args...) 92 | return r.err 93 | } 94 | -------------------------------------------------------------------------------- /minitouch_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type MockConn struct { 13 | buffer *bytes.Buffer 14 | } 15 | 16 | func (c *MockConn) Read(b []byte) (n int, err error) { 17 | return c.buffer.Read(b) 18 | } 19 | 20 | func (c *MockConn) Write(b []byte) (n int, err error) { 21 | return c.buffer.Write(b) 22 | } 23 | 24 | func (c *MockConn) Close() error { return nil } 25 | func (c *MockConn) LocalAddr() net.Addr { return nil } 26 | func (c *MockConn) RemoteAddr() net.Addr { return nil } 27 | func (c *MockConn) SetDeadline(t time.Time) error { return nil } 28 | func (c *MockConn) SetReadDeadline(t time.Time) error { return nil } 29 | func (c *MockConn) SetWriteDeadline(t time.Time) error { return nil } 30 | 31 | func TestDrainTouchRequests(t *testing.T) { 32 | reqC := make(chan TouchRequest, 0) 33 | conn := &MockConn{ 34 | buffer: bytes.NewBuffer(nil), 35 | } 36 | err := drainTouchRequests(conn, reqC) 37 | assert.Error(t, err) 38 | 39 | conn = &MockConn{ 40 | buffer: bytes.NewBufferString(`v 1 41 | ^ 10 1080 1920 255 42 | $ 25654`), 43 | } 44 | reqC = make(chan TouchRequest, 4) 45 | reqC <- TouchRequest{ 46 | Operation: "d", 47 | Index: 1, 48 | PercentX: 1.0, 49 | PercentY: 1.0, 50 | Pressure: 1, 51 | } 52 | reqC <- TouchRequest{ 53 | Operation: "c", 54 | } 55 | reqC <- TouchRequest{ 56 | Operation: "m", 57 | Index: 3, 58 | PercentX: 0.5, 59 | PercentY: 0.5, 60 | Pressure: 1, 61 | } 62 | reqC <- TouchRequest{ 63 | Operation: "u", 64 | Index: 4, 65 | } 66 | close(reqC) 67 | drainTouchRequests(conn, reqC) 68 | output := conn.buffer.String() 69 | assert.Equal(t, "d 1 1080 1920 255\nc\nm 3 540 960 255\nu 4\n", output) 70 | } 71 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/openatx/androidutils" 8 | ) 9 | 10 | type CpuInfo struct { 11 | Cores int `json:"cores"` 12 | Hardware string `json:"hardware"` 13 | } 14 | 15 | type MemoryInfo struct { 16 | Total int `json:"total"` // unit kB 17 | Around string `json:"around,omitempty"` 18 | } 19 | 20 | type OwnerInfo struct { 21 | IP string `json:"ip"` 22 | } 23 | 24 | type DeviceInfo struct { 25 | Udid string `json:"udid,omitempty"` // Unique device identifier 26 | PropertyId string `json:"propertyId,omitempty"` // For device managerment, eg: HIH-PHO-1122 27 | Version string `json:"version,omitempty"` // ro.build.version.release 28 | Serial string `json:"serial,omitempty"` // ro.serialno 29 | Brand string `json:"brand,omitempty"` // ro.product.brand 30 | Model string `json:"model,omitempty"` // ro.product.model 31 | HWAddr string `json:"hwaddr,omitempty"` // persist.sys.wifi.mac 32 | Notes string `json:"notes,omitempty"` // device notes 33 | IP string `json:"ip,omitempty"` 34 | Port int `json:"port,omitempty"` 35 | ReverseProxyAddr string `json:"reverseProxyAddr,omitempty"` 36 | ReverseProxyServerAddr string `json:"reverseProxyServerAddr,omitempty"` 37 | Sdk int `json:"sdk,omitempty"` 38 | AgentVersion string `json:"agentVersion,omitempty"` 39 | Display *androidutils.Display `json:"display,omitempty"` 40 | Battery *androidutils.Battery `json:"battery,omitempty"` 41 | Memory *MemoryInfo `json:"memory,omitempty"` // proc/meminfo 42 | Cpu *CpuInfo `json:"cpu,omitempty"` // proc/cpuinfo 43 | Arch string `json:"arch"` 44 | 45 | Owner *OwnerInfo `json:"owner" gorethink:"owner,omitempty"` 46 | Reserved string `json:"reserved,omitempty"` 47 | 48 | ConnectionCount int `json:"-"` // > 1 happended when phone redial server 49 | CreatedAt time.Time `json:"-" gorethink:"createdAt,omitempty"` 50 | PresenceChangedAt time.Time `json:"presenceChangedAt,omitempty"` 51 | UsingBeganAt time.Time `json:"usingBeganAt,omitempty" gorethink:"usingBeganAt,omitempty"` 52 | 53 | Ready *bool `json:"ready,omitempty"` 54 | Present *bool `json:"present,omitempty"` 55 | Using *bool `json:"using,omitempty"` 56 | 57 | Product *Product `json:"product" gorethink:"product_id,reference,omitempty" gorethink_ref:"id"` 58 | Provider *Provider `json:"provider" gorethink:"provider_id,reference,omitempty" gorethink_ref:"id"` 59 | 60 | // only works when there is provider 61 | ProviderForwardedPort int `json:"providerForwardedPort,omitempty"` 62 | 63 | // used for provider to known agent server url 64 | ServerURL string `json:"serverUrl,omitempty"` 65 | } 66 | 67 | // "Brand Model Memory CPU" together can define a phone 68 | type Product struct { 69 | Id string `json:"id" gorethink:"id,omitempty"` 70 | Name string `json:"name" gorethink:"name,omitempty"` 71 | Brand string `json:"brand" gorethink:"brand,omitempty"` 72 | Model string `json:"model" gorethink:"model,omitempty"` 73 | Memory string `json:"memory,omitempty"` // eg: 4GB 74 | Cpu string `json:"cpu,omitempty"` 75 | 76 | Coverage float32 `json:"coverage" gorethink:"coverage,omitempty"` 77 | Gpu string `json:"gpu,omitempty"` 78 | Link string `json:"link,omitempty"` // Outside link 79 | // AntutuScore int `json:"antutuScore,omitempty"` 80 | } 81 | 82 | // u2init 83 | type Provider struct { 84 | Id string `json:"id" gorethink:"id,omitempty"` // machine id 85 | IP string `json:"ip" gorethink:"ip,omitempty"` 86 | Port int `json:"port" gorethink:"port,omitempty"` 87 | Present *bool `json:"present,omitempty"` 88 | Notes string `json:"notes" gorethink:"notes,omitempty"` 89 | Devices []DeviceInfo `json:"devices" gorethink:"devices,omitempty"` 90 | CreatedAt time.Time `json:"createdAt,omitempty"` 91 | PresenceChangedAt time.Time `json:"presenceChangedAt,omitempty"` 92 | } 93 | 94 | // Addr combined with ip:port 95 | func (p *Provider) Addr() string { 96 | return fmt.Sprintf("%s:%d", p.IP, p.Port) 97 | } 98 | -------------------------------------------------------------------------------- /pubsub/pubsub.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/mux" 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | type PubSub struct { 19 | messageC chan Message 20 | subs map[chan interface{}]Message 21 | mu sync.Mutex 22 | } 23 | 24 | type Message struct { 25 | Topic string 26 | Receiver string 27 | Data interface{} 28 | } 29 | 30 | func New() *PubSub { 31 | return &PubSub{ 32 | messageC: make(chan Message, 10), 33 | } 34 | } 35 | 36 | func (ps *PubSub) drain() { 37 | for message := range ps.messageC { 38 | ps.mu.Lock() 39 | for ch, m := range ps.subs { 40 | if m.Topic == message.Topic && m.Receiver == message.Receiver { 41 | select { 42 | case ch <- message.Data: 43 | case <-time.After(1 * time.Second): 44 | log.Println("Sub-chan receive timeout 1s, deleted") 45 | delete(ps.subs, ch) 46 | } 47 | } 48 | } 49 | ps.mu.Unlock() 50 | } 51 | } 52 | 53 | func (ps *PubSub) Publish(data interface{}, topic string, receiver string) { 54 | ps.messageC <- Message{ 55 | Topic: topic, 56 | Receiver: receiver, 57 | Data: data, 58 | } 59 | } 60 | 61 | func (ps *PubSub) Subscribe(topic string, receiver string) chan interface{} { 62 | ps.mu.Lock() 63 | defer ps.mu.Unlock() 64 | C := make(chan interface{}) 65 | ps.subs[C] = Message{ 66 | Topic: topic, 67 | Receiver: receiver, 68 | } 69 | return C 70 | } 71 | 72 | func (ps *PubSub) Unsubscribe(ch chan interface{}) { 73 | ps.mu.Lock() 74 | defer ps.mu.Unlock() 75 | delete(ps.subs, ch) 76 | } 77 | 78 | type HTTPPubSub struct { 79 | ps *PubSub 80 | r *mux.Router 81 | } 82 | 83 | func NewHTTPPubSub(ps *PubSub) *HTTPPubSub { 84 | r := mux.NewRouter() 85 | upgrader := websocket.Upgrader{ 86 | ReadBufferSize: 1024, 87 | WriteBufferSize: 1024, 88 | CheckOrigin: func(r *http.Request) bool { 89 | return true 90 | }, 91 | } 92 | 93 | // publish 94 | r.HandleFunc("/{topic}/{receiver}", func(w http.ResponseWriter, r *http.Request) { 95 | topic := mux.Vars(r)["topic"] 96 | receiver := mux.Vars(r)["receiver"] 97 | var data interface{} 98 | json.NewDecoder(r.Body).Decode(&data) 99 | ps.Publish(data, topic, receiver) 100 | }).Methods("POST") 101 | 102 | // subscribe WebSocket 103 | r.HandleFunc("/{topic}/{receiver}", func(w http.ResponseWriter, r *http.Request) { 104 | topic := mux.Vars(r)["topic"] 105 | receiver := mux.Vars(r)["receiver"] 106 | ws, err := upgrader.Upgrade(w, r, nil) 107 | if err != nil { 108 | log.Println(err) 109 | return 110 | } 111 | dataC := ps.Subscribe(topic, receiver) 112 | defer ps.Unsubscribe(dataC) 113 | quitC := make(chan bool, 1) 114 | go func() { 115 | for { 116 | select { 117 | case <-quitC: 118 | return 119 | case data := <-dataC: 120 | ws.WriteJSON(data) 121 | } 122 | } 123 | }() 124 | for { 125 | _, _, err := ws.ReadMessage() 126 | if err != nil { 127 | quitC <- true 128 | break 129 | } 130 | } 131 | }).Methods("GET") 132 | 133 | // subscribe hijack 134 | r.HandleFunc("/{topic}/{receiver}", func(w http.ResponseWriter, r *http.Request) { 135 | topic := mux.Vars(r)["topic"] 136 | receiver := mux.Vars(r)["receiver"] 137 | conn, err := hijackHTTPRequest(w) 138 | if err != nil { 139 | http.Error(w, err.Error(), http.StatusInternalServerError) 140 | return 141 | } 142 | dataC := ps.Subscribe(topic, receiver) 143 | defer ps.Unsubscribe(dataC) 144 | for data := range dataC { 145 | jsdata, _ := json.Marshal(data) 146 | if _, err := io.WriteString(conn, string(jsdata)+"\n"); err != nil { 147 | break 148 | } 149 | } 150 | }).Methods("CONNECT") 151 | 152 | return &HTTPPubSub{ 153 | ps: ps, 154 | r: r, 155 | } 156 | } 157 | 158 | func (h *HTTPPubSub) ServeHTTP(w http.ResponseWriter, r *http.Request) { 159 | h.r.ServeHTTP(w, r) 160 | } 161 | 162 | func hijackHTTPRequest(w http.ResponseWriter) (conn net.Conn, err error) { 163 | hj, ok := w.(http.Hijacker) 164 | if !ok { 165 | err = errors.New("webserver don't support hijacking") 166 | return 167 | } 168 | 169 | hjconn, bufrw, err := hj.Hijack() 170 | if err != nil { 171 | return nil, err 172 | } 173 | conn = newHijackReadWriteCloser(hjconn.(*net.TCPConn), bufrw) 174 | return 175 | } 176 | 177 | type hijackRW struct { 178 | *net.TCPConn 179 | bufrw *bufio.ReadWriter 180 | } 181 | 182 | func (this *hijackRW) Write(data []byte) (int, error) { 183 | nn, err := this.bufrw.Write(data) 184 | this.bufrw.Flush() 185 | return nn, err 186 | } 187 | 188 | func (this *hijackRW) Read(p []byte) (int, error) { 189 | return this.bufrw.Read(p) 190 | } 191 | 192 | func newHijackReadWriteCloser(conn *net.TCPConn, bufrw *bufio.ReadWriter) net.Conn { 193 | return &hijackRW{ 194 | bufrw: bufrw, 195 | TCPConn: conn, 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /requirements.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | func installRequirements() error { 11 | log.Println("install uiautomator apk") 12 | if err := installUiautomatorAPK(); err != nil { 13 | return err 14 | } 15 | return installMinicap() 16 | } 17 | 18 | func installUiautomatorAPK() error { 19 | if runtime.GOOS == "windows" { 20 | return nil 21 | } 22 | if checkUiautomatorInstalled() { 23 | return nil 24 | } 25 | baseURL := "https://github.com/openatx/android-uiautomator-server/releases/download/" + apkVersionName 26 | if _, err := httpDownload("/data/local/tmp/app-debug.apk", baseURL+"/app-uiautomator.apk", 0644); err != nil { 27 | return err 28 | } 29 | if _, err := httpDownload("/data/local/tmp/app-debug-test.apk", baseURL+"/app-uiautomator-test.apk", 0644); err != nil { 30 | return err 31 | } 32 | if err := forceInstallAPK("/data/local/tmp/app-debug.apk"); err != nil { 33 | return err 34 | } 35 | if err := forceInstallAPK("/data/local/tmp/app-debug-test.apk"); err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func installMinicap() error { 42 | if runtime.GOOS == "windows" { 43 | return nil 44 | } 45 | log.Println("install minicap") 46 | // if fileExists("/data/local/tmp/minicap") && fileExists("/data/local/tmp/minicap.so") { 47 | // if err := Screenshot("/dev/null"); err != nil { 48 | // log.Println("err:", err) 49 | // } else { 50 | // return nil 51 | // } 52 | // } 53 | // remove first to prevent "text file busy" 54 | os.Remove("/data/local/tmp/minicap") 55 | os.Remove("/data/local/tmp/minicap.so") 56 | 57 | minicapSource := "https://github.com/codeskyblue/stf-binaries/raw/master/node_modules/minicap-prebuilt/prebuilt" 58 | propOutput, err := runShell("getprop") 59 | if err != nil { 60 | return err 61 | } 62 | re := regexp.MustCompile(`\[(.*?)\]:\s*\[(.*?)\]`) 63 | matches := re.FindAllStringSubmatch(string(propOutput), -1) 64 | props := make(map[string]string) 65 | for _, m := range matches { 66 | var key = m[1] 67 | var val = m[2] 68 | props[key] = val 69 | } 70 | abi := props["ro.product.cpu.abi"] 71 | sdk := props["ro.build.version.sdk"] 72 | pre := props["ro.build.version.preview_sdk"] 73 | if pre != "" && pre != "0" { 74 | sdk = sdk + pre 75 | } 76 | binURL := strings.Join([]string{minicapSource, abi, "bin", "minicap"}, "/") 77 | _, err = httpDownload("/data/local/tmp/minicap", binURL, 0755) 78 | if err != nil { 79 | return err 80 | } 81 | libURL := strings.Join([]string{minicapSource, abi, "lib", "android-" + sdk, "minicap.so"}, "/") 82 | _, err = httpDownload("/data/local/tmp/minicap.so", libURL, 0644) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | func installMinitouch() error { 90 | baseURL := "https://github.com/codeskyblue/stf-binaries/raw/master/node_modules/minitouch-prebuilt/prebuilt" 91 | abi := getCachedProperty("ro.product.cpu.abi") 92 | binURL := strings.Join([]string{baseURL, abi, "bin/minitouch"}, "/") 93 | _, err := httpDownload("/data/local/tmp/minitouch", binURL, 0755) 94 | return err 95 | } 96 | -------------------------------------------------------------------------------- /safetimer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // SafeTime add thread-safe for time.Timer 9 | type SafeTimer struct { 10 | *time.Timer 11 | mu sync.Mutex 12 | duration time.Duration 13 | } 14 | 15 | func NewSafeTimer(d time.Duration) *SafeTimer { 16 | return &SafeTimer{ 17 | Timer: time.NewTimer(d), 18 | duration: d, 19 | } 20 | } 21 | 22 | // Reset is thread-safe now, accept one or none argument 23 | func (t *SafeTimer) Reset(ds ...time.Duration) bool { 24 | t.mu.Lock() 25 | defer t.mu.Unlock() 26 | if len(ds) > 0 { 27 | if len(ds) != 1 { 28 | panic("SafeTimer.Reset only accept at most one argument") 29 | } 30 | t.duration = ds[0] 31 | } 32 | return t.Timer.Reset(t.duration) 33 | } 34 | 35 | func (t *SafeTimer) Stop() bool { 36 | t.mu.Lock() 37 | defer t.mu.Unlock() 38 | return t.Timer.Stop() 39 | } 40 | -------------------------------------------------------------------------------- /safetimer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSafeTimer(tt *testing.T) { 10 | deadtime := time.Now().Add(2 * time.Second) 11 | t := NewSafeTimer(100 * time.Hour) 12 | wg := sync.WaitGroup{} 13 | wg.Add(8) 14 | for i := 0; i < 8; i++ { 15 | go func() { 16 | for { 17 | t.Reset(10 * time.Hour) 18 | if time.Now().After(deadtime) { 19 | break 20 | } 21 | } 22 | wg.Done() 23 | }() 24 | } 25 | wg.Wait() 26 | } 27 | 28 | func TestSafeTimerUsage(tt *testing.T) { 29 | t := NewSafeTimer(500 * time.Millisecond) 30 | done := make(chan bool, 1) 31 | go func() { 32 | for range t.C { 33 | done <- true 34 | } 35 | }() 36 | select { 37 | case <-done: 38 | case <-time.After(time.Second): 39 | tt.Fatal("Should accept signal, but got nothing") 40 | } 41 | 42 | t.Reset() 43 | select { 44 | case <-done: 45 | case <-time.After(time.Second): 46 | tt.Fatal("Should accept signal, but got nothing") 47 | } 48 | 49 | t.Reset() 50 | t.Reset() 51 | select { 52 | case <-done: 53 | case <-time.After(time.Second): 54 | tt.Fatal("Should accept signal, but got nothing") 55 | } 56 | select { 57 | case <-done: 58 | tt.Fatal("Should accept nothing, because already accept someting") 59 | case <-time.After(time.Second): 60 | } 61 | 62 | t.Stop() 63 | select { 64 | case <-done: 65 | tt.Fatal("Should not accept for timer already stopped") 66 | case <-time.After(time.Second): 67 | } 68 | } 69 | 70 | // func TestMustPanic(tt *testing.T) { 71 | // defer func() { 72 | // if r := recover(); r == nil { 73 | // tt.Errorf("The code did not panic") 74 | // } 75 | // }() 76 | // deadtime := time.Now().Add(2 * time.Second) 77 | // t := time.NewTimer(100 * time.Hour) 78 | // wg := sync.WaitGroup{} 79 | // wg.Add(8) 80 | // for i := 0; i < 8; i++ { 81 | // go func() { 82 | // for { 83 | // t.Reset(10 * time.Hour) 84 | // if time.Now().After(deadtime) { 85 | // break 86 | // } 87 | // } 88 | // wg.Done() 89 | // }() 90 | // } 91 | // wg.Wait() 92 | // } 93 | -------------------------------------------------------------------------------- /screenshot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func screenshotWithMinicap(filename, thumbnailSize string) (err error) { 12 | output, err := runShellOutput("LD_LIBRARY_PATH=/data/local/tmp", "/data/local/tmp/minicap", "-i") 13 | if err != nil { 14 | return 15 | } 16 | var f MinicapInfo 17 | if er := json.Unmarshal([]byte(output), &f); er != nil { 18 | err = fmt.Errorf("minicap not supported: %v", er) 19 | return 20 | } 21 | if thumbnailSize == "" { 22 | thumbnailSize = fmt.Sprintf("%dx%d", f.Width, f.Height) 23 | } 24 | if _, err = runShell( 25 | "LD_LIBRARY_PATH=/data/local/tmp", 26 | "/data/local/tmp/minicap", 27 | "-P", fmt.Sprintf("%dx%d@%s/%d", f.Width, f.Height, thumbnailSize, f.Rotation), 28 | "-s", ">"+filename); err != nil { 29 | err = errors.Wrap(err, "minicap") 30 | return 31 | } 32 | return nil 33 | } 34 | 35 | func screenshotWithScreencap(filename string) (err error) { 36 | _, err = runShellOutput("screencap", "-p", filename) 37 | err = errors.Wrap(err, "screencap") 38 | return 39 | } 40 | 41 | func isMinicapSupported() bool { 42 | output, err := runShellOutput("LD_LIBRARY_PATH=/data/local/tmp", "/data/local/tmp/minicap", "-i") 43 | if err != nil { 44 | return false 45 | } 46 | var f MinicapInfo 47 | if er := json.Unmarshal([]byte(output), &f); er != nil { 48 | return false 49 | } 50 | output, err = runShell( 51 | "LD_LIBRARY_PATH=/data/local/tmp", 52 | "/data/local/tmp/minicap", 53 | "-P", fmt.Sprintf("%dx%d@%dx%d/%d", f.Width, f.Height, f.Width, f.Height, f.Rotation), 54 | "-s", "2>/dev/null") 55 | if err != nil { 56 | return false 57 | } 58 | return bytes.Equal(output[:2], []byte("\xff\xd8")) // JpegFormat 59 | } 60 | -------------------------------------------------------------------------------- /subcmd/curl.go: -------------------------------------------------------------------------------- 1 | package subcmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/alecthomas/kingpin" 13 | "github.com/codeskyblue/goreq" 14 | ) 15 | 16 | type HTTPHeaderValue http.Header 17 | 18 | func (h *HTTPHeaderValue) Set(value string) error { 19 | parts := strings.SplitN(value, ":", 2) 20 | if len(parts) != 2 { 21 | return fmt.Errorf("expected HEADER:VALUE got '%s'", value) 22 | } 23 | (*http.Header)(h).Add(parts[0], parts[1]) 24 | return nil 25 | } 26 | 27 | func (h *HTTPHeaderValue) String() string { 28 | return "" 29 | } 30 | 31 | func (h *HTTPHeaderValue) IsCumulative() bool { 32 | return true 33 | } 34 | 35 | func HTTPHeader(s kingpin.Settings) (target *http.Header) { 36 | target = &http.Header{} 37 | s.SetValue((*HTTPHeaderValue)(target)) 38 | return 39 | } 40 | 41 | type HTTPURLValue url.Values 42 | 43 | func (h *HTTPURLValue) Set(value string) error { 44 | parts := strings.SplitN(value, "=", 2) 45 | if len(parts) != 2 { 46 | return fmt.Errorf("expected KEY=VALUE got '%s'", value) 47 | } 48 | (*url.Values)(h).Add(parts[0], parts[1]) 49 | return nil 50 | } 51 | 52 | func (h *HTTPURLValue) String() string { 53 | return "" 54 | } 55 | 56 | func (h *HTTPURLValue) IsCumulative() bool { 57 | return true 58 | } 59 | 60 | func HTTPValue(s kingpin.Settings) (target *url.Values) { 61 | target = &url.Values{} 62 | s.SetValue((*HTTPURLValue)(target)) 63 | return 64 | } 65 | 66 | var ( 67 | method string 68 | reqUrl string 69 | headers *http.Header 70 | values *url.Values 71 | bodyData string 72 | duration time.Duration 73 | ) 74 | 75 | func RegisterCurl(curl *kingpin.CmdClause) { 76 | curl.Flag("request", "Specify request command to use").Short('X').Default("GET").StringVar(&method) 77 | curl.Arg("url", "url string").Required().StringVar(&reqUrl) 78 | curl.Flag("data", "body data").StringVar(&bodyData) 79 | curl.Flag("timeout", "timeout send and receive response").Default("10s").DurationVar(&duration) 80 | headers = HTTPHeader(curl.Flag("header", "Add a HTTP header to the request.").Short('H')) 81 | values = HTTPValue(curl.Flag("form", "Add a HTTP form values").Short('F')) 82 | } 83 | 84 | func DoCurl() { 85 | if !regexp.MustCompile(`^https?://`).MatchString(reqUrl) { 86 | reqUrl = "http://" + reqUrl 87 | } 88 | request := goreq.Request{ 89 | Method: method, 90 | Uri: reqUrl, 91 | } 92 | request.ShowDebug = true 93 | request.Timeout = duration 94 | 95 | for k, values := range *headers { 96 | for _, v := range values { 97 | request.AddHeader(k, v) 98 | } 99 | } 100 | if method == "GET" { 101 | request.QueryString = *values 102 | } else if method == "POST" { 103 | request.AddHeader("Content-Type", "application/x-www-form-urlencoded") 104 | if bodyData != "" { 105 | request.Body = bodyData 106 | } else { 107 | request.Body = *values 108 | } 109 | } else { 110 | log.Fatalf("Unsupported method: %s", method) 111 | } 112 | res, err := request.Do() 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | log.Println(res.Body.ToString()) 117 | } 118 | -------------------------------------------------------------------------------- /term_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | "unsafe" 13 | 14 | "github.com/sirupsen/logrus" 15 | "github.com/gorilla/websocket" 16 | "github.com/kr/pty" 17 | ) 18 | 19 | type windowSize struct { 20 | Rows uint16 `json:"rows"` 21 | Cols uint16 `json:"cols"` 22 | X uint16 23 | Y uint16 24 | } 25 | 26 | func lookShellPath() (string, error) { 27 | shPath, err := exec.LookPath("bash") 28 | if err == nil { 29 | return shPath, nil 30 | } 31 | return exec.LookPath("sh") 32 | } 33 | 34 | func handleTerminalWebsocket(w http.ResponseWriter, r *http.Request) { 35 | l := logrus.WithField("remoteaddr", r.RemoteAddr) 36 | conn, err := upgrader.Upgrade(w, r, nil) 37 | if err != nil { 38 | l.WithError(err).Error("Unable to upgrade connection") 39 | return 40 | } 41 | shPath, err := lookShellPath() 42 | if err != nil { 43 | l.WithError(err).Error("Unable find shell path") 44 | return 45 | } 46 | cmd := exec.Command(shPath, "-l") 47 | cmd.Env = append(os.Environ(), "TERM=xterm") 48 | 49 | tty, err := pty.Start(cmd) 50 | if err != nil { 51 | l.WithError(err).Error("Unable to start pty/cmd") 52 | conn.WriteMessage(websocket.TextMessage, []byte(err.Error())) 53 | return 54 | } 55 | defer func() { 56 | cmd.Process.Kill() 57 | cmd.Process.Wait() 58 | tty.Close() 59 | conn.Close() 60 | }() 61 | 62 | go func() { 63 | for { 64 | buf := make([]byte, 1024) 65 | read, err := tty.Read(buf) 66 | if err != nil { 67 | conn.WriteMessage(websocket.TextMessage, []byte(err.Error())) 68 | l.WithError(err).Error("Unable to read from pty/cmd") 69 | return 70 | } 71 | conn.WriteMessage(websocket.BinaryMessage, buf[:read]) 72 | } 73 | }() 74 | 75 | for { 76 | messageType, reader, err := conn.NextReader() 77 | if err != nil { 78 | l.WithError(err).Error("Unable to grab next reader") 79 | return 80 | } 81 | 82 | if messageType == websocket.TextMessage { 83 | l.Warn("Unexpected text message") 84 | conn.WriteMessage(websocket.TextMessage, []byte("Unexpected text message")) 85 | continue 86 | } 87 | 88 | dataTypeBuf := make([]byte, 1) 89 | read, err := reader.Read(dataTypeBuf) 90 | if err != nil { 91 | l.WithError(err).Error("Unable to read message type from reader") 92 | conn.WriteMessage(websocket.TextMessage, []byte("Unable to read message type from reader")) 93 | return 94 | } 95 | 96 | if read != 1 { 97 | l.WithField("bytes", read).Error("Unexpected number of bytes read") 98 | return 99 | } 100 | 101 | switch dataTypeBuf[0] { 102 | case 0: 103 | copied, err := io.Copy(tty, reader) 104 | if err != nil { 105 | l.WithError(err).Errorf("Error after copying %d bytes", copied) 106 | } 107 | case 1: 108 | decoder := json.NewDecoder(reader) 109 | resizeMessage := windowSize{} 110 | err := decoder.Decode(&resizeMessage) 111 | if err != nil { 112 | conn.WriteMessage(websocket.TextMessage, []byte("Error decoding resize message: "+err.Error())) 113 | continue 114 | } 115 | l.WithField("resizeMessage", resizeMessage).Info("Resizing terminal") 116 | _, _, errno := syscall.Syscall( 117 | syscall.SYS_IOCTL, 118 | tty.Fd(), 119 | syscall.TIOCSWINSZ, 120 | uintptr(unsafe.Pointer(&resizeMessage)), 121 | ) 122 | if errno != 0 { 123 | l.WithError(syscall.Errno(errno)).Error("Unable to resize terminal") 124 | } 125 | default: 126 | l.WithField("dataType", dataTypeBuf[0]).Error("Unknown data type") 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /term_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | func handleTerminalWebsocket(w http.ResponseWriter, r *http.Request) { 9 | // for k, v := range r.Header { 10 | // log.Println(k, v) 11 | // } 12 | io.WriteString(w, "not support windows") 13 | } 14 | -------------------------------------------------------------------------------- /tunnelproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/openatx/androidutils" 10 | ) 11 | 12 | var currentDeviceInfo *DeviceInfo 13 | 14 | func getDeviceInfo() *DeviceInfo { 15 | if currentDeviceInfo != nil { 16 | return currentDeviceInfo 17 | } 18 | devInfo := &DeviceInfo{ 19 | Serial: getCachedProperty("ro.serialno"), 20 | Brand: getCachedProperty("ro.product.brand"), 21 | Model: getCachedProperty("ro.product.model"), 22 | Version: getCachedProperty("ro.build.version.release"), 23 | AgentVersion: version, 24 | } 25 | devInfo.Sdk, _ = strconv.Atoi(getCachedProperty("ro.build.version.sdk")) 26 | devInfo.HWAddr, _ = androidutils.HWAddrWLAN() 27 | display, _ := androidutils.WindowSize() 28 | devInfo.Display = &display 29 | battery := androidutils.Battery{} 30 | battery.Update() 31 | devInfo.Battery = &battery 32 | // devInfo.Port = listenPort 33 | 34 | memory, err := androidutils.MemoryInfo() 35 | if err != nil { 36 | log.Println("get memory error:", err) 37 | } else { 38 | total := memory["MemTotal"] 39 | around := int(math.Ceil(float64(total-512*1024) / 1024.0 / 1024.0)) // around GB 40 | devInfo.Memory = &MemoryInfo{ 41 | Total: total, 42 | Around: fmt.Sprintf("%d GB", around), 43 | } 44 | } 45 | 46 | hardware, processors, err := androidutils.ProcessorInfo() 47 | if err != nil { 48 | log.Println("get cpuinfo error:", err) 49 | } else { 50 | devInfo.Cpu = &CpuInfo{ 51 | Hardware: hardware, 52 | Cores: len(processors), 53 | } 54 | } 55 | 56 | // Udid is ${Serial}-${MacAddress}-${model} 57 | udid := fmt.Sprintf("%s-%s-%s", 58 | getCachedProperty("ro.serialno"), 59 | devInfo.HWAddr, 60 | strings.Replace(getCachedProperty("ro.product.model"), " ", "_", -1)) 61 | devInfo.Udid = udid 62 | currentDeviceInfo = devInfo 63 | return currentDeviceInfo 64 | } 65 | 66 | // type versionResponse struct { 67 | // ServerVersion string `json:"version"` 68 | // AgentVersion string `json:"atx-agent"` 69 | // } 70 | 71 | // type TunnelProxy struct { 72 | // ServerAddr string 73 | // Secret string 74 | 75 | // udid string 76 | // } 77 | 78 | // // Need test. Connect with server use github.com/codeskyblue/heartbeat 79 | // func (t *TunnelProxy) Heratbeat() { 80 | // dinfo := getDeviceInfo() 81 | // t.udid = dinfo.Udid 82 | // client := &heartbeat.Client{ 83 | // Secret: t.Secret, 84 | // ServerAddr: "http://" + t.ServerAddr + "/heartbeat", 85 | // Identifier: t.udid, 86 | // } 87 | // lostCnt := 0 88 | // client.OnConnect = func() { 89 | // lostCnt = 0 90 | // t.checkUpdate() 91 | // // send device info on first connect 92 | // dinfo.Battery.Update() 93 | // if err := t.UpdateInfo(dinfo); err != nil { 94 | // log.Println("Update info:", err) 95 | // } 96 | // } 97 | // client.OnError = func(err error) { 98 | // if lostCnt == 0 { 99 | // // open identify to make WIFI reconnected when disconnected 100 | // runShellTimeout(time.Minute, "am", "start", "-n", "com.github.uiautomator/.IdentifyActivity") 101 | // } 102 | // lostCnt++ 103 | // } 104 | // // send heartbeat to server every 10s 105 | // client.Beat(10 * time.Second) 106 | // } 107 | 108 | // func (t *TunnelProxy) checkUpdate() error { 109 | // res, err := goreq.Request{Uri: "http://" + t.ServerAddr + "/version"}.Do() 110 | // if err != nil { 111 | // return err 112 | // } 113 | // defer res.Body.Close() 114 | // verResp := new(versionResponse) 115 | // if err := res.Body.FromJsonTo(verResp); err != nil { 116 | // return err 117 | // } 118 | // log.Println("Disable upgrade, until code fixed") 119 | 120 | // // if verResp.AgentVersion != version { 121 | // // if version == "dev" { 122 | // // log.Printf("dev version, skip version upgrade") 123 | // // } else { 124 | // // log.Printf("server require agent version: %v, but current %s, going to upgrade", verResp.AgentVersion, version) 125 | // // if err := doUpdate(verResp.AgentVersion); err != nil { 126 | // // log.Printf("upgrade error: %v", err) 127 | // // return err 128 | // // } 129 | // // log.Printf("restarting server") 130 | // // os.Setenv(daemon.MARK_NAME, daemon.MARK_VALUE+":reset") 131 | // // runDaemon() 132 | // // os.Exit(0) 133 | // // } 134 | // // } 135 | // return nil 136 | // } 137 | 138 | // func (t *TunnelProxy) UpdateInfo(devInfo *DeviceInfo) error { 139 | // res, err := goreq.Request{ 140 | // Method: "POST", 141 | // Uri: "http://" + t.ServerAddr + "/devices/" + t.udid + "/info", 142 | // Body: devInfo, 143 | // }.Do() 144 | // if err != nil { 145 | // return err 146 | // } 147 | // res.Body.Close() 148 | // return nil 149 | // } 150 | 151 | // type WSClient struct { 152 | // // The websocket connection. 153 | // conn *websocket.Conn 154 | // cancelFunc context.CancelFunc 155 | // changeEventC chan interface{} 156 | 157 | // host string 158 | // udid string 159 | // serial string 160 | // brand string 161 | // model string 162 | // version string 163 | // ip string 164 | // } 165 | 166 | // func (c *WSClient) RunForever() { 167 | // if c.changeEventC == nil { 168 | // c.changeEventC = make(chan interface{}, 0) 169 | // } 170 | // n := 0 171 | // for { 172 | // if c.host == "" { 173 | // <-c.changeEventC 174 | // } 175 | // start := time.Now() 176 | // err := c.Run() 177 | // if time.Since(start) > 10*time.Second { 178 | // n = 0 179 | // } 180 | // n++ 181 | // if n > 20 { 182 | // n = 20 183 | // } 184 | // waitDuration := 3*time.Second + time.Duration(n)*time.Second 185 | // log.Println("wait", waitDuration, "error", err) 186 | // select { 187 | // case <-time.After(waitDuration): 188 | // case <-c.changeEventC: 189 | // log.Println("wait canceled") 190 | // } 191 | // } 192 | // } 193 | 194 | // func (c *WSClient) ChangeHost(host string) { 195 | // c.host = host 196 | // if c.changeEventC != nil { 197 | // c.cancelFunc() 198 | // c.conn.Close() 199 | // select { 200 | // case c.changeEventC <- nil: 201 | // case <-time.After(1 * time.Second): 202 | // } 203 | // } 204 | // } 205 | 206 | // func (client *WSClient) Run() error { 207 | // u := url.URL{Scheme: "ws", Host: client.host, Path: "/websocket/heartbeat"} 208 | // ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 209 | // client.cancelFunc = cancel 210 | // defer cancel() 211 | 212 | // log.Println("Remote:", u.String()) 213 | // c, _, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil) 214 | // if err != nil { 215 | // return err 216 | // } 217 | // client.conn = c 218 | // defer c.Close() 219 | 220 | // c.WriteJSON(map[string]interface{}{ 221 | // "command": "handshake", 222 | // "name": "phone", 223 | // "owner": nil, 224 | // "secret": "", 225 | // "url": client.ip + ":7912", 226 | // "priority": 1, 227 | // }) 228 | 229 | // var response WSResponse 230 | // if err = c.ReadJSON(&response); err != nil { 231 | // log.Fatal(err) 232 | // } 233 | // if !response.Success { 234 | // log.Fatal(response.Description) 235 | // } 236 | 237 | // log.Println("update android device") 238 | // c.WriteJSON(map[string]interface{}{ 239 | // "command": "update", 240 | // "platform": "android", 241 | // "udid": client.udid, 242 | // "properties": map[string]string{ 243 | // "serial": client.serial, // ro.serialno 244 | // "brand": client.brand, // ro.product.brand 245 | // "model": client.model, // ro.product.model 246 | // "version": client.version, // ro.build.version.release 247 | // }, 248 | // "provider": map[string]string{ 249 | // "atxAgentAddress": client.ip + ":7912", 250 | // "remoteConnectAddress": client.ip + ":5555", 251 | // "whatsInputAddress": client.ip + ":6677", 252 | // }, 253 | // }) 254 | 255 | // for { 256 | // response = WSResponse{} 257 | // err = c.ReadJSON(&response) 258 | // if err != nil { 259 | // log.Println("read:", err) 260 | // return err 261 | // } 262 | // if response.Command == "release" { 263 | // c.WriteJSON(map[string]interface{}{ 264 | // "command": "update", 265 | // "udid": client.udid, 266 | // "colding": false, 267 | // }) 268 | // } 269 | // } 270 | // } 271 | 272 | // type WSResponse struct { 273 | // Success bool `json:"success"` 274 | // Description string `json:"description"` 275 | // Command string `json:"command"` 276 | // Udid string `json:"udid"` 277 | // } 278 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "os" 13 | "path/filepath" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/codeskyblue/goreq" 19 | "github.com/getlantern/go-update" 20 | "github.com/mholt/archiver" 21 | "github.com/mitchellh/ioprogress" 22 | ) 23 | 24 | func formatString(format string, params map[string]string) string { 25 | for k, v := range params { 26 | format = strings.Replace(format, "{"+k+"}", v, -1) 27 | } 28 | return format 29 | } 30 | 31 | func makeTempDir() string { 32 | if runtime.GOOS == "linux" && runtime.GOARCH == "arm" { 33 | target := "/data/local/tmp/atx-update.tmp" 34 | os.MkdirAll(target, 0755) 35 | return target 36 | } 37 | os.MkdirAll("atx-update.tmp", 0755) 38 | return "atx-update.tmp" 39 | } 40 | 41 | func getLatestVersion() (version string, err error) { 42 | res, err := goreq.Request{ 43 | Uri: fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo), 44 | }.WithHeader("Authorization", "token e83785ff4e37c67098efcea923b668f4135d1dda").Do() // this GITHUB_TOKEN is only for get lastest version 45 | if err != nil { 46 | return 47 | } 48 | defer res.Body.Close() 49 | if res.StatusCode != http.StatusOK { 50 | return "", fmt.Errorf("http status code is not 200, got %d", res.StatusCode) 51 | } 52 | var t = struct { 53 | TagName string `json:"tag_name"` 54 | }{} 55 | if err = json.NewDecoder(res.Body).Decode(&t); err != nil { 56 | return 57 | } 58 | if t.TagName == "" { 59 | return "", errors.New("TagName empty") 60 | } 61 | return t.TagName, nil 62 | } 63 | 64 | func getChecksums(version string) (map[string]string, error) { 65 | uri := formatString("https://github.com/{owner}/{repo}/releases/download/{version}/{repo}_{version}_checksums.txt", map[string]string{ 66 | "version": version, 67 | "owner": owner, 68 | "repo": repo, 69 | }) 70 | res, err := goreq.Request{ 71 | Uri: uri, 72 | MaxRedirects: 10, 73 | RedirectHeaders: true, 74 | }.Do() 75 | if err != nil { 76 | return nil, err 77 | } 78 | defer res.Body.Close() 79 | scanner := bufio.NewScanner(res.Body) 80 | m := make(map[string]string, 6) 81 | for scanner.Scan() { 82 | var filename, sha256sum string 83 | _, err := fmt.Sscanf(scanner.Text(), "%s\t%s", &sha256sum, &filename) 84 | if err != nil { 85 | continue 86 | } 87 | m[filename] = sha256sum 88 | } 89 | return m, nil 90 | } 91 | 92 | func doUpdate(version string) (err error) { 93 | if version == "" { 94 | version, err = getLatestVersion() 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | arch := runtime.GOARCH 100 | if runtime.GOOS == "linux" && arch == "arm" { 101 | arch += "v7" 102 | } 103 | filename := fmt.Sprintf("%s_%s_%s_%s.tar.gz", repo, version, runtime.GOOS, arch) 104 | log.Printf("update file: %s", filename) 105 | checksums, err := getChecksums(version) 106 | if err != nil { 107 | return err 108 | } 109 | checksum, ok := checksums[filename] 110 | if !ok { 111 | return fmt.Errorf("checksums not found for file: %s", filename) 112 | } 113 | // fixed get latest version 114 | uri := formatString("https://github.com/{owner}/{repo}/releases/download/{version}/{filename}", map[string]string{ 115 | "version": version, 116 | "owner": owner, 117 | "repo": repo, 118 | "filename": filename, 119 | }) 120 | log.Printf("update url: %s", uri) 121 | res, err := goreq.Request{ 122 | Uri: uri, 123 | MaxRedirects: 10, 124 | RedirectHeaders: true, 125 | }.Do() 126 | if err != nil { 127 | return err 128 | } 129 | defer res.Body.Close() 130 | if res.StatusCode != 200 { 131 | err = fmt.Errorf("HTTP download error: [%d] %s", res.StatusCode, res.Status) 132 | return err 133 | } 134 | contentLength, err := strconv.Atoi(res.Header.Get("Content-Length")) 135 | if err != nil { 136 | return err 137 | } 138 | hasher := sha256.New() 139 | progressR := &ioprogress.Reader{ 140 | Reader: res.Body, 141 | Size: int64(contentLength), 142 | DrawFunc: ioprogress.DrawTerminalf(os.Stdout, ioprogress.DrawTextFormatBytes), 143 | } 144 | tmpdir := makeTempDir() 145 | distPath := filepath.Join(tmpdir, "dist.tar.gz") 146 | f, err := os.Create(distPath) 147 | if err != nil { 148 | return err 149 | } 150 | writer := io.MultiWriter(f, hasher) 151 | io.Copy(writer, progressR) 152 | if err = f.Close(); err != nil { 153 | return err 154 | } 155 | realChecksum := hex.EncodeToString(hasher.Sum(nil)) 156 | if realChecksum != checksum { 157 | return fmt.Errorf("update file checksum wrong, expected: %s, got: %s", checksum, realChecksum) 158 | } 159 | if err = archiver.TarGz.Open(distPath, tmpdir); err != nil { 160 | return err 161 | } 162 | log.Println("perform updating") 163 | err, _ = update.New().FromFile(filepath.Join(tmpdir, repo)) 164 | return err 165 | } 166 | -------------------------------------------------------------------------------- /update_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFormatString(t *testing.T) { 10 | s := formatString("a {v} {b} {v}", map[string]string{ 11 | "v": "x", 12 | "b": "y", 13 | }) 14 | assert.Equal(t, s, "a x y x") 15 | } 16 | 17 | //func TestGetLatestVersion(t *testing.T) { 18 | //version, err := getLatestVersion() 19 | //assert.NoError(t, err) 20 | //t.Logf("version: %s", version) 21 | //assert.NotEqual(t, version, "") 22 | //} 23 | 24 | func TestGetChecksums(t *testing.T) { 25 | maps, err := getChecksums("0.0.1") 26 | assert.NoError(t, err) 27 | t.Logf("%#v", maps) 28 | } 29 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/rand" 7 | "encoding/hex" 8 | "fmt" 9 | "image" 10 | "io" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | "github.com/codeskyblue/goreq" 23 | shellquote "github.com/kballard/go-shellquote" 24 | "github.com/openatx/androidutils" 25 | "github.com/pkg/errors" 26 | "github.com/prometheus/procfs" 27 | "github.com/shogo82148/androidbinary/apk" 28 | ) 29 | 30 | // TempFileName generates a temporary filename for use in testing or whatever 31 | func TempFileName(dir, suffix string) string { 32 | randBytes := make([]byte, 16) 33 | rand.Read(randBytes) 34 | return filepath.Join(dir, hex.EncodeToString(randBytes)+suffix) 35 | } 36 | 37 | func fileExists(path string) bool { 38 | _, err := os.Stat(path) 39 | return err == nil 40 | } 41 | 42 | // Command add timeout support for os/exec 43 | type Command struct { 44 | Args []string 45 | Timeout time.Duration 46 | Shell bool 47 | ShellQuote bool 48 | Stdout io.Writer 49 | Stderr io.Writer 50 | } 51 | 52 | func NewCommand(args ...string) *Command { 53 | return &Command{ 54 | Args: args, 55 | } 56 | } 57 | 58 | func (c *Command) shellPath() string { 59 | sh := os.Getenv("SHELL") 60 | if sh == "" { 61 | sh, err := exec.LookPath("sh") 62 | if err == nil { 63 | return sh 64 | } 65 | sh = "/system/bin/sh" 66 | } 67 | return sh 68 | } 69 | 70 | func (c *Command) computedArgs() (name string, args []string) { 71 | if c.Shell { 72 | var cmdline string 73 | if c.ShellQuote { 74 | cmdline = shellquote.Join(c.Args...) 75 | } else { 76 | cmdline = strings.Join(c.Args, " ") // simple, but works well with ">". eg Args("echo", "hello", ">output.txt") 77 | } 78 | args = append(args, "-c", cmdline) 79 | return c.shellPath(), args 80 | } 81 | return c.Args[0], c.Args[1:] 82 | } 83 | 84 | func (c Command) newCommand() *exec.Cmd { 85 | name, args := c.computedArgs() 86 | cmd := exec.Command(name, args...) 87 | if c.Stdout != nil { 88 | cmd.Stdout = c.Stdout 89 | } 90 | if c.Stderr != nil { 91 | cmd.Stderr = c.Stderr 92 | } 93 | return cmd 94 | } 95 | 96 | func (c Command) Run() error { 97 | cmd := c.newCommand() 98 | if c.Timeout > 0 { 99 | timer := time.AfterFunc(c.Timeout, func() { 100 | if cmd.Process != nil { 101 | cmd.Process.Kill() 102 | } 103 | }) 104 | defer timer.Stop() 105 | } 106 | return cmd.Run() 107 | } 108 | 109 | func (c Command) StartBackground() (pid int, err error) { 110 | cmd := c.newCommand() 111 | err = cmd.Start() 112 | if err != nil { 113 | return 114 | } 115 | pid = cmd.Process.Pid 116 | return 117 | } 118 | 119 | func (c Command) Output() (output []byte, err error) { 120 | var b bytes.Buffer 121 | c.Stdout = &b 122 | c.Stderr = nil 123 | err = c.Run() 124 | return b.Bytes(), err 125 | } 126 | 127 | func (c Command) CombinedOutput() (output []byte, err error) { 128 | var b bytes.Buffer 129 | c.Stdout = &b 130 | c.Stderr = &b 131 | err = c.Run() 132 | return b.Bytes(), err 133 | } 134 | 135 | func (c Command) CombinedOutputString() (output string, err error) { 136 | bytesOutput, err := c.CombinedOutput() 137 | return string(bytesOutput), err 138 | } 139 | 140 | // need add timeout 141 | func runShell(args ...string) (output []byte, err error) { 142 | return Command{ 143 | Args: args, 144 | Shell: true, 145 | ShellQuote: false, 146 | Timeout: 10 * time.Minute, 147 | }.CombinedOutput() 148 | } 149 | 150 | func runShellOutput(args ...string) (output []byte, err error) { 151 | return Command{ 152 | Args: args, 153 | Shell: true, 154 | ShellQuote: false, 155 | Timeout: 10 * time.Minute, 156 | }.Output() 157 | } 158 | 159 | func runShellTimeout(duration time.Duration, args ...string) (output []byte, err error) { 160 | return Command{ 161 | Args: args, 162 | Shell: true, 163 | Timeout: duration, 164 | }.CombinedOutput() 165 | } 166 | 167 | type fakeWriter struct { 168 | writeFunc func([]byte) (int, error) 169 | Err chan error 170 | } 171 | 172 | func (w *fakeWriter) Write(data []byte) (int, error) { 173 | n, err := w.writeFunc(data) 174 | if err != nil { 175 | select { 176 | case w.Err <- err: 177 | default: 178 | } 179 | } 180 | return n, err 181 | } 182 | 183 | func newFakeWriter(f func([]byte) (int, error)) *fakeWriter { 184 | return &fakeWriter{ 185 | writeFunc: f, 186 | Err: make(chan error, 1), 187 | } 188 | } 189 | 190 | type ProcInfo struct { 191 | Pid int `json:"pid"` 192 | PPid int `json:"ppid"` 193 | NumThreads int `json:"threadCount"` 194 | Cmdline []string `json:"cmdline"` 195 | Name string `json:"name"` 196 | } 197 | 198 | // Kill by Pid 199 | func (p ProcInfo) Kill() error { 200 | process, err := os.FindProcess(p.Pid) 201 | if err != nil { 202 | return err 203 | } 204 | return process.Kill() 205 | } 206 | 207 | func listAllProcs() (ps []ProcInfo, err error) { 208 | fs, err := procfs.NewFS(procfs.DefaultMountPoint) 209 | if err != nil { 210 | return 211 | } 212 | procs, err := fs.AllProcs() 213 | if err != nil { 214 | return 215 | } 216 | for _, p := range procs { 217 | cmdline, _ := p.CmdLine() 218 | var name string 219 | if len(cmdline) == 1 { 220 | name = cmdline[0] // get package name 221 | } else { 222 | name, _ = p.Comm() 223 | } 224 | stat, _ := p.Stat() 225 | ps = append(ps, ProcInfo{ 226 | Pid: p.PID, 227 | PPid: stat.PPID, 228 | Cmdline: cmdline, 229 | Name: name, 230 | NumThreads: stat.NumThreads, 231 | }) 232 | } 233 | return 234 | } 235 | 236 | func findProcAll(packageName string) (procList []procfs.Proc, err error) { 237 | procs, err := procfs.AllProcs() 238 | for _, proc := range procs { 239 | cmdline, _ := proc.CmdLine() 240 | if len(cmdline) != 1 { 241 | continue 242 | } 243 | if cmdline[0] == packageName || strings.HasPrefix(cmdline[0], packageName+":") { 244 | procList = append(procList, proc) 245 | } 246 | } 247 | return 248 | } 249 | 250 | // pidof 251 | func pidOf(packageName string) (pid int, err error) { 252 | fs, err := procfs.NewFS(procfs.DefaultMountPoint) 253 | if err != nil { 254 | return 255 | } 256 | // when packageName is int 257 | pid, er := strconv.Atoi(packageName) 258 | if er == nil { 259 | _, err = fs.NewProc(pid) 260 | return 261 | } 262 | procs, err := fs.AllProcs() 263 | if err != nil { 264 | return 265 | } 266 | for _, proc := range procs { 267 | cmdline, _ := proc.CmdLine() 268 | if len(cmdline) == 1 && cmdline[0] == packageName { 269 | return proc.PID, nil 270 | } 271 | } 272 | return 0, errors.New("package not found") 273 | } 274 | 275 | type PackageInfo struct { 276 | PackageName string `json:"packageName"` 277 | MainActivity string `json:"mainActivity"` 278 | Label string `json:"label"` 279 | VersionName string `json:"versionName"` 280 | VersionCode int `json:"versionCode"` 281 | Size int64 `json:"size"` 282 | Icon image.Image `json:"-"` 283 | } 284 | 285 | func readPackageInfo(packageName string) (info PackageInfo, err error) { 286 | outbyte, err := runShell("pm", "path", packageName) 287 | lines := strings.Split(string(outbyte), "\n") 288 | if len(lines) == 0 { 289 | err = errors.New("no output received") 290 | return 291 | } 292 | output := strings.TrimSpace(lines[0]) 293 | if !strings.HasPrefix(output, "package:") { 294 | err = errors.New("package " + strconv.Quote(packageName) + " not found") 295 | return 296 | } 297 | apkpath := output[len("package:"):] 298 | return readPackageInfoFromPath(apkpath) 299 | } 300 | 301 | func readPackageInfoFromPath(apkpath string) (info PackageInfo, err error) { 302 | finfo, err := os.Stat(apkpath) 303 | if err != nil { 304 | return 305 | } 306 | info.Size = finfo.Size() 307 | pkg, err := apk.OpenFile(apkpath) 308 | if err != nil { 309 | err = errors.Wrap(err, apkpath) 310 | return 311 | } 312 | defer pkg.Close() 313 | 314 | info.PackageName = pkg.PackageName() 315 | info.Label, _ = pkg.Label(nil) 316 | info.MainActivity, _ = pkg.MainActivity() 317 | info.Icon, _ = pkg.Icon(nil) 318 | info.VersionCode = int(pkg.Manifest().VersionCode.MustInt32()) 319 | info.VersionName = pkg.Manifest().VersionName.MustString() 320 | return 321 | } 322 | 323 | func procWalk(fn func(p procfs.Proc)) error { 324 | fs, err := procfs.NewFS(procfs.DefaultMountPoint) 325 | if err != nil { 326 | return err 327 | } 328 | procs, err := fs.AllProcs() 329 | for _, proc := range procs { 330 | fn(proc) 331 | } 332 | return nil 333 | } 334 | 335 | // get main activity with packageName 336 | func mainActivityOf(packageName string) (activity string, err error) { 337 | output, err := runShellOutput("pm", "list", "packages", "-f", packageName) 338 | if err != nil { 339 | log.Println("pm list err:", err) 340 | return 341 | } 342 | matches := regexp.MustCompile(`package:(.+)=([.\w]+)`).FindAllStringSubmatch(string(output), -1) 343 | for _, match := range matches { 344 | if match[2] != packageName { 345 | continue 346 | } 347 | pkg, err := apk.OpenFile(match[1]) 348 | if err != nil { 349 | return "", err 350 | } 351 | return pkg.MainActivity() 352 | } 353 | return "", errors.New("package not found") 354 | } 355 | 356 | // download minicap or minitouch apk, etc... 357 | func httpDownload(path string, urlStr string, perms os.FileMode) (written int64, err error) { 358 | resp, err := goreq.Request{ 359 | Uri: urlStr, 360 | RedirectHeaders: true, 361 | MaxRedirects: 10, 362 | }.Do() 363 | if err != nil { 364 | return 365 | } 366 | defer resp.Body.Close() 367 | if resp.StatusCode != http.StatusOK { 368 | err = fmt.Errorf("http download <%s> status %v", urlStr, resp.Status) 369 | return 370 | } 371 | file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, perms) 372 | if err != nil { 373 | return 374 | } 375 | defer file.Close() 376 | written, err = io.Copy(file, resp.Body) 377 | log.Println("http download:", written) 378 | return 379 | } 380 | 381 | func hijackHTTPRequest(w http.ResponseWriter) (conn net.Conn, err error) { 382 | hj, ok := w.(http.Hijacker) 383 | if !ok { 384 | err = errors.New("webserver don't support hijacking") 385 | return 386 | } 387 | 388 | hjconn, bufrw, err := hj.Hijack() 389 | if err != nil { 390 | return nil, err 391 | } 392 | conn = newHijackReadWriteCloser(hjconn.(*net.TCPConn), bufrw) 393 | return 394 | } 395 | 396 | type hijactRW struct { 397 | *net.TCPConn 398 | bufrw *bufio.ReadWriter 399 | } 400 | 401 | func (this *hijactRW) Write(data []byte) (int, error) { 402 | nn, err := this.bufrw.Write(data) 403 | this.bufrw.Flush() 404 | return nn, err 405 | } 406 | 407 | func (this *hijactRW) Read(p []byte) (int, error) { 408 | return this.bufrw.Read(p) 409 | } 410 | 411 | func newHijackReadWriteCloser(conn *net.TCPConn, bufrw *bufio.ReadWriter) net.Conn { 412 | return &hijactRW{ 413 | bufrw: bufrw, 414 | TCPConn: conn, 415 | } 416 | } 417 | 418 | func getCachedProperty(name string) string { 419 | return androidutils.CachedProperty(name) 420 | } 421 | 422 | func getProperty(name string) string { 423 | return androidutils.Property(name) 424 | } 425 | 426 | func copyToFile(rd io.Reader, dst string) error { 427 | fd, err := os.Create(dst) 428 | if err != nil { 429 | return err 430 | } 431 | defer fd.Close() 432 | _, err = io.Copy(fd, rd) 433 | return err 434 | } 435 | 436 | // parse output: dumpsys meminfo --local ${pkgname} 437 | // If everything is going, returns json, unit KB 438 | // 439 | // { 440 | // "code": 58548, 441 | // "graphics": 73068, 442 | // "java heap": 160332, 443 | // "native heap": 67708, 444 | // "private Other": 34976, 445 | // "stack": 4728, 446 | // "system": 8288, 447 | // "total": 407648 448 | // } 449 | func parseMemoryInfo(nameOrPid string) (info map[string]int, err error) { 450 | output, err := Command{ 451 | Args: []string{"dumpsys", "meminfo", "--local", nameOrPid}, 452 | Timeout: 10 * time.Second, 453 | }.CombinedOutputString() 454 | if err != nil { 455 | return 456 | } 457 | index := strings.Index(output, "App Summary") 458 | if index == -1 { 459 | err = errors.New("dumpsys meminfo has no [App Summary]") 460 | return 461 | } 462 | re := regexp.MustCompile(`(\w[\w ]+):\s*(\d+)`) 463 | matches := re.FindAllStringSubmatch(output[index:], -1) 464 | if len(matches) == 0 { 465 | err = errors.New("Invalid dumpsys meminfo output") 466 | return 467 | } 468 | info = make(map[string]int, len(matches)) 469 | for _, m := range matches { 470 | key := strings.ToLower(m[1]) 471 | val, _ := strconv.Atoi(m[2]) 472 | info[key] = val 473 | } 474 | return 475 | } 476 | 477 | type CPUStat struct { 478 | Pid int 479 | SystemTotal uint 480 | SystemIdle uint 481 | ProcUser uint 482 | ProcSystem uint 483 | UpdateTime time.Time 484 | } 485 | 486 | func NewCPUStat(pid int) (stat *CPUStat, err error) { 487 | stat = &CPUStat{Pid: pid} 488 | err = stat.Update() 489 | return 490 | } 491 | 492 | func (c *CPUStat) String() string { 493 | return fmt.Sprintf("CPUStat(pid:%d, systotal:%d, sysidle:%d, user:%d, system:%d", 494 | c.Pid, c.SystemTotal, c.SystemIdle, c.ProcUser, c.ProcSystem) 495 | } 496 | 497 | // CPUPercent may > 100.0 if process have multi thread on multi cores 498 | func (c *CPUStat) CPUPercent(last *CPUStat) float64 { 499 | pjiff1 := last.ProcUser + last.ProcSystem 500 | pjiff2 := c.ProcUser + c.ProcSystem 501 | duration := c.SystemTotal - last.SystemTotal 502 | if duration <= 0 { 503 | return 0.0 504 | } 505 | percent := 100.0 * float64(pjiff2-pjiff1) / float64(duration) 506 | if percent < 0.0 { 507 | log.Println("Warning: cpu percent < 0", percent) 508 | percent = 0 509 | } 510 | return percent 511 | } 512 | 513 | func (c *CPUStat) SystemCPUPercent(last *CPUStat) float64 { 514 | idle := c.SystemIdle - last.SystemIdle 515 | jiff := c.SystemTotal - last.SystemTotal 516 | percent := 100.0 * float64(idle) / float64(jiff) 517 | return percent 518 | } 519 | 520 | // Update proc jiffies data 521 | func (c *CPUStat) Update() error { 522 | // retrive /proc//stat 523 | fs, err := procfs.NewFS(procfs.DefaultMountPoint) 524 | if err != nil { 525 | return err 526 | } 527 | proc, err := fs.NewProc(c.Pid) 528 | if err != nil { 529 | return err 530 | } 531 | stat, err := proc.NewStat() 532 | if err != nil { 533 | return errors.Wrap(err, "read /proc//stat") 534 | } 535 | 536 | // retrive /proc/stst 537 | statData, err := ioutil.ReadFile("/proc/stat") 538 | if err != nil { 539 | return errors.Wrap(err, "read /proc/stat") 540 | } 541 | procStat := string(statData) 542 | idx := strings.Index(procStat, "\n") 543 | // cpuName, user, nice, system, idle, iowait, irq, softIrq, steal, guest, guestNice 544 | fields := strings.Fields(procStat[:idx]) 545 | if fields[0] != "cpu" { 546 | return errors.New("/proc/stat not startswith cpu") 547 | } 548 | var total, idle uint 549 | for i, raw := range fields[1:] { 550 | var v uint 551 | fmt.Sscanf(raw, "%d", &v) 552 | if i == 3 { // idle 553 | idle = v 554 | } 555 | total += v 556 | } 557 | 558 | c.ProcSystem = stat.STime 559 | c.ProcUser = stat.UTime 560 | c.SystemTotal = total 561 | c.SystemIdle = idle 562 | c.UpdateTime = time.Now() 563 | return nil 564 | } 565 | 566 | var cpuStats = make(map[int]*CPUStat) 567 | 568 | var _cpuCoreCount int 569 | 570 | // CPUCoreCount return 0 if retrive failed 571 | func CPUCoreCount() int { 572 | if _cpuCoreCount != 0 { 573 | return _cpuCoreCount 574 | } 575 | fs, err := procfs.NewFS(procfs.DefaultMountPoint) 576 | if err != nil { 577 | return 0 578 | } 579 | stat, err := fs.NewStat() 580 | if err != nil { 581 | return 0 582 | } 583 | _cpuCoreCount = len(stat.CPU) 584 | return _cpuCoreCount 585 | } 586 | 587 | type CPUInfo struct { 588 | Pid int `json:"pid"` 589 | User uint `json:"user"` 590 | System uint `json:"system"` 591 | Percent float64 `json:"percent"` 592 | SystemPercent float64 `json:"systemPercent"` 593 | CoreCount int `json:"coreCount"` 594 | } 595 | 596 | func readCPUInfo(pid int) (info CPUInfo, err error) { 597 | last, ok := cpuStats[pid] 598 | if !ok || // need fresh history data 599 | last.UpdateTime.Add(5*time.Second).Before(time.Now()) { 600 | last, err = NewCPUStat(pid) 601 | if err != nil { 602 | return 603 | } 604 | time.Sleep(100 * time.Millisecond) 605 | log.Println("Update data") 606 | } 607 | stat, err := NewCPUStat(pid) 608 | if err != nil { 609 | return 610 | } 611 | cpuStats[pid] = stat 612 | info.Pid = pid 613 | info.User = stat.ProcUser 614 | info.System = stat.ProcSystem 615 | info.Percent = stat.CPUPercent(last) 616 | info.SystemPercent = stat.SystemCPUPercent(last) 617 | info.CoreCount = CPUCoreCount() 618 | return 619 | } 620 | 621 | func dumpHierarchy() (xmlContent string, err error) { 622 | const targetPath = "/sdcard/window_dump.xml" 623 | c := &Command{ 624 | Args: []string{"uiautomator", "dump", targetPath}, 625 | Shell: true, 626 | } 627 | if err = c.Run(); err != nil { 628 | return 629 | } 630 | data, err := ioutil.ReadFile(targetPath) 631 | xmlContent = string(data) 632 | return 633 | } 634 | 635 | func listPackages() (pkgs []PackageInfo, err error) { 636 | c := NewCommand("pm", "list", "packages", "-f", "-3") 637 | c.Shell = true 638 | output, err := c.CombinedOutputString() 639 | if err != nil { 640 | return 641 | } 642 | for _, line := range strings.Split(output, "\n") { 643 | if !strings.HasPrefix(line, "package:") { 644 | continue 645 | } 646 | matches := regexp.MustCompile(`^package:(/.+)=([^=]+)$`).FindStringSubmatch(line) 647 | if len(matches) == 0 { 648 | continue 649 | } 650 | pkgPath := matches[1] 651 | pkgName := matches[2] 652 | pkgInfo, er := readPackageInfoFromPath(pkgPath) 653 | if er != nil { 654 | log.Printf("Read package %s error %v", pkgName, er) 655 | continue 656 | } 657 | pkgs = append(pkgs, pkgInfo) 658 | } 659 | return 660 | } 661 | 662 | func killProcessByName(processName string) bool { 663 | procs, err := procfs.AllProcs() 664 | if err != nil { 665 | return false 666 | } 667 | 668 | killed := false 669 | for _, p := range procs { 670 | cmdline, _ := p.CmdLine() 671 | var name string 672 | if len(cmdline) >= 1 { 673 | name = filepath.Base(cmdline[0]) 674 | } else { 675 | name, _ = p.Comm() 676 | } 677 | 678 | if name == processName { 679 | process, err := os.FindProcess(p.PID) 680 | if err == nil { 681 | process.Kill() 682 | killed = true 683 | } 684 | } 685 | } 686 | return killed 687 | } 688 | 689 | func getPackagePath(packageName string) (string, error) { 690 | pmPathOutput, err := Command{ 691 | Args: []string{"pm", "path", "com.github.uiautomator"}, 692 | Shell: true, 693 | }.CombinedOutputString() 694 | if err != nil { 695 | return "", err 696 | } 697 | if !strings.HasPrefix(pmPathOutput, "package:") { 698 | return "", errors.New("invalid pm path output: " + pmPathOutput) 699 | } 700 | packagePath := strings.TrimSpace(pmPathOutput[len("package:"):]) 701 | return packagePath, nil 702 | } 703 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestTempFileName(t *testing.T) { 9 | tmpDir := os.TempDir() 10 | filename := TempFileName(tmpDir, ".apk") 11 | t.Log(filename) 12 | } 13 | --------------------------------------------------------------------------------