mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 11:51:07 +00:00
ui and functional changes
This commit is contained in:
@@ -18,10 +18,18 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
|
||||
const fitAddon = new FitAddon();
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize XTerm instance
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
theme: { background: "#1d1f21", foreground: "#c5c8c6" }
|
||||
// Initialize XTerm instance
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'bar',
|
||||
fontFamily: 'Courier New, monospace',
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: "#1e1e1e",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#ffcc00"
|
||||
},
|
||||
scrollback: 1000
|
||||
});
|
||||
|
||||
term.loadAddon(fitAddon);
|
||||
@@ -30,8 +38,23 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
|
||||
if (terminalRef.current) {
|
||||
term.open(terminalRef.current);
|
||||
xtermRef.current = term;
|
||||
|
||||
// Ensure the CSS is injected only once
|
||||
if (!document.getElementById("xterm-custom-style")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "xterm-custom-style"; // Unique ID to prevent duplicates
|
||||
style.innerHTML = `
|
||||
.xterm-text-layer {
|
||||
transform: translateX(10px); /* Adds left padding */
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
fitAddon.fit();
|
||||
|
||||
// Adjust terminal size on window resize
|
||||
const handleResize = () => fitAddon.fit();
|
||||
@@ -47,14 +70,20 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
|
||||
|
||||
// Cleanup function to close WebSocket and dispose terminal
|
||||
return () => {
|
||||
if (!socketRef.current) return; // Prevent errors if WebSocket is not initialized
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close(); // Close WebSocket connection
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
term.dispose(); // Clean up XTerm instance
|
||||
|
||||
const styleElement = document.getElementById("xterm-custom-style");
|
||||
if (styleElement) {
|
||||
styleElement.remove(); // Clean up CSS on unmount
|
||||
}
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
return <div ref={terminalRef} style={{ width: "100%", height: "500px" }} />;
|
||||
return <div ref={terminalRef} style={{ width: "100%", height: "500px"}} />;
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export const putEphemeralUserSettings = async (userSubscriptionId: string, userR
|
||||
preferredOsType: OsType.Linux,
|
||||
preferredShellType: ShellType.Bash,
|
||||
preferredLocation: userRegion,
|
||||
networkType: NetworkType.Default,
|
||||
networkType: (!vNetSettings || Object.keys(vNetSettings).length === 0) ? NetworkType.Default : (vNetSettings ? NetworkType.Isolated : NetworkType.Default),
|
||||
sessionType: SessionType.Ephemeral,
|
||||
userSubscription: userSubscriptionId,
|
||||
vnetSettings: vNetSettings ?? {}
|
||||
|
||||
@@ -38,9 +38,9 @@ export type UserSettingProperties = {
|
||||
}
|
||||
|
||||
export type VnetSettings = {
|
||||
networkProfileResourceId: string;
|
||||
relayNamespaceResourceId: string;
|
||||
location: string;
|
||||
networkProfileResourceId?: string;
|
||||
relayNamespaceResourceId?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export type ProvisionConsoleResponse = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
import { RelayNamespaceResponse, VnetModel } from "Explorer/Tabs/CloudShellTab/DataModels";
|
||||
import { RelayNamespaceResponse, VnetModel, VnetSettings } from "Explorer/Tabs/CloudShellTab/DataModels";
|
||||
import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { Terminal } from "xterm";
|
||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
||||
@@ -15,22 +15,37 @@ import { LogError, LogInfo } from "./LogFormatter";
|
||||
export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => {
|
||||
|
||||
// validate that the subscription id is registered in the CloudShell namespace
|
||||
terminal.writeln("");
|
||||
try {
|
||||
const response: any = await verifyCloudShellProviderRegistration(userContext.subscriptionId);
|
||||
if (response.registrationState !== "Registered") {
|
||||
await registerCloudShellProvider(userContext.subscriptionId);
|
||||
}
|
||||
terminal.writeln("\x1B[34mVerifying CloudShell provider registration...\x1B[0m");
|
||||
const response: any = await verifyCloudShellProviderRegistration(userContext.subscriptionId);
|
||||
if (response.registrationState !== "Registered") {
|
||||
terminal.writeln("\x1B[33mCloudShell provider registration is not found. Registering now...\x1B[0m");
|
||||
await registerCloudShellProvider(userContext.subscriptionId);
|
||||
terminal.writeln("\x1B[32mCloudShell provider registration completed successfully.\x1B[0m");
|
||||
}
|
||||
} catch (err) {
|
||||
terminal.writeln(LogError('Unable to verify CloudShell provider registration.'));
|
||||
terminal.writeln("\x1B[31mError: Unable to verify CloudShell provider registration.\x1B[0m");
|
||||
throw err;
|
||||
}
|
||||
|
||||
const settings = await getUserSettings();
|
||||
let vNetSettings = {};
|
||||
terminal.writeln("");
|
||||
terminal.writeln("\x1B[34mFetching user settings...\x1B[0m");
|
||||
let settings;
|
||||
try {
|
||||
settings = await getUserSettings();
|
||||
}
|
||||
catch (err) {
|
||||
terminal.writeln("\x1B[33mNo user settings found. Proceeding with default settings.\x1B[0m");
|
||||
}
|
||||
|
||||
let vNetSettings: VnetSettings;
|
||||
if(settings?.properties?.vnetSettings && Object.keys(settings?.properties?.vnetSettings).length > 0) {
|
||||
|
||||
terminal.writeln(" Network Profile Resource ID: " + settings.properties.vnetSettings.networkProfileResourceId);
|
||||
terminal.writeln(" Relay Namespace Resource ID: " + settings.properties.vnetSettings.relayNamespaceResourceId);
|
||||
terminal.writeln("\x1B[1mUsing existing VNet settings:\x1B[0m");
|
||||
terminal.writeln(" - Network Profile Resource ID: \x1B[32m" + settings.properties.vnetSettings.networkProfileResourceId + "\x1B[0m");
|
||||
terminal.writeln(" - Relay Namespace Resource ID: \x1B[32m" + settings.properties.vnetSettings.relayNamespaceResourceId + "\x1B[0m");
|
||||
terminal.writeln(" - VNet Location: \x1B[32m" + settings.properties.vnetSettings.location + "\x1B[0m");
|
||||
|
||||
vNetSettings = {
|
||||
networkProfileResourceId: settings.properties.vnetSettings.networkProfileResourceId,
|
||||
@@ -40,103 +55,101 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
||||
}
|
||||
else
|
||||
{
|
||||
terminal.writeln("\x1B[1;31m No VNet settings found. Continuing with default settings\x1B[0m");
|
||||
terminal.writeln("\x1B[33mNo existing VNet settings found. Proceeding with default settings.\x1B[0m");
|
||||
}
|
||||
|
||||
terminal.writeln("\x1B[1;37m Press '1' to continue with current or default setting.\x1B[0m");
|
||||
terminal.writeln("\x1B[1;37m Press '2' to configure new VNet to CloudShell.\x1B[0m");
|
||||
terminal.writeln("\x1B[1;37m Press '3' to Reset CloudShell VNet Settings.\x1B[0m");
|
||||
terminal.writeln("\x1B[1;37m Note: To learn how to configure VNet for CloudShell, go to this link https://aka.ms/cloudhellvnetsetup \x1B[0m");
|
||||
terminal.writeln("");
|
||||
|
||||
terminal.writeln("\x1B[1mSelect an option to continue:\x1B[0m");
|
||||
terminal.writeln("\x1B[33m1 - Use current/default VNet settings\x1B[0m");
|
||||
terminal.writeln("\x1B[33m2 - Configure a new VNet for CloudShell\x1B[0m");
|
||||
terminal.writeln("\x1B[33m3 - Reset CloudShell VNet settings to default\x1B[0m");
|
||||
terminal.writeln("\x1B[34mFor details on configuring VNet for CloudShell, visit: https://aka.ms/cloudshellvnetsetup\x1B[0m");
|
||||
|
||||
terminal.focus();
|
||||
|
||||
let isDefaultSetting = false;
|
||||
const handleKeyPress = terminal.onKey(async ({ key }: { key: string }) => {
|
||||
|
||||
terminal.writeln("")
|
||||
if (key === "1") {
|
||||
terminal.writeln("\x1B[1;32m Pressed 1, Continuing with current/default settings.\x1B[0m");
|
||||
|
||||
isDefaultSetting = true;
|
||||
terminal.writeln("\x1B[34mYou selected option 1: Proceeding with current/default settings.\x1B[0m");
|
||||
handleKeyPress.dispose();
|
||||
}
|
||||
else if (key === "2") {
|
||||
isDefaultSetting = false;
|
||||
terminal.writeln("\x1B[34mYou selected option 2: Please provide the following details.\x1B[0m");
|
||||
handleKeyPress.dispose();
|
||||
|
||||
const subscriptionId = await askQuestion(terminal, "Enter VNet Subscription ID");
|
||||
const resourceGroup = await askQuestion(terminal, "Enter VNet Resource Group");
|
||||
const vNetName = await askQuestion(terminal, "Enter VNet Name");
|
||||
|
||||
terminal.writeln("\x1B[34mFetching VNet details...\x1B[0m");
|
||||
const vNetConfig = await getARMInfo<VnetModel>(`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vNetName}`);
|
||||
|
||||
terminal.writeln(" - VNet Location: \x1B[32m" + vNetConfig.location + "\x1B[0m");
|
||||
terminal.writeln(" - Available Network Profiles:");
|
||||
const ipConfigurationProfiles = vNetConfig.properties.subnets.reduce<{ id: string }[]>(
|
||||
(acc, subnet) => acc.concat(subnet.properties.ipConfigurationProfiles || []),
|
||||
[]);
|
||||
for (let i = 0; i < ipConfigurationProfiles.length; i++) {
|
||||
const match = ipConfigurationProfiles[i].id.match(/\/networkProfiles\/([^/]+)/);
|
||||
const result = match ? `${match[1]}` : null;
|
||||
terminal.writeln(" - \x1B[32m" + result + "\x1B[0m");
|
||||
}
|
||||
|
||||
const networkProfile = await askQuestion(terminal, "Network Profile to use");
|
||||
|
||||
const relays = await getARMInfo<RelayNamespaceResponse>(
|
||||
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces`,
|
||||
"2024-01-01");
|
||||
terminal.writeln(" - Available Network Relays:");
|
||||
for (let i = 0; i < relays.value.length; i++) {
|
||||
terminal.writeln(" - \x1B[32m" + relays.value[i].name + "\x1B[0m");
|
||||
}
|
||||
|
||||
const relayName = await askQuestion(terminal, "Network Relay to use:");
|
||||
|
||||
const networkProfileResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfile.replace(/[\n\r]/g, "")}`;
|
||||
const relayResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces/${relayName.replace(/[\n\r]/g, "")}`;
|
||||
|
||||
vNetSettings = {
|
||||
networkProfileResourceId: networkProfileResourceId,
|
||||
relayNamespaceResourceId: relayResourceId,
|
||||
location: vNetConfig.location
|
||||
};
|
||||
}
|
||||
else if (key === "3") {
|
||||
isDefaultSetting = true;
|
||||
terminal.writeln("\x1B[34mYou selected option 3: Resetting VNet settings to default.\x1B[0m");
|
||||
|
||||
vNetSettings = {};
|
||||
handleKeyPress.dispose();
|
||||
}
|
||||
else {
|
||||
terminal.writeln("\x1B[1;31m Entered Wrong Input, only 1 or 2 are allowed. Exiting...\x1B[0m");
|
||||
terminal.writeln("\x1B[31mInvalid selection. Only options 1, 2, and 3 are allowed. Exiting...\x1B[0m");
|
||||
setTimeout(() => terminal.dispose(), 2000); // Close terminal after 2 sec
|
||||
handleKeyPress.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefaultSetting) {
|
||||
terminal.writeln("\x1B[1;32m Pressed 2, Enter below details:\x1B[0m");
|
||||
const subscriptionId = await askQuestion(terminal, "Existing VNet Subscription ID");
|
||||
const resourceGroup = await askQuestion(terminal, "Existing VNet Resource Group");
|
||||
const vNetName = await askQuestion(terminal, "Existing VNet Name");
|
||||
terminal.writeln("\x1B[34mDetermining CloudShell region...\x1B[0m");
|
||||
const region = vNetSettings?.location ?? userContext.databaseAccount?.location;
|
||||
terminal.writeln(" - Identified Database Account Region: \x1B[32m" + region + "\x1B[0m");
|
||||
|
||||
const vNetConfig = await getARMInfo<VnetModel>(`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vNetName}`);
|
||||
|
||||
terminal.writeln("Suggested Network Profiles:");
|
||||
const ipConfigurationProfiles = vNetConfig.properties.subnets.reduce<{ id: string }[]>(
|
||||
(acc, subnet) => acc.concat(subnet.properties.ipConfigurationProfiles || []),
|
||||
[]
|
||||
);
|
||||
for (let i = 0; i < ipConfigurationProfiles.length; i++) {
|
||||
const match = ipConfigurationProfiles[i].id.match(/\/networkProfiles\/([^/]+)/);
|
||||
const result = match ? `/${match[1]}` : null;
|
||||
terminal.writeln(result);
|
||||
}
|
||||
|
||||
const networkProfile = await askQuestion(terminal, "Associated Network Profile");
|
||||
|
||||
const relays = await getARMInfo<RelayNamespaceResponse>(
|
||||
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces`,
|
||||
"2024-01-01");
|
||||
terminal.writeln("Suggested Network Relays:");
|
||||
for (let i = 0; i < relays.value.length; i++) {
|
||||
terminal.writeln(relays.value[i].name);
|
||||
}
|
||||
|
||||
const relayName = await askQuestion(terminal, "Network Relay");
|
||||
|
||||
const networkProfileResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfile}`;
|
||||
const relayResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`;
|
||||
|
||||
// const vNetRules = userContext.databaseAccount.properties?.virtualNetworkRules;
|
||||
// if(vNetRules && vNetRules.length > 0) {
|
||||
// for (let i = 0; i < vNetRules.length; i++) {
|
||||
// const vNetName = vNetRules[i].id;
|
||||
|
||||
// terminal.writeln(vNetName);
|
||||
// }
|
||||
// }
|
||||
|
||||
vNetSettings = {
|
||||
networkProfileResourceId: networkProfileResourceId,
|
||||
relayNamespaceResourceId: relayResourceId,
|
||||
location: userContext.databaseAccount.location
|
||||
};
|
||||
}
|
||||
|
||||
const region = userContext.databaseAccount?.location;
|
||||
terminal.writeln(LogInfo(` Database Account Region identified as '${region}'`));
|
||||
|
||||
const defaultCloudShellRegion = "westus";
|
||||
const resolvedRegion = getNormalizedRegion(region, defaultCloudShellRegion);
|
||||
|
||||
terminal.writeln("\x1B[33mAttempting to provision CloudShell in region: \x1B[1m" + resolvedRegion + "\x1B[0m");
|
||||
|
||||
try {
|
||||
terminal.writeln("\x1B[34mProvisioning CloudShell session...\x1B[0m");
|
||||
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(resolvedRegion, terminal, vNetSettings);
|
||||
}
|
||||
catch (err) {
|
||||
terminal.writeln(LogError(err));
|
||||
terminal.writeln(LogError(`Unable to provision console in request region, Falling back to default region i.e. ${defaultCloudShellRegion}`));
|
||||
terminal.writeln("\x1B[31mError: Unable to provision CloudShell in the requested region.\x1B[0m");
|
||||
terminal.writeln("\x1B[33mFalling back to default region: " + defaultCloudShellRegion + "\x1B[0m");
|
||||
terminal.writeln("\x1B[34mAttempting to provision CloudShell in the default region...\x1B[0m");
|
||||
|
||||
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(defaultCloudShellRegion, terminal, vNetSettings);
|
||||
}
|
||||
|
||||
@@ -162,12 +175,15 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
||||
|
||||
// authorize the session
|
||||
try {
|
||||
terminal.writeln("\x1B[34mAuthorizing CloudShell session...\x1B[0m");
|
||||
|
||||
const authorizeResponse = await authorizeSession(provisionConsoleResponse.properties.uri);
|
||||
const cookieToken = authorizeResponse.token;
|
||||
const a = document.createElement("img");
|
||||
a.src = targetUri + "&token=" + encodeURIComponent(cookieToken);
|
||||
terminal.writeln("\x1B[32mAuthorization successful. Establishing connection...\x1B[0m");
|
||||
} catch (err) {
|
||||
terminal.writeln(LogError('Unable to authorize the session'));
|
||||
terminal.writeln("\x1B[31mError: Unable to authorize CloudShell session.\x1B[0m");
|
||||
socket.close();
|
||||
throw err;
|
||||
}
|
||||
@@ -182,7 +198,8 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
||||
|
||||
const askQuestion = (terminal: any, question: string) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
terminal.write(`\x1B[1;34m${question}: \x1B[0m`);
|
||||
const prompt = `\x1B[1;34m${question}: \x1B[0m`;
|
||||
terminal.writeln(prompt);
|
||||
terminal.focus();
|
||||
let response = "";
|
||||
|
||||
@@ -194,11 +211,18 @@ const askQuestion = (terminal: any, question: string) => {
|
||||
} else if (data === "\u007F" || data === "\b") { // Handle backspace
|
||||
if (response.length > 0) {
|
||||
response = response.slice(0, -1);
|
||||
terminal.write("\b \b"); // Move cursor back, clear character
|
||||
terminal.write("\x1B[D \x1B[D");// Move cursor back, clear character
|
||||
}
|
||||
} else {
|
||||
} else if (data.charCodeAt(0) >= 32) { // Ignore control characters
|
||||
response += data;
|
||||
terminal.write(data); // Display the typed or pasted characters
|
||||
terminal.write(data); // Display typed characters
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent cursor movement beyond the prompt
|
||||
terminal.onKey(({ domEvent} : { domEvent: KeyboardEvent }) => {
|
||||
if (domEvent.key === "ArrowLeft" && response.length === 0) {
|
||||
domEvent.preventDefault(); // Stop moving left beyond the question
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -299,10 +323,12 @@ const provisionCloudShellSession = async(
|
||||
vNetSettings: object
|
||||
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
terminal.writeln("");
|
||||
// Show consent message inside the terminal
|
||||
terminal.writeln(`\x1B[1;33m ⚠️ Are you agreeing to continue with CloudShell terminal at ${resolvedRegion}.\x1B[0m`);
|
||||
terminal.writeln("\x1B[1;37m Press 'Y' to continue or 'N' to exit.\x1B[0m");
|
||||
|
||||
terminal.writeln(`\x1B[1;33m⚠️ CloudShell is not available in your database account region.\x1B[0m`);
|
||||
terminal.writeln(`\x1B[1;33mWould you like to continue with CloudShell in ${resolvedRegion} instead?\x1B[0m`);
|
||||
terminal.writeln("\x1B[1;37mPress 'Y' to proceed or 'N' to cancel.\x1B[0m");
|
||||
|
||||
terminal.focus();
|
||||
// Listen for user input
|
||||
const handleKeyPress = terminal.onKey(async ({ key }: { key: string }) => {
|
||||
@@ -310,8 +336,9 @@ const provisionCloudShellSession = async(
|
||||
handleKeyPress.dispose();
|
||||
|
||||
if (key.toLowerCase() === "y") {
|
||||
terminal.writeln("\x1B[1;32mConsent given. Requesting CloudShell. !\x1B[0m");
|
||||
terminal.writeln(LogInfo('Applying fresh user settings...'));
|
||||
terminal.writeln("\x1B[1;32m✔ Consent received. Requesting CloudShell...\x1B[0m");
|
||||
terminal.writeln("");
|
||||
terminal.writeln(LogInfo('Applying User Settings...'));
|
||||
try {
|
||||
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion, vNetSettings);
|
||||
} catch (err) {
|
||||
@@ -327,19 +354,20 @@ const provisionCloudShellSession = async(
|
||||
terminal.writeln(LogError('Unable to provision console.'));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
|
||||
// Add check for provisioning state
|
||||
if (provisionConsoleResponse.properties.provisioningState !== "Succeeded") {
|
||||
terminal.writeln(LogError("Failed to provision console."));
|
||||
return reject(new Error("Failed to provision console."));
|
||||
}
|
||||
|
||||
terminal.writeln(LogInfo("Connecting to CloudShell Terminal..."));
|
||||
terminal.writeln("\x1B[34mConnecting to CloudShell Terminal...\x1B[0m");
|
||||
// connect the terminal
|
||||
let connectTerminalResponse;
|
||||
try {
|
||||
connectTerminalResponse = await connectTerminal(provisionConsoleResponse.properties.uri, { rows: terminal.rows, cols: terminal.cols });
|
||||
} catch (err) {
|
||||
terminal.writeln(LogError('Unable to connect terminal.'));
|
||||
terminal.writeln(LogError(`Unable to connect terminal. ${err}`));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
@@ -359,7 +387,7 @@ const provisionCloudShellSession = async(
|
||||
|
||||
} else if (key.toLowerCase() === "n") {
|
||||
|
||||
terminal.writeln("\x1B[1;31m Consent denied. Exiting...\x1B[0m");
|
||||
terminal.writeln("\x1B[1;31mConsent denied. Exiting...\x1B[0m");
|
||||
setTimeout(() => terminal.dispose(), 2000); // Close terminal after 2 sec
|
||||
return resolve({});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user