Implemented unified log extraction

This commit is contained in:
Mattias Gustavsson 2019-11-27 14:04:58 +01:00
parent 80ff046b32
commit 604caedd69
7 changed files with 419 additions and 4 deletions

View File

@ -8,6 +8,7 @@ import { analytics } from './analytics-handler';
import { config } from './config-handler';
import { memoryMonitor } from './memory-monitor';
import { protocolHandler } from './protocol-handler';
import { finalizeLogExports, registerLogRetriever } from './reports-handler';
import { screenSnippet } from './screen-snippet-handler';
import { activate, handleKeyPress } from './window-actions';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
@ -53,6 +54,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.IpcMainEvent, arg: IApiArgs) =>
// we make use of it and update the pod version info on SDA
windowHandler.updateVersionInfo();
break;
case apiCmds.registerLogRetriever:
registerLogRetriever( event.sender, arg.logName );
break;
case apiCmds.logReceiver:
finalizeLogExports( arg.logs );
break;
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
setDataUrl(arg.dataUrl, arg.count);

View File

@ -4,6 +4,7 @@ import * as electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { ILogs } from '../common/api-interface';
import { isLinux, isMac } from '../common/env';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
@ -17,19 +18,30 @@ import { logger } from '../common/logger';
* @param fileExtensions {Array} array of file ext
* @return {Promise<void>}
*/
const generateArchiveForDirectory = (source: string, destination: string, fileExtensions: string[]): Promise<void> => {
const generateArchiveForDirectory = (source: string, destination: string, fileExtensions: string[], retrievedLogs: ILogs[]): Promise<void> => {
return new Promise((resolve, reject) => {
logger.info(`reports-handler: generating archive for directory ${source}`);
const output = fs.createWriteStream(destination);
const archive = archiver('zip', { zlib: { level: 9 } });
const filesForCleanup: string[] = [];
output.on('close', () => {
for (const file of filesForCleanup) {
if (fs.existsSync( file )) {
fs.unlinkSync(file);
}
}
logger.info(`reports-handler: generated archive for directory ${source}`);
return resolve();
});
archive.on('error', (err: Error) => {
for (const file of filesForCleanup) {
if (fs.existsSync( file )) {
fs.unlinkSync(file);
}
}
logger.error(`reports-handler: error archiving directory for ${source} with error ${err}`);
return reject(err);
});
@ -53,17 +65,45 @@ const generateArchiveForDirectory = (source: string, destination: string, fileEx
}
});
for (const logs of retrievedLogs) {
for (const logFile of logs.logFiles) {
const file = path.join( source, logFile.filename );
fs.writeFileSync(file, logFile.contents );
archive.file(file, { name: 'logs/' + logFile.filename });
filesForCleanup.push(file);
}
}
archive.finalize();
});
};
interface ILogRetriever {
sender: Electron.WebContents;
logName: string;
}
const logRetrievers: ILogRetriever[] = [];
const receivedLogs: ILogs[] = [];
export const registerLogRetriever = ( sender: Electron.WebContents, logName: string): void => {
logRetrievers.push( { sender, logName } );
};
export const collectLogs = (): void => {
receivedLogs.length = 0;
for (const logRetriever of logRetrievers ) {
logRetriever.sender.send('collect-logs', logRetriever.logName );
}
};
/**
* Compress and export logs stored under system log directory
*
* MacOS - /Library/Logs/Symphony/
* Windows - AppData\Roaming\Symphony\logs
*/
export const exportLogs = (): void => {
export const packageLogs = (retrievedLogs: ILogs[]): void => {
const FILE_EXTENSIONS = [ '.log' ];
const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
const LINUX_LOGS_PATH = '/.config/Symphony/';
@ -86,7 +126,7 @@ export const exportLogs = (): void => {
const timestamp = new Date().getTime();
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS, retrievedLogs)
.then(() => {
shell.showItemInFolder(destination);
})
@ -102,6 +142,36 @@ export const exportLogs = (): void => {
});
};
export const finalizeLogExports = (logs: ILogs) => {
receivedLogs.push(logs);
let allReceived = true;
for (const logRetriever of logRetrievers ) {
let found = false;
for (const log of receivedLogs) {
if (log.logName === logRetriever.logName) {
found = true;
}
}
if (!found) {
allReceived = false;
}
}
if (allReceived) {
packageLogs(receivedLogs);
receivedLogs.length = 0;
}
};
export const exportLogs = (): void => {
if ( logRetrievers.length > 0) {
collectLogs();
} else {
packageLogs([]);
}
};
/**
* Compress and export crash dump stored under system crashes directory
*/
@ -125,7 +195,7 @@ export const exportCrashDumps = (): void => {
const destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS)
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS, [])
.then(() => {
electron.shell.showItemInFolder(destination);
})

View File

@ -7,6 +7,8 @@ export enum apiCmds {
activate = 'activate',
registerBoundsChange = 'register-bounds-change',
registerProtocolHandler = 'register-protocol-handler',
registerLogRetriever = 'register-log-retriever',
logReceiver = 'log-receiver',
registerAnalyticsHandler = 'register-analytics-handler',
registerActivityDetection = 'register-activity-detection',
showNotificationSettings = 'show-notification-settings',
@ -61,6 +63,8 @@ export interface IApiArgs {
displayId: string;
path: string;
type: string;
logName: string;
logs: ILogs;
}
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings';
@ -144,3 +148,13 @@ export interface ILogMsg {
}
export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly';
export interface ILogFile {
filename: string;
contents: string;
}
export interface ILogs {
logName: string;
logFiles: ILogFile[];
}

View File

@ -105,6 +105,16 @@
<p>snippet output:</p>
<image id='snippet-img'/>
<hr>
<p>RTC Logs:</p>
Filename <input type='text' id='rtc-log-filename' value='rtc_log.log'/>
<p>Contents</p>
<textarea id="rtc-log-contents" rows="4">Test log contents</textarea>
<p/>
Filename <input type='text' id='rtc-log-filename2' value=''/>
<p>Contents</p>
<textarea id="rtc-log-contents2" rows="4"></textarea>
<hr>
<p>Window activate:</p>
<button id='open-win'>open window</button>
@ -155,6 +165,8 @@
activate: 'activate',
registerBoundsChange: 'register-bounds-change',
registerProtocolHandler: 'register-protocol-handler',
registerLogRetriever: 'register-log-retriever',
logReceiver: 'log-receiver',
registerAnalyticHandler: 'register-analytic-handler',
registerActivityDetection: 'register-activity-detection',
showNotificationSettings: 'show-notification-settings',
@ -408,9 +420,33 @@
window.postMessage({ method, data }, '*');
};
const getRTCLogs = () => {
if (document.getElementById('rtc-log-filename2').value == '') {
const filename = document.getElementById('rtc-log-filename').value;
const contents = document.getElementById('rtc-log-contents').value;
return [ { filename, contents }, ];
} else {
const filename1 = document.getElementById('rtc-log-filename').value;
const contents1 = document.getElementById('rtc-log-contents').value;
const filename2 = document.getElementById('rtc-log-filename2').value;
const contents2 = document.getElementById('rtc-log-contents2').value;
return [ { filename: filename1, contents: contents1 },
{ filename: filename2, contents: contents2 }, ];
}
}
window.addEventListener('message', (event) => {
const { data, method } = event.data;
switch (method) {
case 'collect-logs':
if( data === 'RTC' ) {
if (window.ssf) {
window.ssf.logReceiver('RTC', getRTCLogs());
} else {
postMessage( apiCmds.logReceiver, { logName: 'RTC', logFiles: getRTCLogs() } );
}
}
break;
case 'activity-callback':
activityCallback(data);
break;
@ -437,6 +473,15 @@
}
});
/**
* Log retriever
*/
if (window.ssf) {
window.ssf.registerLogRetriever('RTC');
} else {
postMessage(apiCmds.registerLogRetriever, 'RTC' );
}
/**
* Analytic events
*/

View File

@ -49,6 +49,7 @@ export class AppBridge {
onRegisterLoggerCallback: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) =>
this.registerLoggerCallback(msg, logLevel, showInConsole),
onRegisterProtocolHandlerCallback: (uri: string) => this.protocolHandlerCallback(uri),
onCollectLogsCallback: (logName: string) => this.collectLogsCallback(logName),
onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) => this.screenSharingIndicatorCallback(arg),
onMediaSourceCallback: (
error: IScreenSourceError | null,
@ -122,6 +123,12 @@ export class AppBridge {
case apiCmds.registerProtocolHandler:
ssf.registerProtocolHandler(this.callbackHandlers.onRegisterProtocolHandlerCallback);
break;
case apiCmds.registerLogRetriever:
ssf.registerLogRetriever(this.callbackHandlers.onCollectLogsCallback, data);
break;
case apiCmds.logReceiver:
ssf.logReceiver( data.logName, data.logFiles );
break;
case apiCmds.openScreenSharingIndicator:
ssf.openScreenSharingIndicator(data as IScreenSharingIndicatorOptions, this.callbackHandlers.onScreenSharingIndicatorCallback);
break;
@ -190,6 +197,8 @@ export class AppBridge {
*/
private protocolHandlerCallback = (uri: string): void => this.broadcastMessage('protocol-callback', uri);
private collectLogsCallback = (logName: string): void => this.broadcastMessage('collect-logs',logName);
/**
* Broadcast event that stops screen sharing
* @param arg {IScreenSharingIndicator}

View File

@ -38,6 +38,7 @@ export interface ILocalObject {
boundsChangeCallback?: (arg: IBoundsChange) => void;
screenSharingIndicatorCallback?: (arg: IScreenSharingIndicator) => void;
protocolActionCallback?: (arg: string) => void;
collectLogsCallback?: (arg: string) => void;
analyticsEventHandler?: (arg: any) => void;
}
@ -263,6 +264,33 @@ export class SSFApi {
}
}
/**
* Allows JS to register a log retriever that can be used by the
* electron main process to retrieve current logs.
*/
public registerLogRetriever(collectLogs: (logName: string) => void, logName: string): void {
if (typeof collectLogs === 'function') {
local.collectLogsCallback = collectLogs;
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.registerLogRetriever,
logName,
});
}
}
/**
* Send log files to main process when requested.
*/
public logReceiver(logName: string, logFiles): void {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.logReceiver,
logs: { logName, logFiles },
});
}
/**
* Allows JS to register analytics event handler
* to pass analytics event data
@ -490,6 +518,12 @@ local.ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet)
}
});
local.ipcRenderer.on('collect-logs', ( _event: Event, arg: string ) => {
if (typeof local.collectLogsCallback === 'function') {
local.collectLogsCallback(arg);
}
});
/**
* An event triggered by the main process
* for ever few minutes if the user is active

View File

@ -0,0 +1,236 @@
diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts
index fd274b1..3df2dd8 100644
--- a/src/app/main-api-handler.ts
+++ b/src/app/main-api-handler.ts
@@ -53,6 +53,13 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.IpcMainEvent, arg: IApiArgs) =>
// we make use of it and update the pod version info on SDA
windowHandler.updateVersionInfo();
break;
+ case apiCmds.registerLogRetriever:
+ logger.info( 'register-log-retriever: ' + JSON.stringify( arg ) );
+ event.sender.send('collect-logs');
+ break;
+ case apiCmds.logReceiver:
+ logger.info( 'log-receiver: ' + JSON.stringify( arg ) );
+ break;
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
setDataUrl(arg.dataUrl, arg.count);
diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts
index 3c46915..a481438 100644
--- a/src/common/api-interface.ts
+++ b/src/common/api-interface.ts
@@ -7,6 +7,8 @@ export enum apiCmds {
activate = 'activate',
registerBoundsChange = 'register-bounds-change',
registerProtocolHandler = 'register-protocol-handler',
+ registerLogRetriever = 'register-log-retriever',
+ logReceiver = 'log-receiver',
registerAnalyticsHandler = 'register-analytics-handler',
registerActivityDetection = 'register-activity-detection',
showNotificationSettings = 'show-notification-settings',
diff --git a/src/demo/index.html b/src/demo/index.html
index bb9961a..0063cf2 100644
--- a/src/demo/index.html
+++ b/src/demo/index.html
@@ -105,6 +105,16 @@
<p>snippet output:</p>
<image id='snippet-img'/>

+ <hr>
+ <p>RTC Logs:</p>
+ Filename <input type='text' id='rtc-log-filename' value='rtc_log'/>
+ <p>Contents</p>
+ <textarea id="rtc-log-contents" rows="4">Test log contents</textarea>
+
+ Filename <input type='text' id='rtc-log-filename2' value=''/>
+ <p>Contents</p>
+ <textarea id="rtc-log-contents2" rows="4"></textarea>
+
<hr>
<p>Window activate:</p>
<button id='open-win'>open window</button>
@@ -155,6 +165,8 @@
activate: 'activate',
registerBoundsChange: 'register-bounds-change',
registerProtocolHandler: 'register-protocol-handler',
+ registerLogRetriever: 'register-log-retriever',
+ logReceiver: 'log-receiver',
registerAnalyticHandler: 'register-analytic-handler',
registerActivityDetection: 'register-activity-detection',
showNotificationSettings: 'show-notification-settings',
@@ -408,9 +420,28 @@
window.postMessage({ method, data }, '*');
};

+ const getRTCLogs = () => {
+ if (document.getElementById('rtc-log-filename2').value == '') {
+ const filename = document.getElementById('rtc-log-filename').value;
+ const contents = document.getElementById('rtc-log-contents').value;
+ return { name: 'RTC', logs: [ { filename, contents }, ] };
+ } else {
+ const filename1 = document.getElementById('rtc-log-filename').value;
+ const contents1 = document.getElementById('rtc-log-contents').value;
+ const filename2 = document.getElementById('rtc-log-filename2').value;
+ const contents2 = document.getElementById('rtc-log-contents2').value;
+ return {name: 'RTC', logs: [ { filename: filename1, contents: contents1 }, 
+ { filename: filename2, contents: contents2 }, ] };
+ }
+ }
+
window.addEventListener('message', (event) => {
const { data, method } = event.data;
+ alert( 'EVENT ' + JSON.stringify( event.data ) );
switch (method) {
+ case 'collect-logs':
+ broadcastMessage( apiCmds.logReceiver, getRTCLogs() );
+ break;
case 'activity-callback':
activityCallback(data);
break;
@@ -437,6 +468,17 @@
}
});

+ /**
+ * Log retriever
+ */
+ window.addEventListener( 'collect-logs', () => alert('send-logs') );
+
+ if (window.ssf) {
+ window.ssf.registerLogRetriever('RTC');
+ } else {
+ postMessage(apiCmds.registerLogRetriever, 'RTC' );
+ }
+
/**
* Analytic events
*/
diff --git a/src/renderer/app-bridge.ts b/src/renderer/app-bridge.ts
index 3b8dda1..eef4fe6 100644
--- a/src/renderer/app-bridge.ts
+++ b/src/renderer/app-bridge.ts
@@ -49,6 +49,7 @@ export class AppBridge {
onRegisterLoggerCallback: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) =>
this.registerLoggerCallback(msg, logLevel, showInConsole),
onRegisterProtocolHandlerCallback: (uri: string) => this.protocolHandlerCallback(uri),
+ onCollectLogsCallback: () => this.collectLogsCallback( ),
onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) => this.screenSharingIndicatorCallback(arg),
onMediaSourceCallback: (
error: IScreenSourceError | null,
@@ -64,6 +65,7 @@ export class AppBridge {
const currentWindow = remote.getCurrentWindow();
// @ts-ignore
this.origin = currentWindow.origin || '';
+ this.origin = '*';
if (ssInstance && typeof ssInstance.setBroadcastMessage === 'function') {
ssInstance.setBroadcastMessage(this.broadcastMessage);
}
@@ -122,6 +124,12 @@ export class AppBridge {
case apiCmds.registerProtocolHandler:
ssf.registerProtocolHandler(this.callbackHandlers.onRegisterProtocolHandlerCallback);
break;
+ case apiCmds.registerLogRetriever:
+ ssf.registerLogRetriever(this.callbackHandlers.onCollectLogsCallback, data);
+ break;
+ case apiCmds.logReceiver:
+ ssf.logReceiver( data );
+ break;
case apiCmds.openScreenSharingIndicator:
ssf.openScreenSharingIndicator(data as IScreenSharingIndicatorOptions, this.callbackHandlers.onScreenSharingIndicatorCallback);
break;
@@ -190,6 +198,12 @@ export class AppBridge {
*/
private protocolHandlerCallback = (uri: string): void => this.broadcastMessage('protocol-callback', uri);

+ /**
+ * Broadcast protocol uri
+ * @param uri {string}
+ */
+ private collectLogsCallback = (): void => this.broadcastMessage('collect-logs',null);
+
/**
* Broadcast event that stops screen sharing
* @param arg {IScreenSharingIndicator}
diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts
index 7d698a8..e00bc96 100644
--- a/src/renderer/ssf-api.ts
+++ b/src/renderer/ssf-api.ts
@@ -38,6 +38,7 @@ export interface ILocalObject {
boundsChangeCallback?: (arg: IBoundsChange) => void;
screenSharingIndicatorCallback?: (arg: IScreenSharingIndicator) => void;
protocolActionCallback?: (arg: string) => void;
+ collectLogsCallback?: () => void;
analyticsEventHandler?: (arg: any) => void;
}

@@ -263,6 +264,56 @@ export class SSFApi {
}
}

+ /**
+ * Allows JS to register a protocol handler that can be used by the
+ * electron main process.
+ *
+ * @param protocolHandler {Function} callback will be called when app is
+ * invoked with registered protocol (e.g., symphony). The callback
+ * receives a single string argument: full uri that the app was
+ * invoked with e.g., symphony://?streamId=xyz123&streamType=chatroom
+ *
+ * Note: this function should only be called after client app is fully
+ * able for protocolHandler callback to be invoked. It is possible
+ * the app was started using protocol handler, in this case as soon as
+ * this registration func is invoked then the protocolHandler callback
+ * will be immediately called.
+ */
+ public registerLogRetriever(collectLogs: () => void, name: string): void {
+ if (typeof collectLogs === 'function') {
+
+ local.collectLogsCallback = collectLogs;
+
+ local.ipcRenderer.send(apiName.symphonyApi, {
+ cmd: apiCmds.registerLogRetriever,
+ name,
+ });
+
+ }
+ }
+
+ /**
+ * Allows JS to register a protocol handler that can be used by the
+ * electron main process.
+ *
+ * @param protocolHandler {Function} callback will be called when app is
+ * invoked with registered protocol (e.g., symphony). The callback
+ * receives a single string argument: full uri that the app was
+ * invoked with e.g., symphony://?streamId=xyz123&streamType=chatroom
+ *
+ * Note: this function should only be called after client app is fully
+ * able for protocolHandler callback to be invoked. It is possible
+ * the app was started using protocol handler, in this case as soon as
+ * this registration func is invoked then the protocolHandler callback
+ * will be immediately called.
+ */
+ public logReceiver(logs): void {
+ local.ipcRenderer.send(apiName.symphonyApi, {
+ cmd: apiCmds.logReceiver,
+ logs,
+ });
+ }
+
/**
* Allows JS to register analytics event handler
* to pass analytics event data
@@ -490,6 +541,12 @@ local.ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet)
}
});

+local.ipcRenderer.on('collect-logs', () => {
+ if (typeof local.collectLogsCallback === 'function') {
+ local.collectLogsCallback();
+ }
+});
+
/**
* An event triggered by the main process
* for ever few minutes if the user is active