feat: ELECTRON-1357: show version info from pod (#703)

* ELECTRON-1357: implement getting dynamic version

* ELECTRON-1357: add support for more info window

* ELECTRON-1357: add unit tests for version handler

* ELECTRON-1357: delete unwanted files

* Delete .gulp-tsc-tmp-11966-41547-768xt5.masn2.ts

* Delete .gulp-tsc-tmp-11966-10574-1bhy8kw.k21y.ts

* ELECTRON-1357: caching server data

* ELECTRON-1357: refactor code to remove event emitter and use net module

* ELECTRON-1357: set version info asynchronously

* ELECTRON-1357: add checks for failed requests
This commit is contained in:
Vishwas Shashidhar 2019-07-08 20:35:57 +05:30 committed by GitHub
parent 9d887338e8
commit 11f69f47ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 491 additions and 88 deletions

View File

@ -20,7 +20,7 @@
"prebuild": "npm run compile && npm run rebuild && npm run browserify-preload",
"rebuild": "electron-rebuild -f",
"start": "npm run compile && npm run browserify-preload && cross-env ELECTRON_DEV=true electron .",
"test": "npm run lint && cross-env ELECTRON_QA=true jest --config jest.config.json --runInBand",
"test": "npm run lint && cross-env ELECTRON_QA=true jest --config jest.config.json --runInBand --detectOpenHandles",
"unpacked-mac": "npm run prebuild && npm run test && build --mac --dir",
"unpacked-win": "npm run prebuild && npm run test && build --win --x64 --dir",
"unpacked-win-x86": "npm run prebuild && npm run test && build --win --ia32 --dir"

View File

@ -16,7 +16,7 @@ exports[`about app should render correctly 1`] = `
<span
className="AboutApp-versionText"
>
Version 0-N/A ()
Version 0 ()
</span>
<span
className="AboutApp-copyrightText"

View File

@ -0,0 +1,158 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`more info should render correctly 1`] = `
<div
className="MoreInfo"
>
<span>
<b>
Version Information
</b>
</span>
<div
className="content"
>
<h4>
Symphony
</h4>
<table>
<tbody>
<tr>
<th>
Pod Version
</th>
<th>
SDA Version
</th>
</tr>
<tr>
<td>
N/A (N/A)
</td>
<td>
N/A (N/A)
</td>
</tr>
</tbody>
</table>
</div>
<div
className="content"
>
<h4>
Electron
</h4>
<span
className="MoreInfo-electron"
>
N/A
</span>
</div>
<div
className="content"
>
<h4>
v8
related
</h4>
<table>
<tbody>
<tr>
<th>
Chrome
</th>
<th>
v8
</th>
<th>
Node
</th>
</tr>
<tr>
<td>
N/A
</td>
<td>
N/A
</td>
<td>
N/A
</td>
</tr>
</tbody>
</table>
</div>
<div
className="content"
>
<h4>
Others
</h4>
<table>
<tbody>
<tr>
<th>
openssl
</th>
<th>
zlib
</th>
<th>
uv
</th>
<th>
ares
</th>
<th>
http_parser
</th>
</tr>
<tr>
<td>
N/A
</td>
<td>
N/A
</td>
<td>
N/A
</td>
<td>
N/A
</td>
<td>
N/A
</td>
</tr>
</tbody>
</table>
</div>
<div
className="content"
>
<h4>
Swift Search
</h4>
<table>
<tbody>
<tr>
<th>
Swift Search Version
</th>
<th>
API Version
</th>
</tr>
<tr>
<td>
N/A
</td>
<td>
N/A
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;

54
spec/moreInfo.spec.ts Normal file
View File

@ -0,0 +1,54 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import MoreInfo from '../src/renderer/components/more-info';
import { ipcRenderer } from './__mocks__/electron';
describe('more info', () => {
const moreInfoDataLabel = 'more-info-data';
const moreInfoDataMock = {
clientVersion: '1.55.2',
buildNumber: '12333',
sdaVersion: '3.8.0',
sdaBuildNumber: '0',
electronVersion: '3.1.11',
chromeVersion: '66.789',
v8Version: '6.7.8',
nodeVersion: '10.12',
openSslVersion: '1.2.3',
zlibVersion: '4.5.6',
uvVersion: '7.8',
aresVersion: '9.10',
httpParserVersion: '11.12',
swiftSearchVersion: '1.55.3-beta.1',
swiftSerchSupportedVersion: '1.55.3',
};
const onLabelEvent = 'on';
const removeListenerLabelEvent = 'removeListener';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(MoreInfo));
expect(wrapper).toMatchSnapshot();
});
it('should call `more-info-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(MoreInfo));
expect(spy).toBeCalledWith(moreInfoDataLabel, expect.any(Function));
});
it('should remove listener `more-info-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(MoreInfo));
expect(spyMount).toBeCalledWith(moreInfoDataLabel, expect.any(Function));
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(moreInfoDataLabel, expect.any(Function));
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(MoreInfo.prototype, 'setState');
shallow(React.createElement(MoreInfo));
ipcRenderer.send('more-info-data', moreInfoDataMock);
expect(spy).toBeCalledWith(moreInfoDataMock);
});
});

View File

@ -1,7 +1,6 @@
import { app } from 'electron';
import * as shellPath from 'shell-path';
import { buildNumber, clientVersion, version } from '../../package.json';
import { isDevEnv, isMac } from '../common/env';
import { logger } from '../common/logger';
import { getCommandLineArgs } from '../common/utils';
@ -13,6 +12,7 @@ import './dialog-handler';
import './main-api-handler';
import { handlePerformanceSettings } from './perf-handler';
import { protocolHandler } from './protocol-handler';
import { IVersionInfo, versionHandler } from './version-handler';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
logger.info(`App started with the args ${JSON.stringify(process.argv)}`);
@ -48,21 +48,29 @@ let isAppAlreadyOpen: boolean = false;
handlePerformanceSettings();
setChromeFlags();
// on windows, we create the protocol handler via the installer
// because electron leaves registry traces upon uninstallation
if (isMac) {
// Sets application version info that will be displayed in about app panel
app.setAboutPanelOptions({ applicationVersion: `${clientVersion}-${version}`, version: buildNumber });
}
// Electron sets the default protocol
app.setAsDefaultProtocolClient('symphony');
const setAboutPanel = (clientVersion: string, buildNumber: string) => {
const appName = app.getName();
const copyright = `Copyright \xA9 ${new Date().getFullYear()} ${appName}`;
app.setAboutPanelOptions({
applicationName: appName,
applicationVersion: clientVersion,
version: buildNumber,
copyright,
});
};
/**
* Main function that init the application
*/
const startApplication = async () => {
await app.whenReady();
versionHandler.getClientVersion()
.then((versionInfo: IVersionInfo) => {
setAboutPanel(versionInfo.clientVersion, versionInfo.buildNumber);
});
logger.info(`main: app is ready, performing initial checks`);
createAppCacheFile();
windowHandler.createApplication();

129
src/app/version-handler.ts Normal file
View File

@ -0,0 +1,129 @@
import { net } from 'electron';
import * as nodeURL from 'url';
import { buildNumber, clientVersion, optionalDependencies, searchAPIVersion, version } from '../../package.json';
import { logger } from '../common/logger';
import { config, IConfig } from './config-handler';
interface IVersionInfo {
clientVersion: string;
buildNumber: string;
sdaVersion: string;
sdaBuildNumber: string;
electronVersion: string;
chromeVersion: string;
v8Version: string;
nodeVersion: string;
openSslVersion: string;
zlibVersion: string;
uvVersion: string;
aresVersion: string;
httpParserVersion: string;
swiftSearchVersion: string;
swiftSerchSupportedVersion: string;
}
class VersionHandler {
private versionInfo: IVersionInfo;
private serverVersionInfo: any;
constructor() {
this.versionInfo = {
clientVersion,
buildNumber,
sdaVersion: version,
sdaBuildNumber: buildNumber,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
v8Version: process.versions.v8,
nodeVersion: process.versions.node,
openSslVersion: process.versions.openssl,
zlibVersion: process.versions.zlib,
uvVersion: process.versions.uv,
aresVersion: process.versions.ares,
httpParserVersion: process.versions.http_parser,
swiftSearchVersion: optionalDependencies['swift-search'],
swiftSerchSupportedVersion: searchAPIVersion,
};
}
/**
* Get Symphony version from the pod
*/
public getClientVersion(): Promise<IVersionInfo> {
return new Promise((resolve) => {
if (this.serverVersionInfo) {
this.versionInfo.clientVersion = this.serverVersionInfo['Implementation-Version'] || this.versionInfo.clientVersion;
this.versionInfo.buildNumber = this.serverVersionInfo['Implementation-Build'] || this.versionInfo.buildNumber;
resolve(this.versionInfo);
return;
}
const { url: podUrl }: IConfig = config.getGlobalConfigFields(['url']);
if (!podUrl) {
logger.error(`version-handler: Unable to get pod url for getting version data from server! Setting defaults!`);
resolve(this.versionInfo);
return;
}
const hostname = nodeURL.parse(podUrl).hostname;
const protocol = nodeURL.parse(podUrl).protocol;
const versionApiPath = '/webcontroller/HealthCheck/version/advanced';
const url = `${protocol}//${hostname}${versionApiPath}`;
logger.info(`version-handler: Trying to get version info for the URL: ${url}`);
const request = net.request(url);
request.on('response', (res) => {
let body: string = '';
res.on('data', (d: Buffer) => {
body += d;
});
res.on('end', () => {
try {
this.serverVersionInfo = JSON.parse(body)[0];
this.versionInfo.clientVersion = this.serverVersionInfo['Implementation-Version'] || this.versionInfo.clientVersion;
this.versionInfo.buildNumber = this.serverVersionInfo['Implementation-Build'] || this.versionInfo.buildNumber;
logger.info(`version-handler: Updated version info from server! ${JSON.stringify(this.versionInfo)}`);
resolve(this.versionInfo);
} catch (error) {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
}
});
res.on('error', (error) => {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
});
request.on('error', (error) => {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
request.on('close', () => {
logger.info(`version-handler: Request closed!!`);
});
request.on('finish', () => {
logger.info(`version-handler: Request finished!!`);
});
request.end();
});
}
}
const versionHandler = new VersionHandler();
export { versionHandler, IVersionInfo };

View File

@ -4,7 +4,6 @@ import * as fs from 'fs';
import * as path from 'path';
import { format, parse } from 'url';
import { buildNumber, clientVersion, version } from '../../package.json';
import { apiName, WindowTypes } from '../common/api-interface';
import { isDevEnv, isMac, isWindowsOS } from '../common/env';
import { i18n } from '../common/i18n';
@ -16,6 +15,8 @@ import { handleChildWindow } from './child-window-handler';
import { config, IConfig } from './config-handler';
import { SpellChecker } from './spell-check-handler';
import { checkIfBuildExpired } from './ttl-handler';
import DesktopCapturerSource = Electron.DesktopCapturerSource;
import { IVersionInfo, versionHandler } from './version-handler.js';
import { handlePermissionRequests, monitorWindowActions } from './window-actions';
import {
createComponentWindow,
@ -27,7 +28,6 @@ import {
preventWindowNavigation,
windowExists,
} from './window-utils';
import DesktopCapturerSource = Electron.DesktopCapturerSource;
interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions {
winKey: string;
@ -401,11 +401,12 @@ export class WindowHandler {
selectedParentWindow ? { parent: selectedParentWindow } : {},
);
this.aboutAppWindow.setVisibleOnAllWorkspaces(true);
this.aboutAppWindow.webContents.once('did-finish-load', () => {
this.aboutAppWindow.webContents.once('did-finish-load', async () => {
if (!this.aboutAppWindow || !windowExists(this.aboutAppWindow)) {
return;
}
this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion, version });
const { clientVersion, buildNumber }: IVersionInfo = await versionHandler.getClientVersion();
this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion });
});
}
@ -422,11 +423,12 @@ export class WindowHandler {
}
this.moreInfoWindow = createComponentWindow('more-info', { width: 550, height: 500 });
this.moreInfoWindow.webContents.once('did-finish-load', () => {
this.moreInfoWindow.webContents.once('did-finish-load', async () => {
if (!this.moreInfoWindow || !windowExists(this.moreInfoWindow)) {
return;
}
this.moreInfoWindow.webContents.send('more-info-data');
const versionInfo: IVersionInfo = await versionHandler.getClientVersion();
this.moreInfoWindow.webContents.send('more-info-data', versionInfo);
});
}

View File

@ -42,7 +42,7 @@ class Logger {
this.logPath = app.getPath('logs');
if (!isElectronQA) {
if (app.isPackaged) {
transports.file.file = path.join(this.logPath, `app_${Date.now()}.log`);
transports.file.level = 'debug';
transports.file.format = '{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}';

View File

@ -6,7 +6,6 @@ interface IState {
copyWrite?: string;
clientVersion: string;
buildNumber: string;
version: string;
}
/**
@ -20,7 +19,6 @@ export default class AboutApp extends React.Component<{}, IState> {
appName: 'Symphony',
buildNumber: '',
clientVersion: '0',
version: 'N/A',
};
this.updateState = this.updateState.bind(this);
}
@ -29,9 +27,9 @@ export default class AboutApp extends React.Component<{}, IState> {
* main render function
*/
public render(): JSX.Element {
const { clientVersion, version, buildNumber } = this.state;
const { clientVersion, buildNumber } = this.state;
const appName = remote.app.getName() || 'Symphony';
const versionString = `Version ${clientVersion}-${version} (${buildNumber})`;
const versionString = `Version ${clientVersion} (${buildNumber})`;
const copyright = `Copyright \xA9 ${new Date().getFullYear()} ${appName}`;
return (
<div className='AboutApp'>

View File

@ -1,83 +1,113 @@
import * as React from 'react';
import { optionalDependencies, searchAPIVersion } from '../../../package.json';
import { ipcRenderer } from 'electron';
import { i18n } from '../../common/i18n-preload';
interface ISSDataInterface {
supportedVersion?: string;
swiftSearchVersion?: string;
interface IState {
clientVersion: string;
buildNumber: string;
sdaVersion: string;
sdaBuildNumber: string;
electronVersion: string;
chromeVersion: string;
v8Version: string;
nodeVersion: string;
openSslVersion: string;
zlibVersion: string;
uvVersion: string;
aresVersion: string;
httpParserVersion: string;
swiftSearchVersion: string;
swiftSerchSupportedVersion: string;
}
const MORE_INFO_NAMESPACE = 'MoreInfo';
/**
* Returns process variable if the value is set
*/
const getSwiftSearchData = () => {
const swiftSearchInfo: ISSDataInterface = {
swiftSearchVersion: optionalDependencies['swift-search'],
supportedVersion: searchAPIVersion,
};
return swiftSearchInfo;
};
/**
* Window that display app version and copyright info
*/
export default class MoreInfo extends React.PureComponent {
export default class MoreInfo extends React.Component<{}, IState> {
/**
* Render Swift-Search version details
*/
public static renderSwiftSearchInfo(): JSX.Element | null {
const { swiftSearchVersion, supportedVersion }: ISSDataInterface = getSwiftSearchData() || {};
if (!swiftSearchVersion || !supportedVersion) {
return null;
}
return (
<div className='content'>
<h4>Swift Search</h4>
<table>
<tbody>
<tr>
<th>{i18n.t('Swift Search Version', MORE_INFO_NAMESPACE)()}</th>
<th>{i18n.t('API Version', MORE_INFO_NAMESPACE)()}</th>
</tr>
<tr>
<td>{swiftSearchVersion || 'N/A'}</td>
<td>{supportedVersion || 'N/A'}</td>
</tr>
</tbody>
</table>
</div>
);
constructor(props) {
super(props);
this.state = {
clientVersion: 'N/A',
buildNumber: 'N/A',
sdaVersion: 'N/A',
sdaBuildNumber: 'N/A',
electronVersion: 'N/A',
chromeVersion: 'N/A',
v8Version: 'N/A',
nodeVersion: 'N/A',
openSslVersion: 'N/A',
zlibVersion: 'N/A',
uvVersion: 'N/A',
aresVersion: 'N/A',
httpParserVersion: 'N/A',
swiftSearchVersion: 'N/A',
swiftSerchSupportedVersion: 'N/A',
};
this.updateState = this.updateState.bind(this);
}
public componentDidMount(): void {
ipcRenderer.on('more-info-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('more-info-data', this.updateState);
}
/**
* main render function
*/
public render(): JSX.Element {
const { clientVersion, buildNumber,
sdaVersion, sdaBuildNumber,
electronVersion, chromeVersion, v8Version,
nodeVersion, openSslVersion, zlibVersion,
uvVersion, aresVersion, httpParserVersion,
swiftSearchVersion, swiftSerchSupportedVersion,
} = this.state;
const podVersion = `${clientVersion} (${buildNumber})`;
const sdaVersionBuild = `${sdaVersion} (${sdaBuildNumber})`;
return (
<div className='MoreInfo'>
<span><b>{i18n.t('Version Information', MORE_INFO_NAMESPACE)()}</b></span>
<div className='content'>
<h4>Symphony</h4>
<table>
<tbody>
<tr>
<th>Pod Version</th>
<th>SDA Version</th>
</tr>
<tr>
<td>{podVersion || 'N/A'}</td>
<td>{sdaVersionBuild || 'N/A'}</td>
</tr>
</tbody>
</table>
</div>
<div className='content'>
<h4>Electron</h4>
<span className='MoreInfo-electron'>{process.versions.electron || 'N/A'}</span>
<span className='MoreInfo-electron'>{electronVersion || 'N/A'}</span>
</div>
<div className='content'>
<h4>v8 {i18n.t('related', MORE_INFO_NAMESPACE)()}</h4>
<table>
<tbody>
<tr>
<th>Chrome</th>
<th>v8</th>
<th>Node</th>
</tr>
<tr>
<td>{process.versions.chrome || 'N/A'}</td>
<td>{process.versions.v8 || 'N/A'}</td>
<td>{process.versions.node || 'N/A'}</td>
</tr>
<tr>
<th>Chrome</th>
<th>v8</th>
<th>Node</th>
</tr>
<tr>
<td>{chromeVersion || 'N/A'}</td>
<td>{v8Version || 'N/A'}</td>
<td>{nodeVersion || 'N/A'}</td>
</tr>
</tbody>
</table>
</div>
@ -85,25 +115,49 @@ export default class MoreInfo extends React.PureComponent {
<h4>{i18n.t('Others', MORE_INFO_NAMESPACE)()}</h4>
<table>
<tbody>
<tr>
<th>openssl</th>
<th>zlib</th>
<th>uv</th>
<th>ares</th>
<th>http_parser</th>
</tr>
<tr>
<td>{process.versions.openssl || 'N/A'}</td>
<td>{process.versions.zlib || 'N/A'}</td>
<td>{process.versions.uv || 'N/A'}</td>
<td>{process.versions.ares || 'N/A'}</td>
<td>{process.versions.http_parser || 'N/A'}</td>
</tr>
<tr>
<th>openssl</th>
<th>zlib</th>
<th>uv</th>
<th>ares</th>
<th>http_parser</th>
</tr>
<tr>
<td>{openSslVersion || 'N/A'}</td>
<td>{zlibVersion || 'N/A'}</td>
<td>{uvVersion || 'N/A'}</td>
<td>{aresVersion || 'N/A'}</td>
<td>{httpParserVersion || 'N/A'}</td>
</tr>
</tbody>
</table>
</div>
<div className='content'>
<h4>Swift Search</h4>
<table>
<tbody>
<tr>
<th>{i18n.t('Swift Search Version', MORE_INFO_NAMESPACE)()}</th>
<th>{i18n.t('API Version', MORE_INFO_NAMESPACE)()}</th>
</tr>
<tr>
<td>{swiftSearchVersion || 'N/A'}</td>
<td>{swiftSerchSupportedVersion || 'N/A'}</td>
</tr>
</tbody>
</table>
</div>
{MoreInfo.renderSwiftSearchInfo()}
</div>
);
}
/**
* Sets the About app state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}