src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js
/*
* Copyright (c) 2018 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*global exports */
/*global process*/
(function () {
"use strict";
var _domainManager;
var request = require('request'),
progress = require('request-progress'),
path = require('path'),
fs = require('fs-extra'),
crypto = require('crypto');
// Current Date and Time needed for log filenames
var curDate = Date.now().toString();
//AUTOUPDATE_PRERELEASE
//Installer log file
var logFile = curDate + 'update.logs',
logFilePath;
//Install status file
var installStatusFile = curDate + 'installStatus.logs',
installStatusFilePath;
var updateDir,
_updateParams;
var MessageIds,
installerPath;
// function map for node functions
var functionMap = {};
var nodeErrorMessages = {
UPDATEDIR_READ_FAILED: 0,
UPDATEDIR_CLEAN_FAILED: 1,
CHECKSUM_DID_NOT_MATCH: 2,
INSTALLER_NOT_FOUND: 3,
DOWNLOAD_ERROR: 4,
NETWORK_SLOW_OR_DISCONNECTED: 5
};
var requesters = {},
isNodeDomainInitialized = false;
/**
* Gets the arguments to a function in an array
* @param {object} args - the arguments object
* @returns {Array} - array of actual arguments
*/
function getFunctionArgs(args) {
if (args.length > 2) {
var fnArgs = new Array(args.length - 2),
i;
for (i = 2; i < args.length; ++i) {
fnArgs[i - 2] = args[i];
}
return fnArgs;
}
return [];
}
/**
* Posts messages to brackets
* @param {string} messageId - Message to be passed
*/
function postMessageToBrackets(messageId, requester) {
if(!requesters[requester]) {
for (var key in requesters) {
requester = key;
break;
}
}
var msgObj = {
fn: messageId,
args: getFunctionArgs(arguments),
requester: requester.toString()
};
_domainManager.emitEvent('AutoUpdate', 'data', [msgObj]);
}
/**
* Quotes and Converts a file path, to accommodate platform dependent paths
* @param {string} qncPath - file path
* @param {boolean} resolve - false if path is only to be quoted, true if both quoted and converted
* @returns {string} quoted and converted file path
*/
function quoteAndConvert(qncPath, resolve) {
if (resolve) {
qncPath = path.resolve(qncPath);
}
return "\"" + qncPath + "\"";
}
/**
* Validates the checksum of a file against a given checksum
* @param {object} params - json containing {
* filePath - path to the file,
* expectedChecksum - the checksum to validate against }
*/
function validateChecksum(requester, params) {
params = params || {
filePath: installerPath,
expectedChecksum: _updateParams.checksum
};
var hash = crypto.createHash('sha256'),
currentRequester = requester || "";
if (fs.existsSync(params.filePath)) {
var stream = fs.createReadStream(params.filePath);
stream.on('data', function (data) {
hash.update(data);
});
stream.on('end', function () {
var calculatedChecksum = hash.digest('hex'),
isValidChecksum = (params.expectedChecksum === calculatedChecksum),
status;
if (isValidChecksum) {
if (process.platform === "darwin") {
status = {
valid: true,
installerPath: installerPath,
logFilePath: logFilePath,
installStatusFilePath: installStatusFilePath
};
} else if (process.platform === "win32") {
status = {
valid: true,
installerPath: quoteAndConvert(installerPath, true),
logFilePath: quoteAndConvert(logFilePath, true),
installStatusFilePath: installStatusFilePath
};
}
} else {
status = {
valid: false,
err: nodeErrorMessages.CHECKSUM_DID_NOT_MATCH
};
}
postMessageToBrackets(MessageIds.NOTIFY_VALIDATION_STATUS, currentRequester, status);
});
} else {
var status = {
valid: false,
err: nodeErrorMessages.INSTALLER_NOT_FOUND
};
postMessageToBrackets(MessageIds.NOTIFY_VALIDATION_STATUS, currentRequester, status);
}
}
/**
* Parse the Installer log and search for a error strings
* one it finds the line which has any of error String
* it return that line and exit
*/
function parseInstallerLog(filepath, searchstring, encoding, callback) {
var line = "";
var searchFn = function searchFn(str) {
var arr = str.split('\n'),
lineNum,
pos;
for (lineNum = arr.length - 1; lineNum >= 0; lineNum--) {
var searchStrNum;
for (searchStrNum = 0; searchStrNum < searchstring.length; searchStrNum++) {
pos = arr[lineNum].search(searchstring[searchStrNum]);
if (pos !== -1) {
line = arr[lineNum];
break;
}
}
if (pos !== -1) {
break;
}
}
callback(line);
};
fs.readFile(filepath, {"encoding": encoding})
.then(function (str) {
return searchFn(str);
}).catch(function () {
callback("");
});
}
/**
* one it finds the line which has any of error String
* after parsing the Log
* it notifies the bracket.
* @param{Object} searchParams is object contains Information Error String
* Encoding of Log File Update Diectory Path.
*/
function checkInstallerStatus(requester, searchParams) {
var installErrorStr = searchParams.installErrorStr,
bracketsErrorStr = searchParams.bracketsErrorStr,
updateDirectory = searchParams.updateDir,
encoding = searchParams.encoding || "utf8",
statusObj = {installError: ": BA_UN"},
logFileAvailable = false,
currentRequester = requester || "";
var notifyBrackets = function notifyBrackets(errorline) {
statusObj.installError = errorline || ": BA_UN";
postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj);
};
var parseLog = function (files) {
files.forEach(function (file) {
var fileExt = path.extname(path.basename(file));
if (fileExt === ".logs") {
var fileName = path.basename(file),
fileFullPath = updateDirectory + '/' + file;
if (fileName.search("installStatus.logs") !== -1) {
logFileAvailable = true;
parseInstallerLog(fileFullPath, bracketsErrorStr, "utf8", notifyBrackets);
} else if (fileName.search("update.logs") !== -1) {
logFileAvailable = true;
parseInstallerLog(fileFullPath, installErrorStr, encoding, notifyBrackets);
}
}
});
if (!logFileAvailable) {
postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj);
}
};
fs.readdir(updateDirectory)
.then(function (files) {
return parseLog(files);
}).catch(function () {
postMessageToBrackets(MessageIds.NOTIFY_INSTALLATION_STATUS, currentRequester, statusObj);
});
}
/**
* Downloads the installer for latest Brackets release
* @param {boolean} sendInfo - true if download status info needs to be
* sent back to Brackets, false otherwise
* @param {object} [updateParams=_updateParams] - json containing update parameters
*/
function downloadInstaller(requester, isInitialAttempt, updateParams) {
updateParams = updateParams || _updateParams;
var currentRequester = requester || "";
try {
var ext = path.extname(updateParams.installerName);
var localInstallerPath = path.resolve(updateDir, Date.now().toString() + ext),
localInstallerFile = fs.createWriteStream(localInstallerPath),
requestCompleted = true,
readTimeOut = 180000;
progress(request(updateParams.downloadURL, {timeout: readTimeOut}), {})
.on('progress', function (state) {
var target = "retry-download";
if (isInitialAttempt) {
target = "initial-download";
}
var info = Math.floor(parseFloat(state.percent) * 100).toString() + '%';
var status = {
target: target,
spans: [{
id: "percent",
val: info
}]
};
postMessageToBrackets(MessageIds.SHOW_STATUS_INFO, currentRequester, status);
})
.on('error', function (err) {
console.log("AutoUpdate : Download failed. Error occurred : " + err.toString());
requestCompleted = false;
localInstallerFile.end();
var error = err.code === 'ESOCKETTIMEDOUT' || err.code === 'ENOTFOUND' ?
nodeErrorMessages.NETWORK_SLOW_OR_DISCONNECTED :
nodeErrorMessages.DOWNLOAD_ERROR;
postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE, currentRequester, error);
})
.pipe(localInstallerFile)
.on('close', function () {
if (requestCompleted) {
try {
fs.renameSync(localInstallerPath, installerPath);
postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_SUCCESS, currentRequester);
} catch (e) {
console.log("AutoUpdate : Download failed. Exception occurred : " + e.toString());
postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE,
currentRequester, nodeErrorMessages.DOWNLOAD_ERROR);
}
}
});
} catch (e) {
console.log("AutoUpdate : Download failed. Exception occurred : " + e.toString());
postMessageToBrackets(MessageIds.NOTIFY_DOWNLOAD_FAILURE,
currentRequester, nodeErrorMessages.DOWNLOAD_ERROR);
}
}
/**
* Performs clean up for the contents in Update Directory in AppData
* @param {Array} filesToCache - array of file types to cache
* @param {boolean} notifyBack - true if Brackets needs to be
* notified post cleanup, false otherwise
*/
function performCleanup(requester, filesToCache, notifyBack) {
var currentRequester = requester || "";
function filterFilesAndNotify(files, filesToCacheArr, notifyBackToBrackets) {
files.forEach(function (file) {
var fileExt = path.extname(path.basename(file));
if (filesToCacheArr.indexOf(fileExt) < 0) {
var fileFullPath = updateDir + '/' + file;
try {
fs.removeSync(fileFullPath);
} catch (e) {
console.log("AutoUpdate : Exception occured in removing ", fileFullPath, e);
}
}
});
if (notifyBackToBrackets) {
postMessageToBrackets(MessageIds.NOTIFY_SAFE_TO_DOWNLOAD, currentRequester);
}
}
fs.stat(updateDir)
.then(function (stats) {
if (stats) {
if (filesToCache) {
fs.readdir(updateDir)
.then(function (files) {
filterFilesAndNotify(files, filesToCache, notifyBack);
})
.catch(function (err) {
console.log("AutoUpdate : Error in Reading Update Dir for Cleanup : " + err.toString());
postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE,
currentRequester, nodeErrorMessages.UPDATEDIR_READ_FAILED);
});
} else {
fs.remove(updateDir)
.then(function () {
console.log('AutoUpdate : Update Directory in AppData Cleaned: Complete');
})
.catch(function (err) {
console.log("AutoUpdate : Error in Cleaning Update Dir : " + err.toString());
postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE,
currentRequester, nodeErrorMessages.UPDATEDIR_CLEAN_FAILED);
});
}
}
})
.catch(function (err) {
console.log("AutoUpdate : Error in Reading Update Dir stats for Cleanup : " + err.toString());
postMessageToBrackets(MessageIds.SHOW_ERROR_MESSAGE,
currentRequester, nodeErrorMessages.UPDATEDIR_CLEAN_FAILED);
});
}
/**
* Initializes the node with update parameters
* @param {object} updateParams - json containing update parameters
*/
function initializeState(requester, updateParams) {
var currentRequester = requester || "";
_updateParams = updateParams;
installerPath = path.resolve(updateDir, updateParams.installerName);
postMessageToBrackets(MessageIds.NOTIFY_INITIALIZATION_COMPLETE, currentRequester);
}
function removeFromRequesters(requester) {
if (requesters.hasOwnProperty(requester.toString())) {
delete requesters[requester];
}
}
/**
* Generates a map for node side functions
*/
function registerNodeFunctions() {
functionMap["node.downloadInstaller"] = downloadInstaller;
functionMap["node.performCleanup"] = performCleanup;
functionMap["node.validateInstaller"] = validateChecksum;
functionMap["node.initializeState"] = initializeState;
functionMap["node.checkInstallerStatus"] = checkInstallerStatus;
functionMap["node.removeFromRequesters"] = removeFromRequesters;
}
/**
* Initializes node for the auto update, registers messages and node side funtions
* @param {object} initObj - json containing init information {
* messageIds : Messages for brackets and node communication
* updateDir : update directory in Appdata
* requester : ID of the current requester domain}
*/
function initNode(initObj) {
var resetUpdateProgres = false;
if (!isNodeDomainInitialized) {
MessageIds = initObj.messageIds;
updateDir = path.resolve(initObj.updateDir);
logFilePath = path.resolve(updateDir, logFile);
installStatusFilePath = path.resolve(updateDir, installStatusFile);
registerNodeFunctions();
isNodeDomainInitialized = true;
resetUpdateProgres = true;
}
postMessageToBrackets(MessageIds.NODE_DOMAIN_INITIALIZED, initObj.requester.toString(), resetUpdateProgres);
requesters[initObj.requester.toString()] = true;
postMessageToBrackets(MessageIds.REGISTER_BRACKETS_FUNCTIONS, initObj.requester.toString());
}
/**
* Receives messages from brackets
* @param {object} msgObj - json containing - {
* fn - function to execute on node side
* args - arguments to the above function }
*/
function receiveMessageFromBrackets(msgObj) {
var argList = msgObj.args;
argList.unshift(msgObj.requester || "");
functionMap[msgObj.fn].apply(null, argList);
}
/**
* Initialize the domain with commands and events related to AutoUpdate
* @param {DomainManager} domainManager - The DomainManager for AutoUpdateDomain
*/
function init(domainManager) {
if (!domainManager.hasDomain("AutoUpdate")) {
domainManager.registerDomain("AutoUpdate", {
major: 0,
minor: 1
});
}
_domainManager = domainManager;
domainManager.registerCommand(
"AutoUpdate",
"initNode",
initNode,
true,
"Initializes node for the auto update",
[
{
name: "initObj",
type: "object",
description: "json object containing init information"
}
],
[]
);
domainManager.registerCommand(
"AutoUpdate",
"data",
receiveMessageFromBrackets,
true,
"Receives messages from brackets",
[
{
name: "msgObj",
type: "object",
description: "json object containing message info"
}
],
[]
);
domainManager.registerEvent(
"AutoUpdate",
"data",
[
{
name: "msgObj",
type: "object",
description: "json object containing message info to pass to brackets"
}
]
);
}
exports.init = init;
}());