-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
isolate_manager.dart
1596 lines (1424 loc) · 60.3 KB
/
isolate_manager.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2021, 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:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:dap/dap.dart';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../rpc_error_codes.dart';
import 'adapters/dart.dart';
import 'adapters/mixins.dart';
import 'utils.dart';
import 'variables.dart';
/// A composite ID for breakpoints made up of an isolate ID and breakpoint ID.
///
/// Breakpoint IDs are not unique across all isolates so any place we need to
/// know about a breakpoint specifically, we must use this.
typedef _UniqueVmBreakpointId = ({String isolateId, String breakpointId});
/// Manages state of Isolates (called Threads by the DAP protocol).
///
/// Handles incoming Isolate and Debug events to track the lifetime of isolates
/// and updating breakpoints for each isolate as necessary.
class IsolateManager {
// TODO(dantup): This class has a lot of overlap with the same-named class
// in DDS. Review what can be shared.
final DartDebugAdapter _adapter;
final Map<String, Completer<void>> _isolateRegistrations = {};
final Map<String, ThreadInfo> _threadsByIsolateId = {};
final Map<int, ThreadInfo> _threadsByThreadId = {};
int _nextThreadNumber = 1;
/// Whether debugging is enabled for this session.
///
/// This must be set before any isolates are spawned and controls whether
/// breakpoints or exception pause modes are sent to the VM.
///
/// If false, requests to send breakpoints or exception pause mode will be
/// dropped. Other functionality (handling pause events, resuming, etc.) will
/// all still function.
///
/// This is used to support debug sessions that have VM Service connections
/// but were run with noDebug: true (for example we may need a VM Service
/// connection for a noDebug flutter app in order to support hot reload).
bool debug = false;
/// Whether SDK libraries should be marked as debuggable.
///
/// Calling [sendLibraryDebuggables] is required after changing this value to
/// apply changes. This allows applying both [debugSdkLibraries] and
/// [debugExternalPackageLibraries] in one step.
bool debugSdkLibraries = true;
/// Whether external package libraries should be marked as debuggable.
///
/// Calling [sendLibraryDebuggables] is required after changing this value to
/// apply changes. This allows applying both [debugSdkLibraries] and
/// [debugExternalPackageLibraries] in one step.
bool debugExternalPackageLibraries = true;
/// Tracks breakpoints last provided by the client so they can be sent to new
/// isolates that appear after initial breakpoints were sent.
final Map<String, List<ClientBreakpoint>> _clientBreakpointsByUri = {};
/// Tracks client breakpoints by the ID assigned by the VM so we can look up
/// conditions/logpoints when hitting breakpoints.
///
/// Because the VM might return the same breakpoint for multiple
/// `addBreakpointWithScriptUri` calls (if they immediately resolve to the
/// same location) there may be multiple client breakpoints for a given VM
/// breakpoint ID.
///
/// When an item is added to this map, any pending events in
/// [_breakpointResolvedEventsByVmId] MUST be processed immediately.
final Map<_UniqueVmBreakpointId, List<ClientBreakpoint>>
_clientBreakpointsByVmId = {};
/// Tracks `BreakpointAdded` or `BreakpointResolved` events for VM
/// breakpoints.
///
/// These are kept for all breakpoints until they are removed by the VM
/// because it's always possible that the VM will reuse a breakpoint ID (eg.
/// if we add a new breakpoint that resolves to the same location as another
/// breakpoint).
///
/// When new breakpoints are added by the client, we must check this map to
/// see it's al already-resolved breakpoint so that we can send resolution
/// info to the client.
final Map<_UniqueVmBreakpointId, vm.Event> _breakpointResolvedEventsByVmId =
{};
/// Tracks breakpoints created in the VM so they can be removed when the
/// editor sends new breakpoints (currently the editor just sends a new list
/// and not requests to add/remove).
///
/// Breakpoints are indexed by their ID so that duplicates are not stored even
/// if multiple client breakpoints resolve to a single VM breakpoint.
///
/// IsolateId -> Uri -> breakpointId -> VM Breakpoint.
final Map<String, Map<String, Map<String, vm.Breakpoint>>>
_vmBreakpointsByIsolateIdAndUri = {};
/// The exception pause mode last provided by the client.
///
/// This will be sent to isolates as they are created, and to all existing
/// isolates at start or when changed.
String _exceptionPauseMode = 'None';
/// An incrementing number used as the reference for [_storedData].
var _nextStoredDataId = 1;
/// A store of data indexed by a number that is used for round tripping
/// references to the client (which only accepts ints).
///
/// For example, when we send a stack frame back to the client we provide only
/// a "sourceReference" integer and the client may later ask us for the source
/// using that number (via sourceRequest).
///
/// Stored data is thread-scoped but the client will not provide the thread
/// when asking for data so it's all stored together here.
final _storedData = <int, StoredData>{};
/// A pattern that matches an opening brace `{` that was not preceded by a
/// dollar.
///
/// Any leading character matched in place of the dollar is in the first capture.
final _braceNotPrefixedByDollarOrBackslashPattern = RegExp(r'(^|[^\\\$]){');
IsolateManager(this._adapter);
/// A list of all current active isolates.
///
/// When isolates exit, they will no longer be returned in this list, although
/// due to the async nature, it's not guaranteed that threads in this list have
/// not exited between accessing this list and trying to use the results.
List<ThreadInfo> get threads => _threadsByIsolateId.values.toList();
/// Re-applies debug options to all isolates/libraries.
///
/// This is required if options like debugSdkLibraries are modified, but is a
/// separate step to batch together changes to multiple options.
Future<void> applyDebugOptions() async {
await Future.wait(_threadsByThreadId.values.map(
// debuggable libraries is the only thing currently affected by these
// changable options.
(thread) => _sendLibraryDebuggables(thread),
));
}
Future<T> getObject<T extends vm.Response>(
vm.IsolateRef isolate,
vm.ObjRef object, {
int? offset,
int? count,
}) async {
final res = await _adapter.vmService?.getObject(
isolate.id!,
object.id!,
offset: offset,
count: count,
);
return res as T;
}
Future<vm.ScriptList> getScripts(vm.IsolateRef isolate) async {
return (await _adapter.vmService?.getScripts(isolate.id!)) as vm.ScriptList;
}
/// Retrieves some basic data indexed by an integer for use in "reference"
/// fields that are round-tripped to the client.
StoredData? getStoredData(int id) {
return _storedData[id];
}
ThreadInfo? getThread(int threadId) => _threadsByThreadId[threadId];
/// Handles Isolate and Debug events.
Future<void> handleEvent(vm.Event event) async {
final isolateId = event.isolate?.id!;
final eventKind = event.kind;
if (eventKind == vm.EventKind.kIsolateStart ||
eventKind == vm.EventKind.kIsolateRunnable) {
await registerIsolate(event.isolate!, eventKind!);
}
// Additionally, ensure the thread registration has completed before trying
// to process any other events. This is to cover the case where we are
// processing the above registerIsolate call in the handler for one isolate
// event but another one arrives and gets us here before the registration
// above (in the other event handler) has finished.
await _isolateRegistrations[isolateId]?.future;
if (eventKind == vm.EventKind.kIsolateExit) {
_handleExit(event);
} else if (eventKind?.startsWith('Pause') ?? false) {
await _handlePause(event);
} else if (eventKind == vm.EventKind.kResume) {
_handleResumed(event);
} else if (eventKind == vm.EventKind.kInspect) {
_handleInspect(event);
} else if (eventKind == vm.EventKind.kBreakpointAdded ||
eventKind == vm.EventKind.kBreakpointResolved) {
_handleBreakpointAddedOrResolved(event);
}
}
/// Registers a new isolate that exists at startup, or has subsequently been
/// created.
///
/// New isolates will be configured with the correct pause-exception behaviour,
/// libraries will be marked as debuggable if appropriate, and breakpoints
/// sent.
Future<ThreadInfo> registerIsolate(
vm.IsolateRef isolate,
String eventKind,
) async {
// Ensure the completer is set up before doing any async work, so future
// events can wait on it.
final registrationCompleter =
_isolateRegistrations.putIfAbsent(isolate.id!, () => Completer<void>());
final thread = _threadsByIsolateId.putIfAbsent(
isolate.id!,
() {
// The first time we see an isolate, start tracking it.
final info = ThreadInfo(this, _nextThreadNumber++, isolate);
_threadsByThreadId[info.threadId] = info;
// And notify the client about it.
_adapter.sendEvent(
ThreadEventBody(reason: 'started', threadId: info.threadId),
);
return info;
},
);
// If it's just become runnable (IsolateRunnable), configure the isolate
// by sending breakpoints etc.
if (eventKind == vm.EventKind.kIsolateRunnable && !thread.runnable) {
thread.runnable = true;
await _configureIsolate(thread);
registrationCompleter.complete();
}
return thread;
}
/// Calls reloadSources for all isolates.
Future<void> reloadSources() async {
await Future.wait(_threadsByThreadId.values.map(
(isolate) => _reloadSources(isolate.isolate),
));
}
Future<void> resumeIsolate(vm.IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
return;
}
await resumeThread(thread.threadId);
}
Future<void> readyToResumeIsolate(vm.IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
return;
}
await readyToResumeThread(thread.threadId);
}
/// Resumes (or steps) an isolate using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
///
/// If the isolate is paused at an async suspension and the [resumeType] is
/// [vm.StepOption.kOver], a [StepOption.kOverAsyncSuspension] step will be
/// sent instead.
Future<void> resumeThread(int threadId, [String? resumeType]) async {
await _resume(threadId, resumeType: resumeType);
}
/// Resumes an isolate using its client [threadId].
///
/// CAUTION: This should only be used for a tool-initiated resume, not a user-
/// initiated resume.
///
/// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
Future<void> readyToResumeThread(int threadId) async {
await _readyToResume(threadId);
}
/// Rewinds an isolate to an earlier frame using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
Future<void> rewindThread(int threadId, {required int frameIndex}) async {
await _resume(
threadId,
resumeType: vm.StepOption.kRewind,
frameIndex: frameIndex,
);
}
/// Resumes (or steps) an isolate using its client [threadId] on behalf
/// of the user.
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
///
/// If the isolate is paused at an async suspension and the [resumeType] is
/// [vm.StepOption.kOver], a [vm.StepOption.kOverAsyncSuspension] step will be
/// sent instead.
///
/// If [resumeType] is [vm.StepOption.kRewind], [frameIndex] must be supplied.
Future<void> _resume(
int threadId, {
String? resumeType,
int? frameIndex,
}) async {
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
throw DebugAdapterException('Thread $threadId was not found');
} else {
// Otherwise, this thread has exited and we don't need to do anything.
// It's possible another debugger unpaused or we're shutting down and
// the VM has terminated it.
return;
}
}
// Check this thread hasn't already been resumed by another handler in the
// meantime (for example if the user performs a hot restart or something
// while we processing some previous events).
if (!thread.paused || thread.hasPendingUserResume) {
return;
}
// We always assume that a step when at an async suspension is intended to
// be an async step.
if (resumeType == vm.StepOption.kOver && thread.atAsyncSuspension) {
resumeType = vm.StepOption.kOverAsyncSuspension;
}
// Finally, when we're resuming, all stored objects become invalid and
// we can drop them to save memory.
thread.clearStoredData();
thread.hasPendingUserResume = true;
try {
await _adapter.vmService?.resume(
thread.isolate.id!,
step: resumeType,
frameIndex: frameIndex,
);
} on vm.SentinelException {
// It's possible during these async requests that the isolate went away
// (for example a shutdown/restart) and we no longer care about
// resuming it.
} on vm.RPCError catch (e) {
if (e.code == RpcErrorCodes.kIsolateMustBePaused) {
// It's possible something else resumed the thread (such as if another
// debugger is attached), we can just continue.
} else {
rethrow;
}
} finally {
thread.hasPendingUserResume = false;
}
}
/// Resumes an isolate using its client [threadId].
///
/// CAUTION: This should only be used for a tool-initiated resume, not a user-
/// initiated resume.
///
/// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
Future<void> _readyToResume(int threadId) async {
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
throw DebugAdapterException('Thread $threadId was not found');
} else {
// Otherwise, this thread has exited and we don't need to do anything.
// It's possible another debugger unpaused or we're shutting down and
// the VM has terminated it.
return;
}
}
// When we're resuming, all stored objects become invalid and we can drop
// to save memory.
thread.clearStoredData();
try {
thread.hasPendingDapResume = true;
await _adapter.vmService?.readyToResume(thread.isolate.id!);
} on UnimplementedError {
// Fallback to a regular resume if the DDS version doesn't support
// `readyToResume`:
return _resume(threadId);
} on vm.SentinelException {
// It's possible during these async requests that the isolate went away
// (for example a shutdown/restart) and we no longer care about
// resuming it.
} on vm.RPCError catch (e) {
if (e.code == RpcErrorCodes.kIsolateMustBePaused) {
// It's possible something else resumed the thread (such as if another
// debugger is attached), we can just continue.
} else if (e.code == RpcErrorCodes.kMethodNotFound) {
// Fallback to a regular resume if the DDS service extension isn't
// available:
return _resume(threadId);
} else {
rethrow;
}
} finally {
thread.hasPendingDapResume = false;
}
}
/// Pauses an isolate using its client [threadId].
///
/// This is simply a _request_ to pause. It does not change any state by
/// itself - we will handle the pause via an event if the pause request
/// succeeds.
Future<void> pauseThread(int threadId) async {
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
throw DebugAdapterException('Thread $threadId was not found');
} else {
// Otherwise, this thread has recently exited so we cannot attempt
// to pause it.
return;
}
}
try {
await _adapter.vmService?.pause(thread.isolate.id!);
} on vm.SentinelException {
// It's possible during these async requests that the isolate went away
// (for example a shutdown/restart) and we no longer care about
// pausing it.
}
}
/// Checks whether [threadId] is invalid and has never been used.
///
/// Returns `false` is [threadId] corresponds to either a live, or previously
/// exited thread.
bool isInvalidThreadId(int threadId) => threadId >= _nextThreadNumber;
/// Sends an event informing the client that a thread is stopped at entry.
void sendStoppedOnEntryEvent(ThreadInfo thread) {
_adapter.sendEvent(
StoppedEventBody(reason: 'entry', threadId: thread.threadId));
}
/// Records breakpoints for [uri].
///
/// [breakpoints] represents the new set and entirely replaces anything given
/// before.
Future<void> setBreakpoints(
String uri,
List<ClientBreakpoint> breakpoints,
) async {
// Track the breakpoints to get sent to any new isolates that start.
_clientBreakpointsByUri[uri] = breakpoints;
// Send the breakpoints to all existing threads.
await Future.wait(_threadsByThreadId.values
.map((thread) => _sendBreakpoints(thread, uri: uri)));
}
/// Clears all breakpoints.
Future<void> clearAllBreakpoints() async {
// Clear all breakpoints for each URI. Do not remove the items from the map
// as that will stop them being tracked/sent by the call below.
_clientBreakpointsByUri.updateAll((key, value) => []);
// Send the breakpoints to all existing threads.
await Future.wait(
_threadsByThreadId.values.map((thread) => _sendBreakpoints(thread)),
);
}
/// Records exception pause mode as one of 'None', 'Unhandled' or 'All'. All
/// existing isolates will be updated to reflect the new setting.
Future<void> setExceptionPauseMode(String mode) async {
_exceptionPauseMode = mode;
// Send to all existing threads.
await Future.wait(_threadsByThreadId.values.map(
(thread) => _sendExceptionPauseMode(thread),
));
}
/// Stores some basic data indexed by an integer for use in "reference" fields
/// that are round-tripped to the client.
int storeData(ThreadInfo thread, Object data) {
final id = _nextStoredDataId++;
_storedData[id] = StoredData(thread, data);
return id;
}
ThreadInfo? threadForIsolate(vm.IsolateRef? isolate) =>
isolate?.id != null ? threadForIsolateId(isolate!.id!) : null;
ThreadInfo? threadForIsolateId(String isolateId) =>
_threadsByIsolateId[isolateId];
/// Evaluates breakpoint condition [condition] and returns whether the result
/// is true (or non-zero for a numeric), sending any evaluation error to the
/// client.
Future<bool> _breakpointConditionEvaluatesTrue(
ThreadInfo thread,
String condition,
) async {
final result =
await _evaluateAndPrintErrors(thread, condition, 'condition');
if (result == null) {
return false;
}
// Values we consider true for breakpoint conditions are boolean true,
// or non-zero numerics.
return (result.kind == vm.InstanceKind.kBool &&
result.valueAsString == 'true') ||
(result.kind == vm.InstanceKind.kInt && result.valueAsString != '0') ||
(result.kind == vm.InstanceKind.kDouble && result.valueAsString != '0');
}
/// Configures a new isolate, setting it's exception-pause mode, which
/// libraries are debuggable, and sending all breakpoints.
Future<void> _configureIsolate(ThreadInfo thread) async {
try {
// Libraries must be set as debuggable _before_ sending breakpoints, or
// they may fail for SDK sources.
await Future.wait([
_sendLibraryDebuggables(thread),
_sendExceptionPauseMode(thread),
], eagerError: true);
await _sendBreakpoints(thread);
} on vm.SentinelException {
// It's possible during these async requests that the isolate went away
// (for example a shutdown/restart) and we no longer care about
// configuring it. State will be cleaned up by the IsolateExit event.
}
}
/// Evaluates an expression, returning the result if it is a [vm.InstanceRef]
/// and sending any error as an [OutputEvent].
Future<vm.InstanceRef?> _evaluateAndPrintErrors(
ThreadInfo thread,
String expression,
String type,
) async {
try {
final result = await _adapter.vmService?.evaluateInFrame(
thread.isolate.id!,
0,
expression,
disableBreakpoints: true,
);
if (result is vm.InstanceRef) {
return result;
} else if (result is vm.ErrorRef) {
final message = result.message ?? '<error ref>';
_adapter.sendConsoleOutput(
'Debugger failed to evaluate breakpoint $type "$expression": $message',
);
} else if (result is vm.Sentinel) {
final message = result.valueAsString ?? '<collected>';
_adapter.sendConsoleOutput(
'Debugger failed to evaluate breakpoint $type "$expression": $message',
);
}
} catch (e) {
_adapter.sendConsoleOutput(
'Debugger failed to evaluate breakpoint $type "$expression": $e',
);
}
return null;
}
void _handleExit(vm.Event event) {
final isolate = event.isolate!;
final isolateId = isolate.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread != null) {
// Notify the client.
_adapter.sendEvent(
ThreadEventBody(reason: 'exited', threadId: thread.threadId),
);
_threadsByIsolateId.remove(isolateId);
_threadsByThreadId.remove(thread.threadId);
}
}
/// Handles a pause event.
///
/// For [vm.EventKind.kPausePostRequest] which occurs after a restart, the
/// isolate will be re-configured (pause-exception behaviour, debuggable
/// libraries, breakpoints) and we'll declare we are ready to resume.
///
/// For [vm.EventKind.kPauseStart] we'll declare we are ready to resume.
///
/// For breakpoints with conditions that are not met and for logpoints, the
/// isolate will be automatically resumed.
///
/// For all other pause types, the isolate will remain paused and a
/// corresponding "Stopped" event sent to the editor.
Future<void> _handlePause(vm.Event event) async {
final eventKind = event.kind;
final isolate = event.isolate!;
final isolateId = isolate.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
return;
}
thread.atAsyncSuspension = event.atAsyncSuspension ?? false;
thread.paused = true;
thread.pauseEvent = event;
// For PausePostRequest we need to re-send all breakpoints; this happens
// after a hot restart.
if (eventKind == vm.EventKind.kPausePostRequest) {
await _configureIsolate(thread);
await readyToResumeThread(thread.threadId);
} else if (eventKind == vm.EventKind.kPauseStart) {
// Don't resume from a PauseStart if this has already happened (see
// comments on [thread.hasBeenStarted]).
if (!thread.startupHandled) {
thread.startupHandled = true;
// Send a Stopped event to inform the client UI the thread is paused and
// declare that we are ready to resume (which might result in an
// immediate resume).
sendStoppedOnEntryEvent(thread);
await readyToResumeThread(thread.threadId);
}
} else {
// PauseExit, PauseBreakpoint, PauseInterrupted, PauseException
var reason = 'pause';
if (eventKind == vm.EventKind.kPauseBreakpoint &&
(event.pauseBreakpoints?.isNotEmpty ?? false)) {
reason = 'breakpoint';
// Look up the client breakpoints that correspond to the VM breakpoint(s)
// we hit. It's possible some of these may be missing because we could
// hit a breakpoint that was set before we were attached.
//
// When multiple client breakpoints have been folded into a single VM
// breakpoint, we (arbitrarily) use the first one for conditions and
// logpoints.
final clientBreakpoints = event.pauseBreakpoints!.map((bp) {
final uniqueBreakpointId =
(isolateId: isolateId, breakpointId: bp.id!);
return _clientBreakpointsByVmId[uniqueBreakpointId]
?.firstOrNull
?.breakpoint;
}).toSet();
// Split into logpoints (which just print messages) and breakpoints.
final logPoints = clientBreakpoints.nonNulls
.where((bp) => bp.logMessage?.isNotEmpty ?? false)
.toSet();
final breakpoints = clientBreakpoints.difference(logPoints);
await _processLogPoints(thread, logPoints);
// Resume if there are no (non-logpoint) breakpoints, of any of the
// breakpoints don't have false conditions.
if (breakpoints.isEmpty ||
!await _shouldHitBreakpoint(thread, breakpoints)) {
await resumeThread(thread.threadId);
return;
}
} else if (eventKind == vm.EventKind.kPauseBreakpoint) {
reason = 'step';
} else if (eventKind == vm.EventKind.kPauseException) {
reason = 'exception';
} else if (eventKind == vm.EventKind.kPauseExit) {
reason = 'exit';
}
// If we stopped at an exception, capture the exception instance so we
// can add a variables scope for it so it can be examined.
final exception = event.exception;
String? text;
if (exception != null) {
_adapter.storeEvaluateName(exception, threadExceptionExpression);
thread.exceptionReference = thread.storeData(exception);
text = await _adapter.getFullString(thread, exception);
}
// Notify the client.
_adapter.sendEvent(
StoppedEventBody(
reason: reason,
threadId: thread.threadId,
text: text,
),
);
}
}
/// Handles a resume event from the VM, updating our local state.
void _handleResumed(vm.Event event) {
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
if (thread != null) {
// When a thread is resumed, we must inform the client. This is not
// necessary when the user has clicked Continue because it is implied.
// However, resume events can now be triggered by other things (eg. other
// in other IDEs or DevTools) so we must notify the client.
_adapter.sendEvent(ContinuedEventBody(threadId: thread.threadId));
thread.paused = false;
thread.pauseEvent = null;
thread.exceptionReference = null;
}
}
/// Handles an inspect event from the VM, sending the value/variable to the
/// debugger.
void _handleInspect(vm.Event event) {
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
final inspectee = event.inspectee;
if (thread != null && inspectee != null) {
final ref = thread.storeData(InspectData(inspectee));
_adapter.sendOutput(
'console',
'', // Not shown by the client because it fetches the variable.
variablesReference: ref,
);
}
}
/// Handles 'BreakpointAdded'/'BreakpointResolved' events from the VM,
/// informing the client of updated information about the breakpoint.
///
/// Information about unresolved breakpoints will be ignored to avoid
/// overwriting resolved breakpoint info with unresolved/stale info in the
/// case of multiple isolates where they haven't all loaded the scripts that
/// we added breakpoints for.
void _handleBreakpointAddedOrResolved(vm.Event event) {
final breakpoint = event.breakpoint!;
final isolateId = event.isolate!.id!;
final breakpointId = breakpoint.id!;
final uniqueBreakpointId =
(isolateId: isolateId, breakpointId: breakpointId);
if (!(breakpoint.resolved ?? false)) {
// Unresolved breakpoint, don't need to do anything.
return;
}
// If we already have an event, assert that the resolution location is the
// same because we are making assumptions that we can reuse these resolution
// events to speed up telling the client a breakpoint was resolved.
assert(() {
final existingResolvedEvent =
_breakpointResolvedEventsByVmId[uniqueBreakpointId];
if (existingResolvedEvent != null) {
final existingLocation =
existingResolvedEvent.breakpoint?.location as vm.SourceLocation?;
final newLocation = event.breakpoint?.location as vm.SourceLocation?;
assert(existingLocation!.line == newLocation!.line);
assert(existingLocation!.column == newLocation!.column);
}
return true;
}());
// Store this event so if we get any future breakpoints that resolve to this
// VM breakpoint, we can access the resolution info.
_breakpointResolvedEventsByVmId[(
isolateId: isolateId,
breakpointId: breakpointId
)] = event;
// And for existing breakpoints, send (or queue) resolved events.
final existingBreakpoints = _clientBreakpointsByVmId[uniqueBreakpointId];
for (final existingBreakpoint in existingBreakpoints ?? const []) {
queueBreakpointResolutionEvent(event, existingBreakpoint);
}
}
/// Queues a breakpoint resolution event that passes resolution info from
/// the VM back to the client.
///
/// This queue will be processed only after the client has been given the ID
/// of this breakpoint. If that has already happened, the event will be
/// processed on the next task queue iteration.
void queueBreakpointResolutionEvent(
vm.Event addedOrResolvedEvent,
ClientBreakpoint clientBreakpoint,
) {
assert(addedOrResolvedEvent.breakpoint != null);
final breakpoint = addedOrResolvedEvent.breakpoint!;
assert(breakpoint.resolved ?? false);
// This is always resolved because of the check above.
final location = breakpoint.location;
final resolvedLocation = location as vm.SourceLocation;
final updatedBreakpoint = Breakpoint(
id: clientBreakpoint.id,
line: resolvedLocation.line,
column: resolvedLocation.column,
verified: true,
);
// Ensure we don't send the breakpoint event until the client has been
// given the breakpoint ID by queueing it.
clientBreakpoint.queueAction(
() => _adapter.sendEvent(
BreakpointEventBody(breakpoint: updatedBreakpoint, reason: 'changed'),
),
);
}
/// Attempts to resolve [uris] to file:/// URIs via the VM Service.
///
/// This method calls the VM service directly. Most requests to resolve URIs
/// should go through [ThreadInfo]'s resolveXxx methods which perform caching
/// of results.
Future<List<Uri?>?> _lookupResolvedPackageUris<T extends vm.Response>(
vm.IsolateRef isolate,
List<Uri> uris,
) async {
final isolateId = isolate.id!;
final uriStrings = uris.map((uri) => uri.toString()).toList();
try {
final res = await _adapter.vmService
?.lookupResolvedPackageUris(isolateId, uriStrings, local: true);
return res?.uris
?.cast<String?>()
.map((uri) => uri != null ? Uri.parse(uri) : null)
.toList();
} on vm.SentinelException {
// If the isolate disappeared before we sent this request, just return
// null responses.
return uris.map((e) => null).toList();
}
}
/// Interpolates and prints messages for any log points.
///
/// Log Points are breakpoints with string messages attached. When the VM hits
/// the breakpoint, we evaluate/print the message and then automatically
/// resume (as long as there was no other breakpoint).
Future<void> _processLogPoints(
ThreadInfo thread,
Set<SourceBreakpoint> logPoints,
) async {
// Otherwise, we need to evaluate all of the conditions and see if any are
// true, in which case we will also hit.
final messages = logPoints.map((bp) => bp.logMessage!).toList();
final results = await Future.wait(messages.map(
(message) {
// Log messages are bare so use jsonEncode to make them valid string
// expressions.
final expression = jsonEncode(message)
// The DAP spec says "Expressions within {} are interpolated" so to
// avoid any clever parsing, just prefix them with $ and treat them
// like other Dart interpolation expressions.
.replaceAllMapped(_braceNotPrefixedByDollarOrBackslashPattern,
(match) => '${match.group(1)}\${')
// Remove any backslashes the user added to "escape" braces.
.replaceAll(r'\\{', '{');
return _evaluateAndPrintErrors(thread, expression, 'log message');
},
));
for (final messageResult in results) {
// TODO(dantup): Format this using other existing code in protocol converter?
_adapter.sendConsoleOutput(messageResult?.valueAsString);
}
}
/// Resumes any paused isolates.
Future<void> resumeAll() async {
final pausedThreads = threads.where((thread) => thread.paused).toList();
await Future.wait(
pausedThreads.map((thread) => resumeThread(thread.threadId)),
);
}
/// Calls reloadSources for the given isolate.
Future<void> _reloadSources(vm.IsolateRef isolateRef) async {
final service = _adapter.vmService;
if (!debug || service == null) {
return;
}
final isolateId = isolateRef.id!;
await service.reloadSources(isolateId);
}
/// Sets breakpoints for an individual isolate.
///
/// If [uri] is provided, only breakpoints for that URI will be sent (used
/// when breakpoints are modified for a single file in the editor). Otherwise
/// breakpoints for all previously set URIs will be sent (used for
/// newly-created isolates).
Future<void> _sendBreakpoints(ThreadInfo thread, {String? uri}) async {
final service = _adapter.vmService;
if (!debug || service == null) {
return;
}
final isolateId = thread.isolate.id!;
// If we were passed a single URI, we should send breakpoints only for that
// (this means the request came from the client), otherwise we should send
// all of them (because this is a new/restarting isolate).
final uris = uri != null ? [uri] : _clientBreakpointsByUri.keys.toList();
for (final uri in uris) {
// Clear existing breakpoints.
final existingBreakpointsForIsolate =
_vmBreakpointsByIsolateIdAndUri.putIfAbsent(isolateId, () => {});
final existingBreakpointsForIsolateAndUri =
existingBreakpointsForIsolate.putIfAbsent(uri, () => {});
// Before doing async work, take a copy of the breakpoints to remove
// and remove them from the list, so any subsequent calls here don't
// try to remove the same ones multiple times.
final breakpointsToRemove =
existingBreakpointsForIsolateAndUri.values.toList();
existingBreakpointsForIsolateAndUri.clear();
await Future.forEach<vm.Breakpoint>(breakpointsToRemove, (bp) async {
try {
await service.removeBreakpoint(isolateId, bp.id!);
} catch (e) {
// Swallow errors removing breakpoints rather than failing the whole
// request as it's very possible that an isolate exited while we were
// sending this and the request will fail.
_adapter.logger?.call('Failed to remove old breakpoint $e');
}
});
// Set new breakpoints.
final newBreakpoints = _clientBreakpointsByUri[uri] ?? const [];
await Future.forEach<ClientBreakpoint>(newBreakpoints, (bp) async {
try {
// Some file URIs (like SDK sources) need to be converted to
// appropriate internal URIs to be able to set breakpoints.
final vmUri = await thread.resolvePathToUri(Uri.parse(uri));
if (vmUri == null) {
return;
}
final vmBp = await service.addBreakpointWithScriptUri(
isolateId, vmUri.toString(), bp.breakpoint.line,
column: bp.breakpoint.column);
final vmBpId = vmBp.id!;
final uniqueBreakpointId =
(isolateId: isolateId, breakpointId: vmBp.id!);
existingBreakpointsForIsolateAndUri[vmBpId] = vmBp;
// Store this client breakpoint by the VM ID, so when we get events
// from the VM we can map them back to client breakpoints (for example
// to send resolved events).
_clientBreakpointsByVmId
.putIfAbsent(uniqueBreakpointId, () => [])
.add(bp);
// Queue any resolved events that may have already arrived
// (either because the VM sent them before responding to us, or
// because it gave us an existing VM breakpoint because it resolved to
// the same location as another).
final resolvedEvent =
_breakpointResolvedEventsByVmId[uniqueBreakpointId];
if (resolvedEvent != null) {
queueBreakpointResolutionEvent(resolvedEvent, bp);
}
} catch (e) {
// Swallow errors setting breakpoints rather than failing the whole
// request as it's very easy for editors to send us breakpoints that
// aren't valid any more.