diff --git a/.gitignore b/.gitignore index 9af4c745..344650f9 100644 --- a/.gitignore +++ b/.gitignore @@ -282,6 +282,10 @@ Session.vim # Visual Studio Code .vscode/ +# docfx generated files +_site +metadata + # Stuff from cake /artifacts/ /.tools/ diff --git a/docs/design/index.md b/docs/design/index.md new file mode 100644 index 00000000..0b04951e --- /dev/null +++ b/docs/design/index.md @@ -0,0 +1,11 @@ +# Design and Implementation + +Design and implementation notes. + +![Host Process](../images/hostprocess.png) + +![Connected Binding](../images/connected_bind.png) + +![Message Dispatch](../images/msgdispatch.png) + +![Message Dispatch Example](../images/msgdispatchexample.png) diff --git a/docs/docfx.json b/docs/docfx.json index f3691bd9..2a620fe6 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -33,6 +33,7 @@ "toc.yml", "index.md", "api/index.md", + "design/index.md", "guide/**.md" ], "exclude": [ diff --git a/docs/guide/toc.md b/docs/guide/toc.md index f5935869..2a9f7369 100644 --- a/docs/guide/toc.md +++ b/docs/guide/toc.md @@ -1,2 +1,3 @@ # [Introduction](introduction.md) -# [Using the .NET API](using_the_dotnet_api.md) \ No newline at end of file +# [Using the JSON-RPC API](using_the_host_process.md) +# [Using the .NET API](using_the_dotnet_api.md) diff --git a/docs/guide/using_the_dotnet_api.md b/docs/guide/using_the_dotnet_api.md index 1ae20455..97d0c86a 100644 --- a/docs/guide/using_the_dotnet_api.md +++ b/docs/guide/using_the_dotnet_api.md @@ -1,4 +1,66 @@ # Using the SQL Tools Service .NET API -> NOTE: This page will eventually provide usage examples of the .NET -> API. For now the [API Reference](../api/index.md) is the best starting point. \ No newline at end of file +> NOTE: The [API Reference](../api/index.md) is the best starting point for working directly with +> the .NET API. + +An example of using the JSON RPC API from a .Net Core console application is available at docs/samples/jsonrpc/netcore. +The following snippet provides a basic example of how to connect to a database and execute a query. + +```typescript +internal static async Task ExecuteQuery(string query) +{ + // create a temporary "workspace" file + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + // create the client helper which wraps the client driver objects + using (ClientHelper testHelper = new ClientHelper()) + { + // connnection details + ConnectParams connectParams = new ConnectParams(); + connectParams.Connection = new ConnectionDetails(); + connectParams.Connection.ServerName = "localhost"; + connectParams.Connection.DatabaseName = "master"; + connectParams.Connection.AuthenticationType = "Integrated"; + + // connect to the database + await testHelper.Connect(queryTempFile.FilePath, connectParams); + + // execute the query + QueryExecuteCompleteParams queryComplete = + await testHelper.RunQuery(queryTempFile.FilePath, query); + + if (queryComplete.BatchSummaries != null && queryComplete.BatchSummaries.Length > 0) + { + var batch = queryComplete.BatchSummaries[0]; + if (batch.ResultSetSummaries != null && batch.ResultSetSummaries.Length > 0) + { + var resultSet = batch.ResultSetSummaries[0]; + + // retrive the results + QueryExecuteSubsetResult querySubset = await testHelper.ExecuteSubset( + queryTempFile.FilePath, batch.Id, + resultSet.Id, 0, (int)resultSet.RowCount); + + // print the header + foreach (var column in resultSet.ColumnInfo) + { + Console.Write(column.ColumnName + ", "); + } + Console.Write(Environment.NewLine); + + // print the rows + foreach (var row in querySubset.ResultSubset.Rows) + { + for (int i = 0; i < resultSet.ColumnInfo.Length; ++i) + { + Console.Write(row.GetValue(i) + ", "); + } + Console.Write(Environment.NewLine); + } + } + } + + // close database connection + await testHelper.Disconnect(queryTempFile.FilePath); + } +} +``` diff --git a/docs/guide/using_the_host_process.md b/docs/guide/using_the_host_process.md new file mode 100644 index 00000000..7890eace --- /dev/null +++ b/docs/guide/using_the_host_process.md @@ -0,0 +1,1419 @@ +# Using the JSON-RPC API + +The JSON-RPC API provides an host-agnostic interface for +leveraging the core .NET API. + +## Launching the Host Process + +From your host process, launch `Microsoft.SqlTools.ServiceLayer(.exe)` using an host-native process +API that allows you to read and write this process' standard in/out streams. All communication +with the host process occurs via this channel. + +It is recommended that the process I/O be dealt with as a byte stream rather than read as a +string since different parts of the message format could be sent with different text encodings +(see next section). + +It is expected that an editor will launch one instance of the host process for each SQL +'workspace' that the user has opened. Generally this would map to a single top-level folder +which contains all of the user's SQL script files for a given project. + +## Messages overview + +General + +* :leftwards_arrow_with_hook: [initialize](#initialize) +* :leftwards_arrow_with_hook: [shutdown](#shutdown) +* :arrow_right: [exit](#exit) +* :arrow_right: [$/cancelRequest](#cancelRequest) + +Window + +* :arrow_left: [window/showMessage](#window_showMessage) +* :arrow_right_hook: [window/showMessageRequest](#window_showMessageRequest) +* :arrow_left: [window/logMessage](#window_logMessage) +* :arrow_left: [telemetry/event](#telemetry_event) + +Workspace + +* :arrow_right: [workspace/didChangeConfiguration](#workspace_didChangeConfiguration) +* :arrow_right: [workspace/didChangeWatchedFiles](#workspace_didChangeWatchedFiles) +* :leftwards_arrow_with_hook: [workspace/symbol](#workspace_symbol) + +Document + +* :arrow_left: [textDocument/publishDiagnostics](#textDocument_publishDiagnostics) +* :arrow_right: [textDocument/didChange](#textDocument_didChange) +* :arrow_right: [textDocument/didClose](#textDocument_didClose) +* :arrow_right: [textDocument/didOpen](#textDocument_didOpen) +* :arrow_right: [textDocument/didSave](#textDocument_didSave) +* :leftwards_arrow_with_hook: [textDocument/completion](#textDocument_completion) +* :leftwards_arrow_with_hook: [completionItem/resolve](#completionItem_resolve) +* :leftwards_arrow_with_hook: [textDocument/hover](#textDocument_hover) +* :leftwards_arrow_with_hook: [textDocument/signatureHelp](#textDocument_signatureHelp) +* :leftwards_arrow_with_hook: [textDocument/references](#textDocument_references) +* :leftwards_arrow_with_hook: [textDocument/documentHighlight](#textDocument_documentHighlight) +* :leftwards_arrow_with_hook: [textDocument/documentSymbol](#textDocument_documentSymbol) +* :leftwards_arrow_with_hook: [textDocument/formatting](#textDocument_formatting) +* :leftwards_arrow_with_hook: [textDocument/rangeFormatting](#textDocument_rangeFormatting) +* :leftwards_arrow_with_hook: [textDocument/onTypeFormatting](#textDocument_onTypeFormatting) +* :leftwards_arrow_with_hook: [textDocument/definition](#textDocument_definition) +* :leftwards_arrow_with_hook: [textDocument/codeAction](#textDocument_codeAction) +* :leftwards_arrow_with_hook: [textDocument/codeLens](#textDocument_codeLens) +* :leftwards_arrow_with_hook: [codeLens/resolve](#codeLens_resolve) +* :leftwards_arrow_with_hook: [textDocument/documentLink](#textDocument_documentLink) +* :leftwards_arrow_with_hook: [documentLink/resolve](#documentLink_resolve) +* :leftwards_arrow_with_hook: [textDocument/rename](#textDocument_rename) + +# Message Protocol + +A message consists of two parts: a header section and the message body. For now, there is +only one header, `Content-Length`. This header, written with ASCII encoding, specifies the +UTF-8 byte length of the message content to follow. The host process expects that all messages +sent to it will come with an accurate `Content-Length` header so that it knows exactly how many +bytes to read. Likewise, all messages returned from the host process will be sent in this manner. + +## Message Schema + +The body of a message is encoded in JSON and conforms to a specific schema. There are three types of +messages that can be transmitted between editor and host process: `Request`, `Response`, and `Event`. + +### Common Fields + +The common fields shared between all message types are as follows: + +- `seq`: A sequence number that increases with each message sent from the editor to the host. + Even though this field shows up on all message types, it is generally only used to correlate + reponse messages to the initial request that caused it. +- `type`: The type of message being transmitted, either `request`, `response`, or `event`. + +### Request Fields + +A request gets sent by the editor when a particular type of behavior is needed. +In this case, the `type` field will be set to `request`. + +- `command`: The request type. There are a variety of request types that will be enumerated + later in this document. +- `arguments`: A JSON object containing arguments for the request, varies per each request `command`. + +*NOTE: Some `request` types do not require a matching `response` or may cause `events` to be raised at a later time* + +### Response Fields + +A response gets sent by the host process when a request completes or fails. In this case, +the `type`field will be set to `response`. + +- `request_seq`: The `seq` number that was included with the original request, used to help + the editor correlate the response to the original request +- `command`: The name of the request command to which this response relates +- `body`: A JSON object body for the response, varies per each response `command`. +- `success`: A boolean indicating whether the request was successful +- `message`: An optional response message, generally used when `success` is set to `false`. + +### Event Fields + +An event gets sent by the host process when + +- `event`: The name of the event type to which this event relates +- `body`: A JSON object body for the event, varies per each `event` type + + + + + + + +## Base Protocol + +The base protocol consists of a header and a content part (comparable to HTTP). The header and content part are +separated by a '\r\n'. + +### Header Part + +The header part consists of header fields. Each header field is comprised of a name and a value, +separated by ': ' (a colon and a space). +Each header field is terminated by '\r\n'. +Considering the last header field and the overall header itself are each terminated with '\r\n', +and that at least one header is mandatory, this means that two '\r\n' sequences always +immediately precede the content part of a message. + +Currently the following header fields are supported: + +| Header Field Name | Value Type | Description | +|:------------------|:------------|:------------| +| Content-Length | number | The length of the content part in bytes. This header is required. | +| Content-Type | string | The mime type of the content part. Defaults to application/vscode-jsonrpc; charset=utf8 | + +The header part is encoded using the 'ascii' encoding. This includes the '\r\n' separating the header and content part. + +### Content Part + +Contains the actual content of the message. The content part of a message uses [JSON-RPC](http://www.jsonrpc.org/) to describe requests, responses and notifications. The content part is encoded using the charset provided in the Content-Type field. It defaults to 'utf8', which is the only encoding supported right now. + + +### Example: + +``` +Content-Length: ...\r\n +\r\n +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/didOpen", + "params": { + ... + } +} +``` +### Base Protocol JSON structures + +The following TypeScript definitions describe the base [JSON-RPC protocol](http://www.jsonrpc.org/specification): + +#### Abstract Message + +A general message as defined by JSON-RPC. The language server protocol always uses "2.0" as the jsonrpc version. + +```typescript +interface Message { + jsonrpc: string; +} +``` +#### RequestMessage + +A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request. + +```typescript +interface RequestMessage extends Message { + + /** + * The request id. + */ + id: number | string; + + /** + * The method to be invoked. + */ + method: string; + + /** + * The method's params. + */ + params?: any +} +``` + +#### Response Message + +Response Message sent as a result of a request. + +```typescript +interface ResponseMessage extends Message { + /** + * The request id. + */ + id: number | string; + + /** + * The result of a request. This can be omitted in + * the case of an error. + */ + result?: any; + + /** + * The error object in case a request fails. + */ + error?: ResponseError; +} + +interface ResponseError { + /** + * A number indicating the error type that occurred. + */ + code: number; + + /** + * A string providing a short description of the error. + */ + message: string; + + /** + * A Primitive or Structured value that contains additional + * information about the error. Can be omitted. + */ + data?: D; +} + +export namespace ErrorCodes { + export const ParseError: number = -32700; + export const InvalidRequest: number = -32600; + export const MethodNotFound: number = -32601; + export const InvalidParams: number = -32602; + export const InternalError: number = -32603; + export const serverErrorStart: number = -32099; + export const serverErrorEnd: number = -32000; + export const serverNotInitialized: number = -32001; +} +``` +#### Notification Message + +A notification message. A processed notification message must not send a response back. They work like events. + +```typescript +interface NotificationMessage extends Message { + /** + * The method to be invoked. + */ + method: string; + + /** + * The notification's params. + */ + params?: any +} +``` + +#### Cancellation Support + +>**New:** The base protocol now offers support for request cancellation. To cancel a request, a notification message with the following properties is sent: + +_Notification_: +* method: '$/cancelRequest' +* params: `CancelParams` defined as follows: +```typescript +interface CancelParams { + /** + * The request id to cancel. + */ + id: number | string; +} +``` + +A request that got canceled still needs to return from the server and send a response back. It can not be left open / hanging. This is in line with the JSON RPC protocol that requires that every request sends a response back. In addition it allows for returning partial results on cancel. + +## Language Server Protocol + +The language server protocol defines a set of JSON-RPC request, response and notification messages which are exchanged using the above base protocol. This section starts describing the basic JSON structures used in the protocol. The document uses TypeScript interfaces to describe these. Based on the basic JSON structures, the actual requests with their responses and the notifications are described. + +The protocol currently assumes that one server serves one tool. There is currently no support in the protocol to share one server between different tools. Such a sharing would require additional protocol to either lock a document to support concurrent editing. + +### Basic JSON Structures + +#### URI + +URI's are transferred as strings. The URI's format is defined in [http://tools.ietf.org/html/rfc3986](http://tools.ietf.org/html/rfc3986) + +``` + foo://example.com:8042/over/there?name=ferret#nose + \_/ \______________/\_________/ \_________/ \__/ + | | | | | +scheme authority path query fragment + | _____________________|__ + / \ / \ + urn:example:animal:ferret:nose +``` + +We also maintain a node module to parse a string into `scheme`, `authority`, `path`, `query`, and `fragment` URI components. The GitHub repository is [https://github.com/Microsoft/vscode-uri](https://github.com/Microsoft/vscode-uri) the npm module is [https://www.npmjs.com/package/vscode-uri](https://www.npmjs.com/package/vscode-uri). + + +#### Position + +Position in a text document expressed as zero-based line and character offset. A position is between two characters like an 'insert' cursor in a editor. + +```typescript +interface Position { + /** + * Line position in a document (zero-based). + */ + line: number; + + /** + * Character offset on a line in a document (zero-based). + */ + character: number; +} +``` +#### Range + +A range in a text document expressed as (zero-based) start and end positions. A range is comparable to a selection in an editor. Therefore the end position is exclusive. + +```typescript +interface Range { + /** + * The range's start position. + */ + start: Position; + + /** + * The range's end position. + */ + end: Position; +} +``` + +#### Location + +Represents a location inside a resource, such as a line inside a text file. +```typescript +interface Location { + uri: string; + range: Range; +} +``` + + + + + + + + + + + +# Request and Response Types + +## File Management + +### `open` + +This request is sent by the editor when the user opens a file with a known PowerShell extension. It causes +the file to be opened by the language service and parsed for syntax errors. + +#### Request + +The arguments object specifies the absolute file path to be loaded. + +```json + { + "seq": 0, + "type": "request", + "command": "open", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1" + } + } +``` + +#### Response + +No response is needed for this command. + +### `close` + +This request is sent by the editor when the user closes a file with a known PowerShell extension which was +previously opened in the language service. + +#### Request + +The arguments object specifies the absolute file path to be closed. + +```json + { + "seq": 3, + "type": "request", + "command": "close", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1" + } + } +``` + +#### Response + +No response is needed for this command. + +### `change` + +This request is sent by the editor when the user changes the contents of a PowerShell file that has previously +been opened in the language service. Depending on how the request arguments are specified, the file change could +either be an arbitrary-length string insertion, region delete, or region replacement. + +It is up to the editor to decide how often to send these requests in response +to the user's typing activity. The language service can deal with change deltas of any length, so it is really +just a matter of preference how often `change` requests are sent. + +#### Request + +The arguments for this request specify the absolute path of the `file` being changed as well as the complete details +of the edit that the user performed. The `line`/`endLine` and `offset`/`endOffset` (column) numbers indicate the +1-based range of the file that is being replaced. The `insertString` field indicates the string that will be +inserted. In the specified range. + +*NOTE: In the very near future, all file locations will be specified with zero-based coordinates.* + +```json + { + "seq": 9, + "type": "request", + "command": "change", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 39, + "offset": 5, + "endLine": 39, + "endOffset": 5, + "insertString": "Test\r\nchange" + } + } +``` + +#### Response + +No response is needed for this command. + +### `geterr` + +This request causes script diagnostics to be performed on a list of script file paths. The editor +will typically send this request after every successful `change` request (though it is best to throttle +these requests on the editor side so that it doesn't overwhelm the host). Responses will be sent back +as `syntaxDiag` and `semanticDiag` events. + +#### Request + +The arguments for this request specify the list of `files` to be analyzed (sorted by those most recently changed) +and a millisecond `delay` value which instructs the language service to wait for some period before performing +diagnostics. If another `geterr` request is sent before the specified `delay` expires, the original request will +be cancelled server-side and a new delay period will start. + +```json + { + "seq": 1, + "type": "request", + "command": "geterr", + "arguments": { + "delay": 750, + "files": [ + "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1" + ] + } + } +``` + +## Code Completions + +### `completions` + +### Request + +```json + { + "seq": 34, + "type": "request", + "command": "completions", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 34, + "offset": 9 + } + } +``` + +#### Response + +```json + { + "request_seq": 34, + "success": true, + "command": "completions", + "message": null, + "body": [ + { + "name": "Get-Acl", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + { + "name": "Get-Alias", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + { + "name": "Get-AliasPattern", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + { + "name": "Get-AppLockerFileInformation", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + { + "name": "Get-AppLockerPolicy", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + { + "name": "Get-AppxDefaultVolume", + "kind": "method", + "kindModifiers": null, + "sortText": null + }, + + ... many more completions ... + + ], + "seq": 0, + "type": "response" + } +``` + +### `completionEntryDetails` + +#### Request + +```json + { + "seq": 37, + "type": "request", + "command": "completionEntryDetails", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 34, + "offset": 9, + "entryNames": [ + "Get-Acl" + ] + } + } +``` + +#### Response + +```json + { + "request_seq": 37, + "success": true, + "command": "completionEntryDetails", + "message": null, + "body": [ + { + "name": null, + "kind": null, + "kindModifiers": null, + "displayParts": null, + "documentation": null, + "docString": null + } + ], + "seq": 0, + "type": "response" + } +``` + +### `signatureHelp` + +#### Request + +```json + { + "seq": 36, + "type": "request", + "command": "signatureHelp", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 34, + "offset": 9 + } + } +``` + +#### Response + +** TODO: This is a bad example, find another** + +```json + { + "request_seq": 36, + "success": true, + "command": "signatureHelp", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +```` + +## Symbol Operations + +### `definition` + +#### Request + +```json + { + "seq": 20, + "type": "request", + "command": "definition", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/StopTest.ps1", + "line": 8, + "offset": 10 + } + } +``` + +#### Response + +```json + { + "request_seq": 20, + "success": true, + "command": "definition", + "message": null, + "body": [ + { + "file": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\Stop-Process2.ps1", + "start": { + "line": 11, + "offset": 10 + }, + "end": { + "line": 11, + "offset": 23 + } + } + ], + "seq": 0, + "type": "response" + } +``` + +### `references` + +#### Request + +```json + { + "seq": 2, + "type": "request", + "command": "references", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 38, + "offset": 12 + } + } +``` + +#### Response + +```json + { + "request_seq": 2, + "success": true, + "command": "references", + "message": null, + "body": { + "refs": [ + { + "lineText": "\t\t\tforeach ($process in $processes)", + "isWriteAccess": true, + "file": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\Stop-Process2.ps1", + "start": { + "line": 32, + "offset": 13 + }, + "end": { + "line": 32, + "offset": 21 + } + } + ], + "symbolName": "$process", + "symbolStartOffest": 690, + "symbolDisplayString": "$process" + }, + "seq": 0, + "type": "response" + } +``` + +### `occurrences` + +#### Request + +```json + { + "seq": 53, + "type": "request", + "command": "occurrences", + "arguments": { + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "line": 32, + "offset": 17 + } + } +``` + +#### Response + +```json + { + "request_seq": 53, + "success": true, + "command": "occurrences", + "message": null, + "body": [ + { + "isWriteAccess": true, + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "start": { + "line": 32, + "offset": 13 + }, + "end": { + "line": 32, + "offset": 21 + } + }, + { + "isWriteAccess": true, + "file": "c:/Users/daviwil/.vscode/extensions/vscode-powershell/examples/Stop-Process2.ps1", + "start": { + "line": 35, + "offset": 11 + }, + "end": { + "line": 35, + "offset": 19 + } + }, + + ... more occurrences ... + + ], + "seq": 0, + "type": "response" + } +``` + +## Debugger Operations + +### `initialize` + +### `launch` + +This request is sent by the editor when the user wants to launch a given script file in the +debugger. + +#### Request + +```json + { + "type": "request", + "seq": 3, + "command": "launch", + "arguments": { + "program": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "stopOnEntry": false, + "arguments": null, + "workingDirectory": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples", + "runtimeExecutable": null, + "runtimeArguments": null + } + } +``` + +```json + { + "request_seq": 3, + "success": true, + "command": "launch", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `disconnect` + +This request is sent by the editor when the user wants to terminate the debugging session before +the script completes. When this message is received, execution of the script is aborted and the +instance of the host process is aborted. + +*NOTE: For now, it is assumed that debugging will be performed in a separate instance of the + host process. This will change in the next couple of minor releases.* + +#### Request + +```json + { + "type": "request", + "seq": 22, + "command": "disconnect", + "arguments": { + "extensionHostData": { + "restart": false + } + } + } +``` + +#### Response + +```json + { + "request_seq": 0, + "success": false, + "command": "disconnect", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `setBreakpoints` + +#### Request + +```json + { + "type": "request", + "seq": 2, + "command": "setBreakpoints", + "arguments": { + "source": { + "path": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1" + }, + "lines": [ + 10 + ] + } + } +``` + +#### Response + +```json + { + "request_seq": 2, + "success": true, + "command": "setBreakpoints", + "message": null, + "body": { + "breakpoints": [ + { + "verified": true, + "line": 10 + } + ] + }, + "seq": 0, + "type": "response" + } +``` + +### `pause` + +#### Request + +```json + { + "type": "request", + "seq": 4, + "command": "pause" + } +``` + +#### Response + +No response needed for this command. The debugging service will send a `stopped` event +when execution is stopped due to this request. + +### `continue` + +#### Request + +```json + { + "type": "request", + "seq": 9, + "command": "continue" + } +``` + +#### Response + +```json + { + "request_seq": 9, + "success": true, + "command": "continue", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `next` + +#### Request + +```json + { + "type": "request", + "seq": 9, + "command": "next" + } +``` + +#### Response + +```json + { + "request_seq": 9, + "success": true, + "command": "next", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `stepIn` + +#### Request + +```json + { + "type": "request", + "seq": 13, + "command": "stepIn" + } +``` + +#### Response + +```json + { + "request_seq": 13, + "success": true, + "command": "stepIn", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `stepOut` + +#### Request + +```json + { + "type": "request", + "seq": 17, + "command": "stepOut" + } +``` + +#### Response + +```json + { + "request_seq": 17, + "success": true, + "command": "stepOut", + "message": null, + "body": null, + "seq": 0, + "type": "response" + } +``` + +### `threads` + +#### Request + +```json + { + "type": "request", + "seq": 5, + "command": "threads" + } +``` + +#### Response + +```json + { + "request_seq": 5, + "success": true, + "command": "threads", + "message": null, + "body": { + "threads": [ + { + "id": 1, + "name": "Main Thread" + } + ] + }, + "seq": 0, + "type": "response" + } +``` + +### `scopes` + +#### Request + +```json + { + "type": "request", + "seq": 7, + "command": "scopes", + "arguments": { + "frameId": 1 + } + } +``` + +#### Response + +```json + { + "request_seq": 7, + "success": true, + "command": "scopes", + "message": null, + "body": { + "scopes": [ + { + "name": "Locals", + "variablesReference": 1, + "expensive": false + } + ] + }, + "seq": 0, + "type": "response" + } +``` + +### `variables` + +#### Request + +```json + { + "type": "request", + "seq": 8, + "command": "variables", + "arguments": { + "variablesReference": 1 + } + } +``` + +#### Response + +```json + { + "request_seq": 8, + "success": true, + "command": "variables", + "message": null, + "body": { + "variables": [ + { + "name": "?", + "value": "True", + "variablesReference": 0 + }, + { + "name": "args", + "value": " ", + "variablesReference": 11 + }, + { + "name": "ConsoleFileName", + "value": "", + "variablesReference": 0 + }, + { + "name": "ExecutionContext", + "value": " ", + "variablesReference": 13 + }, + { + "name": "false", + "value": "False", + "variablesReference": 0 + }, + { + "name": "HOME", + "value": "C:\\Users\\daviwil", + "variablesReference": 0 + }, + + ... more variables ... + + ] + }, + "seq": 0, + "type": "response" + } +``` + +### `stackTrace` + +#### Request + +```json + { + "type": "request", + "seq": 6, + "command": "stackTrace", + "arguments": { + "threadId": 1, + "levels": 20 + } + } +``` + +#### Response + +```json + { + "request_seq": 6, + "success": true, + "command": "stackTrace", + "message": null, + "body": { + "stackFrames": [ + { + "id": 1, + "name": "Write-Item", + "source": { + "name": null, + "path": "C:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "sourceReference": null + }, + "line": 10, + "column": 9 + }, + { + "id": 2, + "name": "Do-Work", + "source": { + "name": null, + "path": "C:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "sourceReference": null + }, + "line": 18, + "column": 5 + }, + { + "id": 3, + "name": "", + "source": { + "name": null, + "path": "C:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "sourceReference": null + }, + "line": 23, + "column": 1 + } + ] + }, + "seq": 0, + "type": "response" + } +``` + +### `evaluate` + +#### Request + +```json + { + "type": "request", + "seq": 13, + "command": "evaluate", + "arguments": { + "expression": "i", + "frameId": 1 + } + } +``` + +#### Response + +```json + { + "request_seq": 13, + "success": true, + "command": "evaluate", + "message": null, + "body": { + "result": "2", + "variablesReference": 0 + }, + "seq": 0, + "type": "response" + } +``` + +# Event Types + +## Script Diagnostics + +### `syntaxDiag` + +```json + { + "event": "syntaxDiag", + "body": { + "file": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "diagnostics": [ + { + "start": { + "line": 3, + "offset": 1 + }, + "end": { + "line": 3, + "offset": 2 + }, + "text": "Missing closing '}' in statement block or type definition.", + "severity": 2 + } + ] + }, + "seq": 0, + "type": "event" + } +``` + +### `semanticDiag` + +```json + { + "event": "semanticDiag", + "body": { + "file": "c:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "diagnostics": [ + { + "start": { + "line": 14, + "offset": 1 + }, + "end": { + "line": 21, + "offset": 2 + }, + "text": "The cmdlet 'Do-Work' uses an unapproved verb.", + "severity": 1 + }, + { + "start": { + "line": 20, + "offset": 5 + }, + "end": { + "line": 20, + "offset": 23 + }, + "text": "File '' uses Write-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all. Use Write-Output instead.", + "severity": 1 + }, + { + "start": { + "line": 18, + "offset": 16 + }, + "end": { + "line": 18, + "offset": 26 + }, + "text": "Variable 'workcount' is not initialized. Non-global variables must be initialized. To fix a violation of this rule, please initialize non-global variables.", + "severity": 1 + } + ] + }, + "seq": 0, + "type": "event" + } +``` + +## Language Service Events + +### `started` + +This message is sent as soon as the language service finishes initializing. The editor will +wait for this message to be received before it starts sending requests to the host. This event +has no body and will always be `null`. + +```json + { + "event": "started", + "body": null, + "seq": 0, + "type": "event" + } +``` + +## Debugger Events + +### `initialized` + +```json + { + "event": "initialized", + "body": null, + "seq": 0, + "type": "event" + } +``` + +### `stopped` + +```json + { + "event": "stopped", + "body": { + "reason": "breakpoint", + "threadId": 1, + "source": { + "name": null, + "path": "C:\\Users\\daviwil\\.vscode\\extensions\\vscode-powershell\\examples\\DebugTest.ps1", + "sourceReference": null + }, + "line": 10, + "column": 9, + "text": null + }, + "seq": 0, + "type": "event" + } +``` + +### `terminated` + +```json + { + "event": "terminated", + "body": null, + "seq": 0, + "type": "event" + } +``` + +# Host Process Lifecycle + +Right now, language and debugging service generally run separately. + +## Language Service + +`started` event, etc + diff --git a/docs/images/connected_bind.png b/docs/images/connected_bind.png new file mode 100644 index 00000000..42be981a Binary files /dev/null and b/docs/images/connected_bind.png differ diff --git a/docs/images/hostprocess.png b/docs/images/hostprocess.png new file mode 100644 index 00000000..1bbded61 Binary files /dev/null and b/docs/images/hostprocess.png differ diff --git a/docs/images/msgdispatch.png b/docs/images/msgdispatch.png new file mode 100644 index 00000000..382069db Binary files /dev/null and b/docs/images/msgdispatch.png differ diff --git a/docs/images/msgdispatchexample.png b/docs/images/msgdispatchexample.png new file mode 100644 index 00000000..8068ebf3 Binary files /dev/null and b/docs/images/msgdispatchexample.png differ diff --git a/docs/index.md b/docs/index.md index ce15d054..60ac90da 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,11 @@ guidance on how to use it. ## [API Reference](api/index.md) -The API Reference contains details about the .NET API. +The .Net API Reference contains details about the .NET API. + +## [Design](design/index.md) + +Design and implementation documentation for the SQL Tools Service SDK. ## Getting Help diff --git a/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriver.cs b/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriver.cs new file mode 100644 index 00000000..b09463ce --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriver.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.JsonRpc.Driver +{ + /// + /// Test driver for the service host + /// + public class ClientDriver : ClientDriverBase + { + public const string ServiceHostEnvironmentVariable = "SQLTOOLSSERVICE_EXE"; + + + private Process[] serviceProcesses; + + private DateTime startTime; + + public ClientDriver() + { + string serviceHostExecutable = Environment.GetEnvironmentVariable(ServiceHostEnvironmentVariable); + string serviceHostArguments = "--enable-logging"; + if (string.IsNullOrWhiteSpace(serviceHostExecutable)) + { + // Include a fallback value to for running tests within visual studio + serviceHostExecutable = @"Microsoft.SqlTools.ServiceLayer.exe"; + } + + // Make sure it exists before continuing + if (!File.Exists(serviceHostExecutable)) + { + throw new FileNotFoundException($"Failed to find Microsoft.SqlTools.ServiceLayer.exe at provided location '{serviceHostExecutable}'. " + + "Please set SQLTOOLSERVICE_EXE environment variable to location of exe"); + } + + this.clientChannel = new StdioClientChannel(serviceHostExecutable, serviceHostArguments); + this.protocolClient = new ProtocolEndpoint(clientChannel, MessageProtocolType.LanguageServer); + } + + /// + /// Start the test driver, and launch the sqltoolsservice executable + /// + public async Task Start() + { + // Store the time we started + startTime = DateTime.Now; + + // Launch the process + await this.protocolClient.Start(); + await Task.Delay(1000); // Wait for the service host to start + + Console.WriteLine("Successfully launched service"); + + // Setup events to queue for testing + this.QueueEventsForType(ConnectionCompleteNotification.Type); + this.QueueEventsForType(IntelliSenseReadyNotification.Type); + this.QueueEventsForType(QueryExecuteCompleteEvent.Type); + this.QueueEventsForType(PublishDiagnosticsNotification.Type); + } + + /// + /// Stop the test driver, and shutdown the sqltoolsservice executable + /// + public async Task Stop() + { + + await this.protocolClient.Stop(); + } + + private async Task GetServiceProcess(CancellationToken token) + { + while (serviceProcesses == null && !token.IsCancellationRequested) + { + var processes = Process.GetProcessesByName("Microsoft.SqlTools.ServiceLayer") + .Where(p => p.StartTime >= startTime).ToArray(); + + // Wait a second if we can't find the process + if (processes.Any()) + { + serviceProcesses = processes; + } + else + { + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + } + } + } +} diff --git a/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriverBase.cs b/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriverBase.cs new file mode 100644 index 00000000..d0b5e68f --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/Driver/ClientDriverBase.cs @@ -0,0 +1,187 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel; +using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.JsonRpc.Driver +{ + /// + /// Wraps the ProtocolEndpoint class with queues to handle events/requests + /// + public class ClientDriverBase + { + protected ProtocolEndpoint protocolClient; + + protected StdioClientChannel clientChannel; + + private ConcurrentDictionary> eventQueuePerType = + new ConcurrentDictionary>(); + + private ConcurrentDictionary> requestQueuePerType = + new ConcurrentDictionary>(); + + public Process ServiceProcess + { + get + { + try + { + return Process.GetProcessById(clientChannel.ProcessId); + } + catch + { + return null; + } + } + } + + public Task SendRequest( + RequestType requestType, + TParams requestParams) + { + return + this.protocolClient.SendRequest( + requestType, + requestParams); + } + + public Task SendEvent(EventType eventType, TParams eventParams) + { + return + this.protocolClient.SendEvent( + eventType, + eventParams); + } + + public void QueueEventsForType(EventType eventType) + { + var eventQueue = + this.eventQueuePerType.AddOrUpdate( + eventType.MethodName, + new AsyncQueue(), + (key, queue) => queue); + + this.protocolClient.SetEventHandler( + eventType, + (p, ctx) => + { + return eventQueue.EnqueueAsync(p); + }); + } + + public async Task WaitForEvent( + EventType eventType, + int timeoutMilliseconds = 5000) + { + Task eventTask = null; + + // Use the event queue if one has been registered + AsyncQueue eventQueue = null; + if (this.eventQueuePerType.TryGetValue(eventType.MethodName, out eventQueue)) + { + eventTask = + eventQueue + .DequeueAsync() + .ContinueWith( + task => (TParams)task.Result); + } + else + { + TaskCompletionSource eventTaskSource = new TaskCompletionSource(); + + this.protocolClient.SetEventHandler( + eventType, + (p, ctx) => + { + if (!eventTaskSource.Task.IsCompleted) + { + eventTaskSource.SetResult(p); + } + + return Task.FromResult(true); + }, + true); // Override any existing handler + + eventTask = eventTaskSource.Task; + } + + await + Task.WhenAny( + eventTask, + Task.Delay(timeoutMilliseconds)); + + if (!eventTask.IsCompleted) + { + throw new TimeoutException( + string.Format( + "Timed out waiting for '{0}' event!", + eventType.MethodName)); + } + + return await eventTask; + } + + public async Task>> WaitForRequest( + RequestType requestType, + int timeoutMilliseconds = 5000) + { + Task>> requestTask = null; + + // Use the request queue if one has been registered + AsyncQueue requestQueue = null; + if (this.requestQueuePerType.TryGetValue(requestType.MethodName, out requestQueue)) + { + requestTask = + requestQueue + .DequeueAsync() + .ContinueWith( + task => (Tuple>)task.Result); + } + else + { + var requestTaskSource = + new TaskCompletionSource>>(); + + this.protocolClient.SetRequestHandler( + requestType, + (p, ctx) => + { + if (!requestTaskSource.Task.IsCompleted) + { + requestTaskSource.SetResult( + new Tuple>(p, ctx)); + } + + return Task.FromResult(true); + }); + + requestTask = requestTaskSource.Task; + } + + await + Task.WhenAny( + requestTask, + Task.Delay(timeoutMilliseconds)); + + if (!requestTask.IsCompleted) + { + throw new TimeoutException( + string.Format( + "Timed out waiting for '{0}' request!", + requestType.MethodName)); + } + + return await requestTask; + } + } +} + diff --git a/docs/samples/jsonrpc/netcore/executequery/Program.cs b/docs/samples/jsonrpc/netcore/executequery/Program.cs new file mode 100644 index 00000000..abfacc2e --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/Program.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Threading.Tasks; +using Microsoft.SqlTools.JsonRpc.Utility; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; + +namespace Microsoft.SqlTools.JsonRpc.ExecuteQuery +{ + /// + /// Simple JSON-RPC API sample to connect to a database, execute a query, and print the results + /// + internal class Program + { + internal static void Main(string[] args) + { + // set SQLTOOLSSERVICE_EXE to location of SQL Tools Service executable + Environment.SetEnvironmentVariable("SQLTOOLSSERVICE_EXE", @"Microsoft.SqlTools.ServiceLayer.exe"); + + // execute a query against localhost, master, with IntegratedAuth + ExecuteQuery("SELECT * FROM sys.objects").Wait(); + } + + internal static async Task ExecuteQuery(string query) + { + // create a temporary "workspace" file + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + // create the client helper which wraps the client driver objects + using (ClientHelper testHelper = new ClientHelper()) + { + // connnection details + ConnectParams connectParams = new ConnectParams(); + connectParams.Connection = new ConnectionDetails(); + connectParams.Connection.ServerName = "localhost"; + connectParams.Connection.DatabaseName = "master"; + connectParams.Connection.AuthenticationType = "Integrated"; + + // connect to the database + await testHelper.Connect(queryTempFile.FilePath, connectParams); + + // execute the query + QueryExecuteCompleteParams queryComplete = + await testHelper.RunQuery(queryTempFile.FilePath, query); + + if (queryComplete.BatchSummaries != null && queryComplete.BatchSummaries.Length > 0) + { + var batch = queryComplete.BatchSummaries[0]; + if (batch.ResultSetSummaries != null && batch.ResultSetSummaries.Length > 0) + { + var resultSet = batch.ResultSetSummaries[0]; + + // retrive the results + QueryExecuteSubsetResult querySubset = await testHelper.ExecuteSubset( + queryTempFile.FilePath, batch.Id, + resultSet.Id, 0, (int)resultSet.RowCount); + + // print the header + foreach (var column in resultSet.ColumnInfo) + { + Console.Write(column.ColumnName + ", "); + } + Console.Write(Environment.NewLine); + + // print the rows + foreach (var row in querySubset.ResultSubset.Rows) + { + for (int i = 0; i < resultSet.ColumnInfo.Length; ++i) + { + Console.Write(row.GetValue(i) + ", "); + } + Console.Write(Environment.NewLine); + } + } + } + + // close database connection + await testHelper.Disconnect(queryTempFile.FilePath); + } + } + } +} diff --git a/docs/samples/jsonrpc/netcore/executequery/Utility/ClientHelper.cs b/docs/samples/jsonrpc/netcore/executequery/Utility/ClientHelper.cs new file mode 100644 index 00000000..28f5f2f5 --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/Utility/ClientHelper.cs @@ -0,0 +1,255 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SqlTools.JsonRpc.Driver; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts; +using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; +using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; + +namespace Microsoft.SqlTools.JsonRpc.Utility +{ + /// + /// Base class for all test suites run by the test driver + /// + public sealed class ClientHelper : IDisposable + { + private bool isRunning = false; + + public ClientHelper() + { + Driver = new ClientDriver(); + Driver.Start().Wait(); + this.isRunning = true; + } + + public void Dispose() + { + if (this.isRunning) + { + WaitForExit(); + } + } + + public void WaitForExit() + { + try + { + this.isRunning = false; + Driver.Stop().Wait(); + Console.WriteLine("Successfully killed process."); + } + catch(Exception e) + { + Console.WriteLine($"Exception while waiting for service exit: {e.Message}"); + } + } + + /// + /// The driver object used to read/write data to the service + /// + public ClientDriver Driver + { + get; + private set; + } + + private object fileLock = new Object(); + + /// + /// Request a new connection to be created + /// + /// True if the connection completed successfully + public async Task Connect(string ownerUri, ConnectParams connectParams, int timeout = 15000) + { + connectParams.OwnerUri = ownerUri; + var connectResult = await Driver.SendRequest(ConnectionRequest.Type, connectParams); + if (connectResult) + { + var completeEvent = await Driver.WaitForEvent(ConnectionCompleteNotification.Type, timeout); + return !string.IsNullOrEmpty(completeEvent.ConnectionId); + } + else + { + return false; + } + } + + /// + /// Request a disconnect + /// + public async Task Disconnect(string ownerUri) + { + var disconnectParams = new DisconnectParams(); + disconnectParams.OwnerUri = ownerUri; + + var disconnectResult = await Driver.SendRequest(DisconnectRequest.Type, disconnectParams); + return disconnectResult; + } + + /// + /// Request a cancel connect + /// + public async Task CancelConnect(string ownerUri) + { + var cancelParams = new CancelConnectParams(); + cancelParams.OwnerUri = ownerUri; + + return await Driver.SendRequest(CancelConnectRequest.Type, cancelParams); + } + + /// + /// Request a cancel connect + /// + public async Task ListDatabases(string ownerUri) + { + var listParams = new ListDatabasesParams(); + listParams.OwnerUri = ownerUri; + + return await Driver.SendRequest(ListDatabasesRequest.Type, listParams); + } + + /// + /// Request the active SQL script is parsed for errors + /// + public async Task RequestQueryExecuteSubset(QueryExecuteSubsetParams subsetParams) + { + return await Driver.SendRequest(QueryExecuteSubsetRequest.Type, subsetParams); + } + + /// + /// Request the active SQL script is parsed for errors + /// + public async Task RequestOpenDocumentNotification(DidOpenTextDocumentNotification openParams) + { + await Driver.SendEvent(DidOpenTextDocumentNotification.Type, openParams); + } + + /// + /// Request a configuration change notification + /// + public async Task RequestChangeConfigurationNotification(DidChangeConfigurationParams configParams) + { + await Driver.SendEvent(DidChangeConfigurationNotification.Type, configParams); + } + + /// + /// /// Request the active SQL script is parsed for errors + /// + public async Task RequestChangeTextDocumentNotification(DidChangeTextDocumentParams changeParams) + { + await Driver.SendEvent(DidChangeTextDocumentNotification.Type, changeParams); + } + + /// + /// Request completion item resolve to look-up additional info + /// + public async Task RequestResolveCompletion(CompletionItem item) + { + var result = await Driver.SendRequest(CompletionResolveRequest.Type, item); + return result; + } + + // /// + // /// Request a Read Credential for given credential id + // /// + // public async Task ReadCredential(string credentialId) + // { + // var credentialParams = new Credential(); + // credentialParams.CredentialId = credentialId; + + // return await Driver.SendRequest(ReadCredentialRequest.Type, credentialParams); + // } + + /// + /// Run a query using a given connection bound to a URI + /// + public async Task RunQuery(string ownerUri, string query, int timeoutMilliseconds = 5000) + { + // Write the query text to a backing file + WriteToFile(ownerUri, query); + + var queryParams = new QueryExecuteParams + { + OwnerUri = ownerUri, + QuerySelection = null + }; + + var result = await Driver.SendRequest(QueryExecuteRequest.Type, queryParams); + if (result != null && string.IsNullOrEmpty(result.Messages)) + { + var eventResult = await Driver.WaitForEvent(QueryExecuteCompleteEvent.Type, timeoutMilliseconds); + return eventResult; + } + else + { + return null; + } + } + + /// + /// Request to save query results as CSV + /// + public async Task SaveAsCsv(string ownerUri, string filename, int batchIndex, int resultSetIndex) + { + var saveParams = new SaveResultsAsCsvRequestParams + { + OwnerUri = ownerUri, + BatchIndex = batchIndex, + ResultSetIndex = resultSetIndex, + FilePath = filename + }; + + var result = await Driver.SendRequest(SaveResultsAsCsvRequest.Type, saveParams); + return result; + } + + /// + /// Request to save query results as JSON + /// + public async Task SaveAsJson(string ownerUri, string filename, int batchIndex, int resultSetIndex) + { + var saveParams = new SaveResultsAsJsonRequestParams + { + OwnerUri = ownerUri, + BatchIndex = batchIndex, + ResultSetIndex = resultSetIndex, + FilePath = filename + }; + + var result = await Driver.SendRequest(SaveResultsAsJsonRequest.Type, saveParams); + return result; + } + + /// + /// Request a subset of results from a query + /// + public async Task ExecuteSubset(string ownerUri, int batchIndex, int resultSetIndex, int rowStartIndex, int rowCount) + { + var subsetParams = new QueryExecuteSubsetParams(); + subsetParams.OwnerUri = ownerUri; + subsetParams.BatchIndex = batchIndex; + subsetParams.ResultSetIndex = resultSetIndex; + subsetParams.RowsStartIndex = rowStartIndex; + subsetParams.RowsCount = rowCount; + + var result = await Driver.SendRequest(QueryExecuteSubsetRequest.Type, subsetParams); + return result; + } + + public void WriteToFile(string ownerUri, string query) + { + lock (fileLock) + { + System.IO.File.WriteAllText(ownerUri, query); + } + } + } +} diff --git a/docs/samples/jsonrpc/netcore/executequery/Utility/SelfCleaningTempFile.cs b/docs/samples/jsonrpc/netcore/executequery/Utility/SelfCleaningTempFile.cs new file mode 100644 index 00000000..69e1f97e --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/Utility/SelfCleaningTempFile.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.IO; + +namespace Microsoft.SqlTools.JsonRpc.Utility +{ + public class SelfCleaningTempFile : IDisposable + { + private bool disposed; + + public SelfCleaningTempFile() + { + FilePath = Path.GetTempFileName(); + } + + public string FilePath { get; private set; } + + #region IDisposable Implementation + + public void Dispose() + { + if (!disposed) + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + + public void Dispose(bool disposing) + { + if (!disposed && disposing) + { + try + { + File.Delete(FilePath); + } + catch + { + Console.WriteLine($"Failed to cleanup {FilePath}"); + } + } + + disposed = true; + } + + #endregion + + } +} diff --git a/docs/samples/jsonrpc/netcore/executequery/project.json b/docs/samples/jsonrpc/netcore/executequery/project.json new file mode 100644 index 00000000..b6842ab4 --- /dev/null +++ b/docs/samples/jsonrpc/netcore/executequery/project.json @@ -0,0 +1,49 @@ +{ + "name": "Microsoft.SqlTools.JsonRpcTest.ExecuteQuery", + "version": "1.0.0-*", + "buildOptions": { + "debugType": "portable", + "emitEntryPoint": true + }, + "dependencies": { + "Microsoft.SqlTools.ServiceLayer": { + "target": "project" + }, + "Newtonsoft.Json": "9.0.1", + "System.Data.Common": "4.1.0", + "System.Data.SqlClient": "4.4.0-sqltools-24613-04", + "Microsoft.SqlServer.Smo": "140.1.12", + "System.Security.SecureString": "4.0.0", + "System.Collections.Specialized": "4.0.1", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.Contracts": "4.0.1", + "System.Diagnostics.TraceSource": "4.0.0", + "NETStandard.Library": "1.6.0", + "Microsoft.NETCore.Runtime.CoreCLR": "1.0.2", + "Microsoft.NETCore.DotNetHostPolicy": "1.0.1", + "System.Diagnostics.Process": "4.1.0", + "System.Threading.Thread": "4.0.0" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0" + } + }, + "imports": "dnxcore50" + } + }, + "runtimes": { + "win7-x64": {}, + "win7-x86": {}, + "osx.10.11-x64": {}, + "ubuntu.14.04-x64": {}, + "ubuntu.16.04-x64": {}, + "centos.7-x64": {}, + "rhel.7.2-x64": {}, + "debian.8-x64": {}, + "fedora.23-x64": {}, + "opensuse.13.2-x64": {} + } +} diff --git a/docs/samples/smo/PowerShellScript/createxmlshema.ps1 b/docs/samples/smo/PowerShellScript/createxmlshema.ps1 new file mode 100644 index 00000000..b48910ab --- /dev/null +++ b/docs/samples/smo/PowerShellScript/createxmlshema.ps1 @@ -0,0 +1,28 @@ +<# +This code example shows how to create an XML schema by using the XmlSchemaCollection object. +#> + +#DLL location needs to be specified +$pathtodll = "" +Add-Type -Path "$pathtodll\Microsoft.SqlServer.Smo.dll" +Add-Type -Path "$pathtodll\Microsoft.SqlServer.ConnectionInfo.dll" + +#Connection context need to be specified +$srv = New-Object Microsoft.SqlServer.Management.Smo.Server() +$srv.ConnectionContext.LoginSecure = $false +$srv.ConnectionContext.ServerInstance = "instance_name" +$srv.ConnectionContext.Login = "user_id" +$srv.ConnectionContext.Password = "pwd" + +#Reference the master database +$db = $srv.Databases["master"] + +#Create a new schema collection +$xsc = New-Object -TypeName Microsoft.SqlServer.Management.SMO.XmlSchemaCollection -argumentlist $db,"SampleCollection" + +#Add the xml +$dq = '"' # the double quote character +$xsc.Text = "" + +#Create the XML schema collection on the instance of SQL Server. +$xsc.Create diff --git a/docs/samples/smo/PowerShellScript/dbiteration.ps1 b/docs/samples/smo/PowerShellScript/dbiteration.ps1 new file mode 100644 index 00000000..093ea602 --- /dev/null +++ b/docs/samples/smo/PowerShellScript/dbiteration.ps1 @@ -0,0 +1,27 @@ +<# +This script demonstrates iterations through the rows and display collation details for a remote or local instance of SQL Server. +#> + +#DLL location needs to be specified +$pathtodll = "" + +Add-Type -Path "$pathtodll\Microsoft.SqlServer.Smo.dll" +Add-Type -Path "$pathtodll\Microsoft.SqlServer.ConnectionInfo.dll" + +#Connection context need to be specified +$srv = New-Object Microsoft.SqlServer.Management.Smo.Server() +$srv.ConnectionContext.LoginSecure = $false +$srv.ConnectionContext.ServerInstance = "instance_name" +$srv.ConnectionContext.Login = "user_id" +$srv.ConnectionContext.Password = "pwd" + +$datatable = $srv.EnumCollations() + +Foreach ($row in $datatable.Rows) +{ + Write-Host "============================================" + Foreach ($column in $row.Table.Columns) + { + Write-Host $column.ColumnName "=" $row[$column].ToString() + } +} \ No newline at end of file diff --git a/docs/samples/smo/PowerShellScript/managetable.ps1 b/docs/samples/smo/PowerShellScript/managetable.ps1 new file mode 100644 index 00000000..424803b0 --- /dev/null +++ b/docs/samples/smo/PowerShellScript/managetable.ps1 @@ -0,0 +1,58 @@ +<# +This code example creates a table that has several columns with different types and purposes. The code also provides examples of how to create an identity field, how to create a primary key, and how to alter table properties. +#> + +#DLL location needs to be specified +$pathtodll = "" +Add-Type -Path "$pathtodll\Microsoft.SqlServer.Smo.dll" +Add-Type -Path "$pathtodll\Microsoft.SqlServer.ConnectionInfo.dll" + +#Connection context need to be specified +$srv = New-Object Microsoft.SqlServer.Management.Smo.Server() +$srv.ConnectionContext.LoginSecure = $false +$srv.ConnectionContext.ServerInstance = "instance_name" +$srv.ConnectionContext.Login = "user_id" +$srv.ConnectionContext.Password = "pwd" + +#And the database object corresponding to master. +$db = $srv.Databases["master"] + +#Create a SMO Table +$tb = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Table -argumentlist $db, "Test_Table" + +#Add various columns to the table. +$Type = [Microsoft.SqlServer.Management.SMO.DataType]::NChar(50) +$col1 = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Column -argumentlist $tb,"Name", $Type +$col1.Collation = "Latin1_General_CI_AS" +$col1.Nullable = $true +$tb.Columns.Add($col1) + +$Type = [Microsoft.SqlServer.Management.SMO.DataType]::Int +$col2 = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Column -argumentlist $tb,"ID", $Type +$col2.Identity = $true +$col2.IdentitySeed = 1 +$col2.IdentityIncrement = 1 +$tb.Columns.Add($col2) + +$Type = [Microsoft.SqlServer.Management.SMO.DataType]::Real +$col3 = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Column -argumentlist $tb,"Value", $Type +$tb.Columns.Add($col3) + +$Type = [Microsoft.SqlServer.Management.SMO.DataType]::DateTime +$col4 = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Column -argumentlist $tb,"Date", $Type +$col4.Nullable = $false +$tb.Columns.Add($col4) + +#Create the table +$tb.Create() + +$Type = [Microsoft.SqlServer.Management.SMO.DataType]::DateTime +$col5 = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Column -argumentlist $tb,"ExpiryDate", $Type +$col5.Nullable = $false +$tb.Columns.Add($col5) + +#Run the Alter method to make the change on the instance of SQL Server. +$tb.Alter() + +#Remove the table from the database. +$tb.Drop() diff --git a/docs/samples/smo/net45/Scripting/Program.cs b/docs/samples/smo/net45/Scripting/Program.cs new file mode 100644 index 00000000..8a2773e2 --- /dev/null +++ b/docs/samples/smo/net45/Scripting/Program.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Sdk.Sfc; + +namespace Microsoft.SqlServer.Management.SmoSdkSamples +{ + // This application demonstrates how to script out a sample database without dependencies and iterate through the list to display the results. + public class Program + { + public static void Main(string[] args) + { + // Connect to the local, default instance of SQL Server. + Smo.Server srv = new Smo.Server(); + // database name + Console.WriteLine("Enter database name for scripting:"); + string dbName = Console.ReadLine(); + // Reference the database. + Database db = srv.Databases[dbName]; + // Define a Scripter object and set the required scripting options. + Scripter scripter = new Scripter(srv); + scripter.Options.ScriptDrops = false; + // To include indexes + scripter.Options.Indexes = true; + // to include referential constraints in the script + scripter.Options.DriAllConstraints = true; + // Iterate through the tables in database and script each one. Display the script. + foreach (Table tb in db.Tables) + { + // check if the table is not a system table + if (tb.IsSystemObject == false) + { + Console.WriteLine("-- Scripting for table " + tb.Name); + // Generating script for table tb + System.Collections.Specialized.StringCollection sc = scripter.Script(new Urn[] { tb.Urn }); + foreach (string st in sc) + { + Console.WriteLine(st); + } + Console.WriteLine("--"); + } + } + + } + } +} \ No newline at end of file diff --git a/docs/samples/smo/net45/Scripting/Scripting.csproj b/docs/samples/smo/net45/Scripting/Scripting.csproj new file mode 100644 index 00000000..73ee5613 --- /dev/null +++ b/docs/samples/smo/net45/Scripting/Scripting.csproj @@ -0,0 +1,66 @@ + + + + + Debug + AnyCPU + {C0CACFBB-FDAF-4B3D-B9F7-EBC3179A5055} + Exe + Properties + Microsoft.SqlServer.Management.SmoSdkSamples + SmoSdkSamples + v4.0 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\SharedManagementObjects.140.1.9\lib\net40\Microsoft.SqlServer.Management.Sdk.Sfc.dll + + + False + ..\packages\SharedManagementObjects.140.1.9\lib\net40\Microsoft.SqlServer.Smo.dll + + + False + ..\packages\SharedManagementObjects.140.1.9\lib\net40\Microsoft.SqlServer.SmoExtended.dll + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/samples/smo/netcore/ModifySetting/Program.cs b/docs/samples/smo/netcore/ModifySetting/Program.cs new file mode 100644 index 00000000..4104c90c --- /dev/null +++ b/docs/samples/smo/netcore/ModifySetting/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.Data.Tools.DataSets; +using Microsoft.SqlServer.Management.Smo; +using System; + +namespace Microsoft.SqlServer.Management.SmoSdkSamples +{ + // This example displays information about the instance of SQL Server in Information and Settings, and modifies settings in Settings and UserOptionsobject properties. + public class Program + { + public static void Main(string[] args) + { + //Connect to the local, default instance of SQL Server. + Microsoft.SqlServer.Management.Smo.Server srv = new Microsoft.SqlServer.Management.Smo.Server(); + //Display all the configuration options. + foreach (ConfigProperty p in srv.Configuration.Properties) + { + Console.WriteLine(p.DisplayName); + } + Console.WriteLine("There are " + srv.Configuration.Properties.Count.ToString() + " configuration options."); + //Display the maximum and minimum values for ShowAdvancedOptions. + int min = srv.Configuration.ShowAdvancedOptions.Minimum; + int max = srv.Configuration.ShowAdvancedOptions.Maximum; + Console.WriteLine("Minimum and Maximum values are " + min + " and " + max + "."); + int configvalue = srv.Configuration.ShowAdvancedOptions.ConfigValue; + //Modify the value of ShowAdvancedOptions and run the Alter method. + srv.Configuration.ShowAdvancedOptions.ConfigValue = 0; + srv.Configuration.Alter(); + //Display when the change takes place according to the IsDynamic property. + if (srv.Configuration.ShowAdvancedOptions.IsDynamic == true) + { + Console.WriteLine("Configuration option has been updated."); + } + else + { + Console.WriteLine("Configuration option will be updated when SQL Server is restarted."); + } + // Recover setting value + srv.Configuration.ShowAdvancedOptions.ConfigValue = configvalue; + srv.Configuration.Alter(); + } + } +} \ No newline at end of file diff --git a/docs/samples/smo/netcore/ModifySetting/project.json b/docs/samples/smo/netcore/ModifySetting/project.json new file mode 100644 index 00000000..b9130f34 --- /dev/null +++ b/docs/samples/smo/netcore/ModifySetting/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "debugType": "portable", + "emitEntryPoint": true + }, + "dependencies": { + "Microsoft.SqlServer.Smo": "140.1.12" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + } + }, + "imports": "dnxcore50" + } + } +} diff --git a/docs/toc.yml b/docs/toc.yml index 01f0d406..9c37b31c 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -3,3 +3,7 @@ homepage: guide/introduction.md - name: API Reference href: api/ + homepage: api/index.md +- name: Design + href: design/ + homepage: design/index.md diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index 1b28daba..d96eb388 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -92,7 +92,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection public delegate Task OnConnectionHandler(ConnectionInfo info); /// - // Callback for ondisconnect handler + /// Callback for ondisconnect handler /// public delegate Task OnDisconnectHandler(ConnectionSummary summary, string ownerUri);