chore: 🤖 add ipc handlers for rdp (#3031)

This commit is contained in:
Dharini committed 2025-11-19 14:22:06 -08:00
1 parent 437d0e686d
commit 6554af1164
6 files changed
+205 -10

No files matched your search

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/
const { path } = require('../cli/path.js');
const { path: boundaryPath } = require('../cli/path.js');
const { spawn, spawnSync } = require('child_process');
const jsonify = require('../utils/jsonify.js');
@@ -34,7 +34,7 @@ module.exports = {
*/
spawnAsyncJSONPromise(command, token, timeout) {
return new Promise((resolve, reject) => {
const childProcess = spawn(path, command, {
const childProcess = spawn(boundaryPath, command, {
env: {
...process.env,
BOUNDARY_TOKEN: token,
@@ -78,8 +78,9 @@ module.exports = {
* This function is intended for non-connection related tasks.
* @param {string[]} args
* @param {object} envVars
* @param {string} path
* @returns {{stdout: string | undefined, stderr: string | undefined}} */
spawnSync(args, envVars = {}) {
spawnSync(args, envVars = {}, path = boundaryPath) {
const childProcess = spawnSync(path, args, {
// Some of our outputs (namely cache daemon searching) can be very large.
// This an undocumented hack to allow for an unlimited buffer size which
@@ -103,8 +104,9 @@ module.exports = {
* Resolves on any output from stdout or stderr.
* @param command
* @param options
* @param path
*/
spawn(command, options) {
spawn(command, options, path = boundaryPath) {
return new Promise((resolve, reject) => {
const childProcess = spawn(path, command, options);
childProcess.stdout.on('data', (data) => {
+3
View File
@@ -26,6 +26,7 @@ const { generateCSPHeader } = require('./config/content-security-policy.js');
const runtimeSettings = require('./services/runtime-settings.js');
const sessionManager = require('./services/session-manager.js');
const cacheDaemonManager = require('./services/cache-daemon-manager');
const rdpClientManager = require('./services/rdp-client-manager');
const store = require('./services/electron-store-manager');
const menu = require('./config/menu.js');
@@ -297,6 +298,8 @@ app.on('before-quit', async (event) => {
app.on('quit', () => {
cacheDaemonManager.stop();
// we should stop any active RDP client processes
rdpClientManager.stopAll();
});
// Handle an unhandled error in the main thread
+11 -6
View File
@@ -19,6 +19,7 @@ const cacheDaemonManager = require('../services/cache-daemon-manager');
const clientAgentDaemonManager = require('../services/client-agent-daemon-manager');
const { releaseVersion } = require('../../config/config.js');
const store = require('../services/electron-store-manager');
const rdpClientManager = require('../services/rdp-client-manager');
/**
* Returns the current runtime clusterUrl, which is used by the main thread to
@@ -272,24 +273,28 @@ handle('getLogPath', () => {
/**
* Returns the available RDP clients
*/
handle('getRdpClients', async () => []);
handle('getRdpClients', async () => rdpClientManager.getAvailableRdpClients());
/**
* Returns the preferred RDP client
*/
handle('getPreferredRdpClient', async () => 'none');
handle('getPreferredRdpClient', async () =>
rdpClientManager.getPreferredRdpClient(),
);
/**
* Sets the preferred RDP client
*/
handle('setPreferredRdpClient', async (rdpClient) => rdpClient);
handle('setPreferredRdpClient', (preferredClient) =>
rdpClientManager.setPreferredRdpClient(preferredClient),
);
/**
* Launches the RDP client with the provided session ID.
*/
handle('launchRdpClient', async (sessionId) => {
return;
});
handle('launchRdpClient', async (sessionId) =>
rdpClientManager.launchRdpClient(sessionId, sessionManager),
);
/**
* Handler to help create terminal windows. We don't use the helper `handle` method
@@ -46,6 +46,15 @@ class SessionManager {
return session?.stop?.();
}
/**
* Get session by identifier.
* @param {string} sessionId
* @returns {Session} The session object
*/
getSessionById(sessionId) {
return this.#sessions.find((session) => session.id === sessionId);
}
/**
* Stop all active and pending target sessions
* Returning Promise.all() ensures all sessions in the list have been
@@ -52,6 +52,14 @@ class Session {
return this.#process && !this.#process.killed;
}
/**
* Get proxy details for the session
* @return {Object}
*/
get proxyDetails() {
return this.#proxyDetails;
}
/**
* Generate cli command for session.
* @returns {string[]}
@@ -0,0 +1,168 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
const { spawn, spawnSync } = require('../helpers/spawn-promise');
const { shell } = require('electron');
const fs = require('fs');
const which = require('which');
const { isMac, isWindows } = require('../helpers/platform.js');
const store = require('./electron-store-manager');
// RDP Client Configuration
const RDP_CLIENTS = [
{
value: 'mstsc',
isAvailable: async () => {
if (!isWindows()) return false;
try {
const mstscPath = await which('mstsc', { nothrow: true });
return Boolean(mstscPath);
} catch {
return false;
}
},
},
{
value: 'windows-app',
isAvailable: () => {
if (!isMac()) return false;
try {
// Check for Windows App
if (fs.existsSync('/Applications/Windows App.app')) {
return true;
}
// Fallback: Use mdfind for Microsoft Remote Desktop
else {
const result = spawnSync(
['kMDItemCFBundleIdentifier == "com.microsoft.rdc.macos"'],
{},
'mdfind',
);
// `mdfind` returns an object with stdout containing the path of the app if found
// Example: '/Applications/Windows App.app\n'
return result.stdout && result.stdout.trim().length > 0;
}
} catch {
return false;
}
},
},
{
value: 'none',
isAvailable: () => true,
},
];
class RdpClientManager {
// Track active RDP processes for cleanup
#activeProcesses = [];
/**
* Gets all available RDP clients on the current system
* @returns {Promise<Array>} Array of available RDP client values
*/
async getAvailableRdpClients() {
const available = [];
for (const client of RDP_CLIENTS) {
if (await client.isAvailable()) {
available.push(client.value);
}
}
return available;
}
/**
* Gets the best default RDP client (first available that's not 'none')
* @returns {Promise<string>} Best RDP client value or 'none'
*/
async getBestDefaultRdpClient() {
const availableClients = await this.getAvailableRdpClients();
const bestClient = availableClients.find((client) => client !== 'none');
return bestClient ?? 'none';
}
/**
* Gets the user's preferred RDP client, auto-detecting if not set
* @returns {Promise<string>} Preferred RDP client value
*/
async getPreferredRdpClient() {
let preferredClient = store.get('preferredRdpClient');
if (!preferredClient) {
// Auto-detect and set the best available client
preferredClient = await this.getBestDefaultRdpClient();
store.set('preferredRdpClient', preferredClient);
}
return preferredClient;
}
/**
* Sets the user's preferred RDP client
* @param {string} preferredClient - The RDP client value to set as preferred
*/
setPreferredRdpClient(preferredClient) {
if (!preferredClient) {
store.set('preferredRdpClient', 'none');
} else {
store.set('preferredRdpClient', preferredClient);
}
}
/**
* Launches RDP connection with the specified address and port
* @param {string} address - Target address
* @param {number} port - Target port
*/
async launchRdpConnection(address, port) {
if (isWindows()) {
// Launch Windows mstsc and track it for cleanup
const mstscArgs = [`/v:${address}:${port}`];
const { childProcess } = await spawn(mstscArgs, {}, 'mstsc');
// Add to activeProcesses array for cleanup
this.#activeProcesses.push(childProcess);
} else if (isMac()) {
// Launch macOS RDP URL - no process to track as it's handled by the system
const fullAddress = `${address}:${port}`;
const encoded = encodeURIComponent(`full address=s:${fullAddress}`);
const rdpUrl = `rdp://${encoded}`;
await shell.openExternal(rdpUrl);
}
}
/**
* Launches RDP client using session ID
* Retrieves session object from session manager and launches appropriate RDP client
* @param {string} sessionId - The session ID to get session for
* @param {Object} sessionManager - Session manager instance to get session from
*/
async launchRdpClient(sessionId, sessionManager) {
// Get session object from session manager
const session = sessionManager.getSessionById(sessionId);
if (!session) {
return;
}
const {
proxyDetails: { address, port },
} = session;
// Launch RDP connection
await this.launchRdpConnection(address, port);
}
/**
* Stop all active RDP processes
*/
stopAll() {
for (const process of this.#activeProcesses) {
if (!process.killed) {
process.kill();
}
}
// Clear the active processes array after stopping all processes
this.#activeProcesses = [];
}
}
module.exports = new RdpClientManager();