blob: e750173a34ff3eb8d5dd80601ce9f45260ac4c10 [file] [log] [blame]
// Copyright (c) 2022, 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.
// ignore_for_file: implementation_imports
// front_end/src imports below that require lint `ignore_for_file` are a
// temporary state of things until frontend team builds better api that would
// replace api used below. This api was made private in an effort to discourage
// further use.
import 'dart:async';
import 'dart:convert';
import 'dart:io'
show exit, File, InternetAddress, ProcessSignal, ServerSocket, Socket;
import 'dart:typed_data' show Uint8List;
import 'package:args/args.dart';
import 'package:front_end/src/api_unstable/vm.dart';
import 'package:kernel/binary/tag.dart' show expectedSdkHash;
import 'package:kernel/kernel.dart' show Component, loadComponentFromBytes;
import 'package:path/path.dart' as path;
import '../frontend_server.dart';
import '../resident_frontend_server_utils.dart'
show
CachedDillAndCompilerOptionsPaths,
computeCachedDillAndCompilerOptionsPaths;
/// Floor the system time by this amount in order to correctly detect modified
/// source files on all platforms. This has no effect on correctness,
/// but may result in more files being marked as modified than strictly
/// required.
const Duration _stateGranularity = const Duration(seconds: 1);
/// Ensures the info file is removed if Ctrl-C is sent to the server.
/// Mostly used when debugging.
StreamSubscription<ProcessSignal>? _cleanupHandler;
extension on DateTime {
/// Truncates by [amount].
///
/// This is needed because the 1 second granularity on DateTime objects
/// returned by file stat on Windows is different than the system-times
/// granularity. We must floor the system time
/// by 1 second so that if a file is modified within the same second of
/// the last compile time, it will be correctly detected as being modified.
DateTime floorTime({Duration amount = _stateGranularity}) {
return new DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch -
millisecondsSinceEpoch % amount.inMilliseconds);
}
}
enum _ResidentState {
waitingForFirstCompile,
compiling,
waitingForRecompile,
}
/// A wrapper around the FrontendCompiler, along with all the state needed
/// to perform incremental compilations
///
/// TODO: Fix the race condition that occurs when the ResidentCompiler returns
/// a kernel file to the CLI and another compilation request is given before
/// the VM is able to launch from the kernel that was returned in the first
/// compile request. The ResidentCompiler will be in the state of waiting for
/// a recompile request and will subsequently process the request and modify
/// the kernel file. However, it should be waiting for the VM to finish
/// launching itself from this kernel until it modifies the kernel.
/// As far as I can tell this race also exists in the current CLI run
/// command when using pub's precompile pipeline.
///
/// TODO Fix the race condition that occurs when the same entry point is
/// compiled concurrently.
class ResidentCompiler {
final File _entryPoint;
File? _currentPackage;
ArgResults _compileOptions;
late FrontendCompiler _compiler;
DateTime _lastCompileStartTime = new DateTime.now().floorTime();
_ResidentState _state = _ResidentState.waitingForFirstCompile;
final StringBuffer _compilerOutput = new StringBuffer();
final Set<Uri> trackedSources = <Uri>{};
final List<String> _formattedOutput = <String>[];
bool incrementalMode = false;
/// The file where kernel data will be output by this [ResidentCompiler].
File get _outputDill =>
new File(_compileOptions.option(ResidentFrontendServer._outputString)!);
ResidentCompiler(this._entryPoint, this._compileOptions) {
_compiler = new FrontendCompiler(_compilerOutput);
updateState(_compileOptions);
}
void resetStateToWaitingForFirstCompile() {
_state = _ResidentState.waitingForFirstCompile;
}
/// The [ResidentCompiler] will use the [newOptions] for future compilation
/// requests.
void updateState(ArgResults newOptions) {
final String? packages = newOptions['packages'];
incrementalMode = newOptions['incremental'] == true;
_compileOptions = newOptions;
_currentPackage = packages == null ? null : new File(packages);
// Refresh the compiler's output for the next compile
_compilerOutput.clear();
_formattedOutput.clear();
resetStateToWaitingForFirstCompile();
}
/// The current compiler options are outdated when any option has changed
/// since the last compile, or when the packages file has been modified
bool areOptionsOutdated(ArgResults newOptions) {
if (newOptions.arguments.length != _compileOptions.arguments.length) {
return true;
}
if (!newOptions.arguments
.toSet()
.containsAll(_compileOptions.arguments.toSet())) {
return true;
}
return _currentPackage != null &&
!_lastCompileStartTime
.isAfter(_currentPackage!.statSync().modified.floorTime());
}
/// Compiles the entry point that this ResidentCompiler is hooked to, abiding
/// by [_compileOptions], and returns a response map detailing compilation
/// results. See [_createResponseMap] for more information about that map.
///
/// Incremental compilations will be performed when possible.
Future<Map<String, dynamic>> compile() async {
bool incremental = false;
// If this entrypoint was previously compiled on this compiler instance,
// check which source files need to be recompiled in the incremental
// compilation request. If no files have been modified, we can return
// the cached kernel. Otherwise, perform an incremental compilation.
if (_state == _ResidentState.waitingForRecompile) {
List<Uri> invalidatedUris =
await _getSourceFilesToRecompile(_lastCompileStartTime);
// No changes to source files detected and cached kernel file exists
// If a kernel file is removed in between compilation requests,
// fall through to produce the kernel in recompileDelta.
if (invalidatedUris.isEmpty && _outputDill.existsSync()) {
return _createResponseMap(
_outputDill.path, _formattedOutput, _compiler.errors.length,
usingCachedKernel: true);
}
_state = _ResidentState.compiling;
incremental = true;
for (Uri invalidatedUri in invalidatedUris) {
_compiler.invalidate(invalidatedUri);
}
_compiler.errors.clear();
_lastCompileStartTime = new DateTime.now().floorTime();
await _compiler.recompileDelta(entryPoint: _entryPoint.path);
} else {
_state = _ResidentState.compiling;
_lastCompileStartTime = new DateTime.now().floorTime();
_compiler.errors.clear();
await _compiler.compile(_entryPoint.path, _compileOptions);
}
_interpretCompilerOutput(new LineSplitter()
.convert(_compilerOutput.toString())
.where((line) => line.isNotEmpty)
.toList());
_compilerOutput.clear();
if (incrementalMode) {
// forces the compiler to produce complete kernel files on each
// request, even when incrementally compiled.
_compiler
..acceptLastDelta()
..resetIncrementalCompiler();
_state = _ResidentState.waitingForRecompile;
} else {
resetStateToWaitingForFirstCompile();
}
return _createResponseMap(
_outputDill.path, _formattedOutput, _compiler.errors.length,
incrementalCompile: incremental);
}
/// WARNING: [compile] must be called on this compiler to populate the
/// required context in it before [compileExpression] can be called on it.
Future<String> compileExpression(
String expression,
List<String> definitions,
List<String> definitionTypes,
List<String> typeDefinitions,
List<String> typeBounds,
List<String> typeDefaults,
String libraryUri,
String? klass,
String? method,
int offset,
String? scriptUri,
bool isStatic,
) async {
await _compiler.compileExpression(
expression,
definitions,
definitionTypes,
typeDefinitions,
typeBounds,
typeDefaults,
libraryUri,
klass,
method,
offset,
scriptUri,
isStatic,
);
_compilerOutput.clear();
// [incrementalMode] can only ever be [false] if `--aot` was passed in the
// 'compileExpression' request received by the [ResidentFrontendServer],
// which should be impossible.
assert(incrementalMode);
// Force the compiler to produce complete kernel files on each request, even
// when incrementally compiled.
_compiler
..acceptLastDelta()
..resetIncrementalCompiler();
resetStateToWaitingForFirstCompile();
final List<String> errors = _compiler.errors;
final int errorCount = errors.length;
return jsonEncode({
'success': errorCount == 0,
'errorCount': errorCount,
if (errorCount > 0) 'compilerOutputLines': errors,
'kernelBytes': base64Encode(_outputDill.readAsBytesSync()),
});
}
/// Reads the compiler's [outputLines] to keep track of which files
/// need to be tracked. Adds correctly ANSI formatted output to
/// the [_formattedOutput] list.
void _interpretCompilerOutput(List<String> outputLines) {
_formattedOutput.clear();
int outputLineIndex = 0;
bool acceptingErrorsOrVerboseOutput = true;
final String boundaryKey = outputLines[outputLineIndex]
.substring(outputLines[outputLineIndex++].indexOf(' ') + 1);
String line = outputLines[outputLineIndex++];
while (acceptingErrorsOrVerboseOutput || !line.startsWith(boundaryKey)) {
if (acceptingErrorsOrVerboseOutput) {
if (line == boundaryKey) {
acceptingErrorsOrVerboseOutput = false;
} else {
_formattedOutput.add(line);
}
} else {
final String diffUri = line.substring(1);
if (line.startsWith('+')) {
trackedSources.add(Uri.parse(diffUri));
} else if (line.startsWith('-')) {
trackedSources.remove(Uri.parse(diffUri));
}
}
line = outputLines[outputLineIndex++];
}
}
/// Returns a list of uris that need to be recompiled, based on the
/// [lastKernelCompileTime] timestamp.
/// Due to Windows timestamp granularity, all timestamps are truncated by
/// the second. This has no effect on correctness but may result in more
/// files being marked as invalid than are strictly required.
Future<List<Uri>> _getSourceFilesToRecompile(
DateTime lastKernelCompileTime) async {
final List<Uri> sourcesToRecompile = <Uri>[];
for (Uri uri in trackedSources) {
final DateTime sourceModifiedTime =
new File(uri.toFilePath()).statSync().modified.floorTime();
if (!lastKernelCompileTime.isAfter(sourceModifiedTime)) {
sourcesToRecompile.add(uri);
}
}
return sourcesToRecompile;
}
/// Returns a [Map] that can be serialized to JSON containing [outputDillPath]
/// and any [formattedErrors].
static Map<String, dynamic> _createResponseMap(
String outputDillPath,
List<String> formattedErrors,
int errorCount, {
bool usingCachedKernel = false,
bool incrementalCompile = false,
}) =>
<String, Object>{
"success": errorCount == 0,
"errorCount": errorCount,
"compilerOutputLines": formattedErrors,
"output-dill": outputDillPath,
if (usingCachedKernel) "returnedStoredKernel": true, // used for testing
if (incrementalCompile) "incremental": true, // used for testing
};
}
/// Maintains [FrontendCompiler] instances for kernel compilations, meant to be
/// used by the Dart CLI via sockets.
///
/// The [ResidentFrontendServer] manages compilation requests for VM targets
/// between any number of dart entry points, and utilizes incremental
/// compilation and existing kernel files for faster compile times.
///
/// Communication is handled on the socket set up by the
/// residentListenAndCompile method.
class ResidentFrontendServer {
static const String _commandString = 'command';
static const String _replaceCachedDillString = 'replaceCachedDill';
static const String _replacementDillPathString = 'replacementDillPath';
static const String _compileString = 'compile';
static const String _executableString = 'executable';
static const String _packageString = 'packages';
static const String _successString = 'success';
static const String _outputString = 'output-dill';
static const String _useCachedCompilerOptionsAsBaseString =
'useCachedCompilerOptionsAsBase';
static const String _compileExpressionString = 'compileExpression';
static const String _libraryUriString = 'libraryUri';
static const String _rootLibraryUriString = 'rootLibraryUri';
static const String _dillExtensionString = '.dill';
static const String _expressionString = 'expression';
static const String _definitionsString = 'definitions';
static const String _definitionTypesString = 'definitionTypes';
static const String _typeDefinitionsString = 'typeDefinitions';
static const String _typeBoundsString = 'typeBounds';
static const String _typeDefaultsString = 'typeDefaults';
static const String _classString = 'class';
static const String _methodString = 'method';
static const String _offsetString = 'offset';
static const String _scriptUriString = 'scriptUri';
static const String _isStaticString = 'isStatic';
static const String _shutdownString = 'shutdown';
static const int _compilerLimit = 3;
static final String shutdownCommand =
jsonEncode(<String, Object>{_commandString: _shutdownString});
static final String _shutdownJsonResponse =
jsonEncode(<String, Object>{_shutdownString: true});
static final Uri _sdkBinariesUri = computePlatformBinariesLocation();
static final Uri _sdkUri = _sdkBinariesUri.resolve('../../');
static final Uri _platformKernelUri =
_sdkBinariesUri.resolve('vm_platform.dill');
static final Map<String, ResidentCompiler> compilers = {};
/// Returns a [ResidentCompiler] that has been configured with
/// [compileOptions] and prepared to compile the [canonicalizedLibraryPath]
/// entrypoint. This function also writes [compileOptions.arguments] to
/// [cachedCompilerOptions] as a JSON list.
static ResidentCompiler _getResidentCompilerForEntrypoint({
required final String canonicalizedLibraryPath,
required final ArgResults compileOptions,
required final File cachedCompilerOptions,
}) {
cachedCompilerOptions.createSync();
cachedCompilerOptions.writeAsStringSync(
compileOptions.arguments.map(jsonEncode).toList().toString(),
);
late final ResidentCompiler residentCompiler;
if (compilers[canonicalizedLibraryPath] == null) {
// Avoids using too much memory.
if (compilers.length >= ResidentFrontendServer._compilerLimit) {
compilers.remove(compilers.keys.first);
}
residentCompiler = new ResidentCompiler(
new File(canonicalizedLibraryPath),
compileOptions,
);
compilers[canonicalizedLibraryPath] = residentCompiler;
} else {
residentCompiler = compilers[canonicalizedLibraryPath]!;
if (residentCompiler.areOptionsOutdated(compileOptions)) {
residentCompiler.updateState(compileOptions);
}
}
return residentCompiler;
}
static Future<String> _handleReplaceCachedDillRequest(
Map<String, dynamic> request,
) async {
if (request[_replacementDillPathString] == null) {
return _encodeErrorMessage(
"'$_replaceCachedDillString' requests must include a "
"'$_replacementDillPathString' property.",
);
}
final File replacementDillFile =
new File(request[_replacementDillPathString]);
final String canonicalizedLibraryPath;
try {
final Component component =
loadComponentFromBytes(replacementDillFile.readAsBytesSync());
canonicalizedLibraryPath = path.canonicalize(
component.mainMethod!.enclosingLibrary.fileUri.toFilePath(),
);
final String cachedDillPath;
try {
cachedDillPath =
computeCachedDillAndCompilerOptionsPaths(canonicalizedLibraryPath)
.cachedDillPath;
} on Exception catch (e) {
return _encodeErrorMessage(e.toString());
}
replacementDillFile.copySync(cachedDillPath);
} catch (e) {
return _encodeErrorMessage('Failed to replace cached dill');
}
if (compilers[canonicalizedLibraryPath] != null) {
compilers[canonicalizedLibraryPath]!.resetStateToWaitingForFirstCompile();
}
return jsonEncode({
_successString: true,
});
}
static Future<String> _handleCompileRequest(
Map<String, dynamic> request,
) async {
if (request[_executableString] == null || request[_outputString] == null) {
return _encodeErrorMessage(
"'$_compileString' requests must include an '$_executableString' "
"property and an '$_outputString' property.",
);
}
final String canonicalizedExecutablePath =
path.canonicalize(request[_executableString]);
final String cachedDillPath;
final File cachedCompilerOptions;
try {
final CachedDillAndCompilerOptionsPaths computationResult =
computeCachedDillAndCompilerOptionsPaths(canonicalizedExecutablePath);
cachedDillPath = computationResult.cachedDillPath;
cachedCompilerOptions =
new File(computationResult.cachedCompilerOptionsPath);
} on Exception catch (e) {
return _encodeErrorMessage(e.toString());
}
final ArgResults options = _generateCompilerOptions(
request: request,
cachedCompilerOptions: cachedCompilerOptions,
outputDillOverride: cachedDillPath,
initializeFromDillPath: cachedDillPath,
);
final ResidentCompiler residentCompiler = _getResidentCompilerForEntrypoint(
canonicalizedLibraryPath: canonicalizedExecutablePath,
compileOptions: options,
cachedCompilerOptions: cachedCompilerOptions,
);
final Map<String, dynamic> response = await residentCompiler.compile();
if (response[_successString] != true) {
return jsonEncode(response);
}
final String outputDillPath = request[_outputString];
if (cachedDillPath != outputDillPath) {
try {
new File(cachedDillPath).copySync(outputDillPath);
} catch (e) {
return _encodeErrorMessage(
'Could not write output dill to ${request[_outputString]}.',
);
}
}
return jsonEncode({...response, _outputString: outputDillPath});
}
static Future<String> _handleCompileExpressionRequest(
Map<String, dynamic> request,
) async {
final String canonicalizedLibraryPath;
try {
if ((request[_libraryUriString] as String).startsWith('dart:')) {
// An argument to the [entrypoint] parameter of
// [FrontendCompiler.compile] is mandatory, and
// [canonicalizedLibraryPath] is what we will use as that argument, so
// if the library URI provided in the request begins with 'dart:', then
// we use the URI of the root library of the isolate group in which the
// evaluation is taking place to compute [canonicalizedLibraryPath]
// instead.
canonicalizedLibraryPath = path.canonicalize(
Uri.parse(request[_rootLibraryUriString]).toFilePath(),
);
} else {
canonicalizedLibraryPath = path
.canonicalize(Uri.parse(request[_libraryUriString]).toFilePath());
}
} catch (e) {
return _encodeErrorMessage(
"Request contains invalid '$_libraryUriString' property",
);
}
final String cachedDillPath;
final File cachedCompilerOptions;
try {
final CachedDillAndCompilerOptionsPaths computationResult =
computeCachedDillAndCompilerOptionsPaths(canonicalizedLibraryPath);
cachedDillPath = computationResult.cachedDillPath;
cachedCompilerOptions =
new File(computationResult.cachedCompilerOptionsPath);
} on Exception catch (e) {
return _encodeErrorMessage(e.toString());
}
// Make the [ResidentCompiler] output the compiled expression to
// [compiledExpressionDillPath] to prevent it from overwriting the
// cached program dill.
assert(cachedDillPath.endsWith(_dillExtensionString));
final String compiledExpressionDillPath = cachedDillPath.replaceRange(
cachedDillPath.length - _dillExtensionString.length,
null,
'.expr.dill',
);
final ArgResults options = _generateCompilerOptions(
request: request,
cachedCompilerOptions: cachedCompilerOptions,
outputDillOverride: compiledExpressionDillPath,
initializeFromDillPath: cachedDillPath,
);
final ResidentCompiler residentCompiler = _getResidentCompilerForEntrypoint(
canonicalizedLibraryPath: canonicalizedLibraryPath,
compileOptions: options,
cachedCompilerOptions: cachedCompilerOptions,
);
final String expression = request[_expressionString];
final List<String> definitions =
(request[_definitionsString] as List<dynamic>).cast<String>();
final List<String> definitionTypes =
(request[_definitionTypesString] as List<dynamic>).cast<String>();
final List<String> typeDefinitions =
(request[_typeDefinitionsString] as List<dynamic>).cast<String>();
final List<String> typeBounds =
(request[_typeBoundsString] as List<dynamic>).cast<String>();
final List<String> typeDefaults =
(request[_typeDefaultsString] as List<dynamic>).cast<String>();
final String libraryUri = request[_libraryUriString];
final String? klass = request[_classString];
final String? method = request[_methodString];
final int offset = request[_offsetString];
final String? scriptUri = request[_scriptUriString];
final bool isStatic = request[_isStaticString];
// [residentCompiler.compile] must be called before
// [residentCompiler.compileExpression] can be called. See the
// documentation of [ResidentCompiler.compile] for more information.
await residentCompiler.compile();
return await residentCompiler.compileExpression(
expression,
definitions,
definitionTypes,
typeDefinitions,
typeBounds,
typeDefaults,
libraryUri,
klass,
method,
offset,
scriptUri,
isStatic,
);
}
/// Takes in JSON [input] from the socket and compiles the request,
/// using incremental compilation if possible. Returns a JSON string to be
/// sent back to the client socket containing either an error message or the
/// kernel file to be used.
///
/// If the command is compile, paths the source file, package_config.json,
/// and the output-dill file must be provided via "executable", "packages",
/// and "output-dill".
static Future<String> handleRequest(String input) async {
Map<String, dynamic> request;
try {
request = jsonDecode(input);
} on FormatException {
return _encodeErrorMessage('$input is not valid JSON.');
}
switch (request[_commandString]) {
case _replaceCachedDillString:
return _handleReplaceCachedDillRequest(request);
case _compileString:
return _handleCompileRequest(request);
case _compileExpressionString:
return _handleCompileExpressionRequest(request);
case _shutdownString:
return _shutdownJsonResponse;
default:
return _encodeErrorMessage(
'Unsupported command: ${request[_commandString]}.');
}
}
/// Generates the compiler options needed to handle the [request].
static ArgResults _generateCompilerOptions({
required Map<String, dynamic> request,
required File cachedCompilerOptions,
/// The compiled kernel file will be stored at this path, and not at
/// [request['--output-dill']].
required String outputDillOverride,
required String initializeFromDillPath,
}) {
final Map<String, dynamic> options = {};
if (request[_useCachedCompilerOptionsAsBaseString] == true &&
cachedCompilerOptions.existsSync()) {
// If [request[_useCachedCompilerOptionsAsBaseString]] is true, then we
// start with the cached options and apply any options specified in
// [request] as overrides.
final String cachedCompilerOptionsContents =
cachedCompilerOptions.readAsStringSync();
final List<String> cachedCompilerOptionsAsList =
(jsonDecode(cachedCompilerOptionsContents) as List<dynamic>)
.cast<String>();
final ArgResults cachedOptions =
argParser.parse(cachedCompilerOptionsAsList);
for (final String option in cachedOptions.options) {
options[option] = cachedOptions[option];
}
}
final ArgResults overrides = argParser.parse(<String>[
'--sdk-root=${_sdkUri.toFilePath()}',
if (!(request['aot'] ?? false)) '--incremental',
'--platform=${_platformKernelUri.path}',
'--output-dill=$outputDillOverride',
'--initialize-from-dill=$initializeFromDillPath',
// We can assume that the cached dill is up-to-date when handling
// 'compileExpression' requests because if dartdev was given a source file
// to run, then it must have compiled it with the resident frontend
// compiler, guaranteeing that the cached dill is up-to-date, and if
// dartdev was given a dill file to run, then it must have used the
// resident frontend compiler's 'replaceCachedDill' endpoint to update the
// dill cache.
if (request[_commandString] == _compileExpressionString)
'--assume-initialize-from-dill-up-to-date',
'--target=vm',
'--filesystem-scheme',
'org-dartlang-root',
if (request.containsKey(_packageString))
'--packages=${request[_packageString]}',
if (request['support-mirrors'] ?? false) '--support-mirrors',
if (request['enable-asserts'] ?? false) '--enable-asserts',
if (request['sound-null-safety'] ?? false) '--sound-null-safety',
if (request['verbosity'] != null) '--verbosity=${request["verbosity"]}',
if (request['verbose'] ?? false) '--verbose',
if (request['aot'] ?? false) '--aot',
if (request['tfa'] ?? false) '--tfa',
if (request['rta'] ?? false) '--rta',
if (request['tree-shake-write-only-fields'] ?? false)
'--tree-shake-write-only-fields',
if (request['protobuf-tree-shaker-v2'] ?? false)
'--protobuf-tree-shaker-v2',
if (request['define'] != null)
for (String define in request['define']) define,
if (request['enable-experiment'] != null)
for (String experiment in request['enable-experiment']) experiment,
]);
for (final String option in overrides.options) {
options[option] = overrides[option];
}
// Transform [options] into a list that can be passed to [argParser.parse].
final List<String> optionsAsList = <String>[];
for (final MapEntry<String, dynamic>(:key, :value) in options.entries) {
if (value is List<dynamic>) {
for (final Object multiOptionValue in value) {
optionsAsList.add('--$key=$multiOptionValue');
}
} else if (value is bool) {
if (value) {
optionsAsList.add('--$key');
}
} else {
optionsAsList.add('--$key=$value');
}
}
return argParser.parse(optionsAsList);
}
/// Encodes the [message] in JSON to be sent over the socket.
static String _encodeErrorMessage(String message) => jsonEncode(
<String, Object>{_successString: false, 'errorMessage': message},
);
/// Used to create compile requests for the ResidentFrontendServer.
/// Returns a JSON string that the resident compiler will be able to
/// interpret.
static String createCompileJSON(
{required String executable,
String? packages,
required String outputDill,
bool? supportMirrors,
bool? enableAsserts,
bool? soundNullSafety,
String? verbosity,
bool? aot,
bool? tfa,
bool? rta,
bool? treeShakeWriteOnlyFields,
bool? protobufTreeShakerV2,
List<String>? define,
List<String>? enableExperiment,
bool verbose = false}) {
return jsonEncode(<String, Object>{
"command": "compile",
"executable": executable,
"output-dill": outputDill,
if (aot != null) "aot": true,
if (define != null) "define": define,
if (enableAsserts != null) "enable-asserts": true,
if (enableExperiment != null) "enable-experiment": enableExperiment,
if (packages != null) "packages": packages,
if (protobufTreeShakerV2 != null) "protobuf-tree-shaker-v2": true,
if (rta != null) "rta": true,
if (soundNullSafety != null) "sound-null-safety": soundNullSafety,
if (supportMirrors != null) "support-mirrors": true,
if (tfa != null) "tfa": true,
if (treeShakeWriteOnlyFields != null)
"tree-shaker-write-only-fields": true,
if (verbosity != null) "verbosity": verbosity,
"verbose": verbose,
});
}
}
/// Closes the ServerSocket and removes the [serverInfoFile] that is used
/// to access this instance of the Resident Frontend Server.
Future<void> residentServerCleanup(
ServerSocket server, File serverInfoFile) async {
try {
if (_cleanupHandler != null) {
await _cleanupHandler!.cancel();
}
} catch (_) {
} finally {
try {
if (serverInfoFile.existsSync()) {
serverInfoFile.deleteSync();
}
} catch (_) {}
}
await server.close();
}
/// Starts a timer that will shut down the resident frontend server in
/// the amount of time specified by [timerDuration], if it is not cancelled.
Timer startShutdownTimer(
Duration timerDuration, ServerSocket server, File serverInfoFile) {
return new Timer(timerDuration, () async {
await residentServerCleanup(server, serverInfoFile);
});
}
/// Listens for compilation commands from socket connections on the
/// provided [address] and [port].
/// If the last request exceeds the amount of time specified by
/// [inactivityTimeout], the server will bring itself down
Future<StreamSubscription<Socket>?> residentListenAndCompile(
InternetAddress address, int port, File serverInfoFile,
{Duration inactivityTimeout = const Duration(minutes: 30)}) async {
ServerSocket server;
try {
try {
serverInfoFile.createSync(exclusive: true);
} catch (e) {
throw new StateError('A server is already running.');
}
server = await ServerSocket.bind(address, port);
// There are particular aspects of the info file format that must be
// preserved to ensure backwards compatibility with the original versions
// of the utilities for parsing this file.
//
// The aspects of the info file format that must be preserved are:
// 1. The file must begin with 'address:$address '. Note that $address IS
// NOT preceded by a space and IS followed by a space.
// 2. The file must end with 'port:$port'. Note that $port IS NOT preceded
// by a space. $port may be followed by zero or more whitespace
// characters.
serverInfoFile.writeAsStringSync(
'address:${server.address.address} '
'sdkHash:${expectedSdkHash} '
'port:${server.port} ',
);
} on StateError catch (e) {
print('Error: $e\n');
return null;
} catch (e) {
// If we created a file, but bind or writing failed, clean up.
try {
serverInfoFile.deleteSync();
} catch (_) {}
print('Error: $e\n');
return null;
}
_cleanupHandler = ProcessSignal.sigint.watch().listen((signal) async {
await residentServerCleanup(server, serverInfoFile);
exit(1);
});
Timer shutdownTimer =
startShutdownTimer(inactivityTimeout, server, serverInfoFile);
// TODO: This should be changed to print to stderr so we don't change the
// stdout text for regular apps.
print('The Resident Frontend Compiler is listening at '
'${server.address.address}:${server.port}');
return server.listen((client) {
client.listen((Uint8List data) async {
String result = await ResidentFrontendServer.handleRequest(
new String.fromCharCodes(data));
client.write(result);
shutdownTimer.cancel();
if (result == ResidentFrontendServer._shutdownJsonResponse) {
await residentServerCleanup(server, serverInfoFile);
} else {
shutdownTimer =
startShutdownTimer(inactivityTimeout, server, serverInfoFile);
}
}, onError: (error) {
client.close();
}, onDone: () {
client.close();
});
}, onError: (_) async {
shutdownTimer.cancel();
await residentServerCleanup(server, serverInfoFile);
});
}