UNPKG

15.2 kBJavaScriptView Raw
1/*
2 * Copyright 2012 Amadeus s.a.s.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16var url = require('url');
17var path = require('path');
18var util = require("util");
19var events = require("events");
20
21var connect = require('connect');
22var compression = require('compression');
23var favicon = require('serve-favicon');
24var serveStatic = require('serve-static');
25var sockjs = require('sockjs');
26var _ = require("lodash");
27
28var attesterResultsUI = require('attester-results-ui');
29
30var http = require('./http-server.js');
31var Slave = require('./slave-server.js');
32var Viewer = require('./viewer-server.js');
33var SlaveController = require('./slave-controller.js');
34var Logger = require('../logging/logger.js');
35
36// middlewares
37var index = require("../middlewares/index");
38var template = require("../middlewares/template");
39
40var detectHostname = require("../util/detectHostname");
41var coverageDisplay = require("./coverage-display");
42
43var arrayRemove = function (array, item) {
44 var index = array.indexOf(item);
45 if (index >= 0) {
46 array.splice(index, 1);
47 return true;
48 }
49 return false;
50};
51
52var clientTypes = {
53 "slave": function (socket, data) {
54 var newSlave = new Slave(socket, data, this.config, this.logger);
55 this.addSlave(newSlave);
56 },
57 "viewer": function (socket, data) {
58 // creates a new Viewer, which will register to campaign results
59 new Viewer(socket, data, this);
60 },
61 "slaveController": function (socket, data) {
62 // creates a new slave controller, which will be notified when its slaves
63 // are connected, disconnected, and idle
64 new SlaveController(socket, data, this);
65 }
66};
67
68var socketConnection = function (socket) {
69 var testServer = this;
70 socket.once('data', function (message) {
71 try {
72 var data = JSON.parse(message);
73 var fn = clientTypes[data.type];
74 if (fn) {
75 fn.call(testServer, socket, data);
76 } else {
77 throw new Error();
78 }
79 } catch (e) {
80 // unknown type or incorrect JSON: close the connection so that the remote
81 // program knows it is not supported
82 socket.close();
83 }
84 });
85};
86
87var routeToCampaign = function (req, res, next) {
88 var testServer = this;
89 if (req.path == '/') {
90 // root of the server, redirect to welcome page:
91 res.statusCode = 301;
92 res.setHeader('Location', '/__attester__/index.html');
93 res.end();
94 return;
95 }
96
97 var campaignIdMatch = req.url.match(/^\/campaign([0-9]+)(\/|$)/);
98 var campaign = campaignIdMatch ? testServer.findCampaign(campaignIdMatch[1]) : null;
99 if (!campaign) {
100 return next();
101 }
102 if (!campaignIdMatch[2]) {
103 // campaign root, display its configuration
104 // it could be later improved by adding a home page for each campaign
105 res.statusCode = 301;
106 res.setHeader('Location', '/__attester__/index.html#campaign' + campaign.id);
107 res.end();
108 return;
109 }
110 // Remove the campaign baseURL
111 req.url = req.url.substr(campaign.baseURL.length);
112 campaign.handleRequest(req, res, next);
113};
114
115var campaignFinished = function (campaign) {
116 var testServer = this;
117 if (this.config.shutdownOnCampaignEnd) {
118 arrayRemove(testServer.campaigns, campaign);
119 testServer.slaves.forEach(testServer.updateMatchingCampaignBrowsers.bind(testServer));
120 }
121};
122
123var slaveDisconnected = function (slave) {
124 var testServer = this;
125 arrayRemove(testServer.slaves, slave);
126 arrayRemove(testServer.availableSlaves, slave);
127 testServer.updateMatchingCampaignBrowsers(slave);
128 testServer.logger.logInfo("Slave disconnected: " + slave.toString());
129};
130
131var slaveAvailable = function (slave) {
132 var testServer = this;
133 // TODO: do an assert to check that this slave is not already available and is correctly in the slaves array
134 testServer.availableSlaves.push(slave);
135
136 // a slave is now available, it is time to assign it a task
137 testServer.assignTasks();
138};
139
140var slaveUnavailable = function (slave) {
141 var testServer = this;
142 arrayRemove(testServer.availableSlaves, slave);
143};
144
145var routeCoverage = function (req, res, next) {
146 var testServer = this;
147 var match = /\/([0-9]+)\/([0-9]+)/.exec(req.path);
148 var campaignId;
149 var taskId;
150 var campaign;
151 if (match) {
152 campaignId = match[1];
153 taskId = match[2];
154 if (!isNaN(campaignId) && !isNaN(taskId)) {
155 campaign = testServer.findCampaign(campaignId);
156 }
157 }
158 if (!campaign) {
159 res.statusCode = 404;
160 res.write('Not found');
161 res.end();
162 return;
163 }
164 var data = [];
165 req.setEncoding('utf-8');
166 req.on('data', function (chunk) {
167 data.push(chunk);
168 });
169 req.on('end', function () {
170 res.write('OK');
171 res.end();
172 var json = JSON.parse(data.join(''));
173 campaign.addCoverageResult(taskId, json);
174 });
175};
176
177var jsonApi = function (req, res, next) {
178 var testServer = this;
179 if (req.path !== "/status.json") {
180 return next();
181 }
182 var status = testServer.getStatus();
183 var jsonResponse = JSON.stringify(status);
184 var parsedUrl = url.parse(req.url, true);
185 var jsonpCallback = parsedUrl.query.callback;
186 if (jsonpCallback) {
187 res.header("Content-Type", "application/javascript");
188 jsonResponse = [jsonpCallback, "(", jsonResponse, ");"].join("");
189 } else {
190 res.header("Content-Type", "application/json");
191 }
192 res.write(jsonResponse);
193 res.end();
194};
195
196var attesterResultsUIConfig = function () {
197 var testServer = this;
198 return {
199 serverURL: "{CURRENTHOST}",
200 loadServerURLs: testServer.campaigns.map(function (campaign) {
201 return "{CURRENTHOST}/campaign" + campaign.id;
202 })
203 };
204};
205
206var createSockJSLogger = function (parentLogger) {
207 var logger = new Logger("sockjs", parentLogger);
208
209 var severityMap = {
210 "debug": Logger.LEVEL_DEBUG,
211 "info": Logger.LEVEL_INFO,
212 "error": Logger.LEVEL_ERROR
213 };
214
215 return function (severity, message) {
216 var level = severityMap[severity] || Logger.LEVEL_TRACE;
217 logger.log(level, message);
218 };
219};
220
221/**
222 * A test server is a web server which browsers can connect to and become its slaves, ready to execute some tests.
223 * Browsers connect to it through a sockjs channel.
224 * @param {Object} config Has the following properties:
225 * <ul>
226 * <li></li>
227 * </ul>
228 */
229var TestServer = function (config, logger) {
230 this.logger = new Logger("TestServer", logger);
231 this.config = config;
232 var app = connect();
233 app.use(compression());
234 app.use(index);
235 app.use(favicon(path.join(__dirname, "client", "favicon.ico")));
236 // Template pages (before the static folder)
237 app.use('/__attester__', template.bind({
238 data: this,
239 page: "/index.html",
240 path: path.join(__dirname, "client", "index.html")
241 }));
242 app.use('/__attester__', template.bind({
243 data: this,
244 page: "/status.html",
245 path: path.join(__dirname, "client", "status.html")
246 }));
247 app.use('/__attester__', template.bind({
248 data: this,
249 page: "/slave.html",
250 path: path.join(__dirname, "client", "slave.html")
251 }));
252 app.use('/__attester__', serveStatic(__dirname + '/client'));
253 app.use('/__attester__/json3', serveStatic(path.dirname(require.resolve("json3/lib/json3.js"))));
254 app.use('/__attester__/sockjs', serveStatic(path.dirname(require.resolve("sockjs-client/dist/sockjs.js"))));
255 app.use('/__attester__/coverage/display', coverageDisplay(this, '/__attester__/coverage/display'));
256 app.use('/__attester__/coverage/data', routeCoverage.bind(this));
257 app.use('/__attester__/results-ui', attesterResultsUI({
258 toJSON: attesterResultsUIConfig.bind(this)
259 }));
260 app.use(routeToCampaign.bind(this));
261 app.use('/__attester__', jsonApi.bind(this));
262 this.app = app;
263 this.server = http.createServer(app);
264 this.sockjs = sockjs.createServer();
265 this.sockjs.installHandlers(this.server, {
266 prefix: '/sockjs',
267 sockjs_url: '/__attester__/sockjs/sockjs.js',
268 disconnect_delay: 60000,
269 log: createSockJSLogger(this.logger)
270 });
271 this.sockjs.on('connection', socketConnection.bind(this));
272 this.slaves = []; // array of all slaves
273 this.availableSlaves = []; // array of available slaves
274 this.campaigns = []; // array of campaigns
275 this.frozen = config.frozen; // if frozen, then don't assign tasks
276};
277
278util.inherits(TestServer, events.EventEmitter);
279
280var serverURLPathnames = {
281 'slave': '/__attester__/slave.html',
282 'home': '/'
283};
284
285TestServer.prototype.getURL = function (urlType) {
286 var pathname = serverURLPathnames[urlType];
287 if (pathname == null) {
288 return;
289 }
290 return url.format({
291 protocol: 'http',
292 port: this.config.publicPort || this.port,
293 hostname: this.config.publicHost || this.hostname,
294 pathname: pathname
295 });
296};
297
298TestServer.prototype.listen = function (port, host, callback) {
299 var self = this;
300 this.server.listen(port, host, function () {
301 var address = self.server.address();
302 self.port = address.port;
303 host = self.hostname = address.address;
304 var anyIPv4 = (host == "0.0.0.0");
305 var anyIPv6 = (host == "::");
306 if (anyIPv4 || anyIPv6) {
307 detectHostname(anyIPv4).then(function (hostname) {
308 self.hostname = hostname;
309 })["finally"](callback);
310 } else {
311 callback();
312 }
313 });
314};
315
316TestServer.prototype.getRemainingTasks = function () {
317 var result = 0;
318 this.campaigns.forEach(function (campaign) {
319 result += campaign.remainingTasks;
320 });
321 return result;
322};
323
324TestServer.prototype.getStatus = function () {
325 return {
326 slaves: this.slaves.map(function (slave) {
327 return {
328 address: slave.address,
329 addressName: slave.addressName,
330 port: slave.port,
331 displayName: slave.displayName,
332 userAgent: slave.userAgent,
333 paused: slave.paused,
334 idle: slave.idle,
335 currentCampaign: slave.currentCampaign ? slave.currentCampaign.id : null,
336 currentTask: slave.currentTask ? slave.currentTask.test.name : null
337 };
338 }),
339 campaigns: this.campaigns.map(function (campaign) {
340 return {
341 campaignNumber: campaign.campaignNumber,
342 id: campaign.id,
343 totalTasks: campaign.tasks.length,
344 remainingTasks: campaign.remainingTasks,
345 browsers: campaign.browsers.map(function (browser) {
346 return browser.getJsonInfo();
347 })
348 };
349 })
350 };
351};
352
353TestServer.prototype.close = function (callback) {
354 var slaves = this.slaves;
355 var slaveDisposeBack = _.after(slaves.length + 1, function () {
356 try {
357 this.server.close(callback);
358 } catch (e) {}
359 }.bind(this));
360
361 for (var i = 0, l = slaves.length; i < l; i++) {
362 this.logger.logDebug("Disposing the slave: " + slaves[i].toString());
363 try {
364 slaves[i].dispose(slaveDisposeBack);
365 } catch (e) {}
366 }
367 slaveDisposeBack();
368};
369
370TestServer.prototype.addSlave = function (slave) {
371 this.logger.logInfo("New slave connected: " + slave.toString());
372 this.updateMatchingCampaignBrowsers(slave);
373 this.slaves.push(slave);
374 slave.once('disconnect', slaveDisconnected.bind(this, slave));
375 slave.on('available', slaveAvailable.bind(this, slave));
376 slave.on('unavailable', slaveUnavailable.bind(this, slave));
377 if (slave.id) {
378 var listeners = this.emit('slave-added-' + slave.id, slave);
379 if (!listeners) {
380 // disconnects any slave which has an unregistered id
381 this.logger.logInfo("Id " + slave.id + " is not registered, slave will be disconnected.");
382 slave.disconnect();
383 return;
384 }
385 }
386 slave.emitAvailable(); // this will trigger assignTasks if the slave is available
387};
388
389TestServer.prototype.addCampaign = function (campaign) {
390 this.campaigns.push(campaign);
391 campaign.once('finished', campaignFinished.bind(this, campaign));
392 var slaveURL = this.getURL("slave");
393 campaign.addResult({
394 event: "serverAttached",
395 homeURL: this.getURL("home"),
396 slaveURL: slaveURL
397 });
398 this.slaves.forEach(this.updateMatchingCampaignBrowsers.bind(this));
399 this.assignTasks();
400};
401
402TestServer.prototype.updateMatchingCampaignBrowsers = function (slave) {
403 var res = [];
404 // checks that the slave is connected:
405 if (slave.socket) {
406 this.campaigns.forEach(function (currentCampaign) {
407 currentCampaign.browsers.forEach(function (currentBrowser) {
408 if (currentBrowser.matches(slave)) {
409 res.push({
410 campaign: currentCampaign,
411 browser: currentBrowser
412 });
413 }
414 });
415 });
416 }
417 slave.matchingCampaignBrowsers = res;
418};
419
420TestServer.prototype.assignTasks = function () {
421 if (this.frozen) {
422 return;
423 }
424 // automatically called when tasks could be assigned to slaves
425 var campaigns = this.campaigns;
426 for (var i = 0, l = campaigns.length; i < l; i++) {
427 var currentCampaign = campaigns[i];
428 currentCampaign.checkFinished();
429 }
430 var availableSlaves = this.availableSlaves;
431 for (var k = availableSlaves.length - 1; k >= 0; k--) {
432 var currentSlave = availableSlaves[k];
433 currentSlave.findTask();
434 }
435};
436
437TestServer.prototype.findCampaign = function (campaignId) {
438 var campaigns = this.campaigns;
439 for (var i = 0, l = campaigns.length; i < l; i++) {
440 var curCampaign = campaigns[i];
441 // campaignNumber is used if predictableUrls == true. The values are 1, 2, 3...
442 // campaign id's values OTOH are timestamps, so there's no risk of collision.
443 if (curCampaign.id == campaignId || curCampaign.campaignNumber == campaignId) {
444 return curCampaign;
445 }
446 }
447 return null;
448};
449
450TestServer.prototype.dispose = function (callback) {
451 this.logger.logDebug("Disposing test server");
452 var self = this;
453 this.close(function () {
454 self.logger.dispose();
455 callback();
456 });
457};
458
459module.exports = TestServer;