Skip to content

Commit 43f03ae

Browse files
authored
chore: 🤖 add ipc handlers for rdp (#3031)
1 parent e749900 commit 43f03ae

File tree

6 files changed

+205
-10
lines changed

6 files changed

+205
-10
lines changed

‎ui/desktop/electron-app/src/helpers/spawn-promise.js‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: BUSL-1.1
44
*/
55

6-
const { path } = require('../cli/path.js');
6+
const { path: boundaryPath } = require('../cli/path.js');
77
const { spawn, spawnSync } = require('child_process');
88
const jsonify = require('../utils/jsonify.js');
99

@@ -34,7 +34,7 @@ module.exports = {
3434
*/
3535
spawnAsyncJSONPromise(command, token, timeout) {
3636
return new Promise((resolve, reject) => {
37-
const childProcess = spawn(path, command, {
37+
const childProcess = spawn(boundaryPath, command, {
3838
env: {
3939
...process.env,
4040
BOUNDARY_TOKEN: token,
@@ -78,8 +78,9 @@ module.exports = {
7878
* This function is intended for non-connection related tasks.
7979
* @param {string[]} args
8080
* @param {object} envVars
81+
* @param {string} path
8182
* @returns {{stdout: string | undefined, stderr: string | undefined}} */
82-
spawnSync(args, envVars = {}) {
83+
spawnSync(args, envVars = {}, path = boundaryPath) {
8384
const childProcess = spawnSync(path, args, {
8485
// Some of our outputs (namely cache daemon searching) can be very large.
8586
// This an undocumented hack to allow for an unlimited buffer size which
@@ -103,8 +104,9 @@ module.exports = {
103104
* Resolves on any output from stdout or stderr.
104105
* @param command
105106
* @param options
107+
* @param path
106108
*/
107-
spawn(command, options) {
109+
spawn(command, options, path = boundaryPath) {
108110
return new Promise((resolve, reject) => {
109111
const childProcess = spawn(path, command, options);
110112
childProcess.stdout.on('data', (data) => {

‎ui/desktop/electron-app/src/index.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const { generateCSPHeader } = require('./config/content-security-policy.js');
2626
const runtimeSettings = require('./services/runtime-settings.js');
2727
const sessionManager = require('./services/session-manager.js');
2828
const cacheDaemonManager = require('./services/cache-daemon-manager');
29+
const rdpClientManager = require('./services/rdp-client-manager');
2930
const store = require('./services/electron-store-manager');
3031

3132
const menu = require('./config/menu.js');
@@ -297,6 +298,8 @@ app.on('before-quit', async (event) => {
297298

298299
app.on('quit', () => {
299300
cacheDaemonManager.stop();
301+
// we should stop any active RDP client processes
302+
rdpClientManager.stopAll();
300303
});
301304

302305
// Handle an unhandled error in the main thread

‎ui/desktop/electron-app/src/ipc/handlers.js‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const cacheDaemonManager = require('../services/cache-daemon-manager');
1919
const clientAgentDaemonManager = require('../services/client-agent-daemon-manager');
2020
const { releaseVersion } = require('../../config/config.js');
2121
const store = require('../services/electron-store-manager');
22+
const rdpClientManager = require('../services/rdp-client-manager');
2223

2324
/**
2425
* Returns the current runtime clusterUrl, which is used by the main thread to
@@ -272,24 +273,28 @@ handle('getLogPath', () => {
272273
/**
273274
* Returns the available RDP clients
274275
*/
275-
handle('getRdpClients', async () => []);
276+
handle('getRdpClients', async () => rdpClientManager.getAvailableRdpClients());
276277

277278
/**
278279
* Returns the preferred RDP client
279280
*/
280-
handle('getPreferredRdpClient', async () => 'none');
281+
handle('getPreferredRdpClient', async () =>
282+
rdpClientManager.getPreferredRdpClient(),
283+
);
281284

282285
/**
283286
* Sets the preferred RDP client
284287
*/
285-
handle('setPreferredRdpClient', async (rdpClient) => rdpClient);
288+
handle('setPreferredRdpClient', (preferredClient) =>
289+
rdpClientManager.setPreferredRdpClient(preferredClient),
290+
);
286291

287292
/**
288293
* Launches the RDP client with the provided session ID.
289294
*/
290-
handle('launchRdpClient', async (sessionId) => {
291-
return;
292-
});
295+
handle('launchRdpClient', async (sessionId) =>
296+
rdpClientManager.launchRdpClient(sessionId, sessionManager),
297+
);
293298

294299
/**
295300
* Handler to help create terminal windows. We don't use the helper `handle` method

‎ui/desktop/electron-app/src/models/session-manager.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ class SessionManager {
4646
return session?.stop?.();
4747
}
4848

49+
/**
50+
* Get session by identifier.
51+
* @param {string} sessionId
52+
* @returns {Session} The session object
53+
*/
54+
getSessionById(sessionId) {
55+
return this.#sessions.find((session) => session.id === sessionId);
56+
}
57+
4958
/**
5059
* Stop all active and pending target sessions
5160
* Returning Promise.all() ensures all sessions in the list have been

‎ui/desktop/electron-app/src/models/session.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ class Session {
5151
return this.#process && !this.#process.killed;
5252
}
5353

54+
/**
55+
* Get proxy details for the session
56+
* @return {Object}
57+
*/
58+
get proxyDetails() {
59+
return this.#proxyDetails;
60+
}
61+
5462
/**
5563
* Generate cli command for session.
5664
* @returns {string[]}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: BUSL-1.1
4+
*/
5+
const { spawn, spawnSync } = require('../helpers/spawn-promise');
6+
const { shell } = require('electron');
7+
const fs = require('fs');
8+
const which = require('which');
9+
const { isMac, isWindows } = require('../helpers/platform.js');
10+
const store = require('./electron-store-manager');
11+
12+
// RDP Client Configuration
13+
const RDP_CLIENTS = [
14+
{
15+
value: 'mstsc',
16+
isAvailable: async () => {
17+
if (!isWindows()) return false;
18+
try {
19+
const mstscPath = await which('mstsc', { nothrow: true });
20+
return Boolean(mstscPath);
21+
} catch {
22+
return false;
23+
}
24+
},
25+
},
26+
{
27+
value: 'windows-app',
28+
isAvailable: () => {
29+
if (!isMac()) return false;
30+
try {
31+
// Check for Windows App
32+
if (fs.existsSync('/Applications/Windows App.app')) {
33+
return true;
34+
}
35+
36+
// Fallback: Use mdfind for Microsoft Remote Desktop
37+
else {
38+
const result = spawnSync(
39+
['kMDItemCFBundleIdentifier == "com.microsoft.rdc.macos"'],
40+
{},
41+
'mdfind',
42+
);
43+
// `mdfind` returns an object with stdout containing the path of the app if found
44+
// Example: '/Applications/Windows App.app\n'
45+
return result.stdout && result.stdout.trim().length > 0;
46+
}
47+
} catch {
48+
return false;
49+
}
50+
},
51+
},
52+
{
53+
value: 'none',
54+
isAvailable: () => true,
55+
},
56+
];
57+
58+
class RdpClientManager {
59+
// Track active RDP processes for cleanup
60+
#activeProcesses = [];
61+
/**
62+
* Gets all available RDP clients on the current system
63+
* @returns {Promise<Array>} Array of available RDP client values
64+
*/
65+
async getAvailableRdpClients() {
66+
const available = [];
67+
for (const client of RDP_CLIENTS) {
68+
if (await client.isAvailable()) {
69+
available.push(client.value);
70+
}
71+
}
72+
return available;
73+
}
74+
75+
/**
76+
* Gets the best default RDP client (first available that's not 'none')
77+
* @returns {Promise<string>} Best RDP client value or 'none'
78+
*/
79+
async getBestDefaultRdpClient() {
80+
const availableClients = await this.getAvailableRdpClients();
81+
const bestClient = availableClients.find((client) => client !== 'none');
82+
return bestClient ?? 'none';
83+
}
84+
85+
/**
86+
* Gets the user's preferred RDP client, auto-detecting if not set
87+
* @returns {Promise<string>} Preferred RDP client value
88+
*/
89+
async getPreferredRdpClient() {
90+
let preferredClient = store.get('preferredRdpClient');
91+
92+
if (!preferredClient) {
93+
// Auto-detect and set the best available client
94+
preferredClient = await this.getBestDefaultRdpClient();
95+
store.set('preferredRdpClient', preferredClient);
96+
}
97+
return preferredClient;
98+
}
99+
100+
/**
101+
* Sets the user's preferred RDP client
102+
* @param {string} preferredClient - The RDP client value to set as preferred
103+
*/
104+
setPreferredRdpClient(preferredClient) {
105+
if (!preferredClient) {
106+
store.set('preferredRdpClient', 'none');
107+
} else {
108+
store.set('preferredRdpClient', preferredClient);
109+
}
110+
}
111+
112+
/**
113+
* Launches RDP connection with the specified address and port
114+
* @param {string} address - Target address
115+
* @param {number} port - Target port
116+
*/
117+
async launchRdpConnection(address, port) {
118+
if (isWindows()) {
119+
// Launch Windows mstsc and track it for cleanup
120+
const mstscArgs = [`/v:${address}:${port}`];
121+
const { childProcess } = await spawn(mstscArgs, {}, 'mstsc');
122+
// Add to activeProcesses array for cleanup
123+
this.#activeProcesses.push(childProcess);
124+
} else if (isMac()) {
125+
// Launch macOS RDP URL - no process to track as it's handled by the system
126+
const fullAddress = `${address}:${port}`;
127+
const encoded = encodeURIComponent(`full address=s:${fullAddress}`);
128+
const rdpUrl = `rdp://${encoded}`;
129+
await shell.openExternal(rdpUrl);
130+
}
131+
}
132+
133+
/**
134+
* Launches RDP client using session ID
135+
* Retrieves session object from session manager and launches appropriate RDP client
136+
* @param {string} sessionId - The session ID to get session for
137+
* @param {Object} sessionManager - Session manager instance to get session from
138+
*/
139+
async launchRdpClient(sessionId, sessionManager) {
140+
// Get session object from session manager
141+
const session = sessionManager.getSessionById(sessionId);
142+
143+
if (!session) {
144+
return;
145+
}
146+
147+
const {
148+
proxyDetails: { address, port },
149+
} = session;
150+
// Launch RDP connection
151+
await this.launchRdpConnection(address, port);
152+
}
153+
154+
/**
155+
* Stop all active RDP processes
156+
*/
157+
stopAll() {
158+
for (const process of this.#activeProcesses) {
159+
if (!process.killed) {
160+
process.kill();
161+
}
162+
}
163+
// Clear the active processes array after stopping all processes
164+
this.#activeProcesses = [];
165+
}
166+
}
167+
168+
module.exports = new RdpClientManager();

0 commit comments

Comments
 (0)