Unverified Commit f614144f authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add "web" server device to allow running flutter for web on arbitrary browsers (#39951)

* add web server device

* remove extra async

* fixes to server device

* testing updates

* fix test cases

* address comments
parent a7aff567
......@@ -43,6 +43,7 @@ dart_library("flutter_tools") {
"//third_party/dart-pkg/pub/quiver",
"//third_party/dart-pkg/pub/shelf_packages_handler",
"//third_party/dart-pkg/pub/shelf_static",
"//third_party/dart-pkg/pub/shelf_proxy",
"//third_party/dart-pkg/pub/stack_trace",
"//third_party/dart-pkg/pub/test",
"//third_party/dart-pkg/pub/usage",
......
......@@ -20,6 +20,7 @@ import '../device.dart';
import '../globals.dart';
import '../project.dart';
import '../resident_runner.dart';
import '../web/web_device.dart';
import '../web/web_runner.dart';
import 'web_fs.dart';
......@@ -63,7 +64,10 @@ class ResidentWebRunner extends ResidentRunner {
// Only the debug builds of the web support the service protocol.
@override
bool get supportsServiceProtocol => isRunningDebug;
bool get supportsServiceProtocol => isRunningDebug && device is! WebServerDevice;
@override
bool get debuggingEnabled => isRunningDebug && device is! WebServerDevice;
WebFs _webFs;
DebugConnection _debugConnection;
......@@ -103,6 +107,7 @@ class ResidentWebRunner extends ResidentRunner {
await _debugConnection?.close();
await _stdOutSub?.cancel();
await _webFs?.stop();
await device.stopApp(null);
_exited = true;
}
......@@ -162,7 +167,11 @@ class ResidentWebRunner extends ResidentRunner {
target: target,
flutterProject: flutterProject,
buildInfo: debuggingOptions.buildInfo,
skipDwds: true,
);
await device.startApp(package, mainPath: target, platformArgs: <String, Object>{
'uri': _webFs.uri
});
if (supportsServiceProtocol) {
_debugConnection = await _webFs.runAndDebug();
unawaited(_debugConnection.onDone.whenComplete(exit));
......@@ -246,15 +255,13 @@ class ResidentWebRunner extends ResidentRunner {
? OperationResult.ok
: OperationResult(1, reloadResponse.toString());
} on vmservice.RPCError {
await _webFs.hardRefresh();
return OperationResult(1, 'Page requires full reload');
return OperationResult(1, 'Page requires refresh.');
} finally {
status.stop();
}
}
// If we're not in hot mode, the only way to restart is to reload the tab.
await _webFs.hardRefresh();
status.stop();
printStatus('Recompile complete. Page requires refresh.');
return OperationResult.ok;
}
......
......@@ -16,7 +16,7 @@ import 'package:http_multi_server/http_multi_server.dart';
import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace;
import 'package:shelf_proxy/shelf_proxy.dart';
import '../artifacts.dart';
import '../asset.dart';
......@@ -74,6 +74,7 @@ typedef WebFsFactory = Future<WebFs> Function({
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo,
@required bool skipDwds,
});
/// The dev filesystem responsible for building and serving web applications.
......@@ -83,12 +84,14 @@ class WebFs {
this._client,
this._server,
this._dwds,
this._chrome,
this.uri,
);
/// The server uri.
final String uri;
final HttpServer _server;
final Dwds _dwds;
final Chrome _chrome;
final BuildDaemonClient _client;
StreamSubscription<void> _connectedApps;
......@@ -96,14 +99,13 @@ class WebFs {
Future<void> stop() async {
await _client.close();
await _dwds.stop();
await _dwds?.stop();
await _server.close(force: true);
await _chrome.close();
await _connectedApps?.cancel();
}
/// Retrieve the [DebugConnection] for the current application.
Future<DebugConnection> runAndDebug() async {
Future<DebugConnection> runAndDebug() {
final Completer<DebugConnection> firstConnection = Completer<DebugConnection>();
_connectedApps = _dwds.connectedApps.listen((AppConnection appConnection) async {
appConnection.runMain();
......@@ -115,18 +117,6 @@ class WebFs {
return firstConnection.future;
}
/// Perform a hard refresh of all connected browser tabs.
Future<void> hardRefresh() async {
final List<ChromeTab> tabs = await _chrome.chromeConnection.getTabs();
for (ChromeTab tab in tabs) {
if (!tab.url.contains('localhost')) {
continue;
}
final WipConnection connection = await tab.connect();
await connection.sendCommand('Page.reload');
}
}
/// Recompile the web application and return whether this was successful.
Future<bool> recompile() async {
_client.startBuild();
......@@ -148,7 +138,8 @@ class WebFs {
static Future<WebFs> start({
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo
@required BuildInfo buildInfo,
@required bool skipDwds,
}) async {
// workaround for https://github.com/flutter/flutter/issues/38290
if (!flutterProject.dartTool.existsSync()) {
......@@ -175,21 +166,6 @@ class WebFs {
// Initialize the dwds server.
final int port = await os.findFreePort();
final Dwds dwds = await dwdsFactory(
hostname: _kHostName,
applicationPort: port,
applicationTarget: kBuildTargetName,
assetServerPort: daemonAssetPort,
buildResults: filteredBuildResults,
chromeConnection: () async {
return (await ChromeLauncher.connectedInstance).chromeConnection;
},
reloadConfiguration: ReloadConfiguration.none,
serveDevTools: true,
verbose: false,
enableDebugExtension: true,
logWriter: (dynamic level, String message) => printTrace(message),
);
// Map the bootstrap files to the correct package directory.
final String targetBaseName = fs.path
.withoutExtension(target).replaceFirst('lib${fs.path.separator}', '');
......@@ -203,7 +179,7 @@ class WebFs {
'${targetBaseName}_web_entrypoint.digests': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.digests',
};
final Handler handler = const Pipeline().addMiddleware((Handler innerHandler) {
final Pipeline pipeline = const Pipeline().addMiddleware((Handler innerHandler) {
return (Request request) async {
// Redirect the main.dart.js to the target file we decided to serve.
if (mappedUrls.containsKey(request.url.path)) {
......@@ -222,19 +198,39 @@ class WebFs {
return innerHandler(request);
}
};
})
.addHandler(dwds.handler);
});
Handler handler;
Dwds dwds;
if (!skipDwds) {
dwds = await dwdsFactory(
hostname: _kHostName,
applicationPort: port,
applicationTarget: kBuildTargetName,
assetServerPort: daemonAssetPort,
buildResults: filteredBuildResults,
chromeConnection: () async {
return (await ChromeLauncher.connectedInstance).chromeConnection;
},
reloadConfiguration: ReloadConfiguration.none,
serveDevTools: true,
verbose: false,
enableDebugExtension: true,
logWriter: (dynamic level, String message) => printTrace(message),
);
handler = pipeline.addHandler(dwds.handler);
} else {
handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
}
Cascade cascade = Cascade();
cascade = cascade.add(handler);
cascade = cascade.add(_assetHandler(flutterProject));
final HttpServer server = await httpMultiServerFactory(_kHostName, port);
shelf_io.serveRequests(server, cascade.handler);
final Chrome chrome = await chromeLauncher.launch('http://$_kHostName:$port/');
return WebFs(
client,
server,
dwds,
chrome,
'http://$_kHostName:$port/',
);
}
......@@ -319,7 +315,11 @@ class WebFs {
} else if (request.url.path.contains('assets')) {
final String assetPath = request.url.path.replaceFirst('assets/', '');
final File file = fs.file(fs.path.join(getAssetBuildDirectory(), assetPath));
if (file.existsSync()) {
return Response.ok(file.readAsBytesSync());
} else {
return Response.notFound('');
}
}
return Response.notFound('');
};
......
......@@ -484,7 +484,7 @@ class AppDomain extends Domain {
Completer<DebugConnectionInfo> connectionInfoCompleter;
if (runner.debuggingOptions.debuggingEnabled) {
if (runner.debuggingEnabled) {
connectionInfoCompleter = Completer<DebugConnectionInfo>();
// We don't want to wait for this future to complete and callbacks won't fail.
// As it just writes to stdout.
......
......@@ -573,6 +573,7 @@ abstract class ResidentRunner {
bool hotMode ;
String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.dill';
bool get debuggingEnabled => debuggingOptions.debuggingEnabled;
bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
......
......@@ -12,6 +12,7 @@ import '../base/process_manager.dart';
import '../build_info.dart';
import '../device.dart';
import '../features.dart';
import '../globals.dart';
import '../project.dart';
import 'chrome.dart';
import 'workflow.dart';
......@@ -36,6 +37,9 @@ class ChromeDevice extends Device {
ephemeral: false,
);
/// The active chrome instance.
Chrome _chrome;
// TODO(jonahwilliams): this is technically false, but requires some refactoring
// to allow hot mode restart only devices.
@override
......@@ -112,7 +116,7 @@ class ChromeDevice extends Device {
version = result.stdout;
}
}
return version;
return version.trim();
}
@override
......@@ -127,11 +131,13 @@ class ChromeDevice extends Device {
}) async {
// See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
// for the web initialization and server logic.
_chrome = await chromeLauncher.launch(platformArgs['uri']);
return LaunchResult.succeeded(observatoryUri: null);
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
await _chrome.close();
return true;
}
......@@ -151,6 +157,7 @@ class WebDevices extends PollingDeviceDiscovery {
WebDevices() : super('chrome');
final ChromeDevice _webDevice = ChromeDevice();
final WebServerDevice _webServerDevice = WebServerDevice();
@override
bool get canListAnything => featureFlags.isWebEnabled;
......@@ -159,6 +166,7 @@ class WebDevices extends PollingDeviceDiscovery {
Future<List<Device>> pollingGetDevices() async {
return <Device>[
_webDevice,
_webServerDevice,
];
}
......@@ -170,3 +178,81 @@ class WebDevices extends PollingDeviceDiscovery {
String parseVersionForWindows(String input) {
return input.split(RegExp('\w')).last;
}
/// A special device type to allow serving for arbitrary browsers.
class WebServerDevice extends Device {
WebServerDevice() : super(
'web',
platformType: PlatformType.web,
category: Category.web,
ephemeral: false,
);
@override
void clearLogs() { }
@override
Future<String> get emulatorId => null;
@override
DeviceLogReader getLogReader({ApplicationPackage app}) {
return NoOpDeviceLogReader(app.name);
}
@override
Future<bool> installApp(ApplicationPackage app) async => true;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;
@override
Future<bool> get isLocalEmulator async => false;
@override
bool isSupported() => featureFlags.isWebEnabled;
@override
bool isSupportedForProject(FlutterProject flutterProject) {
return flutterProject.web.existsSync();
}
@override
String get name => 'Server';
@override
DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
@override
Future<String> get sdkNameAndVersion async => 'Flutter Tools';
@override
Future<LaunchResult> startApp(ApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, Object> platformArgs,
bool prebuiltApplication = false,
bool ipv6 = false,
}) async {
final String url = platformArgs['uri'];
printStatus('$mainPath is being served at $url', emphasis: true);
return LaunchResult.succeeded(observatoryUri: null);
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
return true;
}
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.web_javascript;
@override
Future<bool> uninstallApp(ApplicationPackage app) async {
return true;
}
}
......@@ -42,6 +42,7 @@ void main() {
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo,
@required bool skipDwds,
}) async {
return mockWebFs;
},
......@@ -76,7 +77,6 @@ void main() {
when(mockWebFs.recompile()).thenAnswer((Invocation _) async {
return true;
});
when(mockWebFs.hardRefresh()).thenAnswer((Invocation _) async { });
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(result.code, 0);
......
......@@ -15,6 +15,7 @@ import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/build_runner/resident_web_runner.dart';
import 'package:flutter_tools/src/build_runner/web_fs.dart';
import 'package:flutter_tools/src/web/web_device.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart';
......@@ -49,6 +50,7 @@ void main() {
@required String target,
@required FlutterProject flutterProject,
@required BuildInfo buildInfo,
@required bool skipDwds,
}) async {
return mockWebFs;
},
......@@ -75,6 +77,18 @@ void main() {
when(mockDebugConnection.uri).thenReturn('ws://127.0.0.1/abcd/');
}
test('runner with web server device does not support debugging', () => testbed.run(() {
final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
WebServerDevice(),
flutterProject: FlutterProject.current(),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
ipv6: true,
);
expect(profileResidentWebRunner.debuggingEnabled, false);
expect(residentWebRunner.debuggingEnabled, true);
}));
test('profile does not supportsServiceProtocol', () => testbed.run(() {
final ResidentRunner profileResidentWebRunner = ResidentWebRunner(
MockWebDevice(),
......@@ -204,7 +218,7 @@ void main() {
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
expect(result.code, 1);
expect(result.message, contains('Page requires full reload'));
expect(result.message, contains('Page requires refresh'));
}));
test('printHelp without details is spoopy', () => testbed.run(() async {
......@@ -380,7 +394,7 @@ void main() {
}));
}
class MockWebDevice extends Mock implements Device {}
class MockWebDevice extends Mock implements ChromeDevice {}
class MockBuildDaemonCreator extends Mock implements BuildDaemonCreator {}
class MockFlutterWebFs extends Mock implements WebFs {}
class MockDebugConnection extends Mock implements DebugConnection {}
......
......@@ -4,6 +4,7 @@
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/web/chrome.dart';
import 'package:flutter_tools/src/web/web_device.dart';
import 'package:mockito/mockito.dart';
......@@ -13,21 +14,23 @@ import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group(ChromeDevice, () {
MockChromeLauncher mockChromeLauncher;
MockPlatform mockPlatform;
MockProcessManager mockProcessManager;
MockWebApplicationPackage mockWebApplicationPackage;
setUp(() async {
mockWebApplicationPackage = MockWebApplicationPackage();
mockProcessManager = MockProcessManager();
mockChromeLauncher = MockChromeLauncher();
mockPlatform = MockPlatform();
when(mockChromeLauncher.launch(any)).thenAnswer((Invocation invocation) async {
return null;
});
when(mockWebApplicationPackage.name).thenReturn('test');
});
test('Defaults', () async {
test('Chrome defaults', () async {
final ChromeDevice chromeDevice = ChromeDevice();
expect(chromeDevice.name, 'Chrome');
......@@ -38,9 +41,26 @@ void main() {
expect(chromeDevice.supportsFlutterExit, true);
expect(chromeDevice.supportsScreenshot, false);
expect(await chromeDevice.isLocalEmulator, false);
expect(chromeDevice.getLogReader(app: mockWebApplicationPackage), isInstanceOf<NoOpDeviceLogReader>());
expect(await chromeDevice.portForwarder.forward(1), 1);
});
testUsingContext('Invokes version command on non-Windows platforms', () async{
test('Server defaults', () async {
final WebServerDevice device = WebServerDevice();
expect(device.name, 'Server');
expect(device.id, 'web');
expect(device.supportsHotReload, true);
expect(device.supportsHotRestart, true);
expect(device.supportsStartPaused, true);
expect(device.supportsFlutterExit, true);
expect(device.supportsScreenshot, false);
expect(await device.isLocalEmulator, false);
expect(device.getLogReader(app: mockWebApplicationPackage), isInstanceOf<NoOpDeviceLogReader>());
expect(await device.portForwarder.forward(1), 1);
});
testUsingContext('Chrome invokes version command on non-Windows platforms', () async{
when(mockPlatform.isWindows).thenReturn(false);
when(mockProcessManager.canRun('chrome.foo')).thenReturn(true);
when(mockProcessManager.run(<String>['chrome.foo', '--version'])).thenAnswer((Invocation invocation) async {
......@@ -55,7 +75,7 @@ void main() {
ProcessManager: () => mockProcessManager,
});
testUsingContext('Invokes different version command on windows.', () async {
testUsingContext('Chrome invokes different version command on windows.', () async {
when(mockPlatform.isWindows).thenReturn(true);
when(mockProcessManager.canRun('chrome.foo')).thenReturn(true);
when(mockProcessManager.run(<String>[
......@@ -75,7 +95,6 @@ void main() {
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager,
});
});
}
class MockChromeLauncher extends Mock implements ChromeLauncher {}
......@@ -93,3 +112,4 @@ class MockProcessResult extends Mock implements ProcessResult {
@override
final String stdout;
}
class MockWebApplicationPackage extends Mock implements WebApplicationPackage {}
......@@ -79,6 +79,7 @@ void main() {
test('Can create webFs from mocked interfaces', () => testbed.run(() async {
final FlutterProject flutterProject = FlutterProject.current();
await WebFs.start(
skipDwds: false,
target: fs.path.join('lib', 'main.dart'),
buildInfo: BuildInfo.debug,
flutterProject: flutterProject,
......@@ -87,9 +88,6 @@ void main() {
// The build daemon is told to build once.
verify(mockBuildDaemonClient.startBuild()).called(1);
// Chrome is launched based on port from above.
verify(mockChromeLauncher.launch('http://localhost:1234/')).called(1);
// .dart_tool directory is created.
expect(flutterProject.dartTool.existsSync(), true);
}));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment