From cffc18d5eaf42ece447fd481a23718b6970cc31a Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Wed, 31 Oct 2018 20:32:17 -0700 Subject: [PATCH] Revert "Revert "Add a command line interface for connecting to a SQL Server (#3047)"" This reverts commit a747b6a5000453e89c9d0e5f6418ad018206442d. --- .../parts/commandLine/common/commandLine.ts | 17 ++ .../commandLine/common/commandLineService.ts | 77 ++++++++ .../common/connectionManagementService.ts | 36 ++-- .../commandLine/commandLineService.test.ts | 176 ++++++++++++++++++ .../environment/common/environment.ts | 7 + src/vs/platform/environment/node/argv.ts | 19 +- .../workbench/electron-browser/workbench.ts | 8 +- 7 files changed, 318 insertions(+), 22 deletions(-) create mode 100644 src/sql/parts/commandLine/common/commandLine.ts create mode 100644 src/sql/parts/commandLine/common/commandLineService.ts create mode 100644 src/sqltest/parts/commandLine/commandLineService.test.ts diff --git a/src/sql/parts/commandLine/common/commandLine.ts b/src/sql/parts/commandLine/common/commandLine.ts new file mode 100644 index 0000000000..4290a6308b --- /dev/null +++ b/src/sql/parts/commandLine/common/commandLine.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +export interface ICommandLineProcessing { + _serviceBrand: any; + /** + * Interprets the various Azure Data Studio-specific command line switches and + * performs the requisite tasks such as connecting to a server + */ + processCommandLine() : void; +} + +export const ICommandLineProcessing = createDecorator('commandLineService'); \ No newline at end of file diff --git a/src/sql/parts/commandLine/common/commandLineService.ts b/src/sql/parts/commandLine/common/commandLineService.ts new file mode 100644 index 0000000000..08c6f9dc50 --- /dev/null +++ b/src/sql/parts/commandLine/common/commandLineService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; +import { ICommandLineProcessing } from 'sql/parts/commandLine/common/commandLine'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import * as Constants from 'sql/parts/connection/common/constants'; +import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService'; +import * as platform from 'vs/platform/registry/common/platform'; +import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension'; +import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; +import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class CommandLineService implements ICommandLineProcessing { + private _connectionProfile : ConnectionProfile; + private _showConnectionDialog: boolean; + + constructor( + @IConnectionManagementService private _connectionManagementService : IConnectionManagementService, + @ICapabilitiesService private _capabilitiesService : ICapabilitiesService, + @IEnvironmentService private _environmentService : IEnvironmentService, + @IQueryEditorService private _queryEditorService : IQueryEditorService, + @IObjectExplorerService private _objectExplorerService : IObjectExplorerService, + @IEditorService private _editorService : IEditorService, + ) + { + let profile = null; + if (this._environmentService && this._environmentService.args.server) { + profile = new ConnectionProfile(_capabilitiesService, null); + // We want connection store to use any matching password it finds + profile.savePassword = true; + profile.providerName = Constants.mssqlProviderName; + profile.serverName = _environmentService.args.server; + profile.databaseName = _environmentService.args.database ? _environmentService.args.database : ''; + profile.userName = _environmentService.args.user ? _environmentService.args.user : ''; + profile.authenticationType = _environmentService.args.integrated ? 'Integrated' : 'SqlLogin'; + profile.connectionName = ''; + profile.setOptionValue('applicationName', Constants.applicationName); + profile.setOptionValue('databaseDisplayName', profile.databaseName); + profile.setOptionValue('groupId', profile.groupId); + } + this._connectionProfile = profile; + const registry = platform.Registry.as(ConnectionProviderExtensions.ConnectionProviderContributions); + let sqlProvider = registry.getProperties( Constants.mssqlProviderName); + // We can't connect to object explorer until the MSSQL connection provider is registered + if (sqlProvider) { + this.processCommandLine(); + } else { + registry.onNewProvider(e => { + if (e.id === Constants.mssqlProviderName) + { + this.processCommandLine(); + } + }); + } + } + public _serviceBrand: any; + public processCommandLine(): void { + if (!this._connectionProfile && !this._connectionManagementService.hasRegisteredServers()) { + // prompt the user for a new connection on startup if no profiles are registered + this._connectionManagementService.showConnectionDialog(); + } else if (this._connectionProfile) { + this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection') + .then(result => TaskUtilities.newQuery(this._connectionProfile, + this._connectionManagementService, + this._queryEditorService, + this._objectExplorerService, + this._editorService)) + .catch(() => {}); + } + } +} \ No newline at end of file diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index 80f466fa9e..5d925f59b9 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -77,7 +77,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti private _onConnectRequestSent = new Emitter(); private _onConnectionChanged = new Emitter(); private _onLanguageFlavorChanged = new Emitter(); - private _connectionGlobalStatus = new ConnectionGlobalStatus(this._statusBarService); private _configurationEditService: ConfigurationEditingService; @@ -124,16 +123,6 @@ export class ConnectionManagementService extends Disposable implements IConnecti 100 /* High Priority */ )); - if (_capabilitiesService && Object.keys(_capabilitiesService.providers).length > 0 && !this.hasRegisteredServers()) { - // prompt the user for a new connection on startup if no profiles are registered - this.showConnectionDialog(); - } else if (_capabilitiesService && !this.hasRegisteredServers()) { - _capabilitiesService.onCapabilitiesRegistered(e => { - // prompt the user for a new connection on startup if no profiles are registered - this.showConnectionDialog(); - }); - } - const registry = platform.Registry.as(ConnectionProviderExtensions.ConnectionProviderContributions); let providerRegistration = (p: { id: string, properties: ConnectionProviderProperties }) => { @@ -282,29 +271,30 @@ export class ConnectionManagementService extends Disposable implements IConnecti * @param options to use after the connection is complete */ private tryConnect(connection: IConnectionProfile, owner: IConnectableInput, options?: IConnectionCompletionOptions): Promise { + let self = this; return new Promise((resolve, reject) => { // Load the password if it's not already loaded - this._connectionStore.addSavedPassword(connection).then(result => { + self._connectionStore.addSavedPassword(connection).then(result => { let newConnection = result.profile; let foundPassword = result.savedCred; // If there is no password, try to load it from an existing connection - if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection)) { - let existingConnection = this._connectionStatusManager.findConnectionProfile(connection); + if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection)) { + let existingConnection = self._connectionStatusManager.findConnectionProfile(connection); if (existingConnection && existingConnection.connectionProfile) { newConnection.password = existingConnection.connectionProfile.password; foundPassword = true; } } // If the password is required and still not loaded show the dialog - if (!foundPassword && this._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) { - resolve(this.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options)); + if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) { + resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options)); } else { // Try to connect - this.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => { + self.connectWithOptions(newConnection, owner.uri, options, owner).then(connectionResult => { if (!connectionResult.connected && !connectionResult.errorHandled) { // If connection fails show the dialog - resolve(this.showConnectionDialogOnError(connection, owner, connectionResult, options)); + resolve(self.showConnectionDialogOnError(connection, owner, connectionResult, options)); } else { //Resolve with the connection result resolve(connectionResult); @@ -390,7 +380,15 @@ export class ConnectionManagementService extends Disposable implements IConnecti if (this._connectionStatusManager.isConnected(ownerUri)) { resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri)); } else { - this.connect(connection, ownerUri).then(connectionResult => { + const options: IConnectionCompletionOptions = { + // Should saving the connection be a command line switch? + saveTheConnection : true, + showConnectionDialogOnError : true, + showDashboard : purpose === 'dashboard', + params : undefined, + showFirewallRuleOnError : true, + }; + this.connect(connection, ownerUri, options).then(connectionResult => { if (connectionResult && connectionResult.connected) { resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri)); } else { diff --git a/src/sqltest/parts/commandLine/commandLineService.test.ts b/src/sqltest/parts/commandLine/commandLineService.test.ts new file mode 100644 index 0000000000..f00d529cc9 --- /dev/null +++ b/src/sqltest/parts/commandLine/commandLineService.test.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Constants from 'sql/parts/connection/common/constants'; +import * as Utils from 'sql/parts/connection/common/utils'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import * as sqlops from 'sqlops'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; + +import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; +import { CommandLineService } from 'sql/parts/commandLine/common/commandLineService'; +import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; +import { CapabilitiesService, ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService'; +import { QueryEditorService } from 'sql/parts/query/services/queryEditorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService'; +import { + IConnectionManagementService, IConnectionDialogService, INewConnectionParams, + ConnectionType, IConnectableInput, IConnectionCompletionOptions, IConnectionCallbacks, + IConnectionParams, IConnectionResult, IServerGroupController, IServerGroupDialogCallbacks, + RunQueryOnConnectionMode +} from 'sql/parts/connection/common/connectionManagement'; +import { ConnectionStore } from 'sql/parts/connection/common/connectionStore'; +import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test'; + + +class TestParsedArgs implements ParsedArgs{ + [arg: string]: any; + _: string[]; + aad?: boolean; + add?: boolean; + database?:string; + debugBrkPluginHost?: string; + debugBrkSearch?: string; + debugId?: string; + debugPluginHost?: string; + debugSearch?: string; + diff?: boolean; + 'disable-crash-reporter'?: string; + 'disable-extension'?: string | string[]; + 'disable-extensions'?: boolean; + 'disable-restore-windows'?: boolean; + 'disable-telemetry'?: boolean; + 'disable-updates'?: string; + 'driver'?: string; + 'enable-proposed-api'?: string | string[]; + 'export-default-configuration'?: string; + 'extensions-dir'?: string; + extensionDevelopmentPath?: string; + extensionTestsPath?: string; + 'file-chmod'?: boolean; + 'file-write'?: boolean; + 'folder-uri'?: string | string[]; + goto?: boolean; + help?: boolean; + 'install-extension'?: string | string[]; + 'install-source'?: string; + integrated?: boolean; + 'list-extensions'?: boolean; + locale?: string; + log?: string; + logExtensionHostCommunication?: boolean; + 'max-memory'?: number; + 'new-window'?: boolean; + 'open-url'?: boolean; + performance?: boolean; + 'prof-append-timers'?: string; + 'prof-startup'?: string; + 'prof-startup-prefix'?: string; + 'reuse-window'?: boolean; + server?: string; + 'show-versions'?: boolean; + 'skip-add-to-recently-opened'?: boolean; + 'skip-getting-started'?: boolean; + 'skip-release-notes'?: boolean; + status?: boolean; + 'sticky-quickopen'?: boolean; + 'uninstall-extension'?: string | string[]; + 'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch. + 'upload-logs'?: string; + user?: string; + 'user-data-dir'?: string; + _urls?: string[]; + verbose?: boolean; + version?: boolean; + wait?: boolean; + waitMarkerFilePath?: string; +} +suite('commandLineService tests', () => { + + let capabilitiesService: CapabilitiesTestService; + let commandLineService : CommandLineService; + let environmentService : TypeMoq.Mock; + let queryEditorService : TypeMoq.Mock; + let editorService:TypeMoq.Mock; + let objectExplorerService : TypeMoq.Mock; + let connectionStore: TypeMoq.Mock; + + setup(() => { + capabilitiesService = new CapabilitiesTestService(); + connectionStore = TypeMoq.Mock.ofType(ConnectionStore); + }); + + function getCommandLineService(connectionManagementService : IConnectionManagementService, + environmentService? : IEnvironmentService, + capabilitiesService? : ICapabilitiesService + ) : CommandLineService + { + let service= new CommandLineService( + connectionManagementService, + capabilitiesService, + environmentService, + undefined, + undefined, + undefined + ); + return service; + } + + test('processCommandLine shows connection dialog by default', done => { + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + + connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(); + connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false); + connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + let service = getCommandLineService(connectionManagementService.object); + service.processCommandLine(); + connectionManagementService.verifyAll(); + done(); + }); + + test('processCommandLine does nothing if registered servers exist and no server name is provided', done => { + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + + connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never()); + connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true); + connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + let service = getCommandLineService(connectionManagementService.object); + service.processCommandLine(); + connectionManagementService.verifyAll(); + done(); + }); + + test('processCommandLine opens a new connection if a server name is passed', done => { + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + + const environmentService : TypeMoq.Mock = TypeMoq.Mock.ofType(EnvironmentService); + const args : TestParsedArgs = new TestParsedArgs(); + args.server = 'myserver'; + args.database = 'mydatabase'; + environmentService.setup(e => e.args).returns(() => args).verifiable(TypeMoq.Times.atLeastOnce()); + connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never()); + connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce()); + connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), 'connection')) + .returns(() => new Promise((resolve, reject) => { reject('unused');})) + .verifiable(TypeMoq.Times.once()); + let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService); + service.processCommandLine(); + environmentService.verifyAll(); + connectionManagementService.verifyAll(); + done(); + }); +}); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 2d07d986ee..3d5f67b51d 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -62,6 +62,13 @@ export interface ParsedArgs { 'upload-logs'?: string; 'driver'?: string; 'driver-verbose'?: boolean; + // {{SQL CARBON EDIT}} + aad?: boolean; + database?: string; + integrated?: boolean; + server?: string; + user?: string; + // {{SQL CARBON EDIT}} } export const IEnvironmentService = createDecorator('environmentService'); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index dff9ea9cbc..bd15d92783 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -33,7 +33,12 @@ const options: minimist.Opts = { 'export-default-configuration', 'install-source', 'upload-logs', - 'driver' + 'driver', + // {{SQL CARBON EDIT}} + 'database', + 'server', + 'user', + // {{SQL CARBON EDIT}} ], boolean: [ 'help', @@ -66,7 +71,11 @@ const options: minimist.Opts = { 'status', 'file-write', 'file-chmod', - 'driver-verbose' + 'driver-verbose', + // {{SQL CARBON EDIT}} + 'aad', + 'integrated', + // {{SQL CARBON EDIT}} ], alias: { add: 'a', @@ -85,6 +94,12 @@ const options: minimist.Opts = { 'debugBrkPluginHost': 'inspect-brk-extensions', 'debugSearch': 'inspect-search', 'debugBrkSearch': 'inspect-brk-search', + // {{SQL CARBON EDIT}} + database: 'D', + integrated: 'E', + server: 'S', + user: 'U', + // {{SQL CARBON EDIT}} } }; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 87e4a81d9c..476853e9d8 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -172,6 +172,10 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay'; +// {{SQL CARBON EDIT}} +import { ICommandLineProcessing } from 'sql/parts/commandLine/common/commandLine'; +import { CommandLineService } from 'sql/parts/commandLine/common/commandLineService'; +// {{SQL CARBON EDIT}} interface WorkbenchParams { configuration: IWindowConfiguration; @@ -575,7 +579,9 @@ export class Workbench extends Disposable implements IPartService { serviceCollection.set(IAccountManagementService, accountManagementService); serviceCollection.set(IAccountPickerService, this.instantiationService.createInstance(AccountPickerService)); serviceCollection.set(IProfilerService, this.instantiationService.createInstance(ProfilerService)); - + // {{SQL CARBON EDIT}} + serviceCollection.set(ICommandLineProcessing, this.instantiationService.createInstance(CommandLineService)); + // {{SQL CARBON EDIT}} this._register(toDisposable(() => connectionManagementService.shutdown())); this._register(toDisposable(() => accountManagementService.shutdown())); this._register(toDisposable(() => capabilitiesService.shutdown()));