blob: be389706de1fc0b2700e1ea6b545a622399357f8 [file] [log] [blame] [view] [edit]
# Dart Tooling Daemon Protocol
This document describes the Dart Tooling Daemon protocol. This daemon, and it's
protocol is meant to facilitate communication channels between various tools.
To start a tooling daemon run `dart tooling-daemon`.
If you have access to `Dart` then you can use the [package:dtd](#packagedtd)
package.
Otherwise, if you are creating your own client that will communicate with DTD
using RPC, make sure to follow the entire
[Dart Tooling Daemon RPC protocol](#dart-tooling-daemon-rpc-protocol) section.
The Dart Tooling Daemon Protocol uses JSON-RPC 2.0.
## Common Services Definitions
Some common services are defined to ensure that different tools can provide the
same functionality consistently to DTD clients. Details of these can be found in
[dtd_common_services](./dtd_common_services.md).
## Visualizing DTD interactions
The following diagrams visualize how the Dart Tooling Daemon interacts with
clients.
### Streams
The following is a visualization of normal stream interaction. It is presented
in function notation, instead of RPC calls, for brevity.
```mermaid
sequenceDiagram
participant c1 as Client 1
participant c2 as Client 2
participant dtd as Dart Tooling Daemon
participant c3 as Client 3
c1->>dtd: streamListen(streamId: "foo")
activate dtd
dtd-->>c1: Success
deactivate dtd
c2->>dtd: streamListen(streamId: "foo")
activate dtd
dtd-->>c2: Success
deactivate dtd
Note right of c2: Client 1 and Client 2 are now listening to the "foo" stream.
Note right of dtd: Client 3 posts an event to the "foo" stream.
c3->>dtd: postEvent( streamId: "foo", eventKind: "example", eventData: { "bar": "baz" })
activate dtd
dtd-->>c3: Success
Note right of c2: Client 1 and Client 2 are forwarded the event using streamNotify.
dtd-->>c2: streamNotify( streamId: "foo", eventKind: "example", eventData: { "bar": "baz" })
dtd-->>c1: streamNotify( streamId: "foo", eventKind: "example", eventData: { "bar": "baz" })
deactivate dtd
Note right of c2: Client 2 now cancels listening to the "foo" stream.
c2-->>dtd: streamCancel(streamId: "foo")
activate dtd
dtd-->>c2: Success
deactivate dtd
Note right of dtd: Client 3 posts another event to the "foo" stream.
c3->>dtd: postEvent( streamId: "foo", eventKind: "example", eventData: { "bar": "baz 2" })
activate dtd
dtd-->>c3: Success
Note right of c2: Client 1 is still listening to the "foo" stream, so the event<br/>is still forwarded to it using streamNotify.
dtd-->>c1: streamNotify( streamId: "foo", eventKind: "example", eventData: { "bar": "baz2 " })
deactivate dtd
```
### Service Methods
The following is a visualization of normal service method interaction. It is
presented in function notation, instead of RPC calls, for brevity.
```mermaid
sequenceDiagram
participant c1 as Client 1
participant dtd as Dart Tooling Daemon
participant c2 as Client 2
Note right of c1: Client 1 registers the Foo.bar method.
c1->>dtd: registerService(service: "Foo", method: "bar")
activate dtd
dtd-->>c2: ServiceRegistered({"service": "Foo", "method": "bar"})
dtd-->>c1: Success
deactivate dtd
Note left of c2: Client 2 calls Foo.bar
c2->>dtd: Foo.bar({{"a": 1, "b": 2}})
activate dtd
Note left of dtd: dtd forwards the service method call to Client 1.
dtd->>c1: Foo.bar({{"a": 1, "b": 2}})
activate c1
Note right of c1: Client 1 handles the<br/>request and responds to the Dart Tooling Daemon.
c1-->>dtd: {"example": "response"}
deactivate c1
Note right of dtd: Dart Tooling Daemon forwards the response to Client 2.
dtd-->>c2: {"example": "response"}
deactivate dtd
Note right of c1: Client 1 disconnects.
activate dtd
dtd-->>c2: ServiceUnregistered({"service": "Foo", "method": "bar"})
deactivate dtd
```
`registerService` takes an optional map of capabilities that can be used for
evolving the service method over time (such as expressing support for additional
named values in the arguments).
```json
{
"jsonrpc": "2.0",
"method": "registerService",
"params": {
"service": "Foo",
"method": "bar",
"capabilities": {
"supportsAdditionalFoo": true,
},
},
"id": "2"
}
```
Notifications will be sent over a special `Service` stream when services are
registered and unregistered.
```json
{
"jsonrpc": "2.0",
"method": "streamNotify",
"params": {
"streamId": "Service",
"eventKind": "ServiceRegistered",
"eventData": {
"service": "Foo",
"method": "bar",
// Capabilities are included only if the client provided them
"capabilities": {
"supportsAdditionalFoo": true,
},
}
},
"id": "2"
}
{
"jsonrpc": "2.0",
"method": "streamNotify",
"params": {
"streamId": "Service",
"eventKind": "ServiceUnregistered",
"eventData": {
"service": "Foo",
"method": "bar",
// Unregistered events do not contain capabilities
}
},
"id": "2"
}
```
## Dart Tooling Daemon Interaction
### package:dtd
[package:dtd](https://pub.dev/packages/dtd) is available for use with Dart.
It facilitates access to the Dart Tooling Daemon, by providing a convenient Dart
interface.
### Dart Tooling Daemon RPC protocol
To facilitate RPC interaction with the Dart Tooling Daemon, this section lays
out the methods and interactions involved.
If you are implementing a custom client then make sure to follow all of the
parts in this section, including the
[Custom client methods](#custom-client-methods) section.
#### `streamListen`
Registers the calling client as listening to the stream named _streamId_.
The calling client will then receive [streamNotify](#streamnotify) calls when
any client calls [postEvent](#postevent) on the _streamId_ stream.
##### Parameters
`String streamId` - The name of the stream to start listening to.
##### Result
If successful, responds with [Success](#success-responses).
If the client is already subscribed to the stream, the _103_ (Stream already
subscribed) [RPC error](#rpc-errors) code is returned.
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "streamListen",
"params": {
"streamId": "foo_stream"
},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
#### `streamCancel`
Unregisters the calling client from listening to the stream named _streamId_.
The calling client will no longer receive [streamNotify](#streamnotify) method
calls for events on the _streamId_ stream.
##### Parameters
`String streamId` - The name of the stream to stop listening to.
##### Result
If successful, responds with [Success](#success-responses).
If the client is not subscribed to the stream, the _104_ (Stream not
subscribed) [RPC error](#rpc-errors) code is returned.
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "streamCancel",
"params": {
"streamId": "foo_stream"
},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
#### `postEvent`
Calls [streamNotify](#streamnotify) on all clients that are currently
[streamListen](#streamlisten)ing to the _streamId_ stream.
##### Parameters
- `String streamId` - The stream to post the event to
- `String eventKind` - The kind of event being sent.
- `Map<String, Object?> eventData` - A map object of data to send with the
event. _eventData_ must be serializable to json.
##### Result
If successful, responds with [Success](#success-responses).
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "postEvent",
"params": {
"streamId": "fooStream",
"eventKind": "bar",
"eventData": { "bazKey": "apple" }
},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
#### `registerService`
Registers the calling client as the handler for any service method calls where
the service is _service_ and the method is _method_.
Service names may not include dots `.` (though method names may), but service
names cannot be reused across clients. If one client has registered any method
for the service "Foo", then no other client can register a method under the
service "Foo".
To call the method registered by this call, a client can send a
[service method](#servicemethod) call to the Dart Tooling Daemon.
When a client disconnects, then all of the service methods registered to it are
removed.
##### Parameters
- `String service` - The name of the service to register the client to. Once a
client has registered a method to a service then only that client can register
other methods to that service.
- `String method` - The name of the method to register to _service_.
##### Result
If successful, responds with [Success](#success-responses).
If the _service_ has already been registered by another client, the _111_
(Service already registered) [RPC error](#rpc-errors) code is returned. Only one
client at a time may register to a [service]. Once a client disconnects then
another client may register services under that name.
If the _method_ has already been registered on the _service_, the _132_
(Service method already registered) [RPC error](#rpc-errors) code is returned.
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "registerService",
"params": {
"service": "Foo",
"method": "bar",
},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
#### `service`.`method`
Triggers the service method registered by [registerService](#registerservice),
for _service_ and _method_.
Dart Tooling Daemon will forward the service method call to the client that
[registered](#registerservice) it. The client's response will be returned from
this method call.
##### Parameters
The parameters must be a `Map<String, Object?>` where the contents are defined
on a case by case basis on the expectations of the client who registered
the service method.
##### Result
The result is defined on a case by case basis based on the implementer of the
service method.
If service method does not exist, the -32601 (Method not found)
[RPC error](#rpc-errors) code is returned.
##### Code Sample
Assume that a client has registered a service method with:
- service: `Foo`
- method: `bar`
Then calling that service method might look like:
```json
{
"jsonrpc": "2.0",
"method": "Foo.bar",
"params": { "baz": 3 },
"id": "2"
}
```
##### Response
The response is defined on a case by case basis based on the implementer of the
service method.
#### Custom Client Methods
When creating a client that interacts with the Dart Tooling Daemon, register
the following methods on your client in order to participate in the full
lifecycle.
##### `streamNotify`
In order to handle messages posted to streams that the client is
[streamListen](#streamlisten)ing to, then the calling client may handle incoming
`streamNotify` RPC methods.
When any client calls [postEvent](#postevent) for a given _streamId , if a
client has[streamListen](#streamlisten)ed to _streamId_, then that client
will receive `streamNotify` method calls for those events. Implement this method
in order to receive those calls.
###### Parameters
- `String streamId` - The stream to post the event to
- `String eventKind` - The kind of event being sent.
- `Map<String, Object?> eventData` - A map object of data to send with the
event.
###### Result
No response is given since this is sent as a notification.
###### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "streamNotify",
"params": {
"streamId": "fooStream",
"eventKind": "bar",
"eventData": { "baz": "car" }
},
"id": "2"
}
```
###### Response
None, `streamNotify` is called as a notification and expects no response.
##### `service.method`
Any time the client calls [registerService](#registerservice) then it will
receive method calls for that service method.
The method name will take the form `service`.`method`. Any response returned by
the RPC handler will be forwarded to the service method caller.
###### Parameters
The parameters are defined on a case by case basis based on the expectations of
the client who registered the service method.
###### Result
The result is defined on a case by case basis based on the implementer of the
service method.
###### Code Sample
Assume that a client has registered a service method with:
- service: `Foo`
- method: `bar`
Then calling that service method might look like:
```json
{
"jsonrpc": "2.0",
"method": "Foo.bar",
"params": { "baz": 3 },
"id": "2"
}
```
###### Response
The response is defined on a case by case basis based on the implementer of the
service method. It should be a valid RPC response.
## FileSystem service
The `FileSystem` service is coupled tightly with the Dart Tooling Daemon, and
will be present immediately when the Dart Tooling Daemon has started. This
describes the RPC methods used to interact with the `FileSystem` service.
### readFileAsString
Reads the contents of the file at _uri_ on the system where the Dart Tooling
Daemon is running.
#### Parameters
- `String uri` - An absolute path to the file to read. This should be using the
'file' scheme and be an absolute path.
#### Result
If successful, responds with [_FileContent_](#filecontent).
If the file at _uri_ does not exist, the 141 (The file does not exist)
[RPC error](#rpc-errors) code is returned.
If the _uri_ is not contained in the
[IDE Workspace Roots](#ide-workspace-roots), the 142
(Permission denied) [RPC error](#rpc-errors) code is returned.
If _uri_ does not have a 'file' scheme, the 143 (File scheme expected on uri)
[RPC error](#rpc-errors) code is returned.
#### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.readFileAsString",
"params": {
"uri": "file:///path/to/file.txt",
},
"id": "2"
}
```
#### Response
```json
{
"jsonrpc": "2.0",
"result": {
"type": "FileContent",
"content": "The contents\nof the file",
},
"id": "2"
}
```
### writeFileAsString
Writes the contents of the file at _uri_ on the system where the Dart
Tooling Daemon is running. If the file, or parent directories don't exist then
they are created.
#### Parameters
- `String uri` - An absolute path to the file to read. This should be using the
'file' scheme and be an absolute path.
- `String contents` - The content to write to _uri_.
#### Result
If successful, responds with [Success](#success-responses).
If the _uri_ is not contained in the
[IDE Workspace Roots](#ide-workspace-roots), the 142
(Permission denied) [RPC error](#rpc-errors) code is returned.
If _uri_ does not have a 'file' scheme, the 143 (File scheme expected on uri)
[RPC error](#rpc-errors) code is returned.
#### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.writeFileAsString",
"params": {
"uri": "file:///path/to/file.txt",
"contents": "Some contents to write",
},
"id": "2"
}
```
#### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
### listDirectoryContents
Reads the entries of the directory at _uri_ on the system where the Dart Tooling
Daemon is running.
#### Parameters
- `String uri` - An absolute path to the file to read. This should be using the
'file' scheme and be an absolute path.
#### Result
If successful, responds with [_UriList_](#urilist).
If the _uri_ is not contained in the
[IDE Workspace Roots](#ide-workspace-roots), the 142
(Permission denied) [RPC error](#rpc-errors) code is returned.
If _uri_ does not have a 'file' scheme, the 143 (File scheme expected on uri)
[RPC error](#rpc-errors) code is returned.
#### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.listDirectoryContents",
"params": {
"uri": "file:///path/to/dir/",
},
"id": "2"
}
```
#### Response
```json
{
"jsonrpc": "2.0",
"result": {
"type": "UriList",
"uris": [
"file:///path/to/dir/a.txt",
"file:///path/to/dir/b/",
],
},
"id": "2"
}
```
### IDE Workspace Roots
The IDE workspace roots are used to ensure the `FileSystem` service methods
do not perform file operations in areas outside of the IDE workspace roots.
When the Dart Tooling Daemon is started, a secret is generated and presented to
the process that started the Dart Tooling Daemon. That secret is then used to
authenticate when calling [setIDEWorkspaceRoots](#setideworkspaceroots). This
ensures that the process that started the Dart Tooling Daemon is the one setting
the IDE workspace root.
Once the IDE workspace roots are set then `FileSystem` service methods will be
limited to those workspace directories. Any operations outside of them will
result with the 142 (Permission denied) [RPC error](#rpc-errors) code.
If no workspace roots are set then all `FileSystem` service method calls will
fail with the 142 (Permission denied) [RPC error](#rpc-errors) code.
#### setIDEWorkspaceRoots
##### Parameters
- `String secret` - The secret that was passed back to the calling process when
the Dart Tooling Daemon was started.
- `List<String> roots` - The directories to set as the IDE workspace roots.
These must be uris with a 'file' scheme.
##### Result
If successful, responds with [Success](#success-responses).
If _secret_ does not match the secret created when Dart Tooling Daemon was
created then 142 (Permission denied) [RPC error](#rpc-errors) code is returned.
If one of the _roots_ is missing a 'file' scheme, the 143 (File scheme expected on uri)
[RPC error](#rpc-errors) code is returned.
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.setIDEWorkspaceRoots",
"params": {
"secret": "aBVsK8nRfdJsDEnS",
"roots": [
"file:///path/to/dir/a/",
"file:///path/to/dir/b/",
],
},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {"type": "Success"},
"id": "2"
}
```
#### getIDEWorkspaceRoots
Used to get the IDE workspace roots that have been set using
[setIDEWorkspaceRoots](#setideworkspaceroots).
##### Parameters
Takes no parameters.
##### Result
If successful, responds with [_IDEWorkspaceRoots_](#ideworkspaceroots).
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.getIDEWorkspaceRoots",
"params": {},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {
"type": "IDEWorkspaceRoots",
"ideWorkspaceRoots":: [
"file:///path/to/dir/a/",
"file:///path/to/dir/b/",
],
},
"id": "2"
}
```
#### getProjectRoots
Used to get the project roots contained within the current set of IDE workspace roots
(see [setIDEWorkspaceRoots](#setideworkspaceroots) and
[getIDEWorkspaceRoots](#getideworkspaceroots)). A project root is any directory that
contains a `pubspec.yaml` file.
##### Parameters
- `int depth` - The depth that each IDE workspace root directory tree will be searched
for project roots. Defaults to 4.
##### Result
If successful, responds with [_UriList_](#urilist).
##### Code Sample
```json
{
"jsonrpc": "2.0",
"method": "FileSystem.getProjectRoots",
"params": {"depth": 5},
"id": "2"
}
```
##### Response
```json
{
"jsonrpc": "2.0",
"result": {
"type": "UriList",
"uris": [
"file:///path/to/dir/a/",
"file:///path/to/dir/b/",
],
},
"id": "2"
}
```
### File System Service Types
The File System Service Types are RPC data responses used to facilitate
communication with the `FileSystem` service methods. They are used by
[pkg/dtd_impl](./) and [`package:dtd`](../pkg/) to build and parse responses for
the different service methods.
#### `UriList`
Response for communicating a list of URIs.
The returned uris will be `file://` Uris.
Used by `FileSystem.listDirectoryContents`, and `FileSystem.getProjectRoots`.
```json
{
"type": "UriList",
"uris": [
"file:///path/to/file.txt",
"file:///path/to/dir/",
],
}
```
#### `FileContent`
Response for communicating the contents of a file.
Used by `FileSystem.readFileAsString`
```json
{
"type": "FileContent",
"content": "The contents of a file.",
}
```
#### `IDEWorkspaceRoots`
Response for communicating workspace roots.
The returned uris will be `file://` Uris.
Used by `FileSystem.getIDEWorkspaceRoots`
```json
{
"type": "IDEWorkspaceRoots",
"ideWorkSpaceRoots": [
"file:///path/to/root/A/",
"file:///path/to/root/B/",
],
}
```
## DTD RPC Responses
### RPC Errors
When a request to Dart Tooling Daemon encounters an error, it is provided in the _error_
property of the response object. JSON-RPC errors always provide
_code_, _message_, and _data_ properties.
Here is an example error response for our [streamListen](#streamlisten)
request above. This error would be generated if we were attempting to
subscribe to the _GC_ stream multiple times from the same client.
```json
{
"jsonrpc": "2.0",
"error": {
"code": 103,
"message": "Stream already subscribed",
"data": {
"details": "The stream 'GC' is already subscribed"
}
},
"id": "2"
}
```
In addition to the
[error codes](http://www.jsonrpc.org/specification#error_object) specified in
the JSON-RPC spec, we use the following application specific error codes:
code | message | meaning
---- | ------- | -------
-32601 | Method not found | The method does not exist / is not available.
-32602 | Invalid params | Invalid params. Invalid method parameter(s).
103 | Stream already subscribed | The client is already subscribed to the specified _streamId_.
104 | Stream not subscribed | The client is not subscribed to the specified _streamId_.
111 | Service already registered | Service with such name has already been registered by this client.
112 | Service disappeared | Failed to fulfill service request, likely service handler is no longer available.
132 | Service method already registered | Method for the given service has already been registered by this client.
140 | The directory does not exist | The specified directory does not exist.
141 | The file does not exist | The specified file does not exist.
142 | Permission denied | Permission has been denied for the requested action.
143 | File scheme expected on uri | The uri parameter should be passed as a uri with a file scheme.
### Success Responses
Methods that respond with `Success` do so with the following RPC.
```json
{"id": "2", "type": "Success"}
```