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