From abe22334b1dcd94dfcebc28482b1fd76a7c7cb2f Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Tue, 10 Dec 2013 13:25:58 -0800 Subject: [PATCH] refactor server/main and add --merciful flag to avoid the force quit instruments watcher --- docs/server-args.md | 1 + lib/server/helpers.js | 234 +++++++++++++++++++++++++++++++++++++++++ lib/server/main.js | 237 ++++-------------------------------------- lib/server/parser.js | 10 ++ 4 files changed, 264 insertions(+), 218 deletions(-) create mode 100644 lib/server/helpers.js diff --git a/docs/server-args.md b/docs/server-args.md index 3b512603f..c7399d543 100644 --- a/docs/server-args.md +++ b/docs/server-args.md @@ -25,6 +25,7 @@ All flags are optional, but some are required in conjunction with certain others |`--log-no-colors`|false|Don't use colors in console output|| |`-G`, `--webhook`|null|Also send log output to this HTTP listener|`--webhook localhost:9876`| |`--native-instruments-lib`|false|(IOS-only) IOS has a weird built-in unavoidable delay. We patch this in appium. If you do not want it patched, pass in this flag.|| +|`--merciful`, `-m`|false|Don't run the watcher process that will force-kill an unresponsive instruments|| |`--app-pkg`|null|(Android-only) Java package of the Android app you want to run (e.g., com.example.android.myApp)|`--app-pkg com.example.android.myApp`| |`--app-activity`|null|(Android-only) Activity name for the Android activity you want to launch from your package (e.g., MainActivity)|`--app-activity MainActivity`| |`--app-wait-activity`|false|(Android-only) Activity name for the Android activity you want to wait for (e.g., SplashActivity)|`--app-wait-activity SplashActivity`| diff --git a/lib/server/helpers.js b/lib/server/helpers.js new file mode 100644 index 000000000..c8f477fbb --- /dev/null +++ b/lib/server/helpers.js @@ -0,0 +1,234 @@ +"use strict"; + +var isWindows = require('../helpers.js').isWindows() + , _ = require("underscore") + , exec = require('child_process').exec + , spawn = require('child_process').spawn + , path = require('path') + , endInstrumentsPath = path.resolve(__dirname, '../../build/force_quit/ForceQuitUnresponsiveApps.app/Contents/MacOS/ForceQuitUnresponsiveApps') + , gridRegister = require('./grid-register.js') + , logger = require('./logger.js').get('appium') + , status = require('./status.js') + , async = require('async') + , through = require('through') + , fs = require('fs') + , io = require('socket.io') + , bytes = require('bytes'); + +var watchForUnresponsiveInstruments = function(cb) { + if (isWindows) return; + + var endOldProcess = function(cb) { + exec("killall -9 ForceQuitUnresponsiveApps", { maxBuffer: 524288 }, function() { cb(); }); + }; + + var cleanLogs = function(data) { + var re = /[0-9\-:. ]+ForceQuitUnresponsiveApps\[[0-9:]+\] /g; + return data.replace(re, ''); + }; + + var startNewprocess = function(cb) { + logger.info("Spawning instruments force-quitting watcher process"); + var process = spawn(endInstrumentsPath); + process.stdout.setEncoding('utf8'); + process.stderr.setEncoding('utf8'); + + process.stderr.pipe(through(function(data) { + logger.info('[FQInstruments] ' + cleanLogs(data.trim())); + })); + + process.stdout.pipe(through(function(data) { + logger.info('[FQInstruments STDERR] ' + cleanLogs(data.trim())); + })); + + cb(); + }; + + async.series([ + endOldProcess, + startNewprocess + ], cb); +}; + +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'); + + // need to respond 200 to OPTIONS + + if ('OPTIONS' === req.method) { + res.send(200); + } else { + next(); + } +}; + +module.exports.winstonStream = { + write: function(msg) { + msg = msg.replace(/$\s*$/m, ""); + msg = msg.replace(/\[[^\]]+\] /, ""); + logger.log('debug', msg); + } +}; + +module.exports.catchAllHandler = function(e, req, res, next) { + res.send(500, { + status: status.codes.UnknownError.code + , value: "ERROR running Appium command: " + e.message + }); + next(e); +}; + +module.exports.checkArgs = function(args) { + var exclusives = [ + ['noReset', 'fullReset'] + , ['ipa', 'safari'] + , ['app', 'safari'] + , ['forceIphone', 'forceIpad'] + , ['deviceName', 'defaultDevice'] + ]; + + _.each(exclusives, function(exSet) { + var numFoundInArgs = 0; + _.each(exSet, function(opt) { + if (_.has(args, opt) && args[opt]) { + numFoundInArgs++; + } + }); + if (numFoundInArgs > 1) { + console.error(("You can't pass in more than one argument from the set " + + JSON.stringify(exSet) + ", since they are mutually exclusive").red); + process.exit(1); + } + }); +}; + +module.exports.noColorLogger = function(tokens, req, res) { + var len = parseInt(res.getHeader('Content-Length'), 10); + + len = isNaN(len) ? '' : ' - ' + bytes(len); + return req.method + ' ' + req.originalUrl + ' ' + + res.statusCode + ' '+ (new Date() - req._startTime) + 'ms' + len; +}; + +module.exports.configureServer = function(configFile, appiumRev, appiumVer, + appiumServer, cb) { + fs.readFile(configFile, function(err, data) { + if (err) { + logger.error("Could not find config file; looks like config hasn't " + + "been run! Please run reset.sh or appium configure."); + return cb(err); + } + var rawConfig; + try { + rawConfig = JSON.parse(data.toString('utf8')); + } catch (e) { + logger.error("Error parsing configuration json, please re-run config"); + return cb(e); + } + var versionMismatches = {}; + var excludedKeys = ["git-sha", "node_bin", "built"]; + _.each(rawConfig, function(deviceConfig, key) { + if (deviceConfig.version !== appiumVer && !_.contains(excludedKeys, key)) { + versionMismatches[key] = deviceConfig.version; + } else if (key === "git-sha") { + appiumRev = rawConfig['git-sha']; + } + }); + if (_.keys(versionMismatches).length) { + logger.error("Got some configuration version mismatches. Appium is " + + "at " + appiumVer + "."); + _.each(versionMismatches, function(mismatchedVer, key) { + logger.error(key + " configured at " + mismatchedVer); + }); + logger.error("Please re-run reset.sh or config"); + return cb(new Error("Appium / config version mismatch")); + } else { + appiumServer.registerConfig(rawConfig); + cb(null); + } + }); +}; + +module.exports.conditionallyPreLaunch = function(args, appiumServer, cb) { + if (args.launch) { + logger.info("Starting Appium in pre-launch mode"); + appiumServer.preLaunch(function(err) { + if (err) { + logger.error("Could not pre-launch appium: " + err); + cb(err); + } else { + cb(null); + } + }); + } else { + cb(null); + } +}; + +var startAlertSocket = function(restServer, appiumServer, logColors) { + var alerts = io.listen(restServer, { + 'flash policy port': -1, + 'log colors': logColors + }); + + alerts.configure(function() { + alerts.set('log level', 1); + alerts.set("polling duration", 10); + alerts.set("transports", ['websocket', 'flashsocket']); + }); + + alerts.sockets.on("connection", function (socket) { + socket.set('log level', 1); + logger.info("Client connected: " + (socket.id).toString()); + + socket.on('disconnect', function(data) { + logger.info("Client disconnected: " + data); + }); + }); + + // add web socket so we can emit events + appiumServer.attachSocket(alerts); +}; + +module.exports.startListening = function(server, args, appiumVer, appiumRev, appiumServer, cb) { + var alreadyReturned = false; + server.listen(args.port, args.address, function() { + if (!args.merciful) { + watchForUnresponsiveInstruments(function(){}); + } + var welcome = "Welcome to Appium v" + appiumVer; + if (appiumRev) { + welcome += " (REV " + appiumRev + ")"; + } + logger.info(welcome); + var logMessage = "Appium REST http interface listener started on " + + args.address + ":" + args.port; + logger.info(logMessage); + startAlertSocket(server, appiumServer, !args.logNoColors); + if (args.nodeconfig !== null) { + gridRegister.registerNode(args.nodeconfig); + } + }); + server.on('error', function(err) { + if (err.code === 'EADDRNOTAVAIL') { + logger.error("Couldn't start Appium REST http interface listener. Requested address is not available."); + } else { + logger.error("Couldn't start Appium REST http interface listener. Requested port is already in use. Please make sure there's no other instance of Appium running already."); + } + if (!alreadyReturned) { + alreadyReturned = true; + cb(err); + } + }); + server.on('connection', function(socket) { + socket.setTimeout(600 * 1000); // 10 minute timeout + }); + setTimeout(function() { + if (!alreadyReturned) { + alreadyReturned = true; + cb(null); + } + }, 1000); +}; diff --git a/lib/server/main.js b/lib/server/main.js index 13ceca640..0fc07a38f 100644 --- a/lib/server/main.js +++ b/lib/server/main.js @@ -17,20 +17,19 @@ var http = require('http') , fs = require('fs') , appium = require('../appium.js') , parserWrap = require('./middleware').parserWrap - , status = require('./status.js') , appiumVer = require('../../package.json').version , appiumRev = null , async = require('async') - , _ = require("underscore") - , io = require('socket.io') - , gridRegister = require('./grid-register.js') , configFile = path.resolve(__dirname, "..", "..", ".appiumconfig") - , bytes = require('bytes') - , isWindows = require('../helpers.js').isWindows() - , exec = require('child_process').exec - , spawn = require('child_process').spawn - , through = require('through') - , endInstrumentsPath = path.resolve(__dirname, '../../build/force_quit/ForceQuitUnresponsiveApps.app/Contents/MacOS/ForceQuitUnresponsiveApps'); + , helpers = require('./helpers') + , allowCrossDomain = helpers.allowCrossDomain + , catchAllHandler = helpers.catchAllHandler + , checkArgs = helpers.checkArgs + , winstonStream = helpers.winstonStream + , configureServer = helpers.configureServer + , startListening = helpers.startListening + , conditionallyPreLaunch = helpers.conditionallyPreLaunch + , noColorLogger = helpers.noColorLogger; if (require.main === module && args.showConfig) { try { @@ -43,89 +42,6 @@ if (require.main === module && args.showConfig) { } } -var watchForUnresponsiveInstruments = function(cb) { - if (isWindows) return; - - var endOldProcess = function(cb) { - exec("killall -9 ForceQuitUnresponsiveApps", { maxBuffer: 524288 }, function(err) { cb(); }); - }; - - var startNewprocess = function(cb) { - var process = spawn(endInstrumentsPath); - process.stdout.setEncoding('utf8'); - process.stderr.setEncoding('utf8'); - - process.stderr.pipe(through(function(data) { - logger.info('[ForceQuitUnresponsiveApps] ' + data.trim()); - })); - - process.stdout.pipe(through(function(data) { - logger.info('[ForceQuitUnresponsiveApps STDERR] ' + data.trim()); - })); - - cb(); - }; - - async.series([ - endOldProcess, - startNewprocess - ], cb); -}; - -var 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'); - - // need to respond 200 to OPTIONS - - if ('OPTIONS' === req.method) { - res.send(200); - } else { - next(); - } -}; - -var winstonStream = { - write: function(msg) { - msg = msg.replace(/$\s*$/m, ""); - msg = msg.replace(/\[[^\]]+\] /, ""); - logger.log('debug', msg); - } -}; - -var catchAllHandler = function(e, req, res, next) { - res.send(500, { - status: status.codes.UnknownError.code - , value: "ERROR running Appium command: " + e.message - }); - next(e); -}; - -var checkArgs = function(args) { - var exclusives = [ - ['noReset', 'fullReset'] - , ['ipa', 'safari'] - , ['app', 'safari'] - , ['forceIphone', 'forceIpad'] - , ['deviceName', 'defaultDevice'] - ]; - - _.each(exclusives, function(exSet) { - var numFoundInArgs = 0; - _.each(exSet, function(opt) { - if (_.has(args, opt) && args[opt]) { - numFoundInArgs++; - } - }); - if (numFoundInArgs > 1) { - console.error(("You can't pass in more than one argument from the set " + - JSON.stringify(exSet) + ", since they are mutually exclusive").red); - process.exit(1); - } - }); -}; - var main = function(args, readyCb, doneCb) { checkArgs(args); if (typeof doneCb === "undefined") { @@ -140,15 +56,7 @@ var main = function(args, readyCb, doneCb) { rest.use(allowCrossDomain); if (!args.quiet) { if (args.logNoColors) { - var devNoColor = function(tokens, req, res){ - var status = res.statusCode - , len = parseInt(res.getHeader('Content-Length'), 10); - - len = isNaN(len) ? '' : ' - ' + bytes(len); - return req.method + ' ' + req.originalUrl + ' ' + - res.statusCode + ' '+ (new Date() - req._startTime) + 'ms' + len; - }; - rest.use(express.logger(devNoColor)); + rest.use(express.logger(noColorLogger)); } else { rest.use(express.logger('dev')); } @@ -170,124 +78,17 @@ var main = function(args, readyCb, doneCb) { // Hook up REST http interface appiumServer.attachTo(rest); - var checkSetup = function(cb) { - fs.readFile(configFile, function(err, data) { - if (err) { - logger.error("Could not find config file; looks like config hasn't " + - "been run! Please run reset.sh or appium configure."); - return cb(err); - } - var rawConfig; - try { - rawConfig = JSON.parse(data.toString('utf8')); - } catch (e) { - logger.error("Error parsing configuration json, please re-run config"); - return cb(e); - } - var versionMismatches = {}; - var excludedKeys = ["git-sha", "node_bin", "built"]; - _.each(rawConfig, function(deviceConfig, key) { - if (deviceConfig.version !== appiumVer && !_.contains(excludedKeys, key)) { - versionMismatches[key] = deviceConfig.version; - } else if (key === "git-sha") { - appiumRev = rawConfig['git-sha']; - } - }); - if (_.keys(versionMismatches).length) { - logger.error("Got some configuration version mismatches. Appium is " + - "at " + appiumVer + "."); - _.each(versionMismatches, function(mismatchedVer, key) { - logger.error(key + " configured at " + mismatchedVer); - }); - logger.error("Please re-run reset.sh or config"); - return cb(new Error("Appium / config version mismatch")); - } else { - appiumServer.registerConfig(rawConfig); - cb(null); - } - }); - }; - - var conditionallyPreLaunch = function(cb) { - if (args.launch) { - logger.info("Starting Appium in pre-launch mode"); - appiumServer.preLaunch(function(err) { - if (err) { - logger.error("Could not pre-launch appium: " + err); - cb(err); - } else { - cb(null); - } - }); - } else { - cb(null); - } - }; - - var startAlertSocket = function() { - var alerts = io.listen(server, {'flash policy port': -1, 'log colors': !args.logNoColors}); - alerts.configure(function() { - alerts.set('log level', 1); - alerts.set("polling duration", 10); - alerts.set("transports", ['websocket', 'flashsocket']); - }); - - alerts.sockets.on("connection", function (socket) { - socket.set('log level', 1); - logger.info("Client connected: " + (socket.id).toString()); - - socket.on('disconnect', function(data) { - logger.info("Client disconnected: " + data); - }); - }); - - // add web socket so we can emit events - appiumServer.attachSocket(alerts); - }; - - var startListening = function(cb) { - var alreadyReturned = false; - server.listen(args.port, args.address, function() { - watchForUnresponsiveInstruments(function(){}); - var welcome = "Welcome to Appium v" + appiumVer; - if (appiumRev) { - welcome += " (REV " + appiumRev + ")"; - } - logger.info(welcome); - var logMessage = "Appium REST http interface listener started on " + - args.address + ":" + args.port; - logger.info(logMessage); - startAlertSocket(); - if (args.nodeconfig !== null) { - gridRegister.registerNode(args.nodeconfig); - } - }); - server.on('error', function(err) { - if (err.code === 'EADDRNOTAVAIL') { - logger.error("Couldn't start Appium REST http interface listener. Requested address is not available."); - } else { - logger.error("Couldn't start Appium REST http interface listener. Requested port is already in use. Please make sure there's no other instance of Appium running already."); - } - if (!alreadyReturned) { - alreadyReturned = true; - cb(err); - } - }); - server.on('connection', function(socket) { - socket.setTimeout(600 * 1000); // 10 minute timeout - }); - setTimeout(function() { - if (!alreadyReturned) { - alreadyReturned = true; - cb(null); - } - }, 1000); - }; async.series([ - checkSetup - , conditionallyPreLaunch - , startListening + function(cb) { + configureServer(configFile, appiumRev, appiumVer, appiumServer, cb); + }, + function(cb) { + conditionallyPreLaunch(args, appiumServer, cb); + }, + function(cb) { + startListening(server, args, appiumVer, appiumRev, appiumServer, cb); + } ], function(err) { if (err) { process.exit(1); diff --git a/lib/server/parser.js b/lib/server/parser.js index 296856051..fc975f4a4 100644 --- a/lib/server/parser.js +++ b/lib/server/parser.js @@ -149,6 +149,16 @@ var args = [ , nargs: 0 }], + [['--merciful', '-m'], { + defaultValue: false + , dest: 'merciful' + , action: 'storeTrue' + , required: false + , help: "Don't run the watcher process that will force-kill an " + + "unresponsive instruments" + , nargs: 0 + }], + [['--app-pkg'], { dest: 'androidPackage' , defaultValue: null