blob: 76e50b40b5d642a28e990f767828f7a89e1a9f7c [file] [log] [blame]
andresp@webrtc.org468516c2014-09-02 10:52:54 +00001// 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_ = [];
houssainy@google.com96005192014-09-08 13:01:40 +000029 this.androidDeviceManager_ = new AndroidDeviceManager();
andresp@webrtc.org468516c2014-09-02 10:52:54 +000030}
31
houssainy@google.comc77e4d62014-09-08 10:36:11 +000032BotManager.BotTypes = {
33 CHROME : 'chrome',
houssainy@google.com96005192014-09-08 13:01:40 +000034 ANDROID_CHROME : 'android-chrome',
houssainy@google.comc77e4d62014-09-08 10:36:11 +000035};
36
andresp@webrtc.org468516c2014-09-02 10:52:54 +000037BotManager.prototype = {
houssainy@google.comc77e4d62014-09-08 10:36:11 +000038 createBot_: function (name, botType, callback) {
39 switch(botType) {
40 case BotManager.BotTypes.CHROME:
41 return new BrowserBot(name, callback);
houssainy@google.com96005192014-09-08 13:01:40 +000042 case BotManager.BotTypes.ANDROID_CHROME:
43 return new AndroidChromeBot(name, this.androidDeviceManager_,
44 callback);
houssainy@google.comc77e4d62014-09-08 10:36:11 +000045 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) {
andresp@webrtc.org468516c2014-09-02 10:52:54 +000052 this.startWebSocketServer_();
houssainy@google.comc77e4d62014-09-08 10:36:11 +000053 var bot = this.createBot_(name, botType, callback);
andresp@webrtc.org468516c2014-09-02 10:52:54 +000054 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
houssainy@google.com38ef6642014-09-04 13:44:47 +000063 this.app_.use('/bot/api.js',
andresp@webrtc.org468516c2014-09-02 10:52:54 +000064 this.serveBrowserifyFile_.bind(this,
houssainy@google.com38ef6642014-09-04 13:44:47 +000065 __dirname + '/bot/api.js'));
andresp@webrtc.org468516c2014-09-02 10:52:54 +000066
houssainy@google.com38ef6642014-09-04 13:44:47 +000067 this.app_.use('/bot/', Express.static(__dirname + '/bot'));
andresp@webrtc.org468516c2014-09-02 10:52:54 +000068
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
houssainy@google.com38ef6642014-09-04 13:44:47 +0000119// BrowserBot spawns a process to open "http://localhost:8080/bot/".
andresp@webrtc.org468516c2014-09-02 10:52:54 +0000120//
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');
houssainy@google.com38ef6642014-09-04 13:44:47 +0000131 child.exec('google-chrome "http://localhost:8080/bot/"');
andresp@webrtc.org468516c2014-09-02 10:52:54 +0000132 },
133
134 __proto__: Bot.prototype
135}
136
houssainy@google.com96005192014-09-08 13:01:40 +0000137// 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/';
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
houssainy@google.comc77e4d62014-09-08 10:36:11 +0000166AndroidDeviceManager = 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) {
houssainy@google.com96005192014-09-08 13:01:40 +0000193 console.log(error || stderr);
houssainy@google.comc77e4d62014-09-08 10:36:11 +0000194 }
195 if (stdout) {
196 // The first line is "List of devices attached"
197 // and the following lines:
198 // <serial number> <device/emulator>
houssainy@google.com96005192014-09-08 13:01:40 +0000199 var tempList = stdout.split("\n").slice(1);
houssainy@google.comc77e4d62014-09-08 10:36:11 +0000200 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}
andresp@webrtc.org468516c2014-09-02 10:52:54 +0000211module.exports = BotManager;