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