| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// Logic to wrap a program and run it repeatedly triggering a hot-reload |
| /// between runs. This is used for trying out hot reload behavior with little |
| /// setup involved. |
| /// |
| /// To use this logic, create a program that you wish to test with hot-reload, |
| /// and call `run` wrapping a top-level method tearoff, which contains the logic |
| /// of the program. For example: |
| /// |
| /// ```dart |
| /// import 'hot_reload_helper.dart' as helper; |
| /// |
| /// example() async { |
| /// print('hello world'); |
| /// } |
| /// |
| /// main() => helper.run(example); |
| /// ``` |
| /// |
| /// Then launch this program with a special VM flag to enable the vm service, |
| /// which adds the necessary mechanism to support hot reloads: |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart --disable-dart-dev --enable-vm-service example.dart |
| /// ``` |
| /// |
| /// The program will run `example` once and wait for you to hit `<enter>` before |
| /// performing a hot reload and rerunning the `example` method again. |
| library; |
| |
| import 'dart:developer'; |
| import 'dart:io' as io; |
| import 'package:vm_service/vm_service.dart' show VmService, ReloadReport; |
| import 'package:vm_service/vm_service_io.dart' as vm_service_io; |
| |
| /// Runs [program] repeatedly after each request to do a hot reload. If the |
| /// reload fails, it prints to stdout the reason for the failure. |
| Future<void> run(Function() program) async { |
| final helper = await HotReloadHelper.create(); |
| const yellow = '\x1b[33m'; |
| const green = '\x1b[32m'; |
| var iteration = 0; |
| |
| while (true) { |
| iteration++; |
| try { |
| await program(); |
| } catch (e, s) { |
| print('$e\n$s'); |
| } |
| _colorLine('(iteration $iteration done, press <enter> to reload)', green); |
| |
| var success = false; |
| while (!success) { |
| var line = io.stdin.readLineSync(); |
| if (line == null) break; |
| final result = await helper.reload(); |
| if (result.success ?? false) { |
| success = true; |
| } else { |
| _colorLine('reload failed:', yellow); |
| print(result.reasonForCancelling); |
| _colorLine('(press <enter> to try again)', yellow); |
| } |
| } |
| } |
| } |
| |
| /// Helper to mediate with the vm service protocol. |
| /// |
| /// Contains logic to initiate a connection with the vm service protocol on the |
| /// Dart VM running the current program and for requesting a hot-reload request |
| /// on the current isolate. |
| class HotReloadHelper { |
| final String _id; |
| final VmService _vmService; |
| |
| HotReloadHelper._(this._vmService, this._id); |
| |
| /// Create a helper that is bound to the current VM and isolate. |
| static Future<HotReloadHelper> create() async { |
| final info = await Service.controlWebServer( |
| enable: true, |
| silenceOutput: true, |
| ); |
| final observatoryUri = info.serverUri; |
| if (observatoryUri == null) { |
| print( |
| 'Error: no VM service found. ' |
| 'Please invoke dart with `--enable-vm-service`.', |
| ); |
| io.exit(1); |
| } |
| final wsUri = 'ws://${observatoryUri.authority}${observatoryUri.path}ws'; |
| final vmService = await vm_service_io.vmServiceConnectUri(wsUri); |
| final vm = await vmService.getVM(); |
| final id = vm.isolates! |
| .firstWhere((isolate) => !isolate.isSystemIsolate!) |
| .id!; |
| return HotReloadHelper._(vmService, id); |
| } |
| |
| /// Trigger a hot-reload on the current isolate. |
| Future<ReloadReport> reload() => _vmService.reloadSources(_id); |
| } |
| |
| /// Extension to expose the reason for a failed reload. |
| /// |
| /// This is currently in the json response from the vm-service, but not exposed |
| /// as an API in [ReloadReport]. |
| extension on ReloadReport { |
| String? get reasonForCancelling { |
| final notices = json?['notices'] as List?; |
| if (notices != null) { |
| for (final notice in notices) { |
| if (notice['type'] == 'ReasonForCancelling') { |
| return notice['message'] as String?; |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| void _colorLine(String message, String color) { |
| const none = '\x1b[0m'; |
| print('$color${'--' * 20} $message$none'); |
| } |