原文地址:https://testerhome.com/articles/22617
云服安全组需要放开对应的端口访问权限即可
需要优化sft的stream.js文件内容,修改后最新文件如下:
1 var util = require('util') 2 var gm = require('gm').subClass({imageMagick: true}) 3 var Promise = require('bluebird') 4 var syrup = require('@devicefarmer/stf-syrup') 5 var WebSocket = require('ws') 6 var uuid = require('uuid') 7 var EventEmitter = require('eventemitter3') 8 var split = require('split') 9 var adbkit = require('@devicefarmer/adbkit') 10 11 var logger = require('../../../../util/logger') 12 var lifecycle = require('../../../../util/lifecycle') 13 var bannerutil = require('./util/banner') 14 var FrameParser = require('./util/frameparser') 15 var FrameConfig = require('./util/frameconfig') 16 var BroadcastSet = require('./util/broadcastset') 17 var StateQueue = require('../../../../util/statequeue') 18 var RiskyStream = require('../../../../util/riskystream') 19 var FailCounter = require('../../../../util/failcounter') 20 21 module.exports = syrup.serial() 22 .dependency(require('../../support/adb')) 23 .dependency(require('../../resources/minicap')) 24 .dependency(require('../util/display')) 25 .dependency(require('./options')) 26 .define(function(options, adb, minicap, display, screenOptions) { 27 var log = logger.createLogger('device:plugins:screen:stream') 28 29 function FrameProducer(config) { 30 EventEmitter.call(this) 31 this.actionQueue = [] 32 this.runningState = FrameProducer.STATE_STOPPED 33 this.desiredState = new StateQueue() 34 this.output = null 35 this.socket = null 36 this.pid = -1 37 this.banner = null 38 this.parser = null 39 this.frameConfig = config 40 this.readable = false 41 this.needsReadable = false 42 this.failCounter = new FailCounter(3, 10000) 43 this.failCounter.on('exceedLimit', this._failLimitExceeded.bind(this)) 44 this.failed = false 45 this.readableListener = this._readableListener.bind(this) 46 } 47 48 util.inherits(FrameProducer, EventEmitter) 49 50 FrameProducer.STATE_STOPPED = 1 51 FrameProducer.STATE_STARTING = 2 52 FrameProducer.STATE_STARTED = 3 53 FrameProducer.STATE_STOPPING = 4 54 55 FrameProducer.prototype._ensureState = function() { 56 if (this.desiredState.empty()) { 57 return 58 } 59 60 if (this.failed) { 61 log.warn('Will not apply desired state due to too many failures') 62 return 63 } 64 65 switch (this.runningState) { 66 case FrameProducer.STATE_STARTING: 67 case FrameProducer.STATE_STOPPING: 68 // Just wait. 69 break 70 case FrameProducer.STATE_STOPPED: 71 if (this.desiredState.next() === FrameProducer.STATE_STARTED) { 72 this.runningState = FrameProducer.STATE_STARTING 73 this._startService().bind(this) 74 .then(function(out) { 75 this.output = new RiskyStream(out) 76 .on('unexpectedEnd', this._outputEnded.bind(this)) 77 return this._readOutput(this.output.stream) 78 }) 79 .then(function() { 80 return this._waitForPid() 81 }) 82 .then(function() { 83 return this._connectService() 84 }) 85 .then(function(socket) { 86 this.parser = new FrameParser() 87 this.socket = new RiskyStream(socket) 88 .on('unexpectedEnd', this._socketEnded.bind(this)) 89 return this._readBanner(this.socket.stream) 90 }) 91 .then(function(banner) { 92 this.banner = banner 93 return this._readFrames(this.socket.stream) 94 }) 95 .then(function() { 96 this.runningState = FrameProducer.STATE_STARTED 97 this.emit('start') 98 }) 99 .catch(Promise.CancellationError, function() { 100 return this._stop() 101 }) 102 .catch(function(err) { 103 return this._stop().finally(function() { 104 this.failCounter.inc() 105 this.emit('error', err) 106 }) 107 }) 108 .finally(function() { 109 this._ensureState() 110 }) 111 } 112 else { 113 setImmediate(this._ensureState.bind(this)) 114 } 115 break 116 case FrameProducer.STATE_STARTED: 117 if (this.desiredState.next() === FrameProducer.STATE_STOPPED) { 118 this.runningState = FrameProducer.STATE_STOPPING 119 this._stop().finally(function() { 120 this._ensureState() 121 }) 122 } 123 else { 124 setImmediate(this._ensureState.bind(this)) 125 } 126 break 127 } 128 } 129 130 FrameProducer.prototype.start = function() { 131 log.info('Requesting frame producer to start') 132 this.desiredState.push(FrameProducer.STATE_STARTED) 133 this._ensureState() 134 } 135 136 FrameProducer.prototype.stop = function() { 137 log.info('Requesting frame producer to stop') 138 this.desiredState.push(FrameProducer.STATE_STOPPED) 139 this._ensureState() 140 } 141 142 FrameProducer.prototype.restart = function() { 143 switch (this.runningState) { 144 case FrameProducer.STATE_STARTED: 145 case FrameProducer.STATE_STARTING: 146 this.desiredState.push(FrameProducer.STATE_STOPPED) 147 this.desiredState.push(FrameProducer.STATE_STARTED) 148 this._ensureState() 149 break 150 } 151 } 152 153 FrameProducer.prototype.updateRotation = function(rotation) { 154 if (this.frameConfig.rotation === rotation) { 155 log.info('Keeping %d as current frame producer rotation', rotation) 156 return 157 } 158 159 log.info('Setting frame producer rotation to %d', rotation) 160 this.frameConfig.rotation = rotation 161 this._configChanged() 162 } 163 164 FrameProducer.prototype.updateProjection = function(width, height) { 165 if (this.frameConfig.virtualWidth === width && 166 this.frameConfig.virtualHeight === height) { 167 log.info( 168 'Keeping %dx%d as current frame producer projection', width, height) 169 return 170 } 171 172 log.info('Setting frame producer projection to %dx%d', width, height) 173 this.frameConfig.virtualWidth = width 174 this.frameConfig.virtualHeight = height 175 this._configChanged() 176 } 177 178 FrameProducer.prototype.nextFrame = function() { 179 var frame = null 180 var chunk 181 182 if (this.parser) { 183 while ((frame = this.parser.nextFrame()) === null) { 184 chunk = this.socket.stream.read() 185 if (chunk) { 186 this.parser.push(chunk) 187 } 188 else { 189 this.readable = false 190 break 191 } 192 } 193 } 194 195 return frame 196 } 197 198 FrameProducer.prototype.needFrame = function() { 199 this.needsReadable = true 200 this._maybeEmitReadable() 201 } 202 203 FrameProducer.prototype._configChanged = function() { 204 this.restart() 205 } 206 207 FrameProducer.prototype._socketEnded = function() { 208 log.warn('Connection to minicap ended unexpectedly') 209 this.failCounter.inc() 210 this.restart() 211 } 212 213 FrameProducer.prototype._outputEnded = function() { 214 log.warn('Shell keeping minicap running ended unexpectedly') 215 this.failCounter.inc() 216 this.restart() 217 } 218 219 FrameProducer.prototype._failLimitExceeded = function(limit, time) { 220 this._stop() 221 this.failed = true 222 this.emit('error', new Error(util.format( 223 'Failed more than %d times in %dms' 224 , limit 225 , time 226 ))) 227 } 228 229 FrameProducer.prototype._startService = function() { 230 log.info('Launching screen service') 231 return minicap.run(util.format( 232 '-S -Q %d -P %s' 233 , options.screenJpegQuality 234 , this.frameConfig.toString() 235 )) 236 .timeout(10000) 237 } 238 239 FrameProducer.prototype._readOutput = function(out) { 240 out.pipe(split()).on('data', function(line) { 241 var trimmed = line.toString().trim() 242 243 if (trimmed === '') { 244 return 245 } 246 247 if (/ERROR/.test(line)) { 248 log.fatal('minicap error: "%s"', line) 249 return lifecycle.fatal() 250 } 251 252 var match = /^PID: (d+)$/.exec(line) 253 if (match) { 254 this.pid = Number(match[1]) 255 this.emit('pid', this.pid) 256 } 257 258 log.info('minicap says: "%s"', line) 259 }.bind(this)) 260 } 261 262 FrameProducer.prototype._waitForPid = function() { 263 if (this.pid > 0) { 264 return Promise.resolve(this.pid) 265 } 266 267 var pidListener 268 return new Promise(function(resolve) { 269 this.on('pid', pidListener = resolve) 270 }.bind(this)).bind(this) 271 .timeout(2000) 272 .finally(function() { 273 this.removeListener('pid', pidListener) 274 }) 275 } 276 277 FrameProducer.prototype._connectService = function() { 278 function tryConnect(times, delay) { 279 return adb.openLocal(options.serial, 'localabstract:minicap') 280 .timeout(10000) 281 .then(function(out) { 282 return out 283 }) 284 .catch(function(err) { 285 if (/closed/.test(err.message) && times > 1) { 286 return Promise.delay(delay) 287 .then(function() { 288 return tryConnect(times - 1, delay * 2) 289 }) 290 } 291 return Promise.reject(err) 292 }) 293 } 294 log.info('Connecting to minicap service') 295 return tryConnect(5, 100) 296 } 297 298 FrameProducer.prototype._stop = function() { 299 return this._disconnectService(this.socket).bind(this) 300 .timeout(2000) 301 .then(function() { 302 return this._stopService(this.output).timeout(10000) 303 }) 304 .then(function() { 305 this.runningState = FrameProducer.STATE_STOPPED 306 this.emit('stop') 307 }) 308 .catch(function(err) { 309 // In practice we _should_ never get here due to _stopService() 310 // being quite aggressive. But if we do, well... assume it 311 // stopped anyway for now. 312 this.runningState = FrameProducer.STATE_STOPPED 313 this.emit('error', err) 314 this.emit('stop') 315 }) 316 .finally(function() { 317 this.output = null 318 this.socket = null 319 this.pid = -1 320 this.banner = null 321 this.parser = null 322 }) 323 } 324 325 FrameProducer.prototype._disconnectService = function(socket) { 326 log.info('Disconnecting from minicap service') 327 328 if (!socket || socket.ended) { 329 return Promise.resolve(true) 330 } 331 332 socket.stream.removeListener('readable', this.readableListener) 333 334 var endListener 335 return new Promise(function(resolve) { 336 socket.on('end', endListener = function() { 337 resolve(true) 338 }) 339 340 socket.stream.resume() 341 socket.end() 342 }) 343 .finally(function() { 344 socket.removeListener('end', endListener) 345 }) 346 } 347 348 FrameProducer.prototype._stopService = function(output) { 349 log.info('Stopping minicap service') 350 351 if (!output || output.ended) { 352 return Promise.resolve(true) 353 } 354 355 var pid = this.pid 356 357 function kill(signal) { 358 if (pid <= 0) { 359 return Promise.reject(new Error('Minicap service pid is unknown')) 360 } 361 362 var signum = { 363 SIGTERM: -15 364 , SIGKILL: -9 365 }[signal] 366 367 log.info('Sending %s to minicap', signal) 368 return Promise.all([ 369 output.waitForEnd() 370 , adb.shell(options.serial, ['kill', signum, pid]) 371 .then(adbkit.util.readAll) 372 .return(true) 373 ]) 374 .timeout(2000) 375 } 376 377 function kindKill() { 378 return kill('SIGTERM') 379 } 380 381 function forceKill() { 382 return kill('SIGKILL') 383 } 384 385 function forceEnd() { 386 log.info('Ending minicap I/O as a last resort') 387 output.end() 388 return Promise.resolve(true) 389 } 390 391 return kindKill() 392 .catch(Promise.TimeoutError, forceKill) 393 .catch(forceEnd) 394 } 395 396 FrameProducer.prototype._readBanner = function(socket) { 397 log.info('Reading minicap banner') 398 return bannerutil.read(socket).timeout(2000) 399 } 400 401 FrameProducer.prototype._readFrames = function(socket) { 402 this.needsReadable = true 403 socket.on('readable', this.readableListener) 404 405 // We may already have data pending. Let the user know they should 406 // at least attempt to read frames now. 407 this.readableListener() 408 } 409 410 FrameProducer.prototype._maybeEmitReadable = function() { 411 if (this.readable && this.needsReadable) { 412 this.needsReadable = false 413 this.emit('readable') 414 } 415 } 416 417 FrameProducer.prototype._readableListener = function() { 418 this.readable = true 419 this._maybeEmitReadable() 420 } 421 422 function createServer() { 423 log.info('Starting WebSocket server on port %d', screenOptions.publicPort) 424 425 var wss = new WebSocket.Server({ 426 port: screenOptions.publicPort 427 , perMessageDeflate: false 428 }) 429 430 var listeningListener, errorListener 431 return new Promise(function(resolve, reject) { 432 listeningListener = function() { 433 return resolve(wss) 434 } 435 436 errorListener = function(err) { 437 return reject(err) 438 } 439 440 wss.on('listening', listeningListener) 441 wss.on('error', errorListener) 442 }) 443 .finally(function() { 444 wss.removeListener('listening', listeningListener) 445 wss.removeListener('error', errorListener) 446 }) 447 } 448 449 return createServer() 450 .then(function(wss) { 451 var frameProducer = new FrameProducer( 452 new FrameConfig(display.properties, display.properties)) 453 var broadcastSet = frameProducer.broadcastSet = new BroadcastSet() 454 455 broadcastSet.on('nonempty', function() { 456 frameProducer.start() 457 }) 458 459 broadcastSet.on('empty', function() { 460 frameProducer.stop() 461 }) 462 463 broadcastSet.on('insert', function(id) { 464 // If two clients join a session in the middle, one of them 465 // may not release the initial size because the projection 466 // doesn't necessarily change, and the producer doesn't Getting 467 // restarted. Therefore we have to call onStart() manually 468 // if the producer is already up and running. 469 switch (frameProducer.runningState) { 470 case FrameProducer.STATE_STARTED: 471 broadcastSet.get(id).onStart(frameProducer) 472 break 473 } 474 }) 475 476 display.on('rotationChange', function(newRotation) { 477 frameProducer.updateRotation(newRotation) 478 }) 479 480 frameProducer.on('start', function() { 481 broadcastSet.keys().map(function(id) { 482 return broadcastSet.get(id).onStart(frameProducer) 483 }) 484 }) 485 486 frameProducer.on('readable', function next() { 487 var frame = frameProducer.nextFrame() 488 if (frame) { 489 Promise.settle([broadcastSet.keys().map(function(id) { 490 return broadcastSet.get(id).onFrame(frame) 491 })]).then(next) 492 } 493 else { 494 frameProducer.needFrame() 495 } 496 }) 497 498 frameProducer.on('error', function(err) { 499 log.fatal('Frame producer had an error', err.stack) 500 lifecycle.fatal() 501 }) 502 503 wss.on('connection', function(ws) { 504 var id = uuid.v4() 505 var pingTimer 506 507 function send(message, options) { 508 return new Promise(function(resolve, reject) { 509 switch (ws.readyState) { 510 case WebSocket.OPENING: 511 // This should never happen. 512 log.warn('Unable to send to OPENING client "%s"', id) 513 break 514 case WebSocket.OPEN: 515 // This is what SHOULD happen. 516 ws.send(message, options, function(err) { 517 return err ? reject(err) : resolve() 518 }) 519 break 520 case WebSocket.CLOSING: 521 // Ok, a 'close' event should remove the client from the set 522 // soon. 523 break 524 case WebSocket.CLOSED: 525 // This should never happen. 526 log.warn('Unable to send to CLOSED client "%s"', id) 527 clearInterval(pingTimer) 528 broadcastSet.remove(id) 529 break 530 } 531 }) 532 } 533 534 function wsStartNotifier() { 535 return send(util.format( 536 'start %s' 537 , JSON.stringify(frameProducer.banner) 538 )) 539 } 540 541 function wsPingNotifier() { 542 return send('ping') 543 } 544 545 function wsFrameNotifier(frame) { 546 return send(frame, { 547 binary: true 548 }) 549 } 550 551 // Sending a ping message every now and then makes sure that 552 // reverse proxies like nginx don't time out the connection [1]. 553 // 554 // [1] http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout 555 pingTimer = setInterval(wsPingNotifier, options.screenPingInterval) 556 557 ws.on('message', function(data) { 558 var match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data) 559 if (match) { 560 switch (match[2] || match[1]) { 561 case 'on': 562 broadcastSet.insert(id, { 563 onStart: wsStartNotifier 564 , onFrame: (function (t) { 565 var wait_time = 0; 566 return function (frame) { 567 wait_time++ 568 // return wsFrameNotifier(frame); 569 if (wait_time < 30) { 570 clearTimeout(t) 571 572 wait_time % 4 || gm(frame).quality(10).toBuffer(function (err, buffer) { 573 wsFrameNotifier(buffer) 574 }) 575 576 t = setTimeout(function () { 577 wait_time = 0 578 wsFrameNotifier(frame) 579 }, 100) 580 } 581 } 582 })(0) 583 }) 584 break 585 case 'off': 586 broadcastSet.remove(id) 587 // Keep pinging even when the screen is off. 588 break 589 case 'size': 590 frameProducer.updateProjection( 591 Number(match[3]), Number(match[4])) 592 break 593 } 594 } 595 }) 596 597 ws.on('close', function() { 598 clearInterval(pingTimer) 599 broadcastSet.remove(id) 600 }) 601 }) 602 603 lifecycle.observe(function() { 604 wss.close() 605 }) 606 607 lifecycle.observe(function() { 608 frameProducer.stop() 609 }) 610 611 return frameProducer 612 }) 613 })