| // 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. |
| |
| /// Helper functionality to make working with IO easier. |
| import 'dart:async'; |
| import 'dart:collection'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:http/http.dart' show ByteStream; |
| import 'package:http_multi_server/http_multi_server.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:pedantic/pedantic.dart'; |
| import 'package:pool/pool.dart'; |
| import 'package:stack_trace/stack_trace.dart'; |
| import 'package:tar/tar.dart'; |
| |
| import 'error_group.dart'; |
| import 'exceptions.dart'; |
| import 'exit_codes.dart' as exit_codes; |
| import 'log.dart' as log; |
| import 'utils.dart'; |
| |
| export 'package:http/http.dart' show ByteStream; |
| |
| /// The pool used for restricting access to asynchronous operations that consume |
| /// file descriptors. |
| /// |
| /// The maximum number of allocated descriptors is based on empirical tests that |
| /// indicate that beyond 32, additional file reads don't provide substantial |
| /// additional throughput. |
| final _descriptorPool = Pool(32); |
| |
| /// The assumed default file mode on Linux and macOS |
| const _defaultMode = 420; // 644₈ |
| |
| /// Mask for executable bits in file modes. |
| const _executableMask = 0x49; // 001 001 001 |
| |
| /// Determines if a file or directory exists at [path]. |
| bool entryExists(String path) => |
| dirExists(path) || fileExists(path) || linkExists(path); |
| |
| /// Returns whether [link] exists on the file system. |
| /// |
| /// This returns `true` for any symlink, regardless of what it points at or |
| /// whether it's broken. |
| bool linkExists(String link) => Link(link).existsSync(); |
| |
| /// Returns whether [file] exists on the file system. |
| /// |
| /// This returns `true` for a symlink only if that symlink is unbroken and |
| /// points to a file. |
| bool fileExists(String file) => File(file).existsSync(); |
| |
| /// Returns the canonical path for [pathString]. |
| /// |
| /// This is the normalized, absolute path, with symlinks resolved. As in |
| /// [transitiveTarget], broken or recursive symlinks will not be fully resolved. |
| /// |
| /// This doesn't require [pathString] to point to a path that exists on the |
| /// filesystem; nonexistent or unreadable path entries are treated as normal |
| /// directories. |
| String canonicalize(String pathString) { |
| var seen = <String>{}; |
| var components = |
| Queue<String>.from(path.split(path.normalize(path.absolute(pathString)))); |
| |
| // The canonical path, built incrementally as we iterate through [components]. |
| var newPath = components.removeFirst(); |
| |
| // Move through the components of the path, resolving each one's symlinks as |
| // necessary. A resolved component may also add new components that need to be |
| // resolved in turn. |
| while (components.isNotEmpty) { |
| seen.add(path.join(newPath, path.joinAll(components))); |
| var resolvedPath = |
| _resolveLink(path.join(newPath, components.removeFirst())); |
| var relative = path.relative(resolvedPath, from: newPath); |
| |
| // If the resolved path of the component relative to `newPath` is just ".", |
| // that means component was a symlink pointing to its parent directory. We |
| // can safely ignore such components. |
| if (relative == '.') continue; |
| |
| var relativeComponents = Queue<String>.from(path.split(relative)); |
| |
| // If the resolved path is absolute relative to `newPath`, that means it's |
| // on a different drive. We need to canonicalize the entire target of that |
| // symlink again. |
| if (path.isAbsolute(relative)) { |
| // If we've already tried to canonicalize the new path, we've encountered |
| // a symlink loop. Avoid going infinite by treating the recursive symlink |
| // as the canonical path. |
| if (seen.contains(relative)) { |
| newPath = relative; |
| } else { |
| newPath = relativeComponents.removeFirst(); |
| relativeComponents.addAll(components); |
| components = relativeComponents; |
| } |
| continue; |
| } |
| |
| // Pop directories off `newPath` if the component links upwards in the |
| // directory hierarchy. |
| while (relativeComponents.first == '..') { |
| newPath = path.dirname(newPath); |
| relativeComponents.removeFirst(); |
| } |
| |
| // If there's only one component left, [resolveLink] guarantees that it's |
| // not a link (or is a broken link). We can just add it to `newPath` and |
| // continue resolving the remaining components. |
| if (relativeComponents.length == 1) { |
| newPath = path.join(newPath, relativeComponents.single); |
| continue; |
| } |
| |
| // If we've already tried to canonicalize the new path, we've encountered a |
| // symlink loop. Avoid going infinite by treating the recursive symlink as |
| // the canonical path. |
| var newSubPath = path.join(newPath, path.joinAll(relativeComponents)); |
| if (seen.contains(newSubPath)) { |
| newPath = newSubPath; |
| continue; |
| } |
| |
| // If there are multiple new components to resolve, add them to the |
| // beginning of the queue. |
| relativeComponents.addAll(components); |
| components = relativeComponents; |
| } |
| return newPath; |
| } |
| |
| /// Returns the transitive target of [link] (if A links to B which links to C, |
| /// this will return C). |
| /// |
| /// If [link] is part of a symlink loop (e.g. A links to B which links back to |
| /// A), this returns the path to the first repeated link (so |
| /// `transitiveTarget("A")` would return `"A"` and `transitiveTarget("A")` would |
| /// return `"B"`). |
| /// |
| /// This accepts paths to non-links or broken links, and returns them as-is. |
| String _resolveLink(String link) { |
| var seen = <String>{}; |
| while (linkExists(link) && seen.add(link)) { |
| link = |
| path.normalize(path.join(path.dirname(link), Link(link).targetSync())); |
| } |
| return link; |
| } |
| |
| /// Reads the contents of the text file [file]. |
| String readTextFile(String file) => File(file).readAsStringSync(encoding: utf8); |
| |
| /// Reads the contents of the text file [file]. |
| Future<String> readTextFileAsync(String file) => |
| File(file).readAsString(encoding: utf8); |
| |
| /// Reads the contents of the binary file [file]. |
| List<int> readBinaryFile(String file) { |
| log.io('Reading binary file $file.'); |
| var contents = File(file).readAsBytesSync(); |
| log.io('Read ${contents.length} bytes from $file.'); |
| return contents; |
| } |
| |
| /// Creates [file] and writes [contents] to it. |
| /// |
| /// If [dontLogContents] is `true`, the contents of the file will never be |
| /// logged. |
| void writeTextFile(String file, String contents, |
| {bool dontLogContents = false, Encoding encoding}) { |
| encoding ??= utf8; |
| |
| // Sanity check: don't spew a huge file. |
| log.io('Writing ${contents.length} characters to text file $file.'); |
| if (!dontLogContents && contents.length < 1024 * 1024) { |
| log.fine('Contents:\n$contents'); |
| } |
| |
| deleteIfLink(file); |
| File(file).writeAsStringSync(contents, encoding: encoding); |
| } |
| |
| /// Creates [file] and writes [contents] to it. |
| /// |
| /// If [dontLogContents] is `true`, the contents of the file will never be |
| /// logged. |
| Future<void> writeTextFileAsync(String file, String contents, |
| {bool dontLogContents = false, Encoding encoding}) async { |
| encoding ??= utf8; |
| |
| // Sanity check: don't spew a huge file. |
| log.io('Writing ${contents.length} characters to text file $file.'); |
| if (!dontLogContents && contents.length < 1024 * 1024) { |
| log.fine('Contents:\n$contents'); |
| } |
| |
| deleteIfLink(file); |
| await File(file).writeAsString(contents, encoding: encoding); |
| } |
| |
| /// Writes [stream] to a new file at path [file]. |
| /// |
| /// Replaces any file already at that path. Completes when the file is done |
| /// being written. |
| Future<String> _createFileFromStream(Stream<List<int>> stream, String file) { |
| // TODO(nweiz): remove extra logging when we figure out the windows bot issue. |
| log.io('Creating $file from stream.'); |
| |
| return _descriptorPool.withResource(() async { |
| deleteIfLink(file); |
| await stream.pipe(File(file).openWrite()); |
| log.fine('Created $file from stream.'); |
| return file; |
| }); |
| } |
| |
| void _chmod(int mode, String file) { |
| runProcessSync('chmod', [mode.toRadixString(8), file]); |
| } |
| |
| /// Deletes [file] if it's a symlink. |
| /// |
| /// The [File] class overwrites the symlink targets when writing to a file, |
| /// which is never what we want, so this delete the symlink first if necessary. |
| void deleteIfLink(String file) { |
| if (!linkExists(file)) return; |
| log.io('Deleting symlink at $file.'); |
| Link(file).deleteSync(); |
| } |
| |
| /// Ensures that [dir] and all its parent directories exist. |
| /// |
| /// If they don't exist, creates them. |
| String ensureDir(String dir) { |
| Directory(dir).createSync(recursive: true); |
| return dir; |
| } |
| |
| /// Creates a temp directory in [dir], whose name will be [prefix] with |
| /// characters appended to it to make a unique name. |
| /// |
| /// Returns the path of the created directory. |
| String createTempDir(String base, String prefix) { |
| var tempDir = Directory(base).createTempSync(prefix); |
| log.io('Created temp directory ${tempDir.path}'); |
| return tempDir.path; |
| } |
| |
| /// Creates a temp directory in the system temp directory, whose name will be |
| /// 'pub_' with characters appended to it to make a unique name. |
| /// |
| /// Returns the path of the created directory. |
| Future<String> _createSystemTempDir() async { |
| var tempDir = await Directory.systemTemp.createTemp('pub_'); |
| log.io('Created temp directory ${tempDir.path}'); |
| return tempDir.resolveSymbolicLinksSync(); |
| } |
| |
| String resolveSymlinksOfDir(String dir) { |
| return Directory(dir).resolveSymbolicLinksSync(); |
| } |
| |
| /// Lists the contents of [dir]. |
| /// |
| /// If [recursive] is `true`, lists subdirectory contents (defaults to `false`). |
| /// If [includeHidden] is `true`, includes files and directories beginning with |
| /// `.` (defaults to `false`). If [includeDirs] is `true`, includes directories |
| /// as well as files (defaults to `true`). |
| /// |
| /// [allowed] is a list of hidden filenames to include even when |
| /// [includeHidden] is `false`. |
| /// |
| /// Note that dart:io handles recursive symlinks in an unfortunate way. You |
| /// end up with two copies of every entity that is within the recursive loop. |
| /// We originally had our own directory list code that addressed that, but it |
| /// had a noticeable performance impact. In the interest of speed, we'll just |
| /// live with that annoying behavior. |
| /// |
| /// The returned paths are guaranteed to begin with [dir]. Broken symlinks won't |
| /// be returned. |
| List<String> listDir(String dir, |
| {bool recursive = false, |
| bool includeHidden = false, |
| bool includeDirs = true, |
| Iterable<String> allowed}) { |
| allowed ??= []; |
| var allowlistFilter = createFileFilter(allowed); |
| |
| // This is used in some performance-sensitive paths and can list many, many |
| // files. As such, it leans more heavily towards optimization as opposed to |
| // readability than most code in pub. In particular, it avoids using the path |
| // package, since re-parsing a path is very expensive relative to string |
| // operations. |
| return Directory(dir) |
| .listSync(recursive: recursive, followLinks: true) |
| .where((entity) { |
| if (!includeDirs && entity is Directory) return false; |
| if (entity is Link) return false; |
| if (includeHidden) return true; |
| |
| // Using substring here is generally problematic in cases where dir has one |
| // or more trailing slashes. If you do listDir("foo"), you'll get back |
| // paths like "foo/bar". If you do listDir("foo/"), you'll get "foo/bar" |
| // (note the trailing slash was dropped. If you do listDir("foo//"), you'll |
| // get "foo//bar". |
| // |
| // This means if you strip off the prefix, the resulting string may have a |
| // leading separator (if the prefix did not have a trailing one) or it may |
| // not. However, since we are only using the results of that to call |
| // contains() on, the leading separator is harmless. |
| assert(entity.path.startsWith(dir)); |
| var pathInDir = entity.path.substring(dir.length); |
| |
| // If the basename is in [allowed], don't count its "/." as making the |
| // file hidden. |
| var allowedBasename = |
| allowlistFilter.firstWhere(pathInDir.contains, orElse: () => null); |
| if (allowedBasename != null) { |
| pathInDir = |
| pathInDir.substring(0, pathInDir.length - allowedBasename.length); |
| } |
| |
| if (pathInDir.contains('/.')) return false; |
| if (!Platform.isWindows) return true; |
| return !pathInDir.contains('\\.'); |
| }) |
| .map((entity) => entity.path) |
| .toList(); |
| } |
| |
| /// Returns whether [dir] exists on the file system. |
| /// |
| /// This returns `true` for a symlink only if that symlink is unbroken and |
| /// points to a directory. |
| bool dirExists(String dir) => Directory(dir).existsSync(); |
| |
| /// Tries to resiliently perform [operation]. |
| /// |
| /// Some file system operations can intermittently fail on Windows because |
| /// other processes are locking a file. We've seen this with virus scanners |
| /// when we try to delete or move something while it's being scanned. To |
| /// mitigate that, on Windows, this will retry the operation a few times if it |
| /// fails. |
| void _attempt(String description, void Function() operation) { |
| if (!Platform.isWindows) { |
| operation(); |
| return; |
| } |
| |
| String getErrorReason(FileSystemException error) { |
| if (error.osError.errorCode == 5) { |
| return 'access was denied'; |
| } |
| |
| if (error.osError.errorCode == 32) { |
| return 'it was in use by another process'; |
| } |
| |
| if (error.osError.errorCode == 145) { |
| return 'of dart-lang/sdk#25353'; |
| } |
| |
| return null; |
| } |
| |
| for (var i = 0; i < 2; i++) { |
| try { |
| operation(); |
| return; |
| } on FileSystemException catch (error) { |
| var reason = getErrorReason(error); |
| if (reason == null) rethrow; |
| |
| log.io('Pub failed to $description because $reason. ' |
| 'Retrying in 50ms.'); |
| sleep(Duration(milliseconds: 50)); |
| } |
| } |
| |
| try { |
| operation(); |
| } on FileSystemException catch (error) { |
| var reason = getErrorReason(error); |
| if (reason == null) rethrow; |
| |
| fail('Pub failed to $description because $reason.\n' |
| 'This may be caused by a virus scanner or having a file\n' |
| 'in the directory open in another application.'); |
| } |
| } |
| |
| /// Deletes whatever's at [path], whether it's a file, directory, or symlink. |
| /// |
| /// If it's a directory, it will be deleted recursively. |
| void deleteEntry(String path) { |
| _attempt('delete entry', () { |
| if (linkExists(path)) { |
| log.io('Deleting link $path.'); |
| Link(path).deleteSync(); |
| } else if (dirExists(path)) { |
| log.io('Deleting directory $path.'); |
| Directory(path).deleteSync(recursive: true); |
| } else if (fileExists(path)) { |
| log.io('Deleting file $path.'); |
| File(path).deleteSync(); |
| } |
| }); |
| } |
| |
| /// Attempts to delete whatever's at [path], but doesn't throw an exception if |
| /// the deletion fails. |
| void tryDeleteEntry(String path) { |
| try { |
| deleteEntry(path); |
| } catch (error, stackTrace) { |
| log.fine('Pub failed to delete $path: $error\n' |
| '${Chain.forTrace(stackTrace)}'); |
| } |
| } |
| |
| /// "Cleans" [dir]. |
| /// |
| /// If that directory already exists, it is deleted. Then a new empty directory |
| /// is created. |
| void cleanDir(String dir) { |
| if (entryExists(dir)) deleteEntry(dir); |
| ensureDir(dir); |
| } |
| |
| /// Renames (i.e. moves) the directory [from] to [to]. |
| void renameDir(String from, String to) { |
| _attempt('rename directory', () { |
| log.io('Renaming directory $from to $to.'); |
| try { |
| Directory(from).renameSync(to); |
| } on IOException { |
| // Ensure that [to] isn't left in an inconsistent state. See issue 12436. |
| if (entryExists(to)) deleteEntry(to); |
| rethrow; |
| } |
| }); |
| } |
| |
| /// Creates a new symlink at path [symlink] that points to [target]. |
| /// |
| /// Returns a [Future] which completes to the path to the symlink file. |
| /// |
| /// If [relative] is true, creates a symlink with a relative path from the |
| /// symlink to the target. Otherwise, uses the [target] path unmodified. |
| /// |
| /// Note that on Windows, only directories may be symlinked to. |
| void createSymlink(String target, String symlink, {bool relative = false}) { |
| if (relative) { |
| // Relative junction points are not supported on Windows. Instead, just |
| // make sure we have a clean absolute path because it will interpret a |
| // relative path to be relative to the cwd, not the symlink, and will be |
| // confused by forward slashes. |
| if (Platform.isWindows) { |
| target = path.normalize(path.absolute(target)); |
| } else { |
| // If the directory where we're creating the symlink was itself reached |
| // by traversing a symlink, we want the relative path to be relative to |
| // it's actual location, not the one we went through to get to it. |
| var symlinkDir = canonicalize(path.dirname(symlink)); |
| target = path.normalize(path.relative(target, from: symlinkDir)); |
| } |
| } |
| |
| log.fine('Creating $symlink pointing to $target'); |
| Link(symlink).createSync(target); |
| } |
| |
| /// Creates a new symlink that creates an alias at [symlink] that points to the |
| /// `lib` directory of package [target]. |
| /// |
| /// If [target] does not have a `lib` directory, this shows a warning if |
| /// appropriate and then does nothing. |
| /// |
| /// If [relative] is true, creates a symlink with a relative path from the |
| /// symlink to the target. Otherwise, uses the [target] path unmodified. |
| void createPackageSymlink(String name, String target, String symlink, |
| {bool isSelfLink = false, bool relative = false}) { |
| // See if the package has a "lib" directory. If not, there's nothing to |
| // symlink to. |
| target = path.join(target, 'lib'); |
| if (!dirExists(target)) return; |
| |
| log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'."); |
| createSymlink(target, symlink, relative: relative); |
| } |
| |
| /// Whether the current process is a pub subprocess being run from a test. |
| /// |
| /// The "_PUB_TESTING" variable is automatically set for all the test code's |
| /// invocations of pub. |
| final bool runningFromTest = Platform.environment.containsKey('_PUB_TESTING'); |
| |
| final bool runningFromFlutter = |
| Platform.environment.containsKey('PUB_ENVIRONMENT') && |
| Platform.environment['PUB_ENVIRONMENT'].contains('flutter_cli'); |
| |
| /// A regular expression to match the script path of a pub script running from |
| /// source in the Dart repo. |
| final _dartRepoRegExp = RegExp(r'/third_party/pkg/pub/(' |
| r'bin/pub\.dart' |
| r'|' |
| r'test/.*_test\.dart' |
| r')$'); |
| |
| /// Whether pub is running from source in the Dart repo. |
| /// |
| /// This can happen when running tests against the repo, as well as when |
| /// building Observatory. |
| final bool runningFromDartRepo = Platform.script.path.contains(_dartRepoRegExp); |
| |
| /// The path to the root of the Dart repo. |
| /// |
| /// This throws a [StateError] if it's called when not running pub from source |
| /// in the Dart repo. |
| final String dartRepoRoot = (() { |
| if (!runningFromDartRepo) { |
| throw StateError('Not running from source in the Dart repo.'); |
| } |
| |
| // Get the URL of the repo root in a way that works when either both running |
| // as a test or as a pub executable. |
| var url = Platform.script |
| .replace(path: Platform.script.path.replaceAll(_dartRepoRegExp, '')); |
| return path.fromUri(url); |
| })(); |
| |
| /// A line-by-line stream of standard input. |
| final Stream<String> _stdinLines = |
| ByteStream(stdin).toStringStream().transform(const LineSplitter()); |
| |
| /// Displays a message and reads a yes/no confirmation from the user. |
| /// |
| /// Returns a [Future] that completes to `true` if the user confirms or `false` |
| /// if they do not. |
| /// |
| /// This will automatically append " (y/N)?" to the message, so [message] |
| /// should just be a fragment like, "Are you sure you want to proceed". The |
| /// default for an empty response, or any response not starting with `y` or `Y` |
| /// is false. |
| Future<bool> confirm(String message) { |
| log.fine('Showing confirm message: $message'); |
| if (runningFromTest) { |
| log.message('$message (y/N)?'); |
| } else { |
| stdout.write('$message (y/N)? '); |
| } |
| return _stdinLines.first.then(RegExp(r'^[yY]').hasMatch); |
| } |
| |
| /// Flushes the stdout and stderr streams, then exits the program with the given |
| /// status code. |
| /// |
| /// This returns a Future that will never complete, since the program will have |
| /// exited already. This is useful to prevent Future chains from proceeding |
| /// after you've decided to exit. |
| Future flushThenExit(int status) { |
| return Future.wait([stdout.close(), stderr.close()]) |
| .then((_) => exit(status)); |
| } |
| |
| /// Returns a [EventSink] that pipes all data to [consumer] and a [Future] that |
| /// will succeed when [EventSink] is closed or fail with any errors that occur |
| /// while writing. |
| Pair<EventSink<T>, Future> _consumerToSink<T>(StreamConsumer<T> consumer) { |
| var controller = StreamController<T>(sync: true); |
| var done = controller.stream.pipe(consumer); |
| return Pair(controller.sink, done); |
| } |
| |
| /// Spawns and runs the process located at [executable], passing in [args]. |
| /// |
| /// Returns a [Future] that will complete with the results of the process after |
| /// it has ended. |
| /// |
| /// The spawned process will inherit its parent's environment variables. If |
| /// [environment] is provided, that will be used to augment (not replace) the |
| /// the inherited variables. |
| Future<PubProcessResult> runProcess(String executable, List<String> args, |
| {workingDir, Map<String, String> environment, bool runInShell = false}) { |
| ArgumentError.checkNotNull(executable, 'executable'); |
| |
| return _descriptorPool.withResource(() async { |
| ProcessResult result; |
| try { |
| result = await _doProcess(Process.run, executable, args, |
| workingDir: workingDir, |
| environment: environment, |
| runInShell: runInShell); |
| } on IOException catch (e) { |
| throw RunProcessException( |
| 'Pub failed to run subprocess `$executable`: $e'); |
| } |
| |
| var pubResult = |
| PubProcessResult(result.stdout, result.stderr, result.exitCode); |
| log.processResult(executable, pubResult); |
| return pubResult; |
| }); |
| } |
| |
| /// Spawns the process located at [executable], passing in [args]. |
| /// |
| /// Returns a [Future] that will complete with the [Process] once it's been |
| /// started. |
| /// |
| /// The spawned process will inherit its parent's environment variables. If |
| /// [environment] is provided, that will be used to augment (not replace) the |
| /// the inherited variables. |
| @visibleForTesting |
| Future<_PubProcess> startProcess(String executable, List<String> args, |
| {workingDir, Map<String, String> environment, bool runInShell = false}) { |
| return _descriptorPool.request().then((resource) async { |
| Process ioProcess; |
| try { |
| ioProcess = await _doProcess(Process.start, executable, args, |
| workingDir: workingDir, |
| environment: environment, |
| runInShell: runInShell); |
| } on IOException catch (e) { |
| throw RunProcessException( |
| 'Pub failed to run subprocess `$executable`: $e'); |
| } |
| |
| var process = _PubProcess(ioProcess); |
| unawaited(process.exitCode.whenComplete(resource.release)); |
| return process; |
| }); |
| } |
| |
| /// Like [runProcess], but synchronous. |
| PubProcessResult runProcessSync(String executable, List<String> args, |
| {String workingDir, |
| Map<String, String> environment, |
| bool runInShell = false}) { |
| ArgumentError.checkNotNull(executable, 'executable'); |
| ProcessResult result; |
| try { |
| result = _doProcess(Process.runSync, executable, args, |
| workingDir: workingDir, |
| environment: environment, |
| runInShell: runInShell); |
| } on IOException catch (e) { |
| throw RunProcessException('Pub failed to run subprocess `$executable`: $e'); |
| } |
| var pubResult = |
| PubProcessResult(result.stdout, result.stderr, result.exitCode); |
| log.processResult(executable, pubResult); |
| return pubResult; |
| } |
| |
| /// A wrapper around [Process] that exposes `dart:async`-style APIs. |
| class _PubProcess { |
| /// The underlying `dart:io` [Process]. |
| final Process _process; |
| |
| /// The mutable field for [stdin]. |
| EventSink<List<int>> _stdin; |
| |
| /// The mutable field for [stdinClosed]. |
| Future _stdinClosed; |
| |
| /// The mutable field for [stdout]. |
| ByteStream _stdout; |
| |
| /// The mutable field for [stderr]. |
| ByteStream _stderr; |
| |
| /// The mutable field for [exitCode]. |
| Future<int> _exitCode; |
| |
| /// The sink used for passing data to the process's standard input stream. |
| /// |
| /// Errors on this stream are surfaced through [stdinClosed], [stdout], |
| /// [stderr], and [exitCode], which are all members of an [ErrorGroup]. |
| EventSink<List<int>> get stdin => _stdin; |
| |
| // TODO(nweiz): write some more sophisticated Future machinery so that this |
| // doesn't surface errors from the other streams/futures, but still passes its |
| // unhandled errors to them. Right now it's impossible to recover from a stdin |
| // error and continue interacting with the process. |
| /// A [Future] that completes when [stdin] is closed, either by the user or by |
| /// the process itself. |
| /// |
| /// This is in an [ErrorGroup] with [stdout], [stderr], and [exitCode], so any |
| /// error in process will be passed to it, but won't reach the top-level error |
| /// handler unless nothing has handled it. |
| Future get stdinClosed => _stdinClosed; |
| |
| /// The process's standard output stream. |
| /// |
| /// This is in an [ErrorGroup] with [stdinClosed], [stderr], and [exitCode], |
| /// so any error in process will be passed to it, but won't reach the |
| /// top-level error handler unless nothing has handled it. |
| ByteStream get stdout => _stdout; |
| |
| /// The process's standard error stream. |
| /// |
| /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [exitCode], |
| /// so any error in process will be passed to it, but won't reach the |
| /// top-level error handler unless nothing has handled it. |
| ByteStream get stderr => _stderr; |
| |
| /// A [Future] that will complete to the process's exit code once the process |
| /// has finished running. |
| /// |
| /// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [stderr], so |
| /// any error in process will be passed to it, but won't reach the top-level |
| /// error handler unless nothing has handled it. |
| Future<int> get exitCode => _exitCode; |
| |
| /// Creates a new [_PubProcess] wrapping [process]. |
| _PubProcess(Process process) : _process = process { |
| var errorGroup = ErrorGroup(); |
| |
| var pair = _consumerToSink(process.stdin); |
| _stdin = pair.first; |
| _stdinClosed = errorGroup.registerFuture(pair.last); |
| |
| _stdout = ByteStream(errorGroup.registerStream(process.stdout)); |
| _stderr = ByteStream(errorGroup.registerStream(process.stderr)); |
| |
| var exitCodeCompleter = Completer<int>(); |
| _exitCode = errorGroup.registerFuture(exitCodeCompleter.future); |
| _process.exitCode.then((code) => exitCodeCompleter.complete(code)); |
| } |
| |
| /// Sends [signal] to the underlying process. |
| bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => |
| _process.kill(signal); |
| } |
| |
| /// Calls [fn] with appropriately modified arguments. |
| /// |
| /// [fn] should have the same signature as [Process.start], except that the |
| /// returned value may have any return type. |
| T _doProcess<T>( |
| T Function(String, List<String>, |
| {String workingDirectory, |
| Map<String, String> environment, |
| bool runInShell}) |
| fn, |
| String executable, |
| List<String> args, |
| {String workingDir, |
| Map<String, String> environment, |
| bool runInShell = false}) { |
| // TODO(rnystrom): Should dart:io just handle this? |
| // Spawning a process on Windows will not look for the executable in the |
| // system path. So, if executable looks like it needs that (i.e. it doesn't |
| // have any path separators in it), then spawn it through a shell. |
| if (Platform.isWindows && !executable.contains('\\')) { |
| args = ['/c', executable, ...args]; |
| executable = 'cmd'; |
| } |
| |
| log.process(executable, args, workingDir ?? '.'); |
| |
| return fn(executable, args, |
| workingDirectory: workingDir, |
| environment: environment, |
| runInShell: runInShell); |
| } |
| |
| /// Updates [path]'s modification time. |
| void touch(String path) => File(path).setLastModifiedSync(DateTime.now()); |
| |
| /// Creates a temporary directory and passes its path to [fn]. |
| /// |
| /// Once the [Future] returned by [fn] completes, the temporary directory and |
| /// all its contents are deleted. [fn] can also return `null`, in which case |
| /// the temporary directory is deleted immediately afterwards. |
| /// |
| /// Returns a future that completes to the value that the future returned from |
| /// [fn] completes to. |
| Future<T> withTempDir<T>(FutureOr<T> Function(String path) fn) async { |
| var tempDir = await _createSystemTempDir(); |
| try { |
| return await fn(tempDir); |
| } finally { |
| deleteEntry(tempDir); |
| } |
| } |
| |
| /// Binds an [HttpServer] to [host] and [port]. |
| /// |
| /// If [host] is "localhost", this will automatically listen on both the IPv4 |
| /// and IPv6 loopback addresses. |
| Future<HttpServer> bindServer(String host, int port) async { |
| var server = host == 'localhost' |
| ? await HttpMultiServer.loopback(port) |
| : await HttpServer.bind(host, port); |
| server.autoCompress = true; |
| return server; |
| } |
| |
| /// Extracts a `.tar.gz` file from [stream] to [destination]. |
| Future extractTarGz(Stream<List<int>> stream, String destination) async { |
| log.fine('Extracting .tar.gz stream to $destination.'); |
| |
| destination = path.absolute(destination); |
| final reader = TarReader(stream.transform(gzip.decoder)); |
| while (await reader.moveNext()) { |
| final entry = reader.current; |
| |
| final filePath = path.joinAll([ |
| destination, |
| // Tar file names always use forward slashes |
| ...path.posix.split(entry.name), |
| ]); |
| |
| if (!path.isWithin(destination, filePath)) { |
| // The tar contains entries that would be written outside of the |
| // destination. That doesn't happen by accident, assume that the tar file |
| // is malicious. |
| await reader.cancel(); |
| throw FormatException('Invalid tar entry: ${entry.name}'); |
| } |
| |
| final parentDirectory = path.dirname(filePath); |
| |
| bool checkValidTarget(String linkTarget) { |
| final isValid = path.isWithin(destination, linkTarget); |
| if (!isValid) { |
| log.fine('Skipping ${entry.name}: Invalid link target'); |
| } |
| |
| return isValid; |
| } |
| |
| switch (entry.type) { |
| case TypeFlag.dir: |
| ensureDir(filePath); |
| break; |
| case TypeFlag.reg: |
| case TypeFlag.regA: |
| // Regular file |
| deleteIfLink(filePath); |
| ensureDir(parentDirectory); |
| await _createFileFromStream(entry.contents, filePath); |
| |
| if (Platform.isLinux || Platform.isMacOS) { |
| // Apply executable bits from tar header, but don't change r/w bits |
| // from the default |
| final mode = _defaultMode | (entry.header.mode & _executableMask); |
| |
| if (mode != _defaultMode) { |
| _chmod(mode, filePath); |
| } |
| } |
| break; |
| case TypeFlag.symlink: |
| // Link to another file in this tar, relative from this entry. |
| final resolvedTarget = path.joinAll( |
| [parentDirectory, ...path.posix.split(entry.header.linkName)]); |
| if (!checkValidTarget(resolvedTarget)) { |
| // Don't allow links to files outside of this tar. |
| break; |
| } |
| |
| ensureDir(parentDirectory); |
| createSymlink( |
| path.relative(resolvedTarget, from: parentDirectory), filePath); |
| break; |
| case TypeFlag.link: |
| // We generate hardlinks as symlinks too, but their linkName is relative |
| // to the root of the tar file (unlike symlink entries, whose linkName |
| // is relative to the entry itself). |
| final fromDestination = path.join(destination, entry.header.linkName); |
| if (!checkValidTarget(fromDestination)) { |
| break; // Link points outside of the tar file. |
| } |
| |
| final fromFile = path.relative(fromDestination, from: parentDirectory); |
| ensureDir(parentDirectory); |
| createSymlink(fromFile, filePath); |
| break; |
| default: |
| // Only extract files |
| continue; |
| } |
| } |
| |
| log.fine('Extracted .tar.gz to $destination.'); |
| } |
| |
| /// Create a .tar.gz archive from a list of entries. |
| /// |
| /// Each entry can be a [String], [Directory], or [File] object. The root of |
| /// the archive is considered to be [baseDir], which defaults to the current |
| /// working directory. |
| /// |
| /// Returns a [ByteStream] that emits the contents of the archive. |
| ByteStream createTarGz( |
| List<String> contents, { |
| @required String baseDir, |
| }) { |
| var buffer = StringBuffer(); |
| buffer.write('Creating .tar.gz stream containing:\n'); |
| contents.forEach(buffer.writeln); |
| log.fine(buffer.toString()); |
| |
| ArgumentError.checkNotNull(baseDir, 'baseDir'); |
| baseDir = path.absolute(baseDir); |
| |
| final tarContents = Stream.fromIterable(contents.map((entry) { |
| entry = path.absolute(entry); |
| if (!path.isWithin(baseDir, entry)) { |
| throw ArgumentError('Entry $entry is not inside $baseDir.'); |
| } |
| |
| final relative = path.relative(entry, from: baseDir); |
| // On Windows, we can't open some files without normalizing them |
| final file = File(path.normalize(entry)); |
| final stat = file.statSync(); |
| |
| if (stat.type == FileSystemEntityType.link) { |
| log.message('$entry is a link locally, but will be uploaded as a ' |
| 'duplicate file.'); |
| } |
| |
| return TarEntry( |
| TarHeader( |
| // Ensure paths in tar files use forward slashes |
| name: path.url.joinAll(path.split(relative)), |
| // We want to keep executable bits, but otherwise use the default |
| // file mode |
| mode: _defaultMode | (stat.mode & _executableMask), |
| size: stat.size, |
| modified: stat.changed, |
| userName: 'pub', |
| groupName: 'pub', |
| ), |
| file.openRead(), |
| ); |
| })); |
| |
| return ByteStream(tarContents.transform(tarWriter).transform(gzip.encoder)); |
| } |
| |
| /// Contains the results of invoking a [Process] and waiting for it to complete. |
| class PubProcessResult { |
| final List<String> stdout; |
| final List<String> stderr; |
| final int exitCode; |
| |
| PubProcessResult(String stdout, String stderr, this.exitCode) |
| : stdout = _toLines(stdout), |
| stderr = _toLines(stderr); |
| |
| // TODO(rnystrom): Remove this and change to returning one string. |
| static List<String> _toLines(String output) { |
| var lines = splitLines(output); |
| if (lines.isNotEmpty && lines.last == '') lines.removeLast(); |
| return lines; |
| } |
| |
| bool get success => exitCode == exit_codes.SUCCESS; |
| } |