mirror of
https://github.com/appium/appium.git
synced 2026-04-28 14:30:27 -05:00
587 lines
17 KiB
JavaScript
587 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
var logger = require('../../server/logger.js').get('appium')
|
|
, _ = require('underscore')
|
|
, fs = require('fs')
|
|
, async = require('async')
|
|
, exec = require('child_process').exec
|
|
, path = require('path')
|
|
, ncp = require('ncp')
|
|
, mkdirp = require('mkdirp')
|
|
, xcode = require('./xcode.js')
|
|
, simctl = require('../../future').simctl
|
|
, multiResolve = require('../../helpers.js').multiResolve
|
|
, rimraf = require('rimraf')
|
|
, xmlplist = require('plist')
|
|
, bplistCreate = require('bplist-creator')
|
|
, bplistParse = require('bplist-parser');
|
|
|
|
var Simulator = function (opts) {
|
|
var requiredOpts = ['sdkVer', 'platformVer', 'udid'];
|
|
_.each(requiredOpts, function (opt) {
|
|
if (!_.has(opts, opt)) {
|
|
throw new Error(opt + " is required");
|
|
}
|
|
this[opt] = opts[opt];
|
|
}.bind(this));
|
|
};
|
|
|
|
var wrapInArray = function (res) {
|
|
if (res) {
|
|
return [res];
|
|
} else {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
var rmrf = function (delPath, cb) {
|
|
exec("rm -rf '" + delPath + "'", function (err) {
|
|
cb(err);
|
|
});
|
|
};
|
|
|
|
var safeRimRafSync = function (delPath, tries) {
|
|
if (_.isUndefined(tries)) tries = 0;
|
|
try {
|
|
rimraf.sync(delPath);
|
|
} catch (e) {
|
|
if (tries < 20) {
|
|
if (e.message.indexOf("ENOTEMPTY") !== -1) {
|
|
logger.debug("Path " + delPath + " was not empty during delete; retrying");
|
|
return safeRimRafSync(delPath, tries + 1);
|
|
} else if (e.message.indexOf("ENOENT") !== -1) {
|
|
logger.debug("Path " + delPath + " didn't exist when we tried to delete, ignoring");
|
|
return safeRimRafSync(delPath, tries + 1);
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
Simulator.prototype.getRootDir = function () {
|
|
var u = process.env.USER;
|
|
if (parseFloat(this.sdkVer) >= 8) {
|
|
return path.resolve("/Users", u, "Library", "Developer",
|
|
"CoreSimulator", "Devices");
|
|
}
|
|
|
|
return path.resolve("/Users", u, "Library", "Application Support",
|
|
"iPhone Simulator");
|
|
};
|
|
|
|
Simulator.prototype.getDirs = function () {
|
|
var base;
|
|
if (parseFloat(this.sdkVer) >= 8) {
|
|
base = path.resolve(this.getRootDir(), this.udid, "data");
|
|
if (fs.existsSync(base)) {
|
|
return [base];
|
|
}
|
|
return [];
|
|
} else {
|
|
var sdk = this.platformVer.toString();
|
|
base = this.getRootDir();
|
|
var list = [];
|
|
try {
|
|
list = fs.readdirSync(base);
|
|
} catch (e) {
|
|
}
|
|
var dirs = [];
|
|
_.each(list, function (sdkDir) {
|
|
if (sdkDir.indexOf(sdk) !== -1) {
|
|
dirs.push(path.resolve(base, sdkDir));
|
|
}
|
|
});
|
|
return dirs;
|
|
}
|
|
};
|
|
|
|
Simulator.getPlistData = function (file) {
|
|
var data;
|
|
if (fs.existsSync(file)) {
|
|
var fileData = fs.readFileSync(file);
|
|
try {
|
|
data = bplistParse.parseBuffer(fileData)[0];
|
|
} catch (err) {
|
|
if (err.message.indexOf("Invalid binary plist") !== -1) {
|
|
logger.debug("Plist was not binary format, retrying with xml");
|
|
data = xmlplist(file)[0];
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error("Settings file " + file + " did not exist");
|
|
}
|
|
return data;
|
|
};
|
|
|
|
var findAppByCondition = function (dir, condition) {
|
|
var list;
|
|
try {
|
|
list = fs.readdirSync(dir);
|
|
} catch (e) {
|
|
if (/ENOENT/.test(e.message)) {
|
|
logger.warn("Applications directory " + dir + " doesn't exist. " +
|
|
"Have you run this simulator before?");
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
var appDir = null;
|
|
for (var i = 0; i < list.length; i++) {
|
|
if (condition(list[i])) {
|
|
appDir = path.resolve(dir, list[i]);
|
|
break;
|
|
}
|
|
}
|
|
return appDir;
|
|
};
|
|
|
|
Simulator.prototype.getSafariDirs = function () {
|
|
if (parseFloat(this.platformVer) >= 8) {
|
|
return wrapInArray(this.getSafari8Dir());
|
|
} else {
|
|
return this.getSafari7Dirs();
|
|
}
|
|
};
|
|
|
|
Simulator.prototype.getAppDirs = function (appFile, appBundleId) {
|
|
if (parseFloat(this.platformVer) >= 8) {
|
|
var dataDirs = wrapInArray(this.getApp8Dir(appBundleId, "Data"));
|
|
var bundleDirs = wrapInArray(this.getApp8Dir(appBundleId, "Bundle"));
|
|
return dataDirs.concat(bundleDirs);
|
|
} else {
|
|
return this.getApp7Dirs([appFile]);
|
|
}
|
|
};
|
|
|
|
Simulator.prototype.getSafari7Dirs = function () {
|
|
return this.getApp7Dirs(["MobileSafari.app", "Appium-MobileSafari-" + this.platformVer + ".app"]);
|
|
};
|
|
|
|
Simulator.prototype.getApp7Dirs = function (appFiles) {
|
|
var appsDirs = multiResolve(this.getDirs(), "Applications");
|
|
var foundAppDirs = [];
|
|
_.each(appsDirs, function (appsDir) {
|
|
var cond = function (d) {
|
|
var found = false;
|
|
_.each(appFiles, function (appFile) {
|
|
found = found || fs.existsSync(path.resolve(appsDir, d, appFile));
|
|
});
|
|
return found;
|
|
};
|
|
var appDir = findAppByCondition(appsDir, cond);
|
|
if (appDir !== null) {
|
|
foundAppDirs.push(appDir);
|
|
}
|
|
});
|
|
return foundAppDirs;
|
|
};
|
|
|
|
Simulator.prototype.getSafari8Dir = function () {
|
|
return this.getApp8Dir("com.apple.mobilesafari", "Data");
|
|
};
|
|
|
|
Simulator.prototype.getApp8Dir = function (bundleId, dataOrBundle) {
|
|
var root = this.getDirs()[0];
|
|
if (!root) {
|
|
return null;
|
|
}
|
|
var appsDir = path.resolve(root, "Containers", dataOrBundle, "Application");
|
|
var magicFile = ".com.apple.mobile_container_manager.metadata.plist";
|
|
var cond = function (d) {
|
|
var magicPlist = path.resolve(appsDir, d, magicFile);
|
|
if (!fs.existsSync(magicPlist)) {
|
|
return false;
|
|
}
|
|
var data = Simulator.getPlistData(magicPlist);
|
|
return data.MCMMetadataIdentifier === bundleId;
|
|
};
|
|
return findAppByCondition(appsDir, cond);
|
|
};
|
|
|
|
Simulator.prototype.dirsExist = function () {
|
|
return this.getDirs().length > 0;
|
|
};
|
|
|
|
Simulator.prototype.safariDirsExist = function () {
|
|
try {
|
|
return this.getSafariDirs().length > 0;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
Simulator.prototype.cleanSafari = function (keepPrefs, cb) {
|
|
// this method is async because we use glob paths for deletes and rimraf
|
|
// can't handle them, so we use our own async 'rmrf' model that calls out
|
|
// to shell to do it
|
|
logger.debug("Cleaning mobile safari data files");
|
|
if (!this.dirsExist()) {
|
|
logger.info("Couldn't find Safari support directories to clean out old " +
|
|
"data. Probably there's nothing to clean out");
|
|
return cb();
|
|
}
|
|
|
|
var libraryDirs = multiResolve(this.getDirs(), 'Library');
|
|
var safariLibDirs = multiResolve(this.getSafariDirs(), 'Library');
|
|
var toDeletes = [
|
|
'Caches/Snapshots/com.apple.mobilesafari'
|
|
, 'Caches/com.apple.mobilesafari/Cache.db*'
|
|
, 'Caches/com.apple.WebAppCache/*.db'
|
|
, 'Safari'
|
|
, 'WebKit/LocalStorage/*.*'
|
|
, 'WebKit/GeolocationSites.plist'
|
|
, 'Cookies/*.binarycookies'
|
|
];
|
|
var deleteActions = [];
|
|
|
|
var buildLibraryDeletes = function (libDirs, toDeletes) {
|
|
_.each(libDirs, function (libraryDir) {
|
|
_.each(toDeletes, function (relRmPath) {
|
|
var rmPath = path.resolve(libraryDir, relRmPath);
|
|
deleteActions.push(function (cb) {
|
|
logger.debug("Deleting " + rmPath);
|
|
rmrf(rmPath, cb);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
var safariToDeletes = _.clone(toDeletes);
|
|
if (!keepPrefs) {
|
|
safariToDeletes.push('Preferences/*.plist');
|
|
}
|
|
|
|
buildLibraryDeletes(libraryDirs, toDeletes);
|
|
buildLibraryDeletes(safariLibDirs, safariToDeletes);
|
|
|
|
async.parallel(deleteActions, cb);
|
|
};
|
|
|
|
Simulator.prototype.deleteSafari = function () {
|
|
logger.debug("Deleting Safari apps");
|
|
var safariDirs = multiResolve(this.getSafariDirs());
|
|
if (parseFloat(this.platformVer) >= 8) {
|
|
var safariBundleDir = this.getApp8Dir("com.apple.mobilesafari", "Bundle");
|
|
safariDirs = safariDirs.concat(wrapInArray(safariBundleDir));
|
|
}
|
|
_.each(safariDirs, function (safariDir) {
|
|
logger.debug("Deleting " + safariDir);
|
|
safeRimRafSync(safariDir);
|
|
});
|
|
};
|
|
|
|
Simulator.prototype.cleanCustomApp = function (appFile, appBundleId) {
|
|
logger.debug("Cleaning app data files");
|
|
var appDirs = this.getAppDirs(appFile, appBundleId);
|
|
if (appDirs.length === 0) {
|
|
logger.info("Couldn't find app directories to delete. Probably it's not " +
|
|
"installed");
|
|
return;
|
|
}
|
|
_.each(appDirs, function (appDir) {
|
|
logger.debug("Deleting " + appDir);
|
|
safeRimRafSync(appDir);
|
|
});
|
|
|
|
if (parseFloat(this.platformVer) >= 8) {
|
|
var toDeletes = [
|
|
'Library/Preferences/' + appBundleId + '.plist'
|
|
];
|
|
_.each(this.getDirs(), function (simDir) {
|
|
_.each(toDeletes, function (relRmPath) {
|
|
var rmPath = path.resolve(simDir, relRmPath);
|
|
logger.debug("Deleting " + rmPath);
|
|
safeRimRafSync(rmPath);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
Simulator.prototype.cleanSim = function (keepKeychains, tempDir, cb) {
|
|
logger.debug("Cleaning sim data files");
|
|
|
|
|
|
var cleanSimVLessThanEight = function (keepKeychains, cb) {
|
|
cb = _.once(cb);
|
|
if (!this.dirsExist()) {
|
|
logger.info("Couldn't find support directories to clean out old " +
|
|
"data. Probably there's nothing to clean out");
|
|
return cb();
|
|
}
|
|
var toDeletes = [
|
|
'Library/TCC',
|
|
'Library/Caches/locationd',
|
|
'Library/BackBoard/applicationState.plist',
|
|
'Media'
|
|
];
|
|
if (!keepKeychains) {
|
|
toDeletes.push('Library/Keychains');
|
|
}
|
|
_.each(this.getDirs(), function (simDir) {
|
|
_.each(toDeletes, function (relRmPath) {
|
|
var rmPath = path.resolve(simDir, relRmPath);
|
|
logger.debug("Deleting " + rmPath);
|
|
try {
|
|
safeRimRafSync(rmPath);
|
|
} catch (e) {
|
|
cb(e);
|
|
}
|
|
});
|
|
});
|
|
|
|
logger.debug("Cleaning sim preferences");
|
|
var prefsPlist = Simulator.getSimAppPrefsFile();
|
|
var data = Simulator.getPlistData(prefsPlist);
|
|
var keysToClear = [
|
|
'CurrentDeviceUDID',
|
|
'SimulateDevice'
|
|
];
|
|
var changed = false;
|
|
_.each(keysToClear, function (key) {
|
|
if (_.has(data, key)) {
|
|
logger.debug("Clearing key: " + key);
|
|
delete data[key];
|
|
changed = true;
|
|
}
|
|
});
|
|
if (changed) {
|
|
logger.debug("Writing new preferences plist data");
|
|
return fs.writeFile(prefsPlist, bplistCreate(data), cb);
|
|
}
|
|
return cb();
|
|
}.bind(this);
|
|
|
|
|
|
var cleanSimVEight = function (keepKeychains, tempDir, cb) {
|
|
|
|
var base = path.resolve(this.getRootDir(), this.udid, "data");
|
|
var keychainPath = path.resolve(base, "Library", "Keychains");
|
|
var tempKeychainPath = path.resolve(tempDir, "simkeychains_" + this.udid);
|
|
|
|
var removeKeychains = _.partial(ncp, keychainPath, tempKeychainPath);
|
|
|
|
var returnKeychains = function (err, cb) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
mkdirp(keychainPath, function (err) {
|
|
if (err) { return cb(err); }
|
|
ncp(tempKeychainPath, keychainPath, cb);
|
|
});
|
|
};
|
|
|
|
var performWhileSavingKeychains = function (enclosed, cb) {
|
|
async.waterfall([
|
|
removeKeychains,
|
|
enclosed,
|
|
returnKeychains
|
|
], cb);
|
|
};
|
|
|
|
var cleanSim = _.partial(simctl.eraseDevice, this.udid);
|
|
|
|
if (keepKeychains) {
|
|
logger.debug("Resetting simulator and saving Keychains");
|
|
performWhileSavingKeychains(cleanSim, cb);
|
|
} else {
|
|
cleanSim(cb);
|
|
}
|
|
|
|
}.bind(this);
|
|
|
|
|
|
|
|
if (this.sdkVer >= 8) {
|
|
cleanSimVEight(keepKeychains, tempDir, cb);
|
|
} else {
|
|
cleanSimVLessThanEight(keepKeychains, cb);
|
|
}
|
|
|
|
};
|
|
|
|
Simulator.prototype.moveBuiltInApp = function (appPath, appName, newAppDir, cb) {
|
|
ncp(appPath, newAppDir, function (err) {
|
|
if (err) return cb(err);
|
|
logger.debug("Copied " + appName + " to " + newAppDir);
|
|
rimraf(appPath, function (err) {
|
|
if (err) {
|
|
if (err.message.indexOf("EACCES") !== -1) {
|
|
return cb(new Error("We don't have write access to " + appPath +
|
|
", please re-run authorize as " + process.env.USER));
|
|
}
|
|
return cb(err);
|
|
}
|
|
logger.debug("Temporarily deleted original app at " + appPath);
|
|
cb(null, newAppDir, appPath);
|
|
});
|
|
});
|
|
};
|
|
|
|
Simulator.prototype.getBuiltInApp = function (appName, cb) {
|
|
this.getBuiltInAppDir(function (err, appDir) {
|
|
if (err) return cb(err);
|
|
var appPath = path.resolve(appDir, appName + ".app");
|
|
fs.stat(appPath, function (err, s) {
|
|
if (err && err.message.indexOf("ENOENT") !== -1) {
|
|
logger.debug("App is not at " + appPath);
|
|
return cb(err);
|
|
}
|
|
cb(null, s, appPath);
|
|
});
|
|
});
|
|
};
|
|
|
|
Simulator.prototype.prepareBuiltInApp = function (appName, tmpDir, cb) {
|
|
logger.debug("Looking for built in app " + appName);
|
|
var newAppDir = path.resolve(tmpDir, 'Appium-' + appName + '-' + this.platformVer + '.app');
|
|
var checkApp = function (s, appPath, cb) {
|
|
if (!s.isDirectory()) {
|
|
cb(new Error("App package was not a directory"), appPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
this.getBuiltInApp(appName, function (err, s, appPath) {
|
|
if (err) {
|
|
fs.stat(newAppDir, function (err, s) {
|
|
if (err) {
|
|
logger.warn("App is also not at " + newAppDir);
|
|
return cb(new Error("Couldn't find built in app in its home " +
|
|
"or temp dir!"));
|
|
}
|
|
if (checkApp(s, appPath, cb)) {
|
|
logger.debug("Couldn't find original app, but found the temp " +
|
|
"Appium one so using that");
|
|
cb(null, newAppDir, appPath);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (checkApp(s, appPath, cb)) {
|
|
if (parseInt(this.platformVer, 10) < 7) {
|
|
logger.debug("Got app, trying to copy " + appPath + " to tmp dir");
|
|
ncp(appPath, newAppDir, function (err) {
|
|
if (err) return cb(err);
|
|
logger.debug("Copied " + appName);
|
|
cb(null, newAppDir, null);
|
|
});
|
|
} else {
|
|
logger.debug("Got app, trying to move " + appPath + " to tmp dir");
|
|
this.moveBuiltInApp(appPath, appName, newAppDir, cb);
|
|
}
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
Simulator.prototype.getBuiltInAppDir = function (cb) {
|
|
xcode.getPath(function (err, xcodeDir) {
|
|
if (err) return cb(err);
|
|
var appDir = path.resolve(xcodeDir, "Platforms/iPhoneSimulator.platform/" +
|
|
"Developer/SDKs/iPhoneSimulator" +
|
|
this.platformVer + ".sdk/Applications/");
|
|
fs.stat(appDir, function (err, s) {
|
|
if (err) {
|
|
cb(err);
|
|
} else if (!s.isDirectory()) {
|
|
cb(new Error("Could not load built in applications directory"));
|
|
} else {
|
|
cb(null, appDir);
|
|
}
|
|
});
|
|
}.bind(this));
|
|
};
|
|
|
|
Simulator.prototype.prepareSafari = function (tmpDir, cb) {
|
|
this.prepareBuiltInApp("MobileSafari", tmpDir, cb);
|
|
};
|
|
|
|
Simulator.prototype.preparePreferencesApp = function (tmpDir, cb) {
|
|
this.prepareBuiltInApp("Preferences", tmpDir, cb);
|
|
};
|
|
|
|
Simulator.prototype.deleteSim = function (cb) {
|
|
|
|
var deleteSimVEight = function (cb) {
|
|
logger.debug('Resetting Content and Settings for Simulator');
|
|
simctl.eraseDevice(this.udid, cb);
|
|
}.bind(this);
|
|
|
|
var deleteSimLessThanVEight = function (cb) {
|
|
var root = this.getRootDir();
|
|
logger.debug("Deleting simulator folder: " + root);
|
|
try {
|
|
safeRimRafSync(root);
|
|
} catch (e) {
|
|
return cb(e);
|
|
}
|
|
cb();
|
|
}.bind(this);
|
|
|
|
|
|
if (parseFloat(this.sdkVer) >= 8) {
|
|
deleteSimVEight(cb);
|
|
} else {
|
|
deleteSimLessThanVEight(cb);
|
|
}
|
|
|
|
};
|
|
|
|
Simulator.prototype.setLocale = function (language, locale, calendar) {
|
|
var globalPrefs = multiResolve(this.getDirs(), "Library", "Preferences",
|
|
".GlobalPreferences.plist");
|
|
_.each(globalPrefs, function (plist) {
|
|
var data = Simulator.getPlistData(plist);
|
|
var curLocaleAndCal = data.AppleLocale || null;
|
|
var supportedLangs = data.AppleLanguages || [];
|
|
var newLangs = [];
|
|
if (language) {
|
|
logger.debug("New language: " + language);
|
|
newLangs.push(language);
|
|
newLangs = newLangs.concat(_.without(supportedLangs, language));
|
|
data.AppleLanguages = newLangs;
|
|
}
|
|
if (locale || calendar) {
|
|
var calSplit = "@calendar=";
|
|
if (curLocaleAndCal === null) {
|
|
curLocaleAndCal = language || 'en';
|
|
}
|
|
var curLoc = curLocaleAndCal.split(calSplit)[0];
|
|
var newLocaleAndCal = locale ? locale : curLoc;
|
|
if (calendar) {
|
|
newLocaleAndCal += calSplit + calendar;
|
|
}
|
|
logger.debug("New locale: " + newLocaleAndCal);
|
|
data.AppleLocale = newLocaleAndCal;
|
|
}
|
|
|
|
logger.debug("Writing new locale plist data");
|
|
fs.writeFileSync(plist, bplistCreate(data));
|
|
});
|
|
};
|
|
|
|
Simulator.getSimAppPrefsFile = function () {
|
|
var u = process.env.USER;
|
|
return path.resolve("/Users", u, "Library", "Preferences",
|
|
"com.apple.iphonesimulator.plist");
|
|
};
|
|
|
|
Simulator.prototype.deleteOtherSims = function (cb) {
|
|
logger.debug("Isolating the requested simulator by deleting all others");
|
|
simctl.getDevices(function (err, devices) {
|
|
if (err) return cb(err);
|
|
var udids = [];
|
|
var deleteCalls = [];
|
|
_.each(devices, function (sdkDevices) {
|
|
udids = udids.concat(_.pluck(sdkDevices, 'udid'));
|
|
});
|
|
_.each(_.without(udids, this.udid), function (udid) {
|
|
deleteCalls.push(_.partial(simctl.deleteDevice, udid));
|
|
});
|
|
async.series(deleteCalls, cb);
|
|
}.bind(this));
|
|
};
|
|
|
|
module.exports = Simulator;
|