| Index: sdk/lib/_internal/pub/lib/src/barback/load_transformers.dart
|
| diff --git a/sdk/lib/_internal/pub/lib/src/barback/load_transformers.dart b/sdk/lib/_internal/pub/lib/src/barback/load_transformers.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ec48edfb057fcd2ca378450044c3af7b54bd4f6f
|
| --- /dev/null
|
| +++ b/sdk/lib/_internal/pub/lib/src/barback/load_transformers.dart
|
| @@ -0,0 +1,400 @@
|
| +// Copyright (c) 2013, 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.
|
| +
|
| +library pub.serialize_transformer;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:isolate';
|
| +
|
| +import 'package:barback/barback.dart';
|
| +import 'package:stack_trace/stack_trace.dart';
|
| +
|
| +import '../dart.dart' as dart;
|
| +import '../log.dart' as log;
|
| +import '../utils.dart';
|
| +
|
| +/// A Dart script to run in an isolate.
|
| +///
|
| +/// This script serializes one or more transformers defined in a Dart library and
|
| +/// marhsals calls to and from them with the host isolate.
|
| +const _TRANSFORMER_ISOLATE = """
|
| +import 'dart:async';
|
| +import 'dart:isolate';
|
| +import 'dart:mirrors';
|
| +
|
| +import 'http://localhost:<<PORT>>/packages/barback/barback.dart';
|
| +
|
| +/// Sets up the initial communication with the host isolate.
|
| +void main() {
|
| + port.receive((uri, replyTo) {
|
| + _sendFuture(replyTo, new Future.sync(() {
|
| + return initialize(Uri.parse(uri)).map(_serializeTransformer).toList();
|
| + }));
|
| + });
|
| +}
|
| +
|
| +/// Loads all the transformers defined in [uri] and adds them to [transformers].
|
| +///
|
| +/// We then load the library, find any Transformer subclasses in it, instantiate
|
| +/// them, and return them.
|
| +Iterable<Transformer> initialize(Uri uri) {
|
| + var mirrors = currentMirrorSystem();
|
| + // TODO(nweiz): look this up by name once issue 5897 is fixed.
|
| + var transformerUri = Uri.parse(
|
| + 'http://localhost:<<PORT>>/packages/barback/src/transformer.dart');
|
| + var transformerClass = mirrors.libraries[transformerUri]
|
| + .classes[const Symbol('Transformer')];
|
| +
|
| + return mirrors.libraries[uri].classes.values.where((classMirror) {
|
| + if (classMirror.isPrivate) return false;
|
| + if (isAbstract(classMirror)) return false;
|
| + if (!classIsA(classMirror, transformerClass)) return false;
|
| + var constructor = classMirror.constructors[classMirror.simpleName];
|
| + if (constructor == null) return false;
|
| + if (!constructor.parameters.isEmpty) return false;
|
| + return true;
|
| + }).map((classMirror) {
|
| + return classMirror.newInstance(const Symbol(''), []).reflectee;
|
| + });
|
| +}
|
| +
|
| +/// A wrapper for a [Transform] that's in the host isolate.
|
| +///
|
| +/// This retrieves inputs from and sends outputs and logs to the host isolate.
|
| +class ForeignTransform implements Transform {
|
| + /// The port with which we communicate with the host isolate.
|
| + ///
|
| + /// This port and all messages sent across it are specific to this transform.
|
| + final SendPort _port;
|
| +
|
| + final AssetId primaryId;
|
| +
|
| + Future<Asset> get primaryInput => getInput(primaryId);
|
| +
|
| + // TODO(nweiz): implement this
|
| + TransformLogger get logger {
|
| + throw new UnimplementedError('ForeignTranform.logger is not yet '
|
| + 'implemented.');
|
| + }
|
| +
|
| + /// Creates a transform from a serializable map sent from the host isolate.
|
| + ForeignTransform(Map transform)
|
| + : _port = transform['port'],
|
| + primaryId = _deserializeId(transform['primaryId']);
|
| +
|
| + Future<Asset> getInput(AssetId id) {
|
| + return _receiveFuture(_port.call({
|
| + 'type': 'getInput',
|
| + 'id': _serializeId(id)
|
| + })).then(_deserializeAsset);
|
| + }
|
| +
|
| + void addOutput(Asset output) {
|
| + _port.send({
|
| + 'type': 'addOutput',
|
| + 'output': _serializeAsset(output)
|
| + });
|
| + }
|
| +}
|
| +
|
| +/// Returns the mirror for the root Object type.
|
| +ClassMirror get objectMirror {
|
| + if (_objectMirror == null) {
|
| + _objectMirror = currentMirrorSystem()
|
| + .libraries[Uri.parse('dart:core')]
|
| + .classes[const Symbol('Object')];
|
| + }
|
| + return _objectMirror;
|
| +}
|
| +ClassMirror _objectMirror;
|
| +
|
| +// TODO(nweiz): get rid of this when issue 12439 is fixed.
|
| +/// Returns whether or not [superclass] is a superclass of [mirror].
|
| +///
|
| +/// This includes [superclass] being mixed in to or implemented by [mirror].
|
| +bool classIsA(ClassMirror mirror, ClassMirror superclass) {
|
| + if (mirror == superclass) return true;
|
| + if (mirror == objectMirror) return false;
|
| + return classIsA(mirror.superclass, superclass) ||
|
| + mirror.superinterfaces.any((int) => classIsA(int, superclass));
|
| +}
|
| +
|
| +// TODO(nweiz): get rid of this when issue 12826 is fixed.
|
| +/// Returns whether or not [mirror] is an abstract class.
|
| +bool isAbstract(ClassMirror mirror) => mirror.members.values
|
| + .any((member) => member is MethodMirror && member.isAbstract);
|
| +
|
| +/// Converts [transformer] into a serializable map.
|
| +Map _serializeTransformer(Transformer transformer) {
|
| + var port = new ReceivePort();
|
| + port.receive((message, replyTo) {
|
| + _sendFuture(replyTo, new Future.sync(() {
|
| + if (message['type'] == 'isPrimary') {
|
| + return transformer.isPrimary(_deserializeAsset(message['asset']));
|
| + } else {
|
| + assert(message['type'] == 'apply');
|
| + return transformer.apply(
|
| + new ForeignTransform(message['transform']));
|
| + }
|
| + }));
|
| + });
|
| +
|
| + return {
|
| + 'toString': transformer.toString(),
|
| + 'port': port.toSendPort()
|
| + };
|
| +}
|
| +
|
| +/// Converts a serializable map into an [Asset].
|
| +Asset _deserializeAsset(Map asset) {
|
| + var box = new MessageBox();
|
| + asset['sink'].add(box.sink);
|
| + return new Asset.fromStream(_deserializeId(asset['id']), box.stream);
|
| +}
|
| +
|
| +/// Converts a serializable map into an [AssetId].
|
| +AssetId _deserializeId(Map id) => new AssetId(id['package'], id['path']);
|
| +
|
| +/// Converts [asset] into a serializable map.
|
| +Map _serializeAsset(Asset asset) {
|
| + // We can't send IsolateStreams (issue 12437), so instead we send a sink and
|
| + // get the isolate to send us back another sink.
|
| + var box = new MessageBox();
|
| + box.stream.first.then((sink) {
|
| + asset.read().listen(sink.add,
|
| + onError: sink.addError,
|
| + onDone: sink.close);
|
| + });
|
| +
|
| + return {
|
| + 'id': _serializeId(asset.id),
|
| + 'sink': box.sink
|
| + };
|
| +}
|
| +
|
| +/// Converts [id] into a serializable map.
|
| +Map _serializeId(AssetId id) => {'package': id.package, 'path': id.path};
|
| +
|
| +/// Sends the result of [future] through [port].
|
| +///
|
| +/// This should be received on the other end using [_receiveFuture]. It
|
| +/// re-raises any exceptions on the other side as [CrossIsolateException]s.
|
| +void _sendFuture(SendPort port, Future future) {
|
| + future.then((result) {
|
| + port.send({'success': result});
|
| + }).catchError((error) {
|
| + // TODO(nweiz): at least MissingInputException should be preserved here.
|
| + port.send({'error': CrossIsolateException.serialize(error)});
|
| + });
|
| +}
|
| +
|
| +/// Receives the result of [_sendFuture] from [portCall], which should be the
|
| +/// return value of [SendPort.call].
|
| +Future _receiveFuture(Future portCall) {
|
| + return portCall.then((response) {
|
| + if (response.containsKey('success')) return response['success'];
|
| + return new Future.error(
|
| + new CrossIsolateException.deserialize(response['error']));
|
| + });
|
| +}
|
| +
|
| +/// An exception that was originally raised in another isolate.
|
| +///
|
| +/// Exception objects can't cross isolate boundaries in general, so this class
|
| +/// wraps as much information as can be consistently serialized.
|
| +class CrossIsolateException implements Exception {
|
| + /// The name of the type of exception thrown.
|
| + ///
|
| + /// This is the return value of [error.runtimeType.toString()]. Keep in mind
|
| + /// that objects in different libraries may have the same type name.
|
| + final String type;
|
| +
|
| + /// The exception's message, or its [toString] if it didn't expose a `message`
|
| + /// property.
|
| + final String message;
|
| +
|
| + /// The exception's stack trace, or `null` if no stack trace was available.
|
| + final Trace stackTrace;
|
| +
|
| + /// Loads a [CrossIsolateException] from a map.
|
| + ///
|
| + /// [error] should be the result of [CrossIsolateException.serialize].
|
| + CrossIsolateException.deserialize(Map error)
|
| + : type = error['type'],
|
| + message = error['message'],
|
| + stackTrace = error['stack'] == null ? null :
|
| + new Trace.parse(error['stack']);
|
| +
|
| + /// Serializes [error] to a map that can safely be passed across isolate
|
| + /// boundaries.
|
| + static Map serialize(error, [StackTrace stack]) {
|
| + if (stack == null) stack = getAttachedStackTrace(error);
|
| + return {
|
| + 'type': error.runtimeType.toString(),
|
| + 'message': getErrorMessage(error),
|
| + 'stack': stack == null ? null : stack.toString()
|
| + };
|
| + }
|
| +
|
| + String toString() => "\$message\\n\$stackTrace";
|
| +}
|
| +
|
| +// Get a string description of an exception.
|
| +//
|
| +// Most exception types have a "message" property. We prefer this since
|
| +// it skips the "Exception:", "HttpException:", etc. prefix that calling
|
| +// toString() adds. But, alas, "message" isn't actually defined in the
|
| +// base Exception type so there's no easy way to know if it's available
|
| +// short of a giant pile of type tests for each known exception type.
|
| +//
|
| +// So just try it. If it throws, default to toString().
|
| +String getErrorMessage(error) {
|
| + try {
|
| + return error.message;
|
| + } on NoSuchMethodError catch (_) {
|
| + return error.toString();
|
| + }
|
| +}
|
| +""";
|
| +
|
| +/// Load and return all transformers from the library identified by [library].
|
| +///
|
| +/// [server] is used to serve [library] and any Dart files it imports.
|
| +Future<Set<Transformer>> loadTransformers(BarbackServer server,
|
| + AssetId library) {
|
| + var path = library.path.replaceFirst('lib/', '');
|
| + // TODO(nweiz): load from a "package:" URI when issue 12474 is fixed.
|
| + var uri = 'http://localhost:${server.port}/packages/${library.package}/$path';
|
| + var code = 'import "$uri";' +
|
| + _TRANSFORMER_ISOLATE.replaceAll('<<PORT>>', server.port.toString());
|
| + log.fine("Loading transformers from $library");
|
| + return dart.runInIsolate(code).then((sendPort) {
|
| + return _receiveFuture(sendPort.call(uri)).then((transformers) {
|
| + transformers = transformers
|
| + .map((transformer) => new _ForeignTransformer(transformer))
|
| + .toSet();
|
| + log.fine("Transformers from $library: $transformers");
|
| + return transformers;
|
| + });
|
| + }).catchError((error) {
|
| + if (error is! CrossIsolateException) throw error;
|
| + if (error.type != 'IsolateSpawnException') throw error;
|
| + // TODO(nweiz): don't parse this as a string once issues 12617 and 12689 are
|
| + // fixed.
|
| + if (!error.message.split('\n')[1].startsWith('import "$uri";')) {
|
| + throw error;
|
| + }
|
| +
|
| + // If there was an IsolateSpawnException and the import that actually failed
|
| + // was the one we were loading transformers from, throw an application
|
| + // exception with a more user-friendly message.
|
| + fail('Transformer library "package:${library.package}/$path" not found.');
|
| + });
|
| +}
|
| +
|
| +/// A wrapper for a transformer that's in a different isolate.
|
| +class _ForeignTransformer implements Transformer {
|
| + /// The port with which we communicate with the child isolate.
|
| + ///
|
| + /// This port and all messages sent across it are specific to this
|
| + /// transformer.
|
| + final SendPort _port;
|
| +
|
| + /// The result of calling [toString] on the transformer in the isolate.
|
| + final String _toString;
|
| +
|
| + _ForeignTransformer(Map map)
|
| + : _port = map['port'],
|
| + _toString = map['toString'];
|
| +
|
| + Future<bool> isPrimary(Asset asset) {
|
| + return _receiveFuture(_port.call({
|
| + 'type': 'isPrimary',
|
| + 'asset': _serializeAsset(asset)
|
| + }));
|
| + }
|
| +
|
| + Future apply(Transform transform) {
|
| + return _receiveFuture(_port.call({
|
| + 'type': 'apply',
|
| + 'transform': _serializeTransform(transform)
|
| + }));
|
| + }
|
| +
|
| + String toString() => _toString;
|
| +}
|
| +
|
| +/// Converts [transform] into a serializable map.
|
| +Map _serializeTransform(Transform transform) {
|
| + var receivePort = new ReceivePort();
|
| + receivePort.receive((message, replyTo) {
|
| + if (message['type'] == 'getInput') {
|
| + _sendFuture(replyTo, transform.getInput(_deserializeId(message['id']))
|
| + .then(_serializeAsset));
|
| + } else {
|
| + assert(message['type'] == 'addOutput');
|
| + transform.addOutput(_deserializeAsset(message['output']));
|
| + }
|
| + });
|
| +
|
| + return {
|
| + 'port': receivePort.toSendPort(),
|
| + 'primaryId': _serializeId(transform.primaryId)
|
| + };
|
| +}
|
| +
|
| +/// Converts a serializable map into an [Asset].
|
| +Asset _deserializeAsset(Map asset) {
|
| + var box = new MessageBox();
|
| + asset['sink'].add(box.sink);
|
| + return new Asset.fromStream(_deserializeId(asset['id']), box.stream);
|
| +}
|
| +
|
| +/// Converts a serializable map into an [AssetId].
|
| +AssetId _deserializeId(Map id) => new AssetId(id['package'], id['path']);
|
| +
|
| +// TODO(nweiz): add custom serialization code for assets that can be more
|
| +// efficiently serialized.
|
| +/// Converts [asset] into a serializable map.
|
| +Map _serializeAsset(Asset asset) {
|
| + // We can't send IsolateStreams (issue 12437), so instead we send a sink and
|
| + // get the isolate to send us back another sink.
|
| + var box = new MessageBox();
|
| + box.stream.first.then((sink) {
|
| + asset.read().listen(sink.add,
|
| + onError: sink.addError,
|
| + onDone: sink.close);
|
| + });
|
| +
|
| + return {
|
| + 'id': _serializeId(asset.id),
|
| + 'sink': box.sink
|
| + };
|
| +}
|
| +
|
| +/// Converts [id] into a serializable map.
|
| +Map _serializeId(AssetId id) => {'package': id.package, 'path': id.path};
|
| +
|
| +/// Sends the result of [future] through [port].
|
| +///
|
| +/// This should be received on the other end using [_receiveFuture]. It
|
| +/// re-raises any exceptions on the other side as [CrossIsolateException]s.
|
| +void _sendFuture(SendPort port, Future future) {
|
| + future.then((result) {
|
| + port.send({'success': result});
|
| + }).catchError((error) {
|
| + // TODO(nweiz): at least MissingInputException should be preserved here.
|
| + port.send({'error': CrossIsolateException.serialize(error)});
|
| + });
|
| +}
|
| +
|
| +/// Receives the result of [_sendFuture] from [portCall], which should be the
|
| +/// return value of [SendPort.call].
|
| +Future _receiveFuture(Future portCall) {
|
| + return portCall.then((response) {
|
| + if (response.containsKey('success')) return response['success'];
|
| + return new Future.error(
|
| + new dart.CrossIsolateException.deserialize(response['error']));
|
| + });
|
| +}
|
|
|