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
// Copyright 2014 The Flutter Authors. 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 'dart:io';
import 'package:file/file.dart' as f;
import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart' as fuchsia;
import 'package:path/path.dart' as p;
import 'package:vm_service/vm_service.dart' as vms;
import 'package:webdriver/async_io.dart' as async_io;
import '../../flutter_driver.dart';
/// An implementation of the Flutter Driver over the vmservice protocol.
class VMServiceFlutterDriver extends FlutterDriver {
/// Creates a driver that uses a connection provided by the given
/// [serviceClient] and [appIsolate].
VMServiceFlutterDriver.connectedTo(
this._serviceClient,
this._appIsolate, {
bool printCommunication = false,
bool logCommunicationToFile = true,
}) : _printCommunication = printCommunication,
_logCommunicationToFile = logCommunicationToFile,
_driverId = _nextDriverId++
{
_logFilePathName = p.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log');
}
/// Connects to a Flutter application.
///
/// See [FlutterDriver.connect] for more documentation.
static Future<FlutterDriver> connect({
String? dartVmServiceUrl,
bool printCommunication = false,
bool logCommunicationToFile = true,
int? isolateNumber,
Pattern? fuchsiaModuleTarget,
Map<String, dynamic>? headers,
}) async {
// If running on a Fuchsia device, connect to the first isolate whose name
// matches FUCHSIA_MODULE_TARGET.
//
// If the user has already supplied an isolate number/URL to the Dart VM
// service, then this won't be run as it is unnecessary.
if (Platform.isFuchsia && isolateNumber == null) {
// TODO(awdavies): Use something other than print. On fuchsia
// `stderr`/`stdout` appear to have issues working correctly.
driverLog = (String source, String message) {
print('$source: $message'); // ignore: avoid_print
};
fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
if (fuchsiaModuleTarget == null) {
throw DriverError(
'No Fuchsia module target has been specified.\n'
'Please make sure to specify the FUCHSIA_MODULE_TARGET '
'environment variable.'
);
}
final fuchsia.FuchsiaRemoteConnection fuchsiaConnection = await FuchsiaCompat.connect();
final List<fuchsia.IsolateRef> refs = await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
if (refs.isEmpty) {
throw DriverError('Failed to get any isolate refs!');
}
final fuchsia.IsolateRef ref = refs.first;
isolateNumber = ref.number;
dartVmServiceUrl = ref.dartVm.uri.toString();
await fuchsiaConnection.stop();
FuchsiaCompat.cleanup();
}
dartVmServiceUrl ??= Platform.environment['VM_SERVICE_URL'];
if (dartVmServiceUrl == null) {
throw DriverError(
'Could not determine URL to connect to application.\n'
'Either the VM_SERVICE_URL environment variable should be set, or an explicit '
'URL should be provided to the FlutterDriver.connect() method.'
);
}
// Connect to Dart VM services
_log('Connecting to Flutter application at $dartVmServiceUrl');
final vms.VmService client = await vmServiceConnectFunction(dartVmServiceUrl, headers);
Future<vms.IsolateRef?> waitForRootIsolate() async {
bool checkIsolate(vms.IsolateRef ref) => ref.number == isolateNumber.toString();
while (true) {
final vms.VM vm = await client.getVM();
if (vm.isolates!.isEmpty || (isolateNumber != null && !vm.isolates!.any(checkIsolate))) {
await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
continue;
}
return isolateNumber == null
? vm.isolates!.first
: vm.isolates!.firstWhere(checkIsolate);
}
}
final vms.IsolateRef isolateRef = (await _warnIfSlow<vms.IsolateRef?>(
future: waitForRootIsolate(),
timeout: kUnusuallyLongTimeout,
message: isolateNumber == null
? 'The root isolate is taking an unusually long time to start.'
: 'Isolate $isolateNumber is taking an unusually long time to start.',
))!;
_log('Isolate found with number: ${isolateRef.number}');
vms.Isolate isolate = await client.getIsolate(isolateRef.id!);
if (isolate.pauseEvent!.kind == vms.EventKind.kNone) {
isolate = await client.getIsolate(isolateRef.id!);
}
final VMServiceFlutterDriver driver = VMServiceFlutterDriver.connectedTo(
client,
isolate,
printCommunication: printCommunication,
logCommunicationToFile: logCommunicationToFile,
);
// Attempts to resume the isolate, but does not crash if it fails because
// the isolate is already resumed. There could be a race with other tools,
// such as a debugger, any of which could have resumed the isolate.
Future<vms.Success> resumeLeniently() async {
_log('Attempting to resume isolate');
// Let subsequent isolates start automatically.
try {
final vms.Response result = await client.setFlag('pause_isolates_on_start', 'false');
if (result == null || result.type != 'Success') {
_log('setFlag failure: $result');
}
} catch (e) {
_log('Failed to set pause_isolates_on_start=false, proceeding. Error: $e');
}
return client.resume(isolate.id!).catchError((Object e) {
const int vmMustBePausedCode = 101;
if (e is vms.RPCError && e.code == vmMustBePausedCode) {
// No biggie; something else must have resumed the isolate
_log(
'Attempted to resume an already resumed isolate. This may happen '
'when another tool (usually a debugger) resumed the isolate '
'before the flutter_driver did.'
);
return vms.Success();
} else {
// Failed to resume due to another reason. Fail hard.
throw e; // ignore: only_throw_errors, proxying the error from upstream.
}
});
}
/// Waits for a signal from the VM service that the extension is registered.
///
/// Looks at the list of loaded extensions for the current [isolateRef], as
/// well as the stream of added extensions.
Future<void> waitForServiceExtension() async {
await client.streamListen(vms.EventStreams.kIsolate);
final Future<void> extensionAlreadyAdded = client
.getIsolate(isolateRef.id!)
.then((vms.Isolate isolate) async {
if (isolate.extensionRPCs!.contains(_flutterExtensionMethodName)) {
return;
}
// Never complete. Rely on the stream listener to find the service
// extension instead.
return Completer<void>().future;
});
final Completer<void> extensionAdded = Completer<void>();
late StreamSubscription<vms.Event> isolateAddedSubscription;
isolateAddedSubscription = client.onIsolateEvent.listen(
(vms.Event data) {
if (data.kind == vms.EventKind.kServiceExtensionAdded && data.extensionRPC == _flutterExtensionMethodName) {
extensionAdded.complete();
isolateAddedSubscription.cancel();
}
},
onError: extensionAdded.completeError,
cancelOnError: true,
);
await Future.any(<Future<void>>[
extensionAlreadyAdded,
extensionAdded.future,
]);
await isolateAddedSubscription.cancel();
await client.streamCancel(vms.EventStreams.kIsolate);
}
// Attempt to resume isolate if it was paused
if (isolate.pauseEvent!.kind == vms.EventKind.kPauseStart) {
_log('Isolate is paused at start.');
await resumeLeniently();
} else if (isolate.pauseEvent!.kind == vms.EventKind.kPauseExit ||
isolate.pauseEvent!.kind == vms.EventKind.kPauseBreakpoint ||
isolate.pauseEvent!.kind == vms.EventKind.kPauseException ||
isolate.pauseEvent!.kind == vms.EventKind.kPauseInterrupted) {
// If the isolate is paused for any other reason, assume the extension is
// already there.
_log('Isolate is paused mid-flight.');
await resumeLeniently();
} else if (isolate.pauseEvent!.kind == vms.EventKind.kResume) {
_log('Isolate is not paused. Assuming application is ready.');
} else {
_log(
'Unknown pause event type ${isolate.pauseEvent.runtimeType}. '
'Assuming application is ready.'
);
}
// We will never receive the extension event if the user does not register
// it. If that happens, show a message but continue waiting.
await _warnIfSlow<void>(
future: waitForServiceExtension(),
timeout: kUnusuallyLongTimeout,
message: 'Flutter Driver extension is taking a long time to become available. '
'Ensure your test app (often "lib/main.dart") imports '
'"package:flutter_driver/driver_extension.dart" and '
'calls enableFlutterDriverExtension() as the first call in main().',
);
final Health health = await driver.checkHealth();
if (health.status != HealthStatus.ok) {
await client.dispose();
await client.onDone;
throw DriverError('Flutter application health check failed.');
}
_log('Connected to Flutter application.');
return driver;
}
static int _nextDriverId = 0;
static const String _flutterExtensionMethodName = 'ext.flutter.driver';
static const String _collectAllGarbageMethodName = '_collectAllGarbage';
// The additional blank line in the beginning is for _log.
static const String _kDebugWarning = '''
┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
┇ ⚠ THIS BENCHMARK IS BEING RUN IN DEBUG MODE ⚠ ┇
┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦
│ │
│ Numbers obtained from a benchmark while asserts are │
│ enabled will not accurately reflect the performance │
│ that will be experienced by end users using release ╎
│ builds. Benchmarks should be run using this command ┆
│ line: flutter drive --profile test_perf.dart ┊
│ ┊
└─────────────────────────────────────────────────╌┄┈ 🐢
''';
/// The unique ID of this driver instance.
final int _driverId;
@override
vms.Isolate get appIsolate => _appIsolate;
/// Client connected to the Dart VM running the Flutter application.
///
/// You can use [VMServiceClient] to check VM version, flags and get
/// notified when a new isolate has been instantiated. That could be
/// useful if your application spawns multiple isolates that you
/// would like to instrument.
final vms.VmService _serviceClient;
@override
vms.VmService get serviceClient => _serviceClient;
@override
async_io.WebDriver get webDriver => throw UnsupportedError('VMServiceFlutterDriver does not support webDriver');
/// The main isolate hosting the Flutter application.
///
/// If you used the [registerExtension] API to instrument your application,
/// you can use this [vms.Isolate] to call these extension methods via
/// [invokeExtension].
final vms.Isolate _appIsolate;
/// Whether to print communication between host and app to `stdout`.
final bool _printCommunication;
/// Whether to log communication between host and app to `flutter_driver_commands.log`.
final bool _logCommunicationToFile;
/// Logs are written here when _logCommunicationToFile is true.
late final String _logFilePathName;
/// Getter for file pathname where logs are written when _logCommunicationToFile is true.
String get logFilePathName => _logFilePathName;
@override
Future<Map<String, dynamic>> sendCommand(Command command) async {
late Map<String, dynamic> response;
try {
final Map<String, String> serialized = command.serialize();
_logCommunication('>>> $serialized');
final Future<Map<String, dynamic>> future = _serviceClient.callServiceExtension(
_flutterExtensionMethodName,
isolateId: _appIsolate.id,
args: serialized,
).then<Map<String, dynamic>>((vms.Response value) => value.json!);
response = await _warnIfSlow<Map<String, dynamic>>(
future: future,
timeout: command.timeout ?? kUnusuallyLongTimeout,
message: '${command.kind} message is taking a long time to complete...',
);
_logCommunication('<<< $response');
} catch (error, stackTrace) {
throw DriverError(
'Failed to fulfill ${command.runtimeType} due to remote error',
error,
stackTrace,
);
}
if ((response['isError'] as bool?) ?? false) {
throw DriverError('Error in Flutter application: ${response['response']}');
}
return response['response'] as Map<String, dynamic>;
}
void _logCommunication(String message) {
if (_printCommunication) {
_log(message);
}
if (_logCommunicationToFile) {
assert(_logFilePathName != null);
final f.File file = fs.file(_logFilePathName);
file.createSync(recursive: true); // no-op if file exists
file.writeAsStringSync('${DateTime.now()} $message\n', mode: f.FileMode.append, flush: true);
}
}
@override
Future<List<int>> screenshot() async {
await Future<void>.delayed(const Duration(seconds: 2));
final vms.Response result = await _serviceClient.callMethod('_flutter.screenshot');
return base64.decode(result.json!['screenshot'] as String);
}
@override
Future<List<Map<String, dynamic>>> getVmFlags() async {
final vms.FlagList result = await _serviceClient.getFlagList();
return result.flags != null
? result.flags!.map((vms.Flag flag) => flag.toJson()).toList()
: const <Map<String, dynamic>>[];
}
Future<vms.Timestamp> _getVMTimelineMicros() async {
return _serviceClient.getVMTimelineMicros();
}
@override
Future<void> startTracing({
List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
Duration timeout = kUnusuallyLongTimeout,
}) async {
assert(streams != null && streams.isNotEmpty);
assert(timeout != null);
try {
await _warnIfSlow<vms.Success>(
future: _serviceClient.setVMTimelineFlags(
_timelineStreamsToString(streams),
),
timeout: timeout,
message: 'VM is taking an unusually long time to respond to being told to start tracing...',
);
} catch (error, stackTrace) {
throw DriverError(
'Failed to start tracing due to remote error',
error,
stackTrace,
);
}
}
@override
Future<Timeline> stopTracingAndDownloadTimeline({
Duration timeout = kUnusuallyLongTimeout,
int? startTime,
int? endTime,
}) async {
assert(timeout != null);
assert((startTime == null && endTime == null) ||
(startTime != null && endTime != null));
try {
await _warnIfSlow<vms.Success>(
future: _serviceClient.setVMTimelineFlags(const <String>[]),
timeout: timeout,
message: 'VM is taking an unusually long time to respond to being told to stop tracing...',
);
if (startTime == null) {
final vms.Timeline timeline = await _serviceClient.getVMTimeline();
return Timeline.fromJson(timeline.json!);
}
const int kSecondInMicros = 1000000;
int currentStart = startTime;
int currentEnd = startTime + kSecondInMicros; // 1 second of timeline
final List<Map<String, Object?>?> chunks = <Map<String, Object?>?>[];
do {
final vms.Timeline chunk = await _serviceClient.getVMTimeline(
timeOriginMicros: currentStart,
// The range is inclusive, avoid double counting on the chance something
// aligns on the boundary.
timeExtentMicros: kSecondInMicros - 1,
);
chunks.add(chunk.json);
currentStart = currentEnd;
currentEnd += kSecondInMicros;
} while (currentStart < endTime!);
return Timeline.fromJson(<String, Object>{
'traceEvents': <Object?> [
for (Map<String, Object?>? chunk in chunks)
...chunk!['traceEvents']! as List<Object?>,
],
});
} catch (error, stackTrace) {
throw DriverError(
'Failed to stop tracing due to remote error',
error,
stackTrace,
);
}
}
Future<bool> _isPrecompiledMode() async {
final List<Map<String, dynamic>> flags = await getVmFlags();
for(final Map<String, dynamic> flag in flags) {
if (flag['name'] == 'precompiled_mode') {
return flag['valueAsString'] == 'true';
}
}
return false;
}
@override
Future<Timeline> traceAction(
Future<dynamic> Function() action, {
List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
bool retainPriorEvents = false,
}) async {
if (retainPriorEvents) {
await startTracing(streams: streams);
await action();
if (!(await _isPrecompiledMode())) {
_log(_kDebugWarning);
}
return stopTracingAndDownloadTimeline();
}
await clearTimeline();
final vms.Timestamp startTimestamp = await _getVMTimelineMicros();
await startTracing(streams: streams);
await action();
final vms.Timestamp endTimestamp = await _getVMTimelineMicros();
if (!(await _isPrecompiledMode())) {
_log(_kDebugWarning);
}
return stopTracingAndDownloadTimeline(
startTime: startTimestamp.timestamp,
endTime: endTimestamp.timestamp,
);
}
@override
Future<void> clearTimeline({
Duration timeout = kUnusuallyLongTimeout,
}) async {
assert(timeout != null);
try {
await _warnIfSlow<vms.Success>(
future: _serviceClient.clearVMTimeline(),
timeout: timeout,
message: 'VM is taking an unusually long time to respond to being told to clear its timeline buffer...',
);
} catch (error, stackTrace) {
throw DriverError(
'Failed to clear event timeline due to remote error',
error,
stackTrace,
);
}
}
@override
Future<void> forceGC() async {
try {
await _serviceClient.callMethod(_collectAllGarbageMethodName, isolateId: _appIsolate.id);
} catch (error, stackTrace) {
throw DriverError(
'Failed to force a GC due to remote error',
error,
stackTrace,
);
}
}
@override
Future<void> close() async {
await _serviceClient.dispose();
await _serviceClient.onDone;
}
}
/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;
/// Restores [vmServiceConnectFunction] to its default value.
void restoreVmServiceConnectFunction() {
vmServiceConnectFunction = _waitAndConnect;
}
String _getWebSocketUrl(String url) {
Uri uri = Uri.parse(url);
final List<String> pathSegments = <String>[
// If there's an authentication code (default), we need to add it to our path.
if (uri.pathSegments.isNotEmpty) uri.pathSegments.first,
'ws',
];
if (uri.scheme == 'http') {
uri = uri.replace(scheme: 'ws', pathSegments: pathSegments);
}
return uri.toString();
}
/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
Future<vms.VmService> _waitAndConnect(String url, Map<String, dynamic>? headers) async {
final String webSocketUrl = _getWebSocketUrl(url);
int attempts = 0;
WebSocket? socket;
while (true) {
try {
socket = await WebSocket.connect(webSocketUrl, headers: headers);
final StreamController<dynamic> controller = StreamController<dynamic>();
final Completer<void> streamClosedCompleter = Completer<void>();
socket.listen(
(dynamic data) => controller.add(data),
onDone: () => streamClosedCompleter.complete(),
);
final vms.VmService service = vms.VmService(
controller.stream,
socket.add,
disposeHandler: () => socket!.close(),
streamClosed: streamClosedCompleter.future
);
// This call is to ensure we are able to establish a connection instead of
// keeping on trucking and failing farther down the process.
await service.getVersion();
return service;
} catch (e) {
// We should not be catching all errors arbitrarily here, this might hide real errors.
// TODO(ianh): Determine which exceptions to catch here.
await socket?.close();
if (attempts > 5) {
_log('It is taking an unusually long time to connect to the VM...');
}
attempts += 1;
await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
}
}
}
/// The amount of time we wait prior to making the next attempt to connect to
/// the VM service.
const Duration _kPauseBetweenReconnectAttempts = Duration(seconds: 1);
// See `timeline_streams` in
// https://github.com/dart-lang/sdk/blob/main/runtime/vm/timeline.cc
List<String> _timelineStreamsToString(List<TimelineStream> streams) {
return streams.map<String>((TimelineStream stream) {
switch (stream) {
case TimelineStream.all: return 'all';
case TimelineStream.api: return 'API';
case TimelineStream.compiler: return 'Compiler';
case TimelineStream.compilerVerbose: return 'CompilerVerbose';
case TimelineStream.dart: return 'Dart';
case TimelineStream.debugger: return 'Debugger';
case TimelineStream.embedder: return 'Embedder';
case TimelineStream.gc: return 'GC';
case TimelineStream.isolate: return 'Isolate';
case TimelineStream.vm: return 'VM';
}
}).toList();
}
void _log(String message) {
driverLog('VMServiceFlutterDriver', message);
}
Future<T> _warnIfSlow<T>({
required Future<T> future,
required Duration timeout,
required String message,
}) async {
assert(future != null);
assert(timeout != null);
assert(message != null);
final Completer<void> completer = Completer<void>();
completer.future.timeout(timeout, onTimeout: () {
_log(message);
return null;
});
try {
await future.whenComplete(() { completer.complete(); });
} catch (e) {
// Don't duplicate errors if [future] completes with an error.
}
return future;
}
/// A function that connects to a Dart VM service given the `url` and `headers`.
typedef VMServiceConnectFunction = Future<vms.VmService> Function(String url, Map<String, dynamic>? headers);