SDA-4770 Adaptations on openfin integration (#2268)

* SDA-4770 Adaptations on openfin integration

* SDA-4770 Adaptations on openfin integration
This commit is contained in:
Salah Benmoussati 2025-01-22 12:18:38 +01:00 committed by GitHub
parent 1b83b36f55
commit c9326fd5f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 197 additions and 63 deletions

View File

@ -52,6 +52,8 @@
"uuid": "", "uuid": "",
"licenseKey": "", "licenseKey": "",
"runtimeVersion": "", "runtimeVersion": "",
"autoConnect": false "autoConnect": false,
"channelName": "",
"connectionTimeout": 10000
} }
} }

View File

@ -126,6 +126,7 @@ if [ "$EUID" -ne 0 ]; then
defaults write "$plistFilePath" uuid -string "" defaults write "$plistFilePath" uuid -string ""
defaults write "$plistFilePath" licenseKey -string "" defaults write "$plistFilePath" licenseKey -string ""
defaults write "$plistFilePath" runtimeVersion -string "" defaults write "$plistFilePath" runtimeVersion -string ""
defaults write "$plistFilePath" channelName -string ""
defaults write "$plistFilePath" autoConnect -bool false defaults write "$plistFilePath" autoConnect -bool false
else else
sudo -u "$userName" defaults write "$plistFilePath" url -string "$pod_url" sudo -u "$userName" defaults write "$plistFilePath" url -string "$pod_url"
@ -175,6 +176,7 @@ else
sudo -u "$userName" defaults write "$plistFilePath" uuid -string "" sudo -u "$userName" defaults write "$plistFilePath" uuid -string ""
sudo -u "$userName" defaults write "$plistFilePath" licenseKey -string "" sudo -u "$userName" defaults write "$plistFilePath" licenseKey -string ""
sudo -u "$userName" defaults write "$plistFilePath" runtimeVersion -string "" sudo -u "$userName" defaults write "$plistFilePath" runtimeVersion -string ""
sudo -u "$userName" defaults write "$plistFilePath" channelName -string ""
sudo -u "$userName" defaults write "$plistFilePath" autoConnect -bool false sudo -u "$userName" defaults write "$plistFilePath" autoConnect -bool false
fi fi

View File

@ -54,6 +54,8 @@ jest.mock('../src/app/config-handler', () => {
uuid: 'some-uuid', uuid: 'some-uuid',
licenseKey: 'some-license-key', licenseKey: 'some-license-key',
runtimeVersion: 'some-runtime-version', runtimeVersion: 'some-runtime-version',
channelName: 'some-channel-name',
connectionTimeout: '1000',
}, },
}; };
}), }),

View File

@ -25,6 +25,8 @@ jest.mock('../src/app/config-handler', () => ({
uuid: 'mock-uuid', uuid: 'mock-uuid',
licenseKey: 'mock-license', licenseKey: 'mock-license',
runtimeVersion: 'mock-version', runtimeVersion: 'mock-version',
channelName: 'mock-channel',
connectionTimeout: '10000',
}, },
})), })),
}, },
@ -43,13 +45,16 @@ describe('Openfin', () => {
beforeAll(async () => { beforeAll(async () => {
connectMock = await connect({} as any); connectMock = await connect({} as any);
}); });
beforeEach(() => {
jest.clearAllMocks();
});
it('should not be connected', () => { it('should not be connected', () => {
const info = openfinHandler.getInfo(); const info = openfinHandler.getInfo();
const isConnected = openfinHandler.getConnectionStatus(); const connectionStatus = openfinHandler.getConnectionStatus();
expect(info.isConnected).toBeFalsy(); expect(info.isConnected).toBeFalsy();
expect(isConnected).toBeFalsy(); expect(connectionStatus.isConnected).toBeFalsy();
}); });
it('should connect', async () => { it('should connect', async () => {
@ -65,6 +70,43 @@ describe('Openfin', () => {
expect(isConnected).toBeTruthy(); expect(isConnected).toBeTruthy();
}); });
it('should reject and return false if connection times out', async () => {
jest.useFakeTimers();
const connectSyncSpy = jest
.spyOn(connectMock.Interop, 'connectSync')
.mockImplementationOnce((_channelName) => {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 12000);
});
});
const connectionTimeoutSpy = jest.spyOn(global, 'setTimeout');
let connectionStatus;
const connectPromise = openfinHandler.connect();
const resultPromise = connectPromise.then((res) => {
connectionStatus = res;
});
jest.advanceTimersByTime(10000);
expect(connectionStatus).toBeUndefined();
await resultPromise;
expect(connectionStatus.isConnected).toBe(false);
expect(connectionTimeoutSpy).toHaveBeenCalledTimes(2);
expect(connectionTimeoutSpy.mock.calls[0][1]).toBeGreaterThanOrEqual(10000);
expect(connectSyncSpy).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
it('should fire an intent', async () => { it('should fire an intent', async () => {
const connectSyncMock = await connectMock.Interop.connectSync(); const connectSyncMock = await connectMock.Interop.connectSync();
const fireIntentSpy = jest.spyOn(connectSyncMock, 'fireIntent'); const fireIntentSpy = jest.spyOn(connectSyncMock, 'fireIntent');

View File

@ -95,7 +95,9 @@ describe('Plist Handler', () => {
autoConnect: undefined, autoConnect: undefined,
licenseKey: undefined, licenseKey: undefined,
runtimeVersion: undefined, runtimeVersion: undefined,
channelName: undefined,
uuid: undefined, uuid: undefined,
connectionTimeout: undefined,
}, },
}); });
}); });

View File

@ -168,7 +168,9 @@ export interface IOpenfin {
uuid: string; uuid: string;
licenseKey: string; licenseKey: string;
runtimeVersion: string; runtimeVersion: string;
channelName: string;
autoConnect: boolean; autoConnect: boolean;
connectionTimeout: string;
} }
class Config { class Config {

View File

@ -560,18 +560,12 @@ ipcMain.on(
helpMenu.setValue(helpCenter); helpMenu.setValue(helpCenter);
break; break;
case apiCmds.openfinConnect:
openfinHandler.connect();
break;
case apiCmds.openfinFireIntent: case apiCmds.openfinFireIntent:
openfinHandler.fireIntent(arg.intent); openfinHandler.fireIntent(arg.intent);
break; break;
case apiCmds.openfinJoinContextGroup: case apiCmds.openfinJoinContextGroup:
openfinHandler.joinContextGroup(arg.contextGroupId, arg.target); openfinHandler.joinContextGroup(arg.contextGroupId, arg.target);
break; break;
case apiCmds.openfinRegisterIntentHandler:
openfinHandler.registerIntentHandler(arg.intentName);
break;
case apiCmds.openfinUnregisterIntentHandler: case apiCmds.openfinUnregisterIntentHandler:
openfinHandler.unregisterIntentHandler(arg.intentName); openfinHandler.unregisterIntentHandler(arg.intentName);
break; break;
@ -643,6 +637,10 @@ ipcMain.handle(
return getContentWindowHandle(windowHandle); return getContentWindowHandle(windowHandle);
} }
break; break;
case apiCmds.openfinConnect:
return openfinHandler.connect();
case apiCmds.openfinRegisterIntentHandler:
return openfinHandler.registerIntentHandler(arg.intentName);
case apiCmds.openfinGetConnectionStatus: case apiCmds.openfinGetConnectionStatus:
return openfinHandler.getConnectionStatus(); return openfinHandler.getConnectionStatus();
case apiCmds.openfinGetInfo: case apiCmds.openfinGetInfo:

View File

@ -1,43 +1,95 @@
import { connect } from '@openfin/node-adapter'; import { connect } from '@openfin/node-adapter';
import { randomUUID, UUID } from 'crypto';
import { logger } from '../common/openfin-logger'; import { logger } from '../common/openfin-logger';
import { config, IConfig } from './config-handler'; import { config, IConfig } from './config-handler';
import { windowHandler } from './window-handler'; import { windowHandler } from './window-handler';
const OPENFIN_PROVIDER = 'Openfin';
const TIMEOUT_THRESHOLD = 10000;
export class OpenfinHandler { export class OpenfinHandler {
private interopClient; private interopClient;
private intentHandlerSubscriptions = new Map(); private intentHandlerSubscriptions: Map<UUID, any> = new Map();
private isConnected: boolean = false; private isConnected: boolean = false;
private fin: any;
/** /**
* Connection to interop brocker * Connection to interop brocker
*/ */
public async connect() { public async connect() {
logger.info('openfin-handler: connecting');
const { openfin }: IConfig = config.getConfigFields(['openfin']); const { openfin }: IConfig = config.getConfigFields(['openfin']);
if (openfin) { if (!openfin) {
const fin = await connect({ logger.error('openfin-handler: missing openfin params to connect.');
uuid: openfin.uuid, return { isConnected: false };
licenseKey: openfin.licenseKey, }
runtime: { logger.info('openfin-handler: connecting');
version: openfin.runtimeVersion, const parsedTimeoutValue = parseInt(openfin.connectionTimeout, 10);
}, const timeoutValue = isNaN(parsedTimeoutValue)
}); ? TIMEOUT_THRESHOLD
logger.info('openfin-handler: connected'); : parsedTimeoutValue;
logger.info('openfin-handler: connecting to interop broker'); const connectionTimeoutPromise = new Promise((_, reject) =>
this.interopClient = fin.Interop.connectSync( setTimeout(() => {
'workspace-platform-starter', logger.error(
); `openfin-handler: Connection timeout after ${
this.isConnected = true; timeoutValue / 1000
this.interopClient.onDisconnection((event) => { } seconds`,
const { brokerName } = event; );
logger.warn( return reject(
`openfin-handler: Disconnected from Interop Broker ${brokerName} `, new Error(`Connection timeout after ${timeoutValue / 1000} seconds`),
); );
this.clearSubscriptions(); }, timeoutValue),
}); );
return;
const connectionPromise = (async () => {
try {
if (!this.fin) {
this.fin = await connect({
uuid: openfin.uuid,
licenseKey: openfin.licenseKey,
runtime: {
version: openfin.runtimeVersion,
},
});
}
logger.info(
'openfin-handler: connection established to Openfin runtime',
);
logger.info(
`openfin-handler: starting connection to interop broker using channel ${openfin.channelName}`,
);
this.interopClient = this.fin.Interop.connectSync(openfin.channelName);
this.isConnected = true;
this.interopClient.onDisconnection((event) => {
const { brokerName } = event;
logger.warn(
`openfin-handler: Disconnected from Interop Broker ${brokerName}`,
);
this.clearSubscriptions();
});
return true;
} catch (error) {
logger.error('openfin-handler: error while connecting: ', error);
return false;
}
})();
try {
const isConnected = await Promise.race([
connectionPromise,
connectionTimeoutPromise,
]);
return { isConnected };
} catch (error) {
logger.error(
'openfin-handler: error or timeout while connecting: ',
error,
);
return { isConnected: false };
} }
logger.error('openfin-handler: missing openfin params to connect.');
} }
/** /**
@ -50,23 +102,24 @@ export class OpenfinHandler {
/** /**
* Adds an intent handler for incoming intents * Adds an intent handler for incoming intents
*/ */
public async registerIntentHandler(intentName: string) { public async registerIntentHandler(intentName: string): Promise<UUID> {
const unsubscriptionCallback = const unsubscriptionCallback =
await this.interopClient.registerIntentHandler( await this.interopClient.registerIntentHandler(
this.intentHandler, this.intentHandler,
intentName, intentName,
); );
this.intentHandlerSubscriptions.set(intentName, unsubscriptionCallback); const uuid = randomUUID();
this.intentHandlerSubscriptions.set(uuid, unsubscriptionCallback);
return uuid;
} }
/** /**
* Removes an intent handler for a given intent * Removes an intent handler for a given intent
*/ */
public unregisterIntentHandler(intentName) { public unregisterIntentHandler(uuid: UUID) {
const unsubscriptionCallback = const unsubscriptionCallback = this.intentHandlerSubscriptions.get(uuid);
this.intentHandlerSubscriptions.get(intentName);
unsubscriptionCallback.unsubscribe(); unsubscriptionCallback.unsubscribe();
this.intentHandlerSubscriptions.delete(intentName); this.intentHandlerSubscriptions.delete(uuid);
} }
/** /**
@ -114,8 +167,10 @@ export class OpenfinHandler {
/** /**
* Returns openfin connection status * Returns openfin connection status
*/ */
public getConnectionStatus(): boolean { public getConnectionStatus() {
return this.isConnected; return {
isConnected: this.isConnected,
};
} }
/** /**
@ -123,15 +178,15 @@ export class OpenfinHandler {
*/ */
public getInfo() { public getInfo() {
return { return {
provider: 'Openfin', provider: OPENFIN_PROVIDER,
isConnected: this.getConnectionStatus(), isConnected: this.getConnectionStatus().isConnected,
}; };
} }
private intentHandler = (intent: any) => { private intentHandler = (intent: any) => {
logger.info('openfin-handler: intent received - ', intent); logger.info('openfin-handler: intent received - ', intent);
const mainWebContents = windowHandler.getMainWebContents(); const mainWebContents = windowHandler.getMainWebContents();
mainWebContents?.send('intent-received', intent.name); mainWebContents?.send('intent-received', intent);
}; };
} }

View File

@ -69,7 +69,9 @@ const OPENFIN = {
uuid: 'string', uuid: 'string',
licenseKey: 'string', licenseKey: 'string',
runtimeVersion: 'string', runtimeVersion: 'string',
channelName: 'string',
autoConnect: 'boolean', autoConnect: 'boolean',
connectionTimeout: 'string',
}; };
export const getAllUserDefaults = (): IConfig => { export const getAllUserDefaults = (): IConfig => {

View File

@ -12,6 +12,8 @@ export const ConfigFieldsDefaultValues: Partial<IConfig> = {
uuid: '', uuid: '',
licenseKey: '', licenseKey: '',
runtimeVersion: '', runtimeVersion: '',
channelName: '',
autoConnect: false, autoConnect: false,
connectionTimeout: '10000',
}, },
}; };

View File

@ -1,3 +1,4 @@
import { UUID } from 'crypto';
import { ipcRenderer, webFrame } from 'electron'; import { ipcRenderer, webFrame } from 'electron';
import { import {
buildNumber, buildNumber,
@ -66,14 +67,14 @@ export interface ILocalObject {
c9MessageCallback?: (status: IShellStatus) => void; c9MessageCallback?: (status: IShellStatus) => void;
updateMyPresenceCallback?: (presence: EPresenceStatusCategory) => void; updateMyPresenceCallback?: (presence: EPresenceStatusCategory) => void;
phoneNumberCallback?: (arg: string) => void; phoneNumberCallback?: (arg: string) => void;
intentsCallbacks: {}; intentsCallbacks: Map<string, Map<UUID, any>>;
writeImageToClipboard?: (blob: string) => void; writeImageToClipboard?: (blob: string) => void;
getHelpInfo?: () => Promise<IPodSettingsClientSpecificSupportLink>; getHelpInfo?: () => Promise<IPodSettingsClientSpecificSupportLink>;
} }
const local: ILocalObject = { const local: ILocalObject = {
ipcRenderer, ipcRenderer,
intentsCallbacks: {}, intentsCallbacks: new Map(),
}; };
const notificationActionCallbacks = new Map< const notificationActionCallbacks = new Map<
@ -957,10 +958,14 @@ export class SSFApi {
/** /**
* Openfin Interop client initialization * Openfin Interop client initialization
*/ */
public openfinInit(): void { public async openfinInit(): Promise<boolean> {
local.ipcRenderer.send(apiName.symphonyApi, { const connectionStatus = await local.ipcRenderer.invoke(
cmd: apiCmds.openfinConnect, apiName.symphonyApi,
}); {
cmd: apiCmds.openfinConnect,
},
);
return connectionStatus;
} }
/** /**
@ -1000,25 +1005,39 @@ export class SSFApi {
/** /**
* Registers a handler for a given intent * Registers a handler for a given intent
*/ */
public openfinRegisterIntentHandler( public async openfinRegisterIntentHandler(
intentHandler: any, intentHandler: any,
intentName: any, intentName: any,
): void { ): Promise<UUID> {
local.intentsCallbacks[intentName] = intentHandler; const uuid: UUID = await local.ipcRenderer.invoke(apiName.symphonyApi, {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openfinRegisterIntentHandler, cmd: apiCmds.openfinRegisterIntentHandler,
intentName, intentName,
}); });
if (local.intentsCallbacks.has(intentName)) {
local.intentsCallbacks.get(intentName)?.set(uuid, intentHandler);
} else {
const innerMap = new Map();
innerMap.set(uuid, intentHandler);
local.intentsCallbacks.set(intentName, innerMap);
}
return uuid;
} }
/** /**
* Unregisters a handler based on a given intent name * Unregisters a handler based on a given intent handler callback id
* @param intentName * @param UUID
*/ */
public openfinUnregisterIntentHandler(intentName: string): void { public openfinUnregisterIntentHandler(callbackId: UUID): void {
for (const innerMap of local.intentsCallbacks.values()) {
if (innerMap.has(callbackId)) {
innerMap.delete(callbackId);
break;
}
}
local.ipcRenderer.send(apiName.symphonyApi, { local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openfinUnregisterIntentHandler, cmd: apiCmds.openfinUnregisterIntentHandler,
intentName, callbackId,
}); });
} }
@ -1393,9 +1412,15 @@ local.ipcRenderer.on(
}, },
); );
local.ipcRenderer.on('intent-received', (_event: Event, intentName: string) => { local.ipcRenderer.on('intent-received', (_event: Event, intent: any) => {
if (typeof intentName === 'string' && local.intentsCallbacks[intentName]) { if (
local.intentsCallbacks[intentName](); typeof intent.name === 'string' &&
local.intentsCallbacks.has(intent.name)
) {
const uuidCallbacks = local.intentsCallbacks.get(intent.name);
uuidCallbacks?.forEach((callbacks, _uuid) => {
callbacks(intent.context);
});
} }
}); });
@ -1407,7 +1432,7 @@ const sanitize = (): void => {
windowName: window.name, windowName: window.name,
}); });
} }
local.intentsCallbacks = {}; local.intentsCallbacks = new Map();
}; };
// listens for the online/offline events and updates the main process // listens for the online/offline events and updates the main process