Typescript 🎉 (logger, get-guid, string-format and throttle)

This commit is contained in:
Kiran Niranjan 2018-10-06 23:39:01 +05:30
parent 1fc1e29d1a
commit 7fcc6aadb3
11 changed files with 393 additions and 37 deletions

View File

@ -4,6 +4,7 @@ import * as path from 'path';
import { omit } from 'lodash';
import compareSemVersions from '../common/compare-sem-versions';
import { logger } from '../common/logger';
import { isDevEnv, isMac } from '../common/mics';
import pick from '../common/pick';
@ -181,6 +182,7 @@ class Config {
* Verifies if the application is launched for the first time
*/
private checkFirstTimeLaunch() {
logger.info('checking first launch');
const appVersionString = app.getVersion().toString();
const execPath = path.dirname(this.appPath);
const shouldUpdateUserConfig = execPath.indexOf('AppData\\Local\\Programs') !== -1 || isMac;
@ -195,7 +197,9 @@ class Config {
&& typeof userConfigVersion === 'string'
&& (compareSemVersions(appVersionString, userConfigVersion) !== 1)) && shouldUpdateUserConfig) {
this.isFirstTime = true;
return;
}
this.isFirstTime = false;
}
}

View File

@ -1,26 +1,178 @@
import { ipcMain } from 'electron';
import { logger } from '../common/logger';
export enum IApiCmds {
'isOnline',
'registerLogger',
'setBadgeCount',
'badgeDataUrl',
'activate',
'registerBoundsChange',
'registerProtocolHandler',
'registerActivityDetection',
'showNotificationSettings',
'sanitize',
'bringToFront',
'openScreenPickerWindow',
'popupMenu',
'optimizeMemoryConsumption',
'optimizeMemoryRegister',
'setIsInMeeting',
'setLocale',
'keyPress',
export enum ApiCmds {
isOnline,
registerLogger,
setBadgeCount,
badgeDataUrl,
activate,
registerBoundsChange,
registerProtocolHandler,
registerActivityDetection,
showNotificationSettings,
sanitize,
bringToFront,
openScreenPickerWindow,
popupMenu,
optimizeMemoryConsumption,
optimizeMemoryRegister,
setIsInMeeting,
setLocale,
keyPress,
}
export enum apiName {
'symphonyapi',
}
symphonyApi = 'symphony-api',
}
export interface IApiArgs {
cmd: ApiCmds;
isOnline: boolean;
count: number;
dataUrl: string;
windowName: string;
period: number;
reason: string;
sources: Electron.DesktopCapturerSource[];
id: number;
memory: Electron.ProcessMemoryInfo;
cpuUsage: Electron.CPUUsage;
isInMeeting: boolean;
locale: string;
keyCode: number;
}
/**
* 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: Electron.Event) {
/*if (!checkValidWindow) {
return true;
}*/
const result = false;
if (event && event.sender) {
// validate that event sender is from window we created
// const browserWin = BrowserWindow.fromWebContents(event.sender);
// result = windowMgr.hasWindow(browserWin, event.sender.id);
}
if (!result) {
// log.send(logLevels.WARN, 'invalid window try to perform action, ignoring action');
}
return result;
}
/**
* Handle API related ipc messages from renderers. Only messages from windows
* we have created are allowed.
*/
ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
if (!isValidWindow(event)) {
return;
}
if (!arg) {
return;
}
switch (arg.cmd) {
/*case ApiCmds.isOnline:
if (typeof arg.isOnline === 'boolean') {
windowMgr.setIsOnline(arg.isOnline);
}
break;
case ApiCmds.setBadgeCount:
if (typeof arg.count === 'number') {
badgeCount.show(arg.count);
}
break;
case ApiCmds.registerProtocolHandler:
protocolHandler.setProtocolWindow(event.sender);
protocolHandler.checkProtocolAction();
break;
case ApiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
badgeCount.setDataUrl(arg.dataUrl, arg.count);
}
break;
case ApiCmds.activate:
if (typeof arg.windowName === 'string') {
windowMgr.activate(arg.windowName);
}
break;
case ApiCmds.registerBoundsChange:
windowMgr.setBoundsChangeWindow(event.sender);
break;*/
case ApiCmds.registerLogger:
// renderer window that has a registered logger from JS.
logger.setLoggerWindow(event.sender);
break;
/*case ApiCmds.registerActivityDetection:
if (typeof arg.period === 'number') {
// renderer window that has a registered activity detection from JS.
activityDetection.setActivityWindow(arg.period, event.sender);
}
break;
case ApiCmds.showNotificationSettings:
if (typeof arg.windowName === 'string') {
configureNotification.openConfigurationWindow(arg.windowName);
}
break;
case ApiCmds.sanitize:
if (typeof arg.windowName === 'string') {
sanitize(arg.windowName);
}
break;
case ApiCmds.bringToFront:
// validates the user bring to front config and activates the wrapper
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
bringToFront(arg.windowName, arg.reason);
}
break;
case ApiCmds.openScreenPickerWindow:
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
openScreenPickerWindow(event.sender, arg.sources, arg.id);
}
break;
case ApiCmds.popupMenu: {
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
if (browserWin && !browserWin.isDestroyed()) {
windowMgr.getMenu().popup(browserWin, {x: 20, y: 15, async: true});
}
break;
}
case ApiCmds.optimizeMemoryConsumption:
if (typeof arg.memory === 'object'
&& typeof arg.cpuUsage === 'object'
&& typeof arg.memory.workingSetSize === 'number') {
setPreloadMemoryInfo(arg.memory, arg.cpuUsage);
}
break;
case ApiCmds.optimizeMemoryRegister:
setPreloadWindow(event.sender);
break;
case ApiCmds.setIsInMeeting:
if (typeof arg.isInMeeting === 'boolean') {
setIsInMeeting(arg.isInMeeting);
}
break;
case ApiCmds.setLocale:
if (typeof arg.locale === 'string') {
let browserWin = electron.BrowserWindow.fromWebContents(event.sender);
windowMgr.setLocale(browserWin, { language: arg.locale });
}
break;
case ApiCmds.keyPress:
if (typeof arg.keyCode === 'number') {
windowMgr.handleKeyPress(arg.keyCode);
}
break;*/
default:
}
});

View File

@ -1,6 +1,7 @@
import { app } from 'electron';
import getCmdLineArg from '../common/get-command-line-args';
import { logger } from '../common/logger';
import { isDevEnv } from '../common/mics';
import { cleanUpAppCache, createAppCacheFile } from './app-cache-handler';
import { autoLaunchInstance } from './auto-launch-controller';
@ -24,6 +25,7 @@ async function main() {
windowHandler.createApplication();
if (config.isFirstTimeLaunch()) {
logger.info('first time launch');
await config.setUpFirstTimeLaunch();
/**

View File

@ -17,6 +17,7 @@ export class WindowHandler {
webPreferences: {
nativeWindowOpen: true,
nodeIntegration: false,
preload: path.join(__dirname, '../renderer/preload'),
sandbox: false,
},
};
@ -49,15 +50,8 @@ export class WindowHandler {
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
try {
crashReporter.start({
companyName: this.globalConfig!.crashReporter!.companyName,
extra: {
podUrl: this.globalConfig.url,
process: 'main',
},
submitURL: this.globalConfig!.crashReporter!.submitURL,
uploadToServer: this.globalConfig!.crashReporter!.uploadToServer,
});
const extra = { podUrl: this.globalConfig.url, process: 'main' };
crashReporter.start({ ...this.globalConfig.crashReporter, ...extra });
} catch (e) {
throw new Error('failed to init crash report');
}
@ -67,15 +61,14 @@ export class WindowHandler {
this.mainWindow = new BrowserWindow(this.windowOpts);
const urlFromCmd = getCmdLineArg(process.argv, '--url=', false);
this.mainWindow.loadURL(urlFromCmd && urlFromCmd.substr(6) || this.validateURL(this.globalConfig.url));
this.mainWindow.webContents.on('did-finish-load', () => {
if (this.loadingWindow) {
this.loadingWindow.destroy();
this.loadingWindow = null;
}
if (this.mainWindow) this.mainWindow.show();
});
return this.mainWindow;
}

View File

@ -1,6 +1,3 @@
// import log from '../logs';
// import { LogLevels } from '../logs/interface';
/**
* Search given argv for argName using exact match or starts with. Comparison is case insensitive
* @param {Array} argv Array of strings
@ -11,8 +8,7 @@
*/
export default function getCmdLineArg(argv: string[], argName: string, exactMatch: boolean): string | null {
if (!Array.isArray(argv)) {
// log.send(LogLevels.WARN, `getCmdLineArg: TypeError invalid func arg, must be an array: ${argv}`);
return null;
throw new Error(`get-command-line-args: TypeError invalid func arg, must be an array: ${argv}`);
}
const argNameToFind = argName.toLocaleLowerCase();

14
src/common/get-guid.ts Normal file
View File

@ -0,0 +1,14 @@
/**
* Generates a guid,
* http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*
* @return {String} guid value in string
*/
export default function getGuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
(c) => {
const r = Math.random() * 16 | 0; // tslint:disable-line:no-bitwise
const v = c === 'x' ? r : (r & 0x3 | 0x8); // tslint:disable-line:no-bitwise
return v.toString(16);
});
}

View File

@ -0,0 +1,142 @@
import { app } from 'electron';
import electronLog, { LogLevel, transports } from 'electron-log';
import * as path from 'path';
import getCmdLineArg from './get-command-line-args';
import { isElectronQA } from './mics';
import stringFormat from './string-format';
interface ILogMsg {
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
}
interface IClientLogMsg {
msgs?: ILogMsg[];
logLevel?: LogLevel;
showInConsole?: boolean;
}
const MAX_LOG_QUEUE_LENGTH = 100;
export class Logger {
private readonly showInConsole: boolean = false;
private readonly desiredLogLevel?: LogLevel;
private readonly logQueue: ILogMsg[];
private loggerWindow: Electron.WebContents | null;
constructor() {
this.loggerWindow = null;
this.logQueue = [];
if (!isElectronQA) {
transports.file.file = path.join(app.getPath('logs'), 'app.log');
transports.file.level = 'debug';
transports.file.format = '{h}:{i}:{s}:{ms} {text}';
transports.file.maxSize = 10 * 1024 * 1024;
transports.file.appName = 'Symphony';
}
const logLevel = getCmdLineArg(process.argv, '--logLevel=', false);
if (logLevel) {
const level = logLevel.split('=')[1];
if (level) {
this.desiredLogLevel = level as LogLevel;
}
}
if (getCmdLineArg(process.argv, '--enableConsoleLogging', false)) {
this.showInConsole = true;
}
}
public error(message: string, data?: object): void {
this.log('error', message, data);
}
public warn(message: string, data?: object): void {
this.log('warn', message, data);
}
public info(message: string, data?: object): void {
this.log('info', message, data);
}
public verbose(message: string, data?: object): void {
this.log('verbose', message, data);
}
public debug(message: string, data?: object): void {
this.log('debug', message, data);
}
public silly(message: string, data?: object): void {
this.log('silly', message, data);
}
public setLoggerWindow(window: Electron.WebContents): void {
this.loggerWindow = window;
if (this.loggerWindow) {
const logMsgs: IClientLogMsg = {};
if (this.logQueue.length) logMsgs.msgs = this.logQueue;
if (this.desiredLogLevel) logMsgs.logLevel = this.desiredLogLevel;
if (Object.keys(logMsgs).length) this.loggerWindow.send('log', logMsgs);
}
}
private log(logLevel: LogLevel, message: string, data?: object): void {
message = stringFormat(message, data);
if (!isElectronQA) {
switch (logLevel) {
case 'error': electronLog.error(message); break;
case 'warn': electronLog.warn(message); break;
case 'info': electronLog.info(message); break;
case 'verbose': electronLog.verbose(message); break;
case 'debug': electronLog.debug(message); break;
case 'silly': electronLog.silly(message); break;
default: electronLog.info(message);
}
}
this.sendToCloud(this.formatLogMsg(logLevel, message));
}
private formatLogMsg(level: LogLevel, details: any): ILogMsg {
return {
details,
level,
showInConsole: this.showInConsole,
startTime: Date.now(),
};
}
/**
* This will send the logs to the client if loggerWindow
* else adds the logs to a Queue
*
* @param logMsg {ILogMsg}
*/
private sendToCloud(logMsg: ILogMsg) {
// don't send logs if it is not desired by the user
if (this.desiredLogLevel && this.desiredLogLevel !== logMsg.level) {
return;
}
if (this.loggerWindow) {
this.loggerWindow.send('log', { msgs: [ logMsg ] });
} else {
this.logQueue.push(logMsg);
// don't store more than 100 msgs. keep most recent log msgs.
if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) {
this.logQueue.shift();
}
}
}
}
const logger = new Logger();
export {
logger,
};

View File

@ -1,5 +1,6 @@
export const isDevEnv = process.env.ELECTRON_DEV ?
process.env.ELECTRON_DEV.trim().toLowerCase() === 'true' : false;
export const isElectronQA = process.env.ELECTRON_QA;
export const isMac = (process.platform === 'darwin');
export const isWindowsOS = (process.platform === 'win32');

View File

@ -0,0 +1,30 @@
/**
* Injects content into string
* @param str {String}
* @param data {Object} - content to replace
*
* @example
* StringFormat(this will log {time}`, { time: '1234' })
*
* result:
* this will log 1234
*
* @return {*}
*/
export default function StringFormat(str: string, data?: object): string {
if (!str || !data) return str;
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
return str.replace(/({([^}]+)})/g, (i) => {
const replacedKey = i.replace(/{/, '').replace(/}/, '');
if (!data[replacedKey]) {
return i;
}
return data[replacedKey];
});
}
}
return str;
}

22
src/common/throttle.ts Normal file
View File

@ -0,0 +1,22 @@
/**
* limits your function to be called at most every milliseconds
*
* @param func
* @param wait
* @example const throttled = throttle(anyFunc, 500);
*/
export default function throttle(func: () => void, wait: number): () => void {
if (wait <= 0) {
throw Error('throttle: invalid throttleTime arg, must be a number: ' + wait);
}
let isCalled: boolean = false;
return (...args) => {
if (!isCalled) {
func(...args);
isCalled = true;
setTimeout(() => isCalled = false, wait);
}
};
}

0
src/renderer/preload.ts Normal file
View File