blob: 557dbbaf2e1fab7d39833f3d7de62d5f107e7b13 [file] [log] [blame]
// 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.load_transformers;
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:barback/barback.dart';
import '../barback.dart';
import '../dart.dart' as dart;
import '../log.dart' as log;
import '../utils.dart';
import 'server.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:convert';
import 'dart:mirrors';
import 'http://<<HOST_AND_PORT>>/packages/barback/barback.dart';
/// Sets up the initial communication with the host isolate.
void main() {
port.receive((args, replyTo) {
_sendFuture(replyTo, new Future.sync(() {
var library = Uri.parse(args['library']);
var configuration = JSON.decode(args['configuration']);
return initialize(library, configuration).
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 (with [configuration] if it's non-null), and return them.
Iterable<Transformer> initialize(Uri uri, Map configuration) {
var mirrors = currentMirrorSystem();
// TODO(nweiz): look this up by name once issue 5897 is fixed.
var transformerUri = Uri.parse(
'http://<<HOST_AND_PORT>>/packages/barback/src/transformer.dart');
var transformerClass = mirrors.libraries[transformerUri]
.classes[const Symbol('Transformer')];
// TODO(nweiz): if no valid transformers are found, throw an error message
// describing candidates and why they were rejected.
return mirrors.libraries[uri].classes.values.map((classMirror) {
if (classMirror.isPrivate) return null;
if (isAbstract(classMirror)) return null;
if (!classIsA(classMirror, transformerClass)) return null;
var constructor = getConstructor(classMirror, 'asPlugin');
if (constructor == null) return null;
if (constructor.parameters.isEmpty) {
if (configuration != null) return null;
return classMirror.newInstance(const Symbol('asPlugin'), []).reflectee;
}
if (constructor.parameters.length != 1) return null;
// If the constructor expects configuration and none was passed, it defaults
// to an empty map.
if (configuration == null) configuration = {};
// TODO(nweiz): if the constructor accepts named parameters, automatically
// destructure the configuration map.
return classMirror.newInstance(const Symbol('asPlugin'), [configuration])
.reflectee;
}).where((classMirror) => classMirror != null);
}
/// 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): clean this up when issue 13248 is fixed.
MethodMirror getConstructor(ClassMirror classMirror, String constructor) {
var name = new Symbol("\${MirrorSystem.getName(classMirror.simpleName)}"
".\$constructor");
return classMirror.constructors[name];
}
// 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 [id].
///
/// [server] is used to serve any Dart files needed to load the transformer.
Future<Set<Transformer>> loadTransformers(BarbackServer server,
TransformerId id) {
var path = id.asset.path.replaceFirst('lib/', '');
// TODO(nweiz): load from a "package:" URI when issue 12474 is fixed.
var hostAndPort = '${server.host}:${server.port}';
var uri = 'http://$hostAndPort/packages/${id.asset.package}/$path';
var code = 'import "$uri";' +
_TRANSFORMER_ISOLATE.replaceAll('<<HOST_AND_PORT>>', hostAndPort);
log.fine("Loading transformers from ${id.asset}");
return dart.runInIsolate(code).then((sendPort) {
return _receiveFuture(sendPort.call({
'library': uri,
// TODO(nweiz): support non-JSON-encodable configuration maps.
'configuration': JSON.encode(id.configuration)
})).then((transformers) {
transformers = transformers
.map((transformer) => new _ForeignTransformer(transformer))
.toSet();
log.fine("Transformers from ${id.asset}: $transformers");
return transformers;
});
}).catchError((error) {
if (error is! dart.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:${id.asset.package}/$path" not found.');
});
}
/// A wrapper for a transformer that's in a different isolate.
class _ForeignTransformer extends 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 [dart.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': dart.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']));
});
}