| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 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. | |
| 4 | |
| 5 /// Helper functionality to make working with IO easier. | |
| 6 library io; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 import 'dart:io'; | |
| 10 import 'dart:isolate'; | |
| 11 import 'dart:json'; | |
| 12 import 'dart:uri'; | |
| 13 | |
| 14 import 'package:pathos/path.dart' as path; | |
| 15 import 'package:http/http.dart' show ByteStream; | |
| 16 import 'error_group.dart'; | |
| 17 import 'exit_codes.dart' as exit_codes; | |
| 18 import 'log.dart' as log; | |
| 19 import 'utils.dart'; | |
| 20 | |
| 21 export 'package:http/http.dart' show ByteStream; | |
| 22 | |
| 23 /// Returns whether or not [entry] is nested somewhere within [dir]. This just | |
| 24 /// performs a path comparison; it doesn't look at the actual filesystem. | |
| 25 bool isBeneath(String entry, String dir) { | |
| 26 var relative = path.relative(entry, from: dir); | |
| 27 return !path.isAbsolute(relative) && path.split(relative)[0] != '..'; | |
| 28 } | |
| 29 | |
| 30 /// Determines if a file or directory exists at [path]. | |
| 31 bool entryExists(String path) => | |
| 32 dirExists(path) || fileExists(path) || linkExists(path); | |
| 33 | |
| 34 /// Returns whether [link] exists on the file system. This will return `true` | |
| 35 /// for any symlink, regardless of what it points at or whether it's broken. | |
| 36 bool linkExists(String link) => new Link(link).existsSync(); | |
| 37 | |
| 38 /// Returns whether [file] exists on the file system. This will return `true` | |
| 39 /// for a symlink only if that symlink is unbroken and points to a file. | |
| 40 bool fileExists(String file) => new File(file).existsSync(); | |
| 41 | |
| 42 /// Reads the contents of the text file [file]. | |
| 43 String readTextFile(String file) => | |
| 44 new File(file).readAsStringSync(encoding: Encoding.UTF_8); | |
| 45 | |
| 46 /// Reads the contents of the binary file [file]. | |
| 47 List<int> readBinaryFile(String file) { | |
| 48 log.io("Reading binary file $file."); | |
| 49 var contents = new File(file).readAsBytesSync(); | |
| 50 log.io("Read ${contents.length} bytes from $file."); | |
| 51 return contents; | |
| 52 } | |
| 53 | |
| 54 /// Creates [file] and writes [contents] to it. | |
| 55 /// | |
| 56 /// If [dontLogContents] is true, the contents of the file will never be logged. | |
| 57 String writeTextFile(String file, String contents, {dontLogContents: false}) { | |
| 58 // Sanity check: don't spew a huge file. | |
| 59 log.io("Writing ${contents.length} characters to text file $file."); | |
| 60 if (!dontLogContents && contents.length < 1024 * 1024) { | |
| 61 log.fine("Contents:\n$contents"); | |
| 62 } | |
| 63 | |
| 64 new File(file).writeAsStringSync(contents); | |
| 65 return file; | |
| 66 } | |
| 67 | |
| 68 /// Creates [file] and writes [contents] to it. | |
| 69 String writeBinaryFile(String file, List<int> contents) { | |
| 70 log.io("Writing ${contents.length} bytes to binary file $file."); | |
| 71 new File(file).openSync(mode: FileMode.WRITE) | |
| 72 ..writeFromSync(contents) | |
| 73 ..closeSync(); | |
| 74 log.fine("Wrote text file $file."); | |
| 75 return file; | |
| 76 } | |
| 77 | |
| 78 /// Writes [stream] to a new file at path [file]. Will replace any file already | |
| 79 /// at that path. Completes when the file is done being written. | |
| 80 Future<String> createFileFromStream(Stream<List<int>> stream, String file) { | |
| 81 log.io("Creating $file from stream."); | |
| 82 | |
| 83 return stream.pipe(new File(file).openWrite()).then((_) { | |
| 84 log.fine("Created $file from stream."); | |
| 85 return file; | |
| 86 }); | |
| 87 } | |
| 88 | |
| 89 /// Creates a directory [dir]. | |
| 90 String createDir(String dir) { | |
| 91 new Directory(dir).createSync(); | |
| 92 return dir; | |
| 93 } | |
| 94 | |
| 95 /// Ensures that [dirPath] and all its parent directories exist. If they don't | |
| 96 /// exist, creates them. | |
| 97 String ensureDir(String dirPath) { | |
| 98 log.fine("Ensuring directory $dirPath exists."); | |
| 99 var dir = new Directory(dirPath); | |
| 100 if (dirPath == '.' || dirExists(dirPath)) return dirPath; | |
| 101 | |
| 102 ensureDir(path.dirname(dirPath)); | |
| 103 | |
| 104 try { | |
| 105 createDir(dirPath); | |
| 106 } on DirectoryIOException catch (ex) { | |
| 107 // Error 17 means the directory already exists (or 183 on Windows). | |
| 108 if (ex.osError.errorCode == 17 || ex.osError.errorCode == 183) { | |
| 109 log.fine("Got 'already exists' error when creating directory."); | |
| 110 } else { | |
| 111 throw ex; | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 return dirPath; | |
| 116 } | |
| 117 | |
| 118 /// Creates a temp directory whose name will be based on [dir] with a unique | |
| 119 /// suffix appended to it. If [dir] is not provided, a temp directory will be | |
| 120 /// created in a platform-dependent temporary location. Returns the path of the | |
| 121 /// created directory. | |
| 122 String createTempDir([dir = '']) { | |
| 123 var tempDir = new Directory(dir).createTempSync(); | |
| 124 log.io("Created temp directory ${tempDir.path}"); | |
| 125 return tempDir.path; | |
| 126 } | |
| 127 | |
| 128 /// Lists the contents of [dir]. If [recursive] is `true`, lists subdirectory | |
| 129 /// contents (defaults to `false`). If [includeHidden] is `true`, includes files | |
| 130 /// and directories beginning with `.` (defaults to `false`). | |
| 131 /// | |
| 132 /// The returned paths are guaranteed to begin with [dir]. | |
| 133 List<String> listDir(String dir, {bool recursive: false, | |
| 134 bool includeHidden: false}) { | |
| 135 List<String> doList(String dir, Set<String> listedDirectories) { | |
| 136 var contents = <String>[]; | |
| 137 | |
| 138 // Avoid recursive symlinks. | |
| 139 var resolvedPath = new File(dir).fullPathSync(); | |
| 140 if (listedDirectories.contains(resolvedPath)) return []; | |
| 141 | |
| 142 listedDirectories = new Set<String>.from(listedDirectories); | |
| 143 listedDirectories.add(resolvedPath); | |
| 144 | |
| 145 log.io("Listing directory $dir."); | |
| 146 | |
| 147 var children = <String>[]; | |
| 148 for (var entity in new Directory(dir).listSync()) { | |
| 149 if (!includeHidden && path.basename(entity.path).startsWith('.')) { | |
| 150 continue; | |
| 151 } | |
| 152 | |
| 153 contents.add(entity.path); | |
| 154 if (entity is Directory) { | |
| 155 // TODO(nweiz): don't manually recurse once issue 4794 is fixed. | |
| 156 // Note that once we remove the manual recursion, we'll need to | |
| 157 // explicitly filter out files in hidden directories. | |
| 158 if (recursive) { | |
| 159 children.addAll(doList(entity.path, listedDirectories)); | |
| 160 } | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 log.fine("Listed directory $dir:\n${contents.join('\n')}"); | |
| 165 contents.addAll(children); | |
| 166 return contents; | |
| 167 } | |
| 168 | |
| 169 return doList(dir, new Set<String>()); | |
| 170 } | |
| 171 | |
| 172 /// Returns whether [dir] exists on the file system. This will return `true` for | |
| 173 /// a symlink only if that symlink is unbroken and points to a directory. | |
| 174 bool dirExists(String dir) => new Directory(dir).existsSync(); | |
| 175 | |
| 176 /// Deletes whatever's at [path], whether it's a file, directory, or symlink. If | |
| 177 /// it's a directory, it will be deleted recursively. | |
| 178 void deleteEntry(String path) { | |
| 179 if (linkExists(path)) { | |
| 180 log.io("Deleting link $path."); | |
| 181 new Link(path).deleteSync(); | |
| 182 } else if (dirExists(path)) { | |
| 183 log.io("Deleting directory $path."); | |
| 184 new Directory(path).deleteSync(recursive: true); | |
| 185 } else if (fileExists(path)) { | |
| 186 log.io("Deleting file $path."); | |
| 187 new File(path).deleteSync(); | |
| 188 } | |
| 189 } | |
| 190 | |
| 191 /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a | |
| 192 /// new empty directory will be created. | |
| 193 void cleanDir(String dir) { | |
| 194 if (entryExists(dir)) deleteEntry(dir); | |
| 195 createDir(dir); | |
| 196 } | |
| 197 | |
| 198 /// Renames (i.e. moves) the directory [from] to [to]. | |
| 199 void renameDir(String from, String to) { | |
| 200 log.io("Renaming directory $from to $to."); | |
| 201 new Directory(from).renameSync(to); | |
| 202 } | |
| 203 | |
| 204 /// Creates a new symlink at path [symlink] that points to [target]. Returns a | |
| 205 /// [Future] which completes to the path to the symlink file. | |
| 206 /// | |
| 207 /// If [relative] is true, creates a symlink with a relative path from the | |
| 208 /// symlink to the target. Otherwise, uses the [target] path unmodified. | |
| 209 /// | |
| 210 /// Note that on Windows, only directories may be symlinked to. | |
| 211 void createSymlink(String target, String symlink, | |
| 212 {bool relative: false}) { | |
| 213 if (relative) { | |
| 214 // Relative junction points are not supported on Windows. Instead, just | |
| 215 // make sure we have a clean absolute path because it will interpret a | |
| 216 // relative path to be relative to the cwd, not the symlink, and will be | |
| 217 // confused by forward slashes. | |
| 218 if (Platform.operatingSystem == 'windows') { | |
| 219 target = path.normalize(path.absolute(target)); | |
| 220 } else { | |
| 221 target = path.normalize( | |
| 222 path.relative(target, from: path.dirname(symlink))); | |
| 223 } | |
| 224 } | |
| 225 | |
| 226 log.fine("Creating $symlink pointing to $target"); | |
| 227 new Link(symlink).createSync(target); | |
| 228 } | |
| 229 | |
| 230 /// Creates a new symlink that creates an alias at [symlink] that points to the | |
| 231 /// `lib` directory of package [target]. If [target] does not have a `lib` | |
| 232 /// directory, this shows a warning if appropriate and then does nothing. | |
| 233 /// | |
| 234 /// If [relative] is true, creates a symlink with a relative path from the | |
| 235 /// symlink to the target. Otherwise, uses the [target] path unmodified. | |
| 236 void createPackageSymlink(String name, String target, String symlink, | |
| 237 {bool isSelfLink: false, bool relative: false}) { | |
| 238 // See if the package has a "lib" directory. | |
| 239 target = path.join(target, 'lib'); | |
| 240 log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'."); | |
| 241 if (dirExists(target)) { | |
| 242 createSymlink(target, symlink, relative: relative); | |
| 243 return; | |
| 244 } | |
| 245 | |
| 246 // It's OK for the self link (i.e. the root package) to not have a lib | |
| 247 // directory since it may just be a leaf application that only has | |
| 248 // code in bin or web. | |
| 249 if (!isSelfLink) { | |
| 250 log.warning('Warning: Package "$name" does not have a "lib" directory so ' | |
| 251 'you will not be able to import any libraries from it.'); | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 /// Resolves [target] relative to the location of pub.dart. | |
| 256 String relativeToPub(String target) { | |
| 257 var scriptPath = new File(new Options().script).fullPathSync(); | |
| 258 | |
| 259 // Walk up until we hit the "util(s)" directory. This lets us figure out where | |
| 260 // we are if this function is called from pub.dart, or one of the tests, | |
| 261 // which also live under "utils", or from the SDK where pub is in "util". | |
| 262 var utilDir = path.dirname(scriptPath); | |
| 263 while (path.basename(utilDir) != 'utils' && | |
| 264 path.basename(utilDir) != 'util') { | |
| 265 if (path.basename(utilDir) == '') { | |
| 266 throw new Exception('Could not find path to pub.'); | |
| 267 } | |
| 268 utilDir = path.dirname(utilDir); | |
| 269 } | |
| 270 | |
| 271 return path.normalize(path.join(utilDir, 'pub', target)); | |
| 272 } | |
| 273 | |
| 274 /// A line-by-line stream of standard input. | |
| 275 final Stream<String> stdinLines = streamToLines( | |
| 276 new ByteStream(stdin).toStringStream()); | |
| 277 | |
| 278 /// Displays a message and reads a yes/no confirmation from the user. Returns | |
| 279 /// a [Future] that completes to `true` if the user confirms or `false` if they | |
| 280 /// do not. | |
| 281 /// | |
| 282 /// This will automatically append " (y/n)?" to the message, so [message] | |
| 283 /// should just be a fragment like, "Are you sure you want to proceed". | |
| 284 Future<bool> confirm(String message) { | |
| 285 log.fine('Showing confirm message: $message'); | |
| 286 stdout.write("$message (y/n)? "); | |
| 287 return streamFirst(stdinLines) | |
| 288 .then((line) => new RegExp(r"^[yY]").hasMatch(line)); | |
| 289 } | |
| 290 | |
| 291 /// Reads and discards all output from [stream]. Returns a [Future] that | |
| 292 /// completes when the stream is closed. | |
| 293 Future drainStream(Stream stream) { | |
| 294 return stream.fold(null, (x, y) {}); | |
| 295 } | |
| 296 | |
| 297 /// Returns a [EventSink] that pipes all data to [consumer] and a [Future] that | |
| 298 /// will succeed when [EventSink] is closed or fail with any errors that occur | |
| 299 /// while writing. | |
| 300 Pair<EventSink, Future> consumerToSink(StreamConsumer consumer) { | |
| 301 var controller = new StreamController(); | |
| 302 var done = controller.stream.pipe(consumer); | |
| 303 return new Pair<EventSink, Future>(controller.sink, done); | |
| 304 } | |
| 305 | |
| 306 // TODO(nweiz): remove this when issue 7786 is fixed. | |
| 307 /// Pipes all data and errors from [stream] into [sink]. When [stream] is done, | |
| 308 /// the returned [Future] is completed and [sink] is closed if [closeSink] is | |
| 309 /// true. | |
| 310 /// | |
| 311 /// When an error occurs on [stream], that error is passed to [sink]. If | |
| 312 /// [cancelOnError] is true, [Future] will be completed successfully and no | |
| 313 /// more data or errors will be piped from [stream] to [sink]. If | |
| 314 /// [cancelOnError] and [closeSink] are both true, [sink] will then be | |
| 315 /// closed. | |
| 316 Future store(Stream stream, EventSink sink, | |
| 317 {bool cancelOnError: true, closeSink: true}) { | |
| 318 var completer = new Completer(); | |
| 319 stream.listen(sink.add, | |
| 320 onError: (e) { | |
| 321 sink.addError(e); | |
| 322 if (cancelOnError) { | |
| 323 completer.complete(); | |
| 324 if (closeSink) sink.close(); | |
| 325 } | |
| 326 }, | |
| 327 onDone: () { | |
| 328 if (closeSink) sink.close(); | |
| 329 completer.complete(); | |
| 330 }, cancelOnError: cancelOnError); | |
| 331 return completer.future; | |
| 332 } | |
| 333 | |
| 334 /// Spawns and runs the process located at [executable], passing in [args]. | |
| 335 /// Returns a [Future] that will complete with the results of the process after | |
| 336 /// it has ended. | |
| 337 /// | |
| 338 /// The spawned process will inherit its parent's environment variables. If | |
| 339 /// [environment] is provided, that will be used to augment (not replace) the | |
| 340 /// the inherited variables. | |
| 341 Future<PubProcessResult> runProcess(String executable, List<String> args, | |
| 342 {workingDir, Map<String, String> environment}) { | |
| 343 return _doProcess(Process.run, executable, args, workingDir, environment) | |
| 344 .then((result) { | |
| 345 // TODO(rnystrom): Remove this and change to returning one string. | |
| 346 List<String> toLines(String output) { | |
| 347 var lines = splitLines(output); | |
| 348 if (!lines.isEmpty && lines.last == "") lines.removeLast(); | |
| 349 return lines; | |
| 350 } | |
| 351 | |
| 352 var pubResult = new PubProcessResult(toLines(result.stdout), | |
| 353 toLines(result.stderr), | |
| 354 result.exitCode); | |
| 355 | |
| 356 log.processResult(executable, pubResult); | |
| 357 return pubResult; | |
| 358 }); | |
| 359 } | |
| 360 | |
| 361 /// Spawns the process located at [executable], passing in [args]. Returns a | |
| 362 /// [Future] that will complete with the [Process] once it's been started. | |
| 363 /// | |
| 364 /// The spawned process will inherit its parent's environment variables. If | |
| 365 /// [environment] is provided, that will be used to augment (not replace) the | |
| 366 /// the inherited variables. | |
| 367 Future<PubProcess> startProcess(String executable, List<String> args, | |
| 368 {workingDir, Map<String, String> environment}) => | |
| 369 _doProcess(Process.start, executable, args, workingDir, environment) | |
| 370 .then((process) => new PubProcess(process)); | |
| 371 | |
| 372 /// A wrapper around [Process] that exposes `dart:async`-style APIs. | |
| 373 class PubProcess { | |
| 374 /// The underlying `dart:io` [Process]. | |
| 375 final Process _process; | |
| 376 | |
| 377 /// The mutable field for [stdin]. | |
| 378 EventSink<List<int>> _stdin; | |
| 379 | |
| 380 /// The mutable field for [stdinClosed]. | |
| 381 Future _stdinClosed; | |
| 382 | |
| 383 /// The mutable field for [stdout]. | |
| 384 ByteStream _stdout; | |
| 385 | |
| 386 /// The mutable field for [stderr]. | |
| 387 ByteStream _stderr; | |
| 388 | |
| 389 /// The mutable field for [exitCode]. | |
| 390 Future<int> _exitCode; | |
| 391 | |
| 392 /// The sink used for passing data to the process's standard input stream. | |
| 393 /// Errors on this stream are surfaced through [stdinClosed], [stdout], | |
| 394 /// [stderr], and [exitCode], which are all members of an [ErrorGroup]. | |
| 395 EventSink<List<int>> get stdin => _stdin; | |
| 396 | |
| 397 // TODO(nweiz): write some more sophisticated Future machinery so that this | |
| 398 // doesn't surface errors from the other streams/futures, but still passes its | |
| 399 // unhandled errors to them. Right now it's impossible to recover from a stdin | |
| 400 // error and continue interacting with the process. | |
| 401 /// A [Future] that completes when [stdin] is closed, either by the user or by | |
| 402 /// the process itself. | |
| 403 /// | |
| 404 /// This is in an [ErrorGroup] with [stdout], [stderr], and [exitCode], so any | |
| 405 /// error in process will be passed to it, but won't reach the top-level error | |
| 406 /// handler unless nothing has handled it. | |
| 407 Future get stdinClosed => _stdinClosed; | |
| 408 | |
| 409 /// The process's standard output stream. | |
| 410 /// | |
| 411 /// This is in an [ErrorGroup] with [stdinClosed], [stderr], and [exitCode], | |
| 412 /// so any error in process will be passed to it, but won't reach the | |
| 413 /// top-level error handler unless nothing has handled it. | |
| 414 ByteStream get stdout => _stdout; | |
| 415 | |
| 416 /// The process's standard error stream. | |
| 417 /// | |
| 418 /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [exitCode], | |
| 419 /// so any error in process will be passed to it, but won't reach the | |
| 420 /// top-level error handler unless nothing has handled it. | |
| 421 ByteStream get stderr => _stderr; | |
| 422 | |
| 423 /// A [Future] that will complete to the process's exit code once the process | |
| 424 /// has finished running. | |
| 425 /// | |
| 426 /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [stderr], so | |
| 427 /// any error in process will be passed to it, but won't reach the top-level | |
| 428 /// error handler unless nothing has handled it. | |
| 429 Future<int> get exitCode => _exitCode; | |
| 430 | |
| 431 /// Creates a new [PubProcess] wrapping [process]. | |
| 432 PubProcess(Process process) | |
| 433 : _process = process { | |
| 434 var errorGroup = new ErrorGroup(); | |
| 435 | |
| 436 var pair = consumerToSink(process.stdin); | |
| 437 _stdin = pair.first; | |
| 438 _stdinClosed = errorGroup.registerFuture(pair.last); | |
| 439 | |
| 440 _stdout = new ByteStream( | |
| 441 errorGroup.registerStream(process.stdout)); | |
| 442 _stderr = new ByteStream( | |
| 443 errorGroup.registerStream(process.stderr)); | |
| 444 | |
| 445 var exitCodeCompleter = new Completer(); | |
| 446 _exitCode = errorGroup.registerFuture(exitCodeCompleter.future); | |
| 447 _process.exitCode.then((code) => exitCodeCompleter.complete(code)); | |
| 448 } | |
| 449 | |
| 450 /// Sends [signal] to the underlying process. | |
| 451 bool kill([ProcessSignal signal = ProcessSignal.SIGTERM]) => | |
| 452 _process.kill(signal); | |
| 453 } | |
| 454 | |
| 455 /// Calls [fn] with appropriately modified arguments. [fn] should have the same | |
| 456 /// signature as [Process.start], except that the returned [Future] may have a | |
| 457 /// type other than [Process]. | |
| 458 Future _doProcess(Function fn, String executable, List<String> args, | |
| 459 String workingDir, Map<String, String> environment) { | |
| 460 // TODO(rnystrom): Should dart:io just handle this? | |
| 461 // Spawning a process on Windows will not look for the executable in the | |
| 462 // system path. So, if executable looks like it needs that (i.e. it doesn't | |
| 463 // have any path separators in it), then spawn it through a shell. | |
| 464 if ((Platform.operatingSystem == "windows") && | |
| 465 (executable.indexOf('\\') == -1)) { | |
| 466 args = flatten(["/c", executable, args]); | |
| 467 executable = "cmd"; | |
| 468 } | |
| 469 | |
| 470 final options = new ProcessOptions(); | |
| 471 if (workingDir != null) { | |
| 472 options.workingDirectory = workingDir; | |
| 473 } | |
| 474 | |
| 475 if (environment != null) { | |
| 476 options.environment = new Map.from(Platform.environment); | |
| 477 environment.forEach((key, value) => options.environment[key] = value); | |
| 478 } | |
| 479 | |
| 480 log.process(executable, args); | |
| 481 | |
| 482 return fn(executable, args, options); | |
| 483 } | |
| 484 | |
| 485 /// Wraps [input] to provide a timeout. If [input] completes before | |
| 486 /// [milliseconds] have passed, then the return value completes in the same way. | |
| 487 /// However, if [milliseconds] pass before [input] has completed, it completes | |
| 488 /// with a [TimeoutException] with [description] (which should be a fragment | |
| 489 /// describing the action that timed out). | |
| 490 /// | |
| 491 /// Note that timing out will not cancel the asynchronous operation behind | |
| 492 /// [input]. | |
| 493 Future timeout(Future input, int milliseconds, String description) { | |
| 494 var completer = new Completer(); | |
| 495 var timer = new Timer(new Duration(milliseconds: milliseconds), () { | |
| 496 completer.completeError(new TimeoutException( | |
| 497 'Timed out while $description.')); | |
| 498 }); | |
| 499 input.then((value) { | |
| 500 if (completer.isCompleted) return; | |
| 501 timer.cancel(); | |
| 502 completer.complete(value); | |
| 503 }).catchError((e) { | |
| 504 if (completer.isCompleted) return; | |
| 505 timer.cancel(); | |
| 506 completer.completeError(e); | |
| 507 }); | |
| 508 return completer.future; | |
| 509 } | |
| 510 | |
| 511 /// Creates a temporary directory and passes its path to [fn]. Once the [Future] | |
| 512 /// returned by [fn] completes, the temporary directory and all its contents | |
| 513 /// will be deleted. [fn] can also return `null`, in which case the temporary | |
| 514 /// directory is deleted immediately afterwards. | |
| 515 /// | |
| 516 /// Returns a future that completes to the value that the future returned from | |
| 517 /// [fn] completes to. | |
| 518 Future withTempDir(Future fn(String path)) { | |
| 519 return new Future.sync(() { | |
| 520 var tempDir = createTempDir(); | |
| 521 return new Future.sync(() => fn(tempDir)) | |
| 522 .whenComplete(() => deleteEntry(tempDir)); | |
| 523 }); | |
| 524 } | |
| 525 | |
| 526 /// Extracts a `.tar.gz` file from [stream] to [destination]. Returns whether | |
| 527 /// or not the extraction was successful. | |
| 528 Future<bool> extractTarGz(Stream<List<int>> stream, String destination) { | |
| 529 log.fine("Extracting .tar.gz stream to $destination."); | |
| 530 | |
| 531 if (Platform.operatingSystem == "windows") { | |
| 532 return _extractTarGzWindows(stream, destination); | |
| 533 } | |
| 534 | |
| 535 return startProcess("tar", | |
| 536 ["--extract", "--gunzip", "--directory", destination]).then((process) { | |
| 537 // Ignore errors on process.std{out,err}. They'll be passed to | |
| 538 // process.exitCode, and we don't want them being top-levelled by | |
| 539 // std{out,err}Sink. | |
| 540 store(process.stdout.handleError((_) {}), stdout, closeSink: false); | |
| 541 store(process.stderr.handleError((_) {}), stderr, closeSink: false); | |
| 542 return Future.wait([ | |
| 543 store(stream, process.stdin), | |
| 544 process.exitCode | |
| 545 ]); | |
| 546 }).then((results) { | |
| 547 var exitCode = results[1]; | |
| 548 if (exitCode != 0) { | |
| 549 throw new Exception("Failed to extract .tar.gz stream to $destination " | |
| 550 "(exit code $exitCode)."); | |
| 551 } | |
| 552 log.fine("Extracted .tar.gz stream to $destination. Exit code $exitCode."); | |
| 553 }); | |
| 554 } | |
| 555 | |
| 556 Future<bool> _extractTarGzWindows(Stream<List<int>> stream, | |
| 557 String destination) { | |
| 558 // TODO(rnystrom): In the repo's history, there is an older implementation of | |
| 559 // this that does everything in memory by piping streams directly together | |
| 560 // instead of writing out temp files. The code is simpler, but unfortunately, | |
| 561 // 7zip seems to periodically fail when we invoke it from Dart and tell it to | |
| 562 // read from stdin instead of a file. Consider resurrecting that version if | |
| 563 // we can figure out why it fails. | |
| 564 | |
| 565 // Note: This line of code gets munged by create_sdk.py to be the correct | |
| 566 // relative path to 7zip in the SDK. | |
| 567 var pathTo7zip = '../../third_party/7zip/7za.exe'; | |
| 568 var command = relativeToPub(pathTo7zip); | |
| 569 | |
| 570 return withTempDir((tempDir) { | |
| 571 // Write the archive to a temp file. | |
| 572 var dataFile = path.join(tempDir, 'data.tar.gz'); | |
| 573 return createFileFromStream(stream, dataFile).then((_) { | |
| 574 // 7zip can't unarchive from gzip -> tar -> destination all in one step | |
| 575 // first we un-gzip it to a tar file. | |
| 576 // Note: Setting the working directory instead of passing in a full file | |
| 577 // path because 7zip says "A full path is not allowed here." | |
| 578 return runProcess(command, ['e', 'data.tar.gz'], workingDir: tempDir); | |
| 579 }).then((result) { | |
| 580 if (result.exitCode != 0) { | |
| 581 throw new Exception('Could not un-gzip (exit code ${result.exitCode}). ' | |
| 582 'Error:\n' | |
| 583 '${result.stdout.join("\n")}\n' | |
| 584 '${result.stderr.join("\n")}'); | |
| 585 } | |
| 586 | |
| 587 // Find the tar file we just created since we don't know its name. | |
| 588 var tarFile = listDir(tempDir).firstWhere( | |
| 589 (file) => path.extension(file) == '.tar', | |
| 590 orElse: () { | |
| 591 throw new FormatException('The gzip file did not contain a tar file.'); | |
| 592 }); | |
| 593 | |
| 594 // Untar the archive into the destination directory. | |
| 595 return runProcess(command, ['x', tarFile], workingDir: destination); | |
| 596 }).then((result) { | |
| 597 if (result.exitCode != 0) { | |
| 598 throw new Exception('Could not un-tar (exit code ${result.exitCode}). ' | |
| 599 'Error:\n' | |
| 600 '${result.stdout.join("\n")}\n' | |
| 601 '${result.stderr.join("\n")}'); | |
| 602 } | |
| 603 return true; | |
| 604 }); | |
| 605 }); | |
| 606 } | |
| 607 | |
| 608 /// Create a .tar.gz archive from a list of entries. Each entry can be a | |
| 609 /// [String], [Directory], or [File] object. The root of the archive is | |
| 610 /// considered to be [baseDir], which defaults to the current working directory. | |
| 611 /// Returns a [ByteStream] that will emit the contents of the archive. | |
| 612 ByteStream createTarGz(List contents, {baseDir}) { | |
| 613 var buffer = new StringBuffer(); | |
| 614 buffer.write('Creating .tag.gz stream containing:\n'); | |
| 615 contents.forEach((file) => buffer.write('$file\n')); | |
| 616 log.fine(buffer.toString()); | |
| 617 | |
| 618 var controller = new StreamController<List<int>>(); | |
| 619 | |
| 620 if (baseDir == null) baseDir = path.current; | |
| 621 baseDir = path.absolute(baseDir); | |
| 622 contents = contents.map((entry) { | |
| 623 entry = path.absolute(entry); | |
| 624 if (!isBeneath(entry, baseDir)) { | |
| 625 throw new ArgumentError('Entry $entry is not inside $baseDir.'); | |
| 626 } | |
| 627 return path.relative(entry, from: baseDir); | |
| 628 }).toList(); | |
| 629 | |
| 630 if (Platform.operatingSystem != "windows") { | |
| 631 var args = ["--create", "--gzip", "--directory", baseDir]; | |
| 632 args.addAll(contents); | |
| 633 // TODO(nweiz): It's possible that enough command-line arguments will make | |
| 634 // the process choke, so at some point we should save the arguments to a | |
| 635 // file and pass them in via --files-from for tar and -i@filename for 7zip. | |
| 636 startProcess("tar", args).then((process) { | |
| 637 store(process.stdout, controller); | |
| 638 }).catchError((e) { | |
| 639 // We don't have to worry about double-signaling here, since the store() | |
| 640 // above will only be reached if startProcess succeeds. | |
| 641 controller.addError(e); | |
| 642 controller.close(); | |
| 643 }); | |
| 644 return new ByteStream(controller.stream); | |
| 645 } | |
| 646 | |
| 647 withTempDir((tempDir) { | |
| 648 // Create the tar file. | |
| 649 var tarFile = path.join(tempDir, "intermediate.tar"); | |
| 650 var args = ["a", "-w$baseDir", tarFile]; | |
| 651 args.addAll(contents.map((entry) => '-i!"$entry"')); | |
| 652 | |
| 653 // Note: This line of code gets munged by create_sdk.py to be the correct | |
| 654 // relative path to 7zip in the SDK. | |
| 655 var pathTo7zip = '../../third_party/7zip/7za.exe'; | |
| 656 var command = relativeToPub(pathTo7zip); | |
| 657 | |
| 658 // We're passing 'baseDir' both as '-w' and setting it as the working | |
| 659 // directory explicitly here intentionally. The former ensures that the | |
| 660 // files added to the archive have the correct relative path in the archive. | |
| 661 // The latter enables relative paths in the "-i" args to be resolved. | |
| 662 return runProcess(command, args, workingDir: baseDir).then((_) { | |
| 663 // GZIP it. 7zip doesn't support doing both as a single operation. Send | |
| 664 // the output to stdout. | |
| 665 args = ["a", "unused", "-tgzip", "-so", tarFile]; | |
| 666 return startProcess(command, args); | |
| 667 }).then((process) { | |
| 668 // Ignore 7zip's stderr. 7zip writes its normal output to stderr. We don't | |
| 669 // want to show that since it's meaningless. | |
| 670 // | |
| 671 // TODO(rnystrom): Should log the stderr and display it if an actual error | |
| 672 // occurs. | |
| 673 return store(process.stdout, controller); | |
| 674 }); | |
| 675 }).catchError((e) { | |
| 676 // We don't have to worry about double-signaling here, since the store() | |
| 677 // above will only be reached if everything succeeds. | |
| 678 controller.addError(e); | |
| 679 controller.close(); | |
| 680 }); | |
| 681 return new ByteStream(controller.stream); | |
| 682 } | |
| 683 | |
| 684 /// Exception thrown when an operation times out. | |
| 685 class TimeoutException implements Exception { | |
| 686 final String message; | |
| 687 | |
| 688 const TimeoutException(this.message); | |
| 689 | |
| 690 String toString() => message; | |
| 691 } | |
| 692 | |
| 693 /// Contains the results of invoking a [Process] and waiting for it to complete. | |
| 694 class PubProcessResult { | |
| 695 final List<String> stdout; | |
| 696 final List<String> stderr; | |
| 697 final int exitCode; | |
| 698 | |
| 699 const PubProcessResult(this.stdout, this.stderr, this.exitCode); | |
| 700 | |
| 701 bool get success => exitCode == 0; | |
| 702 } | |
| 703 | |
| 704 /// Gets a [Uri] for [uri], which can either already be one, or be a [String]. | |
| 705 Uri _getUri(uri) { | |
| 706 if (uri is Uri) return uri; | |
| 707 return Uri.parse(uri); | |
| 708 } | |
| OLD | NEW |