How to resume a paused isolate in Dart using service extensions?

30 Views Asked by At

I'm experimenting with Dart's isolates and the VM service.

Minimal reproducible example

No external packages are required.

I have two programs:

  • printer which hosts a VM service and registers a service extension. The service extension pauses or resumes the current isolate when called.
  • controller which calls printer service extension

printer.dart

import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:isolate';

Capability? capability;

void main() {
  var count = 0;
  () async {
    while (true) {
      print('Count: $count');
      count++;
      await Future.delayed(Duration(seconds: 1));
    }
  }();
  const String extensionName = 'ext.printer.getCount';
  developer.registerExtension('ext.printer.control', _getControlHandler);
  final String isolateId = developer.Service.getIsolateId(Isolate.current)!;
  print('Registered service extension $extensionName in $isolateId');
}

Future<developer.ServiceExtensionResponse> _getControlHandler(
  String method,
  Map<String, String> parameters,
) async {
  print('Received command: ${parameters['command']}');
  if (parameters['command'] == 'pause') {
    print('Pausing...');
    capability = Isolate.current.pause();
    print('Paused! Capability: $capability'); // This is printed
  } else if (parameters['command'] == 'resume') {
    print('Resuming...');
    Isolate.current.resume(capability!);
    capability = null;
  }
  return developer.ServiceExtensionResponse.result(jsonEncode({}));
}

To run printer:

$ dart run --enable-vm-service --serve-observatory bin/printer.dart

controller.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;

Future<void> main(List<String> args) async {
  final String webSocketUrl = args[0];
  final String isolateId = args[1];
  final io.WebSocket socket = await io.WebSocket.connect(webSocketUrl);
  socket.listen(
    (dynamic data) {
      var encoder = JsonEncoder.withIndent("  ");
      final response = encoder.convert(jsonDecode(data));
      print('Got response from ext.printer.control:\n$response');
    },
  );

  io.stdin
    ..lineMode = false
    ..echoMode = false;

  StreamSubscription? subscription;
  void onData(List<int> codes) {
    print('received: $codes');
    if (codes.first == 27) {
      // Exit on ESC key
      print('Canceling subscription and exiting...');
      subscription?.cancel();
      return;
    }

    final char = String.fromCharCode(codes.first).toUpperCase();
    if (char == 'P') {
      print('Will pause...');
      socket.add(jsonEncode({
        'jsonrpc': '2.0',
        'id': 21,
        'method': 'ext.printer.control',
        'params': {'isolateId': isolateId, 'command': 'pause'},
      }));
    } else if (char == 'R') {
      print('Will resume...');
      socket.add(jsonEncode({
        'jsonrpc': '2.0',
        'id': 37,
        'method': 'ext.printer.control',
        'params': {'isolateId': isolateId, 'command': 'resume'},
      }));
    }
  }

  subscription = io.stdin.listen(onData);
}

To run controller, you need two strings that are output when printer is run: the Dart VM service address and the main isolate ID:

# let's assume that:
#  * printer's VM service address is "http://127.0.0.1:8181/_Tm_1iNeskE=/" (http -> ws)
#  * printer's main isolate ID is "isolates/8509839127081443"
# then you call controller like this:
$ dart run bin/controller.dart ws://127.0.0.1:8181/_Tm_1iNeskE=/ isolates/8509839127081443

Main question

I start both programs and then in controller, I press "P" on keyboard to call a service extension inside printer that stops its main isolate:

printer output:

Received command: pause
Pausing...
Paused! Capability: Capability

I would expect that when I press "R" again, the printer's main isolate will be resumed – however, that does not happen. It stays paused and I was not able to bring it back to the resumed state. How can I do that?

I thought that my mistake was that I had only one isolate, and the service extension's code is executed inside that isolate - but it is paused, so no code can execute. Is this correct?

More questions

When I open the Dart Observatory for the printer program, I see there are 3 isolates:

The Dart Observatory can be opened by opening in the browser the first link output by the printer program:

$ dart run --enable-vm-service --serve-observatory bin/printer.dart 
The Dart VM service is listening on http://127.0.0.1:8181/_Tm_1iNeskE=/

Dart Observatory screenshot showing 3 isolates

The following questions arise:

  1. The main isolate (running bin/printer.dart) shows up as "idle" no matter if it's paused or started. Why is that?

  2. There is a vm-service isolate. What is its purpose? Would it be true to say that Dart's VM service runs in a separate isolate when --enable-vm-service flag is passed?

  3. Is the vm-service isolate the first to receive my websocket data, and then it tries to pass that data to the main isolate, but it is paused?

In general I'm trying to build a correct mental model of what exactly is happening. Thank you in advance.

0

There are 0 best solutions below