OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, 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 /// Helper functionality to make working with IO easier. | 5 /// Helper functionality to make working with IO easier. |
6 library io; | 6 library io; |
7 | 7 |
8 import 'dart:async'; | 8 import 'dart:async'; |
9 import 'dart:io'; | 9 import 'dart:io'; |
10 import 'dart:isolate'; | 10 import 'dart:isolate'; |
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
74 ..closeSync(); | 74 ..closeSync(); |
75 log.fine("Wrote text file $file."); | 75 log.fine("Wrote text file $file."); |
76 return file; | 76 return file; |
77 } | 77 } |
78 | 78 |
79 /// Writes [stream] to a new file at path [file]. Will replace any file already | 79 /// Writes [stream] to a new file at path [file]. Will replace any file already |
80 /// at that path. Completes when the file is done being written. | 80 /// at that path. Completes when the file is done being written. |
81 Future<String> createFileFromStream(Stream<List<int>> stream, String file) { | 81 Future<String> createFileFromStream(Stream<List<int>> stream, String file) { |
82 log.io("Creating $file from stream."); | 82 log.io("Creating $file from stream."); |
83 | 83 |
84 var outputStream = new File(file).openOutputStream(); | 84 return stream.pipe(new File(file).openWrite()).then((_) { |
85 return stream.pipe(wrapOutputStream(outputStream)).then((_) { | |
86 log.fine("Created $file from stream."); | 85 log.fine("Created $file from stream."); |
87 return file; | 86 return file; |
88 }); | 87 }); |
89 } | 88 } |
90 | 89 |
91 /// Creates a directory [dir]. | 90 /// Creates a directory [dir]. |
92 String createDir(String dir) { | 91 String createDir(String dir) { |
93 new Directory(dir).createSync(); | 92 new Directory(dir).createSync(); |
94 return dir; | 93 return dir; |
95 } | 94 } |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
151 if (listedDirectories.contains(resolvedPath)) { | 150 if (listedDirectories.contains(resolvedPath)) { |
152 return new Future.immediate([]); | 151 return new Future.immediate([]); |
153 } | 152 } |
154 | 153 |
155 listedDirectories = new Set<String>.from(listedDirectories); | 154 listedDirectories = new Set<String>.from(listedDirectories); |
156 listedDirectories.add(resolvedPath); | 155 listedDirectories.add(resolvedPath); |
157 | 156 |
158 log.io("Listing directory $dir."); | 157 log.io("Listing directory $dir."); |
159 var lister = new Directory(dir).list(); | 158 var lister = new Directory(dir).list(); |
160 | 159 |
161 lister.onDone = (done) { | |
162 // TODO(rnystrom): May need to sort here if it turns out onDir and onFile | |
163 // aren't guaranteed to be called in a certain order. So far, they seem to
. | |
164 if (done) { | |
165 log.fine("Listed directory $dir:\n${contents.join('\n')}"); | |
166 completer.complete(contents); | |
167 } | |
168 }; | |
169 | |
170 // TODO(nweiz): remove this when issue 4061 is fixed. | |
171 var stackTrace; | |
172 try { | |
173 throw ""; | |
174 } catch (_, localStackTrace) { | |
175 stackTrace = localStackTrace; | |
176 } | |
177 | |
178 var children = []; | 160 var children = []; |
179 lister.onError = (error) => completer.completeError(error, stackTrace); | 161 lister.listen( |
180 lister.onDir = (file) { | 162 (entity) { |
181 if (!includeHiddenFiles && path.basename(file).startsWith('.')) return; | 163 if (entity is File) { |
182 file = path.join(dir, path.basename(file)); | 164 var file = entity.name; |
183 contents.add(file); | 165 if (!includeHiddenFiles && path.basename(file).startsWith('.')) { |
184 // TODO(nweiz): don't manually recurse once issue 7358 is fixed. Note that | 166 return; |
185 // once we remove the manual recursion, we'll need to explicitly filter | 167 } |
186 // out files in hidden directories. | 168 contents.add(path.join(dir, path.basename(file))); |
187 if (recursive) { | 169 } else if (entity is Directory) { |
188 children.add(doList(file, listedDirectories)); | 170 var file = entity.path; |
189 } | 171 if (!includeHiddenFiles && path.basename(file).startsWith('.')) { |
190 }; | 172 return; |
191 | 173 } |
192 lister.onFile = (file) { | 174 file = path.join(dir, path.basename(file)); |
193 if (!includeHiddenFiles && path.basename(file).startsWith('.')) return; | 175 contents.add(file); |
194 contents.add(path.join(dir, path.basename(file))); | 176 // TODO(nweiz): don't manually recurse once issue 7358 is fixed. |
195 }; | 177 // Note that once we remove the manual recursion, we'll need to |
| 178 // explicitly filter out files in hidden directories. |
| 179 if (recursive) { |
| 180 children.add(doList(file, listedDirectories)); |
| 181 } |
| 182 } |
| 183 }, |
| 184 onDone: () { |
| 185 // TODO(rnystrom): May need to sort here if it turns out |
| 186 // onDir and onFile aren't guaranteed to be called in a |
| 187 // certain order. So far, they seem to. |
| 188 log.fine("Listed directory $dir:\n${contents.join('\n')}"); |
| 189 completer.complete(contents); |
| 190 }, |
| 191 onError: (error) => completer.completeError(error, stackTrace)); |
196 | 192 |
197 return completer.future.then((contents) { | 193 return completer.future.then((contents) { |
198 return Future.wait(children).then((childContents) { | 194 return Future.wait(children).then((childContents) { |
199 contents.addAll(flatten(childContents)); | 195 contents.addAll(flatten(childContents)); |
200 return contents; | 196 return contents; |
201 }); | 197 }); |
202 }); | 198 }); |
203 } | 199 } |
204 | 200 |
205 return doList(dir, new Set<String>()); | 201 return doList(dir, new Set<String>()); |
(...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
357 /// A sink that writes to standard output. Errors piped to this stream will be | 353 /// A sink that writes to standard output. Errors piped to this stream will be |
358 /// surfaced to the top-level error handler. | 354 /// surfaced to the top-level error handler. |
359 final StreamSink<List<int>> stdoutSink = _wrapStdio(stdout, "stdout"); | 355 final StreamSink<List<int>> stdoutSink = _wrapStdio(stdout, "stdout"); |
360 | 356 |
361 /// A sink that writes to standard error. Errors piped to this stream will be | 357 /// A sink that writes to standard error. Errors piped to this stream will be |
362 /// surfaced to the top-level error handler. | 358 /// surfaced to the top-level error handler. |
363 final StreamSink<List<int>> stderrSink = _wrapStdio(stderr, "stderr"); | 359 final StreamSink<List<int>> stderrSink = _wrapStdio(stderr, "stderr"); |
364 | 360 |
365 /// Wrap the standard output or error [stream] in a [StreamSink]. Any errors are | 361 /// Wrap the standard output or error [stream] in a [StreamSink]. Any errors are |
366 /// logged, and then the program is terminated. [name] is used for debugging. | 362 /// logged, and then the program is terminated. [name] is used for debugging. |
367 StreamSink<List<int>> _wrapStdio(OutputStream stream, String name) { | 363 StreamSink<List<int>> _wrapStdio(IOSink sink, String name) { |
368 var pair = consumerToSink(wrapOutputStream(stream)); | 364 var pair = consumerToSink(sink); |
369 pair.last.catchError((e) { | 365 pair.last.catchError((e) { |
370 // This log may or may not work, depending on how the stream failed. Not | 366 // This log may or may not work, depending on how the stream failed. Not |
371 // much we can do about that. | 367 // much we can do about that. |
372 log.error("Error writing to $name: $e"); | 368 log.error("Error writing to $name: $e"); |
373 exit(exit_codes.IO); | 369 exit(exit_codes.IO); |
374 }); | 370 }); |
375 return pair.first; | 371 return pair.first; |
376 } | 372 } |
377 | 373 |
378 /// A line-by-line stream of standard input. | 374 /// A line-by-line stream of standard input. |
379 final Stream<String> stdinLines = | 375 final Stream<String> stdinLines = streamToLines( |
380 streamToLines(wrapInputStream(stdin).toStringStream()); | 376 new ByteStream(stdin).toStringStream()); |
381 | 377 |
382 /// Displays a message and reads a yes/no confirmation from the user. Returns | 378 /// Displays a message and reads a yes/no confirmation from the user. Returns |
383 /// a [Future] that completes to `true` if the user confirms or `false` if they | 379 /// a [Future] that completes to `true` if the user confirms or `false` if they |
384 /// do not. | 380 /// do not. |
385 /// | 381 /// |
386 /// This will automatically append " (y/n)?" to the message, so [message] | 382 /// This will automatically append " (y/n)?" to the message, so [message] |
387 /// should just be a fragment like, "Are you sure you want to proceed". | 383 /// should just be a fragment like, "Are you sure you want to proceed". |
388 Future<bool> confirm(String message) { | 384 Future<bool> confirm(String message) { |
389 log.fine('Showing confirm message: $message'); | 385 log.fine('Showing confirm message: $message'); |
390 stdoutSink.add("$message (y/n)? ".charCodes); | 386 stdoutSink.add("$message (y/n)? ".charCodes); |
391 return streamFirst(stdinLines) | 387 return streamFirst(stdinLines) |
392 .then((line) => new RegExp(r"^[yY]").hasMatch(line)); | 388 .then((line) => new RegExp(r"^[yY]").hasMatch(line)); |
393 } | 389 } |
394 | 390 |
395 /// Reads and discards all output from [inputStream]. Returns a [Future] that | 391 /// Reads and discards all output from [stream]. Returns a [Future] that |
396 /// completes when the stream is closed. | 392 /// completes when the stream is closed. |
397 Future drainInputStream(InputStream inputStream) { | 393 Future drainStream(Stream stream) { |
398 var completer = new Completer(); | 394 return stream.reduce(null, (x, y) {}); |
399 if (inputStream.closed) { | |
400 completer.complete(); | |
401 return completer.future; | |
402 } | |
403 | |
404 inputStream.onClosed = () => completer.complete(); | |
405 inputStream.onData = inputStream.read; | |
406 inputStream.onError = (error) => completer.completeError(error); | |
407 return completer.future; | |
408 } | |
409 | |
410 /// Wraps [stream] in a single-subscription [Stream] that emits the same data. | |
411 ByteStream wrapInputStream(InputStream stream) { | |
412 var controller = new StreamController(); | |
413 if (stream.closed) { | |
414 controller.close(); | |
415 return new ByteStream(controller.stream); | |
416 } | |
417 | |
418 stream.onClosed = controller.close; | |
419 stream.onData = () => controller.add(stream.read()); | |
420 stream.onError = (e) => controller.signalError(new AsyncError(e)); | |
421 return new ByteStream(controller.stream); | |
422 } | |
423 | |
424 /// Wraps [stream] in a [StreamConsumer] so that [Stream]s can by piped into it | |
425 /// using [Stream.pipe]. Errors piped to the returned [StreamConsumer] will be | |
426 /// forwarded to the [Future] returned by [Stream.pipe]. | |
427 StreamConsumer<List<int>, dynamic> wrapOutputStream(OutputStream stream) => | |
428 new _OutputStreamConsumer(stream); | |
429 | |
430 /// A [StreamConsumer] that pipes data into an [OutputStream]. | |
431 class _OutputStreamConsumer implements StreamConsumer<List<int>, dynamic> { | |
432 final OutputStream _outputStream; | |
433 | |
434 _OutputStreamConsumer(this._outputStream); | |
435 | |
436 Future consume(Stream<List<int>> stream) { | |
437 // TODO(nweiz): we have to manually keep track of whether or not the | |
438 // completer has completed since the output stream could signal an error | |
439 // after close() has been called but before it has shut down internally. See | |
440 // the following TODO. | |
441 var completed = false; | |
442 var completer = new Completer(); | |
443 stream.listen((data) { | |
444 // Writing empty data to a closed stream can cause errors. | |
445 if (data.isEmpty) return; | |
446 | |
447 // TODO(nweiz): remove this try/catch when issue 7836 is fixed. | |
448 try { | |
449 _outputStream.write(data); | |
450 } catch (e, stack) { | |
451 if (!completed) completer.completeError(e, stack); | |
452 completed = true; | |
453 } | |
454 }, onError: (e) { | |
455 if (!completed) completer.completeError(e.error, e.stackTrace); | |
456 completed = true; | |
457 }, onDone: () => _outputStream.close()); | |
458 | |
459 _outputStream.onError = (e) { | |
460 if (!completed) completer.completeError(e); | |
461 completed = true; | |
462 }; | |
463 | |
464 _outputStream.onClosed = () { | |
465 if (!completed) completer.complete(); | |
466 completed = true; | |
467 }; | |
468 | |
469 return completer.future; | |
470 } | |
471 } | 395 } |
472 | 396 |
473 /// Returns a [StreamSink] that pipes all data to [consumer] and a [Future] that | 397 /// Returns a [StreamSink] that pipes all data to [consumer] and a [Future] that |
474 /// will succeed when [StreamSink] is closed or fail with any errors that occur | 398 /// will succeed when [StreamSink] is closed or fail with any errors that occur |
475 /// while writing. | 399 /// while writing. |
476 Pair<StreamSink, Future> consumerToSink(StreamConsumer consumer) { | 400 Pair<StreamSink, Future> consumerToSink(StreamConsumer consumer) { |
477 var controller = new StreamController(); | 401 var controller = new StreamController(); |
478 var done = controller.stream.pipe(consumer); | 402 var done = controller.stream.pipe(consumer); |
479 return new Pair<StreamSink, Future>(controller.sink, done); | 403 return new Pair<StreamSink, Future>(controller.sink, done); |
480 } | 404 } |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
602 /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [stderr], so | 526 /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [stderr], so |
603 /// any error in process will be passed to it, but won't reach the top-level | 527 /// any error in process will be passed to it, but won't reach the top-level |
604 /// error handler unless nothing has handled it. | 528 /// error handler unless nothing has handled it. |
605 Future<int> get exitCode => _exitCode; | 529 Future<int> get exitCode => _exitCode; |
606 | 530 |
607 /// Creates a new [PubProcess] wrapping [process]. | 531 /// Creates a new [PubProcess] wrapping [process]. |
608 PubProcess(Process process) | 532 PubProcess(Process process) |
609 : _process = process { | 533 : _process = process { |
610 var errorGroup = new ErrorGroup(); | 534 var errorGroup = new ErrorGroup(); |
611 | 535 |
612 var pair = consumerToSink(wrapOutputStream(process.stdin)); | 536 var pair = consumerToSink(process.stdin); |
613 _stdin = pair.first; | 537 _stdin = pair.first; |
614 _stdinClosed = errorGroup.registerFuture(pair.last); | 538 _stdinClosed = errorGroup.registerFuture(pair.last); |
615 | 539 |
616 _stdout = new ByteStream( | 540 _stdout = new ByteStream( |
617 errorGroup.registerStream(wrapInputStream(process.stdout))); | 541 errorGroup.registerStream(process.stdout)); |
618 _stderr = new ByteStream( | 542 _stderr = new ByteStream( |
619 errorGroup.registerStream(wrapInputStream(process.stderr))); | 543 errorGroup.registerStream(process.stderr)); |
620 | 544 |
621 var exitCodeCompleter = new Completer(); | 545 var exitCodeCompleter = new Completer(); |
622 _exitCode = errorGroup.registerFuture(exitCodeCompleter.future); | 546 _exitCode = errorGroup.registerFuture(exitCodeCompleter.future); |
623 _process.onExit = (code) => exitCodeCompleter.complete(code); | 547 _process.exitCode.then((code) => exitCodeCompleter.complete(code)); |
624 } | 548 } |
625 | 549 |
626 /// Sends [signal] to the underlying process. | 550 /// Sends [signal] to the underlying process. |
627 bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => | 551 bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => |
628 _process.kill(signal); | 552 _process.kill(signal); |
629 } | 553 } |
630 | 554 |
631 /// Calls [fn] with appropriately modified arguments. [fn] should have the same | 555 /// Calls [fn] with appropriately modified arguments. [fn] should have the same |
632 /// signature as [Process.start], except that the returned [Future] may have a | 556 /// signature as [Process.start], except that the returned [Future] may have a |
633 /// type other than [Process]. | 557 /// type other than [Process]. |
(...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
789 /// Create a .tar.gz archive from a list of entries. Each entry can be a | 713 /// Create a .tar.gz archive from a list of entries. Each entry can be a |
790 /// [String], [Directory], or [File] object. The root of the archive is | 714 /// [String], [Directory], or [File] object. The root of the archive is |
791 /// considered to be [baseDir], which defaults to the current working directory. | 715 /// considered to be [baseDir], which defaults to the current working directory. |
792 /// Returns a [ByteStream] that will emit the contents of the archive. | 716 /// Returns a [ByteStream] that will emit the contents of the archive. |
793 ByteStream createTarGz(List contents, {baseDir}) { | 717 ByteStream createTarGz(List contents, {baseDir}) { |
794 var buffer = new StringBuffer(); | 718 var buffer = new StringBuffer(); |
795 buffer.add('Creating .tag.gz stream containing:\n'); | 719 buffer.add('Creating .tag.gz stream containing:\n'); |
796 contents.forEach((file) => buffer.add('$file\n')); | 720 contents.forEach((file) => buffer.add('$file\n')); |
797 log.fine(buffer.toString()); | 721 log.fine(buffer.toString()); |
798 | 722 |
799 // TODO(nweiz): Propagate errors to the returned stream (including non-zero | |
800 // exit codes). See issue 3657. | |
801 var controller = new StreamController<List<int>>(); | 723 var controller = new StreamController<List<int>>(); |
802 | 724 |
803 if (baseDir == null) baseDir = path.current; | 725 if (baseDir == null) baseDir = path.current; |
804 baseDir = path.absolute(baseDir); | 726 baseDir = path.absolute(baseDir); |
805 contents = contents.map((entry) { | 727 contents = contents.map((entry) { |
806 entry = path.absolute(entry); | 728 entry = path.absolute(entry); |
807 if (!isBeneath(entry, baseDir)) { | 729 if (!isBeneath(entry, baseDir)) { |
808 throw 'Entry $entry is not inside $baseDir.'; | 730 throw 'Entry $entry is not inside $baseDir.'; |
809 } | 731 } |
810 return path.relative(entry, from: baseDir); | 732 return path.relative(entry, from: baseDir); |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
882 const PubProcessResult(this.stdout, this.stderr, this.exitCode); | 804 const PubProcessResult(this.stdout, this.stderr, this.exitCode); |
883 | 805 |
884 bool get success => exitCode == 0; | 806 bool get success => exitCode == 0; |
885 } | 807 } |
886 | 808 |
887 /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. | 809 /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. |
888 Uri _getUri(uri) { | 810 Uri _getUri(uri) { |
889 if (uri is Uri) return uri; | 811 if (uri is Uri) return uri; |
890 return Uri.parse(uri); | 812 return Uri.parse(uri); |
891 } | 813 } |
OLD | NEW |