blob: bbad96948504d098b9dbadbe51c3604af824508e [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:convert';
import 'dart:io';
import 'package:build_daemon/data/build_target.dart';
import 'package:built_value/serializer.dart';
import 'package:web_socket_channel/io.dart';
import 'constants.dart';
import 'data/build_request.dart';
import 'data/build_status.dart';
import 'data/build_target_request.dart';
import 'data/serializers.dart';
import 'data/server_log.dart';
import 'src/file_wait.dart';
Future<int> _existingPort(String workingDirectory) async {
var portFile = File(portFilePath(workingDirectory));
if (!await waitForFile(portFile)) throw MissingPortFile();
return int.parse(portFile.readAsStringSync());
}
Future<void> _handleDaemonStartup(
Process process,
void Function(ServerLog) logHandler,
) async {
process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
logHandler(ServerLog((b) => b..log = line));
});
var stream = process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.asBroadcastStream();
// The daemon may log critical information prior to it successfully
// starting. Capture this data and forward to the logHandler.
var sub = stream.where((line) => !_isActionMessage(line)).listen((line) {
logHandler(ServerLog((b) => b..log = line));
});
var daemonAction =
await stream.firstWhere(_isActionMessage, orElse: () => null);
if (daemonAction == null) {
throw StateError('Unable to start build daemon.');
} else if (daemonAction == versionSkew) {
throw VersionSkew();
} else if (daemonAction == optionsSkew) {
throw OptionsSkew();
}
await sub.cancel();
}
bool _isActionMessage(String line) =>
line == versionSkew || line == readyToConnectLog || line == optionsSkew;
/// A client of the build daemon.
///
/// Handles starting and connecting to the build daemon.
///
/// Example:
/// https://pub.dartlang.org/packages/build_daemon#-example-tab-
class BuildDaemonClient {
final _buildResults = StreamController<BuildResults>.broadcast();
final Serializers _serializers;
IOWebSocketChannel _channel;
BuildDaemonClient._(
int port,
this._serializers,
void Function(ServerLog) logHandler,
) {
_channel = IOWebSocketChannel.connect('ws://localhost:$port')
..stream.listen((data) {
var message = _serializers.deserialize(jsonDecode(data as String));
if (message is ServerLog) {
logHandler(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');
}
})
// TODO(grouma) - Implement proper error handling.
.onError(print);
}
Stream<BuildResults> get buildResults => _buildResults.stream;
Future<void> get finished async => await _channel.sink.done;
/// Registers a build target to be built upon any file change.
void registerBuildTarget(BuildTarget target) => _channel.sink.add(jsonEncode(
_serializers.serialize(BuildTargetRequest((b) => b..target = target))));
/// Builds all registered targets, including those not from this client.
///
/// Note this will wait for any ongoing build to finish before starting a new
/// one.
void startBuild() {
var request = BuildRequest();
_channel.sink.add(jsonEncode(_serializers.serialize(request)));
}
Future<void> close() => _channel.sink.close();
/// Connects to the current daemon instance.
///
/// If one is not running, a new daemon instance will be started.
static Future<BuildDaemonClient> connect(
String workingDirectory,
List<String> daemonCommand, {
Serializers serializersOverride,
void Function(ServerLog) logHandler,
bool includeParentEnvironment,
Map<String, String> environment,
}) async {
logHandler ??= (_) {};
includeParentEnvironment ??= true;
var daemonSerializers = serializersOverride ?? serializers;
var process = await Process.start(
daemonCommand.first,
daemonCommand.sublist(1),
mode: ProcessStartMode.detachedWithStdio,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
);
await _handleDaemonStartup(process, logHandler);
return BuildDaemonClient._(
await _existingPort(workingDirectory), daemonSerializers, logHandler);
}
}
/// Thrown when the port file for the running daemon instance can't be found.
class MissingPortFile implements Exception {}
/// Thrown if the client requests conflicting options with the current daemon
/// instance.
class OptionsSkew implements Exception {}
/// Thrown if the current daemon instance version does not match that of the
/// client.
class VersionSkew implements Exception {}