From 337e07fab6b415e96d3554bc23a8da69507d7745 Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Wed, 8 Jan 2025 14:51:12 -0800 Subject: [PATCH] [VM/Service] Use the resident frontend server for hot reload when it's available TEST=pkg/vm_service/test/reload_sources_with_resident_compiler_test.dart and pkg/vm_service/test/breakpoint_resolution_after_reloading_with_resident_compiler_test.dart CoreLibraryReviewExempt: This CL does not include any core library API changes, only VM Service implementation changes within sdk/lib/vmservice/. Change-Id: Ibc99cd37439ddd8aca97fa7e18a5112cbfc3b4cb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/401646 Reviewed-by: Ben Konyi --- pkg/pkg.status | 1 + ...point_resolution_after_reloading_test.dart | 174 +--------------- ...esolution_after_reloading_test_common.dart | 175 +++++++++++++++++ ...reloading_with_resident_compiler_test.dart | 14 ++ pkg/vm_service/test/reload_sources_test.dart | 65 +----- .../test/reload_sources_test_common.dart | 64 ++++++ ...d_sources_with_resident_compiler_test.dart | 14 ++ runtime/vm/service.cc | 85 ++++++++ sdk/lib/vmservice/running_isolates.dart | 185 +++++++++++++----- sdk/lib/vmservice/vmservice.dart | 2 +- 10 files changed, 497 insertions(+), 282 deletions(-) create mode 100644 pkg/vm_service/test/breakpoint_resolution_after_reloading_test_common.dart create mode 100644 pkg/vm_service/test/breakpoint_resolution_after_reloading_with_resident_compiler_test.dart create mode 100644 pkg/vm_service/test/reload_sources_test_common.dart create mode 100644 pkg/vm_service/test/reload_sources_with_resident_compiler_test.dart diff --git a/pkg/pkg.status b/pkg/pkg.status index 27f3c056473..28075e5aa31 100644 --- a/pkg/pkg.status +++ b/pkg/pkg.status @@ -191,6 +191,7 @@ vm_service/test/regress_55559_test: SkipByDesign # Spawns a child process from s vm_service/test/regress_88104_test: SkipByDesign # Debugger is disabled in AOT mode. vm_service/test/reload_sources_rpc_triggers_isolate_reload_event_test: SkipByDesign # Hot reload is disabled in AOT mode. vm_service/test/reload_sources_test: SkipByDesign # Hot reload is disabled in AOT mode. +vm_service/test/reload_sources_with_resident_compiler_test: SkipByDesign # Hot reload is disabled in AOT mode. vm_service/test/resume_shutdown_race_test: SkipByDesign # Debugger is disabled in AOT mode. vm_service/test/rewind*: SkipByDesign # Debugger is disabled in AOT mode. vm_service/test/sdk_break_with_mixin_test: SkipByDesign # Debugger is disabled in AOT mode. diff --git a/pkg/vm_service/test/breakpoint_resolution_after_reloading_test.dart b/pkg/vm_service/test/breakpoint_resolution_after_reloading_test.dart index 2e0310e3b12..bc1c7df4c7a 100644 --- a/pkg/vm_service/test/breakpoint_resolution_after_reloading_test.dart +++ b/pkg/vm_service/test/breakpoint_resolution_after_reloading_test.dart @@ -2,182 +2,12 @@ // 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. -import 'dart:developer' show debugger; -import 'dart:io' show Directory, File; -import 'dart:isolate' as i; - -import 'package:path/path.dart' show join; -import 'package:test/test.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'common/service_test_common.dart'; +import 'breakpoint_resolution_after_reloading_test_common.dart'; import 'common/test_helper.dart'; -// AUTOGENERATED START -// -// Update these constants by running: -// -// dart pkg/vm_service/test/update_line_numbers.dart -// -const LINE_A = 78; -// AUTOGENERATED END - -const v0Contents = ''' -import 'dart:developer'; - -void f() {} - -void main() { - debugger(); - f(); - f(); -} -'''; - -const v1Contents = ''' -import 'dart:developer'; - -void f() { - (() { - (() { - print('v1'); - })(); - })(); -} - -void main() { - f(); - f(); -} -'''; - -const v2Contents = ''' -import 'dart:developer'; - -void f() { - (() { - print('v2.a'); - print('v2.b'); - })(); -} - -void main() { - f(); - f(); -} -'''; - -Future testeeMain() async { - // Spawn the child isolate. - final tempDir = Directory.systemTemp.createTempSync(); - try { - final rootLib = File(join(tempDir.path, 'main.dart')); - rootLib.writeAsStringSync(v0Contents); - - await i.Isolate.spawnUri(rootLib.uri, [], null); - debugger(); // LINE_A - tempDir.deleteSync(recursive: true); - } catch (_) { - tempDir.deleteSync(recursive: true); - rethrow; - } -} - -final tests = [ - // Ensure that the main isolate has stopped at the [debugger] statement at the - // end of [testeeMain]. - hasStoppedAtBreakpoint, - stoppedAtLine(LINE_A), - (VmService service, IsolateRef isolateRef) async { - final tempDir = Directory.systemTemp.createTempSync(); - try { - // This test is a regression test against a bug caused by comparing script - // URLs instead of script pointers in the debugger. To produce a situation - // in which the bug used to occur, this test loads - // [spawnedIsolateRootLib], modifies [spawnedIsolateRootLib], and then - // reloads [spawnedIsolateRootLib]. - final spawnedIsolateRootLib = File(join(tempDir.path, 'main.dart')); - - // Find the spawned isolate. - final vm = await service.getVM(); - final isolates = vm.isolates!; - expect(isolates.length, 2); - final spawnedIsolateRef = isolates.firstWhere( - (i) => i != isolateRef, - ); - final spawnedIsolateId = spawnedIsolateRef.id!; - - // Load [v1Contents] into the spawned isolate. - spawnedIsolateRootLib.writeAsStringSync(v1Contents); - await service.reloadSources( - spawnedIsolateId, - rootLibUri: spawnedIsolateRootLib.uri.toString(), - force: true, - ); - - Isolate spawnedIsolate = await service.getIsolate(spawnedIsolateId); - Library rootLib = await service.getObject( - spawnedIsolateId, - spawnedIsolate.rootLib!.id!, - ) as Library; - String scriptId = rootLib.scripts![0].id!; - - // Add a breakpoint at `print('v1');`. - await service.addBreakpoint(spawnedIsolateId, scriptId, 6); - - // Resuming the spawned isolate should let it run until it gets paused at - // the breakpoint at `print('v1');`. - await resumeIsolate(service, spawnedIsolateRef); - await hasStoppedAtBreakpoint(service, spawnedIsolateRef); - await stoppedAtLine(6)(service, spawnedIsolateRef); - - // Load [v2Contents] into the spawned isolate. - spawnedIsolateRootLib.writeAsStringSync(v2Contents); - await service.reloadSources( - spawnedIsolateId, - rootLibUri: spawnedIsolateRootLib.uri.toString(), - force: true, - ); - - spawnedIsolate = await service.getIsolate(spawnedIsolateId); - rootLib = await service.getObject( - spawnedIsolateId, - spawnedIsolate.rootLib!.id!, - ) as Library; - scriptId = rootLib.scripts![0].id!; - - // Add a breakpoint at `print('v2.a');`. - await service.addBreakpoint(spawnedIsolateId, scriptId, 5); - - // Resuming the spawned isolate should let it run until it gets paused at - // the breakpoint at `print('v2.a');`. - await resumeIsolate(service, spawnedIsolateRef); - await hasStoppedAtBreakpoint(service, spawnedIsolateRef); - await stoppedAtLine(5)(service, spawnedIsolateRef); - - // Add a breakpoint at `print('v2.b');`. - final breakpoint3 = - await service.addBreakpoint(spawnedIsolateId, scriptId, 6); - expect(breakpoint3.breakpointNumber, 3); - - // We previously had a bug that would have made the breakpoint resolution - // code get confused by the old closure that was defined in [v1Contents]. - // We prevent a reintroduction of that bug by ensuring that the newly set - // breakpoint has been resolved immediately. - expect(breakpoint3.resolved, true); - - await resumeIsolate(service, spawnedIsolateRef); - tempDir.deleteSync(recursive: true); - } catch (_) { - tempDir.deleteSync(recursive: true); - rethrow; - } - }, -]; - void main([args = const []]) => runIsolateTests( args, - tests, + breakpointResolutionAfterReloadingTests, 'breakpoint_resolution_after_reloading_test.dart', testeeConcurrent: testeeMain, ); diff --git a/pkg/vm_service/test/breakpoint_resolution_after_reloading_test_common.dart b/pkg/vm_service/test/breakpoint_resolution_after_reloading_test_common.dart new file mode 100644 index 00000000000..8b61b6eeb59 --- /dev/null +++ b/pkg/vm_service/test/breakpoint_resolution_after_reloading_test_common.dart @@ -0,0 +1,175 @@ +// Copyright (c) 2025, 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. + +import 'dart:developer' show debugger; +import 'dart:io' show Directory, File; +import 'dart:isolate' as i; + +import 'package:path/path.dart' show join; +import 'package:test/test.dart'; +import 'package:vm_service/vm_service.dart'; + +import 'common/service_test_common.dart'; + +// AUTOGENERATED START +// +// Update these constants by running: +// +// dart pkg/vm_service/test/update_line_numbers.dart +// +const LINE_A = 77; +// AUTOGENERATED END + +const _v0Contents = ''' +import 'dart:developer'; + +void f() {} + +void main() { + debugger(); + f(); + f(); +} +'''; + +const _v1Contents = ''' +import 'dart:developer'; + +void f() { + (() { + (() { + print('v1'); + })(); + })(); +} + +void main() { + f(); + f(); +} +'''; + +const _v2Contents = ''' +import 'dart:developer'; + +void f() { + (() { + print('v2.a'); + print('v2.b'); + })(); +} + +void main() { + f(); + f(); +} +'''; + +Future testeeMain() async { + // Spawn the child isolate. + final tempDir = Directory.systemTemp.createTempSync(); + try { + final rootLib = File(join(tempDir.path, 'main.dart')); + rootLib.writeAsStringSync(_v0Contents); + + await i.Isolate.spawnUri(rootLib.uri, [], null); + debugger(); // LINE_A + tempDir.deleteSync(recursive: true); + } catch (_) { + tempDir.deleteSync(recursive: true); + rethrow; + } +} + +final breakpointResolutionAfterReloadingTests = [ + // Ensure that the main isolate has stopped at the [debugger] statement at the + // end of [testeeMain]. + hasStoppedAtBreakpoint, + stoppedAtLine(LINE_A), + (VmService service, IsolateRef isolateRef) async { + final tempDir = Directory.systemTemp.createTempSync(); + try { + // This test is a regression test against a bug caused by comparing script + // URLs instead of script pointers in the debugger. To produce a situation + // in which the bug used to occur, this test loads + // [spawnedIsolateRootLib], modifies [spawnedIsolateRootLib], and then + // reloads [spawnedIsolateRootLib]. + final spawnedIsolateRootLib = File(join(tempDir.path, 'main.dart')); + + // Find the spawned isolate. + final vm = await service.getVM(); + final isolates = vm.isolates!; + expect(isolates.length, 2); + final spawnedIsolateRef = isolates.firstWhere( + (i) => i != isolateRef, + ); + final spawnedIsolateId = spawnedIsolateRef.id!; + + // Load [v1Contents] into the spawned isolate. + spawnedIsolateRootLib.writeAsStringSync(_v1Contents); + await service.reloadSources( + spawnedIsolateId, + rootLibUri: spawnedIsolateRootLib.uri.toString(), + force: true, + ); + + Isolate spawnedIsolate = await service.getIsolate(spawnedIsolateId); + Library rootLib = await service.getObject( + spawnedIsolateId, + spawnedIsolate.rootLib!.id!, + ) as Library; + String scriptId = rootLib.scripts![0].id!; + + // Add a breakpoint at `print('v1');`. + await service.addBreakpoint(spawnedIsolateId, scriptId, 6); + + // Resuming the spawned isolate should let it run until it gets paused at + // the breakpoint at `print('v1');`. + await resumeIsolate(service, spawnedIsolateRef); + await hasStoppedAtBreakpoint(service, spawnedIsolateRef); + await stoppedAtLine(6)(service, spawnedIsolateRef); + + // Load [v2Contents] into the spawned isolate. + spawnedIsolateRootLib.writeAsStringSync(_v2Contents); + await service.reloadSources( + spawnedIsolateId, + rootLibUri: spawnedIsolateRootLib.uri.toString(), + force: true, + ); + + spawnedIsolate = await service.getIsolate(spawnedIsolateId); + rootLib = await service.getObject( + spawnedIsolateId, + spawnedIsolate.rootLib!.id!, + ) as Library; + scriptId = rootLib.scripts![0].id!; + + // Add a breakpoint at `print('v2.a');`. + await service.addBreakpoint(spawnedIsolateId, scriptId, 5); + + // Resuming the spawned isolate should let it run until it gets paused at + // the breakpoint at `print('v2.a');`. + await resumeIsolate(service, spawnedIsolateRef); + await hasStoppedAtBreakpoint(service, spawnedIsolateRef); + await stoppedAtLine(5)(service, spawnedIsolateRef); + + // Add a breakpoint at `print('v2.b');`. + final breakpoint3 = + await service.addBreakpoint(spawnedIsolateId, scriptId, 6); + expect(breakpoint3.breakpointNumber, 3); + + // We previously had a bug that would have made the breakpoint resolution + // code get confused by the old closure that was defined in [v1Contents]. + // We prevent a reintroduction of that bug by ensuring that the newly set + // breakpoint has been resolved immediately. + expect(breakpoint3.resolved, true); + + await resumeIsolate(service, spawnedIsolateRef); + tempDir.deleteSync(recursive: true); + } catch (_) { + tempDir.deleteSync(recursive: true); + rethrow; + } + }, +]; diff --git a/pkg/vm_service/test/breakpoint_resolution_after_reloading_with_resident_compiler_test.dart b/pkg/vm_service/test/breakpoint_resolution_after_reloading_with_resident_compiler_test.dart new file mode 100644 index 00000000000..f65cc5ddbfc --- /dev/null +++ b/pkg/vm_service/test/breakpoint_resolution_after_reloading_with_resident_compiler_test.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2025, 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. + +import 'breakpoint_resolution_after_reloading_test_common.dart'; +import 'common/test_helper.dart'; + +void main([args = const []]) => runIsolateTests( + args, + breakpointResolutionAfterReloadingTests, + 'breakpoint_resolution_after_reloading_with_resident_compiler_test.dart', + testeeConcurrent: testeeMain, + shouldTesteeBeLaunchedWithDartRunResident: true, + ); diff --git a/pkg/vm_service/test/reload_sources_test.dart b/pkg/vm_service/test/reload_sources_test.dart index a97ad2d62da..5b34175f0a9 100644 --- a/pkg/vm_service/test/reload_sources_test.dart +++ b/pkg/vm_service/test/reload_sources_test.dart @@ -2,71 +2,12 @@ // 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. -import 'dart:developer'; - -import 'package:test/test.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'common/service_test_common.dart'; import 'common/test_helper.dart'; - -// AUTOGENERATED START -// -// Update these constants by running: -// -// dart pkg/vm_service/test/update_line_numbers.dart -// -const LINE_A = 23; -// AUTOGENERATED END - -void testMain() { - debugger(); // LINE_A. - print('1'); - while (true) {} -} - -Future isolateIsRunning(VmService service, IsolateRef isolateRef) async { - final isolate = await service.getIsolate(isolateRef.id!); - final pauseEvent = isolate.pauseEvent; - final isPaused = pauseEvent == null - ? false - : isolate.pauseEvent!.kind != EventKind.kResume; - final topFrame = pauseEvent?.topFrame; - expect(!isPaused && topFrame != null, true); -} - -final tests = [ - // Stopped at 'debugger' statement. - hasStoppedAtBreakpoint, - stoppedAtLine(LINE_A), - // Reload sources and request to pause post reload. The pause request will be - // ignored because we are already paused at a breakpoint. - reloadSources(pause: true), - // Ensure that we are still stopped at a breakpoint. - hasStoppedAtBreakpoint, - stoppedAtLine(LINE_A), - // Resume the isolate into the while loop. - resumeIsolate, - // Verify that it is running. - isolateIsRunning, - // Reload sources and request to pause post reload. The pause request will - // be respected because we are not already paused. - reloadSources(pause: true), - // Ensure that we are paused post reload request. - hasStoppedPostRequest, - // Resume the isolate. - resumeIsolate, - // Verify that it is running. - isolateIsRunning, - // Reload sources and do not request to pause post reload. - reloadSources(), - // Verify that it is running. - isolateIsRunning, -]; +import 'reload_sources_test_common.dart' show reloadSourcesTests, testeeMain; void main([args = const []]) => runIsolateTests( args, - tests, + reloadSourcesTests, 'reload_sources_test.dart', - testeeConcurrent: testMain, + testeeConcurrent: testeeMain, ); diff --git a/pkg/vm_service/test/reload_sources_test_common.dart b/pkg/vm_service/test/reload_sources_test_common.dart new file mode 100644 index 00000000000..51f726e0679 --- /dev/null +++ b/pkg/vm_service/test/reload_sources_test_common.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2025, 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. + +import 'dart:developer'; + +import 'package:test/test.dart'; +import 'package:vm_service/vm_service.dart'; + +import 'common/service_test_common.dart'; + +// AUTOGENERATED START +// +// Update these constants by running: +// +// dart pkg/vm_service/test/update_line_numbers.dart +// +const LINE_A = 22; +// AUTOGENERATED END + +void testeeMain() { + debugger(); // LINE_A. + print('1'); + while (true) {} +} + +Future isolateIsRunning(VmService service, IsolateRef isolateRef) async { + final isolate = await service.getIsolate(isolateRef.id!); + final pauseEvent = isolate.pauseEvent; + final isPaused = pauseEvent == null + ? false + : isolate.pauseEvent!.kind != EventKind.kResume; + final topFrame = pauseEvent?.topFrame; + expect(!isPaused && topFrame != null, true); +} + +final reloadSourcesTests = [ + // Stopped at 'debugger' statement. + hasStoppedAtBreakpoint, + stoppedAtLine(LINE_A), + // Reload sources and request to pause post reload. The pause request will be + // ignored because we are already paused at a breakpoint. + reloadSources(pause: true), + // Ensure that we are still stopped at a breakpoint. + hasStoppedAtBreakpoint, + stoppedAtLine(LINE_A), + // Resume the isolate into the while loop. + resumeIsolate, + // Verify that it is running. + isolateIsRunning, + // Reload sources and request to pause post reload. The pause request will + // be respected because we are not already paused. + reloadSources(pause: true), + // Ensure that we are paused post reload request. + hasStoppedPostRequest, + // Resume the isolate. + resumeIsolate, + // Verify that it is running. + isolateIsRunning, + // Reload sources and do not request to pause post reload. + reloadSources(), + // Verify that it is running. + isolateIsRunning, +]; diff --git a/pkg/vm_service/test/reload_sources_with_resident_compiler_test.dart b/pkg/vm_service/test/reload_sources_with_resident_compiler_test.dart new file mode 100644 index 00000000000..da9ba176b86 --- /dev/null +++ b/pkg/vm_service/test/reload_sources_with_resident_compiler_test.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2025, 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. + +import 'common/test_helper.dart'; +import 'reload_sources_test_common.dart' show reloadSourcesTests, testeeMain; + +void main([args = const []]) => runIsolateTests( + args, + reloadSourcesTests, + 'reload_sources_with_resident_compiler_test.dart', + testeeConcurrent: testeeMain, + shouldTesteeBeLaunchedWithDartRunResident: true, + ); diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc index 27f2a3c56a3..d6dfda5988a 100644 --- a/runtime/vm/service.cc +++ b/runtime/vm/service.cc @@ -27,6 +27,7 @@ #include "vm/heap/safepoint.h" #include "vm/isolate.h" #include "vm/json_stream.h" +#include "vm/kernel.h" #include "vm/kernel_isolate.h" #include "vm/lockers.h" #include "vm/message.h" @@ -3953,6 +3954,88 @@ static void GetSourceReport(Thread* thread, JSONStream* js) { #endif // !DART_PRECOMPILED_RUNTIME } +static const MethodParameter* const reload_kernel_params[] = { + RUNNABLE_ISOLATE_PARAMETER, + new BoolParameter("force", false), + new BoolParameter("pause", false), + new StringParameter("kernelFilePath", false), + nullptr, +}; + +/// Replaces the program running in the isolate group containing the isolate +/// specified by |js->LookupParam("isolateId")| with the program defined by +/// |js->LookupParam("kernelFilePath")|. +static void ReloadKernel(Thread* thread, JSONStream* js) { +#if defined(DART_PRECOMPILED_RUNTIME) + js->PrintError(kFeatureDisabled, "Compiler is disabled in AOT mode."); +#else + if (!js->HasParam("kernelFilePath")) { + PrintMissingParamError(js, "kernelFilePath"); + return; + } + + IsolateGroup* isolate_group = thread->isolate_group(); + if (isolate_group->library_tag_handler() == nullptr) { + js->PrintError(kFeatureDisabled, + "A library tag handler must be installed."); + return; + } + Isolate* isolate = thread->isolate(); + if ((isolate->sticky_error() != Error::null()) || + (Thread::Current()->sticky_error() != Error::null())) { + js->PrintError(kIsolateReloadBarred, + "The specified isolate cannot reload from a kernel file " + "anymore because it encountered an unhandled exception. " + "Restart the isolate."); + return; + } + if (isolate_group->IsReloading()) { + js->PrintError(kIsolateIsReloading, + "The specified isolate group is already in the process of " + "being reloaded."); + return; + } + if (!isolate_group->CanReload()) { + js->PrintError(kFeatureDisabled, + "The specified isolate group cannot reload from a kernel " + "file right now."); + return; + } + + Dart_FileOpenCallback file_open = Dart::file_open_callback(); + Dart_FileReadCallback file_read = Dart::file_read_callback(); + Dart_FileCloseCallback file_close = Dart::file_close_callback(); + if ((file_open == nullptr) || (file_read == nullptr) || + (file_close == nullptr)) { + js->PrintError(kInternalError, + "An internal error occurred when trying to read the " + "specified kernel file."); + return; + } + + void* file = (*file_open)(js->LookupParam("kernelFilePath"), /*write=*/false); + if (file == nullptr) { + js->PrintError(kIsolateReloadBarred, + "The specified kernel file could not be read. Please ensure " + "that the provided 'kernelFilePath' argument is correct."); + return; + } + + uint8_t* kernel_buffer = nullptr; + intptr_t kernel_buffer_size = -1; + (*file_read)(&kernel_buffer, &kernel_buffer_size, file); + + const bool force_reload = + BoolParameter::Parse(js->LookupParam("force"), false); + isolate_group->ReloadKernel(js, force_reload, kernel_buffer, + kernel_buffer_size); + + free(kernel_buffer); + + Service::CheckForPause(isolate, js); +#endif // defined(DART_PRECOMPILED_RUNTIME) +} + static const MethodParameter* const reload_sources_params[] = { RUNNABLE_ISOLATE_PARAMETER, new BoolParameter("force", false), @@ -6207,6 +6290,8 @@ static const ServiceMethodDescriptor service_methods_[] = { pause_params }, { "removeBreakpoint", RemoveBreakpoint, remove_breakpoint_params }, + { "_reloadKernel", ReloadKernel, + reload_kernel_params }, { "reloadSources", ReloadSources, reload_sources_params }, { "_reloadSources", ReloadSources, diff --git a/sdk/lib/vmservice/running_isolates.dart b/sdk/lib/vmservice/running_isolates.dart index 2cfeed29f7a..a4ecf4485b3 100644 --- a/sdk/lib/vmservice/running_isolates.dart +++ b/sdk/lib/vmservice/running_isolates.dart @@ -10,7 +10,27 @@ final class _CompileExpressionErrorDetails { _CompileExpressionErrorDetails(this.details); } +/// The message in an error response from the resident frontend compiler can +/// either be in the 'errorMessage' property or the 'compilerOutputLines' +/// property of the response. +String _extractErrorMessageFromResidentFrontendCompilerResponse( + Map response, +) { + const errorMessageString = 'errorMessage'; + + if (response[errorMessageString] != null) { + return response[errorMessageString]; + } else { + return (response['compilerOutputLines'] as List) + .cast() + .join('\n'); + } +} + class RunningIsolates implements MessageRouter { + static const _isolateIdString = 'isolateId'; + static const _successString = 'success'; + final isolates = {}; int? _rootPortId; @@ -31,6 +51,77 @@ class RunningIsolates implements MessageRouter { (isolates.remove(portId))?.onIsolateExit(); } + Future _handleReloadSourcesRequest( + VMService service, + Message message, + RunningIsolate isolate, + ) async { + if (VMServiceEmbedderHooks.getResidentCompilerInfoFile!() == null) { + // If there isn't a resident frontend compiler available, we let the VM + // take care of the request. + return isolate.routeRequest(service, message); + } else { + const rootLibUriString = 'rootLibUri'; + + final String rootLibUri; + if (message.params[rootLibUriString] == null) { + // If a 'rootLibUri' property was not included in the request, we have + // to ask the VM for [isolate]'s root library URI. + final getIsolateRequest = Message.forMethod('getIsolate'); + getIsolateRequest.params[_isolateIdString] = + message.params[isolate.serviceId]; + + final getIsolateResponse = await isolate.routeRequest( + service, + getIsolateRequest, + ); + final isolateJson = + (getIsolateResponse.decodeJson() as Map)['result'] + as Map; + final rootLibJson = isolateJson['rootLib'] as Map; + rootLibUri = rootLibJson['uri']; + } else { + rootLibUri = message.params[rootLibUriString]; + } + + final tempDirectory = Directory.systemTemp.createTempSync(); + final outputDill = File( + '${tempDirectory.path}${Platform.pathSeparator}for_hot_reload.dill', + ); + final responseFromResidentCompiler = + await _sendRequestToResidentFrontendCompilerAndRecieveResponse( + jsonEncode({ + 'command': 'compile', + 'executable': Uri.parse(rootLibUri).toFilePath(), + 'output-dill': outputDill.path, + }), + VMServiceEmbedderHooks.getResidentCompilerInfoFile!()!, + ); + + if (responseFromResidentCompiler[_successString] == false) { + return Response.from( + encodeRpcError( + message, + kInternalError, + details: _extractErrorMessageFromResidentFrontendCompilerResponse( + responseFromResidentCompiler, + ), + ), + ); + } + + final reloadKernelRequest = Message.forMethod('_reloadKernel'); + reloadKernelRequest.params[_isolateIdString] = + message.params[isolate.serviceId]; + reloadKernelRequest.params['kernelFilePath'] = + outputDill.uri.toFilePath(); + final response = isolate.routeRequest(service, message); + + tempDirectory.deleteSync(recursive: true); + return response; + } + } + @override Future routeRequest(VMService service, Message message) { String isolateParam = message.params['isolateId']! as String; @@ -71,6 +162,8 @@ class RunningIsolates implements MessageRouter { if (message.method == 'evaluateInFrame' || message.method == 'evaluate') { return _Evaluator(message, isolate, service).run(); + } else if (message.method == 'reloadSources') { + return _handleReloadSourcesRequest(service, message, isolate); } else { return isolate.routeRequest(service, message); } @@ -128,11 +221,46 @@ final class _ResidentCompilerInfo { }); } +// NOTE: The following function is a duplicate of one in +// 'package:frontend_server/resident_frontend_server_utils.dart'. We are +// forced to duplicate it because `dart:_vmservice` is not allowed to import +// `package:frontend_server`. + +/// Sends a compilation [request] to the resident frontend compiler associated +/// with [serverInfoFile], and returns the compiler's JSON response. +/// +/// Throws a [FileSystemException] if [serverInfoFile] cannot be accessed. +Future> +_sendRequestToResidentFrontendCompilerAndRecieveResponse( + String request, + File serverInfoFile, +) async { + Socket? client; + Map jsonResponse; + final residentCompilerInfo = _ResidentCompilerInfo.fromFile(serverInfoFile); + + try { + client = await Socket.connect( + residentCompilerInfo.address, + residentCompilerInfo.port, + ); + client.write(request); + final data = String.fromCharCodes(await client.first); + jsonResponse = jsonDecode(data); + } catch (e) { + jsonResponse = { + 'success': false, + 'errorMessage': e.toString(), + }; + } + client?.destroy(); + return jsonResponse; +} + /// Class that knows how to orchestrate expression evaluation in dart2 world. class _Evaluator { - static const _successString = 'success'; + static const _kernelBytesString = 'kernelBytes'; static const _compileExpressionString = 'compileExpression'; - static const _isolateIdString = 'isolateId'; static const _expressionString = 'expression'; static const _definitionsString = 'definitions'; static const _definitionTypesString = 'definitionTypes'; @@ -212,7 +340,7 @@ class _Evaluator { Map response, ) { if (response['result'] != null) { - return (response['result'] as Map)['kernelBytes'] + return (response['result'] as Map)[_kernelBytesString] as String; } final error = response['error'] as Map; @@ -220,42 +348,6 @@ class _Evaluator { throw _CompileExpressionErrorDetails(data['details']); } - // NOTE: The following function is a duplicate of one in - // 'package:frontend_server/resident_frontend_server_utils.dart'. We are - // forced to duplicate it because `dart:_vmservice` is not allowed to import - // `package:frontend_server`. - - /// Sends a compilation [request] to the resident frontend compiler associated - /// with [serverInfoFile], and returns the compiler's JSON response. - /// - /// Throws a [FileSystemException] if [serverInfoFile] cannot be accessed. - static Future> - _sendRequestToResidentFrontendCompilerAndRecieveResponse( - String request, - File serverInfoFile, - ) async { - Socket? client; - Map jsonResponse; - final residentCompilerInfo = _ResidentCompilerInfo.fromFile(serverInfoFile); - - try { - client = await Socket.connect( - residentCompilerInfo.address, - residentCompilerInfo.port, - ); - client.write(request); - final data = String.fromCharCodes(await client.first); - jsonResponse = jsonDecode(data); - } catch (e) { - jsonResponse = { - _successString: false, - 'errorMessage': e.toString(), - }; - } - client?.destroy(); - return jsonResponse; - } - /// If compilation fails, this method will throw a /// [_CompileExpressionErrorDetails] object that will be used to populate the /// 'details' field of the response to the evaluation RPC that requested this @@ -268,7 +360,8 @@ class _Evaluator { ); final compileParams = { - _isolateIdString: _message.params[_isolateIdString]!, + RunningIsolates._isolateIdString: + _message.params[RunningIsolates._isolateIdString]!, _expressionString: _message.params[_expressionString]!, _definitionsString: buildScopeResponseResult['param_names']!, _definitionTypesString: buildScopeResponseResult['param_types']!, @@ -346,14 +439,12 @@ class _Evaluator { VMServiceEmbedderHooks.getResidentCompilerInfoFile!()!, ); - if (response[_successString] == true) { - return response['kernelBytes']; - } else if (response['errorMessage'] != null) { - throw _CompileExpressionErrorDetails(response['errorMessage']); + if (response[RunningIsolates._successString] == true) { + return response[_kernelBytesString]; } else { - final compilerOutputLines = - (response['compilerOutputLines'] as List).cast(); - throw _CompileExpressionErrorDetails(compilerOutputLines.join('\n')); + throw _CompileExpressionErrorDetails( + _extractErrorMessageFromResidentFrontendCompilerResponse(response), + ); } } else { // fallback to compile using kernel service diff --git a/sdk/lib/vmservice/vmservice.dart b/sdk/lib/vmservice/vmservice.dart index d12644f0f55..934f06c4324 100644 --- a/sdk/lib/vmservice/vmservice.dart +++ b/sdk/lib/vmservice/vmservice.dart @@ -7,7 +7,7 @@ library dart._vmservice; import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; -import 'dart:io' show File, InternetAddress, Socket; +import 'dart:io' show Directory, File, InternetAddress, Platform, Socket; import 'dart:math'; import 'dart:typed_data';