From 8a7d5c0fcfe3861fe5f0bf9cc1c2c2446560b39c Mon Sep 17 00:00:00 2001 From: Robin Westberg Date: Tue, 5 Jul 2022 09:43:35 +0200 Subject: [PATCH] SDA-3770 Add support for optional symphony-c9-shell (#1446) * Add support for optional symphony-c9-shell --- .../win/WixSharpInstaller/CloseDialog.cs | 2 +- .../WixSharpInstaller/MaintenanceDialog.cs | 2 +- installer/win/WixSharpInstaller/Symphony.cs | 26 ++- .../win/WixSharpInstaller/WelcomeDialog.cs | 4 +- package-lock.json | 17 ++ package.json | 1 + scripts/build-win64.bat | 4 + spec/c9PipeHandler.spec.ts | 44 +++++ spec/mainApiHandler.spec.ts | 12 ++ src/app/c9-pipe-handler.ts | 115 +++++++++++++ src/app/c9-shell-handler.ts | 161 ++++++++++++++++++ src/app/main-api-handler.ts | 14 ++ src/app/window-handler.ts | 3 + src/common/api-interface.ts | 11 ++ src/renderer/preload-main.ts | 2 + src/renderer/ssf-api.ts | 92 ++++++++++ 16 files changed, 501 insertions(+), 9 deletions(-) create mode 100644 spec/c9PipeHandler.spec.ts create mode 100644 src/app/c9-pipe-handler.ts create mode 100644 src/app/c9-shell-handler.ts diff --git a/installer/win/WixSharpInstaller/CloseDialog.cs b/installer/win/WixSharpInstaller/CloseDialog.cs index 7ca5b569..ae02d8a7 100644 --- a/installer/win/WixSharpInstaller/CloseDialog.cs +++ b/installer/win/WixSharpInstaller/CloseDialog.cs @@ -13,7 +13,7 @@ namespace Symphony void dialog_Load(object sender, System.EventArgs e) { // Detect if Symphony is running - bool isRunning = System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1; + bool isRunning = (System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1 || System.Diagnostics.Process.GetProcessesByName("C9Shell").Length >= 1); if (isRunning) { // If it is running, disable the "next" button diff --git a/installer/win/WixSharpInstaller/MaintenanceDialog.cs b/installer/win/WixSharpInstaller/MaintenanceDialog.cs index 1e4baa05..d202ae4d 100644 --- a/installer/win/WixSharpInstaller/MaintenanceDialog.cs +++ b/installer/win/WixSharpInstaller/MaintenanceDialog.cs @@ -12,7 +12,7 @@ namespace Symphony private void MaintenanceDialog_Shown(object sender, System.EventArgs e) { // Detect if Symphony is running - bool isRunning = System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1; + bool isRunning = (System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1 || System.Diagnostics.Process.GetProcessesByName("C9Shell").Length >= 1); if (isRunning) { // If it is running, continue to the "Close Symphony" screen diff --git a/installer/win/WixSharpInstaller/Symphony.cs b/installer/win/WixSharpInstaller/Symphony.cs index 98e28cc1..f8ff4109 100644 --- a/installer/win/WixSharpInstaller/Symphony.cs +++ b/installer/win/WixSharpInstaller/Symphony.cs @@ -53,8 +53,8 @@ class Script // StartOn = SvcEvent.Install, // StopOn = SvcEvent.InstallUninstall_Wait, // RemoveOn = SvcEvent.Uninstall_Wait, - // }; - + // }; + // Create a wixsharp project instance and assign the project name to it, and a hierarchy of all files to include // Files are taken from multiple locations, and not all files in each location should be included, which is why // the file list is rather long and explicit. At some point we might make the `dist` folder match exactly the @@ -116,7 +116,9 @@ class Script new Files(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\*.*") ) ) - ) + ), + new Dir(@"cloud9", + new Files(@"..\..\..\dist\win-unpacked\cloud9\*.*") ), // Add a launch condition to require Windows Server 2008 or later @@ -124,7 +126,7 @@ class Script // https://docs.microsoft.com/en-us/windows/win32/msi/operating-system-property-values new LaunchCondition("VersionNT>=600 AND WindowsBuild>=6001", "OS not supported"), - // Add registry entry used by protocol handler to launch symphony when opening symphony:// URIs + // Add registry entry used by protocol handler to launch symphony when opening symphony:// URIs new RegValue(WixSharp.RegistryHive.ClassesRoot, productName, "", "URL:symphony"), new RegValue(WixSharp.RegistryHive.ClassesRoot, productName, "URL Protocol", ""), new RegValue(WixSharp.RegistryHive.ClassesRoot, productName + @"\shell\open\command", "", "\"[INSTALLDIR]Symphony.exe\" " + userDataPathArgument + " \"%1\""), @@ -133,7 +135,8 @@ class Script // will not work for us, as we have a "minimize on close" option, which stops the app from terminating on WM_CLOSE. So we // instruct the installer to not send a Close message, but instead send the EndSession message, and we have a custom event // handler in the SDA code which listens for this message, and ensures app termination when it is received. - new CloseApplication("Symphony.exe", false) { EndSessionMessage = true } + new CloseApplication("Symphony.exe", false) { EndSessionMessage = true }, + new CloseApplication("C9Shell.exe", false) { EndSessionMessage = true } ); // The build script which calls the wix# builder, will be run from a command environment which has %SYMVER% set. @@ -312,6 +315,19 @@ class Script } } }); + + // The embedded C9 Shell should terminate when the parent window is closed, but it doesn't always work. So we'll just force it to exit + System.Diagnostics.Process.GetProcessesByName("C9Shell").ForEach(p => + { + if (System.IO.Path.GetFileName(p.MainModule.FileName) == "C9Shell.exe") + { + if (!p.HasExited) + { + p.Kill(); + p.WaitForExit(); + } + } + }); } } catch (System.ComponentModel.Win32Exception ex) diff --git a/installer/win/WixSharpInstaller/WelcomeDialog.cs b/installer/win/WixSharpInstaller/WelcomeDialog.cs index 03616a9a..09a56f86 100644 --- a/installer/win/WixSharpInstaller/WelcomeDialog.cs +++ b/installer/win/WixSharpInstaller/WelcomeDialog.cs @@ -41,7 +41,7 @@ namespace Symphony } // Detect if Symphony is running - bool isRunning = System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1; + bool isRunning = (System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1 || System.Diagnostics.Process.GetProcessesByName("C9Shell").Length >= 1); if (!isRunning) { // If it is not running, change the label of the "Next" button to "Install" as the CloseDialog will be skipped @@ -81,7 +81,7 @@ namespace Symphony } // Detect if Symphony is running - bool isRunning = System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1; + bool isRunning = (System.Diagnostics.Process.GetProcessesByName("Symphony").Length > 1 || System.Diagnostics.Process.GetProcessesByName("C9Shell").Length >= 1); if (isRunning) { // If it is running, continue to the "Close Symphony" screen diff --git a/package-lock.json b/package-lock.json index 5114ec20..5ef28a9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "typescript": "3.9.7" }, "optionalDependencies": { + "@symphony/symphony-c9-shell": "3.14.99-37", "auto-update": "file:auto_update", "screen-share-indicator-frame": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.13", "screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet2.git#9.2.2", @@ -2288,6 +2289,16 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, + "node_modules/@symphony/symphony-c9-shell": { + "version": "3.14.99-37", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@symphony/symphony-c9-shell/-/@symphony/symphony-c9-shell-3.14.99-37.tgz", + "integrity": "sha1-Xl2kS4dN4jlSd6oBdzBLfUd/Mnc=", + "license": "UNLICENSED", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -26817,6 +26828,12 @@ "integrity": "sha1-jaXGUwkVZT86Hzj9XxAdjD+AecU=", "dev": true }, + "@symphony/symphony-c9-shell": { + "version": "3.14.99-37", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@symphony/symphony-c9-shell/-/@symphony/symphony-c9-shell-3.14.99-37.tgz", + "integrity": "sha1-Xl2kS4dN4jlSd6oBdzBLfUd/Mnc=", + "optional": true + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", diff --git a/package.json b/package.json index 536c9631..42998866 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "shell-path": "^3.0.0" }, "optionalDependencies": { + "@symphony/symphony-c9-shell": "3.14.99-37", "auto-update": "file:auto_update", "screen-share-indicator-frame": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.13", "screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet2.git#9.2.2", diff --git a/scripts/build-win64.bat b/scripts/build-win64.bat index aee6882d..e4b2a6f1 100644 --- a/scripts/build-win64.bat +++ b/scripts/build-win64.bat @@ -91,6 +91,10 @@ set installerDir="%CD%\installer\win" set distDir="%CD%\dist" set rootDir="%CD%" +echo "Move optional symphony-c9-shell files into place: " "%distDir%\win-unpacked\cloud9" +mkdir "%distDir%\win-unpacked\cloud9" +move /y "%rootDir%\node_modules\@symphony\symphony-c9-shell\shell" "%distDir%\win-unpacked\cloud9" + if NOT EXIST "%PFX_DIR%\%PFX_FILE%" ( echo "can not find .pfx file" "%pfxDir%\%pfxFile%" exit /b -1 diff --git a/spec/c9PipeHandler.spec.ts b/spec/c9PipeHandler.spec.ts new file mode 100644 index 00000000..3235c42d --- /dev/null +++ b/spec/c9PipeHandler.spec.ts @@ -0,0 +1,44 @@ +import { connectC9Pipe } from '../src/app/c9-pipe-handler'; +import { createConnection } from 'net'; + +jest.mock('net'); + +describe('C9 pipe handler', () => { + const webContentsMocked = { send: jest.fn() }; + const mockConnectionEvents = new Map(); + const mockCreateConnection = (createConnection as unknown) as jest.MockInstance< + typeof createConnection + >; + + beforeEach(() => { + jest.clearAllMocks().resetModules(); + mockCreateConnection.mockImplementation((_path, onConnect: () => void) => { + onConnect(); + return { + on: (event, callback) => { + mockConnectionEvents.set(event, callback); + }, + destroy: jest.fn(), + }; + }); + }); + + describe('connect', () => { + it('success', () => { + connectC9Pipe(webContentsMocked as any, 'symphony-c9-test'); + expect(webContentsMocked.send).toHaveBeenCalledWith( + 'c9-pipe-event', + expect.objectContaining({ event: 'connected' }), + ); + }); + + it('data', () => { + connectC9Pipe(webContentsMocked as any, 'symphony-c9-test'); + mockConnectionEvents.get('data')('the data'); + expect(webContentsMocked.send).toHaveBeenCalledWith( + 'c9-pipe-event', + expect.objectContaining({ event: 'data', arg: 'the data' }), + ); + }); + }); +}); diff --git a/spec/mainApiHandler.spec.ts b/spec/mainApiHandler.spec.ts index 784a5190..a1ed3eb6 100644 --- a/spec/mainApiHandler.spec.ts +++ b/spec/mainApiHandler.spec.ts @@ -7,6 +7,7 @@ import * as windowActions from '../src/app/window-actions'; import { windowHandler } from '../src/app/window-handler'; import * as utils from '../src/app/window-utils'; import { apiCmds, apiName } from '../src/common/api-interface'; +import * as c9PipeHandler from '../src/app/c9-pipe-handler'; import { logger } from '../src/common/logger'; import { BrowserWindow, ipcMain } from './__mocks__/electron'; @@ -503,5 +504,16 @@ describe('main api handler', () => { expect(windows['popout1'].getNativeWindowHandle).toBeCalledTimes(1); expect(windows['popout2'].getNativeWindowHandle).toBeCalledTimes(0); }); + + it('should call `connectC9Pipe` correctly', () => { + const spy = jest.spyOn(c9PipeHandler, 'connectC9Pipe'); + const value = { + cmd: apiCmds.connectCloud9Pipe, + pipe: 'pipe-name', + }; + const expectedValue = [{ send: expect.any(Function) }, 'pipe-name']; + ipcMain.send(apiName.symphonyApi, value); + expect(spy).toBeCalledWith(...expectedValue); + }); }); }); diff --git a/src/app/c9-pipe-handler.ts b/src/app/c9-pipe-handler.ts new file mode 100644 index 00000000..7a28ef32 --- /dev/null +++ b/src/app/c9-pipe-handler.ts @@ -0,0 +1,115 @@ +import { WebContents } from 'electron'; +import { createConnection, Socket } from 'net'; +import { logger } from '../common/logger'; + +class C9PipeHandler { + private _socket: Socket | undefined; + + /** + * Connects to the C9 pipe server. Errors will be reported as events. + * @param sender Where to send incoming events + * @param pipe pipe identifier + */ + public connect(sender: WebContents, pipe: string) { + let connectionSuccess = false; + if (!pipe.startsWith('symphony-c9-')) { + logger.info('c9-pipe: Invalid pipe name specified: ' + pipe); + sender.send('c9-pipe-event', { + event: 'connection-failed', + arg: 'invalid pipe', + }); + return; + } + + const path = '\\\\?\\pipe\\' + pipe; + logger.info('c9-pipe: Connecting to ' + path); + const client = createConnection(path, () => { + connectionSuccess = true; + logger.info('c9-pipe: Connected to ' + path); + sender.send('c9-pipe-event', { event: 'connected' }); + }); + this._socket = client; + + client.on('data', (data) => { + sender.send('c9-pipe-event', { event: 'data', arg: data }); + }); + client.on('close', () => { + // If the socket is successfully connected, the close is coming from the server side, or + // is otherwise unexpected. In this case, send close event to the extension so it can go + // into a reconnect loop. + if (connectionSuccess) { + logger.info('c9-pipe: Server closed ' + path); + sender.send('c9-pipe-event', { event: 'close' }); + this._socket?.destroy(); + } + }); + client.on('error', (err: Error) => { + // If the connection is already established, any error will also result in a 'close' event + // and will be handled above. + logger.info('c9-pipe: Error from ' + path, err); + if (!connectionSuccess) { + sender.send('c9-pipe-event', { + event: 'connection-failed', + arg: err.message, + }); + } + }); + } + + /** + * Writes data to the pipe + * @param data the data to be written + */ + public write(data: Uint8Array) { + this._socket?.write(data); + } + + /** + * Closes an open pipe + */ + public close() { + logger.info('c9-pipe: Closing pipe'); + this._socket?.destroy(); + this._socket = undefined; + } + + /** + * Returns whether the pipe is open + */ + public isConnected() { + return this._socket !== undefined; + } +} + +let c9PipeHandler: C9PipeHandler | undefined; + +/** + * Connects to the C9 pipe server. Errors will be reported as callbacks. + * @param sender Where to send incoming events + * @param pipe pipe identifier + */ +export const connectC9Pipe = (sender: WebContents, pipe: string) => { + if (!c9PipeHandler) { + c9PipeHandler = new C9PipeHandler(); + } else { + if (c9PipeHandler.isConnected()) { + c9PipeHandler.close(); + } + } + c9PipeHandler.connect(sender, pipe); +}; + +/** + * Writes data to the pipe + * @param data the data to be written + */ +export const writeC9Pipe = (data: Uint8Array) => { + c9PipeHandler?.write(data); +}; + +/** + * Closes an open pipe + */ +export const closeC9Pipe = () => { + c9PipeHandler?.close(); +}; diff --git a/src/app/c9-shell-handler.ts b/src/app/c9-shell-handler.ts new file mode 100644 index 00000000..1d387f3a --- /dev/null +++ b/src/app/c9-shell-handler.ts @@ -0,0 +1,161 @@ +import { app, WebContents } from 'electron'; +import { isDevEnv, isWindowsOS } from '../common/env'; +import { logger } from '../common/logger'; + +import { ChildProcess, spawn } from 'child_process'; +import * as path from 'path'; +import { getCommandLineArgs, getGuid } from '../common/utils'; + +/** + * Current state of the C9 shell process. + */ +export interface IShellStatus { + status: 'inactive' | 'starting' | 'active'; + pipeName?: string; +} + +type StatusCallback = (status: IShellStatus) => void; + +class C9ShellHandler { + private _c9shell: ChildProcess | undefined; + private _curStatus: IShellStatus | undefined; + private _statusCallback: StatusCallback | undefined; + + /** + * Starts the c9shell process + */ + public startShell() { + if (this._attachExistingC9Shell()) { + return; + } + + if (!this._c9shell) { + this._c9shell = this._launchC9Shell(); + } + } + + /** + * Allows the C9 extension to subscribe to shell status updates. Immediately sends current status. + */ + public setStatusCallback(callback: StatusCallback) { + this._statusCallback = callback; + if (!this._statusCallback) { + return; + } + if (this._curStatus) { + this._statusCallback(this._curStatus); + } + } + + /** + * Update the current shell status and notify the callback if set. + */ + private _updateStatus(status: IShellStatus) { + this._curStatus = status; + if (this._statusCallback) { + this._statusCallback(status); + } + } + + /** + * Checks if the user wants to control the C9 shell process explicitly, returns true if so + * @returns + */ + private _attachExistingC9Shell(): boolean { + const customC9ShellPipe = getCommandLineArgs( + process.argv, + '--c9pipe=', + false, + ); + if (customC9ShellPipe) { + logger.info(`c9-shell: Using custom pipe: ${customC9ShellPipe}`); + this._updateStatus({ + status: 'active', + pipeName: 'symphony-c9-' + customC9ShellPipe.substring(9), + }); + return true; + } else { + return false; + } + } + + /** + * Launches the correct c9shell process + */ + private _launchC9Shell(): ChildProcess | undefined { + this._curStatus = undefined; + const uniquePipeName = getGuid(); + + const c9ShellPath = isDevEnv + ? path.join( + __dirname, + '../../../node_modules/@symphony/symphony-c9-shell/shell/c9shell.exe', + ) + : path.join(path.dirname(app.getPath('exe')), 'cloud9/shell/c9shell.exe'); + + const customC9ShellArgs = getCommandLineArgs( + process.argv, + '--c9args=', + false, + ); + const customC9ShellArgList = customC9ShellArgs + ? customC9ShellArgs.substring(9).split(' ') + : []; + + logger.info('c9-shell: launching shell', c9ShellPath, customC9ShellArgList); + this._updateStatus({ status: 'starting' }); + + const c9Shell = spawn( + c9ShellPath, + [ + '--allowmultiproc', + '--symphonyHost', + uniquePipeName, + ...customC9ShellArgList, + ], + { + stdio: 'pipe', + }, + ); + c9Shell.on('close', (code) => { + logger.info('c9-shell: closed with code', code); + this._c9shell = undefined; + this._updateStatus({ status: 'inactive' }); + }); + c9Shell.on('spawn', () => { + logger.info('c9-shell: shell process successfully spawned'); + this._updateStatus({ + status: 'active', + pipeName: 'symphony-c9-' + uniquePipeName, + }); + }); + c9Shell.stdout.on('data', (data) => { + logger.info(`c9-shell: ${data.toString().trim()}`); + }); + c9Shell.stderr.on('data', (data) => { + logger.error(`c9-shell: ${data.toString().trim()}`); + }); + + return c9Shell; + } +} + +let c9ShellHandler: C9ShellHandler | undefined; + +/** + * Starts the C9 shell process asynchronously, if not already started. + */ +export const loadC9Shell = (sender: WebContents) => { + if (!isWindowsOS) { + logger.error("c9-shell: can't load shell on non-Windows OS"); + return; + } + if (!c9ShellHandler) { + c9ShellHandler = new C9ShellHandler(); + } + c9ShellHandler.setStatusCallback((status: IShellStatus) => { + logger.info('c9-shell: sending status', status); + sender.send('c9-status-event', { status }); + }); + c9ShellHandler.startShell(); +}; diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index fc540604..68c062a4 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -18,6 +18,8 @@ import { logger } from '../common/logger'; import { activityDetection } from './activity-detection'; import { analytics } from './analytics-handler'; import appStateHandler from './app-state-handler'; +import { closeC9Pipe, connectC9Pipe, writeC9Pipe } from './c9-pipe-handler'; +import { loadC9Shell } from './c9-shell-handler'; import { getCitrixMediaRedirectionStatus } from './citrix-handler'; import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler'; import { downloadHandler } from './download-handler'; @@ -356,6 +358,18 @@ ipcMain.on( swiftSearchInstance.handleMessageEvents(arg.swiftSearchData); } break; + case apiCmds.connectCloud9Pipe: + connectC9Pipe(event.sender, arg.pipe); + break; + case apiCmds.writeCloud9Pipe: + writeC9Pipe(arg.data); + break; + case apiCmds.closeCloud9Pipe: + closeC9Pipe(); + break; + case apiCmds.launchCloud9: + loadC9Shell(event.sender); + break; default: break; } diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index b309b166..b01fa08a 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -31,6 +31,7 @@ import { import { notification } from '../renderer/notification'; import { cleanAppCacheOnCrash } from './app-cache-handler'; import { AppMenu } from './app-menu'; +import { closeC9Pipe } from './c9-pipe-handler'; import { handleChildWindow } from './child-window-handler'; import { CloudConfigDataTypes, @@ -468,6 +469,8 @@ export class WindowHandler { // reset to false when the client reloads this.isMana = false; logger.info(`window-handler: main window web contents finished loading!`); + // Make sure there is no lingering C9 pipe connection + closeC9Pipe(); // early exit if the window has already been destroyed if (!this.mainWebContents || this.mainWebContents.isDestroyed()) { logger.info( diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index bce6415c..945d2e0e 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -65,6 +65,10 @@ export enum apiCmds { getNativeWindowHandle = 'get-native-window-handle', getCitrixMediaRedirectionStatus = 'get-citrix-media-redirection-status', getSources = 'getSources', + launchCloud9 = 'launch-cloud9', + connectCloud9Pipe = 'connect-cloud9-pipe', + writeCloud9Pipe = 'write-cloud9-pipe', + closeCloud9Pipe = 'close-cloud9-pipe', } export enum apiName { @@ -115,6 +119,8 @@ export interface IApiArgs { swiftSearchData: any; types: string[]; thumbnailSize: Size; + pipe: string; + data: Uint8Array; } export type Themes = 'light' | 'dark'; @@ -269,3 +275,8 @@ export type NotificationActionCallback = ( ) => void; export type ConfigUpdateType = 'restart' | 'reload'; + +export interface ICloud9Pipe { + write(data: Uint8Array): void; + close(): void; +} diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index a3a8b9f6..40294ea7 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -92,6 +92,8 @@ if (ssfWindow.ssf) { getCitrixMediaRedirectionStatus: ssfWindow.ssf.getCitrixMediaRedirectionStatus, registerClientBanner: ssfWindow.ssf.registerClientBanner, + launchCloud9: ssfWindow.ssf.launchCloud9, + connectCloud9Pipe: ssfWindow.ssf.connectCloud9Pipe, }); } diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts index a3332c03..6ef0b40a 100644 --- a/src/renderer/ssf-api.ts +++ b/src/renderer/ssf-api.ts @@ -5,6 +5,7 @@ import { searchAPIVersion, version, } from '../../package.json'; +import { IShellStatus } from '../app/c9-shell-handler'; import { RedirectionStatus } from '../app/citrix-handler'; import { IDownloadItem } from '../app/download-handler'; import { @@ -13,6 +14,7 @@ import { ConfigUpdateType, IBadgeCount, IBoundsChange, + ICloud9Pipe, ICPUUsage, ILogMsg, IMediaPermission, @@ -69,6 +71,8 @@ export interface ILocalObject { showClientBannerCallback?: Array< (reason: string, action: ConfigUpdateType) => void >; + c9PipeEventCallback?: (event: string, arg?: any) => void; + c9MessageCallback?: (status: IShellStatus) => void; } const local: ILocalObject = { @@ -776,6 +780,80 @@ export class SSFApi { local.showClientBannerCallback.push(callback); } } + + /** + * Connects to a Cloud9 pipe + * + * @param pipe pipe name + * @param onData callback that is invoked when data is received over the connection + * @param onClose callback that is invoked when the connection is closed by the remote side + * @returns Cloud9 pipe instance promise + */ + public connectCloud9Pipe( + pipe: string, + onData: (data: Uint8Array) => void, + onClose: () => void, + ): Promise { + if ( + typeof pipe === 'string' && + typeof onData === 'function' && + typeof onClose === 'function' + ) { + if (local.c9PipeEventCallback) { + return Promise.reject("Can't connect to pipe, already connected"); + } + + return new Promise((resolve, reject) => { + local.c9PipeEventCallback = (event: string, arg?: any) => { + switch (event) { + case 'connected': + const ret = { + write: (data: Uint8Array) => { + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.writeCloud9Pipe, + data, + }); + }, + close: () => { + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.closeCloud9Pipe, + }); + }, + }; + resolve(ret); + break; + case 'connection-failed': + local.c9PipeEventCallback = undefined; + reject(arg); + break; + case 'data': + onData(arg); + break; + case 'close': + local.c9PipeEventCallback = undefined; + onClose(); + break; + } + }; + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.connectCloud9Pipe, + pipe, + }); + }); + } else { + return Promise.reject('Invalid arguments'); + } + } + + /** + * Launches the Cloud9 client. + */ + public launchCloud9(callback: (status: IShellStatus) => void): void { + local.c9MessageCallback = callback; + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.launchCloud9, + }); + } } /** @@ -1018,6 +1096,20 @@ local.ipcRenderer.on('display-client-banner', (_event, args) => { } }); +/** + * An event triggered by the main process when a cloud9 pipe event occurs + */ +local.ipcRenderer.on('c9-pipe-event', (_event, args) => { + local.c9PipeEventCallback?.call(null, args.event, args?.arg); +}); + +/** + * An event triggered by the main process when the status of the cloud9 client changes + */ +local.ipcRenderer.on('c9-status-event', (_event, args) => { + local.c9MessageCallback?.call(null, args?.status); +}); + // Invoked whenever the app is reloaded/navigated const sanitize = (): void => { if (window.name === apiName.mainWindowName) {