// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file in the root of the source // tree. An additional intellectual property rights grant can be found // in the file PATENTS. All contributing project authors may // be found in the AUTHORS file in the root of the source tree. // // botmanager.js module allows a test to spawn bots that expose an RPC API // to be controlled by tests. var https = require('https'); var fs = require('fs'); var child = require('child_process'); var Browserify = require('browserify'); var Dnode = require('dnode'); var Express = require('express'); var WebSocketServer = require('ws').Server; var WebSocketStream = require('websocket-stream'); // BotManager runs a HttpsServer that serves bots assets and and WebSocketServer // that listens to incoming connections. Once a connection is available it // connects it to bots pending endpoints. // // TODO(andresp): There should be a way to control which bot was spawned // and what bot instance it gets connected to. BotManager = function () { this.webSocketServer_ = null; this.bots_ = []; this.pendingConnections_ = []; this.androidDeviceManager_ = new AndroidDeviceManager(); } BotManager.BotTypes = { CHROME : 'chrome', ANDROID_CHROME : 'android-chrome', }; BotManager.prototype = { createBot_: function (name, botType, callback) { switch(botType) { case BotManager.BotTypes.CHROME: return new BrowserBot(name, callback); case BotManager.BotTypes.ANDROID_CHROME: return new AndroidChromeBot(name, this.androidDeviceManager_, callback); default: console.log('Error: Type ' + botType + ' not supported by rtc-Bot!'); process.exit(1); } }, spawnNewBot: function (name, botType, callback) { this.startWebSocketServer_(); var bot = this.createBot_(name, botType, callback); this.bots_.push(bot); this.pendingConnections_.push(bot.onBotConnected.bind(bot)); }, startWebSocketServer_: function () { if (this.webSocketServer_) return; this.app_ = new Express(); this.app_.use('/bot/api.js', this.serveBrowserifyFile_.bind(this, __dirname + '/bot/api.js')); this.app_.use('/bot/', Express.static(__dirname + '/bot')); var options = options = { key: fs.readFileSync('configurations/priv.pem', 'utf8'), cert: fs.readFileSync('configurations/cert.crt', 'utf8') }; this.server_ = https.createServer(options, this.app_); this.webSocketServer_ = new WebSocketServer({ server: this.server_ }); this.webSocketServer_.on('connection', this.onConnection_.bind(this)); this.server_.listen(8080); }, onConnection_: function (ws) { var callback = this.pendingConnections_.shift(); callback(new WebSocketStream(ws)); }, serveBrowserifyFile_: function (file, request, result) { // TODO(andresp): Cache browserify result for future serves. var browserify = new Browserify(); browserify.add(file); browserify.bundle().pipe(result); } } // A basic bot waits for onBotConnected to be called with a stream to the actual // endpoint with the bot. Once that stream is available it establishes a dnode // connection and calls the callback with the other endpoint interface so the // test can interact with it. Bot = function (name, callback) { this.name_ = name; this.onbotready_ = callback; } Bot.prototype = { log: function (msg) { console.log("bot:" + this.name_ + " > " + msg); }, name: function () { return this.name_; }, onBotConnected: function (stream) { this.log('Connected'); this.stream_ = stream; this.dnode_ = new Dnode(); this.dnode_.on('remote', this.onRemoteFromDnode_.bind(this)); this.dnode_.pipe(this.stream_).pipe(this.dnode_); }, onRemoteFromDnode_: function (remote) { this.onbotready_(remote); } } // BrowserBot spawns a process to open "https://localhost:8080/bot/browser". // // That page once loaded, connects to the websocket server run by BotManager // and exposes the bot api. BrowserBot = function (name, callback) { Bot.call(this, name, callback); this.spawnBotProcess_(); } BrowserBot.prototype = { spawnBotProcess_: function () { this.log('Spawning browser'); child.exec('google-chrome "https://localhost:8080/bot/browser/"'); }, __proto__: Bot.prototype } // AndroidChromeBot spawns a process to open // "https://localhost:8080/bot/browser/" on chrome for Android. AndroidChromeBot = function (name, androidDeviceManager, callback) { Bot.call(this, name, callback); androidDeviceManager.getNewDevice(function (serialNumber) { this.serialNumber_ = serialNumber; this.spawnBotProcess_(); }.bind(this)); } AndroidChromeBot.prototype = { spawnBotProcess_: function () { this.log('Spawning Android device with serial ' + this.serialNumber_); var runChrome = 'adb -s ' + this.serialNumber_ + ' shell am start ' + '-n com.android.chrome/com.google.android.apps.chrome.Main ' + '-d https://localhost:8080/bot/browser/'; child.exec(runChrome, function (error, stdout, stderr) { if (error) { this.log(error); process.exit(1); } this.log('Opening Chrome for Android...'); this.log(stdout); }.bind(this)); }, __proto__: Bot.prototype } AndroidDeviceManager = function () { this.connectedDevices_ = []; } AndroidDeviceManager.prototype = { getNewDevice: function (callback) { this.listDevices_(function (devices) { for (var i = 0; i < devices.length; i++) { if (!this.connectedDevices_[devices[i]]) { this.connectedDevices_[devices[i]] = devices[i]; callback(this.connectedDevices_[devices[i]]); return; } } if (devices.length == 0) { console.log('Error: No connected devices!'); } else { console.log('Error: There is no enough connected devices.'); } process.exit(1); }.bind(this)); }, listDevices_: function (callback) { child.exec('adb devices' , function (error, stdout, stderr) { var devices = []; if (error || stderr) { console.log(error || stderr); } if (stdout) { // The first line is "List of devices attached" // and the following lines: // var tempList = stdout.split("\n").slice(1); for (var i = 0; i < tempList.length; i++) { if (tempList[i] == "") { continue; } devices.push(tempList[i].split("\t")[0]); } } callback(devices); }); }, } module.exports = BotManager;