VulpesOS/gaia/apps/homescreen/test/marionette/server/child.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

315 lines
7.7 KiB
JavaScript
Executable file

/* global Buffer, process */
'use strict';
/**
App server for fixtures. Serves up packaged apps.
*/
var debug = require('debug')('appserver:child');
var http = require('http');
var fs = require('fs');
var fsPath = require('path');
var mime = require('mime');
var archiver = require('archiver');
var EventEmitter = require('events').EventEmitter;
function readdirRecursive(dir) {
dir = fsPath.join(dir, '/');
var results = [];
var files = fs.readdirSync(dir);
var file;
while((file = files.shift())) {
file = fsPath.join(dir, file);
var stat = fs.statSync(file);
if (stat.isDirectory()) {
results = results.concat(readdirRecursive(file));
} else {
results.push(file);
}
}
return results;
}
function writeJSON(res, object) {
var json = JSON.stringify(object);
res.writeHead(200, {
'Content-Length': Buffer.byteLength(json),
'Content-Type': 'application/json'
});
res.end(json);
}
function decorateHandlerForJSON(method) {
return function(req, res) {
// if it is not json just skip it
var type = req.headers['content-type'];
if (!type || type.indexOf('json') === -1) {
return method.call(this, req, res);
}
var buffer = '';
req.on('data', function(data) {
buffer += data;
});
req.once('end', function() {
req.body = JSON.parse(buffer);
method.call(this, req, res);
}.bind(this));
};
}
function defaultSettings() {
return {
corked: false,
fail: false
};
}
/**
* @param {String} root directory where content is.@
* @param {Object} routes for the app server.
* @constructor
*/
function AppServer(root, routes) {
EventEmitter.call(this);
// ensure we always have the slash suffix
if (root[root.length - 1] !== '/') {
root += '/';
}
this.root = root;
this.routes = routes;
this._settings = {};
}
AppServer.prototype = {
__proto__: EventEmitter.prototype,
/**
* @return {Object} default settings for a given url.
*/
settings: function(url) {
return (this._settings[url] = this._settings[url] || defaultSettings());
},
requestHandler: function() {
var routes = this.routes;
return function(req, res) {
debug('request', req.method, req.url);
var handler = routes[req.url] || routes['*'];
var settings = this.settings(req.url);
// If this url has a http error on it just return without calling the
// handler.
if (settings.response) {
res.writeHead(settings.response.status, settings.response.headers);
res.end(settings.response.body);
return;
}
// routes are always invoked with the context of the app server.
return handler.call(this, req, res);
}.bind(this);
},
};
var routes = {
/**
Change the root serving directory for this server.
*/
'/settings/set_root': decorateHandlerForJSON(function(req, res) {
this.root = req.body;
writeJSON(res, req.body);
}),
/**
Override or set the response of a particular url.
*/
'/settings/set_response': decorateHandlerForJSON(function(req, res) {
var options = req.body;
var settings = this.settings(options.path);
settings.response = options;
writeJSON(res, options.path);
}),
/**
Clear the response override.
*/
'/settings/clear_response': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
delete settings.response;
writeJSON(res, url);
}),
/**
A 'corked' request will send headers but no body until 'uncork' is used.
*/
'/settings/cork': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.corked = true;
// issue a cork to the particular endpoint
var event = 'corked ' + url;
debug('cork event', event);
this.emit('corked ' + url);
writeJSON(res, url);
}),
/**
The inverse of 'cork' will uncork a uri and send the body through.
*/
'/settings/uncork': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.corked = false;
var event = 'uncorked ' + url;
debug('uncork event', event);
this.emit(event);
writeJSON(res, url);
}),
/**
Fail is a combination of 'cork' and a flag which will cause the incoming http
request to be closed after a short delay.
*/
'/settings/fail': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
// must be corked to ensure we don't send the body
settings.corked = true;
settings.fail = true;
writeJSON(res, url);
}),
/**
The inverse to fail but unlike uncork this cannot be called in the middle of
a request (for obvious reasons).
*/
'/settings/unfail': decorateHandlerForJSON(function(req, res) {
var url = req.body;
var settings = this.settings(url);
settings.fail = settings.corked = false;
writeJSON(res, url);
}),
'/package.manifest': function(req, res) {
var json = JSON.parse(fs.readFileSync(this.root + '/manifest.webapp'));
json.package_path = 'http://' + req.headers.host + '/app.zip';
// TODO: Remove this once we add the ability to override requests
delete json.role;
var body = JSON.stringify(json, null, 2);
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'application/x-web-app-manifest+json'
});
res.end(body);
},
'/app.zip': function(req, res) {
var url = req.url;
var settings = this.settings(url);
// always write the head we need it in all cases
res.writeHead(200, {
'Content-Type': 'application/zip'
});
var root = this.root;
function writeZip() {
// read the entire app directory
var files = readdirRecursive(root);
var zip = archiver.create('zip');
files.forEach(function(file) {
debug('packaged app zip file', file);
var zipPath = file.replace(root, '');
zip.append(fs.createReadStream(file), { name: zipPath });
});
zip.finalize();
zip.pipe(res);
}
if (settings.fail) {
process.nextTick(function() {
req.socket.destroy();
});
return;
}
// if we are not corked just write the zip and we are done
if (!settings.corked) {
return writeZip();
} else {
this.once('uncorked ' + url, writeZip);
}
},
/**
Catch all handler which serves up static assets.
*/
'*': function(req, res) {
var url = req.url;
var settings = this.settings(url);
var filePath = fsPath.join(this.root, url);
// XXX: This is not a production quality server don't use sync methods in
// in servers in your user facing node code.
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end();
return;
}
var contentType = mime.lookup(filePath);
var contentLength = fs.statSync(filePath).size;
debug('static asset', filePath, contentType, contentLength);
// write out the head
res.writeHead(200, {
'Content-Type': contentType,
'Content-Length': contentLength
});
function writeContent() {
fs.createReadStream(filePath).pipe(res);
}
if (settings.fail) {
process.nextTick(function() {
req.socket.destroy();
});
return;
}
if (!settings.corked) {
// if we are not corked just return the stream
return writeContent();
} else {
// otherwise wait for the uncork
this.once('uncorked ' + url, writeContent);
}
}
};
var port = parseInt((process.argv[3] || 0), 10);
var appServer = new AppServer(process.argv[2], routes);
var server = http.createServer(appServer.requestHandler());
server.listen(port, function() {
if (process.send) {
process.send({ type: 'started', port: server.address().port });
}
});