| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library pub.transformer_isolate; | 5 library pub.asset.transformer_isolate; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:convert'; |
| 8 import 'dart:isolate'; | 8 import 'dart:isolate'; |
| 9 import 'dart:convert'; | |
| 10 import 'dart:mirrors'; | 9 import 'dart:mirrors'; |
| 11 | 10 |
| 12 import 'package:source_maps/span.dart'; | |
| 13 import 'package:stack_trace/stack_trace.dart'; | |
| 14 import 'package:barback/barback.dart'; | 11 import 'package:barback/barback.dart'; |
| 15 // TODO(nweiz): don't import from "src" once issue 14966 is fixed. | 12 |
| 16 import 'package:barback/src/internal_asset.dart'; | 13 import 'serialize.dart'; |
| 17 | 14 |
| 18 /// Sets up the initial communication with the host isolate. | 15 /// Sets up the initial communication with the host isolate. |
| 19 void loadTransformers(SendPort replyTo) { | 16 void loadTransformers(SendPort replyTo) { |
| 20 var port = new ReceivePort(); | 17 var port = new ReceivePort(); |
| 21 replyTo.send(port.sendPort); | 18 replyTo.send(port.sendPort); |
| 22 port.first.then((wrappedMessage) { | 19 port.first.then((wrappedMessage) { |
| 23 _respond(wrappedMessage, (message) { | 20 respond(wrappedMessage, (message) { |
| 24 var library = Uri.parse(message['library']); | 21 var library = Uri.parse(message['library']); |
| 25 var configuration = JSON.decode(message['configuration']); | 22 var configuration = JSON.decode(message['configuration']); |
| 26 var mode = new BarbackMode(message['mode']); | 23 var mode = new BarbackMode(message['mode']); |
| 27 return _initialize(library, configuration, mode). | 24 return _initialize(library, configuration, mode). |
| 28 map(_serializeTransformerOrGroup).toList(); | 25 map(serializeTransformerOrGroup).toList(); |
| 29 }); | 26 }); |
| 30 }); | 27 }); |
| 31 } | 28 } |
| 32 | 29 |
| 33 /// Loads all the transformers and groups defined in [uri]. | 30 /// Loads all the transformers and groups defined in [uri]. |
| 34 /// | 31 /// |
| 35 /// Loads the library, finds any Transformer or TransformerGroup subclasses in | 32 /// Loads the library, finds any Transformer or TransformerGroup subclasses in |
| 36 /// it, instantiates them with [configuration] and [mode], and returns them. | 33 /// it, instantiates them with [configuration] and [mode], and returns them. |
| 37 Iterable _initialize(Uri uri, Map configuration, BarbackMode mode) { | 34 Iterable _initialize(Uri uri, Map configuration, BarbackMode mode) { |
| 38 var mirrors = currentMirrorSystem(); | 35 var mirrors = currentMirrorSystem(); |
| 39 var transformerClass = reflectClass(Transformer); | 36 var transformerClass = reflectClass(Transformer); |
| 40 var groupClass = reflectClass(TransformerGroup); | 37 var groupClass = reflectClass(TransformerGroup); |
| 41 | 38 |
| 42 // TODO(nweiz): if no valid transformers are found, throw an error message | 39 // TODO(nweiz): if no valid transformers are found, throw an error message |
| 43 // describing candidates and why they were rejected. | 40 // describing candidates and why they were rejected. |
| 44 return mirrors.libraries[uri].declarations.values.map((declaration) { | 41 return mirrors.libraries[uri].declarations.values.map((declaration) { |
| 45 if (declaration is! ClassMirror) return null; | 42 if (declaration is! ClassMirror) return null; |
| 46 var classMirror = declaration; | 43 var classMirror = declaration; |
| 47 if (classMirror.isPrivate) return null; | 44 if (classMirror.isPrivate) return null; |
| 48 if (classMirror.isAbstract) return null; | 45 if (classMirror.isAbstract) return null; |
| 49 if (!classIsA(classMirror, transformerClass) && | 46 if (!classMirror.isSubtypeOf(transformerClass) && |
| 50 !classIsA(classMirror, groupClass)) { | 47 !classMirror.isSubtypeOf(groupClass)) { |
| 51 return null; | 48 return null; |
| 52 } | 49 } |
| 53 | 50 |
| 54 var constructor = getConstructor(classMirror, 'asPlugin'); | 51 var constructor = _getConstructor(classMirror, 'asPlugin'); |
| 55 if (constructor == null) return null; | 52 if (constructor == null) return null; |
| 56 if (constructor.parameters.isEmpty) { | 53 if (constructor.parameters.isEmpty) { |
| 57 if (configuration.isNotEmpty) return null; | 54 if (configuration.isNotEmpty) return null; |
| 58 return classMirror.newInstance(const Symbol('asPlugin'), []).reflectee; | 55 return classMirror.newInstance(const Symbol('asPlugin'), []).reflectee; |
| 59 } | 56 } |
| 60 if (constructor.parameters.length != 1) return null; | 57 if (constructor.parameters.length != 1) return null; |
| 61 | 58 |
| 62 return classMirror.newInstance(const Symbol('asPlugin'), | 59 return classMirror.newInstance(const Symbol('asPlugin'), |
| 63 [new BarbackSettings(configuration, mode)]).reflectee; | 60 [new BarbackSettings(configuration, mode)]).reflectee; |
| 64 }).where((classMirror) => classMirror != null); | 61 }).where((classMirror) => classMirror != null); |
| 65 } | 62 } |
| 66 | 63 |
| 67 /// A wrapper for a [Transform] that's in the host isolate. | |
| 68 /// | |
| 69 /// This retrieves inputs from and sends outputs and logs to the host isolate. | |
| 70 class ForeignTransform implements Transform { | |
| 71 /// The port with which we communicate with the host isolate. | |
| 72 /// | |
| 73 /// This port and all messages sent across it are specific to this transform. | |
| 74 final SendPort _port; | |
| 75 | |
| 76 final Asset primaryInput; | |
| 77 | |
| 78 TransformLogger get logger => _logger; | |
| 79 TransformLogger _logger; | |
| 80 | |
| 81 /// Creates a transform from a serializable map sent from the host isolate. | |
| 82 ForeignTransform(Map transform) | |
| 83 : _port = transform['port'], | |
| 84 primaryInput = deserializeAsset(transform['primaryInput']) { | |
| 85 _logger = new TransformLogger((assetId, level, message, span) { | |
| 86 _call(_port, { | |
| 87 'type': 'log', | |
| 88 'level': level.name, | |
| 89 'message': message, | |
| 90 'assetId': assetId == null ? null : _serializeId(assetId), | |
| 91 'span': span == null ? null : _serializeSpan(span) | |
| 92 }); | |
| 93 }); | |
| 94 } | |
| 95 | |
| 96 Future<Asset> getInput(AssetId id) { | |
| 97 return _call(_port, { | |
| 98 'type': 'getInput', | |
| 99 'id': _serializeId(id) | |
| 100 }).then(deserializeAsset); | |
| 101 } | |
| 102 | |
| 103 Future<String> readInputAsString(AssetId id, {Encoding encoding}) { | |
| 104 if (encoding == null) encoding = UTF8; | |
| 105 return getInput(id).then((input) => input.readAsString(encoding: encoding)); | |
| 106 } | |
| 107 | |
| 108 Stream<List<int>> readInput(AssetId id) => | |
| 109 _futureStream(getInput(id).then((input) => input.read())); | |
| 110 | |
| 111 Future<bool> hasInput(AssetId id) { | |
| 112 return getInput(id).then((_) => true).catchError((error) { | |
| 113 if (error is AssetNotFoundException && error.id == id) return false; | |
| 114 throw error; | |
| 115 }); | |
| 116 } | |
| 117 | |
| 118 void addOutput(Asset output) { | |
| 119 _call(_port, { | |
| 120 'type': 'addOutput', | |
| 121 'output': serializeAsset(output) | |
| 122 }); | |
| 123 } | |
| 124 | |
| 125 void consumePrimary() { | |
| 126 _call(_port, {'type': 'consumePrimary'}); | |
| 127 } | |
| 128 } | |
| 129 | |
| 130 /// Returns the mirror for the root Object type. | |
| 131 ClassMirror get objectMirror => reflectClass(Object); | |
| 132 | |
| 133 // TODO(nweiz): clean this up when issue 13248 is fixed. | 64 // TODO(nweiz): clean this up when issue 13248 is fixed. |
| 134 MethodMirror getConstructor(ClassMirror classMirror, String constructor) { | 65 MethodMirror _getConstructor(ClassMirror classMirror, String constructor) { |
| 135 var name = new Symbol("${MirrorSystem.getName(classMirror.simpleName)}" | 66 var name = new Symbol("${MirrorSystem.getName(classMirror.simpleName)}" |
| 136 ".$constructor"); | 67 ".$constructor"); |
| 137 var candidate = classMirror.declarations[name]; | 68 var candidate = classMirror.declarations[name]; |
| 138 if (candidate is MethodMirror && candidate.isConstructor) return candidate; | 69 if (candidate is MethodMirror && candidate.isConstructor) return candidate; |
| 139 return null; | 70 return null; |
| 140 } | 71 } |
| 141 | |
| 142 // TODO(nweiz): get rid of this when issue 12439 is fixed. | |
| 143 /// Returns whether or not [mirror] is a subtype of [superclass]. | |
| 144 /// | |
| 145 /// This includes [superclass] being mixed in to or implemented by [mirror]. | |
| 146 bool classIsA(ClassMirror mirror, ClassMirror superclass) { | |
| 147 if (mirror == superclass) return true; | |
| 148 if (mirror == objectMirror) return false; | |
| 149 return classIsA(mirror.superclass, superclass) || | |
| 150 mirror.superinterfaces.any((int) => classIsA(int, superclass)); | |
| 151 } | |
| 152 | |
| 153 /// Converts [transformerOrGroup] into a serializable map. | |
| 154 Map _serializeTransformerOrGroup(transformerOrGroup) { | |
| 155 if (transformerOrGroup is Transformer) { | |
| 156 return _serializeTransformer(transformerOrGroup); | |
| 157 } else { | |
| 158 assert(transformerOrGroup is TransformerGroup); | |
| 159 return _serializeTransformerGroup(transformerOrGroup); | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 /// Converts [transformer] into a serializable map. | |
| 164 Map _serializeTransformer(Transformer transformer) { | |
| 165 var port = new ReceivePort(); | |
| 166 port.listen((wrappedMessage) { | |
| 167 _respond(wrappedMessage, (message) { | |
| 168 if (message['type'] == 'isPrimary') { | |
| 169 return transformer.isPrimary(deserializeAsset(message['asset'])); | |
| 170 } else { | |
| 171 assert(message['type'] == 'apply'); | |
| 172 | |
| 173 // Make sure we return null so that if the transformer's [apply] returns | |
| 174 // a non-serializable value it doesn't cause problems. | |
| 175 return transformer.apply( | |
| 176 new ForeignTransform(message['transform'])).then((_) => null); | |
| 177 } | |
| 178 }); | |
| 179 }); | |
| 180 | |
| 181 return { | |
| 182 'type': 'Transformer', | |
| 183 'toString': transformer.toString(), | |
| 184 'port': port.sendPort | |
| 185 }; | |
| 186 } | |
| 187 | |
| 188 // Converts [group] into a serializable map. | |
| 189 Map _serializeTransformerGroup(TransformerGroup group) { | |
| 190 return { | |
| 191 'type': 'TransformerGroup', | |
| 192 'toString': group.toString(), | |
| 193 'phases': group.phases.map((phase) { | |
| 194 return phase.map(_serializeTransformerOrGroup).toList(); | |
| 195 }).toList() | |
| 196 }; | |
| 197 } | |
| 198 | |
| 199 /// Converts a serializable map into an [AssetId]. | |
| 200 AssetId _deserializeId(Map id) => new AssetId(id['package'], id['path']); | |
| 201 | |
| 202 /// Converts [id] into a serializable map. | |
| 203 Map _serializeId(AssetId id) => {'package': id.package, 'path': id.path}; | |
| 204 | |
| 205 /// Converts [span] into a serializable map. | |
| 206 Map _serializeSpan(Span span) { | |
| 207 // TODO(nweiz): convert FileSpans to FileSpans. | |
| 208 return { | |
| 209 'type': 'fixed', | |
| 210 'sourceUrl': span.sourceUrl, | |
| 211 'start': _serializeLocation(span.start), | |
| 212 'text': span.text, | |
| 213 'isIdentifier': span.isIdentifier | |
| 214 }; | |
| 215 } | |
| 216 | |
| 217 /// Converts [location] into a serializable map. | |
| 218 Map _serializeLocation(Location location) { | |
| 219 // TODO(nweiz): convert FileLocations to FileLocations. | |
| 220 return { | |
| 221 'type': 'fixed', | |
| 222 'sourceUrl': location.sourceUrl, | |
| 223 'offset': location.offset, | |
| 224 'line': location.line, | |
| 225 'column': location.column | |
| 226 }; | |
| 227 } | |
| 228 | |
| 229 /// Responds to a message sent by [_call]. | |
| 230 /// | |
| 231 /// [wrappedMessage] is the raw message sent by [_call]. This unwraps it and | |
| 232 /// passes the contents of the message to [callback], then sends the return | |
| 233 /// value of [callback] back to [_call]. If [callback] returns a Future or | |
| 234 /// throws an error, that will also be sent. | |
| 235 void _respond(wrappedMessage, callback(message)) { | |
| 236 var replyTo = wrappedMessage['replyTo']; | |
| 237 new Future.sync(() => callback(wrappedMessage['message'])) | |
| 238 .then((result) => replyTo.send({'type': 'success', 'value': result})) | |
| 239 .catchError((error, stackTrace) { | |
| 240 replyTo.send({ | |
| 241 'type': 'error', | |
| 242 'error': _serializeException(error, stackTrace) | |
| 243 }); | |
| 244 }); | |
| 245 } | |
| 246 | |
| 247 /// Wraps [message] and sends it across [port], then waits for a response which | |
| 248 /// should be sent using [_respond]. | |
| 249 /// | |
| 250 /// The returned Future will complete to the value or error returned by | |
| 251 /// [_respond]. | |
| 252 Future _call(SendPort port, message) { | |
| 253 var receivePort = new ReceivePort(); | |
| 254 port.send({ | |
| 255 'message': message, | |
| 256 'replyTo': receivePort.sendPort | |
| 257 }); | |
| 258 | |
| 259 return receivePort.first.then((response) { | |
| 260 if (response['type'] == 'success') return response['value']; | |
| 261 assert(response['type'] == 'error'); | |
| 262 var exception = _deserializeException(response['error']); | |
| 263 return new Future.error(exception, exception.stackTrace); | |
| 264 }); | |
| 265 } | |
| 266 | |
| 267 /// An exception that was originally raised in another isolate. | |
| 268 /// | |
| 269 /// Exception objects can't cross isolate boundaries in general, so this class | |
| 270 /// wraps as much information as can be consistently serialized. | |
| 271 class CrossIsolateException implements Exception { | |
| 272 /// The name of the type of exception thrown. | |
| 273 /// | |
| 274 /// This is the return value of [error.runtimeType.toString()]. Keep in mind | |
| 275 /// that objects in different libraries may have the same type name. | |
| 276 final String type; | |
| 277 | |
| 278 /// The exception's message, or its [toString] if it didn't expose a `message` | |
| 279 /// property. | |
| 280 final String message; | |
| 281 | |
| 282 /// The exception's stack chain, or `null` if no stack chain was available. | |
| 283 final Chain stackTrace; | |
| 284 | |
| 285 /// Loads a [CrossIsolateException] from a serialized representation. | |
| 286 /// | |
| 287 /// [error] should be the result of [CrossIsolateException.serialize]. | |
| 288 CrossIsolateException.deserialize(Map error) | |
| 289 : type = error['type'], | |
| 290 message = error['message'], | |
| 291 stackTrace = error['stack'] == null ? null : | |
| 292 new Chain.parse(error['stack']); | |
| 293 | |
| 294 /// Serializes [error] to an object that can safely be passed across isolate | |
| 295 /// boundaries. | |
| 296 static Map serialize(error, [StackTrace stack]) { | |
| 297 if (stack == null && error is Error) stack = error.stackTrace; | |
| 298 return { | |
| 299 'type': error.runtimeType.toString(), | |
| 300 'message': getErrorMessage(error), | |
| 301 'stack': stack == null ? null : new Chain.forTrace(stack).toString() | |
| 302 }; | |
| 303 } | |
| 304 | |
| 305 String toString() => "$message\n$stackTrace"; | |
| 306 } | |
| 307 | |
| 308 /// An [AssetNotFoundException] that was originally raised in another isolate. | |
| 309 class _CrossIsolateAssetNotFoundException extends CrossIsolateException | |
| 310 implements AssetNotFoundException { | |
| 311 final AssetId id; | |
| 312 | |
| 313 String get message => "Could not find asset $id."; | |
| 314 | |
| 315 /// Loads a [_CrossIsolateAssetNotFoundException] from a serialized | |
| 316 /// representation. | |
| 317 /// | |
| 318 /// [error] should be the result of | |
| 319 /// [_CrossIsolateAssetNotFoundException.serialize]. | |
| 320 _CrossIsolateAssetNotFoundException.deserialize(Map error) | |
| 321 : id = new AssetId(error['package'], error['path']), | |
| 322 super.deserialize(error); | |
| 323 | |
| 324 /// Serializes [error] to an object that can safely be passed across isolate | |
| 325 /// boundaries. | |
| 326 static Map serialize(AssetNotFoundException error, [StackTrace stack]) { | |
| 327 var map = CrossIsolateException.serialize(error); | |
| 328 map['package'] = error.id.package; | |
| 329 map['path'] = error.id.path; | |
| 330 return map; | |
| 331 } | |
| 332 } | |
| 333 | |
| 334 /// Serializes [error] to an object that can safely be passed across isolate | |
| 335 /// boundaries. | |
| 336 /// | |
| 337 /// This handles [AssetNotFoundException]s specially, ensuring that their | |
| 338 /// metadata is preserved. | |
| 339 Map _serializeException(error, [StackTrace stack]) { | |
| 340 if (error is AssetNotFoundException) { | |
| 341 return _CrossIsolateAssetNotFoundException.serialize(error, stack); | |
| 342 } else { | |
| 343 return CrossIsolateException.serialize(error, stack); | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 /// Loads an exception from a serialized representation. | |
| 348 /// | |
| 349 /// This handles [AssetNotFoundException]s specially, ensuring that their | |
| 350 /// metadata is preserved. | |
| 351 CrossIsolateException _deserializeException(Map error) { | |
| 352 if (error['type'] == 'AssetNotFoundException') { | |
| 353 return new _CrossIsolateAssetNotFoundException.deserialize(error); | |
| 354 } else { | |
| 355 return new CrossIsolateException.deserialize(error); | |
| 356 } | |
| 357 } | |
| 358 | |
| 359 /// A regular expression to match the exception prefix that some exceptions' | |
| 360 /// [Object.toString] values contain. | |
| 361 final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): '); | |
| 362 | |
| 363 /// Get a string description of an exception. | |
| 364 /// | |
| 365 /// Many exceptions include the exception class name at the beginning of their | |
| 366 /// [toString], so we remove that if it exists. | |
| 367 String getErrorMessage(error) => | |
| 368 error.toString().replaceFirst(_exceptionPrefix, ''); | |
| 369 | |
| 370 /// Returns a buffered stream that will emit the same values as the stream | |
| 371 /// returned by [future] once [future] completes. If [future] completes to an | |
| 372 /// error, the return value will emit that error and then close. | |
| 373 Stream _futureStream(Future<Stream> future) { | |
| 374 var controller = new StreamController(sync: true); | |
| 375 future.then((stream) { | |
| 376 stream.listen( | |
| 377 controller.add, | |
| 378 onError: controller.addError, | |
| 379 onDone: controller.close); | |
| 380 }).catchError((e, stackTrace) { | |
| 381 controller.addError(e, stackTrace); | |
| 382 controller.close(); | |
| 383 }); | |
| 384 return controller.stream; | |
| 385 } | |
| 386 | |
| 387 Stream callbackStream(Stream callback()) { | |
| 388 var subscription; | |
| 389 var controller; | |
| 390 controller = new StreamController(onListen: () { | |
| 391 subscription = callback().listen(controller.add, | |
| 392 onError: controller.addError, | |
| 393 onDone: controller.close); | |
| 394 }, | |
| 395 onCancel: () => subscription.cancel(), | |
| 396 onPause: () => subscription.pause(), | |
| 397 onResume: () => subscription.resume(), | |
| 398 sync: true); | |
| 399 return controller.stream; | |
| 400 } | |
| OLD | NEW |