STF STF 集成 iOS 之设备连接

mrx102 · April 04, 2019 · Last by mrx102 replied at June 19, 2019 · 3156 hits

前言

目前,iOS的设备集群工具已经有不少了,但是还没人直接在STF上做集成,现在就来说下STF怎么集成iOS。

Android的设备连接

我们先来看下Android的设备是怎么连接上的。在STF中,设备是通过provider进程来管理的,一个provider可以连接多个设备,设备的连接就是通过provider这个进程来处理的。我们先找到provider的代码,在/stf/lib/units/provider/index.js文件,注意以下代码段:

var client = adb.createClient({
host: options.adbHost
, port: options.adbPort
})
......
// Track and manage devices
client.trackDevices().then(function(tracker) {
log.info('Tracking devices')
......
tracker.on('add', filterDevice(function(device) {
log.info('Found device "%s" (%s)', device.id, device.type)
......
tracker.on('change', filterDevice(function(device) {
flippedTracker.emit(device.id, 'change', device)
}))
tracker.on('remove', filterDevice(function(device) {
flippedTracker.emit(device.id, 'remove', device)
}))

首先,通过传入参数adbHost和adbPort创建一个adbkit的对象client,调用client.trackDevices返回tracker对象,然后监听tracker的add/change/remove事件,顾名思义,这三个事件分别就是设备的增加/改变/删除了。我们主要看下add事件的处理函数里做了什么,首先有个匿名函数

// Wait for others to acknowledge the device
var register = new Promise(function(resolve) {
// Tell others we found a device
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceIntroductionMessage(
device.id
, wireutil.toDeviceStatus(device.type)
, new wire.ProviderMessage(
solo
, options.name
)
))
])

privateTracker.once('register', resolve)
})

这个函数的作用就是通过zmq发送DeviceIntroductionMessage消息通知其他模块,我发现了一个设备。接下来有一个spawn函数

// Spawn a device worker
function spawn() {
var allocatedPorts = ports.splice(0, 4)
var proc = options.fork(device, allocatedPorts.slice())
var resolver = Promise.defer()
......
function messageListener(message) {
switch (message) {
case 'ready':
_.pull(lists.waiting, device.id)
lists.ready.push(device.id)
......
}
}
......
lists.waiting.push(device.id)

这个options.fork是什么呢?我们知道options是传入的参数对象,那我们先看看provider的传入参数有哪些,找到文件/stf/stf/lib/cli/provider/index.js

return require('../../units/provider')({
name: argv.name
......
, fork: function(device, ports) {
var fork = require('child_process').fork
var args = [
'device'
, '--serial', device.id
, '--provider', argv.name
......
]
......
return fork(cli, args)
}
......

这里可以看到fork参数实际上是一个函数,在这个函数里通过fork的方式启动了一个device的子进程。再回到前面spawn函数,也就是说,spawn函数实际上是开启了一个device子进程,后面的代码就是对device子进程事件和消息的处理了。可能有人已经注意到了lists,这个lists的定义如下:

var lists = {
all: []
, ready: []
, waiting: []
}

lists用来存储所有设备和两种状态的设备列表,设备首先会添加到waiting列表,只有当device进程的所有模块加载完成,收到device进程的ready消息才会被添加到ready列表,同时从waiting列表移除。接下来有个work函数

// Starts a device worker and keeps it alive
function work() {
return (worker = workers[device.id] = spawn())
.then(function() {
......
}
......
}

这个主要是调用spawn函数启动device进程,如果进程抛出异常则重启device进程。那么work函数又是在哪调用的呢,这里面的函数调用链如下:匿名函数发送DeviceIntroductionMessage消息返回register-->register.then(()=>check())-->work-->spawn-->开启device子进程。这就是设备连接的大致过程。

iOS的设备连接

了解了android设备的连接过程,那要接入iOS的设备连接就简单多了。从上面的分析可以知道,其实大部分的东西我们可以复用android的,我们需要做的只是一个发现iOS设备的模块,当有设备新增时,发送一个add消息,有设备断开时,发送remove消息,就是这么简单。
那么iOS用什么工具来发现设备呢,答案当然是功能强大的libimobiledevice工具集了,我们用idevice_id这个工具来获取设备列表,具体命令为idevice_id -l。值得注意的时,add/remove消息发送的数据是包含id和type的map,type可取的值如下(取左边的),如果填了其他的值,会出现状态转换错误。

{
device: 'ONLINE'
, emulator: 'ONLINE'
, unauthorized: 'UNAUTHORIZED'
, offline: 'OFFLINE'
, connecting: 'CONNECTING'
, authorizing: 'AUTHORIZING'
}

设备信息获取

在设备连接完成后,我们需要向前端发送设备信息。我们先来看下device模块,打开文件/stf/lib/units/device/index.js,

return syrup.serial()
// We want to send logs before anything else starts happening
......
.define(function(options) {
return syrup.serial()
.dependency(require('./plugins/heartbeat'))
.dependency(require('./plugins/solo'))
......
.define(function(options, heartbeat, solo) {
if (process.send) {
// Only if we have a parent process
process.send('ready')
}
log.info('Fully operational')
return solo.poke()
})
......
})

可以看到device进程会加载很多的依赖模块,设备信息的获取主要在solo这个模块,打开文件/stf/lib/units/device/plugins/solo.js

router.on(wire.ProbeMessage, function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceIdentityMessage(
options.serial
......
})
return {
channel: channel
, poke: function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceReadyMessage(
options.serial
......
}

可以看到这个文件里会发出两个广播消息,一个是DeviceIdentityMessage,一个是DeviceReadyMessage,很显然,第一个就是设备信息的消息了,而第二个则是在poke方法中发送的.在上一段代码中可以看到,在最后调用了solo.poke,就是通知前端,这个设备已经准备好了,可以使用了。
那么设备信息的获取是在哪里呢,我们看solo文件中引用的identity模块,打开/stf/lib/units/device/plugins/util/identity.js文件

function solve() {
log.info('Solving identity')
var identity = devutil.makeIdentity(options.serial, properties)
identity.display = display.properties
identity.phone = phone
return identity
}

这里返回了一个identity对象,这就是我们需要的设备信息了。这个对象包含了三个模块,一个用于获取设备的基础信息properties,一个用于获取设备的屏幕信息display,还有一个则用于获取手机imei/联网状态等信息。对于iOS,我们需要重写properties和display,在properties中,使用libimobiledevice工具集下的ideviceinfo获取基础信息。至于屏幕信息,我目前只获取了分辨率,而且是通过截图,取图片分辨率的方式获取的,目前没找到其他更好的办法获取iOS的分辨率。
做完这些,那恭喜你,你可以在STF的设备列表上看到你的iOS设备了。
最后上个图给大家看看效果,我这里改了前端页面,样式比较丑,大家可以忽略。

参考文档:

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 13 条回复 时间 点赞
mrx102 关闭了讨论 06 Apr 09:13
mrx102 重新开启了讨论 06 Apr 09:13

支持楼主的分享精神,会帮助越来越多的人

Sutune · #4 · May 17, 2019
Author only
5Floor has been deleted

好的,谢谢。

mrx102 STF 集成 iOS 之远程控制 中提及了此贴 17 May 18:55

这个是真的还是假的?怎么没有人搭理呢

stronger iOS 远程真机方案整理 中提及了此贴 04 Jun 14:21
mrx102 #10 · June 04, 2019 作者
stronger 回复

我有必要写个假的么?

佩服!好多大神,比如codebluesky都没完成在stf基础上加上ios,而是另起炉灶弄了atxserver2的。楼主有git地址吗,给大家分享下

有个疑问,这里只提到了如何让iOS设备出现在设备列表中,好像没有提到如何使用 stf 控制 iOS 设备?

mrx102 #13 · June 05, 2019 作者
stronger 回复

由于是公司项目,得公司这边同意才能分享出来,目前在推进开源

mrx102 #14 · June 05, 2019 作者
陈恒捷 回复

https://testerhome.com/topics/19203在这篇里面有说屏幕传输和远程控制

mrx102 回复

哦哦,那很期待啊!希望尽快能公布出来哈

赞,虎牙哥这么好的帖子,必须加精,不仅实现了STF 的ios-provider,而且对OC 源码进行了修改,提高了ios-远程真机的流畅度

琉丶言 回复

跪求分享呀,好多兄弟在呼唤~~~

mrx102 STF 集成 iOS 之 开源了 中提及了此贴 19 Jun 17:47
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up