blob: 5be319620b13be8d2a40d190ec768bafe3e93fc2 [file] [log] [blame]
// 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 'package:build_daemon/client.dart';
import 'package:dwds/dwds.dart';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vmservice;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
hide StackTrace;
import '../application_package.dart';
import '../base/async_guard.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../base/os.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../convert.dart';
import '../devfs.dart';
import '../device.dart';
import '../features.dart';
import '../globals.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../resident_runner.dart';
import '../run_hot.dart';
import '../web/chrome.dart';
import '../web/devfs_web.dart';
import '../web/web_device.dart';
import '../web/web_runner.dart';
import 'web_fs.dart';
/// Injectable factory to create a [ResidentWebRunner].
class DwdsWebRunnerFactory extends WebRunnerFactory {
@override
ResidentRunner createWebRunner(
FlutterDevice device, {
String target,
@required bool stayResident,
@required FlutterProject flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions,
@required List<String> dartDefines,
@required UrlTunneller urlTunneller,
}) {
if (featureFlags.isWebIncrementalCompilerEnabled && debuggingOptions.buildInfo.isDebug) {
return _ExperimentalResidentWebRunner(
device,
target: target,
flutterProject: flutterProject,
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: stayResident,
dartDefines: dartDefines,
// TODO(dantup): If this becomes default it may need to urlTunneller.
);
}
return _DwdsResidentWebRunner(
device,
target: target,
flutterProject: flutterProject,
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: stayResident,
dartDefines: dartDefines,
urlTunneller: urlTunneller,
);
}
}
/// A hot-runner which handles browser specific delegation.
abstract class ResidentWebRunner extends ResidentRunner {
ResidentWebRunner(
this.device, {
String target,
@required this.flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions,
bool stayResident = true,
@required this.dartDefines,
}) : super(
<FlutterDevice>[],
target: target ?? fs.path.join('lib', 'main.dart'),
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: stayResident,
);
final FlutterDevice device;
final FlutterProject flutterProject;
final List<String> dartDefines;
DateTime firstBuildTime;
// Only the debug builds of the web support the service protocol.
@override
bool get supportsServiceProtocol => isRunningDebug && deviceIsDebuggable;
@override
bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable;
/// WebServer device is debuggable when running with --start-paused.
bool get deviceIsDebuggable => device.device is! WebServerDevice || debuggingOptions.startPaused;
bool get _enableDwds => debuggingEnabled;
WebFs _webFs;
ConnectionResult _connectionResult;
StreamSubscription<vmservice.Event> _stdOutSub;
bool _exited = false;
WipConnection _wipConnection;
vmservice.VmService get _vmService =>
_connectionResult?.debugConnection?.vmService;
@override
bool get canHotRestart {
return true;
}
@override
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate(
String method, {
Map<String, dynamic> params,
}) async {
final vmservice.Response response =
await _vmService.callServiceExtension(method, args: params);
return response.toJson();
}
@override
Future<void> cleanupAfterSignal() async {
await _cleanup();
}
@override
Future<void> cleanupAtFinish() async {
await _cleanup();
}
Future<void> _cleanup() async {
if (_exited) {
return;
}
await _stdOutSub?.cancel();
await _webFs?.stop();
await device.device.stopApp(null);
if (ChromeLauncher.hasChromeInstance) {
final Chrome chrome = await ChromeLauncher.connectedInstance;
await chrome.close();
}
_exited = true;
}
Future<void> _cleanupAndExit() async {
await _cleanup();
appFinished();
}
@override
void printHelp({bool details = true}) {
if (details) {
return printHelpDetails();
}
const String fire = '🔥';
const String rawMessage =
' To hot restart changes while running, press "r". '
'To hot restart (and refresh the browser), press "R".';
final String message = terminal.color(
fire + terminal.bolden(rawMessage),
TerminalColor.red,
);
printStatus(
'Warning: Flutter\'s support for web development is not stable yet and hasn\'t');
printStatus('been thoroughly tested in production environments.');
printStatus('For more information see https://flutter.dev/web');
printStatus('');
printStatus(message);
const String quitMessage = 'To quit, press "q".';
printStatus('For a more detailed help message, press "h". $quitMessage');
}
@override
Future<void> debugDumpApp() async {
try {
await _vmService?.callServiceExtension(
'ext.flutter.debugDumpApp',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpRenderTree() async {
try {
await _vmService?.callServiceExtension(
'ext.flutter.debugDumpRenderTree',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpLayerTree() async {
try {
await _vmService?.callServiceExtension(
'ext.flutter.debugDumpLayerTree',
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
try {
await _vmService?.callServiceExtension(
'ext.flutter.debugDumpSemanticsTreeInTraversalOrder');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugTogglePlatform() async {
try {
final vmservice.Response response = await _vmService
?.callServiceExtension('ext.flutter.platformOverride');
final String currentPlatform = response.json['value'] as String;
String nextPlatform;
switch (currentPlatform) {
case 'android':
nextPlatform = 'iOS';
break;
case 'iOS':
nextPlatform = 'android';
break;
}
if (nextPlatform == null) {
return;
}
await _vmService?.callServiceExtension('ext.flutter.platformOverride',
args: <String, Object>{
'value': nextPlatform,
});
printStatus('Switched operating system to $nextPlatform');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
try {
await _vmService?.callServiceExtension(
'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder');
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleDebugPaintSizeEnabled() async {
try {
final vmservice.Response response =
await _vmService?.callServiceExtension(
'ext.flutter.debugPaint',
);
await _vmService?.callServiceExtension(
'ext.flutter.debugPaint',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleDebugCheckElevationsEnabled() async {
try {
final vmservice.Response response =
await _vmService?.callServiceExtension(
'ext.flutter.debugCheckElevationsEnabled',
);
await _vmService?.callServiceExtension(
'ext.flutter.debugCheckElevationsEnabled',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugTogglePerformanceOverlayOverride() async {
try {
final vmservice.Response response = await _vmService
?.callServiceExtension('ext.flutter.showPerformanceOverlay');
await _vmService?.callServiceExtension(
'ext.flutter.showPerformanceOverlay',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleWidgetInspector() async {
try {
final vmservice.Response response = await _vmService
?.callServiceExtension('ext.flutter.debugToggleWidgetInspector');
await _vmService?.callServiceExtension(
'ext.flutter.debugToggleWidgetInspector',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError {
return;
}
}
@override
Future<void> debugToggleProfileWidgetBuilds() async {
try {
final vmservice.Response response = await _vmService
?.callServiceExtension('ext.flutter.profileWidgetBuilds');
await _vmService?.callServiceExtension(
'ext.flutter.profileWidgetBuilds',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError {
return;
}
}
}
class _ExperimentalResidentWebRunner extends ResidentWebRunner {
_ExperimentalResidentWebRunner(
FlutterDevice device, {
String target,
@required FlutterProject flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions,
bool stayResident = true,
@required List<String> dartDefines,
}) : super(
device,
flutterProject: flutterProject,
target: target ?? fs.path.join('lib', 'main.dart'),
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: stayResident,
dartDefines: dartDefines,
);
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
}) async {
firstBuildTime = DateTime.now();
final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
TargetPlatform.web_javascript,
applicationBinary: null,
);
if (package == null) {
printError('This application is not configured to build on the web.');
printError('To add web support to a project, run `flutter create .`.');
return 1;
}
if (!fs.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.';
if (target == null) {
message +=
'\nConsider using the -t option to specify the Dart file to start.';
}
printError(message);
return 1;
}
final String modeName = debuggingOptions.buildInfo.friendlyModeName;
printStatus('Launching ${getDisplayPath(target)} on ${device.device.name} in $modeName mode...');
final String effectiveHostname = debuggingOptions.hostname ?? 'localhost';
final int hostPort = debuggingOptions.port == null
? await os.findFreePort()
: int.tryParse(debuggingOptions.port);
device.devFS = WebDevFS(
effectiveHostname,
hostPort,
packagesFilePath,
);
await device.devFS.create();
await _updateDevFS(fullRestart: true);
device.generator.accept();
await device.device.startApp(
package,
mainPath: target,
debuggingOptions: debuggingOptions,
platformArgs: <String, Object>{
'uri': 'http://$effectiveHostname:$hostPort',
},
);
return attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
);
}
@override
Future<OperationResult> restart({
bool fullRestart = false,
bool pause = false,
String reason,
bool benchmarkMode = false,
}) async {
final Stopwatch timer = Stopwatch()..start();
final Status status = logger.startProgress(
'Performing hot restart...',
timeout: supportsServiceProtocol
? timeoutConfiguration.fastOperation
: timeoutConfiguration.slowOperation,
progressId: 'hot.restart',
);
final UpdateFSReport report = await _updateDevFS(fullRestart: fullRestart);
if (report.success) {
device.generator.accept();
} else {
await device.generator.reject();
}
final String modules = report.invalidatedModules
.map((String module) => '"$module"')
.join(',');
try {
if (fullRestart) {
await _wipConnection?.sendCommand('Page.reload');
} else {
await _wipConnection?.debugger
?.sendCommand('Runtime.evaluate', params: <String, Object>{
'expression': 'window.\$hotReloadHook([$modules])',
'awaitPromise': true,
'returnByValue': true,
});
}
} on WipError catch (err) {
printError(err.toString());
return OperationResult(1, err.toString());
} finally {
status.stop();
}
final String verb = fullRestart ? 'Restarted' : 'Reloaded';
printStatus('$verb application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
if (!fullRestart) {
flutterUsage.sendTiming('hot', 'web-incremental-restart', timer.elapsed);
}
HotEvent(
'restart',
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
sdkName: await device.device.sdkNameAndVersion,
emulator: false,
fullRestart: true,
reason: reason,
).send();
return OperationResult.ok;
}
Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
final bool isFirstUpload = !assetBundle.wasBuiltOnce();
final bool rebuildBundle = assetBundle.needsBuild();
if (rebuildBundle) {
printTrace('Updating assets');
final int result = await assetBundle.build();
if (result != 0) {
return UpdateFSReport(success: false);
}
}
final List<Uri> invalidatedFiles =
await projectFileInvalidator.findInvalidated(
lastCompiled: device.devFS.lastCompiled,
urisToMonitor: device.devFS.sources,
packagesPath: packagesFilePath,
);
final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.device.name}...',
timeout: timeoutConfiguration.fastOperation,
);
final UpdateFSReport report = await device.devFS.update(
mainPath: mainPath,
target: target,
bundle: assetBundle,
firstBuildTime: firstBuildTime,
bundleFirstUpload: isFirstUpload,
generator: device.generator,
fullRestart: fullRestart,
dillOutputPath: dillOutputPath,
projectRootPath: projectRootPath,
pathToReload: getReloadPath(fullRestart: fullRestart),
invalidatedFiles: invalidatedFiles,
trackWidgetCreation: true,
);
devFSStatus.stop();
printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
return report;
}
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async {
if (device.device is ChromeDevice) {
final Chrome chrome = await ChromeLauncher.connectedInstance;
final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
return chromeTab.url.contains(debuggingOptions.hostname);
});
_wipConnection = await chromeTab.connect();
}
appStartedCompleter?.complete();
connectionInfoCompleter?.complete();
if (stayResident) {
await waitForAppToFinish();
} else {
await stopEchoingDeviceLog();
await exitApp();
}
await cleanupAtFinish();
return 0;
}
}
class _DwdsResidentWebRunner extends ResidentWebRunner {
_DwdsResidentWebRunner(
FlutterDevice device, {
String target,
@required FlutterProject flutterProject,
@required bool ipv6,
@required DebuggingOptions debuggingOptions,
@required this.urlTunneller,
bool stayResident = true,
@required List<String> dartDefines,
}) : super(
device,
flutterProject: flutterProject,
target: target ?? fs.path.join('lib', 'main.dart'),
debuggingOptions: debuggingOptions,
ipv6: ipv6,
stayResident: stayResident,
dartDefines: dartDefines,
);
UrlTunneller urlTunneller;
@override
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
}) async {
firstBuildTime = DateTime.now();
final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
TargetPlatform.web_javascript,
applicationBinary: null,
);
if (package == null) {
printError('This application is not configured to build on the web.');
printError('To add web support to a project, run `flutter create .`.');
return 1;
}
if (!fs.isFileSync(mainPath)) {
String message = 'Tried to run $mainPath, but that file does not exist.';
if (target == null) {
message +=
'\nConsider using the -t option to specify the Dart file to start.';
}
printError(message);
return 1;
}
final String modeName = debuggingOptions.buildInfo.friendlyModeName;
printStatus(
'Launching ${getDisplayPath(target)} on ${device.device.name} in $modeName mode...');
Status buildStatus;
bool statusActive = false;
try {
// dwds does not handle uncaught exceptions from its servers. To work
// around this, we need to catch all uncaught exceptions and determine if
// they are fatal or not.
buildStatus = logger.startProgress('Building application for the web...', timeout: null);
statusActive = true;
final int result = await asyncGuard(() async {
_webFs = await webFsFactory(
target: target,
flutterProject: flutterProject,
buildInfo: debuggingOptions.buildInfo,
initializePlatform: debuggingOptions.initializePlatform,
hostname: debuggingOptions.hostname,
port: debuggingOptions.port,
urlTunneller: urlTunneller,
skipDwds: !_enableDwds,
dartDefines: dartDefines,
);
// When connecting to a browser, update the message with a seemsSlow notification
// to handle the case where we fail to connect.
buildStatus.stop();
statusActive = false;
if (supportsServiceProtocol) {
buildStatus = logger.startProgress(
'Attempting to connect to browser instance..',
timeout: const Duration(seconds: 30),
);
statusActive = true;
}
await device.device.startApp(
package,
mainPath: target,
debuggingOptions: debuggingOptions,
platformArgs: <String, Object>{
'uri': _webFs.uri,
},
);
if (_enableDwds) {
final bool useDebugExtension = device.device is WebServerDevice && debuggingOptions.startPaused;
_connectionResult = await _webFs.connect(useDebugExtension);
unawaited(_connectionResult.debugConnection.onDone.whenComplete(_cleanupAndExit));
}
if (statusActive) {
buildStatus.stop();
statusActive = false;
}
appStartedCompleter?.complete();
return attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
);
});
return result;
} on VersionSkew {
// Thrown if an older build daemon is already running.
throwToolExit(
'Another build daemon is already running with an older version.\n'
'Try exiting other Flutter processes in this project and try again.');
} on OptionsSkew {
// Thrown if a build daemon is already running with different configuration.
throwToolExit(
'Another build daemon is already running with different configuration.\n'
'Exit other Flutter processes running in this project and try again.');
} on WebSocketException {
throwToolExit('Failed to connect to WebSocket.');
} on BuildException {
throwToolExit('Failed to build application for the Web.');
} on ChromeDebugException catch (err, stackTrace) {
throwToolExit(
'Failed to establish connection with Chrome. Try running the application again.\n'
'If this problem persists, please file an issue with the details below:\n$err\n$stackTrace');
} on AppConnectionException {
throwToolExit(
'Failed to establish connection with the application instance in Chrome.\n'
'This can happen if the websocket connection used by the web tooling is '
'unabled to correctly establish a connection, for example due to a firewall.');
} on MissingPortFile {
throwToolExit(
'Failed to connect to build daemon.\nThe daemon either failed to '
'start or was killed by another process.');
} on SocketException catch (err) {
throwToolExit(err.toString());
} on StateError catch (err) {
final String message = err.toString();
if (message.contains('Unable to start build daemon')) {
throwToolExit('Failed to start build daemon. The process might have '
'exited unexpectedly during startup. Try running the application '
'again.');
}
rethrow;
} finally {
if (statusActive) {
buildStatus.stop();
}
}
return 1;
}
@override
Future<OperationResult> restart({
bool fullRestart = false,
bool pause = false,
String reason,
bool benchmarkMode = false,
}) async {
final Stopwatch timer = Stopwatch()..start();
final Status status = logger.startProgress(
'Performing hot restart...',
timeout: supportsServiceProtocol
? timeoutConfiguration.fastOperation
: timeoutConfiguration.slowOperation,
progressId: 'hot.restart',
);
final bool success = await _webFs.recompile();
if (!success) {
status.stop();
return OperationResult(1, 'Failed to recompile application.');
}
if (supportsServiceProtocol) {
// Send an event for only recompilation.
final Duration recompileDuration = timer.elapsed;
flutterUsage.sendTiming('hot', 'web-recompile', recompileDuration);
try {
final vmservice.Response reloadResponse = fullRestart
? await _vmService.callServiceExtension('fullReload')
: await _vmService.callServiceExtension('hotRestart');
final String verb = fullRestart ? 'Restarted' : 'Reloaded';
printStatus(
'$verb application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
// Send timing analytics for full restart and for refresh.
final bool wasSuccessful = reloadResponse.type == 'Success';
if (!wasSuccessful) {
return OperationResult(1, reloadResponse.toString());
}
if (!fullRestart) {
flutterUsage.sendTiming('hot', 'web-restart', timer.elapsed);
flutterUsage.sendTiming('hot', 'web-refresh', timer.elapsed - recompileDuration);
}
return OperationResult.ok;
} on vmservice.RPCError {
return OperationResult(1, 'Page requires refresh.');
} finally {
status.stop();
HotEvent(
'restart',
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
sdkName: await device.device.sdkNameAndVersion,
emulator: false,
fullRestart: true,
reason: reason,
).send();
}
}
// Allows browser refresh hot restart on non-debug builds.
if (device.device is ChromeDevice && !isRunningDebug) {
try {
final Chrome chrome = await ChromeLauncher.connectedInstance;
final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
return chromeTab.url.contains(debuggingOptions.hostname);
});
final WipConnection wipConnection = await chromeTab.connect();
await wipConnection.sendCommand('Page.reload');
status.stop();
return OperationResult.ok;
} catch (err) {
// Ignore error and continue with posted message;
}
}
status.stop();
printStatus('Recompile complete. Page requires refresh.');
return OperationResult.ok;
}
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
}) async {
Uri websocketUri;
if (supportsServiceProtocol) {
// Cleanup old subscriptions. These will throw if there isn't anything
// listening, which is fine because that is what we want to ensure.
try {
await _vmService.streamCancel('Stdout');
} on vmservice.RPCError {
// It is safe to ignore this error because we expect an error to be
// thrown if we're not already subscribed.
}
try {
await _vmService.streamListen('Stdout');
} on vmservice.RPCError {
// It is safe to ignore this error because we expect an error to be
// thrown if we're not already subscribed.
}
_stdOutSub = _vmService.onStdoutEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes)).trim();
printStatus(message);
});
unawaited(_vmService.registerService('reloadSources', 'FlutterTools'));
websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
// Always run main after connecting because start paused doesn't work yet.
if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
_connectionResult.appConnection.runMain();
} else {
StreamSubscription<void> resumeSub;
resumeSub = _connectionResult.debugConnection.vmService.onDebugEvent
.listen((vmservice.Event event) {
if (event.type == vmservice.EventKind.kResume) {
_connectionResult.appConnection.runMain();
resumeSub.cancel();
}
});
}
}
if (websocketUri != null) {
printStatus('Debug service listening on $websocketUri');
}
connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
if (stayResident) {
await waitForAppToFinish();
} else {
await stopEchoingDeviceLog();
await exitApp();
}
await cleanupAtFinish();
return 0;
}
}