基于 STF 的 iOS 远程真机控制,踩坑经验分享

3,912 阅读12分钟

介绍

STF (or Smartphone Test Farm) is a web application for debugging smartphones, smartwatches and other gadgets remotely, from the comfort of your browser.

b80a268831bca0f4361aa33d48760580.png

Github 传送门

简单来说,就是可以在浏览器上远程操作真机的解决方案,是由 "Device Farmer" 这个组织提供。本身 stf 不支持 iOS 设备,所以 “Device Framer” 又单独提供了 “stf_ios_support” 项目,本文讲述该项目部署操作的过程以及遇到的问题。

原理

d704948d4beb7da25d63607793e8d08e.png

实际上,stf_ios_support 真机操作也是基于 WebDriverAgent 项目实现,基本想要远程操控 iOS 设备,都依赖 WebDrviverAgent 项目。由于国内的 iPhone 手机端口封闭,所以外部无法直接访问,因此依赖 wdaproxy 将手机端口映射,PC 就可以直接通过http://localhost:7000/status来访问、操作对应的设备。而 stf_ios_support 实际上就是一个 WebSocket 服务器,用来监听、设备的连接,和传输 STF Server 发过来的指令(简单理解,就是包了一层 Proxy)。当然还有最重要的,真机画面是通过 ios_video_enablerffmpeg 实现.

开始

材料

  • Docker Desktop 容器
  • Mac 笔记本
  • Xcode
  • VPN(科学上网,非必要)
  • 苹果开发者账号、以及签名证书

第 1 步:工程初始化

检出 stf_ios_support 目录到 Mac 本机目录,并 cdstf_ios_support 目录,执行 ./init.sh 文件,脚本执行的前提需要先安装好 Xcode 并将开发者信息配置正确(这里不讲述 Xcode 如何配置,默认你已经配置成功),如果网络不好的建议科学上网,否则很多组件会拉不下来。

git clone https://github.com/DeviceFarmer/stf_ios_support.git;
cd stf_ios_support
./init.sh

如果成功,会显示如下信息:

zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % ./init.sh 
Xcode 13.0 installed
jq		=> version 1.6
graphicsmagick		=> version 1.3.36
zeromq		=> version 4.3.4
protobuf		=> version 3.17.3
yasm		=> version 1.3.0_2
pkg-config		=> version 0.29.2_3
carthage		=> version 0.38.0
automake		=> version 1.16.3_1
autoconf		=> version 2.71
libtool		=> version 2.4.6_3
wget		=> version 1.21.1
go		=> version 1.16.5
node@12		=> version 12.22.3
libsodium		=> version 1.0.18_1
czmq		=> version 4.2.0
jpeg-turbo		=> version 2.1.0
nanomsg		=> version 1.1.5
libgcrypt		=> version 1.9.3
gnutls		=> version 3.6.16
mobiledevice		=> version 2.0.0
libplist - HEAD already installed
libplist - installed HEAD is version 2.2.1 ( ==2.2.1 )
libusbmuxd - HEAD already installed
libusbmuxd - installed HEAD is version 2.0.3 ( ==2.0.3 )
zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % 

第 2 步:安装 STF 服务

首先,需要安装 Docker DeskTop 客户端容器,安装完成后,确保 docker 容器配置正确。

然后,复制 stf_ios_support 目录下的 server 文件夹,到另外一个目标目录(目的是区分下,不容易造成混淆,不复制也没问题),cd 到目标目录 server 中的 cert 下,执行 gencert.sh 脚本,该服务就是 Docker 下的 STF 服务器。

cp stf_ios_support/server xxxx/xxxx/test/;
cd xxxx/xxxx/test/server/cert/;
./gencert.sh

显示如下信息,完成自签名证书:

zhudezhen@zhudezhendeMacBook-Pro cert % ./gencert.sh 
Generating a 2048 bit RSA private key
.....................+++
...............................................................+++
writing new private key to 'server.key'
-----
zhudezhen@zhudezhendeMacBook-Pro cert % 

然后,修改目录文件夹 server/.env 文件,将 PUBLIC_IPHOSTNAME 改为本机 ip,例如:这里的 192.168.199.191

PUBLIC_IP=192.168.199.191
SECRET=secret
RETHINKDB_PORT_28015_TCP=tcp://rethinkdb:28015
HOSTNAME=192.168.199.191
STF_IMAGE=livxtrm/devicefarmer:latest

然后,切回到目标文件夹 server 根目录,执行 docker-compose up 命令(科学上网),向 Docker 容器中,安装 STF 服务器,命令行开始,等待完成即可。

zhudezhen@zhudezhendeMacBook-Pro server % docker-compose up
Creating network "server_default" with the default driver
Creating volume "server_rethinkdb" with default driver
Creating volume "server_storage-temp" with default driver
Pulling rethinkdb (rethinkdb:2.3)...
2.3: Pulling from library/rethinkdb
2746a4a261c9: Download complete
4c1d20cdee96: Download complete
0d3160e1d0de: Download complete
c8e37668deea: Download complete
b7d1bf5200eb: Download complete
f6b24861af7c: Download complete
677900de5c00: Download complete
7c6f2faef424: Download complete

安装成功显示如下。 3.png

注意:如果容器中有任何服务起不来,建议将容器的内容(包括:Containers/Apps、Images、Volumes),全部删除,然后,重新执行 docker-compose up 命令。

这时候,你已经可以通过浏览器访问 http://192.168.199.191 查看 STF 服务,然后你会发现,浏览器访问不了,如下:

4.png

你需要将目标文件夹中的 server/cert/server.crt 导入进系统的“登陆钥匙串”中,并修改为“始终信任”。

5.png

这时候,你刷新浏览器,会发现高级中的选项,多了个熟悉的操作,点击继续前往192.168.199.191(不安全),就可以访问了。(账号/邮箱随意输入)

6.png

第 3 步:编译 stf_ios_support 服务器

回到 stf_ios_support 目录中,复制 config.json.example 到 config.json ,编辑里面的信息,因为是 json 文件,所以里面的注释要全部移除,有两段配置信息,上面一段 json 是最简单配置的事例,下面一段是完整配置事例,我们复制下面一段即可。

需要修改 json 字段中的 "xcode_dev_team_id"、 “stf”、“install”、“bin_paths” 中的信息,其他不用修改,如下:

{
   ...
   // iOS 开发者组织的 teamid,这个开发者证书的组织id有关,可以前往要是串查看
  "xcode_dev_team_id": "xxxx",
  "stf": {
    // STF 服务所在服务的 ip 地址,就是上文 server/.env 环境配置的地址
    "ip": "192.168.199.191",
    "hostname": "192.168.199.191"
  },
  "install": {
    // 修改为当前 stf_ios_support 中的目录地址即可
    "root_path": "xxxxx/xxxx/stf_ios_support",
    "config_path": "xxxxx/xxxx/stf_ios_support/config.json",
    "set_working_dir": false
  },
  "bin_paths": {
    // 这里需要修改 osx_ios_video_enabler => video_enabler
    "video_enabler": "bin/video_enabler",
  }
}

配置完成后,在命令行执行,make 命令,开始编译 stf_ios_support 服务器(同样科学上网),等待编译完成,信息如下:

zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % make
GIT_COMMIT="12aabb5d3ac0b9b6fce56de4a4c7368bff695f27" GIT_DATE="1619067214" GIT_REMOTE="https://github.com/DeviceFarmer/stf_ios_support.git" EASY_VERSION="1.0" /Applications/Xcode13-beta.app/Contents/Developer/usr/bin/make -C coordinator
go get
go get .
go build -o ../bin/coordinator -ldflags "-X main.GitCommit=12aabb5d3ac0b9b6fce56de4a4c7368bff695f27 -X main.GitDate=1619067214 -X main.GitRemote=https://github.com/DeviceFarmer/stf_ios_support.git -X main.EasyVersion=1.0" . 
git clone https://github.com/nanoscopic/ios_video_stream.git repos/ios_video_stream
Cloning into 'repos/ios_video_stream'...
remote: Enumerating objects: 1923, done.
remote: Counting objects: 100% (1923/1923), done.
remote: Compressing objects: 100% (649/649), done.
Receiving objects:  29% (574/1923), 11.18 MiB | 1.09 MiB/s s  

这时候,其实编译无法成功,首先你会遇到第 1 个报错,信息如下:

o build -o ios_video_pull .
go: github.com/nanoscopic/ios_video_pull: package github.com/google/gousb imported from implicitly required module; to add missing requirements, run:
	go get github.com/google/gousb@v0.0.0-20190812193832-18f4c1d8a750
make[1]: *** [ios_video_pull] Error 1
make: *** [repos/ios_video_pull/ios_video_pull] Error 2

这个报错是因为,ios_video_pull 的配置文件中缺少了 gousb 模块,需要修改 repos/ios_video_pull/go.mod 文件,内容如下:

module github.com/nanoscopic/ios_video_pull

go 1.14

require (
    github.com/danielpaulus/quicktime_video_hack v0.0.0-20200514194616-c4570b6b687c
    //  ******* BEGIN  添加这一行即可 ******
    github.com/google/gousb v0.0.0-20190812193832-18f4c1d8a750 
    //  ******* END  添加这一行即可 ******
    // indirect
    github.com/nanomsg/mangos v2.0.0+incompatible
    github.com/sirupsen/logrus v1.6.0
    go.nanomsg.org/mangos/v3 v3.0.1
    nanomsg.org/go/mangos/v2 v2.0.8 // indirect
)

然后,重新执行 make 命令,就继续往下走了,然后又会遇到第 2 个报错:

go build .
cp repos/wdaproxy/wdaproxy bin/wdaproxy
go get github.com/fsnotify/fsnotify
go get github.com/sirupsen/logrus
go build view_log.go
view_log.go:13:5: no required module provides package github.com/fsnotify/fsnotify: go.mod file not found in current directory or any parent directory; see 'go help modules'
view_log.go:14:5: no required module provides package github.com/sirupsen/logrus: go.mod file not found in current directory or any parent directory; see 'go help modules'
make: *** [view_log] Error 1
zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % 

意思是,go.mod 文档找不到,这时候需要执行如下命令:

go mod init view_log.go;
go mod tidy ;

继续 make,又继续往下走了...,接下来又会遇到 WebDriverAgent 的报错:

/bin/sh: ./Scripts/bootstrap.sh: No such file or directory
make: *** [repos/WebDriverAgent/Carthage/Checkouts/RoutingHTTPServer/Info.plist] Error 127

这是因为 WebDriverAgent 最新的项目你不用 bootstrap.sh 了,直接可以用,所以我们需要手动更改配置、并修改脚本文件。

首先,cd 到 stf_ios_support/repos/WebDriverAgent 的目录,使用 Xcode IDE 打开这个项目

7.png

修改 Target 为 WebDriverAgentLib 的 BundleId 属性,以及自己可用的描述文件。

8.png

修改 Target 为 WebDriverAgentRunner 的 BundleId,以及自己可用的描述文件,BundleId 和上文的值保持一致。

9.png

10.png

最后,可以修改 Target 目标,和构建的目标真机,可以按 command+U 进行测试。

11.png

有如下信息说明成功了。 12.png

然后,我们回到 stf_ios_support 上,找到 stf_ios_support/makefile 文件,找到下这行代码:

repos/WebDriverAgent/Carthage/Checkouts/RoutingHTTPServer/Info.plist: | repos/WebDriverAgent
    cd repos/WebDriverAgent && ./Scripts/bootstrap.sh

修改为如下:

repos/WebDriverAgent/Carthage/Checkouts/RoutingHTTPServer/Info.plist: | repos/WebDriverAgent
    pwd

然后,继续 make ...,基本上到这里,就 ok 了,如下信息:

    Signing Identity:     "Apple Development: 德振 朱 (5LLQQ4S25F)"
    Provisioning Profile: "epoint_develop"
                          (99f3dc52-90a2-4a39-ba42-2d4f14987b3e)
    
    /usr/bin/codesign --force --sign A7EAFF73719AECC112E9E746E0659D08F20BD121 --entitlements /Users/zhudezhen/Library/Developer/Xcode/DerivedData/WebDriverAgent-dvupaxwdbymikpdqfnltimkvxrvr/Build/Intermediates.noindex/WebDriverAgent.build/Debug-iphoneos/WebDriverAgentRunner.build/WebDriverAgentRunner.xctest.xcent --timestamp\=none --generate-entitlement-der /Users/zhudezhen/Library/Developer/Xcode/DerivedData/WebDriverAgent-dvupaxwdbymikpdqfnltimkvxrvr/Build/Products/Debug-iphoneos/WebDriverAgentRunner-Runner.app
/Users/zhudezhen/Library/Developer/Xcode/DerivedData/WebDriverAgent-dvupaxwdbymikpdqfnltimkvxrvr/Build/Products/Debug-iphoneos/WebDriverAgentRunner-Runner.app: replacing existing signature

** TEST BUILD SUCCEEDED **

zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % 

编译的产出物如下:

13.png

第 4 步

插上你的设备,然后执行如下命令:

stf_ios_support/run;

控制台会有如下,输出信息:

zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % ./run
INFO[0000] auto network interface set; using en0         interface_name=en0 type=default_iface
INFO[0000] Process start - device_trigger                binary=bin/ios-deploy proc=device_trigger type=proc_start
INFO[0000] Process start - stf_ios_provider              binary=/usr/local/opt/node@12/bin/node client_hostname=zhudezhendeMacBook-Pro.local client_ip=192.168.199.191 location=macmini/zhudezhendeMacBook-Pro.local proc=stf_ios_provider server_hostname=192.168.199.191 server_ip=192.168.199.191 type=proc_start
INFO[0000] Device object created                         dev_ios_port=9240 dev_name=iPhone15 dev_uuid="***401E" type=devd_create usbmuxd_port=9920 vid_port=8000 vnc_port=5901 wda_port=8100
INFO[0000] Device connected                              dev_name=iPhone15 dev_uuid="***401E" type=dev_connect
INFO[0000] Process start - ivf                           binary=bin/ivf_pull outSpec="tcp://127.0.0.1:7879" proc=ivf type=proc_start uuid="***401E"
INFO[0000] Process start - video_enabler                 binary=bin/video_enabler proc=video_enabler type=proc_start uuid="***401E"
INFO[0000] Process start - ios_video_stream              binary=bin/ios_video_stream port=8000 proc=ios_video_stream pullSpec="tcp://127.0.0.1:7879" tunName=en0 type=proc_start uuid="***401E"
WARN[0000] Process end - ivf                             proc=ivf type=proc_end uuid="***401E"
INFO[0000] Process start - ivf                           binary=bin/ivf_pull outSpec="tcp://127.0.0.1:7879" proc=ivf type=proc_start uuid="***401E"
INFO[0000] Device disconnected                           dev_name=iPhone15 dev_uuid="***401E" type=dev_disconnect
String to parse:{"type":"frame1","width":1170,"height":2532,"clickScale":1000,"uuid":"00008101-001945562001401E"

这时候,我们看到 stf_ios_support,似乎找到设备了,但是在STF网页上依旧找不到设备。

14.png

这是因为,wda 没有启动,所以没有设备,为什么没有启动呢?你可以把 stf_ios_support/config.json 中的 video.enable 修改为 false,重新尝试下:

  "video": {
    "enabled": false,
  },

重新启动服务 stf_ios_support/run,惊奇地发现:

dezhendeMacBook-Pro.local proc=stf_ios_provider server_hostname=192.168.199.191 server_ip=192.168.199.191 type=proc_start
INFO[0001] Device object created                         dev_ios_port=9240 dev_name=iPhone15 dev_uuid="***401E" type=devd_create usbmuxd_port=9920 vid_port=8000 vnc_port=5901 wda_port=8100
INFO[0001] Device connected                              dev_name=iPhone15 dev_uuid="***401E" type=dev_connect
trying to get ios version
INFO[0003] Process start - wdaproxy                      --iosDeploy=bin/ios-deploy binary=../wdaproxy iosVersion=15.0 proc=wdaproxy type=proc_start uuid="***401E" wdaPort=8100
INFO[0007] WDA Running                                   proc=wdaproxy type=wda_started uuid="***401E"
Status response: {"value":{"build":{"productBundleIdentifier":"com.facebook.WebDriverAgentRunner","time":"Jul 17 2021 14:34:00"},"device":{"name":"whocares","udid":"00008101-001945562001401E"},"ios":{"ip":"169.254.124.5"},"message":"WebDriverAgent is ready to accept commands","os":{"name":"iOS","sdkVersion":"15.0","testmanagerdVersion":28,"version":"15.0"},"ready":"true","state":"success"},"status":0}
INFO[0010] Fetched WDA session                           id=AD259002-A2CD-4C72-AF56-B38F42FB60B4 type=wda_session uuid="***401E"
window size response: {
  "value" : {
    "width" : 390,
    "height" : 844
  },
  "sessionId" : "AD259002-A2CD-4C72-AF56-B38F42FB60B4"
}
INFO[0010] Fetched device screen dimensions              height=844 type=device_dimensions uuid="***401E" width=390
INFO[0010] Process start - stf_device_ios                binary=/usr/local/opt/node@12/bin/node clickHeight=844 clickScale=1000 clickWidth=390 client_ip=192.168.199.191 device_name=iPhone15 frame_server="ws://192.168.199.191:8000/echo" node_port=9240 proc=stf_device_ios server_host=192.168.199.191 server_ip=192.168.199.191 stream_height=0 stream_width=0 type=proc_start uuid="***401E" video_port=8000 vnc_scale=2

WDA 居然启动了,于是在浏览器器上,我们发现了设备(居然还是 Android 的图标,真丑):

15.png

我们点击设备进去,发现黑屏,没有内容,啥也没有(T_T)

16.png

但是进行点击 home 按键、手势拖拽操作时,对应的设备也会进行操作,想必肯定和之前的 “video.enable" 有关,于是将参数,改回 true 之后,重新运行 stf_ios_support,发现 wda 又启不来,连不上 STF(T_T),无奈,我去看了 go 代码,反复打日志、重新编译,在 stf_ios_support/coordinator/coordiantor.go文件中,找到一处可疑代码:

if !o.config.Video.Enabled ||
    ( o.devd.okVidInterface == true && o.devd.okFirstFrame == true ) ||
    videoMethod == "app" {
        o.devd.wdaStarted = true
        
        time.Sleep( time.Second * 2 )
        
        fmt.Printf("trying to get ios version\n")
        
        log.WithFields( log.Fields{
            "type":        "ios_version",
            "dev_name":    o.devd.name,
            "dev_uuid":    uuid,
            "ios_version": o.devd.iosVersion,
        } ).Debug("IOS Version")

        proc_wdaproxy( o, devEventCh, false )
}

全文只有这里在执行启动 wda,用了!o.config.Video.Enable去判断,为什么?此时此刻,我不想分析作者的用意了,于是我将代码修改为如下:

// if !o.config.Video.Enabled ||
//     ( o.devd.okVidInterface == true && o.devd.okFirstFrame == true ) ||
//     videoMethod == "app" {
        o.devd.wdaStarted = true
        
        time.Sleep( time.Second * 2 )
        
        fmt.Printf("trying to get ios version\n")
        
        log.WithFields( log.Fields{
            "type":        "ios_version",
            "dev_name":    o.devd.name,
            "dev_uuid":    uuid,
            "ios_version": o.devd.iosVersion,
        } ).Debug("IOS Version")

        proc_wdaproxy( o, devEventCh, false )
// }

然后执行如下命令:

rm ./bin/coordinator && make && ./run

这时候,我们发现,视频插件和 wda 一起启动了,但是依旧看不到画面,这里是因为 iOS 的画面是依赖视频流的,所以需要在设备上挂起视频流服务,怎么操作呢?

需要做如下操作,验证是否连接到设备,可以使用如下命令:

stf_ios_support/bin/ivf_pull list;

如果什么也没有输出,说名设备没有链接上,这时候,可以选择系统的“quicktime-> 新建影片录制 -> 选择 iPhone 设备”,这时候再执行命令,我们发现会有如下输出:

zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % ./bin/ivf_pull list
--Device--
  Name:amp
  UDID:b5457eeaa93eb8a3c6ce3fee90c5c9f75251593b
zhudezhen@zhudezhendeMacBook-Pro stf_ios_support % 

然后依次,输入如下命令,信任设备:

// 设备信任
idevicepair pair
// 确认设备视频拉取服务的 pid
./bin/ios_video_pull -devices -decimal
// 重置设备的视频流
./bin/devreset [decimal product ID] 1452

然后重新执行 stf_ios_support/run,这一次效果如下:

17.png

18.png

画面也有了,同时也能远程操作,到这里,流程算是全部走完了。

FAQ

docker-compose up 安装报错

Creating server_rethinkdb_1 ... 
Creating server_auth_1      ... 
Creating server_storage-temp_1 ... error
Creating server_rethinkdb_1    ... done
Creating server_auth_1         ... done

Creating server_triproxy_1     ... done
Creating server_dev-triproxy_1 ... done
Creating server_migrate_1      ... done
Creating server_reaper_1       ... done
Creating server_processor_1    ... done
Creating server_api_1          ... done

ERROR: for storage-temp  Cannot start service storage-temp: OCI runtime create failed: invalid mount {Destination:data Type:bind Source:/var/lib/docker/volumes/af0f503b3f078ce72a2b09a66ca00da675709239e3c4f67bcf42575e44aa511d/_data Options:[rbind]}: mount destination data not absolute: unknown
ERROR: Encountered errors while bringing up the project.

需要修改目标目录 server/storage-temp/Dockerfile 文件中的 VOLUME 中的值为绝对目录,然后清理 Docker 中的环境,重新执行 docker-compose up 命令,如下:

FROM livxtrm/devicefarmer:latest

USER root
RUN mkdir data && chown stf:stf data
USER stf
VOLUME ["/Users/zhudezhen/Desktop/xxxx/xxx/基于STF的iOS远程真机器控制部署指南/test/server/storage-temp/data"]

iOS15 设备支持不是特别好

测试下来,对 iOS15 的设备,链接不是很稳定,经常断开,wda 起不来。

quicktime 可以关掉吗?

连接上之后,可以关掉的。

偶尔 ctrl+c 关不掉服务,怎么办?

需要前往“活动监视器”,找到 coordinator 服务,强制关闭即可。

多设备连接问题

设备分别信任可以使用以下命令

idevicepair pair -u [设备 udid]

修改文件

stf_ios_provider/repos/stf-ios-provider/lib/units/device-ios/support/deviceinfo.js

设备信息获取不正确,ios-deploy 这个程序存在问题,需要手动将判断修改为 false

原来代码
  if( options.iosDeployPath != '' ) {
            devInfo = iosDeployInfo( options, serial, [
                "ModelNumber",
                "InternationalMobileSubscriberIdentity",
                "IntegratedCircuitCardIdentity",
                "CPUArchitecture",
                "ProductVersion"
            ] );
        }
        else devInfo = iDeviceInfo( serial )


修改成
  if( false ) {
            devInfo = iosDeployInfo( options, serial, [
                "ModelNumber",
                "InternationalMobileSubscriberIdentity",
                "IntegratedCircuitCardIdentity",
                "CPUArchitecture",
                "ProductVersion"
            ] );
        }
        else devInfo = iDeviceInfo( serial )