nw-skeleton

Source: app-wrapper/js/lib/main/mainScript.js

/**
 * @fileOverview MainScript class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const os = require('os');
const path = require('path');
const fs = require('fs');
const EventEmitter = require('events');
const MainBaseClass = require('./mainBase').MainBaseClass;

const MenuHelper = require('./menuHelper').MenuHelper;
const MainMessageHandlers = require('./mainMessageHandlers').MainMessageHandlers;
const MainAsyncMessageHandlers = require('./mainAsyncMessageHandlers').MainAsyncMessageHandlers;

/**
 * A Utility class for handling main script (nwjs bg-script) tasks
 *
 * @class
 * @memberOf mainScript
 * @extends {mainScript.MainBaseClass}
 * @property {Object}                   config                  App configuration
 * @property {Date}                     startTime               App starting time
 * @property {Object}                   inspectOptions          Util.inspect default options
 * @property {Window}                   mainWindow              Reference to main nw.Window
 * @property {Object}                   manifest                Manifest file data
 * @property {MainMessageHandlers}      messageHandlers         Object that handles messages
 * @property {MainAsyncMessageHandlers} asyncMessageHandlers    Object that handles async messages
 * @property {Object}                   boundMethods            Wrapper object for bound method references
 */
class MainScript extends MainBaseClass {

    /**
     * Creates MainScript instance
     *
     * @constructor
     * @return {MainScript}              Instance of MainScript class
     */
    constructor() {
        super();

        this.config = null;
        this.startTime = null;

        this.inspectOptions = {
            showHidden: true,
            showProxy: true,
            colors: true
        };
        this.mainWindow = null;
        this.manifest = null;
        this.messageHandlers = null;
        this.asyncMessageHandlers = null;
        this.menuHelper = null;
        this.debugToFileStarted = false;

        let boundMethods = _.cloneDeep(this.boundMethods);

        this.boundMethods = _.extend({
            windowClose: null,
            windowClosed: null,
            windowLoaded: null,
            onMessage: null,
            uncaughtException: null,
            sigInt: null,
            sigTerm: null,
        }, boundMethods);

        this.mainState = {
            menuInitialized: false,
            trayInitialized: false
        };

        return this;

    }

    /**
     * Initializes MainScript using manifest and app config data
     *
     * @async
     * @param  {Object}     options  Object with 'manifest' property containing manifest file data and 'config' property containing config data
     * @return {MainScript}          Instance of MainScript class
     */
    async initialize (options){
        await super.initialize(options);

        this.startTime = new Date();

        if (this.getConfig('main.debug.debugToFile')){
            let debugMessageFilePath = this.getDebugMessageFilePath();
            if (!await this.isFile(debugMessageFilePath) || !this.getConfig('main.debug.debugToFileAppend')) {
                this.createDirFileRecursive(debugMessageFilePath);
            } else if (this.getConfig('main.debug.debugToFileAppend')) {
                await this.initializeDebugMessageLog();
            }
        }

        this.messageHandlers = new MainMessageHandlers();
        await this.messageHandlers.initialize(options);
        this.asyncMessageHandlers = new MainAsyncMessageHandlers();
        await this.asyncMessageHandlers.initialize(options);
        this.menuHelper = new MenuHelper();
        await this.menuHelper.initialize();
        return this;
    }

    /**
     * Destroys current MainScript class instance
     *
     * @async
     * @return {undefined}
     */
    async destroy () {
        this.removeMainWindowEventListeners();
        this.removeGlobalEmitterEventListeners();
        await super.destroy();
    }

    /**
     * Starts the application
     *
     * @async
     * @return {undefined}
     */
    async start () {
        var returnPromise;
        var resolveReference;
        returnPromise = new Promise((resolve) => {
            resolveReference = resolve;
        });
        nw.Window.open(this.manifest.mainTemplate, this.manifest.window, (win)=>{
            this.mainWindow = win;
            this.addMainWindowEventListeners();
            this.initializeGlobalEmitter();
            this.mainWindow.window.sessionStorage.setItem('appStartTime', this.startTime);
            resolveReference(true);
        });
        return returnPromise;
    }

    /**
     * Initializes globalEmitter object for communication with the app
     *
     * @async
     * @return {undefined}
     */
    async initializeGlobalEmitter () {
        this.mainWindow.globalEmitter = new EventEmitter();
        this.addGlobalEmitterEventListeners();
    }

    /**
     * Adds event listeners
     *
     * @return {undefined}
     */
    addEventListeners() {
        process.on('uncaughtException', this.boundMethods.uncaughtException);
        process.on('SIGINT', this.boundMethods.sigInt);
        process.on('SIGTERM', this.boundMethods.sigTerm);
    }

    /**
     * Removes event listeners
     *
     * @return {undefined}
     */
    removeEventListeners() {
        process.removeListener('uncaughtException', this.boundMethods.uncaughtException);
        process.removeListener('SIGINT', this.boundMethods.sigInt);
        process.removeListener('SIGTERM', this.boundMethods.sigTerm);
    }

    /**
     * Adds main window event listeners
     *
     * @return {undefined}
     */
    addMainWindowEventListeners() {
        this.mainWindow.on('close', this.boundMethods.windowClose);
        this.mainWindow.on('closed', this.boundMethods.windowClosed);
        this.mainWindow.on('loaded', this.boundMethods.windowLoaded);
    }

    /**
     * Removes main window event listeners
     *
     * @return {undefined}
     */
    removeMainWindowEventListeners() {
        this.mainWindow.removeListener('close', this.boundMethods.windowClose);
        this.mainWindow.removeListener('closed', this.boundMethods.windowClosed);
        this.mainWindow.removeListener('loaded', this.boundMethods.windowLoaded);
    }

    /**
     * Adds global emitter event listeners
     *
     * @return {undefined}
     */
    addGlobalEmitterEventListeners() {
        this.mainWindow.globalEmitter.on('message', this.boundMethods.onMessage);
        this.mainWindow.globalEmitter.on('asyncMessage', this.boundMethods.onMessage);
    }

    /**
     * Removes global emitter window event listeners
     *
     * @return {undefined}
     */
    removeGlobalEmitterEventListeners() {
        this.mainWindow.globalEmitter.removeListener('message', this.boundMethods.onMessage);
        this.mainWindow.globalEmitter.removeListener('asyncMessage', this.boundMethods.onMessage);
    }

    /**
     * Handles messages received from the app
     *
     * @param  {Object} data    Data passed with message
     * @return {mixed}          Result of message execution
     */
    onMessage (data) {
        if (data){
            let responseMessage = 'messageResponse';
            let messageType = 'Message';
            if (data._async_){
                responseMessage = 'asyncMessageResponse';
                messageType = 'Async message';
            }
            if (data.instruction && data.uuid){
                let instruction = data.instruction;
                let uuid = data.uuid;
                let result;
                if (data._async_){
                    result = this.asyncMessageHandlers.execute(instruction, uuid, data);
                } else {
                    result = this.messageHandlers.execute(instruction, uuid, data);
                }
                if (!result){
                    this.log('{1} "{2}" handler for instruction "{3}" not found!', 'error', [messageType, uuid, instruction]);
                    this.mainWindow.globalEmitter.emit(responseMessage, _.extend(data, {_result_: false}));
                } else {
                    this.log('{1} "{2}" with instruction "{3}" received', 'debug', [messageType, uuid, instruction]);
                    this.log('{1} "{2}" with instruction "{3}" data: {4}', 'debug', [messageType, uuid, instruction, data]);
                }
                return result;
            } else {
                if (!data.instruction){
                    this.log('{1} "{2}" received without instruction!', 'error', [messageType, data.uuid]);
                } else if (!data.uuid) {
                    this.log('{1} with instruction "{2}" received without uuid!', 'error', [messageType, data.instruction]);
                }
                let responseData = {
                    _result_: false
                };
                if (data && _.isObject(data)){
                    responseData = _.extend(data, responseData);
                }
                this.mainWindow.globalEmitter.emit(responseMessage, responseData);
                return false;
            }
        } else {
            this.log('Message received without data!', 'error');
        }
    }

    /**
     * Handler for mainWindow 'closed' event
     *
     * @param  {Event} e        Event that triggered the method
     * @param  {Boolean} noExit Flag to prevent exiting main process
     * @return {undefined}
     */
    async windowClosed (e, noExit) {
        _.noop(e);
        this.log('Main window closed, exiting', 'info');
        await this.finalizeDebugMessageLog();
        if (!noExit){
            process.exit(0);
        }
    }

    /**
     * Handler for mainWindow 'close' event
     *
     * @param  {Event} e        Event that triggered the method
     * @return {undefined}
     */
    async windowClose (e) {
        _.noop(e);
        this.log('Main window closing', 'info');
    }


    /**
     * Handler for main window 'loaded' event
     *
     * @return {undefined}
     */
    windowLoaded () {
        this.log('Main window loaded', 'debug');
    }

    /**
     * Handler for uncaught exceptions
     *
     * @param  {Error} err  Uncaught exception
     * @return {undefined}
     */
    uncaughtException (err) {
        let message = 'EXCEPTION: {1}';
        let data = [];
        if (err.message){
            data[0] = err.message;
        }
        if (err.stack){
            data[0] = err.stack;
        }
        if (!data[0]){
            data[0] = err;
        }
        this.log(message, 'error', data, true, true);
        this.mainWindow.window.appState.appError.error = true;
        if (err && err.message){
            this.mainWindow.globalEmitter.emit('mainMessage', {instruction: 'callMethod', data: {method: 'addUserMessage', arguments: [err.message, 'error', [], false, true]}});
        } else {
            this.mainWindow.globalEmitter.emit('mainMessage', {instruction: 'callMethod', data: {method: 'addUserMessage', arguments: ['Main script error occured', 'error', [], false, true]}});
        }

        this.mainWindow.window.appWrapper.exitApp();

        let timeoutDuration = 30000;
        if (this.config && this.config.cancelOperationTimeout){
            timeoutDuration = this.config.cancelOperationTimeout;
        }
        setTimeout(() => {
            this.mainWindow.window.appWrapper.exitApp(true);
        }, timeoutDuration);
    }

    /**
     * Handler for SIGINT signal
     *
     * @param  {Integer} code Optional exit code for the app
     * @return {undefined}
     */
    sigInt (code) {
        if (_.isUndefined(code)){
            code = 4;
        }
        this.log('Caught SIGINT, code: {1}, shutting down.', 'warning', [code], true);
    }

    /**
     * Handler for SIGTERM signal
     *
     * @param  {Integer} code Optional exit code for the app
     * @return {undefined}
     */
    sigTerm (code) {
        if (_.isUndefined(code)){
            code = 0;
        }
        this.log('Caught SIGTERM, code: {1}, shutting down.', 'warning', [code], true);
        if (this.mainWindow && this.mainWindow.window && this.mainWindow.window.appWrapper){
            this.mainWindow.window.appWrapper.exitApp();
        } else {
            process.exit(code);
        }
    }

    /**
     * Sets new config to data from argument
     *
     * @async
     * @param {Object} configData Object with config data
     * @return {undefined}
     */
    async setNewConfig (configData){
        this.log('Setting new config', 'debug');
        this.config = configData;
    }

    /**
     * Initializes debug message log file if debug message logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log initialization
     */
    async initializeDebugMessageLog(){
        if (this.getConfig('main.debug.debugToFile')){
            let debugMessageFilePath = this.getDebugMessageFilePath();
            let debugLogFile = path.resolve(debugMessageFilePath);
            let debugLogContents = fs.readFileSync(debugLogFile) + '';
            if (debugLogContents){
                debugLogContents = debugLogContents.replace(/\n?\[\n/g, '');
                debugLogContents = debugLogContents.replace(/\n\],?\n/g, ',\n');
                debugLogContents = debugLogContents.replace(/,+/g, ',');
                fs.writeFileSync(debugLogFile, debugLogContents, {flag: 'w'});
            }
        }
        return true;
    }

    /**
     * Finalizes debug log file if debug logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log finalizing
     */
    async finalizeDebugMessageLog(){
        if (this.getConfig('main.debug.debugToFile')){
            let debugMessageFilePath = this.getDebugMessageFilePath();
            let debugLogFile = path.resolve(debugMessageFilePath);
            let debugLogContents = '[\n' + fs.readFileSync(debugLogFile) + '\n]\n';
            debugLogContents = debugLogContents.replace(/\n,\n/g, '\n');
            fs.writeFileSync(debugLogFile, debugLogContents, {flag: 'w'});
        }
        return true;
    }

    /**
     * Returns platform data for current platform
     *
     * @return {Object} Platform data
     */
    getPlatformData (){
        let name = os.platform();
        let platform = {
            isLinux: false,
            isMac: false,
            isWindows: false,
            isWindows8: false,
            version: os.release()
        };

        if(name === 'darwin'){
            platform.name = 'mac';
            platform.isMac = true;
        } else if(name === 'linux'){
            platform.name = 'linux';
            platform.isLinux = true;
        } else {
            platform.name = 'windows';
            platform.isWindows = true;
        }

        platform.is64Bit = os.arch() === 'x64' || process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');

        return {
            platform: platform,
            versions: process.versions
        };
    }

    /**
     * Checks whether current platform is mac
     *
     * @return {Boolean} True if mac, false otherwise
     */
    isMac (){
        return this.getPlatformData().platform.isMac;
    }

    /**
     * Checks whether current platform is windows
     *
     * @return {Boolean} True if windows, false otherwise
     */
    isWindows (){
        return this.getPlatformData().platform.isWindows;
    }

    /**
     * Checks whether current platform is linux
     *
     * @return {Boolean} True if linux, false otherwise
     */
    isLinux (){
        return this.getPlatformData().platform.isLinux;
    }

    /**
     * Returns base exec path (root dir of the app)
     *
     * @return {string} Root app dir
     */
    getExecPath () {
        let execPath = nw.__dirname;
        if (this.isMac()){
            if (execPath.match(/app\.nw$/)){
                execPath = path.join(execPath, '../../../..');
            }
        }
        return execPath;
    }

    /**
     * Loads config overrides if present, and returns config object for the app
     *
     * @param  {Object} defaultAppConfig Default application config
     * @return {Object}                  Application config object
     */
    getInitialAppConfig(defaultAppConfig){
        let initialAppConfig = defaultAppConfig;
        let execPath = this.getExecPath();

        let configFilePath = path.resolve(path.join(execPath, 'config', 'config.js'));
        let configFileExists = fs.existsSync(configFilePath);

        if (!configFileExists){
            configFilePath = path.resolve(path.join(execPath, 'config.js'));
            configFileExists = fs.existsSync(configFilePath);
        }

        if (configFileExists){
            let initialConfigData;
            try {
                initialConfigData = require(configFilePath);
                initialAppConfig = initialConfigData.config;
            } catch (ex) {
                console.error(ex);
            }
        }
        return initialAppConfig;
    }
}

exports.MainScript = MainScript;