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

lucasluo · 2018年01月20日 · 最后由 fs123wb 回复于 2018年05月17日 · 2167 次阅读
本帖已被设为精华帖!

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)
            })
          })
      }
    }
    
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 26 条回复 时间 点赞

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

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

Lihuazhang 将本帖设为了精华贴 01月20日 22:41
Lihuazhang 回复

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

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

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

0x88 回复

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

qawow 回复

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

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

mling 回复

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

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

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

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

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

fs123wb 回复

所有的改动都一致么?

lucasluo 回复

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

17楼 已删除
18楼 已删除
19楼 已删除
20楼 已删除
21楼 已删除

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

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


改完了,还是没有出现

fs123wb 回复

npm link一下。webpack得重编译。

lucasluo 回复

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

fs123wb 回复

客气了,我不是大神。

lucasluo 回复

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

fs123wb 回复

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

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

cc0804_ 回复

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


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

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册