blob: f22294fba8cae75f0990591f55c9449bc5e90312 [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.
import 'dart:async';
import 'dart:io';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart';
import 'package:build_daemon/data/server_log.dart';
import 'package:logging/logging.dart' as logging;
import 'package:path/path.dart' as p;
import '../command/configuration.dart';
import '../daemon_client.dart';
import '../logging.dart';
import 'chrome.dart';
import 'server_manager.dart';
import 'webdev_server.dart';
Future<BuildDaemonClient> _startBuildDaemon(
String workingDirectory, List<String> buildOptions) async {
try {
logWriter(logging.Level.INFO, 'Connecting to the build daemon...');
return await connectClient(
workingDirectory,
buildOptions,
(serverLog) {
logWriter(toLoggingLevel(serverLog.level), serverLog.message,
loggerName: serverLog.loggerName,
error: serverLog.error,
stackTrace: serverLog.stackTrace);
},
);
} on OptionsSkew {
// TODO(grouma) - Give an option to kill the running daemon.
throw StateError(
'Incompatible options with current running build daemon.\n\n'
'Please stop other WebDev instances running in this directory '
'before starting a new instance with these options.');
}
}
String _uriForLaunchApp(String launchApp, ServerManager serverManager) {
final parts = p.url.split(launchApp);
final dir = parts.first;
final server =
serverManager.servers.firstWhere((server) => server.target == dir);
return Uri(
scheme: 'http',
host: server.host,
port: server.port,
pathSegments: parts.skip(1))
.toString();
}
Future<Chrome?> _startChrome(
Configuration configuration,
ServerManager serverManager,
BuildDaemonClient client,
) async {
final uris = [
if (configuration.launchApps.isEmpty)
for (final s in serverManager.servers)
Uri(scheme: 'http', host: s.host, port: s.port).toString()
else
for (final app in configuration.launchApps)
_uriForLaunchApp(app, serverManager)
];
try {
if (configuration.launchInChrome) {
final userDataDir = configuration.userDataDir == autoOption
? autoDetectChromeUserDataDirectory()
: configuration.userDataDir;
return await Chrome.start(uris,
port: configuration.chromeDebugPort, userDataDir: userDataDir);
} else if (configuration.chromeDebugPort != 0) {
return await Chrome.fromExisting(configuration.chromeDebugPort);
}
} on ChromeError {
await serverManager.stop();
await client.close();
rethrow;
}
return null;
}
Future<ServerManager> _startServerManager(
Configuration configuration,
Map<String, int> targetPorts,
String workingDirectory,
BuildDaemonClient client,
) async {
final assetPort = daemonPort(workingDirectory);
final serverOptions = <ServerOptions>{};
for (final target in targetPorts.keys) {
serverOptions.add(ServerOptions(
configuration,
targetPorts[target]!,
target,
assetPort,
));
}
logWriter(logging.Level.INFO, 'Starting resource servers...');
final serverManager =
await ServerManager.start(serverOptions, client.buildResults);
for (final server in serverManager.servers) {
logWriter(
logging.Level.INFO,
'Serving `${server.target}` on '
'${Uri(scheme: server.protocol, host: server.host, port: server.port)}\n');
}
return serverManager;
}
void _registerBuildTargets(
BuildDaemonClient client,
Configuration configuration,
Map<String, int> targetPorts,
) {
// Register a target for each serve target.
for (final target in targetPorts.keys) {
OutputLocation? outputLocation;
if (configuration.outputPath != null &&
(configuration.outputInput == null ||
target == configuration.outputInput)) {
outputLocation = OutputLocation((b) => b
..output = configuration.outputPath
..useSymlinks = true
..hoist = true);
}
client.registerBuildTarget(DefaultBuildTarget((b) => b
..target = target
..outputLocation = outputLocation?.toBuilder()));
}
// Empty string indicates we should build everything, register a corresponding
// target.
if (configuration.outputInput == '' && configuration.outputPath != null) {
final outputLocation = OutputLocation((b) => b
..output = configuration.outputPath
..useSymlinks = true
..hoist = false);
client.registerBuildTarget(DefaultBuildTarget((b) => b
..target = ''
..outputLocation = outputLocation.toBuilder()));
}
}
/// Controls the web development workflow.
///
/// Connects to the Build Daemon, creates servers, launches Chrome and wires up
/// the DevTools.
class DevWorkflow {
final _doneCompleter = Completer();
final BuildDaemonClient _client;
final Chrome? _chrome;
final ServerManager serverManager;
StreamSubscription? _resultsSub;
final _wrapWidth = stdout.hasTerminal ? stdout.terminalColumns - 8 : 72;
DevWorkflow._(
this._client,
this._chrome,
this.serverManager,
) {
_resultsSub = _client.buildResults.listen((data) {
if (data.results.any((result) =>
result.status == BuildStatus.failed ||
result.status == BuildStatus.succeeded)) {
logWriter(logging.Level.INFO, '${'-' * _wrapWidth}\n');
}
});
_client.shutdownNotifications.listen((data) {
logWriter(logging.Level.WARNING, data.message);
if (data.failureType == 75) {
logWriter(logging.Level.WARNING, 'Please restart WebDev.\n');
}
shutDown();
});
}
Future<void> get done => _doneCompleter.future;
static Future<DevWorkflow> start(
Configuration configuration,
List<String> buildOptions,
Map<String, int> targetPorts,
) async {
final workingDirectory = Directory.current.path;
final client = await _startBuildDaemon(workingDirectory, buildOptions);
logWriter(logging.Level.INFO, 'Registering build targets...');
_registerBuildTargets(client, configuration, targetPorts);
logWriter(logging.Level.INFO, 'Starting initial build...');
client.startBuild();
final serverManager = await _startServerManager(
configuration, targetPorts, workingDirectory, client);
final chrome = await _startChrome(configuration, serverManager, client);
return DevWorkflow._(client, chrome, serverManager);
}
Future<void> shutDown() async {
await _resultsSub?.cancel();
await _chrome?.close();
await _client.close();
await serverManager.stop();
if (!_doneCompleter.isCompleted) _doneCompleter.complete();
}
}