Skip to content

Commit

Permalink
Initial Build Daemon Support (dart-lang#1949)
Browse files Browse the repository at this point in the history
* Initial Build Daemon Support
  • Loading branch information
grouma authored Jan 4, 2019
1 parent b5de59e commit fe0c72d
Show file tree
Hide file tree
Showing 31 changed files with 2,161 additions and 4 deletions.
12 changes: 11 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,24 @@ jobs:
script: ./tool/travis.sh command_3
env: PKG="build_config"
dart: dev
- stage: analyze_and_format
name: "SDK: dev - DIR: build_daemon - TASKS: [dartfmt -n --set-exit-if-changed ., dartanalyzer --fatal-infos --fatal-warnings .]"
script: ./tool/travis.sh dartfmt dartanalyzer
env: PKG="build_daemon"
dart: dev
- stage: unit_test
name: "SDK: dev - DIR: build_daemon - TASKS: pub run test"
script: ./tool/travis.sh test_06
env: PKG="build_daemon"
dart: dev
- stage: analyze_and_format
name: "SDK: dev - DIR: build_modules - TASKS: [dartfmt -n --set-exit-if-changed ., dartanalyzer --fatal-infos --fatal-warnings .]"
script: ./tool/travis.sh dartfmt dartanalyzer
env: PKG="build_modules"
dart: dev
- stage: unit_test
name: "SDK: dev - DIR: build_modules - TASKS: dart $(pub run build_runner generate-build-script) test --delete-conflicting-outputs-- -P presubmit"
script: ./tool/travis.sh command_4
script: ./tool/travis.sh command_5
env: PKG="build_modules"
dart: dev
- stage: analyze_and_format
Expand Down
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ linter:
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null
#- avoid_returning_null
- avoid_shadowing_type_parameters
- avoid_types_as_parameter_names
- avoid_unused_constructor_parameters
Expand Down
30 changes: 30 additions & 0 deletions build_daemon/bin/build_daemon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:async';

import 'package:build_daemon/constants.dart';
import 'package:build_daemon/src/daemon.dart';
import 'package:build_daemon/daemon_builder.dart';
import 'package:watcher/watcher.dart';

/// Entrypoint for the Dart Build Daemon.
Future main(List<String> args) async {
var workingDirectory = args.first;

var daemon = Daemon(workingDirectory);

if (!daemon.tryGetLock()) {
if (runningVersion(workingDirectory) == currentVersion) {
print('Daemon is already running.');
print(readyToConnectLog);
} else {
print(versionSkew);
}
} else {
print('Starting daemon...');
// TODO(grouma) - Create a real builder for package:build
var builder = DaemonBuilder();
var watcher = Watcher(workingDirectory);
await daemon.start(builder, watcher.events);
print(readyToConnectLog);
await daemon.onDone;
}
}
29 changes: 29 additions & 0 deletions build_daemon/example/example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:io';
import 'dart:math';

import 'package:build_daemon/client.dart';

void main(List<String> args) async {
BuildDaemonClient client;
var workingDirectory = Directory.current.path;
try {
client = await BuildDaemonClient.connect(workingDirectory);
} on VersionSkew {
print('Version skew. Please disconnect all other clients '
'before trying to start a new one.');
exit(1);
}
if (client == null) throw Exception('Error connecting');
print('Connected to Dart Build Daemon');
if (Random().nextBool()) {
client.registerBuildTarget('/some/client/path', [r'.*_test\.dart$']);
client.addBuildOptions(['--define=DART_CHECKS=none']);
print('Registered example client target...');
} else {
client.registerBuildTarget('/some/test/path', []);
print('Registered test target...');
}
client.buildResults.listen((status) => print('BUILD STATUS: $status'));
client.startBuild();
await client.finished;
}
122 changes: 122 additions & 0 deletions build_daemon/lib/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:web_socket_channel/io.dart';

import 'constants.dart';
import 'data/build_options_request.dart';
import 'data/build_request.dart';
import 'data/build_status.dart';
import 'data/build_target_request.dart';
import 'data/log_to_paths_request.dart';
import 'data/serializers.dart';
import 'data/server_log.dart';

class BuildDaemonClient {
IOWebSocketChannel _channel;

final _buildResults = StreamController<BuildResults>.broadcast();

final _serverLogStreamController = StreamController<ServerLog>.broadcast();

BuildDaemonClient._();

Stream<BuildResults> get buildResults => _buildResults.stream;
Future<void> get finished async {
if (_channel != null) await _channel.sink.done;
}

Stream<ServerLog> get serverLogs => _serverLogStreamController.stream;

/// Adds a list of build options to be used for all builds.
///
/// When this client disconnects these options will no longer be used by the
/// build daemon.
void addBuildOptions(Iterable<String> options) {
var request = BuildOptionsRequest((b) => b..options.replace(options));
_channel.sink.add(jsonEncode(serializers.serialize(request)));
}

/// Adds paths to write usage logs to.
///
/// When the client disconnects these files will no longer be logged to.
void logToPaths(Iterable<String> paths) {
var request = LogToPathsRequest((b) => b..paths.replace(paths));
_channel.sink.add(jsonEncode(serializers.serialize(request)));
}

/// Registers a build target to be built upon any file change.
///
/// Changes that match the patterns in [blackListPattern] will be ignored.
void registerBuildTarget(String target, Iterable<String> blackListPattern) {
var request = BuildTargetRequest((b) => b
..target = target
..blackListPattern.replace(blackListPattern));
_channel.sink.add(jsonEncode(serializers.serialize(request)));
}

/// Builds all registered targets.
void startBuild() {
var request = BuildRequest();
_channel.sink.add(jsonEncode(serializers.serialize(request)));
}

Future<void> _connect(String workingDirectory, int port) async {
_channel = IOWebSocketChannel.connect('ws://localhost:$port');
_channel.stream.listen(_handleServerMessage)
// TODO(grouma) - Implement proper error handling.
..onError(print);
}

void _handleServerMessage(dynamic data) {
var message = serializers.deserialize(jsonDecode(data as String));
if (message is ServerLog) {
_serverLogStreamController.add(message);
} else if (message is BuildResults) {
_buildResults.add(message);
} else {
// In practice we should never reach this state due to the
// deserialize call.
throw StateError(
'Unexpected message from the Dart Build Daemon\n $message');
}
}

static Future<BuildDaemonClient> connect(String workingDirectory,
{String daemonCommand = ''}) async {
Process process;
if (daemonCommand.isEmpty) {
process = await Process.start(
'pub', ['run', 'build_daemon', workingDirectory],
mode: ProcessStartMode.detachedWithStdio);
} else {
process = await Process.start(daemonCommand, [workingDirectory],
mode: ProcessStartMode.detachedWithStdio);
}
// Print errors coming from the Dart Build Daemon to help with debugging.
process.stderr.transform(utf8.decoder).listen(print);
var result = await process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.firstWhere((line) => line == versionSkew || line == readyToConnectLog);

if (result == versionSkew) {
throw VersionSkew();
}

var port = _existingPort(workingDirectory);
var client = BuildDaemonClient._();
await client._connect(workingDirectory, port);
return client;
}

static int _existingPort(String workingDirectory) {
var portFile = File('${daemonWorkspace(workingDirectory)}'
'/.dart_build_daemon_port');
if (!portFile.existsSync()) throw Exception('Unable to read port file.');
return int.parse(portFile.readAsStringSync());
}
}

class VersionSkew extends Error {}
24 changes: 24 additions & 0 deletions build_daemon/lib/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'dart:io';

const readyToConnectLog = 'READY TO CONNECT';

const versionSkew = 'DIFFERENT RUNNING VERSION';

// TODO(grouma) - use pubspec version when this is open sourced.
const currentVersion = '1.0.0';

String daemonWorkspace(String workingDirectory) =>
'${Directory.systemTemp.path}/dart_build_daemon/'
'${workingDirectory.replaceAll("/", "_")}';

/// Used to ensure that only one instance of this daemon is running at a time.
String lockFilePath(String workingDirectory) =>
'${daemonWorkspace(workingDirectory)}/.dart_build_lock';

/// Used to signal to clients on what port the running daemon is listening.
String portFilePath(String workingDirectory) =>
'${daemonWorkspace(workingDirectory)}/.dart_build_daemon_port';

/// Used to signal to clients the current version of the build daemon.
String versionFilePath(String workingDirectory) =>
'${daemonWorkspace(workingDirectory)}/.dart_build_daemon_version';
15 changes: 15 additions & 0 deletions build_daemon/lib/daemon_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'dart:async';

import 'data/build_status.dart';
import 'data/server_log.dart';

class DaemonBuilder {
Stream<BuildResults> get builds => Stream.empty();

Stream<ServerLog> get logs => Stream.empty();

Future<void> build(
Set<String> targets, Set<String> options, Set<String> logToPaths) async {}

Future<void> stop() async {}
}
18 changes: 18 additions & 0 deletions build_daemon/lib/data/build_options_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';

part 'build_options_request.g.dart';

abstract class BuildOptionsRequest
implements Built<BuildOptionsRequest, BuildOptionsRequestBuilder> {
static Serializer<BuildOptionsRequest> get serializer =>
_$buildOptionsRequestSerializer;

factory BuildOptionsRequest([updates(BuildOptionsRequestBuilder b)]) =
_$BuildOptionsRequest;

BuildOptionsRequest._();

BuiltList<String> get options;
}
Loading

0 comments on commit fe0c72d

Please sign in to comment.