blob: 978bd845e19d7e6903e34728a5b2b0955bf252ca [file] [log] [blame]
// Copyright (c) 2019, 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 'package:async/async.dart';
import 'package:dwds/src/debugging/remote_debugger.dart';
import 'package:logging/logging.dart';
abstract class ExecutionContext {
/// Returns the context ID that contains the running Dart application,
/// if available.
Future<int?> get id;
}
/// The execution context in which to do remote evaluations.
class RemoteDebuggerExecutionContext extends ExecutionContext {
final RemoteDebugger _remoteDebugger;
final _logger = Logger('RemoteDebuggerExecutionContext');
/// Contexts that may contain a Dart application.
///
/// Context can be null if an error has occurred and we cannot detect
/// and parse the context ID.
late StreamQueue<int> _contexts;
final _contextController = StreamController<int>();
final _seenContexts = <int>[];
int? _id;
Future<int?> _lookUpId() async {
_logger.fine('Looking for Dart execution context...');
const timeoutInMs = 100;
while (await _contexts.hasNext.timeout(
const Duration(milliseconds: timeoutInMs),
onTimeout: () {
_logger.warning(
'Timed out finding an execution context after $timeoutInMs ms.',
);
return false;
},
)) {
final context = await _contexts.next;
_seenContexts.add(context);
_logger.fine('Checking context id: $context');
try {
final result = await _remoteDebugger.sendCommand(
'Runtime.evaluate',
params: {
'expression': r'window["$dartAppInstanceId"];',
'contextId': context,
},
);
if (result.result?['result']?['value'] != null) {
_logger.fine('Found dart execution context: $context');
return context;
}
} catch (_) {
// Errors may be thrown if we attempt to evaluate in a stale a context.
// Ignore and continue.
_logger.fine('Stale execution context: $context');
_seenContexts.remove(context);
}
}
return null;
}
@override
Future<int?> get id async {
if (_id != null) return _id;
_id = await _lookUpId();
if (_id == null) {
// Add seen contexts back to the queue in case the injected
// client is still loading, so the next call to `id` succeeds.
_seenContexts.forEach(_contextController.add);
_seenContexts.clear();
}
return _id;
}
RemoteDebuggerExecutionContext(this._id, this._remoteDebugger) {
_remoteDebugger
.eventStream('Runtime.executionContextsCleared', (e) => e)
.listen((_) => _id = null);
_remoteDebugger.eventStream('Runtime.executionContextCreated', (e) {
// Parse and add the context ID to the stream.
// If we cannot detect or parse the context ID, add `null` to the stream
// to indicate an error context - those will be skipped when trying to find
// the dart context, with a warning.
final id = e.params?['context']?['id']?.toString();
final parsedId = id == null ? null : int.parse(id);
if (id == null) {
_logger.warning('Cannot find execution context id: $e');
} else if (parsedId == null) {
_logger.warning('Cannot parse execution context id: $id');
}
return parsedId;
}).listen((e) {
if (e != null) _contextController.add(e);
});
_contexts = StreamQueue(_contextController.stream);
}
}