diff --git a/lib/server/controller.js b/lib/server/controller.js index 9c611da31..95d26f68f 100644 --- a/lib/server/controller.js +++ b/lib/server/controller.js @@ -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) { diff --git a/lib/server/helpers.js b/lib/server/helpers.js index b87a64cc0..a686acf80 100644 --- a/lib/server/helpers.js +++ b/lib/server/helpers.js @@ -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); + }; +}; diff --git a/lib/server/main.js b/lib/server/main.js index 38b96de0c..75ac36756 100644 --- a/lib/server/main.js +++ b/lib/server/main.js @@ -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), diff --git a/lib/server/proxy.js b/lib/server/proxy.js index d99f5ecfb..25c29df75 100644 --- a/lib/server/proxy.js +++ b/lib/server/proxy.js @@ -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); + }); }); }; diff --git a/lib/server/responses.js b/lib/server/responses.js index 228195ac2..eb88e9d4c 100644 --- a/lib/server/responses.js +++ b/lib/server/responses.js @@ -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" + } + }); }); };