API Docs for: 2.0.0

client/electron.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization
// and Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014-2015

/**
 * Electron SAGE2 client
 *
 * @class electron
 * @module electron
 * @submodule electron
 * @requires electron commander
 */

'use strict';

const electron = require('electron');
// electron.app.setAppPath(process.cwd());

//
// handle install/update for Windows
//
if (require('electron-squirrel-startup')) {
	return;
}
// this should be placed at top of main.js to handle setup events quickly
if (handleSquirrelEvent()) {
	// squirrel event handled and app will exit in 1000ms, so don't do anything else
	return;
}

function handleSquirrelEvent() {
	if (process.argv.length === 1) {
		return false;
	}

	const ChildProcess = require('child_process');
	const path = require('path');

	const appFolder = path.resolve(process.execPath, '..');
	const rootAtomFolder = path.resolve(appFolder, '..');
	const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe'));
	const exeName = path.basename(process.execPath);

	const spawn = function(command, args) {
		let spawnedProcess;

		try {
			spawnedProcess = ChildProcess.spawn(command, args, {detached: true});
		} catch (error) {
			// pass
		}

		return spawnedProcess;
	};

	const spawnUpdate = function(args) {
		return spawn(updateDotExe, args);
	};

	const squirrelEvent = process.argv[1];
	switch (squirrelEvent) {
		case '--squirrel-install':
		case '--squirrel-updated':
			// Install desktop and start menu shortcuts
			spawnUpdate(['--createShortcut', exeName]);
			setTimeout(app.quit, 1000);
			return true;
		case '--squirrel-uninstall':
			// Remove desktop and start menu shortcuts
			spawnUpdate(['--removeShortcut', exeName]);
			setTimeout(app.quit, 1000);
			return true;
		case '--squirrel-obsolete':
			app.quit();
			return true;
	}
}


// Module to control application life.
const app = electron.app;
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;
// Module to handle ipc with Browser Window
const ipcMain = electron.ipcMain;

// parsing command-line arguments
var commander = require('commander');
// get hardware and performance data
var si = require('systeminformation');
// Get the version from the package file
var version = require('./package.json').version;

/**
 * Setup the command line argument parsing (commander module)
 */
var args = process.argv;
if (args.length === 1) {
	// seems to make commander happy when using binary packager
	args = args[0];
}

// Generate the command line handler
commander
	.version(version)
	.option('-a, --audio',               'Open the audio manager (instead of display)', false)
	.option('-d, --display <n>',         'Display client ID number (int)', parseInt, 0)
	.option('-f, --fullscreen',          'Fullscreen (boolean)', false)
	.option('-m, --monitor <n>',         'Select a monitor (int)', myParseInt, null)
	.option('-n, --no_decoration',       'Remove window decoration (boolean)', false)
	.option('-p, --plugins',             'Enables plugins and flash (boolean)', false)
	.option('-s, --server <s>',          'Server URL (string)', 'http://localhost:9292')
	.option('-u, --ui',                  'Open the user interface (instead of display)', false)
	.option('-x, --xorigin <n>',         'Window position x (int)', myParseInt, 0)
	.option('-y, --yorigin <n>',         'Window position y (int)', myParseInt, 0)
	.option('--allowDisplayingInsecure', 'Allow displaying of insecure content (http on https)', false)
	.option('--allowRunningInsecure',    'Allow running insecure content (scripts accessed on http vs https)', false)
	.option('--cache',                   'Clear the cache', false)
	.option('--console',                 'Open the devtools console', false)
	.option('--debug',                   'Open the port debug protocol (port number is 9222 + clientID)', false)
	.option('--experimentalFeatures',    'Enable experimental features', false)
	.option('--hash <s>',                'Server password hash (string)', null)
	.option('--height <n>',              'Window height (int)', myParseInt, 720)
	.option('--password <s>',            'Server password (string)', null)
	.option('--show-fps',                'Display the Chrome FPS counter', false)
	.option('--width <n>',               'Window width (int)', myParseInt, 1280)
	.parse(args);

// Load the flash plugin if asked
if (commander.plugins) {
	// Flash loader
	const flashLoader = require('flash-player-loader');

	flashLoader.debug({enable: true});
	if (process.platform === 'darwin') {
		flashLoader.addSource('@chrome');
	}
	flashLoader.addSource('@system');
	flashLoader.load();
}

// Reset the desktop scaling
const os = require('os');
if (os.platform() === "win32") {
	app.commandLine.appendSwitch("force-device-scale-factor", "1");
}

// Remove the limit on the number of connections per domain
//  the usual value is around 6
const url = require('url');
var parsedURL = url.parse(commander.server);
// default domais are local
var domains   = "localhost,127.0.0.1";
if (parsedURL.hostname) {
	// add the hostname
	domains +=  "," + parsedURL.hostname;
}
app.commandLine.appendSwitch("ignore-connections-limit", domains);

// Enable the Chrome builtin FPS display for debug
if (commander.showFps) {
	app.commandLine.appendSwitch("show-fps-counter");
}

// Enable port for Chrome DevTools Protocol to control low-level
// features of the browser. See:
// https://chromedevtools.github.io/devtools-protocol/
if (commander.debug) {
	// Common port for this protocol
	let port = 9222;
	// Offset the port by the client number, so every client gets a different one
	port += commander.display;
	// Add the parameter to the list of options on the command line
	app.commandLine.appendSwitch("remote-debugging-port", port.toString());
}

/**
 * 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.
 */
var mainWindow;

/**
 * Opens a window.
 *
 * @method     openWindow
 */
function openWindow() {
	if (!commander.fullscreen) {
		mainWindow.show();
	}

	if (commander.audio) {
		if (commander.width === 1280 && (commander.height === 720)) {
			// if default values specified, tweak them for the audio manager
			commander.width  = 800;
			commander.height = 400;
		}
	}

	// Setup initial position and size
	mainWindow.setBounds({
		x:      commander.xorigin,
		y:      commander.yorigin,
		width:  commander.width,
		height: commander.height
	});

	// Start to build a URL to load
	var location = commander.server;

	// Test if we want an audio client
	if (commander.audio) {
		location = location + "/audioManager.html";
		if (commander.hash) {
			// add the password hash to the URL
			location += '?hash=' + commander.hash;
		} else if (commander.password) {
			// add the password hash to the URL
			location += '?session=' + commander.password;
		}
	} else if (commander.ui) {
		// or an UI client
		location = location + "/index.html";
		if (commander.hash) {
			// add the password hash to the URL
			location += '?hash=' + commander.hash;
		} else if (commander.password) {
			// add the password hash to the URL
			location += '?session=' + commander.password;
		}
	} else {
		// and by default a display client
		location = location + "/display.html?clientID=" + commander.display;
		if (commander.hash) {
			// add the password hash to the URL
			location += '&hash=' + commander.hash;
		} else if (commander.password) {
			// add the password hash to the URL
			location += '?session=' + commander.password;
		}
	}
	mainWindow.loadURL(location);

	if (commander.monitor !== null) {
		mainWindow.on('show', function() {
			mainWindow.setFullScreen(true);
			// Once all done, prevent changing the fullscreen state
			mainWindow.setFullScreenable(false);
		});
	} else {
		// Once all done, prevent changing the fullscreen state
		mainWindow.setFullScreenable(false);
	}
}

/**
 * Creates an electron window.
 *
 * @method     createWindow
 */
function createWindow() {
	// If a monitor is specified
	if (commander.monitor !== null) {
		// get all the display data
		let displays = electron.screen.getAllDisplays();
		// get the bounds of the interesting one
		let bounds = displays[commander.monitor].bounds;
		// overwrite the values specified
		commander.width   = bounds.width;
		commander.height  = bounds.height;
		commander.xorigin = bounds.x;
		commander.yorigin = bounds.y;
		commander.no_decoration = true;
	}

	// Create option data structure
	var options = {
		width:  commander.width,
		height: commander.height,
		frame:  !commander.no_decoration,
		fullscreen: commander.fullscreen,
		show: !commander.fullscreen,
		fullscreenable: commander.fullscreen,
		alwaysOnTop: commander.fullscreen,
		kiosk: commander.fullscreen,
		// a default color while loading
		backgroundColor: "#565656",
		// resizable: !commander.fullscreen,
		webPreferences: {
			nodeIntegration: true,
			webSecurity: false, // seems to be an issue on Windows
			backgroundThrottling: false,
			plugins: commander.plugins,
			allowDisplayingInsecureContent: false,
			allowRunningInsecureContent: false,
			// this enables things like the CSS grid. add a commander option up top for enable / disable on start.
			experimentalFeatures: (commander.experimentalFeatures) ? true : false
		}
	};

	if (process.platform === 'darwin') {
		// noting for now
	} else {
		options.titleBarStyle = "hidden";
	}

	// Create the browser window.
	mainWindow = new BrowserWindow(options);

	if (commander.cache) {
		// clear the caches, useful to remove password cookies
		const session = electron.session.defaultSession;
		session.clearStorageData({
			storages: ["appcache", "cookies", "local storage", "serviceworkers"]
		}, function() {
			console.log('Electron>	Caches cleared');
			openWindow();
		});
	} else {
		openWindow();
	}

	// When the webview tries to download something
	electron.session.defaultSession.on('will-download', (event, item, webContents) => {
		// do nothing
		event.preventDefault();
		// send message to the render process (browser)
		mainWindow.webContents.send('warning', 'File download not supported');
	});

	// Mute the audio (just in case)
	var playAudio = commander.audio || (commander.display === 0);
	mainWindow.webContents.setAudioMuted(!playAudio);

	// Open the DevTools.
	if (commander.console) {
		mainWindow.webContents.openDevTools();
	}

	// Emitted when the window is closed.
	mainWindow.on('closed', function() {
		// Dereference the window object
		mainWindow = null;
	});

	// when the display client is loaded
	mainWindow.webContents.on('did-finish-load', function() {
		// Get the basic information of the system
		si.getStaticData(function(data) {
			// Send it to the page, since it has the connection
			// to the server
			data.hostname = os.hostname();
			// fix on some system with no memory layout
			if (data.memLayout.length === 0) {
				si.mem(function(mem) {
					data.memLayout[0] = {size: mem.total};
					// send data to the HTML page, ie SAGE2_Display.js
					mainWindow.webContents.send('hardwareData', data);
				});
			} else {
				// send data to the HTML page, ie SAGE2_Display.js
				mainWindow.webContents.send('hardwareData', data);
			}
		});
	});

	// If the window opens before the server is ready,
	// wait 2 sec. and try again
	mainWindow.webContents.on('did-fail-load', function(ev) {
		setTimeout(function() {
			mainWindow.reload();
		}, 2000);
	});

	mainWindow.webContents.on('will-navigate', function(ev) {
		// ev.preventDefault();
	});

	ipcMain.on('getPerformanceData', function() {
		var perfData = {};
		var displayLoad = {
			cpuPercent: 0,
			memPercent: 0,
			memVirtual: 0,
			memResidentSet: 0
		};
		// CPU Load
		var load = si.currentLoad();
		var mem = load.then(function(data) {
			perfData.cpuLoad = data;
			return si.mem();
		});
		mem.then(data => {
			perfData.mem = data;
			return si.processes();
		})
			.then(data => {
				var displayProcess = data.list.filter(function(d) {
					return parseInt(d.pid) === parseInt(process.pid)
						|| parseInt(d.pid) === mainWindow.webContents.getOSProcessId();
				});

				displayProcess.forEach(el => {
					displayLoad.cpuPercent += el.pcpu;
					displayLoad.memPercent += el.pmem;
					displayLoad.memVirtual += el.mem_vsz;
					displayLoad.memResidentSet += el.mem_rss;
				});

				perfData.processLoad = displayLoad;
				mainWindow.webContents.send('performanceData', perfData);
			})
			.catch(error => console.error(error));
	});
}

/**
 * This method will be called when Electron has finished
 * initialization and is ready to create a browser window.
 */
app.on('ready', createWindow);

/**
 * Quit when all windows are closed.
 */
app.on('window-all-closed', function() {
	// 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();
	}
});

/**
 * activate callback
 * 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 window open.
 */
app.on('activate', function() {
	if (mainWindow === null) {
		createWindow();
	}
});


/**
 * Utiltiy function to parse command line arguments as number
 *
 * @method     myParseInt
 * @param      {String}    str           the argument
 * @param      {Number}    defaultValue  The default value
 * @return     {Number}    return an numerical value
 */
function myParseInt(str, defaultValue) {
	var int = parseInt(str, 10);
	if (typeof int == 'number') {
		return int;
	}
	return defaultValue;
}