Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 61 additions & 123 deletions webdev/lib/src/command/build_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,85 +3,20 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:io' show Directory;

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart';
import 'package:logging/logging.dart';

import '../daemon_client.dart';
import '../logging.dart';
import 'configuration.dart';
import 'shared.dart';

const _bootstrapScript = r'''
import 'dart:io';
import 'dart:isolate';

import 'package:build_runner/build_script_generate.dart';
import 'package:path/path.dart' as p;

void main(List<String> args, [SendPort sendPort]) async {
var buildScript = await generateBuildScript();
var scriptFile = new File(scriptLocation)..createSync(recursive: true);
scriptFile.writeAsStringSync(buildScript);
sendPort.send(p.absolute(scriptLocation));
}
''';
const _packagesFileName = '.packages';

Future<Uri> _buildRunnerScript() async {
var packagesFile = File(_packagesFileName);
if (!packagesFile.existsSync()) {
throw FileSystemException(
'A `$_packagesFileName` file does not exist in the target directory.',
packagesFile.absolute.path);
}

var dataUri = Uri.dataFromString(_bootstrapScript);

var messagePort = ReceivePort();
var exitPort = ReceivePort();
var errorPort = ReceivePort();

try {
await Isolate.spawnUri(dataUri, [], messagePort.sendPort,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true,
packageConfig: Uri.file(_packagesFileName));

var allErrorsFuture = errorPort.forEach((error) {
var errorList = error as List;
var message = errorList[0] as String;
var stack = StackTrace.fromString(errorList[1] as String);

stderr.writeln(message);
stderr.writeln(stack);
});

var items = await Future.wait([
messagePort.toList(),
allErrorsFuture,
exitPort.first.whenComplete(() {
messagePort.close();
errorPort.close();
})
]);

var messages = items[0] as List;
if (messages.isEmpty) {
throw StateError('An error occurred while bootstrapping.');
}

assert(messages.length == 1);
return Uri.file(messages.single as String);
} finally {
messagePort.close();
exitPort.close();
errorPort.close();
}
}

/// Command to execute pub run build_runner build.
class BuildCommand extends Command<int> {
@override
Expand All @@ -98,71 +33,74 @@ class BuildCommand extends Command<int> {
}

@override
Future<int> run() {
Future<int> run() async {
if (argResults.rest.isNotEmpty) {
throw UsageException(
'Arguments were provided that are not supported: '
'"${argResults.rest.join(' ')}".',
argParser.usage);
}
return runCore('build', extraArgs: ['--fail-on-severe']);
}

Future<int> runCore(String command, {List<String> extraArgs}) async {
var configuration = Configuration.fromArgs(argResults);
setVerbosity(configuration.verbose);
var pubspecLock = await readPubspecLock(configuration);
final arguments = [command]
..addAll(extraArgs ?? const [])
..addAll(buildRunnerArgs(pubspecLock, configuration));

stdout.write('Creating build script');
var stopwatch = Stopwatch()..start();
var buildRunnerScript = await _buildRunnerScript();
stdout.writeln(', took ${stopwatch.elapsedMilliseconds}ms');

var exitCode = 0;

// Heavily inspired by dart-lang/build @ 0c77443dd7
// /build_runner/bin/build_runner.dart#L58-L85
var exitPort = ReceivePort();
var errorPort = ReceivePort();
var messagePort = ReceivePort();
var errorListener = errorPort.listen((e) {
stderr.writeln('\n\nYou have hit a bug in build_runner');
stderr.writeln('Please file an issue with reproduction steps at '
'https://github.com/dart-lang/build/issues\n\n');
final error = e[0];
final trace = e[1] as String;
stderr.writeln(error);
stderr.writeln(Trace.parse(trace).terse);
if (exitCode == 0) exitCode = 1;
});
final arguments = buildRunnerArgs(pubspecLock, configuration);

try {
await Isolate.spawnUri(buildRunnerScript, arguments, messagePort.sendPort,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
automaticPackageResolution: true);
StreamSubscription exitCodeListener;
exitCodeListener = messagePort.listen((isolateExitCode) {
if (isolateExitCode is! int) {
throw StateError(
'Bad response from isolate, expected an exit code but got '
'$isolateExitCode');
logHandler(Level.INFO, 'Connecting to the build daemon...');
var client = await connectClient(
Directory.current.path,
arguments,
(serverLog) {
var recordLevel = levelForLog(serverLog) ?? Level.INFO;
logHandler(recordLevel, trimLevel(recordLevel, serverLog.log));
},
);
OutputLocation outputLocation;
if (configuration.outputPath != null) {
outputLocation = OutputLocation((b) => b
..output = configuration.outputPath
..useSymlinks = false
..hoist = configuration.outputInput.isNotEmpty);
}
client.registerBuildTarget(DefaultBuildTarget((b) => b
..target = configuration.outputInput
..outputLocation = outputLocation?.toBuilder()));
client.startBuild();
var exitCode = 0;
var gotBuildStart = false;
await for (final result in client.buildResults) {
var targetResult = result.results.firstWhere(
(buildResult) => buildResult.target == configuration.outputInput,
orElse: () => null);
if (targetResult == null) continue;
// We ignore any builds that happen before we get a `started` event,
// because those could be stale (from some other client).
gotBuildStart =
gotBuildStart || targetResult.status == BuildStatus.started;
if (!gotBuildStart) continue;

// Shouldn't happen, but being a bit defensive here.
if (targetResult.status == BuildStatus.started) continue;

if (targetResult.status == BuildStatus.failed) {
exitCode = 1;
}
exitCode = isolateExitCode as int;
exitCodeListener.cancel();
exitCodeListener = null;
});
await exitPort.first;
await errorListener.cancel();
await exitCodeListener?.cancel();

if (targetResult.error?.isNotEmpty == true) {
logHandler(Level.SEVERE, targetResult.error);
}
break;
}
await client.close();
return exitCode;
} finally {
exitPort.close();
errorPort.close();
messagePort.close();
} on OptionsSkew catch (_) {
logHandler(
Level.SEVERE,
'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.\n\n');
return 1;
}
}
}
10 changes: 5 additions & 5 deletions webdev/lib/src/command/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import 'package:args/args.dart';
import 'package:logging/logging.dart';

import '../logging.dart';
import '../serve/injected/configuration.dart';
import '../serve/logging.dart';

const autoOption = 'auto';
const chromeDebugPortFlag = 'chrome-debug-port';
Expand Down Expand Up @@ -169,12 +169,12 @@ class Configuration {
String outputInput;
if (output != 'NONE') {
var splitOutput = output.split(':');
if (splitOutput.length == 2) {
outputInput = splitOutput.first;
outputPath = splitOutput.last;
} else {
if (splitOutput.length == 1) {
outputInput = '';
outputPath = output;
} else {
outputInput = splitOutput.first;
outputPath = splitOutput.skip(1).join(':');
}
}

Expand Down
5 changes: 2 additions & 3 deletions webdev/lib/src/command/daemon_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import 'package:pedantic/pedantic.dart';
import '../daemon/app_domain.dart';
import '../daemon/daemon.dart';
import '../daemon/daemon_domain.dart';
import '../logging.dart';
import '../serve/dev_workflow.dart';
import '../serve/logging.dart';
import '../serve/utils.dart';
import 'configuration.dart';
import 'shared.dart';
Expand Down Expand Up @@ -63,8 +63,7 @@ class DaemonCommand extends Command<int> {
var configuration =
Configuration(launchInChrome: true, debug: true, autoRun: false);
var pubspecLock = await readPubspecLock(configuration);
var buildOptions =
buildRunnerArgs(pubspecLock, configuration, includeOutput: false);
var buildOptions = buildRunnerArgs(pubspecLock, configuration);
var port = await findUnusedPort();
workflow = await DevWorkflow.start(
configuration,
Expand Down
11 changes: 5 additions & 6 deletions webdev/lib/src/command/serve_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';

import '../logging.dart';
import '../serve/dev_workflow.dart';
import '../serve/logging.dart';
import 'configuration.dart';
import 'shared.dart';

Expand Down Expand Up @@ -102,11 +102,10 @@ refresh: Performs a full page refresh.
var pubspecLock = await readPubspecLock(configuration);
// Forward remaining arguments as Build Options to the Daemon.
// This isn't documented. Should it be advertised?
var buildOptions =
buildRunnerArgs(pubspecLock, configuration, includeOutput: false)
..addAll(argResults.rest
.where((arg) => !arg.contains(':') || arg.startsWith('--'))
.toList());
var buildOptions = buildRunnerArgs(pubspecLock, configuration)
..addAll(argResults.rest
.where((arg) => !arg.contains(':') || arg.startsWith('--'))
.toList());
var directoryArgs = argResults.rest
.where((arg) => arg.contains(':') || !arg.startsWith('--'))
.toList();
Expand Down
10 changes: 1 addition & 9 deletions webdev/lib/src/command/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,12 @@ void addSharedArgs(ArgParser argParser,
/// Parses the provided [Configuration] to return a list of
/// `package:build_runner` appropriate arguments.
List<String> buildRunnerArgs(
PubspecLock pubspecLock, Configuration configuration,
{bool includeOutput}) {
includeOutput ??= true;
PubspecLock pubspecLock, Configuration configuration) {
var arguments = <String>[];
if (configuration.release) {
arguments.add('--$releaseFlag');
}

if (includeOutput &&
configuration.output != null &&
configuration.output != outputNone) {
arguments.addAll(['--$outputFlag', configuration.output]);
}

if (configuration.verbose) {
arguments.add('--$verboseFlag');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import 'dart:io';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/constants.dart';
import 'package:build_daemon/data/server_log.dart';
import 'package:webdev/src/util.dart';

import 'util.dart';

/// Connects to the `build_runner` daemon.
Future<BuildDaemonClient> connectClient(String workingDirectory,
Expand Down
22 changes: 22 additions & 0 deletions webdev/lib/src/serve/logging.dart → webdev/lib/src/logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:io';

import 'package:io/ansi.dart';
import 'package:build_daemon/data/server_log.dart';
import 'package:logging/logging.dart';

var _verbose = false;
Expand Down Expand Up @@ -46,3 +47,24 @@ void _colorLog(Level level, String message, {bool verbose}) {
stdout.writeln('');
}
}

/// Trims [level] from [message] if it is prefixed by it.
String trimLevel(Level level, String message) => message.startsWith('[$level]')
? message.replaceFirst('[$level]', '').trimLeft()
: message;

/// Detects if the [ServerLog] contains a [Level] and returns the
/// resulting value.
///
/// If the [ServerLog] does not contain a [Level], null will be returned.
Level levelForLog(ServerLog serverLog) {
var log = serverLog.log;
Level recordLevel;
for (var level in Level.LEVELS) {
if (log.startsWith('[$level]')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to end up building this string over and over. I wonder if we want something like:

final levelsByLabel = Map.fromIterable(Level.LEVELS, key: (l) => '[$l]'); // should be static

...
var messageLabel = levelsByLabel.keys.firstWhere(log.startsWith);
return messageLabel == null ? null : levelsByLabel[messageLabel];

Then we can look forward to writing it instead as

var levelsByLabel = {for (var l in Level.LEVELS) '[$l]': l};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved these utilities around in this pr since they aren't specific to the serve command any more, we could follow up with something but I think we have a lot of other much slower things going on in the logging than this :D.

Also probably a switch statement or something would probably be faster than a map for that if we are concerned about perf?

recordLevel = level;
break;
}
}
return recordLevel;
}
19 changes: 8 additions & 11 deletions webdev/lib/src/serve/dev_workflow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@ import 'package:build_daemon/data/build_target.dart';
import 'package:logging/logging.dart';

import '../command/configuration.dart';
import '../serve/chrome.dart';
import '../serve/daemon_client.dart';
import '../serve/debugger/devtools.dart';
import '../serve/logging.dart';
import '../serve/server_manager.dart';
import '../serve/utils.dart';
import '../serve/webdev_server.dart';
import '../daemon_client.dart';
import '../logging.dart';
import 'chrome.dart';
import 'debugger/devtools.dart';
import 'server_manager.dart';
import 'webdev_server.dart';

Future<BuildDaemonClient> _startBuildDaemon(
String workingDirectory,
List<String> buildOptions,
) async {
logHandler(Level.INFO, 'Connecting to the build daemon...');
String workingDirectory, List<String> buildOptions) async {
try {
logHandler(Level.INFO, 'Connecting to the build daemon...');
return await connectClient(
workingDirectory,
buildOptions,
Expand Down
Loading