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 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 section.

The Dart Tooling Daemon Protocol uses JSON-RPC 2.0.

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.

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.

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).

{
  "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.

{
    "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 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 section.

streamListen

Registers the calling client as listening to the stream named streamId.

The calling client will then receive streamNotify calls when any client calls postEvent on the streamId stream.

Parameters

String streamId - The name of the stream to start listening to.

Result

If successful, responds with Success.

If the client is already subscribed to the stream, the 103 (Stream already subscribed) RPC error code is returned.

Code Sample
{
  "jsonrpc": "2.0",
  "method": "streamListen",
  "params": {
    "streamId": "foo_stream"
  },
  "id": "2"
}
Response
{
  "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 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.

If the client is not subscribed to the stream, the 104 (Stream not subscribed) RPC error code is returned.

Code Sample
{
  "jsonrpc": "2.0",
  "method": "streamCancel",
  "params": {
    "streamId": "foo_stream"
  },
  "id": "2"
}
Response
{
  "jsonrpc": "2.0",
  "result": {"type": "Success"},
  "id": "2"
}

postEvent

Calls streamNotify on all clients that are currently streamListening 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.

Code Sample
{
  "jsonrpc": "2.0",
  "method": "postEvent",
  "params": {
    "streamId": "fooStream",
    "eventKind": "bar",
    "eventData": { "bazKey": "apple" }
  },
  "id": "2"
}
Response
{
  "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 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.

If the service has already been registered by another client, the 111 (Service already registered) RPC error 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 code is returned.

Code Sample
{
  "jsonrpc": "2.0",
  "method": "registerService",
  "params": {
    "service": "Foo",
    "method": "bar",
  },
  "id": "2"
}
Response
{
  "jsonrpc": "2.0",
  "result": {"type": "Success"},
  "id": "2"
}

service.method

Triggers the service method registered by registerService, for service and method.

Dart Tooling Daemon will forward the service method call to the client that registered 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 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:

{
  "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 streamListening to, then the calling client may handle incoming streamNotify RPC methods.

When any client calls postEvent for a given _streamId , if a client hasstreamListened 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
{
    "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 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:

{
  "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.

If the file at uri does not exist, the 141 (The file does not exist) RPC error code is returned.

If the uri is not contained in the IDE Workspace Roots, the 142 (Permission denied) RPC error code is returned.

If uri does not have a ‘file’ scheme, the 143 (File scheme expected on uri) RPC error code is returned.

Code Sample

{
    "jsonrpc": "2.0",
    "method": "FileSystem.readFileAsString",
    "params": {
        "uri": "file:///path/to/file.txt",
    },
    "id": "2"
}

Response

{
  "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.

If the uri is not contained in the IDE Workspace Roots, the 142 (Permission denied) RPC error code is returned.

If uri does not have a ‘file’ scheme, the 143 (File scheme expected on uri) RPC error code is returned.

Code Sample

{
    "jsonrpc": "2.0",
    "method": "FileSystem.writeFileAsString",
    "params": {
        "uri": "file:///path/to/file.txt",
        "contents": "Some contents to write",
    },
    "id": "2"
}

Response

{
  "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.

If the uri is not contained in the IDE Workspace Roots, the 142 (Permission denied) RPC error code is returned.

If uri does not have a ‘file’ scheme, the 143 (File scheme expected on uri) RPC error code is returned.

Code Sample

{
    "jsonrpc": "2.0",
    "method": "FileSystem.listDirectoryContents",
    "params": {
        "uri": "file:///path/to/dir/",
    },
    "id": "2"
}

Response

{
  "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 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. 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 code.

If no workspace roots are set then all FileSystem service method calls will fail with the 142 (Permission denied) RPC error 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.

If secret does not match the secret created when Dart Tooling Daemon was created then 142 (Permission denied) RPC error code is returned.

If one of the roots is missing a ‘file’ scheme, the 143 (File scheme expected on uri) RPC error code is returned.

Code Sample
{
    "jsonrpc": "2.0",
    "method": "FileSystem.setIDEWorkspaceRoots",
    "params": {
        "secret": "aBVsK8nRfdJsDEnS",
        "roots": [
          "file:///path/to/dir/a/",
          "file:///path/to/dir/b/",
        ],
    },
    "id": "2"
}
Response
{
  "jsonrpc": "2.0",
  "result": {"type": "Success"},
  "id": "2"
}

getIDEWorkspaceRoots

Used to get the IDE workspace roots that have been set using setIDEWorkspaceRoots.

Parameters

Takes no parameters.

Result

If successful, responds with IDEWorkspaceRoots.

Code Sample
{
    "jsonrpc": "2.0",
    "method": "FileSystem.getIDEWorkspaceRoots",
    "params": {},
    "id": "2"
}
Response
{
  "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 and 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.

Code Sample
{
    "jsonrpc": "2.0",
    "method": "FileSystem.getProjectRoots",
    "params": {"depth": 5},
    "id": "2"
}
Response
{
  "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 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.

{
  "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

{
  "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

{
  "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 request above. This error would be generated if we were attempting to subscribe to the GC stream multiple times from the same client.

{
  "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 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.

{"id": "2", "type": "Success"}