diff --git a/.vscode/launch.json b/.vscode/launch.json index 880b5c1ddb..241ff99f7a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -92,6 +92,30 @@ "webRoot": "${workspaceFolder}", "timeout": 45000 }, + { + "type": "chrome", + "request": "launch", + "name": "Launch azuredatastudio with new notebook command", + "windows": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.bat" + }, + "osx": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" + }, + "linux": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" + }, + "urlFilter": "*index.html*", + "runtimeArgs": [ + "--inspect=5875", + "--command=notebook.command.new" + ], + "skipFiles": [ + "**/winjs*.js" + ], + "webRoot": "${workspaceFolder}", + "timeout": 45000 + }, { "type": "node", "request": "launch", diff --git a/src/sql/parts/commandLine/common/commandLine.ts b/src/sql/parts/commandLine/common/commandLine.ts index 4290a6308b..852dc234df 100644 --- a/src/sql/parts/commandLine/common/commandLine.ts +++ b/src/sql/parts/commandLine/common/commandLine.ts @@ -11,7 +11,7 @@ export interface ICommandLineProcessing { * Interprets the various Azure Data Studio-specific command line switches and * performs the requisite tasks such as connecting to a server */ - processCommandLine() : void; + processCommandLine() : Promise; } 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 index 945f88bf08..8179b2042c 100644 --- a/src/sql/parts/commandLine/common/commandLineService.ts +++ b/src/sql/parts/commandLine/common/commandLineService.ts @@ -15,10 +15,13 @@ import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions a 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'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { warn } from 'sql/base/common/log'; export class CommandLineService implements ICommandLineProcessing { private _connectionProfile: ConnectionProfile; private _showConnectionDialog: boolean; + private _commandName:string; constructor( @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @@ -27,49 +30,96 @@ export class CommandLineService implements ICommandLineProcessing { @IQueryEditorService private _queryEditorService: IQueryEditorService, @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, @IEditorService private _editorService: IEditorService, + @ICommandService private _commandService: ICommandService ) { 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); + if (this._environmentService) { + if (this._commandService) { + this._commandName = this._environmentService.args.command; + } + if (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(); + this.processCommandLine().catch(reason=>{warn('processCommandLine failed: ' + reason);}); } else { registry.onNewProvider(e => { if (e.id === Constants.mssqlProviderName) { - this.processCommandLine(); + this.processCommandLine().catch(reason=>{warn('processCommandLine failed: ' + reason);}); } }); } } 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', true) - .then(result => TaskUtilities.newQuery(this._connectionProfile, - this._connectionManagementService, - this._queryEditorService, - this._objectExplorerService, - this._editorService)) - .catch(() => { }); - } + // We base our logic on the combination of (server, command) values. + // (serverName, commandName) => Connect object explorer and execute the command, passing the connection profile to the command. Do not load query editor. + // (null, commandName) => Launch the command with a null connection. If the command implementation needs a connection, it will need to create it. + // (serverName, null) => Connect object explorer and open a new query editor + // (null, null) => Prompt for a connection unless there are registered servers + public processCommandLine(): Promise { + + let self = this; + return new Promise((resolve, reject) => { + + if (!self._commandName && !self._connectionProfile && !self._connectionManagementService.hasRegisteredServers()) { + // prompt the user for a new connection on startup if no profiles are registered + self._connectionManagementService.showConnectionDialog() + .then(() => { + resolve(); + }, + error => { + reject(error); + }); + } else if (self._connectionProfile) { + if (!self._commandName) { + self._connectionManagementService.connectIfNotConnected(self._connectionProfile, 'connection', true) + .then(() => { + TaskUtilities.newQuery(self._connectionProfile, + self._connectionManagementService, + self._queryEditorService, + self._objectExplorerService, + self._editorService) + .then( () => { + resolve(); + }, error => { + // ignore query editor failing to open. + // the tests don't mock this out + warn('unable to open query editor ' + error); + resolve(); + }); + }, error => { + reject(error); + }); + } else { + self._connectionManagementService.connectIfNotConnected(self._connectionProfile, 'connection', true) + .then(() => { + self._commandService.executeCommand(self._commandName, self._connectionProfile).then(() => resolve(), error => reject(error)); + }, error => { + reject(error); + }); + } + } else if (self._commandName) { + self._commandService.executeCommand(self._commandName).then(() => resolve(), error => reject(error)); + } + else { + resolve(); + } + }); } } \ No newline at end of file diff --git a/src/sqltest/parts/commandLine/commandLineService.test.ts b/src/sqltest/parts/commandLine/commandLineService.test.ts index c3e9a9780b..f44f6f15ad 100644 --- a/src/sqltest/parts/commandLine/commandLineService.test.ts +++ b/src/sqltest/parts/commandLine/commandLineService.test.ts @@ -30,7 +30,8 @@ import { } from 'sql/parts/connection/common/connectionManagement'; import { ConnectionStore } from 'sql/parts/connection/common/connectionStore'; import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test'; - +import { ICommandService, ICommandEvent, CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; class TestParsedArgs implements ParsedArgs{ [arg: string]: any; @@ -38,6 +39,7 @@ class TestParsedArgs implements ParsedArgs{ aad?: boolean; add?: boolean; database?:string; + command?:string; debugBrkPluginHost?: string; debugBrkSearch?: string; debugId?: string; @@ -111,7 +113,8 @@ suite('commandLineService tests', () => { function getCommandLineService(connectionManagementService : IConnectionManagementService, environmentService? : IEnvironmentService, - capabilitiesService? : ICapabilitiesService + capabilitiesService? : ICapabilitiesService, + commandService? : ICommandService ) : CommandLineService { let service= new CommandLineService( @@ -120,7 +123,8 @@ suite('commandLineService tests', () => { environmentService, undefined, undefined, - undefined + undefined, + commandService ); return service; } @@ -129,14 +133,18 @@ suite('commandLineService tests', () => { const connectionManagementService : TypeMoq.Mock = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); - connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(); + connectionManagementService.setup((c) => c.showConnectionDialog()) + .returns(() => new Promise((resolve, reject) => { resolve();})) + .verifiable(); connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false); connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); + .returns(() => new Promise((resolve, reject) => { resolve('unused');})) + .verifiable(TypeMoq.Times.never()); let service = getCommandLineService(connectionManagementService.object); - service.processCommandLine(); - connectionManagementService.verifyAll(); - done(); + service.processCommandLine().then( () => { + connectionManagementService.verifyAll(); + done(); + }, error => {assert.fail(error, null, 'processCommandLine rejected ' + error); done(); }); }); test('processCommandLine does nothing if registered servers exist and no server name is provided', done => { @@ -146,11 +154,13 @@ suite('commandLineService tests', () => { 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()); + .returns(() => new Promise((resolve, reject) => { resolve('unused');})) + .verifiable(TypeMoq.Times.never()); let service = getCommandLineService(connectionManagementService.object); - service.processCommandLine(); - connectionManagementService.verifyAll(); - done(); + service.processCommandLine().then( () => { + connectionManagementService.verifyAll(); + done(); + }, error => {assert.fail(error, null, 'processCommandLine rejected ' + error); done(); }); }); test('processCommandLine opens a new connection if a server name is passed', done => { @@ -165,12 +175,88 @@ suite('commandLineService tests', () => { 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', true)) - .returns(() => new Promise((resolve, reject) => { reject('unused');})) - .verifiable(TypeMoq.Times.once()); + .returns(() => new Promise((resolve, reject) => { resolve('unused');})) + .verifiable(TypeMoq.Times.once()); let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService); - service.processCommandLine(); - environmentService.verifyAll(); - connectionManagementService.verifyAll(); - done(); + service.processCommandLine().then( () => { + environmentService.verifyAll(); + connectionManagementService.verifyAll(); + done(); + }, error => {assert.fail(error, null, 'processCommandLine rejected ' + error); done(); }); + }); + + test('processCommandLine invokes a command without a profile parameter when no server is passed', done => { + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + const environmentService : TypeMoq.Mock = TypeMoq.Mock.ofType(EnvironmentService); + const commandService : TypeMoq.Mock = TypeMoq.Mock.ofType(TestCommandService); + const args : TestParsedArgs = new TestParsedArgs(); + + args.command = 'mycommand'; + environmentService.setup(e => e.args).returns(() => args); + 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(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + commandService.setup(c => c.executeCommand('mycommand')) + .returns(() => TPromise.wrap(1)) + .verifiable(TypeMoq.Times.once()); + let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object); + service.processCommandLine().then( () => { + connectionManagementService.verifyAll(); + commandService.verifyAll(); + done(); + }, error => {assert.fail(error, null, 'processCommandLine rejected ' + error); done(); }); + }); + + + test('processCommandLine invokes a command with a profile parameter when a server is passed', done => { + + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + + const environmentService : TypeMoq.Mock = TypeMoq.Mock.ofType(EnvironmentService); + const commandService : TypeMoq.Mock = TypeMoq.Mock.ofType(TestCommandService); + const args : TestParsedArgs = new TestParsedArgs(); + args.command = 'mycommand'; + args.server = 'myserver'; + environmentService.setup(e => e.args).returns(() => args); + 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.is( p => p.serverName === 'myserver'), 'connection', true)) + .returns(() => new Promise((resolve, reject) => { resolve('unused');})) + .verifiable(TypeMoq.Times.once()); + commandService.setup(c => c.executeCommand('mycommand', TypeMoq.It.is( p => p.serverName === 'myserver'))) + .returns(() => TPromise.wrap(1)) + .verifiable(TypeMoq.Times.once()); + let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object); + service.processCommandLine().then( () => { + connectionManagementService.verifyAll(); + commandService.verifyAll(); + done(); + }, error => {assert.fail(error, null, 'processCommandLine rejected ' + error); done(); }); + }); + + test('processCommandLine rejects unknown commands', done => { + const connectionManagementService : TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + const environmentService : TypeMoq.Mock = TypeMoq.Mock.ofType(EnvironmentService); + const commandService : TypeMoq.Mock = TypeMoq.Mock.ofType(TestCommandService); + const args : TestParsedArgs = new TestParsedArgs(); + + args.command = 'mycommand'; + environmentService.setup(e => e.args).returns(() => args); + connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true); + commandService.setup(c => c.executeCommand('mycommand')) + .returns(() => TPromise.wrapError(new Error('myerror'))) + .verifiable(TypeMoq.Times.once()); + let service = getCommandLineService(connectionManagementService.object, environmentService.object, capabilitiesService, commandService.object); + service.processCommandLine().then( () => { + assert.fail(1,null, 'processCommandLine should reject when executeCommand errors out'); + done(); + }, error => { + assert.equal(error.message, 'myerror', 'unexpected error from processCommandLine ' + error); + done(); + }); }); }); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 3d5f67b51d..61e6465ce5 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -68,6 +68,7 @@ export interface ParsedArgs { integrated?: boolean; server?: string; user?: string; + command?: string; // {{SQL CARBON EDIT}} } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index bd15d92783..d1adc841d6 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -38,6 +38,7 @@ const options: minimist.Opts = { 'database', 'server', 'user', + 'command' // {{SQL CARBON EDIT}} ], boolean: [ @@ -99,6 +100,7 @@ const options: minimist.Opts = { integrated: 'E', server: 'S', user: 'U', + command : 'c', // {{SQL CARBON EDIT}} } };