Skip to content

Example Usage

This page starts an RPC server against a workspace, makes a call to it by hand, and then outlines the same exchange as a client program. It assumes you already have a workspace; Getting Started creates one.

Start a server

fxv rpc serves a single connection over its own standard input and standard output, then exits. There is no daemon to install, no port to choose, and no address to connect to. A client starts the server as a child process and talks to it through its pipes.

The server operates on one workspace, and it picks that workspace when it starts. By default it uses the directory it was started in:

fxv rpc

Pass --working-dir to serve a workspace somewhere else, which is what a client that does not control its own working directory should do:

fxv rpc --working-dir /path/to/my-workspace
fxv rpc --working-dir C:\path\to\my-workspace

Either path may be any directory inside the workspace, not only its root. It is fixed for the life of the server, so no request can ask for a different one; see The workspace.

If the directory is not part of a workspace, the server exits immediately with the reason on standard error instead of serving anything. Check for that at startup, or you will wait for a response that never comes.

Make a call

The server reads one request per line from standard input, so you can drive it from a shell by piping a line of JSON into it. Run this from inside a workspace.

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"server.info"}' | fxv rpc
'{"jsonrpc":"2.0","id":1,"method":"server.info"}' | fxv rpc

The server writes one line back:

Received
{"jsonrpc":"2.0","result":{"methods":["status","history","user","ping","shutdown","server.info"],"protocol":"jsonrpc-2.0-ndjson","version":"0.6.0"},"id":1}

server.info is how a client discovers what the server it just started can do. Read the methods array rather than calling a method to find out whether it exists.

The pipe closed after that one line, so the server saw end of input and exited. That is one of the three ways a connection ends, and it is the simplest way to make a one-off call.

Send more than one request

Each line is a separate request, and the server answers each one on its own line:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"ping"}' \
  '{"jsonrpc":"2.0","id":2,"method":"status"}' | fxv rpc
@(
  '{"jsonrpc":"2.0","id":1,"method":"ping"}',
  '{"jsonrpc":"2.0","id":2,"method":"status"}'
) | fxv rpc

There will be two responses, which may come out in either order. Requests should be matched to their responses with the id field, never by order of receipt.

One response contains the complete ping result:

Received
{"jsonrpc":"2.0","result":"pong","id":1}

And the other is the same JSON that fxv status --format json produces, which is too long to show here.

To reiterate, match each response to the request that produced it by its id, never by the order the lines arrive in. See Protocol.

Write a simple client

The following is a simple python script that can be used to spawn a subprocess that starts the server and then connect to it via json RPC. Assuming a standard python environment, this content can be copied to a file client_example.py and run with python3 client_example.py:

import json
import subprocess

server = subprocess.Popen(
    ["fxv", "rpc", "--working-dir", "/path/to/my-workspace"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    text=True,
)

next_id = 0

def call(method, params=None):
    global next_id
    next_id += 1
    request = {"jsonrpc": "2.0", "id": next_id, "method": method}
    if params is not None:
        request["params"] = params

    server.stdin.write(json.dumps(request) + "\n")
    server.stdin.flush()

    return json.loads(server.stdout.readline())

print(call("ping")["result"])
print(call("history", {"num": 5})["result"])

Two details are worth mentioning explicitly when writing an integration:

  • Flush after writing each request, because a buffered write leaves the server waiting for input that your client believes it already sent.
  • Read exactly one line per response, since the framing is one JSON object per line with nothing spanning lines.

This example reads the response to each request before sending the next one, which keeps it simple. A client may also write several requests before reading any of them, as long as it matches responses by id.

End the session

Close the server's standard input when you are done. The server finishes the request it is working on, exits, and returns 0.

To confirm the shutdown rather than infer it, call shutdown first. The server acknowledges it and then ends the connection:

Sent
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
Received
{"jsonrpc":"2.0","result":{"ok":true},"id":3}

Either way, wait for the child process to exit so it is not left behind.

Where to next

  • Protocol specifies the framing, the message objects, and the connection lifecycle.
  • Methods lists every method and how each one differs from the command line.
  • Errors documents what the server sends back when a request fails.