Chromium Code Reviews| 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..383fac08b9ca833b1079925c177df19fe8bd1c7b |
| --- /dev/null |
| +++ b/sdk/lib/_internal/pub/lib/src/barback/load_transformers.dart |
| @@ -0,0 +1,368 @@ |
| +// 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.wrap_transformer; |
| + |
| +import 'dart:async'; |
| +import 'dart:isolate'; |
| + |
| +import 'package:barback/barback.dart'; |
| +import 'package:stack_trace/stack_trace.dart'; |
| + |
| +import '../dart.dart' as dart; |
| + |
| +/// A Dart script to run in an isolate. |
| +/// |
| +/// This script wraps 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(_wrapTransformer).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((mirror) { |
|
Bob Nystrom
2013/08/27 22:12:30
"mirror" -> "classMirror".
nweiz
2013/08/28 20:45:23
Done.
|
| + if (mirror.isPrivate) return false; |
|
Bob Nystrom
2013/08/27 22:12:30
Might want to check and see if the class is abstra
nweiz
2013/08/28 20:45:23
Done.
|
| + if (!classIsA(mirror, transformerClass)) return false; |
| + var constructor = mirror.constructors[mirror.simpleName]; |
| + if (constructor == null) return false; |
| + if (!constructor.parameters.isEmpty) return false; |
|
Bob Nystrom
2013/08/27 22:12:30
Add a TODO to report this error to the user.
nweiz
2013/08/28 20:45:23
I don't think this should be an error. Users shoul
|
| + return true; |
| + }).map((mirror) => mirror.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 = _unwrapId(transform['primaryId']); |
| + |
| + Future<Asset> getInput(AssetId id) { |
| + return _receiveFuture(_port.call({'type': 'getInput', 'id': _wrapId(id)})) |
| + .then(_unwrapAsset); |
| + } |
| + |
| + void addOutput(Asset output) { |
| + _port.send({ |
| + 'type': 'addOutput', |
| + 'output': _wrapAsset(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; |
| + |
| +/// Returns whether or not [superclass] is a superclass of [mirror]. |
|
Bob Nystrom
2013/08/27 22:12:30
Clearer would be "[mirror] is an instance of [supe
nweiz
2013/08/28 20:45:23
A class isn't an instance of its superclasses.
Bob Nystrom
2013/08/29 00:12:01
I guess that depends on your definition of "instan
nweiz
2013/09/04 00:21:58
Done.
|
| +/// |
| +/// 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)); |
| +} |
| + |
| +/// Converts [transformer] into a serializable map. |
| +Map _wrapTransformer(Transformer transformer) { |
|
Bob Nystrom
2013/08/27 22:12:30
"wrap" -> "serialize", "unwrap" -> "deserialize" h
nweiz
2013/08/28 20:45:23
Done.
|
| + var port = new ReceivePort(); |
| + port.receive((message, replyTo) { |
| + _sendFuture(replyTo, new Future.sync(() { |
| + if (message['type'] == 'isPrimary') { |
| + return transformer.isPrimary(_unwrapAsset(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 _unwrapAsset(Map asset) { |
| + var box = new MessageBox(); |
| + asset['sink'].add(box.sink); |
| + return new Asset.fromStream(_unwrapId(asset['id']), box.stream); |
|
Bob Nystrom
2013/08/27 22:12:30
I really think we should serialize assets directly
nweiz
2013/08/28 20:45:23
I'll add a TODO for this.
|
| +} |
| + |
| +/// Converts a serializable map into an [AssetId]. |
| +AssetId _unwrapId(Map id) => new AssetId(id['package'], id['path']); |
|
Bob Nystrom
2013/08/27 22:12:30
I'd prefer adding a serialize() method directly on
nweiz
2013/08/28 20:45:23
I don't like the idea of just manually adding a se
Bob Nystrom
2013/08/29 00:12:01
True, but the nice thing is that third-party users
|
| + |
| +/// Converts [asset] into a serializable map. |
| +Map _wrapAsset(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': _wrapId(asset.id), |
| + 'sink': box.sink |
| + }; |
| +} |
| + |
| +/// Converts [id] into a serializable map. |
| +Map _wrapId(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)}); |
| + }); |
| +} |
| + |
| +/// Sends the result of [_sendFuture] from [portCall], which should be the |
|
Bob Nystrom
2013/08/27 22:12:30
"Sends" -> "Receives".
nweiz
2013/08/28 20:45:23
Done.
|
| +/// 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 uri = 'http://localhost:${server.port}/packages/${library.package}/' |
| + '${library.path.replaceFirst('lib/', '')}'; |
| + var code = 'import "$uri";' + |
| + _TRANSFORMER_ISOLATE.replaceAll('<<PORT>>', server.port.toString()); |
|
Bob Nystrom
2013/08/27 22:12:30
Add a TODO to do something less unholy here...
nweiz
2013/08/28 20:45:23
Done.
|
| + return dart.runInIsolate(code).then((sendPort) { |
| + return _receiveFuture(sendPort.call(uri)).then((transformers) { |
| + return transformers |
| + .map((transformer) => new _ForeignTransformer(transformer)) |
| + .toSet(); |
| + }); |
| + }); |
| +} |
| + |
| +/// A wrapper for a transformer that's in a different isolate. |
| +class _ForeignTransformer implements Transformer { |
| + /// The port with which we communicate with the host isolate. |
|
Bob Nystrom
2013/08/27 22:12:30
"host" -> "child"?
nweiz
2013/08/28 20:45:23
Done.
|
| + /// |
| + /// 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': _wrapAsset(asset) |
| + })); |
| + } |
| + |
| + Future apply(Transform transform) { |
| + return _receiveFuture(_port.call({ |
| + 'type': 'apply', |
| + 'transform': _wrapTransform(transform) |
| + })); |
| + } |
| + |
| + String toString() => _toString; |
| +} |
| + |
| +/// Converts [transform] into a serializable map. |
| +Map _wrapTransform(Transform transform) { |
| + var receivePort = new ReceivePort(); |
| + receivePort.receive((message, replyTo) { |
| + if (message['type'] == 'getInput') { |
| + _sendFuture(replyTo, |
| + transform.getInput(_unwrapId(message['id'])).then(_wrapAsset)); |
| + } else { |
| + assert(message['type'] == 'addOutput'); |
| + transform.addOutput(_unwrapAsset(message['output'])); |
| + } |
| + }); |
| + |
| + return { |
| + 'port': receivePort.toSendPort(), |
| + 'primaryId': _wrapId(transform.primaryId) |
| + }; |
| +} |
| + |
| +/// Converts a serializable map into an [Asset]. |
| +Asset _unwrapAsset(Map asset) { |
| + var box = new MessageBox(); |
| + asset['sink'].add(box.sink); |
| + return new Asset.fromStream(_unwrapId(asset['id']), box.stream); |
| +} |
| + |
| +/// Converts a serializable map into an [AssetId]. |
| +AssetId _unwrapId(Map id) => new AssetId(id['package'], id['path']); |
| + |
| +/// Converts [asset] into a serializable map. |
| +Map _wrapAsset(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': _wrapId(asset.id), |
| + 'sink': box.sink |
| + }; |
| +} |
| + |
| +/// Converts [id] into a serializable map. |
| +Map _wrapId(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)}); |
| + }); |
| +} |
| + |
| +/// Sends 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'])); |
| + }); |
| +} |