# Remote Capture and Replay

This example is a bit different since it’s not ready-to-run. It provides a template for how you can capture and replay on a remote machine, instead of the local machine. It also shows how to use device protocols to automatically manage devices.

First we can enumerate which device protocols are currently supported.

    protocols = rd.GetSupportedDeviceProtocols()

Each string in the list corresponds to a protocol that can be used for managing devices. If we’re using one we can call `GetDeviceProtocolController()` passing the protocol name and retrieve the controller.

The controller provides a few methods for managing devices. First we can call `GetDevices()` to return a list of device IDs. The format of these device IDs is protocol-dependent but will be equivalent to a normal hostname. Devices may have human-readable names obtainable via `GetFriendlyName()`.

    protocol = rd.GetDeviceProtocolController(protocol_to_use)

    devices = protocol.GetDevices()

    if len(devices) == 0:
        raise RuntimeError(f"no {protocol_to_use} devices connected")

    # Choose the first device
    dev = devices[0]
    name = protocol.GetFriendlyName(dev)

    print(f"Running test on {dev} - named {name}")

    URL = protocol.GetProtocolName() + "://" + dev

The URL will be used the same as we would use a hostname, when connecting for target control or remote servers.

Note that protocols may have additional restrictions - be sure to check `IsSupported()` to check if the device is expected to function at all, and `SupportsMultiplePrograms()` to see if it supports launching multiple programs. If multiple programs are not supported, you should ensure all running capturable programs are closed before launching a new one.

To begin with we create a remote server connection using `CreateRemoteServerConnection()`. The URL is as constructed above for protocol-based connections, or a simple hostname/IP if we’re connecting directly to remote machine.

If the connection fails, normally we must fail but if we have a device protocol available we can attempt to launch the remote server automatically using `StartRemoteServer()`.

    if status == rd.ReplayStatus.NetworkIOFailed and protocol is not None:
      # If there's just no I/O, most likely the server is not running. If we have
      # a protocol, we can try to start the remote server
      print("Couldn't connect to remote server, trying to start it")

      status = protocol.StartRemoteServer(URL)

      if status != rd.ReplayStatus.Succeeded:
        raise RuntimeError(f"Couldn't launch remote server, got error {str(status)}")

      # Try to connect again!
      status,remote = rd.CreateRemoteServerConnection(URL)

Note

The remote server connection has a default timeout of 5 seconds. If the connection is unused for 5 seconds, the other side will disconnect and subsequent use of the interface will fail.

Once we have a remote server connection, we can browse the remote filesystem for the executable we want to launch using `GetHomeFolder()` and `ListFolder()`.

Then once we’ve selected the executable, we can launch the remote program for capturing with `ExecuteAndInject()`. This function is almost identical to the local `ExecuteAndInject()` except that it is not possible to wait for the program to exit.

In our sample, we now place the remote server connection on a background thread that will ping it each second to keep the connection alive while we use a target control connection to trigger a capture in the application.

    def ping_remote(remote, kill):
      success = True
      while success and not kill.is_set():
        success = remote.Ping()
        time.sleep(1)

    kill = threading.Event()
    ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
    ping_thread.start()

To connect to and control an application we use `CreateTargetControl()` with the URL as before and the ident returned from `ExecuteAndInject()`.

    target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)

    # Here we wait for whichever condition you want
    target.TriggerCapture(1)

There are a couple of ways to trigger a capture, both `TriggerCapture()` and `QueueCapture()` depending on whether you want a time-based or frame-based trigger. The application itself can also use the in-application API to trigger a capture.

The target control connection can be intermittently polled for messages using `ReceiveMessage()`, which keeps the connection alive and will return any new information such as the data for a new capture that has been created. A message of type `NewCapture` indicates a new capture has been created, and `newCapture` contains the information including the path.

    msg = target.ReceiveMessage(None)

    # Once msg.type == rd.TargetControlMessageType.NewCapture has been retrieved

    cap_path = msg.newCapture.path
    cap_id = msg.newCapture.captureId

Once the capture has been found we are finished with the target control connection so we can shut it down and stop the background thread that was keeping the remote server connection alive. Using the remote server connection we can copy the capture back to the local machine with `CopyCaptureFromRemote()`. Similarly if we wanted to load a previously made capture that wasn’t on the remote machine `CopyCaptureToRemote()` would be useful to copy it ready to be opened.

Finally to open the capture we use, and that returns a `ReplayController` which can be used as normal and will tunnel over the remote server connection. It can be useful to intermittently ping the remote server connection to check that it’s still valid, and remote server and controller calls can be interleaved as long as they don’t overlap on multiple threads.

## Example Source

`Download the example script`.

    import renderdoc as rd
    import threading
    import time

    # This sample is intended as an example of how to do remote capture and replay
    # as well as using device protocols to automatically enumerate remote targets.
    #
    # It is not complete since it requires filling in with custom logic to select
    # the executable and trigger the capture at the desired time
    raise RuntimeError("This sample should not be run directly, read the source")

    rd.InitialiseReplay(rd.GlobalEnvironment(), [])

    protocols = rd.GetSupportedDeviceProtocols()

    print(f"Supported device protocols: {protocols}")

    # Protocols are optional - they allow automatic detection and management of
    # devices.
    if protocol_to_use is not None:
        # the protocol must be supported
        if protocol_to_use not in protocols:
            raise RuntimeError(f"{protocol_to_use} protocol not supported")

        protocol = rd.GetDeviceProtocolController(protocol_to_use)

        devices = protocol.GetDevices()

        if len(devices) == 0:
            raise RuntimeError(f"no {protocol_to_use} devices connected")

        # Choose the first device
        dev = devices[0]
        name = protocol.GetFriendlyName(dev)

        print(f"Running test on {dev} - named {name}")

        URL = protocol.GetProtocolName() + "://" + dev

        # Protocols can enumerate devices which are not supported. Capture/replay
        # is not guaranteed to work on these devices
        if not protocol.IsSupported(URL):
            raise RuntimeError(f"{dev} doesn't support capture/replay - too old?")

        # Protocol devices may be single-use and not support multiple captured programs
        # If so, trying to execute a program for capture is an error
        if not protocol.SupportsMultiplePrograms(URL):
            # check to see if anything is running. Just use the URL
            ident = rd.EnumerateRemoteTargets(URL, 0)

            if ident != 0:
                raise RuntimeError(f"{name} already has a program running on {ident}")
    else:
        # If you're not using a protocol then the URL can simply be a hostname.
        # The remote server must be running already - how that is done is up
        # to you. Everything else will work the same over a normal TCP connection
        protocol = None
        URL = hostname

    # Let's try to connect
    status,remote = rd.CreateRemoteServerConnection(URL)

    if status == rd.ReplayStatus.NetworkIOFailed and protocol is not None:
        # If there's just no I/O, most likely the server is not running. If we have
        # a protocol, we can try to start the remote server
        print("Couldn't connect to remote server, trying to start it")

        status = protocol.StartRemoteServer(URL)

        if status != rd.ReplayStatus.Succeeded:
            raise RuntimeError(f"Couldn't launch remote server, got error {str(status)}")

        # Try to connect again!
        status,remote = rd.CreateRemoteServerConnection(URL)

    if status != rd.ReplayStatus.Succeeded:
        raise RuntimeError(f"Couldn't connect to remote server, got error {str(status)}")

    # We now have a remote connection. This works regardless of whether it's a device
    # with a protocol or not. In fact we are done with the protocol at this point
    protocol = None

    print("Got connection to remote server")

    # GetHomeFolder() gives you a good default path to start with.
    # ListFolder() lists the contents of a folder and can recursively
    # browse the remote filesystem.
    home = remote.GetHomeFolder()
    paths = remote.ListFolder(home)

    print(f"Executables in home folder '{home}':")

    for p in paths:
        print("  - " + p.filename)

    # Select your executable, perhaps hardcoded or browsing using the above
    # functions
    exe,workingDir,cmdLine,env,opts = select_executable()

    print(f"Running {exe}")

    result = remote.ExecuteAndInject(exe, workingDir, cmdLine, env, opts)

    if result.status != rd.ReplayStatus.Succeeded:
        remote.ShutdownServerAndConnection()
        raise RuntimeError(f"Couldn't launch {exe}, got error {str(result.status)}")

    # Spin up a thread to keep the remote server connection alive while we make a capture,
    # as it will time out after 5 seconds of inactivity
    def ping_remote(remote, kill):
        success = True
        while success and not kill.is_set():
            success = remote.Ping()
            time.sleep(1)

    kill = threading.Event()
    ping_thread = threading.Thread(target=ping_remote, args=(remote,kill))
    ping_thread.start()

    # Create target control connection
    target = rd.CreateTargetControl(URL, result.ident, 'remote_capture.py', True)

    if target is None:
        kill.set()
        ping_thread.join()
        remote.ShutdownServerAndConnection()
        raise RuntimeError(f"Couldn't connect to target control for {exe}")

    print("Connected - waiting for desired capture")

    # Wait for the capture condition we want
    capture_condition()

    print("Triggering capture")

    target.TriggerCapture(1)

    # Pump messages, keep waiting until we get a capture message. Time out after 30 seconds
    msg = None
    start = time.clock()
    while msg is None or msg.type != rd.TargetControlMessageType.NewCapture:
        msg = target.ReceiveMessage(None)

        if time.clock() - start > 30:
            break

    # Close the target connection, we're done either way
    target.Shutdown()
    target = None

    # Stop the background ping thread
    kill.set()
    ping_thread.join()

    # If we didn't get a capture, error now
    if msg.type != rd.TargetControlMessageType.NewCapture:
        remote.ShutdownServerAndConnection()
        raise RuntimeError("Didn't get new capture notification after triggering capture")

    cap_path = msg.newCapture.path
    cap_id = msg.newCapture.captureId

    print(f"Got new capture at {cap_path} which is frame {msg.newCapture.frameNumber} with {msg.newCapture.api}")

    # We could save the capture locally
    # remote.CopyCaptureFromRemote(cap_path, local_path, None)

    # Open a replay. It's recommended to set no proxy preference, but you could
    # call remote.LocalProxies and choose an index.
    #
    # The path must be remote - if the capture isn't freshly created then you need
    # to copy it with remote.CopyCaptureToRemote()
    status,controller = remote.OpenCapture(rd.RemoteServer.NoPreference, cap_path, rd.ReplayOptions(), None)

    if status != rd.ReplayStatus.Succeeded:
        remote.ShutdownServerAndConnection()
        raise RuntimeError(f"Couldn't open {cap_path}, got error {str(result.status)}")

    # We can now use replay as normal.
    #
    # The replay is tunnelled over the remote connection, so you don't have to keep
    # pinging the remote connection while using the controller. Use of the remote
    # connection and controller can be interleaved though you should only access
    # them from one thread at once. If they are both unused for 5 seconds though,
    # the timeout will happen, so if the controller is idle it's advisable to ping
    # the remote connection

    sampleCode(controller)

    print("Shutting down")

    controller.Shutdown()

    # We can still use remote here - e.g. capture again, replay something else,
    # save the capture, etc

    remote.ShutdownServerAndConnection()

    rd.ShutdownReplay()