Merge pull request #2968 from sebv/server-crash

Generic method to avoid server crash.
This commit is contained in:
Jonathan Lipps
2014-06-26 08:11:56 -07:00
5 changed files with 155 additions and 79 deletions

View File

@@ -16,8 +16,8 @@ var status = require('./status.js')
, notYetImplemented = responses.notYetImplemented
, helpers = require('../helpers.js')
, logCustomDeprecationWarning = helpers.logCustomDeprecationWarning
, _ = require('underscore');
, _ = require('underscore')
, safely = require('./helpers').safely;
exports.getGlobalBeforeFilter = function (appium) {
return function (req, res, next) {
@@ -51,7 +51,9 @@ exports.sessionBeforeFilter = function (req, res, next) {
}
// if we don't actually have a valid session, respond with an error
if (sessId && (!req.device || req.appium.sessionId !== sessId)) {
res.send(404, {sessionId: null, status: status.codes.NoSuchDriver.code, value: ''});
safely(req, function () {
res.send(404, {sessionId: null, status: status.codes.NoSuchDriver.code, value: ''});
});
} else {
next();
}
@@ -94,7 +96,7 @@ exports.installApp = function (req, res) {
exports.removeApp = function (req, res) {
req.body.appId = req.body.appId || req.body.bundleId;
if (checkMissingParams(res, {appId: req.body.appId}, true)) {
if (checkMissingParams(req, res, {appId: req.body.appId}, true)) {
req.device.removeApp(req.body.appId, function (error, response) {
if (error !== null) {
respondError(req, res, response);
@@ -108,7 +110,7 @@ exports.removeApp = function (req, res) {
// TODO: fix this method so it expects a callback with a boolean value, not
// some weird stdout thing
exports.isAppInstalled = function (req, res) {
if (checkMissingParams(res, {bundleId: req.body.bundleId}, true)) {
if (checkMissingParams(req, res, {bundleId: req.body.bundleId}, true)) {
req.device.isAppInstalled(req.body.bundleId, function (error, stdout) {
if (error !== null) {
respondSuccess(req, res, false);
@@ -145,8 +147,10 @@ exports.createSession = function (req, res) {
}
var next = function (reqHost, sessionId) {
res.set('Location', "http://" + reqHost + "/wd/hub/session/" + sessionId);
res.send(303);
safely(req, function () {
res.set('Location', "http://" + reqHost + "/wd/hub/session/" + sessionId);
res.send(303);
});
};
if (req.appium.preLaunched && req.appium.sessionId) {
req.appium.preLaunched = false;
@@ -189,14 +193,14 @@ exports.reset = function (req, res) {
exports.lock = function (req, res) {
var seconds = req.body.seconds;
if (checkMissingParams(res, {seconds: seconds})) {
if (checkMissingParams(req, res, {seconds: seconds})) {
req.device.lock(seconds, getResponseHandler(req, res));
}
};
exports.background = function (req, res) {
var seconds = req.body.seconds;
if (checkMissingParams(res, {seconds: seconds})) {
if (checkMissingParams(req, res, {seconds: seconds})) {
req.device.background(seconds, getResponseHandler(req, res));
}
};
@@ -216,7 +220,7 @@ exports.findElements = function (req, res) {
var strategy = req.body.using
, selector = req.body.value;
if (checkMissingParams(res, {strategy: strategy, selector: selector}, true)) {
if (checkMissingParams(req, res, {strategy: strategy, selector: selector}, true)) {
req.device.findElements(strategy, selector, getResponseHandler(req, res));
}
};
@@ -225,7 +229,7 @@ exports.findElement = function (req, res) {
var strategy = req.body.using
, selector = req.body.value;
if (checkMissingParams(res, {strategy: strategy, selector: selector}, true)) {
if (checkMissingParams(req, res, {strategy: strategy, selector: selector}, true)) {
req.device.findElement(strategy, selector, getResponseHandler(req, res));
}
};
@@ -316,9 +320,9 @@ exports.touchLongClick = function (req, res) {
var y = req.body.y;
var duration = req.body.duration;
if (element && checkMissingParams(res, {element: element}, true)) {
if (element && checkMissingParams(req, res, {element: element}, true)) {
req.device.touchLongClick(element, x, y, duration, getResponseHandler(req, res));
} else if (checkMissingParams(res, {x: x, y: y}, true)) {
} else if (checkMissingParams(req, res, {x: x, y: y}, true)) {
req.device.touchLongClick(element, x, y, duration, getResponseHandler(req, res));
}
};
@@ -328,9 +332,9 @@ exports.touchDown = function (req, res) {
var x = req.body.x;
var y = req.body.y;
if (element && checkMissingParams(res, {element: element}, true)) {
if (element && checkMissingParams(req, res, {element: element}, true)) {
req.device.touchDown(element, x, y, getResponseHandler(req, res));
} else if (checkMissingParams(res, {x: x, y: y}, true)) {
} else if (checkMissingParams(req, res, {x: x, y: y}, true)) {
req.device.touchDown(element, x, y, getResponseHandler(req, res));
}
};
@@ -340,9 +344,9 @@ exports.touchUp = function (req, res) {
var x = req.body.x;
var y = req.body.y;
if (element && checkMissingParams(res, {element: element}, true)) {
if (element && checkMissingParams(req, res, {element: element}, true)) {
req.device.touchUp(element, x, y, getResponseHandler(req, res));
} else if (checkMissingParams(res, {x: x, y: y}, true)) {
} else if (checkMissingParams(req, res, {x: x, y: y}, true)) {
req.device.touchUp(element, x, y, getResponseHandler(req, res));
}
};
@@ -352,9 +356,9 @@ exports.touchMove = function (req, res) {
var x = req.body.x;
var y = req.body.y;
if (element && checkMissingParams(res, {element: element}, true)) {
if (element && checkMissingParams(req, res, {element: element}, true)) {
req.device.touchMove(element, x, y, getResponseHandler(req, res));
} else if (checkMissingParams(res, {x: x, y: y}, true)) {
} else if (checkMissingParams(req, res, {x: x, y: y}, true)) {
req.device.touchMove(element, x, y, getResponseHandler(req, res));
}
};
@@ -724,7 +728,7 @@ exports.asyncScriptTimeout = function (req, res) {
exports.timeouts = function (req, res) {
var timeoutType = req.body.type
, ms = req.body.ms;
if (checkMissingParams(res, {type: timeoutType, ms: ms})) {
if (checkMissingParams(req, res, {type: timeoutType, ms: ms})) {
if (timeoutType === "implicit") {
exports.implicitWait(req, res);
} else if (timeoutType === "script") {
@@ -797,7 +801,7 @@ exports.flick = function (req, res) {
ySpeed = req.body.yspeed;
}
if (checkMissingParams(res, {xSpeed: xSpeed, ySpeed: ySpeed})) {
if (checkMissingParams(req, res, {xSpeed: xSpeed, ySpeed: ySpeed})) {
if (element) {
exports.flickElement(req, res);
} else {
@@ -812,7 +816,7 @@ exports.flickElement = function (req, res) {
, yoffset = req.body.yoffset
, speed = req.body.speed;
if (checkMissingParams(res, {element: element, xoffset: xoffset, yoffset: yoffset})) {
if (checkMissingParams(req, res, {element: element, xoffset: xoffset, yoffset: yoffset})) {
req.device.fakeFlickElement(element, xoffset, yoffset, speed, getResponseHandler(req, res));
}
};
@@ -821,7 +825,7 @@ exports.execute = function (req, res) {
var script = req.body.script
, args = req.body.args;
if (checkMissingParams(res, {script: script, args: args})) {
if (checkMissingParams(req, res, {script: script, args: args})) {
if (_s.startsWith(script, "mobile: ")) {
var realCmd = script.replace("mobile: ", "");
exports.executeMobileMethod(req, res, realCmd);
@@ -839,7 +843,7 @@ exports.executeAsync = function (req, res) {
responseUrl += 'http://' + req.appium.args.address + ':' + req.appium.args.port;
responseUrl += '/wd/hub/session/' + req.appium.sessionId + '/receive_async_response';
if (checkMissingParams(res, {script: script, args: args})) {
if (checkMissingParams(req, res, {script: script, args: args})) {
req.device.executeAsync(script, args, responseUrl, getResponseHandler(req, res));
}
};
@@ -857,8 +861,10 @@ exports.executeMobileMethod = function (req, res, cmd) {
if (args.length) {
if (args.length !== 1) {
res.send(400, "Mobile methods only take one parameter, which is a " +
"hash of named parameters to send to the method");
safely(req, function () {
res.send(400, "Mobile methods only take one parameter, which is a " +
"hash of named parameters to send to the method");
});
} else {
params = args[0];
}
@@ -888,7 +894,7 @@ exports.submit = function (req, res) {
exports.postUrl = function (req, res) {
var url = req.body.url;
if (checkMissingParams(res, {url: url})) {
if (checkMissingParams(req, res, {url: url})) {
req.device.url(url, getResponseHandler(req, res));
}
};
@@ -904,7 +910,7 @@ exports.active = function (req, res) {
exports.setContext = function (req, res) {
var name = req.body.name;
if (checkMissingParams(res, {name: name})) {
if (checkMissingParams(req, res, {name: name})) {
req.device.setContext(name, getResponseHandler(req, res));
}
};
@@ -924,7 +930,7 @@ exports.getWindowHandle = function (req, res) {
exports.setWindow = function (req, res) {
var name = req.body.name;
if (checkMissingParams(res, {name: name})) {
if (checkMissingParams(req, res, {name: name})) {
req.device.setWindow(name, getResponseHandler(req, res));
}
};
@@ -940,7 +946,7 @@ exports.getWindowHandles = function (req, res) {
exports.setCommandTimeout = function (req, res) {
var timeout = req.body.timeout;
if (checkMissingParams(res, {timeout: timeout})) {
if (checkMissingParams(req, res, {timeout: timeout})) {
timeout = parseInt(timeout, 10);
req.appium.setCommandTimeout(timeout, getResponseHandler(req, res));
}
@@ -949,13 +955,15 @@ exports.setCommandTimeout = function (req, res) {
exports.receiveAsyncResponse = function (req, res) {
var asyncResponse = req.body;
req.device.receiveAsyncResponse(asyncResponse);
res.send(200, 'OK');
safely(req, function () {
res.send(200, 'OK');
});
};
exports.setValueImmediate = function (req, res) {
var element = req.params.elementId
, value = req.body.value;
if (checkMissingParams(res, {element: element, value: value})) {
if (checkMissingParams(req, res, {element: element, value: value})) {
req.device.setValueImmediate(element, value, getResponseHandler(req, res));
}
};
@@ -966,7 +974,7 @@ exports.getCookies = function (req, res) {
exports.setCookie = function (req, res) {
var cookie = req.body.cookie;
if (checkMissingParams(res, {cookie: cookie})) {
if (checkMissingParams(req, res, {cookie: cookie})) {
if (typeof cookie.name !== "string" || typeof cookie.value !== "string") {
return respondError(req, res, status.codes.UnknownError,
"setCookie requires cookie of form {name: 'xxx', value: 'yyy'}");
@@ -991,7 +999,7 @@ exports.getCurrentActivity = function (req, res) {
exports.getLog = function (req, res) {
var logType = req.body.type;
if (checkMissingParams(res, {logType: logType})) {
if (checkMissingParams(req, res, {logType: logType})) {
req.device.getLog(logType, getResponseHandler(req, res));
}
};
@@ -1010,15 +1018,17 @@ exports.getStrings = function (req, res) {
exports.unknownCommand = function (req, res) {
logger.debug("Responding to client that we did not find a valid resource");
res.set('Content-Type', 'text/plain');
res.send(404, "That URL did not map to a valid JSONWP resource");
safely(req, function () {
res.set('Content-Type', 'text/plain');
res.send(404, "That URL did not map to a valid JSONWP resource");
});
};
exports.pushFile = function (req, res) {
var data = req.body.data; // base64 data
var path = req.body.path; // remote path
if (checkMissingParams(res, {data: data, path: path})) {
if (checkMissingParams(req, res, {data: data, path: path})) {
req.device.pushFile(data, path, getResponseHandler(req, res));
}
};
@@ -1026,7 +1036,7 @@ exports.pushFile = function (req, res) {
exports.pullFile = function (req, res) {
var path = req.body.path; // remote path
if (checkMissingParams(res, {path: path})) {
if (checkMissingParams(req, res, {path: path})) {
req.device.pullFile(path, getResponseHandler(req, res));
}
};
@@ -1034,7 +1044,7 @@ exports.pullFile = function (req, res) {
exports.pullFolder = function (req, res) {
var path = req.body.path; // remote path
if (checkMissingParams(res, {path: path})) {
if (checkMissingParams(req, res, {path: path})) {
req.device.pullFolder(path, getResponseHandler(req, res));
}
};
@@ -1043,7 +1053,7 @@ exports.endCoverage = function (req, res) {
var intent = req.body.intent;
var path = req.body.path;
if (checkMissingParams(res, {intent: intent, path: path})) {
if (checkMissingParams(req, res, {intent: intent, path: path})) {
req.device.endCoverage(intent, path, getResponseHandler(req, res));
}
};
@@ -1097,14 +1107,16 @@ exports.guineaPig = function (req, res) {
if (req.method === "POST") {
params.comment = req.body.comments || params.comment;
}
res.set('Content-Type', 'text/html');
res.cookie('guineacookie1', 'i am a cookie value', {path: '/'});
res.cookie('guineacookie2', 'cookié2', {path: '/'});
res.cookie('guineacookie3', 'cant access this', {
domain: '.blargimarg.com',
path: '/'
safely(req, function () {
res.set('Content-Type', 'text/html');
res.cookie('guineacookie1', 'i am a cookie value', {path: '/'});
res.cookie('guineacookie2', 'cookié2', {path: '/'});
res.cookie('guineacookie3', 'cant access this', {
domain: '.blargimarg.com',
path: '/'
});
res.send(exports.getTemplate('guinea-pig')(params));
});
res.send(exports.getTemplate('guinea-pig')(params));
};
exports.getTemplate = function (templateName) {

View File

@@ -6,17 +6,24 @@ var _ = require("underscore")
, status = require('./status.js')
, io = require('socket.io')
, mkdirp = require('mkdirp')
, bytes = require('bytes');
, bytes = require('bytes')
, domain = require('domain')
, format = require('util').format
, Args = require("vargs").Constructor;
module.exports.allowCrossDomain = function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,OPTIONS,DELETE');
res.header('Access-Control-Allow-Headers', 'origin, content-type, accept');
safely(req, function () {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,OPTIONS,DELETE');
res.header('Access-Control-Allow-Headers', 'origin, content-type, accept');
});
// need to respond 200 to OPTIONS
if ('OPTIONS' === req.method) {
res.send(200);
safely(req, function () {
res.send(200);
});
} else {
next();
}
@@ -31,9 +38,11 @@ module.exports.winstonStream = {
};
module.exports.catchAllHandler = function (e, req, res, next) {
res.send(500, {
status: status.codes.UnknownError.code
, value: "ERROR running Appium command: " + e.message
safely(req, function () {
res.send(500, {
status: status.codes.UnknownError.code
, value: "ERROR running Appium command: " + e.message
});
});
next(e);
};
@@ -230,3 +239,42 @@ module.exports.requestEndLoggingFormat = function (tokens, req, res) {
return fn(tokens, req, res);
};
function getRequestContext(req) {
if (!req) return '';
var data = '';
try {
if (req.body) data = JSON.stringify(req.body).substring(0, 200);
} catch (ign) {}
return format('context: [%s %s %s]', req.method, req.url, data).replace(/ ]$/, '');
}
// Mainly used to wrap http response methods, or for cases where errors
// perdure the domain
var safely = function () {
var args = new(Args)(arguments);
var req = args.all[0];
var fn = args.callback;
try {
fn();
} catch (err) {
logger.error('Unexpected error:', err.stack, getRequestContext(req));
}
};
module.exports.safely = safely;
module.exports.domainMiddleware = function () {
return function (req, res, next) {
var reqDomain = domain.create();
reqDomain.add(req);
reqDomain.add(res);
res.on('close', function () {
setTimeout(function () {
reqDomain.dispose();
}, 5000);
});
reqDomain.on('error', function (err) {
logger.error('Unhandled error:', err.stack, getRequestContext(req));
});
reqDomain.run(next);
};
};

View File

@@ -66,7 +66,8 @@ var http = require('http')
, conditionallyPreLaunch = helpers.conditionallyPreLaunch
, prepareTmpDir = helpers.prepareTmpDir
, requestStartLoggingFormat = require('./helpers.js').requestStartLoggingFormat
, requestEndLoggingFormat = require('./helpers.js').requestEndLoggingFormat;
, requestEndLoggingFormat = require('./helpers.js').requestEndLoggingFormat
, domainMiddleware = require('./helpers.js').domainMiddleware;
var main = function (args, readyCb, doneCb) {
@@ -88,6 +89,7 @@ var main = function (args, readyCb, doneCb) {
var rest = express()
, server = http.createServer(rest);
rest.use(domainMiddleware());
rest.use(morgan(function (tokens, req, res) {
// morgan output is redirected straight to winston
logger.info(requestEndLoggingFormat(tokens, req, res),

View File

@@ -5,7 +5,8 @@ var _s = require('underscore.string')
, status = require('./status.js')
, doRequest = require('../devices/common.js').doRequest
, respondError = require('./responses.js').respondError
, _ = require('underscore');
, _ = require('underscore')
, safely = require('./helpers').safely;
module.exports.shouldProxy = function (req) {
@@ -73,8 +74,10 @@ module.exports.doProxy = function (req, res) {
logger.debug("Proxied response received with status " +
response.statusCode + ": " +
sbody);
res.headers = response.headers;
res.set('Content-Type', response.headers['content-type']);
res.send(response.statusCode, body);
safely(req, function () {
res.headers = response.headers;
res.set('Content-Type', response.headers['content-type']);
res.send(response.statusCode, body);
});
});
};

View File

@@ -2,7 +2,8 @@
var logger = require('./logger.js').get('appium')
, status = require('./status.js')
, _ = require('underscore');
, _ = require('underscore')
, safely = require('./helpers').safely;
var getSessionId = function (req, response) {
var sessionId = (typeof response === 'undefined') ? undefined : response.sessionId;
@@ -22,13 +23,15 @@ var getSessionId = function (req, response) {
var notImplementedInThisContext = function (req, res) {
logger.debug("Responding to client that a method is not implemented " +
"in this context");
res.send(501, {
status: status.codes.UnknownError.code
, sessionId: getSessionId(req)
, value: {
message: "Not implemented in this context, try switching " +
"into or out of a web view"
}
safely(req, function () {
res.send(501, {
status: status.codes.UnknownError.code
, sessionId: getSessionId(req)
, value: {
message: "Not implemented in this context, try switching " +
"into or out of a web view"
}
});
});
};
@@ -62,7 +65,9 @@ var respondError = exports.respondError = function (req, res, statusObj, value)
var response = {status: code, value: newValue};
response.sessionId = getSessionId(req, response);
logger.debug("Responding to client with error: " + JSON.stringify(response));
res.send(500, response);
safely(req, function () {
res.send(500, response);
});
};
var respondSuccess = exports.respondSuccess = function (req, res, value, sid) {
@@ -80,7 +85,9 @@ var respondSuccess = exports.respondSuccess = function (req, res, value, sid) {
}
res.jsonResp = JSON.stringify(printResponse);
logger.debug("Responding to client with success: " + res.jsonResp);
res.send(response);
safely(req, function () {
res.send(response);
});
};
exports.getResponseHandler = function (req, res) {
@@ -118,7 +125,7 @@ exports.getResponseHandler = function (req, res) {
};
};
exports.checkMissingParams = function (res, params, strict) {
exports.checkMissingParams = function (req, res, params, strict) {
if (typeof strict === "undefined") {
strict = false;
}
@@ -131,7 +138,9 @@ exports.checkMissingParams = function (res, params, strict) {
if (missingParamNames.length > 0) {
var missingList = JSON.stringify(missingParamNames);
logger.debug("Missing params for request: " + missingList);
res.send(400, "Missing parameters: " + missingList);
safely(req, function () {
res.send(400, "Missing parameters: " + missingList);
});
return false;
} else {
return true;
@@ -140,12 +149,14 @@ exports.checkMissingParams = function (res, params, strict) {
var notYetImplemented = exports.notYetImplemented = function (req, res) {
logger.debug("Responding to client that a method is not implemented");
res.send(501, {
status: status.codes.UnknownError.code
, sessionId: getSessionId(req)
, value: {
message: "Not yet implemented. " +
"Please help us: http://appium.io/get-involved.html"
}
safely(req, function () {
res.send(501, {
status: status.codes.UnknownError.code
, sessionId: getSessionId(req)
, value: {
message: "Not yet implemented. " +
"Please help us: http://appium.io/get-involved.html"
}
});
});
};