1// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2// 3// Use of this source code is governed by a BSD-style license 4// that can be found in the LICENSE file in the root of the source 5// tree. An additional intellectual property rights grant can be found 6// in the file PATENTS. All contributing project authors may 7// be found in the AUTHORS file in the root of the source tree. 8// 9// botmanager.js module allows a test to spawn bots that expose an RPC API 10// to be controlled by tests. 11var https = require('https'); 12var fs = require('fs'); 13var child = require('child_process'); 14var Browserify = require('browserify'); 15var Dnode = require('dnode'); 16var Express = require('express'); 17var WebSocketServer = require('ws').Server; 18var WebSocketStream = require('websocket-stream'); 19 20// BotManager runs a HttpsServer that serves bots assets and and WebSocketServer 21// that listens to incoming connections. Once a connection is available it 22// connects it to bots pending endpoints. 23// 24// TODO(andresp): There should be a way to control which bot was spawned 25// and what bot instance it gets connected to. 26BotManager = function () { 27 this.webSocketServer_ = null; 28 this.bots_ = []; 29 this.pendingConnections_ = []; 30 this.androidDeviceManager_ = new AndroidDeviceManager(); 31} 32 33BotManager.BotTypes = { 34 CHROME : 'chrome', 35 ANDROID_CHROME : 'android-chrome', 36}; 37 38BotManager.prototype = { 39 createBot_: function (name, botType, callback) { 40 switch(botType) { 41 case BotManager.BotTypes.CHROME: 42 return new BrowserBot(name, callback); 43 case BotManager.BotTypes.ANDROID_CHROME: 44 return new AndroidChromeBot(name, this.androidDeviceManager_, 45 callback); 46 default: 47 console.log('Error: Type ' + botType + ' not supported by rtc-Bot!'); 48 process.exit(1); 49 } 50 }, 51 52 spawnNewBot: function (name, botType, callback) { 53 this.startWebSocketServer_(); 54 var bot = this.createBot_(name, botType, callback); 55 this.bots_.push(bot); 56 this.pendingConnections_.push(bot.onBotConnected.bind(bot)); 57 }, 58 59 startWebSocketServer_: function () { 60 if (this.webSocketServer_) return; 61 62 this.app_ = new Express(); 63 64 this.app_.use('/bot/api.js', 65 this.serveBrowserifyFile_.bind(this, 66 __dirname + '/bot/api.js')); 67 68 this.app_.use('/bot/', Express.static(__dirname + '/bot')); 69 70 var options = options = { 71 key: fs.readFileSync('configurations/priv.pem', 'utf8'), 72 cert: fs.readFileSync('configurations/cert.crt', 'utf8') 73 }; 74 this.server_ = https.createServer(options, this.app_); 75 76 this.webSocketServer_ = new WebSocketServer({ server: this.server_ }); 77 this.webSocketServer_.on('connection', this.onConnection_.bind(this)); 78 79 this.server_.listen(8080); 80 }, 81 82 onConnection_: function (ws) { 83 var callback = this.pendingConnections_.shift(); 84 callback(new WebSocketStream(ws)); 85 }, 86 87 serveBrowserifyFile_: function (file, request, result) { 88 // TODO(andresp): Cache browserify result for future serves. 89 var browserify = new Browserify(); 90 browserify.add(file); 91 browserify.bundle().pipe(result); 92 } 93} 94 95// A basic bot waits for onBotConnected to be called with a stream to the actual 96// endpoint with the bot. Once that stream is available it establishes a dnode 97// connection and calls the callback with the other endpoint interface so the 98// test can interact with it. 99Bot = function (name, callback) { 100 this.name_ = name; 101 this.onbotready_ = callback; 102} 103 104Bot.prototype = { 105 log: function (msg) { 106 console.log("bot:" + this.name_ + " > " + msg); 107 }, 108 109 name: function () { return this.name_; }, 110 111 onBotConnected: function (stream) { 112 this.log('Connected'); 113 this.stream_ = stream; 114 this.dnode_ = new Dnode(); 115 this.dnode_.on('remote', this.onRemoteFromDnode_.bind(this)); 116 this.dnode_.pipe(this.stream_).pipe(this.dnode_); 117 }, 118 119 onRemoteFromDnode_: function (remote) { 120 this.onbotready_(remote); 121 } 122} 123 124// BrowserBot spawns a process to open "https://localhost:8080/bot/browser". 125// 126// That page once loaded, connects to the websocket server run by BotManager 127// and exposes the bot api. 128BrowserBot = function (name, callback) { 129 Bot.call(this, name, callback); 130 this.spawnBotProcess_(); 131} 132 133BrowserBot.prototype = { 134 spawnBotProcess_: function () { 135 this.log('Spawning browser'); 136 child.exec('google-chrome "https://localhost:8080/bot/browser/"'); 137 }, 138 139 __proto__: Bot.prototype 140} 141 142// AndroidChromeBot spawns a process to open 143// "https://localhost:8080/bot/browser/" on chrome for Android. 144AndroidChromeBot = function (name, androidDeviceManager, callback) { 145 Bot.call(this, name, callback); 146 androidDeviceManager.getNewDevice(function (serialNumber) { 147 this.serialNumber_ = serialNumber; 148 this.spawnBotProcess_(); 149 }.bind(this)); 150} 151 152AndroidChromeBot.prototype = { 153 spawnBotProcess_: function () { 154 this.log('Spawning Android device with serial ' + this.serialNumber_); 155 var runChrome = 'adb -s ' + this.serialNumber_ + ' shell am start ' + 156 '-n com.android.chrome/com.google.android.apps.chrome.Main ' + 157 '-d https://localhost:8080/bot/browser/'; 158 child.exec(runChrome, function (error, stdout, stderr) { 159 if (error) { 160 this.log(error); 161 process.exit(1); 162 } 163 this.log('Opening Chrome for Android...'); 164 this.log(stdout); 165 }.bind(this)); 166 }, 167 168 __proto__: Bot.prototype 169} 170 171AndroidDeviceManager = function () { 172 this.connectedDevices_ = []; 173} 174 175AndroidDeviceManager.prototype = { 176 getNewDevice: function (callback) { 177 this.listDevices_(function (devices) { 178 for (var i = 0; i < devices.length; i++) { 179 if (!this.connectedDevices_[devices[i]]) { 180 this.connectedDevices_[devices[i]] = devices[i]; 181 callback(this.connectedDevices_[devices[i]]); 182 return; 183 } 184 } 185 if (devices.length == 0) { 186 console.log('Error: No connected devices!'); 187 } else { 188 console.log('Error: There is no enough connected devices.'); 189 } 190 process.exit(1); 191 }.bind(this)); 192 }, 193 194 listDevices_: function (callback) { 195 child.exec('adb devices' , function (error, stdout, stderr) { 196 var devices = []; 197 if (error || stderr) { 198 console.log(error || stderr); 199 } 200 if (stdout) { 201 // The first line is "List of devices attached" 202 // and the following lines: 203 // <serial number> <device/emulator> 204 var tempList = stdout.split("\n").slice(1); 205 for (var i = 0; i < tempList.length; i++) { 206 if (tempList[i] == "") { 207 continue; 208 } 209 devices.push(tempList[i].split("\t")[0]); 210 } 211 } 212 callback(devices); 213 }); 214 }, 215} 216module.exports = BotManager; 217