Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dwds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## 26.2.3-wip
## 26.2.3

- Bump `build_web_compilers` to ^4.4.1.
- Remove unused `clientFuture` arg from `DwdsVmClient` methods.
- Fix pausing starting of `main` after the hot restart.
- Updating bootstrapper for DDC library bundler module format + Frontend Server.
- Fix setting up breakpoints when handling in-app restarts with attached debugger.
- Fix issue where the web socket connections with the target application and Chrome debugger close when the computer sleeps.
- Fix setting up breakpoints when handling full reloads from attached
debugger / page refreshes.

Expand Down
1 change: 1 addition & 0 deletions dwds/lib/src/debugging/debugger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ class Debugger {
skipLists,
root,
);
remoteDebugger.onReconnect = debugger._initialize;
await debugger._initialize();
return debugger;
}
Expand Down
4 changes: 4 additions & 0 deletions dwds/lib/src/debugging/remote_debugger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class TargetCrashedEvent extends WipEvent {

/// A generic debugger used in remote debugging.
abstract class RemoteDebugger {
/// An optional callback that's invoked in the case where the debugger
/// connection needs to be reinitialized.
Future<void> Function()? onReconnect;

Stream<ConsoleAPIEvent> get onConsoleAPICalled;

Stream<ExceptionThrownEvent> get onExceptionThrown;
Expand Down
153 changes: 137 additions & 16 deletions dwds/lib/src/debugging/webkit_debugger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,136 @@
// 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:dwds/src/debugging/remote_debugger.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

typedef _EventStreamState = ({
StreamController controller,
WipEventTransformer transformer,
});

/// A remote debugger with a Webkit Inspection Protocol connection.
class WebkitDebugger implements RemoteDebugger {
final WipDebugger _wipDebugger;
WipDebugger _wipDebugger;

/// Null until [close] is called.
///
/// All subsequent calls to [close] will return this future.
Future<void>? _closed;

WebkitDebugger(this._wipDebugger);
// We need to forward the events from the [WipDebugger] instance rather than
// directly expose its streams as it may need to be recreated due to an
// unexpected loss in connection to the debugger.
final _onClosedController = StreamController<WipConnection>.broadcast();
final _onConsoleAPICalledController =
StreamController<ConsoleAPIEvent>.broadcast();
final _onExceptionThrownController =
StreamController<ExceptionThrownEvent>.broadcast();
final _onGlobalObjectClearedController =
StreamController<GlobalObjectClearedEvent>.broadcast();
final _onPausedController = StreamController<DebuggerPausedEvent>.broadcast();
final _onResumedController =
StreamController<DebuggerResumedEvent>.broadcast();
final _onScriptParsedController =
StreamController<ScriptParsedEvent>.broadcast();
final _onTargetCrashedController =
StreamController<TargetCrashedEvent>.broadcast();

/// Tracks and manages all subscriptions to streams created with
/// `eventStream`.
final _eventStreams = <String, _EventStreamState>{};

late final _controllers = <StreamController>[
_onClosedController,
_onConsoleAPICalledController,
_onExceptionThrownController,
_onGlobalObjectClearedController,
_onPausedController,
_onResumedController,
_onScriptParsedController,
_onTargetCrashedController,
];

final _streamSubscriptions = <StreamSubscription>[];

@override
Future<void> Function()? onReconnect;

WebkitDebugger(this._wipDebugger) {
_initialize();
}

void _initialize() {
late StreamSubscription sub;
sub = _wipDebugger.connection.onClose.listen((connection) async {
await sub.cancel();
if (_closed != null) {
// The connection closing is expected.
_onClosedController.add(connection);
await Future.wait([
for (final controller in _controllers) controller.close(),
for (final MapEntry(value: (:controller, transformer: _))
in _eventStreams.entries)
controller.close(),
]);
return;
}
var retry = false;
var retryCount = 0;
const maxAttempts = 5;
do {
retry = false;
try {
_wipDebugger = WipDebugger(
await WipConnection.connect(connection.url),
);
await onReconnect?.call();
} on Exception {
await Future.delayed(Duration(milliseconds: 25 << retryCount));
retry = true;
retryCount++;
}
} while (retry && retryCount <= maxAttempts);
_initialize();
});

final runtime = _wipDebugger.connection.runtime;
_streamSubscriptions
..forEach((e) => e.cancel())
..clear()
..addAll([
runtime.onConsoleAPICalled.listen(_onConsoleAPICalledController.add),
runtime.onExceptionThrown.listen(_onExceptionThrownController.add),
_wipDebugger.onGlobalObjectCleared.listen(
_onGlobalObjectClearedController.add,
),
_wipDebugger.onPaused.listen(_onPausedController.add),
_wipDebugger.onResumed.listen(_onResumedController.add),
_wipDebugger.onScriptParsed.listen(_onScriptParsedController.add),
_wipDebugger
.eventStream(
'Inspector.targetCrashed',
(WipEvent event) => TargetCrashedEvent(event.json),
)
.listen(_onTargetCrashedController.add),

for (final MapEntry(:key, value: (:controller, :transformer))
in _eventStreams.entries)
_wipDebugger
.eventStream(key, transformer)
.listen(controller.add, onError: controller.addError),
]);
}

@override
Stream<ConsoleAPIEvent> get onConsoleAPICalled =>
_wipDebugger.connection.runtime.onConsoleAPICalled;
_onConsoleAPICalledController.stream;

@override
Stream<ExceptionThrownEvent> get onExceptionThrown =>
_wipDebugger.connection.runtime.onExceptionThrown;
_onExceptionThrownController.stream;

@override
Future<WipResponse> sendCommand(
Expand All @@ -31,7 +140,10 @@ class WebkitDebugger implements RemoteDebugger {
}) => _wipDebugger.sendCommand(command, params: params);

@override
Future<void> close() => _closed ??= _wipDebugger.connection.close();
Future<void> close() => _closed ??= () async {
await _wipDebugger.connection.close();
await Future.wait([for (final sub in _streamSubscriptions) sub.cancel()]);
}();

@override
Future<void> disable() => _wipDebugger.disable();
Expand Down Expand Up @@ -103,31 +215,40 @@ class WebkitDebugger implements RemoteDebugger {
}

@override
Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) =>
_wipDebugger.eventStream(method, transformer);
Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
return _eventStreams
.putIfAbsent(method, () {
final controller = StreamController<T>();
final stream = _wipDebugger.eventStream(method, transformer);
stream.listen(controller.add, onError: controller.addError);
return (controller: controller, transformer: transformer);
})
.controller
.stream
.cast<T>();
}

@override
Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared =>
_wipDebugger.onGlobalObjectCleared;
_onGlobalObjectClearedController.stream;

@override
Stream<DebuggerPausedEvent> get onPaused => _wipDebugger.onPaused;
Stream<DebuggerPausedEvent> get onPaused => _onPausedController.stream;

@override
Stream<DebuggerResumedEvent> get onResumed => _wipDebugger.onResumed;
Stream<DebuggerResumedEvent> get onResumed => _onResumedController.stream;

@override
Stream<ScriptParsedEvent> get onScriptParsed => _wipDebugger.onScriptParsed;
Stream<ScriptParsedEvent> get onScriptParsed =>
_onScriptParsedController.stream;

@override
Stream<TargetCrashedEvent> get onTargetCrashed => _wipDebugger.eventStream(
'Inspector.targetCrashed',
(WipEvent event) => TargetCrashedEvent(event.json),
);
Stream<TargetCrashedEvent> get onTargetCrashed =>
_onTargetCrashedController.stream;

@override
Map<String, WipScript> get scripts => _wipDebugger.scripts;

@override
Stream<WipConnection> get onClose => _wipDebugger.connection.onClose;
Stream<WipConnection> get onClose => _onClosedController.stream;
}
13 changes: 6 additions & 7 deletions dwds/lib/src/handlers/dev_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,13 @@ class DevHandler {
) async {
ChromeTab? appTab;
ExecutionContext? executionContext;
WipConnection? tabConnection;
WipConnection? connection;
late final debugger = WebkitDebugger(WipDebugger(connection!));
final appInstanceId = appConnection.request.instanceId;
for (final tab in await chromeConnection.getTabs()) {
if (tab.isChromeExtension || tab.isBackgroundPage) continue;

final connection = tabConnection = await tab.connect();
connection = await tab.connect();
if (_enableLogging) {
connection.onSend.listen((message) {
_log(' wip', '==> $message');
Expand Down Expand Up @@ -208,30 +209,28 @@ class DevHandler {
appTab = tab;
executionContext = RemoteDebuggerExecutionContext(
contextId,
WebkitDebugger(WipDebugger(connection)),
debugger,
);
break;
}
}
if (appTab != null) break;
safeUnawaited(connection.close());
}
if (appTab == null || tabConnection == null || executionContext == null) {
if (appTab == null || connection == null || executionContext == null) {
throw AppConnectionException(
'Could not connect to application with appInstanceId: '
'$appInstanceId',
);
}

final webkitDebugger = WebkitDebugger(WipDebugger(tabConnection));

return ChromeDebugService.start(
// We assume the user will connect to the debug service on the same
// machine. This allows consumers of DWDS to provide a `hostname` for
// debugging through the Dart Debug Extension without impacting the local
// debug workflow.
hostname: 'localhost',
remoteDebugger: webkitDebugger,
remoteDebugger: debugger,
executionContext: executionContext,
assetReader: _assetReader,
appConnection: appConnection,
Expand Down
Loading
Loading