STF [STF] 二次开发之批量安装 APK

Lucas · January 20, 2018 · Last by Lucas replied at October 29, 2018 · 5431 hits
本帖已被设为精华帖!

1. 前言

STF四年前就有了吧?很遗憾,没能赶上时代的浪潮,现在只能作为债务进行偿还。我写不出这么牛的框架,只能在其基础上平凡创新,批量安装就是一个例子。当然这个也是来自部门的需要,我作为基层实施者,通过我的实践谈谈我的理解。如果有人从中受益,也算我为社区做了点贡献。

2. 安装

不做介绍了,这方面的内容实在太多了,自己找找多试试就行了。

3. 基础知识

这个也不打算介绍,鄙人毕竟非资深前端。除了官网介绍的几个安装组件,以下内容还是要知道的,否则改造起来就会一头雾水。(磨刀不误砍材工嘛)

  1. NodeJS,基石。
  2. express,http服务器。
  3. socket.io,websocket通信。
  4. protobuf,通信。
  5. ZMQ,消息队列,各种模式需要知道:pub/sub,push/pull,router/dealer,request/reply。
  6. angluar,前端mvc框架,这些概念要清楚:module,controller,router,service,directive。
  7. bower,包管理工具,angular的组件多是基于brower。
  8. wepback,打包工具,将nodejs打包成js,这里是打包前端Angular。
  9. bluebird,ES6之前的支持Promise编码形式的框架,对该组件的使用主要是明白STF用它做了什么。
  10. Event,清楚如何注册事件,出发websocket以及zmq的。很多路由多是基于event。
  11. rethinkdb,这个列出来是因为前端拉取设备信息是从db里读取出来的。(原来想修改设备表结构了)
  12. minitouchminicapminirevSTFService.apk等高级linux知识略。

4. STF的拓扑结构

这个拓扑一开始肯定懵,尤其ZMQ不熟悉,还有triproxy干什么的根本不知道,所以不下一番功夫理解是有一定难度的。
如何理解这个拓扑,我是先看拓扑,再看代码,然后看部署文档+代码一起看,反复看,之后自己加log打日志,再仔细阅读文档。一定要自己阅读deployment.md这个文档,而且要结合units里面的代码看,适当看看cli里的参数帮助理解个unit之间的绑定关系。在强调一遍,deployment.md这个文档仔细看。
拓扑图如下。(有人已经做过介绍了,可以自行搜索,我这里再重复唠叨唠叨方便记忆,算是查缺补漏吧)

5. 拓扑结构解释

我就按照从上到下的顺序说说我的理解吧,有问题欢迎指正。
(当然,这些内容也出自deployment.md加上我个人的一些理解)

5.1 websocket

websocket本机实际启动了一个websocket,这个unit是一个中间层,它用来将客户端的javscript与服务端的zmq以及wire proto进行结合。客户端实际上就是前端那个界面,前端代码对应的是app,前端的载体及服务器对应的是unit的app。zmq和protobuf这个对应的比较多,多个unit都用到了。

5.2 triproxy

这个组件没有任何逻辑,他就是个分发器,这个triproxy连接的是websocket和processor,5.1说了websocket是客户端与服务端的结合者,客户端是app,app就是stf启动的那个前端网站,所以它用来接收和发送app端(网站)发来的请求,然后分发给processor进行处理。所以这个triproxy是app端的proxy,文档有介绍。triproxy应该翻译成三端代理,就想文档中介绍的,它实际连接了三个端,app端pull,app端pub,processor端dealer。(triple是三个一组的意思,triple-proxy简称triproxy,三端一个组)

5.3 processor

processor是stf的主要工作组件,它连接了devices和app之间的几乎所有的通信。devices指代设备,app指代前端app,这说明它连接了前端和设备。前端其实不只app,还有storage,storage负责apk、图片的上传下载等逻辑。

5.4 triproxy

又回到了triproxy,这个是dev端proxy,即设备端的triproxy。它是用来发送和接受来自provider端的请求(provider指的是你的宿主机),然后分发给processor组件去处理。

5.5 dev

这个是设备unit,文档上没有说明。看一下units/device的代码就知道了,主要封装了对设备操作的逻辑。譬如实际的apk安装,touch操作,实时显示图像,调用adbkit安装app等逻辑。

5.6 provider

provider用来连接adb,然后为每个设备启动一个worker process。之后接受并发送从这些processors传过了的命令。

6. 批量安装实践

前面讲了大部分概念,也没有涉及具体实践。这是因为,不搞懂前面的诸多内容和原理,改造起来实在是个一团糟。没有一个清晰的思路和逻辑,只能越做越懵。

6.1 改动的文件

6.2 device-list/index.js

  1. 引入ng-file-upload。
  2. angluar引入文件上传服务。
require('./device-list.css')
require('ng-file-upload') // 这里引用了angluar上传文件功能

module.exports = angular.module('device-list', [
require('angular-xeditable').name,
require('stf/device').name,
require('stf/user/group').name,
require('stf/control').name,
require('stf/common-ui').name,
require('stf/settings').name,
require('./column').name,
require('./details').name,
require('./empty').name,
require('./icons').name,
require('./stats').name,
require('./customize').name,
require('./search').name,
'angularFileUpload' // 这里添加上传文件服务
])
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/devices', {
template: require('./device-list.pug'),
controller: 'DeviceListCtrl'
})
}])
.run(function(editableOptions) {
// bootstrap3 theme for xeditables
editableOptions.theme = 'bs3'
})
.controller('DeviceListCtrl', require('./device-list-controller'))

6.3 device-list/device-controller.js

  1. 在controller最后添加installApk方法。
  2. 使用$http服务获取设备信息。
  3. 使用upload服务将文件上传到服务器并获取响应里面的上传路径地址以及manifest信息。
  4. 调用angular的controlService,在app/components/stf/control。
  5. 调用install方法。
$scope.installApk = function($files) {
var installDevices = []
var cbList = document.getElementsByClassName('installCheckbox')

if($files.length) {
$http.get('/api/v1/devices')
.success(function(res) {
var devices = res.devices
var len = cbList.length

;devices.forEach(function(device) {
for (var i = 0; i < len; i++) {
var cb = cbList[i]
var serial = cb.getAttribute('serial')

if (cb.checked && serial === device.serial) {
installDevices.push(device)
}
}
})
})

$upload.upload({
url: '/s/upload/apk'
, method: 'POST'
, file: $files
})
.then(function(value) {
var href = value.data.resources.file.href
$http.get(href + '/manifest')
.then(function(res) {
if (res.data.success) {
installDevices.forEach(function(installDevice) {
var control = ControlService.create(installDevice, installDevice.channel)
control.install({
href: href
, manifest: res.data.manifest
, launch: true
})
.progressed(function(result) {

})
})
} else {
throw new Error('Install apk to all selected devices failed')
}
})
.then(function() {
console.log('Install apk to all devices task succeed')
})
.catch(function(err) {
throw new Error('Install apk meet error.')
})
})
}
}

6.4 device-list/device-list.pug

这个是前端,我随便加了一个安装apk的ng方法

.stf-device-list
.row.stf-stats-container.unselectable
.col-md-12
device-list-stats(tracker='tracker')

.row.unselectable
.col-md-12
.widget-container.fluid-height.stf-device-list-tabs
.widget-content.padded
.filtering-buttons
datalist(id='searchFields')
select(name='searchFields')
option(ng-repeat='column in columns', ng-value='column.name + ": "',
ng-bind='columnDefinitions[column.name].title | translate')
input(type='search', autosave='deviceFilter'
name='deviceFilter', ng-model='search.deviceFilter', ng-change='applyFilter(search.deviceFilter)',
ng-model-options='{debounce: 150}'
autocorrect='off', autocapitalize='off', spellcheck='false',
list='searchFields', multiple, focus-element='search.focusElement',
text-focus-select, accesskey='4').form-control.input-sm.device-search.pull-right

span.pull-right(ng-if='activeTabs.details && !$root.basicMode')
// ====== 加在这个地方了 ====
.btn-group(uib-dropdown).pull-right
button.btn.btn-sm.btn-primary-outline(type='button', ng-file-select='installApk($files)')
i.fa.fa-columns
span install apk
// ========================
button.btn.btn-sm.btn-primary-outline(type='button', uib-dropdown-toggle)
i.fa.fa-columns
span(ng-bind='"Customize"|translate')

6.5 device-list/column/device-column-service.js

  1. 为DeviceColuknService添加一列。
  2. 添加DeviceInsallCell方法用来显示该列。
install: DeviceInstallCell({
title: 'Install'
})
function DeviceInstallCell(options) {
return _.defaults(options, {
title: options.title
, defaultOrder: 'asc'
, build: function() {
var td = document.createElement('td')
var input = document.createElement('input')
input.type = 'checkbox'
input.className = 'installCheckbox'
td.appendChild(input)

return td
}
, update: function(td, device) {
var cb = td.firstChild

if(device.state === 'using' || device.state === 'absent') {
cb.disabled = true
} else {
cb.setAttribute('serial', device.serial)
}

return td
}
, compare: function(a, b) {

}
, filter: function(item, filter) {

}
})
}

7. 实际界面

8 安装流程分析(自顶向下)

  1. STF的安装是在dashboard里面的安装逻辑的。
  2. 顶层的apk安装逻辑在app/control-panes/dashboard/install/install-controller.js

    $scope.installFile = function($files) {
    if ($files.length) {
    return InstallService.installFile($scope.control, $files)
    }
    }
  3. 这里面调用了InstallService的installFile方法。

  4. InstallService在app/component/stf/install/install-service.js。

    installService.installFile = function(control, $files) {
    var installation = new Installation('uploading')
    $rootScope.$broadcast('installation', installation)
    return StorageService.storeFile('apk', $files, {
    filter: function(file) {
    return /\.apk$/i.test(file.name)
    }
    })
    .progressed(function(e) {
    if (e.lengthComputable) {
    installation.update(e.loaded / e.total * 100 / 2, 'uploading')
    }
    })
    .then(function(res) {
    installation.update(100 / 2, 'processing')
    installation.href = res.data.resources.file.href

    return $http.get(installation.href + '/manifest')
    .then(function(res) {
    if (res.data.success) {
    installation.manifest = res.data.manifest
    return control.install({
    href: installation.href
    , manifest: installation.manifest
    , launch: installation.launch
    })
    .progressed(function(result) {
    installation.update(50 + result.progress / 2, result.lastData)
    })
    }
    else {
    throw new Error('Unable to retrieve manifest')
    }
    })
    })
    .then(function() {
    installation.okay('installed')
    })
    .catch(function(err) {
    installation.fail(err.code || err.message)
    })
    }
  5. installFile里面广播了一个instalation事件,这个主要是通知前端也即更新前端进度条状态信息。

  6. 之后调用StorageService的storeFile,同时过滤掉非apk结尾的文件。这个调用跟我上面写的上传逻辑是一样的。stf在安装apk的时候,需要先将apk上传到/var/1的目录里面,然后获取apk的安装路径,以及manifest信息,这些信息需要传给controll.install方法,下面是StorageService的上传文件的方法。这个方法的upload调用的unit/storage/temp.js里面的api,temp标识将文件上传的临时区域或临时内存等等。

    service.storeFile = function(type, files, options) {
    var resolver = Promise.defer()
    var input = options.filter ? files.filter(options.filter) : files

    if (input.length) {
    $upload.upload({
    url: '/s/upload/' + type
    , method: 'POST'
    , file: input
    })
    .then(
    function(value) {
    resolver.resolve(value)
    }
    , function(err) {
    resolver.reject(err)
    }
    , function(progressEvent) {
    resolver.progress(progressEvent)
    }
    )
    }
    else {
    var err = new Error('No input files')
    err.code = 'no_input_files'
    resolver.reject(err)
    }

    return resolver.promise
    }
  7. 上传成功后即调用controll.install进行安装。需要注意的是,controll是在device-list-controller里面已经初始化了的。通过ControllService.create初始化。该方法接受两个参数,一个是target是设备实体信息即device,对应数据库的device的一条记录。channel是通道信息,其实就是sub订阅的一个message。

  8. install发送device.isntall的websocket命令,scoket出发device.install事件。这个socket在socket及transaction服务里面进行定义。

    this.install = function(options) {
    return sendTwoWay('device.install', options)
    }

    function sendTwoWay(action, data) {
    var tx = TransactionService.create(target)
    socket.emit(action, channel, tx.channel, data)
    return tx.promise
    }
  9. websocket的uni接收到前端的时间后,并将其装成protobuf,然后推送到zmq。

    .on('device.install', function(channel, responseChannel, data) {
    joinChannel(responseChannel)
    push.send([
    channel
    , wireutil.transaction(
    responseChannel
    , new wire.InstallMessage(
    data.href
    , data.launch === true
    , JSON.stringify(data.manifest)
    )
    )
    ])
    })
  10. 设备端收到消息后,正是开始安装apk。

    router.on(wire.InstallMessage, function(channel, message) {
    function pushApp() {
    var req = request({
    url: url.resolve(options.storageUrl, message.href)
    })

    // We need to catch the Content-Length on the fly or we risk
    // losing some of the initial chunks.
    var contentLength = null
    req.on('response', function(res) {
    contentLength = parseInt(res.headers['content-length'], 10)
    })

    var source = new stream.Readable().wrap(req)
    var target = '/data/local/tmp/_app.apk'

    return adb.push(options.serial, source, target)
    .timeout(10000)
    .then(function(transfer) {
    var resolver = Promise.defer()

    function progressListener(stats) {
    if (contentLength) {
    // Progress 0% to 70%
    sendProgress(
    'pushing_app'
    , 50 * Math.max(0, Math.min(
    50
    , stats.bytesTransferred / contentLength
    ))
    )
    }
    }

    function errorListener(err) {
    resolver.reject(err)
    }

    function endListener() {
    resolver.resolve(target)
    }

    transfer.on('progress', progressListener)
    transfer.on('error', errorListener)
    transfer.on('end', endListener)

    return resolver.promise.finally(function() {
    transfer.removeListener('progress', progressListener)
    transfer.removeListener('error', errorListener)
    transfer.removeListener('end', endListener)
    })
    })
    }
    }
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 31 条回复 时间 点赞
Lucas #1 · January 20, 2018 作者

m

Lucas #2 · January 20, 2018 作者

一开始可能觉得STF比较高深莫测,随着技术的熟悉,以及思路的理清,并没有想象的那么难。

被你一讲就像回过头来看大学课程

恒温 将本帖设为了精华贴 20 Jan 22:41
Lucas #5 · January 22, 2018 作者
恒温 回复

恒温过奖了。回头再看,发现还是有很多不足的地方。以后弥补。

不考虑不同机型安装时需要点击确定按钮么?而且这种写法一定会导致手机断链的情况。

@lucasluo 感谢分享,请问安装过程中的系统弹框有什么好的处理机制不?

Lucas #8 · January 26, 2018 作者
0x88 回复

恩,暂时先不考虑,先上一版,满足基础需求,以后优化。如有好建议,也希望多指教。

Lucas #9 · January 26, 2018 作者
Kun 回复

不root进行静默安装的话,有计划修改STFService.apk通过Accessibility安装,规划中。有些手机可以更改系统设置,多数还是不太支持。

这样写,安装进度,安装错误信息什么的都没有

Lucas #11 · January 30, 2018 作者
mling 回复

对,是的。正在优化中。高手,一眼看穿问题本质。赞。

赞一个!只是对STF上传apk装包是覆盖装包。
😂 默默的搞了个页面批量 可卸载可覆盖 安装apk的功能。

感觉大家都还是挺多的时间专门去深入研究

—— 来自TesterHome官方 安卓客户端

代码和你的一样,为什么页面没有显示install apk按钮

Lucas #15 · April 28, 2018 作者
fs123wb 回复

所有的改动都一致么?

Lucas 回复

对啊,服务也重启了,node.js一点不会,直接复制粘贴你的代码

17Floor has been deleted
18Floor has been deleted
19Floor has been deleted
20Floor has been deleted
21Floor has been deleted
Lucas #22 · April 28, 2018 作者

device-list-controller的defaultColumns的json对象字面量添加一下:

var defaultColumns = [
{
name: 'install' // <-------------
, selected: true
}
, {
name: 'state'
, selected: true
}
Lucas 回复


改完了,还是没有出现

Lucas #25 · April 28, 2018 作者
fs123wb 回复

npm link一下。webpack得重编译。

Lucas 回复

有了,大神请收下我的膝盖,谢谢

Lucas #27 · April 28, 2018 作者
fs123wb 回复

客气了,我不是大神。

Lucas 回复

选择文件上传后,怎么一点反应都没有,没有安装apk

Lucas #29 · April 28, 2018 作者
fs123wb 回复

这个方案比较简单,你后台看一下日志,或者打开浏览器的控制台,输出一些东西。

@lucasluo 我照着你的提供的代码修改了之后,执行npm link后,把服务起来,依然没有什么效果,是还需要什么步骤吗?

Lucas #31 · May 15, 2018 作者

不是很清楚你具体怎么改的。你看看fs123wb同学的问题,是否你也遇到了。


点击重置按钮,install就出来了

楼主的环境是什么那,各种依赖都安装了 为什么我在源码目录下执行npm install,总是报错,然后装了个phantomjs,再执行npm install ,又报fsevents版本不支持的错,我这儿用的centos7,node9.0.0,npm是6.4.1

Lucas #34 · September 17, 2018 作者
zhanglimin 回复

centos 7我部署过好几套。基本什么坑都遇见过。zmq,lib-jpeg,node等源码安装。确认gcc版本,centos7 应该不需要确认。如果报gcc的错,升级即可。

Lucas 回复

嗯 谢谢 已解决。是因为npm没把包装全,换成cnpm好了。而且出现的bower安装包问题,单独安装也可以了,bower安装需要使用root权限。

感谢提供思路,大神少说了一个要改的地方device-list-controller.js的DeviceListCtrl需要引用$http和$upload,要不然点了install后没反应~
看了下好像stf底层也是通过adb命令来安装的,不过这样很多手机会安装不成功,因为会弹出确认框不点的话就安装失败了~
感觉这个静默安装有点麻烦的

Lucas #37 · October 29, 2018 作者
zlp 回复

嗯。是的。

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 13 Dec 14:44
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up