From caf7d789e9c7f99a74b4daeae89effae3d275af4 Mon Sep 17 00:00:00 2001 From: eComRobotics Team Date: Mon, 4 Nov 2013 20:19:31 +0400 Subject: [PATCH] Support for iOS logs for devices and simulators Fix after comments by @jlipps Fix require section --- .gitmodules | 3 + Gruntfile.js | 1 + lib/devices/android/android-controller.js | 44 ++++++- lib/devices/common.js | 26 ---- lib/devices/ios/ios-controller.js | 39 +++++- lib/devices/ios/ios-log.js | 137 ++++++++++++++++++++++ lib/devices/ios/ios.js | 21 ++++ package.json | 3 +- reset.sh | 6 +- submodules/libimobiledevice-macosx | 1 + 10 files changed, 247 insertions(+), 34 deletions(-) create mode 100644 lib/devices/ios/ios-log.js create mode 160000 submodules/libimobiledevice-macosx diff --git a/.gitmodules b/.gitmodules index efa24170ae0..c79fe4b88c5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "submodules/SafariLauncher"] path = submodules/SafariLauncher url = https://github.com/snevesbarros/SafariLauncher.git +[submodule "submodules/libimobiledevice-macosx"] + path = submodules/libimobiledevice-macosx + url = https://github.com/benvium/libimobiledevice-macosx.git diff --git a/Gruntfile.js b/Gruntfile.js index e7b8e91cb75..de437ade644 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -71,6 +71,7 @@ module.exports = function(grunt) { reporter: 'spec' } } + , maxBuffer: 2000*1024 }); grunt.loadNpmTasks('grunt-mocha-test'); diff --git a/lib/devices/android/android-controller.js b/lib/devices/android/android-controller.js index edb24ca3c30..2420f73180f 100644 --- a/lib/devices/android/android-controller.js +++ b/lib/devices/android/android-controller.js @@ -15,6 +15,11 @@ var errors = require('../../server/errors.js') var androidController = {}; + +var logTypesSupported = { + 'logcat' : 'Logs for Android applications on real device and emulators via ADB' +}; + androidController.keyevent = function(keycode, metastate, cb) { this.proxy(["pressKeyCode", {keycode: keycode, metastate: metastate}], cb); }; @@ -661,8 +666,43 @@ androidController.installApp = function(appPackage, cb) { deviceCommon.installApp(installationCommand, this.udid, appPackage, cb); }; -androidController.getLog = deviceCommon.getLog; -androidController.getLogTypes = deviceCommon.getLogTypes; +androidController.getLog = function(logType, cb) { + // Check if passed logType is supported + if (!_.has(logTypesSupported, logType)) { + return cb(null, { + status: status.codes.UnknownError.code + , value: "Unsupported log type '" + logType + "', supported types : " + JSON.stringify(logTypesSupported) + }); + } + var logs; + // Check that current logType and instance is compatible + if (logType == 'logcat') { + try { + logs = this.adb.getLogcatLogs(); + } catch (e) { + return cb(e); + } + } + // If logs captured sucessfully send response with data, else send error + if (logs) { + return cb(null, { + status: status.codes.Success.code + , value: logs + }); + } else { + return cb(null, { + status: status.codes.UnknownError.code + , value: "Incompatible logType for this device" + }); + } +}; + +androidController.getLogTypes = function(cb) { + return cb(null, { + status: status.codes.Success.code + , value: logTypesSupported + }); +}; androidController.unpackApp = function(req, cb) { deviceCommon.unpackApp(req, '.apk', cb); diff --git a/lib/devices/common.js b/lib/devices/common.js index afddd401d3b..c56c85e447c 100644 --- a/lib/devices/common.js +++ b/lib/devices/common.js @@ -10,7 +10,6 @@ var errors = require('../server/errors.js') var UnknownError = errors.UnknownError , ProtocolError = errors.ProtocolError; - exports.respond = function(response, cb) { if (typeof response === 'undefined') { cb(null, ''); @@ -233,28 +232,3 @@ exports.convertElementForAtoms = function(args, cb) { cb(null, args); }; -exports.getLog = function(logType, cb) { - // go ahead and respond with 'logcat' type no matter what they send in - if (logType !== 'logcat') { - logger.warn("Trying to get log type of " + logType + ", giving logcat " + - "instead"); - } - var logs; - try { - logs = this.adb.getLogcatLogs(); - } catch (e) { - return cb(e); - } - cb(null, { - status: status.codes.Success.code - , value: logs - }); -}; - -exports.getLogTypes = function(cb) { - return cb(null, { - status: status.codes.Success.code - , value: ['logcat'] - }); -}; - diff --git a/lib/devices/ios/ios-controller.js b/lib/devices/ios/ios-controller.js index f612aa5ba58..c96a4ac470a 100644 --- a/lib/devices/ios/ios-controller.js +++ b/lib/devices/ios/ios-controller.js @@ -19,6 +19,10 @@ var uuid = require('uuid-js') var iOSController = {}; +var logTypesSupported = { + 'syslog' : 'Logs for iOS applications on real device and simulators' +}; + iOSController.findUIElementOrElements = function(strategy, selector, ctx, many, cb) { selector = escapeSpecialChars(selector, "'"); if (typeof ctx === "undefined" || !ctx) { @@ -1427,12 +1431,39 @@ iOSController.getCurrentActivity= function(cb) { cb(new NotYetImplementedError(), null); }; -iOSController.getLogs = function(logType, cb) { - cb(new NotYetImplementedError(), null); +iOSController.getLogTypes = function(cb) { + return cb(null, { + status: status.codes.Success.code + , value: logTypesSupported + }); }; -iOSController.getLogTypes = function(cb) { - cb(new NotYetImplementedError(), null); +iOSController.getLog = function(logType, cb) { + // Check if passed logType is supported + if (!_.has(logTypesSupported, logType)) { + return cb(null, { + status: status.codes.UnknownError.code + , value: "Unsupported log type '" + logType + "' for this device, supported types : " + JSON.stringify(logTypesSupported) + }); + } + var logs; + try { + logs = this.logs.getLogs(logType); + } catch (e) { + return cb(e); + } + // If logs captured successfully send response with data, else send error + if (logs) { + return cb(null, { + status: status.codes.Success.code + , value: logs + }); + } else { + return cb(null, { + status: status.codes.UnknownError.code + , value: "Unknown error while getting logs" + }); + } }; module.exports = iOSController; diff --git a/lib/devices/ios/ios-log.js b/lib/devices/ios/ios-log.js new file mode 100644 index 00000000000..c61744a6e90 --- /dev/null +++ b/lib/devices/ios/ios-log.js @@ -0,0 +1,137 @@ +"use strict"; + +var spawn = require('win-spawn') + , through = require('through') + , _ = require('underscore') + , logger = require('../../server/logger.js').get('appium'); + +// Date-Utils: Polyfills for the Date object +require('date-utils'); + +var IosLog = function(opts) { + this.udid = opts.udid; + this.xcodeVersion = opts.xcodeVersion; + this.debugMode = opts.debug; + this.debugTrace = opts.debugTrace; + this.proc = null; + this.onIosLogStart = null; + this.iosLogStarted = false; + this.iosLogStartTime = null; + this.calledBack = false; + this.logs = []; + this.logRow = ""; + this.logsSinceLastRequest = []; +}; + +IosLog.prototype.debug = function(msg) { + if (this.debugMode) { + logger.debug(("[IOS_SYSLOG_CAPTURE] " + msg).grey); + } +}; + +IosLog.prototype.startCapture = function(cb) { + this.onIosLogStart = cb; + // Select cmd for log capture + if (this.udid) { + var spawnEnv = _.clone(process.env); + spawnEnv.PATH = process.env.PATH + ":" + process.cwd() + "/build/libimobiledevice-macosx/"; + spawnEnv.DYLD_LIBRARY_PATH = process.cwd() + "/build/libimobiledevice-macosx/:" + process.env.DYLD_LIBRARY_PATH + this.debug("Starting iOS device log capture via idevicesyslog"); + this.proc = spawn("idevicesyslog", ["-u", this.udid], {env: spawnEnv}); + } + else { + if (parseInt(this.xcodeVersion.split(".")[0], 10) >= 5) { + this.debug("Starting iOS 7.* simulator log capture"); + this.proc = spawn("tail", ["-f", "-n0", process.env.HOME + "/Library/Logs/iOS Simulator/7.0/system.log"]); + } + else { + this.debug("Starting iOS 6.* simulator log capture"); + this.proc = spawn("tail", ["-f", "-n0", "/var/log/system.log"]); + } + } + this.proc.stdout.setEncoding('utf8'); + this.proc.stderr.setEncoding('utf8'); + this.proc.on('error', function(err) { + logger.error("iOS log capture failed: " + err.message); + if (!this.calledBack) { + this.calledBack = true; + cb(err); + } + }.bind(this)); + this.proc.stdout.pipe(through(this.onStdout.bind(this))); + this.proc.stderr.pipe(through(this.onStderr.bind(this))); +}; + +IosLog.prototype.stopCapture = function() { + this.debug("Stopping iOS log capture"); + this.proc.kill(); + this.proc = null; +}; + +IosLog.prototype.onStdout = function(data) { + this.logRow += data; + if (data.substr(-1,1) == "\n") { + this.onOutput(data, ""); + this.logRow = ""; + } +}; + +IosLog.prototype.onStderr = function(data) { + if (/execvp\(\)/.test(data)) { + logger.error("iOS log capture process failed to start"); + if (!this.calledBack) { + this.calledBack = true; + this.onIosLogStart(new Error("iOS log capture process failed to start")); + return; + } + } + this.onOutput(data, ' STDERR'); +}; + +IosLog.prototype.onOutput = function(data, prefix) { + if (!this.iosLogStarted) { + this.iosLogStarted = true; + this.iosLogStartTime = new Date(); + if (!this.calledBack) { + this.calledBack = true; + this.onIosLogStart(); + } + } + var logs = this.logRow.split("\n"); + _.each(logs, function(log) { + log = log.trim(); + if (log) { + // Filter old log rows + var logRowParts = log.split(" "); + var logRowDate = new Date( + Date.parse(this.iosLogStartTime.getFullYear() + " " + logRowParts[0] + " " + logRowParts[2] + " " + logRowParts[3]) + ); + if (logRowDate.isAfter(this.iosLogStartTime)) { + var logObj = { + timestamp: Date.now() + , level: 'ALL' + , message: log + }; + this.logs.push(logObj); + this.logsSinceLastRequest.push(logObj); + if (this.debugMode && this.debugTrace) { + logger.debug('[IOS_SYSLOG_ROW ' + prefix + '] ' + log); + } + } + } + }.bind(this)); +}; + +IosLog.prototype.getLogs = function() { + var ret = this.logsSinceLastRequest; + this.logsSinceLastRequest = []; + return ret; +}; + +IosLog.prototype.getAllLogs = function() { + return this.logs; +}; + +module.exports = function(opts) { + return new IosLog(opts); +}; \ No newline at end of file diff --git a/lib/devices/ios/ios.js b/lib/devices/ios/ios.js index ff89da0530b..b201b39c2cb 100644 --- a/lib/devices/ios/ios.js +++ b/lib/devices/ios/ios.js @@ -14,6 +14,7 @@ var path = require('path') , helpers = require('../../helpers.js') , errors = require('../../server/errors.js') , deviceCommon = require('../common.js') + , iOSLog = require('./ios-log.js') , status = require("../../server/status.js") , IDevice = require('node-idevice') , async = require('async') @@ -38,6 +39,7 @@ var IOS = function(args) { this.automationTraceTemplatePath = args.automationTraceTemplatePath; this.removeTraceDir = args.removeTraceDir; this.deviceType = args.deviceType; + this.logs = null; this.startingOrientation = args.startingOrientation || "PORTRAIT"; this.curOrientation = this.startingOrientation; this.instruments = null; @@ -171,6 +173,7 @@ IOS.prototype.start = function(cb, onDie) { this.detectUdid.bind(this), this.parseLocalizableStrings.bind(this), this.setDeviceType.bind(this), + this.startLogCapture.bind(this), this.installToRealDevice.bind(this), this.startInstruments.bind(this), this.onInstrumentsLaunch.bind(this), @@ -301,6 +304,10 @@ IOS.prototype.onInstrumentsExit = function(code, traceDir, launchCb) { } }.bind(this); + if (this.logs !== null) { + this.logs.stopCapture(); + } + async.series([removeTraceDir, cleanup], function() {}); }; @@ -705,6 +712,20 @@ IOS.prototype.unpackApp = function(req, cb) { deviceCommon.unpackApp(req, '.app', cb); }; +IOS.prototype.startLogCapture = function(cb) { + if (this.logs !== null) { + cb(new Error("Trying to start iOS log capture but it's already started!")); + return; + } + this.logs = new iOSLog({ + udid: this.udid + , xcodeVersion: this.xcodeVersion + , debug: false + , debugTrace: false + }); + this.logs.startCapture(cb); +}; + _.each(_.keys(iOSHybrid), function(method) { IOS.prototype[method] = iOSHybrid[method]; }); diff --git a/package.json b/package.json index 1af2031e0f3..3f505273656 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "ws": "0.4.31", "socket.io" : "~0.9.16", "MD5" : "~1.1.0", - "through": "~2.3.4" + "through": "~2.3.4", + "date-utils": "~1.2.14" }, "scripts": { "test": "grunt travis" diff --git a/reset.sh b/reset.sh index c82a9b3db68..554a6b6128b 100755 --- a/reset.sh +++ b/reset.sh @@ -179,7 +179,11 @@ reset_ios() { echo "* Copying SafariLauncher for real devices to build" run_cmd zip -r build/SafariLauncher/SafariLauncher submodules/SafariLauncher/build/Release-iphoneos/SafariLauncher.app fi - + echo "* Cloning/updating libimobiledevice-macosx" + run_cmd git submodule update --init submodules/libimobiledevice-macosx + echo "* Copying libimobiledevice-macosx to build" + run_cmd rm -rf build/libimobiledevice-macosx + run_cmd cp -r submodules/libimobiledevice-macosx build/libimobiledevice-macosx } get_apidemos() { diff --git a/submodules/libimobiledevice-macosx b/submodules/libimobiledevice-macosx new file mode 160000 index 00000000000..8507dd21f9f --- /dev/null +++ b/submodules/libimobiledevice-macosx @@ -0,0 +1 @@ +Subproject commit 8507dd21f9f3198e988ad68b306613097721523c