/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /// 'use strict'; import * as nls from 'vscode-nls'; import * as path from 'path'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IConfig, ServerProvider } from 'service-downloader'; import { Telemetry } from './telemetry'; import * as utils from './utils'; import { ChildProcess, exec, ExecException } from 'child_process'; import { stringify } from 'querystring'; const baseConfig = require('./config.json'); const localize = nls.loadMessageBundle(); let exePath: string; let runningProcesses: Map = new Map(); // Params to pass to SsmsMin.exe, only an action and server are required - the rest are optional based on the // action used. Exported for use in testing. export interface LaunchSsmsDialogParams { action: string; server: string; database?: string; user?: string; password?: string; useAad?: boolean; urn?: string; } export async function activate(context: vscode.ExtensionContext): Promise { // This is for Windows-specific support so do nothing on other platforms if (process.platform === 'win32') { Telemetry.sendTelemetryEvent('startup/ExtensionActivated'); let config: IConfig = JSON.parse(JSON.stringify(baseConfig)); config.installDirectory = path.join(context.extensionPath, config.installDirectory); config.proxy = utils.getConfiguration('http').get('proxy'); config.strictSSL = utils.getConfiguration('http').get('proxyStrictSSL') || true; const serverdownloader = new ServerProvider(config); const installationStart = Date.now(); try { let downloadedExePath = await serverdownloader.getOrDownloadServer(); const installationComplete = Date.now(); // Don't register the command if we couldn't find the EXE since it won't be able to do anything if (downloadedExePath) { exePath = downloadedExePath; } else { throw new Error('Could not find SsmsMin.exe after downloading'); } // Add the command now that we have the exePath to run the tool with context.subscriptions.push( vscode.commands.registerCommand('adminToolExtWin.launchSsmsServerPropertiesDialog', handleLaunchSsmsServerPropertiesDialogCommand)); Telemetry.sendTelemetryEvent('startup/ExtensionStarted', { installationTime: String(installationComplete - installationStart), beginningTimestamp: String(installationStart) }); } catch (err) { Telemetry.sendTelemetryEvent('startup/ExtensionInitializationFailed'); console.error(`Error Initializing Admin Tool Extension for Windows - ${err}`); } } } export async function deactivate(): Promise { // If the extension is being deactivated we want to kill all processes that are still currently // running otherwise they will continue to run as orphan processes. We use taskkill here in case // they started off child processes of their own runningProcesses.forEach(p => exec('taskkill /pid ' + p.pid + ' /T /F')); } /** * Handler for command to launch SSMS Server Properties dialog * @param connectionId The connection context from the command */ function handleLaunchSsmsServerPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext) { if (connectionContext && connectionContext.connectionProfile) { launchSsmsDialog( /*action*/'sqla:Properties@Microsoft.SqlServer.Management.Smo.Server', /*connectionProfile*/connectionContext.connectionProfile); } } /** * Launches SsmsMin with parameters from the specified connection * @param action The action to launch * @param params The params used to construct the command * @param urn The URN to pass to SsmsMin */ function launchSsmsDialog(action: string, connectionProfile: azdata.IConnectionProfile, urn?: string) { if (!exePath) { vscode.window.showErrorMessage(localize('adminToolExtWin.noExeError', 'Unable to find SsmsMin.exe.')); return; } Telemetry.sendTelemetryEvent('LaunchSsmsDialog', { 'action': action }); let params: LaunchSsmsDialogParams = { action: action, server: connectionProfile.serverName, database: connectionProfile.databaseName, password: connectionProfile.password, user: connectionProfile.userName, useAad: connectionProfile.authenticationType === 'AzureMFA', urn: urn }; let args = buildSsmsMinCommandArgs(params); // This will be an async call since we pass in the callback var proc: ChildProcess = exec( /*command*/`"${exePath}" ${args}`, /*options*/undefined, (execException, stdout, stderr) => { // Process has exited so remove from map of running processes runningProcesses.delete(proc.pid); Telemetry.sendTelemetryEvent('LaunchSsmsDialogResult', { 'action': params.action, 'returnCode': execException && execException.code ? execException.code.toString() : '0' }); let err = stderr.toString(); if (err !== '') { console.warn(`Error calling SsmsMin with args '${args}' - ${err}`); } }); // If we're not using AAD the tool prompts for a password on stdin if (params.useAad !== true) { proc.stdin.end(params.password ? params.password : ''); } // Save the process into our map so we can make sure to stop them if we exit before shutting down runningProcesses.set(proc.pid, proc); } /** * Builds the command arguments to pass to SsmsMin.exe. Values are expected to be escaped correctly * already per their - they will be further escaped * for command-line usage but no additional * escaping will occur. * @param params The params used to build up the command parameter string */ export function buildSsmsMinCommandArgs(params: LaunchSsmsDialogParams): string { return `${params.action ? '-a "' + params.action.replace(/"/g, '\\"') + '"' : ''}\ ${params.server ? ' -S "' + params.server.replace(/"/g, '\\"') + '"' : ''}\ ${params.database ? ' -D "' + params.database.replace(/"/g, '\\"') + '"' : ''}\ ${params.useAad !== true && params.user ? ' -U "' + params.user.replace(/"/g, '\\"') + '"' : ''}\ ${params.useAad === true ? ' -G' : ''}\ ${params.urn ? ' -u "' + params.urn.replace(/"/g, '\\"') + '"' : ''}`; }