| // 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. |
| |
| // @dart = 2.9 |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:dwds/data/build_result.dart'; |
| import 'package:dwds/dwds.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| |
| import '../serve/server_manager.dart'; |
| import '../serve/webdev_server.dart'; |
| import 'daemon.dart'; |
| import 'domain.dart'; |
| import 'utilites.dart'; |
| |
| /// A collection of method and events relevant to the running application. |
| class AppDomain extends Domain { |
| bool _isShutdown = false; |
| int _buildProgressEventId; |
| var _progressEventId = 0; |
| |
| final _appStates = <String, _AppState>{}; |
| |
| void _handleBuildResult(BuildResult result, String appId) { |
| switch (result.status) { |
| case BuildStatus.started: |
| _buildProgressEventId = _progressEventId++; |
| sendEvent('app.progress', { |
| 'appId': appId, |
| 'id': '$_buildProgressEventId', |
| 'message': 'Building...', |
| }); |
| break; |
| case BuildStatus.failed: |
| sendEvent('app.progress', { |
| 'appId': appId, |
| 'id': '$_buildProgressEventId', |
| 'finished': true, |
| }); |
| break; |
| case BuildStatus.succeeded: |
| sendEvent('app.progress', { |
| 'appId': appId, |
| 'id': '$_buildProgressEventId', |
| 'finished': true, |
| }); |
| break; |
| } |
| } |
| |
| void _initialize(ServerManager serverManager) { |
| serverManager.servers.forEach(_handleAppConnections); |
| } |
| |
| void _handleAppConnections(WebDevServer server) async { |
| var dwds = server.dwds; |
| // The connection is established right before `main()` is called. |
| await for (var appConnection in dwds.connectedApps) { |
| var debugConnection = await dwds.debugConnection(appConnection); |
| var vmService = debugConnection.vmService; |
| var appId = appConnection.request.appId; |
| unawaited(debugConnection.onDone.then((_) { |
| sendEvent('app.log', { |
| 'appId': appId, |
| 'log': 'Lost connection to device.', |
| }); |
| sendEvent('app.stop', { |
| 'appId': appId, |
| }); |
| daemon.shutdown(); |
| })); |
| sendEvent('app.start', { |
| 'appId': appId, |
| 'directory': Directory.current.path, |
| 'deviceId': 'chrome', |
| 'launchMode': 'run' |
| }); |
| sendEvent('app.started', { |
| 'appId': appId, |
| }); |
| // TODO(grouma) - limit the catch to the appropriate error. |
| try { |
| await vmService.streamCancel('Stdout'); |
| } catch (_) {} |
| try { |
| await vmService.streamListen('Stdout'); |
| } catch (_) {} |
| // ignore: cancel_subscriptions |
| var stdOutSub = vmService.onStdoutEvent.listen((log) { |
| sendEvent('app.log', { |
| 'appId': appId, |
| 'log': utf8.decode(base64.decode(log.bytes)), |
| }); |
| }); |
| sendEvent('app.debugPort', { |
| 'appId': appId, |
| 'port': debugConnection.port, |
| 'wsUri': debugConnection.uri, |
| }); |
| // ignore: cancel_subscriptions |
| var resultSub = |
| server.buildResults.listen((r) => _handleBuildResult(r, appId)); |
| |
| var appState = _AppState(debugConnection, resultSub, stdOutSub); |
| _appStates[appId] = appState; |
| |
| appConnection.runMain(); |
| |
| unawaited(debugConnection.onDone.whenComplete(() { |
| appState.dispose(); |
| _appStates.remove(appId); |
| })); |
| } |
| |
| // Shutdown could have been triggered while awaiting above. |
| if (_isShutdown) dispose(); |
| } |
| |
| AppDomain(Daemon daemon, ServerManager serverManager) : super(daemon, 'app') { |
| registerHandler('restart', _restart); |
| registerHandler('callServiceExtension', _callServiceExtension); |
| registerHandler('stop', _stop); |
| |
| _initialize(serverManager); |
| } |
| |
| Future<Map<String, dynamic>> _callServiceExtension( |
| Map<String, dynamic> args) async { |
| var appId = getStringArg(args, 'appId', required: true); |
| var appState = _appStates[appId]; |
| if (appState == null) { |
| throw ArgumentError.value(appId, 'appId', 'Not found'); |
| } |
| var methodName = getStringArg(args, 'methodName', required: true); |
| var params = args['params'] != null |
| ? (args['params'] as Map<String, dynamic>) |
| : <String, dynamic>{}; |
| var response = |
| await appState.vmService.callServiceExtension(methodName, args: params); |
| return response.json; |
| } |
| |
| Future<Map<String, dynamic>> _restart(Map<String, dynamic> args) async { |
| var appId = getStringArg(args, 'appId', required: true); |
| var appState = _appStates[appId]; |
| if (appState == null) { |
| throw ArgumentError.value(appId, 'appId', 'Not found'); |
| } |
| var fullRestart = getBoolArg(args, 'fullRestart') ?? false; |
| if (!fullRestart) { |
| return { |
| 'code': 1, |
| 'message': 'hot reload not yet supported by webdev', |
| }; |
| } |
| // TODO(grouma) - Support pauseAfterRestart. |
| // var pauseAfterRestart = getBoolArg(args, 'pause') ?? false; |
| var stopwatch = Stopwatch()..start(); |
| _progressEventId++; |
| sendEvent('app.progress', { |
| 'appId': appId, |
| 'id': '$_progressEventId', |
| 'message': 'Performing hot restart...', |
| 'progressId': 'hot.restart', |
| }); |
| var response = await appState.vmService.callServiceExtension('hotRestart'); |
| sendEvent('app.progress', { |
| 'appId': appId, |
| 'id': '$_progressEventId', |
| 'finished': true, |
| 'progressId': 'hot.restart', |
| }); |
| sendEvent('app.log', { |
| 'appId': appId, |
| 'log': 'Restarted application in ${stopwatch.elapsedMilliseconds}ms' |
| }); |
| return { |
| 'code': response.type == 'Success' ? 0 : 1, |
| 'message': response.toString() |
| }; |
| } |
| |
| Future<bool> _stop(Map<String, dynamic> args) async { |
| var appId = getStringArg(args, 'appId', required: true); |
| var appState = _appStates[appId]; |
| if (appState == null) { |
| throw ArgumentError.value(appId, 'appId', 'Not found'); |
| } |
| // Note that this triggers the daemon to shutdown as we listen for the |
| // tabConnection to close to initiate a shutdown. |
| await appState._debugConnection?.close(); |
| // Wait for the daemon to gracefully shutdown before sending success. |
| await daemon.onExit; |
| return true; |
| } |
| |
| @override |
| void dispose() { |
| _isShutdown = true; |
| for (var state in _appStates.values) { |
| state.dispose(); |
| } |
| _appStates.clear(); |
| } |
| } |
| |
| class _AppState { |
| final DebugConnection _debugConnection; |
| final StreamSubscription<BuildResult> _resultSub; |
| final StreamSubscription<Event> _stdOutSub; |
| |
| bool _isDisposed = false; |
| |
| VmService get vmService => _debugConnection?.vmService; |
| |
| _AppState(this._debugConnection, this._resultSub, this._stdOutSub); |
| |
| void dispose() { |
| if (_isDisposed) return; |
| _isDisposed = true; |
| _stdOutSub?.cancel(); |
| _resultSub?.cancel(); |
| _debugConnection?.close(); |
| } |
| } |