add logging and refactor (#26)

This commit is contained in:
Lynn 2017-03-01 16:32:21 -08:00 committed by GitHub
parent c337c62e03
commit 40e6b173af
9 changed files with 366 additions and 189 deletions

12
js/enums/logLevels.js Normal file
View File

@ -0,0 +1,12 @@
'use strict';
var keyMirror = require('keymirror');
module.exports = keyMirror({
ERROR: null,
CONFLICT: null,
WARN: null,
ACTION: null,
INFO: null,
DEBUG: null
});

28
js/log.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
let logWindow;
/**
* Send log messages from main process to logger hosted by
* renderer process. Allows main process to use logger
* provided by JS.
* @param {enum} level enum from ./enums/LogLevel.js
* @param {string} details msg to be logged
*/
function send(level, details) {
if (logWindow && level && details) {
logWindow.send('log', {
logLevel: level,
logDetails: details
});
}
}
function setLogWindow(win) {
logWindow = win;
}
module.exports = {
send: send,
setLogWindow: setLogWindow
};

View File

@ -1,190 +1,22 @@
'use strict';
const electron = require('electron');
const packageJSON = require('../package.json');
const menuTemplate = require('./menuTemplate.js');
const path = require('path');
const app = electron.app;
const nodeURL = require('url');
const getConfig = require('./getConfig.js');
const { isMac, isDevEnv, getGuid } = require('./utils.js');
const loadErrors = require('./dialogs/showLoadError.js');
// show dialog when certificate errors occur
require('./dialogs/showCertError.js');
// 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 windows = {};
let willQuitApp = false;
let isOnline = true;
const { isMac } = require('./utils.js');
// exit early for squirrel installer
if (require('electron-squirrel-startup')) {
return;
}
function createMainWindow (url) {
let key = getGuid();
require('./mainApiMgr.js');
mainWindow = new electron.BrowserWindow({
title: 'Symphony',
width: 1024, height: 768,
show: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
preload: path.join(__dirname, '/preload.js'),
winKey: key
}
});
// monitor memory of main process
require('./memoryMonitor.js');
function retry() {
if (isOnline) {
mainWindow.webContents && mainWindow.webContents.reload();
} else {
loadErrors.showNetworkConnectivityError(mainWindow, url, retry);
}
}
// content can be cached and will still finish load but
// we might not have netowrk connectivity, so warn the user.
mainWindow.webContents.once('did-finish-load', function() {
if (!isOnline) {
loadErrors.showNetworkConnectivityError(mainWindow, url, retry);
}
});
mainWindow.webContents.once('did-fail-load', function(event, errorCode,
errorDesc, validatedURL, isMainFrame) {
loadErrors.showLoadFailure(mainWindow, url, errorDesc, errorCode, retry);
});
storeWindowKey(key, mainWindow);
mainWindow.loadURL(url);
const menu = electron.Menu.buildFromTemplate(menuTemplate(app));
electron.Menu.setApplicationMenu(menu);
mainWindow.on('close', function(e) {
if (willQuitApp) {
mainWindow = null;
return;
}
// mac should hide window when hitting x close
if (isMac) {
mainWindow.hide();
e.preventDefault();
}
});
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
// open external links in default browser - window.open
mainWindow.webContents.on('new-window', function(event, url) {
event.preventDefault();
electron.shell.openExternal(url);
});
}
function storeWindowKey(key, browserWin) {
windows[key] = browserWin;
}
/**
* Ensure events comes from a window that we have created.
* @param {EventEmitter} event node emitter event to be tested
* @return {Boolean} returns true if exists otherwise false
*/
function isValidWindow(event) {
if (event && event.sender) {
// validate that event sender is from window we created
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
let winKey = event.sender.browserWindowOptions &&
event.sender.browserWindowOptions.webPreferences &&
event.sender.browserWindowOptions.webPreferences.winKey;
if (browserWin instanceof electron.BrowserWindow) {
let win = windows[winKey];
return win && win === browserWin;
}
}
return false;
}
/**
* Only permit certain cmds for some windows
* @param {EventEmitter} event node emitter event to be tested
* @param {String} cmd cmd name
* @return {Boolean} true if cmd is allowed for window, otherwise false
*/
function isCmdAllowed(event, cmd) {
if (event && event.sender && cmd) {
// validate that event sender is from window we created
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
if (browserWin === mainWindow) {
// allow all commands for main window
return true;
} else {
// allow only certain cmds for child windows
// e.g., open cmd not allowed for child windows
return (arg.cmd !== 'open');
}
}
return false;
}
/**
* Handle ipc messages from renderers. Only messages from windows we have
* created are allowed.
*/
electron.ipcMain.on('symphony-msg', (event, arg) => {
if (!isValidWindow(event)) {
console.log('invalid window try to perform action, ignoring action.');
return;
}
if (!isCmdAllowed(event, arg && arg.cmd)) {
console.log('cmd not allowed for this window: ' + arg.cmd);
return;
}
if (arg && arg.cmd === 'isOnline') {
isOnline = arg.isOnline;
return;
}
if (arg && arg.cmd === 'open' && arg.url) {
let width = arg.width || 1024;
let height = arg.height || 768;
let title = arg.title || 'Symphony';
let winKey = getGuid();
let childWindow = new electron.BrowserWindow({
title: title,
width: width,
height: height,
webPreferences: {
sandbox: true,
nodeIntegration: false,
preload: path.join(__dirname, '/preload.js'),
winKey: winKey
}
});
storeWindowKey(winKey, childWindow);
childWindow.loadURL(arg.url);
return;
}
});
const windowMgr = require('./windowMgr.js');
/**
* This method will be called when Electron has finished
@ -206,17 +38,13 @@ function getUrlAndOpenMainWindow() {
slahes: true,
pathname: parsedUrl.href
});
createMainWindow(url);
windowMgr.createMainWindow(url);
}).catch(function(err) {
let title = 'Error loading configuration';
electron.dialog.showErrorBox(title, title + ': ' + err);
});
}
app.on('before-quit', function() {
willQuitApp = true;
});
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
@ -226,9 +54,9 @@ app.on('window-all-closed', function () {
});
app.on('activate', function () {
if (mainWindow === null) {
if (windowMgr.isMainWindow(null)) {
getUrlAndOpenMainWindow();
} else {
mainWindow.show();
windowMgr.showMainWindow();
}
});

96
js/mainApiMgr.js Normal file
View File

@ -0,0 +1,96 @@
'use strict';
/**
* This module runs in the main process and handles api calls
* from the renderer process.
*/
const electron = require('electron');
const path = require('path');
const windowMgr = require('./windowMgr.js');
const log = require('./log.js');
/**
* Ensure events comes from a window that we have created.
* @param {EventEmitter} event node emitter event to be tested
* @return {Boolean} returns true if exists otherwise false
*/
function isValidWindow(event) {
if (event && event.sender) {
// validate that event sender is from window we created
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
let winKey = event.sender.browserWindowOptions &&
event.sender.browserWindowOptions.webPreferences &&
event.sender.browserWindowOptions.webPreferences.winKey;
return windowMgr.hasWindow(browserWin, winKey);
}
return false;
}
// only these cmds are allowed by main window
let cmdBlackList = [ 'open', 'registerLogger' ];
/**
* Only permit certain cmds for some windows
* @param {EventEmitter} event node emitter event to be tested
* @param {String} cmd cmd name
* @return {Boolean} true if cmd is allowed for window, otherwise false
*/
function isCmdAllowed(event, cmd) {
if (event && event.sender && cmd) {
// validate that event sender is from window we created
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
if (windowMgr.isMainWindow(browserWin)) {
// allow all commands for main window
return true;
} else {
// allow only certain cmds for child windows
// e.g., open cmd not allowed for child windows
return (cmdBlackList.indexOf(cmd) === -1)
}
}
return false;
}
/**
* Handle API related ipc messages from renderers. Only messages from windows
* we have created are allowed.
*/
electron.ipcMain.on('symphony-api', (event, arg) => {
if (!isValidWindow(event)) {
console.log('invalid window try to perform action, ignoring action.');
return;
}
if (!isCmdAllowed(event, arg && arg.cmd)) {
console.log('cmd not allowed for this window: ' + arg.cmd);
return;
}
if (!arg) {
return;
}
if (arg.cmd === 'isOnline') {
windowMgr.setIsOnline(arg.isOnline);
return;
}
if (arg.cmd === 'registerLogger') {
// renderer window that has a registered logger from JS.
log.setLogWindow(event.sender);
return;
}
if (arg.cmd === 'open' && arg.url) {
let title = arg.title || 'Symphony';
let width = arg.width || 1024;
let height = arg.height || 768;
windowMgr.createChildWindow(arg.url, title, width, height);
return;
}
});

18
js/memoryMonitor.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
const electron = require('electron');
const log = require('./log.js');
const logLevels = require('./enums/logLevels.js')
// once a minute
setInterval(gatherMemory, 1000 * 60);
function gatherMemory() {
var memory = process.getProcessMemoryInfo();
var details =
'workingSetSize: ' + memory.workingSetSize +
' peakWorkingSetSize: ' + memory.peakWorkingSetSize +
' privatesBytes: ' + memory.privatesBytes +
' sharedBytes: ' + memory.sharedBytes;
log.send(logLevels.INFO, details);
}

View File

@ -19,6 +19,8 @@ const local = {
ipcRenderer: ipcRenderer
};
const api = 'symphony-api';
// API exposed by Symphony to renderer processes:
// Note: certain cmds are only allowed on some windows, this is checked by
// main process.
@ -27,21 +29,51 @@ window.SYM_API = {
// only allowed by main window - enforced by main process.
openWindow: function(url) {
local.ipcRenderer.send('symphony-msg', {
local.ipcRenderer.send(api, {
cmd: 'open',
url: url
});
},
networkStatusChange: function(isOnline) {
local.ipcRenderer.send('symphony-msg', {
cmd: 'isOnline',
isOnline: isOnline
});
/**
* allows JS to register a logger that can be used by electron main process.
* @param {Object} logger function that can be called accepting
* object: {
* logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG',
* logDetails: String
* }
*
* note: only main window is allowed to register a logger, others are
* ignored.
*/
registerLogger: function(logger) {
if (typeof logger === 'function') {
local.logger = logger;
// only main window can register
local.ipcRenderer.send(api, {
cmd: 'registerLogger'
});
}
}
};
// listen for log message from main process
local.ipcRenderer.on('log', (event, arg) => {
console.log('got msg:' + arg)
if (local.logger && arg && arg.level && arg.msg) {
local.logger({
logLevel: arg.level,
logDetails: arg.msg
});
}
});
function updateOnlineStatus() {
window.SYM_API.networkStatusChange(navigator.onLine);
local.ipcRenderer.send(api, {
cmd: 'isOnline',
isOnline: navigator.onLine
});
}
window.addEventListener('offline', updateOnlineStatus, false);

160
js/windowMgr.js Normal file
View File

@ -0,0 +1,160 @@
'use strict';
const electron = require('electron');
const app = electron.app;
const path = require('path');
const menuTemplate = require('./menuTemplate.js');
const loadErrors = require('./dialogs/showLoadError.js');
const { isMac, getGuid } = require('./utils.js');
const log = require('./log.js')
const logLevels = require('./enums/logLevels.js');
// show dialog when certificate errors occur
require('./dialogs/showCertError.js');
// 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 windows = {};
let willQuitApp = false;
let isOnline = true;
const preloadScript = path.join(__dirname, '/RendererPreload.js');
function addWindowKey(key, browserWin) {
windows[key] = browserWin;
}
function removeWindowKey(key) {
delete windows[key];
}
function createMainWindow (url) {
let key = getGuid();
mainWindow = new electron.BrowserWindow({
title: 'Symphony',
width: 1024, height: 768,
show: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
preload: preloadScript,
winKey: key
}
});
function retry() {
if (isOnline) {
mainWindow.webContents && mainWindow.webContents.reload();
} else {
loadErrors.showNetworkConnectivityError(mainWindow, url, retry);
}
}
// content can be cached and will still finish load but
// we might not have netowrk connectivity, so warn the user.
mainWindow.webContents.on('did-finish-load', function() {
if (!isOnline) {
loadErrors.showNetworkConnectivityError(mainWindow, url, retry);
} else {
log.send(logLevels.INFO, 'main window loaded');
}
});
mainWindow.webContents.on('did-fail-load', function(event, errorCode,
errorDesc, validatedURL, isMainFrame) {
loadErrors.showLoadFailure(mainWindow, url, errorDesc, errorCode, retry);
});
addWindowKey(key, mainWindow);
mainWindow.loadURL(url);
const menu = electron.Menu.buildFromTemplate(menuTemplate(app));
electron.Menu.setApplicationMenu(menu);
mainWindow.on('close', function(e) {
if (willQuitApp) {
mainWindow = null;
return;
}
// mac should hide window when hitting x close
if (isMac) {
mainWindow.hide();
e.preventDefault();
}
});
mainWindow.on('closed', function () {
removeWindowKey(key);
mainWindow.removeAllEventListeners();
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
// open external links in default browser - window.open
mainWindow.webContents.on('new-window', function(event, url) {
event.preventDefault();
electron.shell.openExternal(url);
});
}
app.on('before-quit', function() {
willQuitApp = true;
});
function showMainWindow() {
mainWindow.show();
}
function isMainWindow(win) {
return mainWindow === win;
}
function hasWindow(win, winKey) {
if (win instanceof electron.BrowserWindow) {
let browserWin = windows[winKey];
return browserWin && win === browserWin;
}
return false;
}
function createChildWindow(url, title, width, height) {
let winKey = getGuid();
let childWindow = new electron.BrowserWindow({
title: title,
width: width,
height: height,
webPreferences: {
sandbox: true,
nodeIntegration: false,
preload: preloadScript,
winKey: winKey
}
});
addWindowKey(winKey, childWindow);
childWindow.loadURL(url);
childWindow.on('closed', function() {
childWindow.removeAllEventListeners();
removeWindowKey(winKey);
});
}
function setIsOnline(status) {
isOnline = status;
}
module.exports = {
createMainWindow: createMainWindow,
showMainWindow: showMainWindow,
isMainWindow: isMainWindow,
hasWindow: hasWindow,
createChildWindow: createChildWindow,
setIsOnline: setIsOnline
};

View File

@ -62,6 +62,7 @@
"jest": "^19.0.2"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
"electron-squirrel-startup": "^1.0.0",
"keymirror": "0.1.1"
}
}

2
tests/README.md Normal file
View File

@ -0,0 +1,2 @@
# to override dependencies
- use https://github.com/thlorenz/proxyquire