mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
SDA-3770 Add support for optional symphony-c9-shell (#1446)
* Add support for optional symphony-c9-shell
This commit is contained in:
parent
5977ed8642
commit
8a7d5c0fcf
@ -13,7 +13,7 @@ namespace Symphony
|
|||||||
void dialog_Load(object sender, System.EventArgs e)
|
void dialog_Load(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
// Detect if Symphony is running
|
// 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 (isRunning)
|
||||||
{
|
{
|
||||||
// If it is running, disable the "next" button
|
// If it is running, disable the "next" button
|
||||||
|
@ -12,7 +12,7 @@ namespace Symphony
|
|||||||
private void MaintenanceDialog_Shown(object sender, System.EventArgs e)
|
private void MaintenanceDialog_Shown(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
// Detect if Symphony is running
|
// 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 (isRunning)
|
||||||
{
|
{
|
||||||
// If it is running, continue to the "Close Symphony" screen
|
// If it is running, continue to the "Close Symphony" screen
|
||||||
|
@ -53,8 +53,8 @@ class Script
|
|||||||
// StartOn = SvcEvent.Install,
|
// StartOn = SvcEvent.Install,
|
||||||
// StopOn = SvcEvent.InstallUninstall_Wait,
|
// StopOn = SvcEvent.InstallUninstall_Wait,
|
||||||
// RemoveOn = SvcEvent.Uninstall_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
|
// 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
|
// 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
|
// 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 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
|
// 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
|
// https://docs.microsoft.com/en-us/windows/win32/msi/operating-system-property-values
|
||||||
new LaunchCondition("VersionNT>=600 AND WindowsBuild>=6001", "OS not supported"),
|
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:symphony"),
|
||||||
new RegValue(WixSharp.RegistryHive.ClassesRoot, productName, "URL Protocol", ""),
|
new RegValue(WixSharp.RegistryHive.ClassesRoot, productName, "URL Protocol", ""),
|
||||||
new RegValue(WixSharp.RegistryHive.ClassesRoot, productName + @"\shell\open\command", "", "\"[INSTALLDIR]Symphony.exe\" " + userDataPathArgument + " \"%1\""),
|
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
|
// 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
|
// 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.
|
// 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.
|
// 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)
|
catch (System.ComponentModel.Win32Exception ex)
|
||||||
|
@ -41,7 +41,7 @@ namespace Symphony
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect if Symphony is running
|
// 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 (!isRunning)
|
||||||
{
|
{
|
||||||
// If it is not running, change the label of the "Next" button to "Install" as the CloseDialog will be skipped
|
// 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
|
// 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 (isRunning)
|
||||||
{
|
{
|
||||||
// If it is running, continue to the "Close Symphony" screen
|
// If it is running, continue to the "Close Symphony" screen
|
||||||
|
17
package-lock.json
generated
17
package-lock.json
generated
@ -70,6 +70,7 @@
|
|||||||
"typescript": "3.9.7"
|
"typescript": "3.9.7"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
"@symphony/symphony-c9-shell": "3.14.99-37",
|
||||||
"auto-update": "file:auto_update",
|
"auto-update": "file:auto_update",
|
||||||
"screen-share-indicator-frame": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.13",
|
"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",
|
"screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet2.git#9.2.2",
|
||||||
@ -2288,6 +2289,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "(Unlicense OR Apache-2.0)"
|
"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": {
|
"node_modules/@szmarczak/http-timer": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
"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=",
|
"integrity": "sha1-jaXGUwkVZT86Hzj9XxAdjD+AecU=",
|
||||||
"dev": true
|
"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": {
|
"@szmarczak/http-timer": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||||
|
@ -170,6 +170,7 @@
|
|||||||
"shell-path": "^3.0.0"
|
"shell-path": "^3.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
"@symphony/symphony-c9-shell": "3.14.99-37",
|
||||||
"auto-update": "file:auto_update",
|
"auto-update": "file:auto_update",
|
||||||
"screen-share-indicator-frame": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.13",
|
"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",
|
"screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet2.git#9.2.2",
|
||||||
|
@ -91,6 +91,10 @@ set installerDir="%CD%\installer\win"
|
|||||||
set distDir="%CD%\dist"
|
set distDir="%CD%\dist"
|
||||||
set rootDir="%CD%"
|
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%" (
|
if NOT EXIST "%PFX_DIR%\%PFX_FILE%" (
|
||||||
echo "can not find .pfx file" "%pfxDir%\%pfxFile%"
|
echo "can not find .pfx file" "%pfxDir%\%pfxFile%"
|
||||||
exit /b -1
|
exit /b -1
|
||||||
|
44
spec/c9PipeHandler.spec.ts
Normal file
44
spec/c9PipeHandler.spec.ts
Normal file
@ -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<String, any>();
|
||||||
|
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' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -7,6 +7,7 @@ import * as windowActions from '../src/app/window-actions';
|
|||||||
import { windowHandler } from '../src/app/window-handler';
|
import { windowHandler } from '../src/app/window-handler';
|
||||||
import * as utils from '../src/app/window-utils';
|
import * as utils from '../src/app/window-utils';
|
||||||
import { apiCmds, apiName } from '../src/common/api-interface';
|
import { apiCmds, apiName } from '../src/common/api-interface';
|
||||||
|
import * as c9PipeHandler from '../src/app/c9-pipe-handler';
|
||||||
import { logger } from '../src/common/logger';
|
import { logger } from '../src/common/logger';
|
||||||
import { BrowserWindow, ipcMain } from './__mocks__/electron';
|
import { BrowserWindow, ipcMain } from './__mocks__/electron';
|
||||||
|
|
||||||
@ -503,5 +504,16 @@ describe('main api handler', () => {
|
|||||||
expect(windows['popout1'].getNativeWindowHandle).toBeCalledTimes(1);
|
expect(windows['popout1'].getNativeWindowHandle).toBeCalledTimes(1);
|
||||||
expect(windows['popout2'].getNativeWindowHandle).toBeCalledTimes(0);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
115
src/app/c9-pipe-handler.ts
Normal file
115
src/app/c9-pipe-handler.ts
Normal file
@ -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();
|
||||||
|
};
|
161
src/app/c9-shell-handler.ts
Normal file
161
src/app/c9-shell-handler.ts
Normal file
@ -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();
|
||||||
|
};
|
@ -18,6 +18,8 @@ import { logger } from '../common/logger';
|
|||||||
import { activityDetection } from './activity-detection';
|
import { activityDetection } from './activity-detection';
|
||||||
import { analytics } from './analytics-handler';
|
import { analytics } from './analytics-handler';
|
||||||
import appStateHandler from './app-state-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 { getCitrixMediaRedirectionStatus } from './citrix-handler';
|
||||||
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
||||||
import { downloadHandler } from './download-handler';
|
import { downloadHandler } from './download-handler';
|
||||||
@ -356,6 +358,18 @@ ipcMain.on(
|
|||||||
swiftSearchInstance.handleMessageEvents(arg.swiftSearchData);
|
swiftSearchInstance.handleMessageEvents(arg.swiftSearchData);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
import { notification } from '../renderer/notification';
|
import { notification } from '../renderer/notification';
|
||||||
import { cleanAppCacheOnCrash } from './app-cache-handler';
|
import { cleanAppCacheOnCrash } from './app-cache-handler';
|
||||||
import { AppMenu } from './app-menu';
|
import { AppMenu } from './app-menu';
|
||||||
|
import { closeC9Pipe } from './c9-pipe-handler';
|
||||||
import { handleChildWindow } from './child-window-handler';
|
import { handleChildWindow } from './child-window-handler';
|
||||||
import {
|
import {
|
||||||
CloudConfigDataTypes,
|
CloudConfigDataTypes,
|
||||||
@ -468,6 +469,8 @@ export class WindowHandler {
|
|||||||
// reset to false when the client reloads
|
// reset to false when the client reloads
|
||||||
this.isMana = false;
|
this.isMana = false;
|
||||||
logger.info(`window-handler: main window web contents finished loading!`);
|
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
|
// early exit if the window has already been destroyed
|
||||||
if (!this.mainWebContents || this.mainWebContents.isDestroyed()) {
|
if (!this.mainWebContents || this.mainWebContents.isDestroyed()) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -65,6 +65,10 @@ export enum apiCmds {
|
|||||||
getNativeWindowHandle = 'get-native-window-handle',
|
getNativeWindowHandle = 'get-native-window-handle',
|
||||||
getCitrixMediaRedirectionStatus = 'get-citrix-media-redirection-status',
|
getCitrixMediaRedirectionStatus = 'get-citrix-media-redirection-status',
|
||||||
getSources = 'getSources',
|
getSources = 'getSources',
|
||||||
|
launchCloud9 = 'launch-cloud9',
|
||||||
|
connectCloud9Pipe = 'connect-cloud9-pipe',
|
||||||
|
writeCloud9Pipe = 'write-cloud9-pipe',
|
||||||
|
closeCloud9Pipe = 'close-cloud9-pipe',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum apiName {
|
export enum apiName {
|
||||||
@ -115,6 +119,8 @@ export interface IApiArgs {
|
|||||||
swiftSearchData: any;
|
swiftSearchData: any;
|
||||||
types: string[];
|
types: string[];
|
||||||
thumbnailSize: Size;
|
thumbnailSize: Size;
|
||||||
|
pipe: string;
|
||||||
|
data: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Themes = 'light' | 'dark';
|
export type Themes = 'light' | 'dark';
|
||||||
@ -269,3 +275,8 @@ export type NotificationActionCallback = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export type ConfigUpdateType = 'restart' | 'reload';
|
export type ConfigUpdateType = 'restart' | 'reload';
|
||||||
|
|
||||||
|
export interface ICloud9Pipe {
|
||||||
|
write(data: Uint8Array): void;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
@ -92,6 +92,8 @@ if (ssfWindow.ssf) {
|
|||||||
getCitrixMediaRedirectionStatus:
|
getCitrixMediaRedirectionStatus:
|
||||||
ssfWindow.ssf.getCitrixMediaRedirectionStatus,
|
ssfWindow.ssf.getCitrixMediaRedirectionStatus,
|
||||||
registerClientBanner: ssfWindow.ssf.registerClientBanner,
|
registerClientBanner: ssfWindow.ssf.registerClientBanner,
|
||||||
|
launchCloud9: ssfWindow.ssf.launchCloud9,
|
||||||
|
connectCloud9Pipe: ssfWindow.ssf.connectCloud9Pipe,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
searchAPIVersion,
|
searchAPIVersion,
|
||||||
version,
|
version,
|
||||||
} from '../../package.json';
|
} from '../../package.json';
|
||||||
|
import { IShellStatus } from '../app/c9-shell-handler';
|
||||||
import { RedirectionStatus } from '../app/citrix-handler';
|
import { RedirectionStatus } from '../app/citrix-handler';
|
||||||
import { IDownloadItem } from '../app/download-handler';
|
import { IDownloadItem } from '../app/download-handler';
|
||||||
import {
|
import {
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
ConfigUpdateType,
|
ConfigUpdateType,
|
||||||
IBadgeCount,
|
IBadgeCount,
|
||||||
IBoundsChange,
|
IBoundsChange,
|
||||||
|
ICloud9Pipe,
|
||||||
ICPUUsage,
|
ICPUUsage,
|
||||||
ILogMsg,
|
ILogMsg,
|
||||||
IMediaPermission,
|
IMediaPermission,
|
||||||
@ -69,6 +71,8 @@ export interface ILocalObject {
|
|||||||
showClientBannerCallback?: Array<
|
showClientBannerCallback?: Array<
|
||||||
(reason: string, action: ConfigUpdateType) => void
|
(reason: string, action: ConfigUpdateType) => void
|
||||||
>;
|
>;
|
||||||
|
c9PipeEventCallback?: (event: string, arg?: any) => void;
|
||||||
|
c9MessageCallback?: (status: IShellStatus) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const local: ILocalObject = {
|
const local: ILocalObject = {
|
||||||
@ -776,6 +780,80 @@ export class SSFApi {
|
|||||||
local.showClientBannerCallback.push(callback);
|
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<ICloud9Pipe> {
|
||||||
|
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<ICloud9Pipe>((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
|
// Invoked whenever the app is reloaded/navigated
|
||||||
const sanitize = (): void => {
|
const sanitize = (): void => {
|
||||||
if (window.name === apiName.mainWindowName) {
|
if (window.name === apiName.mainWindowName) {
|
||||||
|
Loading…
Reference in New Issue
Block a user