Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
626 lines
17 KiB
JavaScript
Executable file
626 lines
17 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
/**
|
|
* This file contains all Node.js version of utils functions.
|
|
* Since we're stil in the progress to migrate to Node.js, the
|
|
* missing functions in this file should be in the 'utils-xpc.js'
|
|
*
|
|
* About documents:
|
|
* Users can find comments in the 'utils-xpc.js',
|
|
* which contains the complete functions used in 'utils.js'
|
|
*/
|
|
|
|
var utils = require('./utils');
|
|
var path = require('path');
|
|
var childProcess = require('child_process');
|
|
var fs = require('fs-extra');
|
|
var JSZip = require('jszip');
|
|
var Q = require('q');
|
|
var os = require('os');
|
|
var vm = require('vm');
|
|
var http = require('http');
|
|
var https = require('https');
|
|
var url = require('url');
|
|
var dive = require('diveSync');
|
|
var nodeUUID = require('node-uuid');
|
|
var jsdom = require('jsdom');
|
|
var esprima = require('esprima');
|
|
var procRunning = require('is-running');
|
|
var mime = require('mime');
|
|
var crypto = require('crypto');
|
|
|
|
// Our gecko will transfer .opus file to audio/ogg datauri type.
|
|
mime.define({'audio/ogg': ['opus']});
|
|
|
|
module.exports = {
|
|
Q: Q,
|
|
|
|
scriptParser: esprima.parse,
|
|
|
|
log: function() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
if (!args.length) {
|
|
console.log('\n');
|
|
return;
|
|
}
|
|
console.log('[' + args[0] + '] ' + args.slice(1).join(' '));
|
|
},
|
|
|
|
normalizePath: function(string) {
|
|
return path.normalize(string);
|
|
},
|
|
|
|
joinPath: function() {
|
|
return path.join.apply(path, arguments);
|
|
},
|
|
|
|
getOsType: function() {
|
|
var type = os.type();
|
|
return type === 'Windows_NT' ? 'WINNT' : type;
|
|
},
|
|
|
|
relativePath: function(from, to) {
|
|
return path.relative(from, to);
|
|
},
|
|
|
|
getFile: function() {
|
|
var self = module.exports;
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var src = path.join.apply(path, args);
|
|
src = path.resolve(src);
|
|
var fileStat;
|
|
try {
|
|
fileStat = fs.statSync(src);
|
|
} catch (e) {
|
|
// In order to have compability with xpcshell interface,
|
|
// we just simply let fileStat undefined and don't need
|
|
// to do anything here.
|
|
}
|
|
return {
|
|
exists: function() {
|
|
return self.fileExists(src);
|
|
},
|
|
remove: function() {
|
|
fs.removeSync(src);
|
|
},
|
|
isDirectory: function() {
|
|
return !!fileStat && fileStat.isDirectory();
|
|
},
|
|
isFile: function() {
|
|
return !!fileStat && fileStat.isFile();
|
|
},
|
|
isHidden: function() {
|
|
return /^\./.test(path.basename(src));
|
|
},
|
|
path: src,
|
|
leafName: path.basename(src)
|
|
};
|
|
},
|
|
|
|
getFileContent: function(file, type) {
|
|
if (file.exists() && file.isFile()) {
|
|
return fs.readFileSync(file.path, { encoding: type || 'utf8' });
|
|
}
|
|
},
|
|
|
|
Commander: function(cmd) {
|
|
this.initPath = function() {};
|
|
this.run = function(args, callback, options) {
|
|
var cmds = args.join(' ');
|
|
|
|
// In *nix and OSX version commands are run via sh -c YOUR_COMMAND,
|
|
// but in Windows commands are run via cmd /C YOUR_COMMAND,
|
|
// so, we just let execSync module to handle the difference.
|
|
if (cmd === 'sh') {
|
|
cmds = cmds.replace(/^-c/, '');
|
|
} else {
|
|
cmds = cmd + ' ' + cmds;
|
|
}
|
|
console.log(cmds);
|
|
// XXX: Most cmds should run synchronously, we should use either promise
|
|
// pattern inside each script or find a sync module which doesn't
|
|
// require recompile again since TPBL doesn't support that.
|
|
return new Promise(function(resolve, reject) {
|
|
childProcess.exec(cmds, { maxBuffer: (4096 * 1024) },
|
|
function(err, stdout, stderr) {
|
|
if (err) {
|
|
options && options.stderr && options.stderr(stderr);
|
|
reject(stderr);
|
|
} else {
|
|
options && options.stdout && options.stdout(stdout);
|
|
callback && callback(stdout);
|
|
resolve(stdout);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
this.runWithSubprocess = function(args, options) {
|
|
this.run(args, null, options);
|
|
};
|
|
},
|
|
|
|
readZipManifest: function(file) {
|
|
var zipPath = this.joinPath(file.path, 'application.zip');
|
|
if (!this.fileExists(zipPath)) {
|
|
return;
|
|
}
|
|
var manifest = new JSZip(fs.readFileSync(zipPath)).file('manifest.webapp');
|
|
if (!manifest) {
|
|
throw new Error('manifest.webapp not found in ' + zipPath);
|
|
}
|
|
return JSON.parse(manifest.asText());
|
|
},
|
|
|
|
killAppByPid: function(appName) {
|
|
childProcess.exec('adb shell b2g-ps', function(err, stdout) {
|
|
if (!err && stdout) {
|
|
var psMap = utils.psParser(stdout);
|
|
if (psMap[appName] && psMap[appName].PID) {
|
|
childProcess.exec('adb shell kill ' + psMap[appName].PID);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
spawnProcess: function(module, appOptions) {
|
|
this.exitCode = null;
|
|
var args = [
|
|
'--harmony',
|
|
'-e', 'require("' + module + '").execute("' + JSON.stringify(appOptions)
|
|
.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '");'
|
|
];
|
|
var proc = childProcess.spawn('node', args);
|
|
proc.on('close', function(code) {
|
|
this.exitCode = code;
|
|
}.bind(this));
|
|
return proc;
|
|
},
|
|
|
|
processIsRunning: function(proc) {
|
|
return procRunning(proc.pid);
|
|
},
|
|
|
|
getProcessExitCode: function() {
|
|
return this.exitCode;
|
|
},
|
|
|
|
resolve: function(relativePath, gaiaDir) {
|
|
var absPath;
|
|
if (relativePath[0] === '/') {
|
|
absPath = relativePath;
|
|
} else {
|
|
absPath = path.join(gaiaDir, relativePath);
|
|
}
|
|
return this.getFile(absPath);
|
|
},
|
|
|
|
getFileAsDataURI: function(file) {
|
|
var data = this.getFileContent(file, 'base64');
|
|
return 'data:' + mime.lookup(file.path) + ';base64,' + data;
|
|
},
|
|
|
|
getJSON: function(file) {
|
|
var content = this.getFileContent(file);
|
|
try {
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
console.log('Invalid JSON file : ' + file.path + '\n');
|
|
if (content) {
|
|
console.log('Content of JSON file:\n' + content + '\n');
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
getDocument: function(content) {
|
|
return jsdom.jsdom(content);
|
|
},
|
|
|
|
getXML: function(file) {
|
|
return jsdom.jsdom(this.getFileContent(file));
|
|
},
|
|
|
|
getEnv: function(name) {
|
|
return process.env[name];
|
|
},
|
|
|
|
setEnv: function(name, value) {
|
|
process.env[name] = value;
|
|
},
|
|
|
|
getEnvPath: function() {
|
|
return process.env.PATH.split(path.delimiter);
|
|
},
|
|
|
|
getTempFolder: function(dirName) {
|
|
var tmpPath = path.join(os.tmpdir(), dirName);
|
|
fs.mkdirpSync(tmpPath);
|
|
return this.getFile(tmpPath);
|
|
},
|
|
|
|
ensureFolderExists: function(file) {
|
|
fs.mkdirpSync(file.path);
|
|
},
|
|
|
|
normalizeString: function(appname) {
|
|
return appname.replace(' ', '-').toLowerCase().replace(/\W/g, '');
|
|
},
|
|
|
|
ls: function(dir, recursive) {
|
|
var files = [];
|
|
if (!dir || !dir.exists()) {
|
|
return [];
|
|
}
|
|
dive(dir.path, { recursive: recursive }, function(err, filePath) {
|
|
if (err) {
|
|
// Skip error
|
|
return;
|
|
}
|
|
files.push(this.getFile(filePath));
|
|
}.bind(this));
|
|
return files;
|
|
},
|
|
|
|
download: function(fileUrl, dest, callback, errorCallback) {
|
|
var protocol = url.parse(fileUrl).protocol;
|
|
var request = (protocol === 'http:') ? http : https;
|
|
var file = fs.createWriteStream(dest);
|
|
request.get(fileUrl, function(response) {
|
|
response.pipe(file);
|
|
file.on('finish', function() {
|
|
file.close(callback);
|
|
});
|
|
}).on('error', function(err) {
|
|
fs.unlinkSync(dest);
|
|
if (errorCallback) {
|
|
errorCallback();
|
|
}
|
|
});
|
|
},
|
|
|
|
downloadJSON: function(fileUrl, callback) {
|
|
var protocol = url.parse(fileUrl).protocol;
|
|
var request = (protocol === 'http:') ? http : https;
|
|
request.get(fileUrl, function(res) {
|
|
var body = '';
|
|
res.on('data', function(chunk) {
|
|
body += chunk;
|
|
});
|
|
res.on('end', function() {
|
|
if (callback) {
|
|
callback(JSON.parse(body));
|
|
}
|
|
});
|
|
}).on('error', function(e) {
|
|
throw new Error('Download JSON with error: ' + e);
|
|
});
|
|
},
|
|
|
|
readJSONFromPath: function(path) {
|
|
return require(path);
|
|
},
|
|
|
|
concatenatedScripts: function(scriptsPaths, targetPath) {
|
|
var concatedScript = scriptsPaths.map(function(path) {
|
|
return this.getFileContent(this.getFile(path));
|
|
}, this).join('\n');
|
|
|
|
var targetFile = this.getFile(targetPath);
|
|
|
|
this.writeContent(targetFile, concatedScript);
|
|
},
|
|
|
|
getDistributionFileContent: function(name, defaultContent, distDir) {
|
|
if (distDir) {
|
|
let distributionFile = this.getFile(distDir, name + '.json');
|
|
if (distributionFile.exists()) {
|
|
return this.getFileContent(distributionFile);
|
|
}
|
|
}
|
|
return JSON.stringify(defaultContent, null, ' ');
|
|
},
|
|
|
|
getUUIDMapping: function(config) {
|
|
var UUIDMapping = this.UUIDMapping;
|
|
if (UUIDMapping) {
|
|
return UUIDMapping;
|
|
}
|
|
this.UUIDMapping = UUIDMapping = {};
|
|
// Try to retrieve it from $GAIA_DISTRIBUTION_DIR/uuid.json if exists.
|
|
try {
|
|
var uuidFile = this.getFile(config.GAIA_DISTRIBUTION_DIR, 'uuid.json');
|
|
if (uuidFile.exists()) {
|
|
console.log('uuid.json in GAIA_DISTRIBUTION_DIR found.');
|
|
UUIDMapping = JSON.parse(this.getFileContent(uuidFile));
|
|
}
|
|
} catch (e) {
|
|
// ignore exception if GAIA_DISTRIBUTION_DIR does not exist.
|
|
}
|
|
return UUIDMapping;
|
|
},
|
|
|
|
getWebapp: function(app, config) {
|
|
let appDir = this.getFile(app);
|
|
if (!appDir.exists()) {
|
|
throw new Error(' -*- build/utils.js: file not found (' + app + ')\n');
|
|
}
|
|
|
|
let manifestFile = appDir.path + '/manifest.webapp';
|
|
let updateFile = appDir.path + '/update.webapp';
|
|
|
|
// Ignore directories without manifest
|
|
if (!this.fileExists(manifestFile) && !this.fileExists(updateFile)) {
|
|
return;
|
|
}
|
|
|
|
let manifest = this.fileExists(manifestFile) ? manifestFile : updateFile;
|
|
let manifestJSON = this.getJSON(this.getFile(manifest));
|
|
|
|
// Use the folder name as the the domain name
|
|
let appDomain = appDir.leafName + '.' + config.GAIA_DOMAIN;
|
|
if (manifestJSON.origin) {
|
|
appDomain = this.getNewURI(manifestJSON.origin).host;
|
|
}
|
|
|
|
let self = this;
|
|
let webapp = {
|
|
appDirPath: appDir.path,
|
|
manifest: manifestJSON,
|
|
manifestFilePath: manifest,
|
|
url: config.GAIA_SCHEME + appDomain,
|
|
domain: appDomain,
|
|
sourceDirectoryFilePath: appDir.path,
|
|
sourceDirectoryName: appDir.leafName,
|
|
sourceAppDirectoryName: self.getFile(appDir.path, '..').leafName
|
|
};
|
|
|
|
// External webapps have a `metadata.json` file
|
|
let metaData = this.getFile(webapp.sourceDirectoryFilePath,
|
|
'metadata.json');
|
|
if (metaData.exists()) {
|
|
webapp.pckManifest = this.readZipManifest(appDir);
|
|
webapp.metaData = this.getJSON(metaData);
|
|
webapp.appStatus = utils.getAppStatus(
|
|
webapp.metaData.type ||
|
|
(webapp.pckManifest && webapp.pckManifest.type) ||
|
|
'web'
|
|
);
|
|
} else {
|
|
webapp.appStatus = utils.getAppStatus(webapp.manifest.type);
|
|
}
|
|
|
|
// Some webapps control their own build
|
|
webapp.buildDirectoryFilePath = this.joinPath(config.STAGE_DIR,
|
|
webapp.sourceDirectoryName);
|
|
webapp.buildManifestFilePath = this.joinPath(webapp.buildDirectoryFilePath,
|
|
'manifest.webapp');
|
|
|
|
// Generate the webapp folder name in the profile. Only if it's privileged
|
|
// and it has an origin in its manifest file it'll be able to specify a
|
|
// custom folder name. Otherwise, generate an UUID to use as folder name.
|
|
var webappTargetDirName;
|
|
if (this.isExternalApp(webapp)) {
|
|
var type = webapp.appStatus;
|
|
var isPackaged = false;
|
|
if (webapp.pckManifest) {
|
|
isPackaged = true;
|
|
if (webapp.metaData.origin) {
|
|
throw new Error('External webapp `' + webapp.sourceDirectoryName +
|
|
'` can not have origin in metadata because is packaged');
|
|
}
|
|
}
|
|
if (type === 2 && isPackaged && webapp.pckManifest.origin) {
|
|
webappTargetDirName = this.getNewURI(webapp.pckManifest.origin).host;
|
|
} else {
|
|
// uuid is used for webapp directory name, save it for further usage
|
|
let mapping = this.getUUIDMapping(config);
|
|
var uuid = mapping[webapp.sourceDirectoryName] ||
|
|
'{' + nodeUUID.v4() + '}';
|
|
mapping[webapp.sourceDirectoryName] = webappTargetDirName = uuid;
|
|
}
|
|
} else {
|
|
webappTargetDirName = webapp.domain;
|
|
}
|
|
webapp.profileDirectoryFilePath = this.joinPath(config.COREWEBAPPS_DIR,
|
|
'webapps',
|
|
webappTargetDirName);
|
|
|
|
return webapp;
|
|
},
|
|
|
|
getNewURI: function(uri) {
|
|
return {
|
|
host: /[a-z]+:\/\/[@]?([^\/:]+)/.exec(uri)[1],
|
|
prePath: /[a-z]+:\/\/[^\/]+/.exec(uri)[0],
|
|
resolve: function(to) {
|
|
return url.resolve(uri, to);
|
|
}
|
|
};
|
|
},
|
|
|
|
isExternalApp: function(webapp) {
|
|
if (webapp.metaData && webapp.metaData.external === undefined) {
|
|
throw new Error('"external" property in metadata.json is required ' +
|
|
'since Firefox OS 2.1, please add it into metadata.json and update ' +
|
|
'preload.py if you use this script to perload your apps. If you ' +
|
|
'created metadata.json for non-external apps, please set "external" ' +
|
|
'to false. metadata.json is in ' + webapp.sourceDirectoryFilePath);
|
|
}
|
|
if (!webapp.metaData || webapp.metaData.external === false) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
|
|
processEvents: function(exitResultFunc) {
|
|
// FIXME: A fake blocking function for porting utils-xpc
|
|
// This workaround will be removed once node.js migration is done
|
|
var exitResult = exitResultFunc();
|
|
if (exitResult.error) {
|
|
throw exitResult.error;
|
|
}
|
|
},
|
|
|
|
writeContent :function(file, content) {
|
|
fs.writeFileSync(file.path, content);
|
|
},
|
|
|
|
createZip: function() {
|
|
return new JSZip();
|
|
},
|
|
|
|
getCompression: function(type) {
|
|
switch(type) {
|
|
case 'none':
|
|
return 'STORE';
|
|
case 'best':
|
|
return 'DEFLATE';
|
|
}
|
|
},
|
|
|
|
addFileToZip: function(zip, pathInZip, file, compression) {
|
|
if (!file.exists()) {
|
|
return;
|
|
}
|
|
|
|
zip.file(pathInZip, fs.readFileSync(file.path), {
|
|
compression: compression || 'DEFLATE'
|
|
});
|
|
},
|
|
|
|
hasFileInZip: function(zip, pathInZip) {
|
|
return zip.file(pathInZip);
|
|
},
|
|
|
|
closeZip: function(zip, zipPath) {
|
|
fs.writeFileSync(zipPath, zip.generate({
|
|
type: 'nodebuffer',
|
|
platform: process.platform
|
|
}));
|
|
},
|
|
|
|
getLocaleBasedir: function(src) {
|
|
return os.type() === 'Windows_NT' ? src.replace('/', '\\', 'g') : src;
|
|
},
|
|
|
|
deleteFile: function(filePath) {
|
|
fs.removeSync(filePath);
|
|
},
|
|
|
|
fileExists: function(path) {
|
|
try {
|
|
fs.accessSync(path);
|
|
return true;
|
|
} catch(err) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
listFiles: function(path, type, recursive, exclude) {
|
|
var file = (typeof path === 'string' ? this.getFile(path) : path);
|
|
if (!file.isDirectory()) {
|
|
throw new Error('The path is not a directory.');
|
|
}
|
|
var files = this.ls(file, recursive === true, exclude);
|
|
var detectFunc = (type === 0 ? 'isFile' : 'isDirectory');
|
|
// To return simple JavaScript type, We need to put the file path to the
|
|
// array instead of nsIFile.
|
|
var results = [];
|
|
files.forEach(function(file) {
|
|
if (file[detectFunc]()) {
|
|
results.push(file.path);
|
|
}
|
|
});
|
|
|
|
return results;
|
|
},
|
|
|
|
mkdirs: function(path) {
|
|
fs.mkdirpSync(path);
|
|
},
|
|
|
|
dirname: function(filePath) {
|
|
return path.dirname(filePath);
|
|
},
|
|
|
|
basename: function(filePath) {
|
|
return path.basename(filePath);
|
|
},
|
|
|
|
existsInAppDirs: function(appDirs, appName) {
|
|
var apps = appDirs.split(' ');
|
|
var exists = apps.some(function (appPath) {
|
|
let appFile = this.getFile(appPath);
|
|
return (appName === appFile.leafName);
|
|
}, this);
|
|
return exists;
|
|
},
|
|
|
|
copyFileTo: function(filePath, toParent, name) {
|
|
var file = ((typeof filePath === 'string') ?
|
|
this.getFile(filePath) : filePath);
|
|
fs.copySync(file.path, path.join(toParent, name));
|
|
},
|
|
|
|
copyDirTo: function(filePath, toParent, name) {
|
|
var file = ((typeof filePath === 'string') ?
|
|
this.getFile(filePath) : filePath);
|
|
fs.copySync(file.path, path.join(toParent, name));
|
|
},
|
|
|
|
copyToStage: function(options) {
|
|
var appDir = this.getFile(options.APP_DIR);
|
|
this.copyDirTo(appDir, options.STAGE_DIR, appDir.leafName);
|
|
},
|
|
|
|
exit: function(code) {
|
|
process.exit(code);
|
|
},
|
|
|
|
scriptLoader: {
|
|
|
|
scripts: {},
|
|
|
|
load: function(filePath, exportObj, withoutCache) {
|
|
if (!withoutCache && this.scripts[filePath]) {
|
|
return;
|
|
}
|
|
|
|
var doc = jsdom.jsdom();
|
|
var win = doc.defaultView;
|
|
|
|
exportObj.Promise = Q;
|
|
|
|
global.addEventListener = win.addEventListener;
|
|
global.dispatchEvent = win.dispatchEvent;
|
|
|
|
for (let key in exportObj) {
|
|
global[key] = exportObj[key];
|
|
}
|
|
|
|
try {
|
|
var script = fs.readFileSync(filePath, { encoding: 'utf8' });
|
|
vm.runInThisContext(script, filePath);
|
|
this.scripts[filePath] = true;
|
|
} catch(error) {
|
|
delete this.scripts[filePath];
|
|
throw error;
|
|
}
|
|
}
|
|
},
|
|
|
|
createSandbox: function() {
|
|
return {};
|
|
},
|
|
|
|
runScriptInSandbox: function(filePath, sandbox) {
|
|
var script = fs.readFileSync(filePath, { encoding: 'utf8' });
|
|
return vm.runInNewContext(script, sandbox);
|
|
},
|
|
|
|
getHash: function(string) {
|
|
return crypto.createHash('sha1').update(string).digest('hex');
|
|
}
|
|
};
|