| // 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 Asset primaryInput; |
| |
| // 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'], |
| primaryInput = _deserializeAsset(transform['primaryInput']); |
| |
| 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 [mirror] is a subtype of [superclass]. |
| /// |
| /// 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(), |
| 'primaryInput': _serializeAsset(transform.primaryInput) |
| }; |
| } |
| |
| /// 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'])); |
| }); |
| } |