adobe/brackets

View on GitHub
src/extensions/default/AutoUpdate/node/AutoUpdateDomain.js

Summary

Maintainability
D
2 days
Test Coverage
/*
 * 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;

}());