From 8ea54fbffceaf0fe9ce077b4f8cb1aaa8f3cf4a2 Mon Sep 17 00:00:00 2001 From: Robin Westberg Date: Wed, 2 Nov 2022 13:26:53 +0100 Subject: [PATCH] Initial implementation --- spec/c9ShellHandler.spec.ts | 166 +++++++++++++++++++++++++++++++++++ src/app/c9-shell-handler.ts | 30 +++++-- src/app/main-api-handler.ts | 5 +- src/common/api-interface.ts | 1 + src/renderer/preload-main.ts | 1 + src/renderer/ssf-api.ts | 9 ++ 6 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 spec/c9ShellHandler.spec.ts diff --git a/spec/c9ShellHandler.spec.ts b/spec/c9ShellHandler.spec.ts new file mode 100644 index 00000000..583ae166 --- /dev/null +++ b/spec/c9ShellHandler.spec.ts @@ -0,0 +1,166 @@ +describe('C9 shell handler', () => { + const webContentsMocked = { send: jest.fn() }; + const mockSpawnEvents = new Map(); + const mockSpawn = jest.fn(); + const mockGetCommandLineArgs = jest.fn(); + const mockGetGuid = jest.fn(); + const mockKill = jest.fn(); + let mockIsWindows: boolean; + + jest.mock('child_process', () => { + return { + spawn: mockSpawn, + ChildProcess: () => {}, + }; + }); + + jest.mock('../src/common/utils', () => { + return { + getCommandLineArgs: mockGetCommandLineArgs, + getGuid: mockGetGuid, + }; + }); + + jest.mock('../src/common/env', () => { + return { + isDevEnv: false, + isElectronQA: true, + isMac: false, + isWindowsOS: mockIsWindows, + isLinux: false, + }; + }); + + beforeEach(() => { + jest.clearAllMocks().resetModules().resetAllMocks(); + mockSpawnEvents.clear(); + mockSpawn.mockImplementation((_cmd, _args) => { + return { + on: (event, callback) => { + mockSpawnEvents.set(event, callback); + }, + stdout: { on: jest.fn() }, + stderr: { on: jest.fn() }, + kill: mockKill, + }; + }); + mockIsWindows = true; + }); + + describe('launch', () => { + it('success', () => { + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'starting' }, + }); + + mockSpawnEvents.get('spawn')(); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: expect.objectContaining({ status: 'active' }), + }); + }); + + it('failure', () => { + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'starting' }, + }); + + mockSpawnEvents.get('close')(1); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: expect.objectContaining({ status: 'inactive' }), + }); + }); + + it('with attach', () => { + mockGetCommandLineArgs.mockReturnValue('--c9pipe=custompipe'); + + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'active', pipeName: 'symphony-c9-custompipe' }, + }); + }); + + it('cached status on relaunch', () => { + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'starting' }, + }); + + mockSpawnEvents.get('spawn')(); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: expect.objectContaining({ status: 'active' }), + }); + + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: expect.objectContaining({ status: 'active' }), + }); + }); + + it('args', () => { + mockGetGuid.mockReturnValue('just-another-guid'); + + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(mockSpawn).toBeCalledWith( + expect.stringContaining('c9shell.exe'), + ['--symphonyHost', 'just-another-guid'], + { stdio: 'pipe' }, + ); + }); + + it('non-windows', () => { + mockIsWindows = false; + + const { loadC9Shell } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(mockSpawn).not.toBeCalled(); + }); + }); + + describe('terminate', () => { + it('success', () => { + const { + loadC9Shell, + terminateC9Shell, + } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'starting' }, + }); + + terminateC9Shell(webContentsMocked as any); + expect(mockKill).toBeCalledTimes(1); + }); + + it('no terminate if never started', () => { + const { terminateC9Shell } = require('../src/app/c9-shell-handler'); + terminateC9Shell(webContentsMocked as any); + expect(mockKill).toBeCalledTimes(0); + }); + + it('no terminate if already exited', () => { + const { + loadC9Shell, + terminateC9Shell, + } = require('../src/app/c9-shell-handler'); + loadC9Shell(webContentsMocked as any); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: { status: 'starting' }, + }); + + mockSpawnEvents.get('close')(1); + expect(webContentsMocked.send).lastCalledWith('c9-status-event', { + status: expect.objectContaining({ status: 'inactive' }), + }); + + terminateC9Shell(webContentsMocked as any); + expect(mockKill).toBeCalledTimes(0); + }); + }); +}); diff --git a/src/app/c9-shell-handler.ts b/src/app/c9-shell-handler.ts index 1d387f3a..ada4ae39 100644 --- a/src/app/c9-shell-handler.ts +++ b/src/app/c9-shell-handler.ts @@ -39,14 +39,21 @@ class C9ShellHandler { */ public setStatusCallback(callback: StatusCallback) { this._statusCallback = callback; - if (!this._statusCallback) { - return; - } if (this._curStatus) { this._statusCallback(this._curStatus); } } + /** + * Terminates the c9shell process if it was started by this handler. + */ + public terminateShell() { + if (!this._c9shell) { + return; + } + this._c9shell.kill(); + } + /** * Update the current shell status and notify the callback if set. */ @@ -107,12 +114,7 @@ class C9ShellHandler { const c9Shell = spawn( c9ShellPath, - [ - '--allowmultiproc', - '--symphonyHost', - uniquePipeName, - ...customC9ShellArgList, - ], + ['--symphonyHost', uniquePipeName, ...customC9ShellArgList], { stdio: 'pipe', }, @@ -159,3 +161,13 @@ export const loadC9Shell = (sender: WebContents) => { }); c9ShellHandler.startShell(); }; + +/** + * Terminates the C9 shell process asynchronously, if it is running. + */ +export const terminateC9Shell = (_sender: WebContents) => { + if (!c9ShellHandler) { + return; + } + c9ShellHandler.terminateShell(); +}; diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index 7dd7a0f2..725b0649 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -19,7 +19,7 @@ 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 { loadC9Shell, terminateC9Shell } from './c9-shell-handler'; import { getCitrixMediaRedirectionStatus } from './citrix-handler'; import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler'; import { downloadHandler } from './download-handler'; @@ -378,6 +378,9 @@ ipcMain.on( case apiCmds.launchCloud9: loadC9Shell(event.sender); break; + case apiCmds.terminateCloud9: + terminateC9Shell(event.sender); + break; case apiCmds.updateAndRestart: autoUpdate.updateAndRestart(); break; diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 8299f03d..4f04f246 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -66,6 +66,7 @@ export enum apiCmds { getCitrixMediaRedirectionStatus = 'get-citrix-media-redirection-status', getSources = 'getSources', launchCloud9 = 'launch-cloud9', + terminateCloud9 = 'terminate-cloud9', connectCloud9Pipe = 'connect-cloud9-pipe', writeCloud9Pipe = 'write-cloud9-pipe', closeCloud9Pipe = 'close-cloud9-pipe', diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index 05a35ee7..5ff493d9 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -91,6 +91,7 @@ if (ssfWindow.ssf) { ssfWindow.ssf.getCitrixMediaRedirectionStatus, registerClientBanner: ssfWindow.ssf.registerClientBanner, launchCloud9: ssfWindow.ssf.launchCloud9, + terminateCloud9: ssfWindow.ssf.terminateCloud9, connectCloud9Pipe: ssfWindow.ssf.connectCloud9Pipe, updateAndRestart: ssfWindow.ssf.updateAndRestart, downloadUpdate: ssfWindow.ssf.downloadUpdate, diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts index 7cfa4cd2..3394127a 100644 --- a/src/renderer/ssf-api.ts +++ b/src/renderer/ssf-api.ts @@ -798,6 +798,15 @@ export class SSFApi { }); } + /** + * Terminates the Cloud9 client. + */ + public terminateCloud9(): void { + ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.terminateCloud9, + }); + } + /** * Allows JS to install new update and restart SDA */