dsi-icl/optimise

View on GitHub
packages/optimise-electron/index.js

Summary

Maintainability
D
2 days
Test Coverage
const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');
const express = require('express');
const { app, session, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const log = require('electron-log');
const { autoUpdater } = require('electron-updater');
const unhandled = require('electron-unhandled');
const optimiseCore = require('./dist/server').default;
const packageInfo = require('./package.json');
const { menu } = require('./src/menu');

autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';

// Setup about panel
app.setAboutPanelOptions({
    applicationName: packageInfo.productName,
    applicationVersion: packageInfo.version
});

// Setup Menu
Menu.setApplicationMenu(menu);

const devMode = /electron/.test(path.basename(app.getPath('exe'), '.exe'));
if (devMode) {
    // Set appname and userData to indicate development environment
    app.setName(`${app.getName()}-dev`);
    app.setPath('userData', `${app.getPath('userData')}-dev`);

    autoUpdater.updateConfigPath = path.join(
        __dirname,
        'src/dev-app-update.yml'
    );

    // For tests, ensure that checkForUpdatesAndNotify doesn't short circuit
    if (process.env.NODE_ENV === 'test')
        Object.defineProperty(app, 'isPackaged', {
            get() {
                return true;
            }
        });

    // Setup reload
    // require('electron-reload')(path.join(__dirname, 'dist/app.js'), {
    //     electron: path.join(__dirname, 'node_modules', '.bin', 'electron')
    // });
} else {
    unhandled({
        showDialog: process.argv.indexOf('--devTools') !== -1
    });
}

const web_app = express();

console.log('starting server...', optimiseCore);
console.log('optimise.db', path.join(app.getPath('userData'), 'optimise.db'));

const optimise_server = new optimiseCore({
    optimiseDBLocation: path.join(app.getPath('userData'), 'optimise.db')
});

console.log('has started server ...');

let cookie;
const httpify = ({ url, options = {} }) => new Promise((resolve, reject) => {

    if (options.method === undefined)
        options.method = 'GET';

    if (cookie !== undefined) {
        if (options.headers === undefined) options.headers = {};
        options.headers.cookie = cookie;
    }

    let queue = '';

    const req = Object.assign(options, {
        url,
        _readableState: {},
        socket: {},
        pipe: (destination) => {
            const s = new Readable();
            s.push(queue);
            s.push(null);
            s.pipe(destination);
        },
        unpipe: function () {
            return;
        },
        connection: {
            remoteAddress: '::1'
        }
    });

    if (
        options.headers !== undefined &&
        options.headers['content-type'] &&
        options.headers['content-type'].search('multipart/form-data') >= 0
    ) {
        const boundary = `--------------------------${Math.random()
            .toString(5)
            .substr(2, 16)}`;
        let content;
        Object.keys(options.body).forEach((k) => {
            queue += `${boundary}\r\nContent-Disposition: form-data; name="${k}"; filename="${options.body[k].name}";\r\nContent-Type: text/plain\r\n\r\n`;
            content = fs.readFileSync(options.body[k].path, 'ascii');
            queue += `${content}`;
        });
        queue += `\r\n${boundary}--`;
        req.headers = req.headers || {};
        req.headers[
            'content-type'
        ] = `multipart/form-data; boundary=${boundary.substr(2)}`;
        req.headers['content-length'] = queue.length;
        req.body = queue;
    } else {
        req.body = options.body ? JSON.parse(options.body) : undefined;
    }

    const res = {
        _sent: Buffer.from(''),
        _headers: {},
        setHeader: (name, value) => {
            res._headers[name] = value;
        },
        set: (name, value) => {
            res._headers[name] = value;
        },
        getHeader: (name) => res._headers[name],
        get: (name) => res._headers[name],
        write: (chunk) => {
            res._sent = Buffer.concat([res._sent, chunk]);
        },
        end: (chunk) => {
            try {
                if (chunk !== undefined)
                    res._sent = Buffer.concat([res._sent, chunk]);
                if (Buffer.byteLength(res._sent) === 0)
                    reject({
                        error: 'Could not process relevant reponse in IPC Fetch'
                    });

                let type;
                // res.writeHead(res.statusCode);
                Object.keys(res._headers).forEach((e) => {
                    if (e.toLowerCase() === 'set-cookie')
                        cookie = res._headers[e][0].split(';')[0];
                    if (e.toLowerCase() === 'content-type')
                        type = res._headers[e];
                });
                if (type.search('application/json') >= 0) {
                    let json = {
                        error: 'Could not parse JSON response'
                    };
                    try {
                        json = JSON.parse(res._sent.toString());
                    } catch (e) {
                        console.error(e);
                    }
                    resolve({
                        headers: res._headers,
                        statusCode: res.statusCode,
                        json
                    });
                } else {
                    resolve({
                        headers: res._headers,
                        statusCode: res.statusCode,
                        buffer: res._sent
                    });
                }
            } catch (e) {
                reject({
                    error: `An error occurred processing IPC Fetch: ${e.message}`
                });
            }
        }
    };

    web_app(req, res);
});

const createApi = () => optimise_server
    .start()
    .then((optimise_router) => {

        console.log('optimise_router', optimise_router);

        // Remove unwanted express headers
        web_app.set('x-powered-by', false);
        web_app.use('/api', optimise_router);

        ipcMain.on('optimiseApiCall', (event, { cid, ...parameters }) => {
            httpify(parameters)
                .then((res) =>
                    event.sender.send('optimiseApiResult', {
                        cid,
                        res
                    }))
                .catch((error) => {
                    event.sender.send('optimiseApiResult', {
                        cid,
                        error: {
                            json: {
                                ...error
                            },
                            statusCode: 500
                        }
                    });
                });
        });

        ipcMain.on('optimiseExportCall', (__unused__event, parameters) => {
            httpify(parameters).then((res) => {
                const filename = (
                    res.headers['Content-Disposition'] || ''
                ).match(/filename="(.*?)"/i);
                const options = {
                    title: 'Save export archive',
                    defaultPath: `${app.getPath('downloads')}/${filename === null || filename[1] === undefined
                        ? 'optimise-data.zip'
                        : filename[1]}`
                };
                return dialog
                    .showSaveDialog(null, options)
                    .then(({ filePath }) =>
                        new Promise((resolve, reject) => {
                            if (filePath !== undefined)
                                fs.writeFile(
                                    filePath,
                                    res.buffer,
                                    function (err) {
                                        if (err)
                                            return reject(err);
                                        return resolve();
                                    }
                                );
                        })
                    );
            })
                .catch((err) => {
                    console.error(err);
                    // eslint-disable-next-line no-alert
                    alert(err);
                });
        });

        return true;
    })
    .catch((error) => {
        console.error(
            'An error occurred while starting the Optimise core.',
            error
        ); // eslint-disable-line no-console
        console.log(error.stack);
        return false;
    });

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
let closingBlock = false;
let updatingBlock = false;
const indexLocation = `file://${__dirname}/dist/index.html`;

const createWindow = () => {
    // Create the browser window.
    mainWindow = new BrowserWindow({
        toolbar: false,
        show: false,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
            enableRemoteModule: true,
            preload: path.join(__dirname, 'preload.js')
        }
    });
    mainWindow.once('ready-to-show', () => {
        mainWindow.show();
    });

    mainWindow.maximize();

    // and load the index.html of the app.
    mainWindow.loadURL(indexLocation);

    // Open the DevTools.
    if ((devMode && process.argv.indexOf('--noDevTools') === -1) || process.argv.indexOf('--devTools') !== -1) {
        mainWindow.webContents.openDevTools();
    } else {
        mainWindow.setMenu(null);
    }

    mainWindow.on('close', () => {
        if (updatingBlock === false) {
            if (mainWindow !== null && closingBlock === false) {
                closingBlock = true;
                mainWindow.close();
                mainWindow = null;
            }
            // On OS X it is common for applications and their menu bar
            // to stay active until the user quits explicitly with Cmd + Q
            if (process.platform !== 'darwin') app.quit();
        }
    });
};

const sendUpdateStatusToWindow = (message) => {
    log.info(message);
    if (mainWindow) mainWindow.webContents.send('update-message', message);
};

const sendAlertToWindow = (message) => {
    log.info(message);
    if (mainWindow) mainWindow.webContents.send('update-message', message);
};

autoUpdater.on('checking-for-update', () => {
    sendUpdateStatusToWindow({
        ready: false,
        text: 'Checking for update...'
    });
});

autoUpdater.on('update-available', () => {
    // Prompt the user if an update is available!
    if (process.env.NODE_ENV !== 'test')
        dialog.showMessageBox({
            message: 'There is an update available! It is being downloaded...'
        });
    sendUpdateStatusToWindow({
        ready: false,
        text: 'There is an update available! It is being downloaded...'
    });
});

autoUpdater.on('update-not-available', () => {
    sendUpdateStatusToWindow({
        ready: false,
        text: 'Nothing to update today.'
    });
});

autoUpdater.on('error', (err) => {
    sendUpdateStatusToWindow({
        ready: false,
        text: `There was an error with the update process. ${err.message !== undefined ? err.message : ''}`
    });
});

autoUpdater.on('download-progress', (info) => {
    info = info || { percent: 0 };
    sendUpdateStatusToWindow({
        ready: false,
        text: `Update download in progress... (${parseFloat(
            info.percent
        ).toFixed(2)}%)`
    });
});

autoUpdater.on('update-downloaded', () => {
    // Promot the user to update, if update is available
    const options = {
        buttons: ['Yes', 'No'],
        message: 'Do you want to update the software now?'
    };

    dialog.showMessageBox(options).then((res) =>
        new Promise((resolve) => {
            if (res.response === 0) {
                setImmediate(() => {
                    app.removeAllListeners('window-all-closed');

                    // Silent update & force restart works only for windows
                    resolve(optimise_server
                        .stop()
                        .then(() => autoUpdater.quitAndInstall(true, true)));
                });
            }
            resolve();
        }))
        .catch((err) => {
            console.error(err);
            // eslint-disable-next-line no-alert
            alert(err);
        });

    sendUpdateStatusToWindow({
        ready: true,
        text: 'The update is ready! Click the button below to install.'
    });
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', () => {

    // Set CSP HTTP headers
    session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
        callback({
            responseHeaders: {
                ...details.responseHeaders,
                'Content-Security-Policy': ['default-src \'self\' \'unsafe-inline\' data: blob:']
            }
        });
    });

    if (process.argv.includes('--autoupdate-test')) {
        console.log('This run is for testing auto-update and fires no UI');
    } else createApi().then(createWindow)
        .catch((err) => {
            sendAlertToWindow(err.toString());
            console.error(err);
            if (devMode || process.argv.indexOf('--devTools') !== -1) {
                // eslint-disable-next-line no-alert
                alert(err);
            }
        });
    setTimeout(() => autoUpdater.checkForUpdates(), 1000);
});

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin')
        optimise_server.stop().then(() => app.quit())
            .catch((err) => {
                console.error(err);
                if (devMode || process.argv.indexOf('--devTools') !== -1) {
                    // eslint-disable-next-line no-alert
                    alert(err);
                }
            });
});

app.on('activate', () => {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (mainWindow === null) {
        closingBlock = false;
        createWindow();
    }
});

ipcMain.on('quitAndInstall', () => {
    updatingBlock = true;
    setImmediate(() => {
        app.removeAllListeners('window-all-closed');
        optimise_server.stop().then(() => autoUpdater.quitAndInstall(false))
            .catch((err) => {
                console.error(err);
                if (devMode || process.argv.indexOf('--devTools') !== -1) {
                    // eslint-disable-next-line no-alert
                    alert(err);
                }
            });
    });
});

ipcMain.on('rendererIsFinished', () => {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin' && updatingBlock === false)
        optimise_server.stop().then(() => app.quit())
            .catch((err) => {
                console.error(err);
                if (devMode || process.argv.indexOf('--devTools') !== -1) {
                    // eslint-disable-next-line no-alert
                    alert(err);
                }
            });
});