blob: 30169f8316c0592dde89db40b66371e6c976d478 [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.
// @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();
}
}