From 5dc916716e5f31d2d4199106ad558bdb568bf349 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 12:49:23 -0400 Subject: [PATCH 01/11] Initial, unchanged snapshot of original repo. --- .../process/.github/workflows/process.yml | 38 ++ packages/process/.gitignore | 17 + packages/process/AUTHORS | 6 + packages/process/CHANGELOG.md | 188 ++++++ packages/process/CONTRIBUTING.md | 24 + packages/process/LICENSE | 26 + packages/process/README.md | 15 + packages/process/analysis_options.yaml | 6 + packages/process/lib/process.dart | 8 + .../process/lib/src/interface/common.dart | 164 +++++ .../process/lib/src/interface/exceptions.dart | 106 ++++ .../src/interface/local_process_manager.dart | 153 +++++ .../lib/src/interface/process_manager.dart | 183 ++++++ .../lib/src/interface/process_wrapper.dart | 83 +++ packages/process/pubspec.yaml | 16 + .../test/src/interface/common_test.dart | 577 ++++++++++++++++++ .../src/interface/process_wrapper_test.dart | 115 ++++ packages/process/test/utils.dart | 14 + 18 files changed, 1739 insertions(+) create mode 100644 packages/process/.github/workflows/process.yml create mode 100644 packages/process/.gitignore create mode 100644 packages/process/AUTHORS create mode 100644 packages/process/CHANGELOG.md create mode 100644 packages/process/CONTRIBUTING.md create mode 100644 packages/process/LICENSE create mode 100644 packages/process/README.md create mode 100644 packages/process/analysis_options.yaml create mode 100644 packages/process/lib/process.dart create mode 100644 packages/process/lib/src/interface/common.dart create mode 100644 packages/process/lib/src/interface/exceptions.dart create mode 100644 packages/process/lib/src/interface/local_process_manager.dart create mode 100644 packages/process/lib/src/interface/process_manager.dart create mode 100644 packages/process/lib/src/interface/process_wrapper.dart create mode 100644 packages/process/pubspec.yaml create mode 100644 packages/process/test/src/interface/common_test.dart create mode 100644 packages/process/test/src/interface/process_wrapper_test.dart create mode 100644 packages/process/test/utils.dart diff --git a/packages/process/.github/workflows/process.yml b/packages/process/.github/workflows/process.yml new file mode 100644 index 00000000000..dfcf6cdf389 --- /dev/null +++ b/packages/process/.github/workflows/process.yml @@ -0,0 +1,38 @@ +name: Process Package + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + correctness: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 + with: + sdk: dev + - name: Install dependencies + run: dart pub upgrade + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + - name: Analyze project source + run: dart analyze --fatal-infos + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + sdk: [stable, beta, dev] + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 + with: + sdk: ${{ matrix.sdk }} + - name: Install dependencies + run: dart pub upgrade + - name: Run Tests + run: dart test diff --git a/packages/process/.gitignore b/packages/process/.gitignore new file mode 100644 index 00000000000..fda175088d2 --- /dev/null +++ b/packages/process/.gitignore @@ -0,0 +1,17 @@ +### Dart template +# Don’t commit the following directories created by pub. +.buildlog +.dart_tool +.pub/ +build/ +packages +.packages + +# Include when developing application packages. +pubspec.lock + +# IDE +.project +.settings +.idea +.c9 diff --git a/packages/process/AUTHORS b/packages/process/AUTHORS new file mode 100644 index 00000000000..ad59f118417 --- /dev/null +++ b/packages/process/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Process project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/process/CHANGELOG.md b/packages/process/CHANGELOG.md new file mode 100644 index 00000000000..57232d44240 --- /dev/null +++ b/packages/process/CHANGELOG.md @@ -0,0 +1,188 @@ +#### 5.0.0 + +* Remove the `covariant` keyword from `stderrEncoding` and `stdoutEncoding` + parameters. +* Update dependencies to work on Dart 3. +* Bumped min SDK dependency to nearest non-prerelease version (2.14.0) + +#### 4.2.4 + +* Mark `stderrEncoding` and `stdoutEncoding` parameters as nullable again, + now that the upstream SDK issue has been fixed. + +#### 4.2.3 + +* Rollback to version 4.2.1 (https://github.com/google/process.dart/issues/64) + +#### 4.2.2 + +* Mark `stderrEncoding` and `stdoutEncoding` parameters as nullable. + +#### 4.2.1 + +* Added custom exception types `ProcessPackageException` and + `ProcessPackageExecutableNotFoundException` to provide extra + information from exception conditions. + +#### 4.2.0 + +* Fix the signature of `ProcessManager.canRun` to be consistent with + `LocalProcessManager`. + +#### 4.1.1 + +* Fixed `getExecutablePath()` to only return path items that are + executable and readable to the user. + +#### 4.1.0 + +* Fix the signatures of `ProcessManager.run`, `.runSync`, and `.start` to be + consistent with `LocalProcessManager`'s. +* Added more details to the `ArgumentError` thrown when a command cannot be resolved + to an executable. + +#### 4.0.0 + +* First stable null safe release. + +#### 4.0.0-nullsafety.4 + +* Update supported SDK range. + +#### 4.0.0-nullsafety.3 + +* Update supported SDK range. + +#### 4.0.0-nullsafety.2 + +* Update supported SDK range. + +#### 4.0.0-nullsafety.1 + +* Migrate to null-safety. +* Remove record/replay functionality. +* Remove implicit casts in preparation for null-safety. +* Remove dependency on `package:intl` and `package:meta`. + +#### 3.0.13 + +* Handle `currentDirectory` throwing an exception in `getExecutablePath()`. + +#### 3.0.12 + +* Updated version constraint on intl. + +#### 3.0.11 + +* Fix bug: don't add quotes if the file name already has quotes. + +#### 3.0.10 + +* Added quoted strings to indicate where the command name ends and the arguments +begin otherwise, the file name is ambiguous on Windows. + +#### 3.0.9 + +* Fixed bug in `ProcessWrapper` + +#### 3.0.8 + +* Fixed bug in `ProcessWrapper` + +#### 3.0.7 + +* Renamed `Process` to `ProcessWrapper` + +#### 3.0.6 + +* Added class `Process`, a simple wrapper around dart:io's `Process` class. + +#### 3.0.5 + +* Fixes for missing_return analysis errors with 2.10.0-dev.1.0. + +#### 3.0.4 + +* Fix unit tests +* Update SDK constraint to 3. + +#### 3.0.3 + +* Update dependency on `package:file` + +#### 3.0.2 + +* Remove upper case constants. +* Update SDK constraint to 2.0.0-dev.54.0. +* Fix tests for Dart 2. + +#### 3.0.1 + +* General cleanup + +#### 3.0.0 + +* Cleanup getExecutablePath() to better respect the platform + +#### 2.0.9 + +* Bumped `package:file` dependency + +### 2.0.8 + +* Fixed method getArguments to qualify the map method with the specific + String type + +### 2.0.7 + +* Remove `set exitCode` instances + +### 2.0.6 + +* Fix SDK constraint. +* rename .analysis_options file to analaysis_options.yaml. +* Use covariant in place of @checked. +* Update comment style generics. + +### 2.0.5 + +* Bumped maximum Dart SDK version to 2.0.0-dev.infinity + +### 2.0.4 + +* relax dependency requirement for `intl` + +### 2.0.3 + +* relax dependency requirement for `platform` + +#### 2.0.2 + +* Fix a strong mode function expression return type inference bug with Dart + 1.23.0-dev.10.0. + +#### 2.0.1 + +* Fixed bug in `ReplayProcessManager` whereby it could try to write to `stdout` + or `stderr` after the streams were closed. + +#### 2.0.0 + +* Bumped `package:file` dependency to 2.0.1 + +#### 1.1.0 + +* Added support to transparently find the right executable under Windows. + +#### 1.0.1 + +* The `executable` and `arguments` parameters have been merged into one + `command` parameter in the `run`, `runSync`, and `start` methods of + `ProcessManager`. +* Added support for sanitization of command elements in + `RecordingProcessManager` and `ReplayProcessManager` via the `CommandElement` + class. + +#### 1.0.0 + +* Initial version diff --git a/packages/process/CONTRIBUTING.md b/packages/process/CONTRIBUTING.md new file mode 100644 index 00000000000..0786fdf4346 --- /dev/null +++ b/packages/process/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# How to contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult [GitHub Help] for more +information on using pull requests. + +[GitHub Help]: https://help.github.com/articles/about-pull-requests/ diff --git a/packages/process/LICENSE b/packages/process/LICENSE new file mode 100644 index 00000000000..389ce985634 --- /dev/null +++ b/packages/process/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/process/README.md b/packages/process/README.md new file mode 100644 index 00000000000..de445e0b990 --- /dev/null +++ b/packages/process/README.md @@ -0,0 +1,15 @@ +# Process + +[![Build Status -](https://travis-ci.org/google/process.dart.svg?branch=master)](https://travis-ci.org/google/process.dart) +[![Coverage Status -](https://coveralls.io/repos/github/google/process.dart/badge.svg?branch=master)](https://coveralls.io/github/google/process.dart?branch=master) + +A generic process invocation abstraction for Dart. + +Like `dart:io`, `package:process` supplies a rich, Dart-idiomatic API for +spawning OS processes. + +Unlike `dart:io`, `package:process`: + +- Can be used to implement custom process invocation backends. +- Comes with a record-replay implementation out-of-the-box, making it super + easy to test code that spawns processes in a hermetic way. diff --git a/packages/process/analysis_options.yaml b/packages/process/analysis_options.yaml new file mode 100644 index 00000000000..8fbd2e443ac --- /dev/null +++ b/packages/process/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +analyzer: + errors: + # Allow having TODOs in the code + todo: ignore diff --git a/packages/process/lib/process.dart b/packages/process/lib/process.dart new file mode 100644 index 00000000000..af513a0bd78 --- /dev/null +++ b/packages/process/lib/process.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2017, 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. + +export 'src/interface/exceptions.dart'; +export 'src/interface/local_process_manager.dart'; +export 'src/interface/process_manager.dart'; +export 'src/interface/process_wrapper.dart'; diff --git a/packages/process/lib/src/interface/common.dart b/packages/process/lib/src/interface/common.dart new file mode 100644 index 00000000000..25d52f5e04a --- /dev/null +++ b/packages/process/lib/src/interface/common.dart @@ -0,0 +1,164 @@ +// Copyright (c) 2017, 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. + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:path/path.dart' show Context; +import 'package:platform/platform.dart'; + +import 'exceptions.dart'; + +const Map _osToPathStyle = { + 'linux': 'posix', + 'macos': 'posix', + 'android': 'posix', + 'ios': 'posix', + 'fuchsia': 'posix', + 'windows': 'windows', +}; + +/// Sanitizes the executable path on Windows. +/// https://github.com/dart-lang/sdk/issues/37751 +String sanitizeExecutablePath(String executable, + {Platform platform = const LocalPlatform()}) { + if (executable.isEmpty) { + return executable; + } + if (!platform.isWindows) { + return executable; + } + if (executable.contains(' ') && !executable.contains('"')) { + // Use quoted strings to indicate where the file name ends and the arguments begin; + // otherwise, the file name is ambiguous. + return '"$executable"'; + } + return executable; +} + +/// Searches the `PATH` for the executable that [executable] is supposed to launch. +/// +/// This first builds a list of candidate paths where the executable may reside. +/// If [executable] is already an absolute path, then the `PATH` environment +/// variable will not be consulted, and the specified absolute path will be the +/// only candidate that is considered. +/// +/// Once the list of candidate paths has been constructed, this will pick the +/// first such path that represents an existent file. +/// +/// Return `null` if there were no viable candidates, meaning the executable +/// could not be found. +/// +/// If [platform] is not specified, it will default to the current platform. +String? getExecutablePath( + String executable, + String? workingDirectory, { + Platform platform = const LocalPlatform(), + FileSystem fs = const LocalFileSystem(), + bool throwOnFailure = false, +}) { + assert(_osToPathStyle[platform.operatingSystem] == fs.path.style.name); + try { + workingDirectory ??= fs.currentDirectory.path; + } on FileSystemException { + // The `currentDirectory` getter can throw a FileSystemException for example + // when the process doesn't have read/list permissions in each component of + // the cwd path. In this case, fall back on '.'. + workingDirectory ??= '.'; + } + Context context = Context(style: fs.path.style, current: workingDirectory); + + // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2 + // is available. + String pathSeparator = platform.isWindows ? ';' : ':'; + + List extensions = []; + if (platform.isWindows && context.extension(executable).isEmpty) { + extensions = platform.environment['PATHEXT']!.split(pathSeparator); + } + + List candidates = []; + List searchPath; + if (executable.contains(context.separator)) { + // Deal with commands that specify a relative or absolute path differently. + searchPath = [workingDirectory]; + } else { + searchPath = platform.environment['PATH']!.split(pathSeparator); + } + candidates = _getCandidatePaths(executable, searchPath, extensions, context); + final List foundCandidates = []; + for (String path in candidates) { + final File candidate = fs.file(path); + FileStat stat = candidate.statSync(); + // Only return files or links that exist. + if (stat.type == FileSystemEntityType.notFound || + stat.type == FileSystemEntityType.directory) { + continue; + } + + // File exists, but we don't know if it's readable/executable yet. + foundCandidates.add(candidate.path); + + const int isExecutable = 0x40; + const int isReadable = 0x100; + const int isExecutableAndReadable = isExecutable | isReadable; + // Should only return files or links that are readable and executable by the + // user. + + // On Windows it's not actually possible to only return files that are + // readable, since Dart reports files that have had read permission removed + // as being readable, but not checking for it is the same as checking for it + // and finding it readable, so we use the same check here on all platforms, + // so that if Dart ever gets fixed, it'll just work. + if (stat.mode & isExecutableAndReadable == isExecutableAndReadable) { + return path; + } + } + if (throwOnFailure) { + if (foundCandidates.isNotEmpty) { + throw ProcessPackageExecutableNotFoundException( + executable, + message: + 'Found candidates, but lacked sufficient permissions to execute "$executable".', + workingDirectory: workingDirectory, + candidates: foundCandidates, + searchPath: searchPath, + ); + } else { + throw ProcessPackageExecutableNotFoundException( + executable, + message: 'Failed to find "$executable" in the search path.', + workingDirectory: workingDirectory, + searchPath: searchPath, + ); + } + } + return null; +} + +/// Returns all possible combinations of `$searchPath\$command.$ext` for +/// `searchPath` in [searchPaths] and `ext` in [extensions]. +/// +/// If [extensions] is empty, it will just enumerate all +/// `$searchPath\$command`. +/// If [command] is an absolute path, it will just enumerate +/// `$command.$ext`. +List _getCandidatePaths( + String command, + List searchPaths, + List extensions, + Context context, +) { + List withExtensions = extensions.isNotEmpty + ? extensions.map((String ext) => '$command$ext').toList() + : [command]; + if (context.isAbsolute(command)) { + return withExtensions; + } + return searchPaths + .map((String path) => + withExtensions.map((String command) => context.join(path, command))) + .expand((Iterable e) => e) + .toList() + .cast(); +} diff --git a/packages/process/lib/src/interface/exceptions.dart b/packages/process/lib/src/interface/exceptions.dart new file mode 100644 index 00000000000..ee407ca514c --- /dev/null +++ b/packages/process/lib/src/interface/exceptions.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2017, 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. + +import 'dart:io' show ProcessException; + +/// A specialized exception class for this package, so that it can throw +/// customized exceptions with more information. +class ProcessPackageException extends ProcessException { + /// Create a const ProcessPackageException. + /// + /// The [executable] should be the name of the executable to be run. + /// + /// The optional [workingDirectory] is the directory where the command + /// execution is attempted. + /// + /// The optional [arguments] is a list of the arguments to given to the + /// executable, already separated. + /// + /// The optional [message] is an additional message to be included in the + /// exception string when printed. + /// + /// The optional [errorCode] is the error code received when the executable + /// was run. Zero means it ran successfully, or that no error code was + /// available. + /// + /// See [ProcessException] for more information. + const ProcessPackageException( + String executable, { + List arguments = const [], + String message = "", + int errorCode = 0, + this.workingDirectory, + }) : super(executable, arguments, message, errorCode); + + /// Creates a [ProcessPackageException] from a [ProcessException]. + factory ProcessPackageException.fromProcessException( + ProcessException exception, { + String? workingDirectory, + }) { + return ProcessPackageException( + exception.executable, + arguments: exception.arguments, + message: exception.message, + errorCode: exception.errorCode, + workingDirectory: workingDirectory, + ); + } + + /// The optional working directory that the command was being executed in. + final String? workingDirectory; + + // Don't implement a toString() for this exception, since code may be + // depending upon the format of ProcessException.toString(). +} + +/// An exception for when an executable is not found that was expected to be found. +class ProcessPackageExecutableNotFoundException + extends ProcessPackageException { + /// Creates a const ProcessPackageExecutableNotFoundException + /// + /// The optional [candidates] are the files matching the expected executable + /// on the [searchPath]. + /// + /// The optional [searchPath] is the list of directories searched for the + /// expected executable. + /// + /// See [ProcessPackageException] for more information. + const ProcessPackageExecutableNotFoundException( + String executable, { + List arguments = const [], + String message = "", + int errorCode = 0, + String? workingDirectory, + this.candidates = const [], + this.searchPath = const [], + }) : super( + executable, + arguments: arguments, + message: message, + errorCode: errorCode, + workingDirectory: workingDirectory, + ); + + /// The list of non-viable executable candidates found. + final List candidates; + + /// The search path used to find candidates. + final List searchPath; + + @override + String toString() { + StringBuffer buffer = StringBuffer('$runtimeType: $message\n'); + // Don't add an extra space if there are no arguments. + final String args = arguments.isNotEmpty ? ' ${arguments.join(' ')}' : ''; + buffer.writeln(' Command: $executable$args'); + if (workingDirectory != null && workingDirectory!.isNotEmpty) { + buffer.writeln(' Working Directory: $workingDirectory'); + } + if (candidates.isNotEmpty) { + buffer.writeln(' Candidates:\n ${candidates.join('\n ')}'); + } + buffer.writeln(' Search Path:\n ${searchPath.join('\n ')}'); + return buffer.toString(); + } +} diff --git a/packages/process/lib/src/interface/local_process_manager.dart b/packages/process/lib/src/interface/local_process_manager.dart new file mode 100644 index 00000000000..fc260b22d22 --- /dev/null +++ b/packages/process/lib/src/interface/local_process_manager.dart @@ -0,0 +1,153 @@ +// Copyright (c) 2017, 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. + +import 'dart:convert'; +import 'dart:io' + show + Process, + ProcessResult, + ProcessSignal, + ProcessStartMode, + ProcessException, + systemEncoding; + +import 'common.dart'; +import 'exceptions.dart'; +import 'process_manager.dart'; + +/// Local implementation of the `ProcessManager` interface. +/// +/// This implementation delegates directly to the corresponding static methods +/// in `dart:io`. +/// +/// All methods that take a `command` will run `toString()` on the command +/// elements to derive the executable and arguments that should be passed to +/// the underlying `dart:io` methods. Thus, the degenerate case of +/// `List` will trivially work as expected. +class LocalProcessManager implements ProcessManager { + /// Creates a new `LocalProcessManager`. + const LocalProcessManager(); + + @override + Future start( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal, + }) { + try { + return Process.start( + sanitizeExecutablePath(_getExecutable( + command, + workingDirectory, + runInShell, + )), + _getArguments(command), + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + mode: mode, + ); + } on ProcessException catch (exception) { + throw ProcessPackageException.fromProcessException(exception, + workingDirectory: workingDirectory); + } + } + + @override + Future run( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, + }) { + try { + return Process.run( + sanitizeExecutablePath(_getExecutable( + command, + workingDirectory, + runInShell, + )), + _getArguments(command), + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + ); + } on ProcessException catch (exception) { + throw ProcessPackageException.fromProcessException(exception, + workingDirectory: workingDirectory); + } + } + + @override + ProcessResult runSync( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, + }) { + try { + return Process.runSync( + sanitizeExecutablePath(_getExecutable( + command, + workingDirectory, + runInShell, + )), + _getArguments(command), + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + ); + } on ProcessException catch (exception) { + throw ProcessPackageException.fromProcessException(exception, + workingDirectory: workingDirectory); + } + } + + @override + bool canRun(covariant String executable, {String? workingDirectory}) => + getExecutablePath(executable, workingDirectory, throwOnFailure: false) != + null; + + @override + bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) { + return Process.killPid(pid, signal); + } +} + +String _getExecutable( + List command, String? workingDirectory, bool runInShell) { + String commandName = command.first.toString(); + if (runInShell) { + return commandName; + } + return getExecutablePath( + commandName, + workingDirectory, + throwOnFailure: true, + )!; +} + +List _getArguments(List command) => + // Adding a specific type to map in order to workaround dart issue + // https://github.com/dart-lang/sdk/issues/32414 + command + .skip(1) + .map((dynamic element) => element.toString()) + .toList(); diff --git a/packages/process/lib/src/interface/process_manager.dart b/packages/process/lib/src/interface/process_manager.dart new file mode 100644 index 00000000000..f799dd9843a --- /dev/null +++ b/packages/process/lib/src/interface/process_manager.dart @@ -0,0 +1,183 @@ +// Copyright (c) 2017, 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. + +import 'dart:convert'; +import 'dart:io' + show + Process, + ProcessResult, + ProcessSignal, + ProcessStartMode, + systemEncoding; + +/// Manages the creation of abstract processes. +/// +/// Using instances of this class provides level of indirection from the static +/// methods in the [Process] class, which in turn allows the underlying +/// implementation to be mocked out or decorated for testing and debugging +/// purposes. +abstract class ProcessManager { + /// Starts a process by running the specified [command]. + /// + /// The first element in [command] will be treated as the executable to run, + /// with subsequent elements being passed as arguments to the executable. It + /// is left to implementations to decide what element types they support in + /// the [command] list. + /// + /// Returns a `Future` that completes with a Process instance when + /// the process has been successfully started. That [Process] object can be + /// used to interact with the process. If the process cannot be started, the + /// returned [Future] completes with an exception. + /// + /// Use [workingDirectory] to set the working directory for the process. Note + /// that the change of directory occurs before executing the process on some + /// platforms, which may have impact when using relative paths for the + /// executable and the arguments. + /// + /// Use [environment] to set the environment variables for the process. If not + /// set, the environment of the parent process is inherited. Currently, only + /// US-ASCII environment variables are supported and errors are likely to occur + /// if an environment variable with code-points outside the US-ASCII range is + /// passed in. + /// + /// If [includeParentEnvironment] is `true`, the process's environment will + /// include the parent process's environment, with [environment] taking + /// precedence. Default is `true`. + /// + /// If [runInShell] is `true`, the process will be spawned through a system + /// shell. On Linux and OS X, `/bin/sh` is used, while + /// `%WINDIR%\system32\cmd.exe` is used on Windows. + /// + /// Users must read all data coming on the `stdout` and `stderr` + /// streams of processes started with [start]. If the user + /// does not read all data on the streams the underlying system + /// resources will not be released since there is still pending data. + /// + /// The following code uses `start` to grep for `main` in the + /// file `test.dart` on Linux. + /// + /// ProcessManager mgr = new LocalProcessManager(); + /// mgr.start(['grep', '-i', 'main', 'test.dart']).then((process) { + /// stdout.addStream(process.stdout); + /// stderr.addStream(process.stderr); + /// }); + /// + /// If [mode] is [ProcessStartMode.normal] (the default) a child + /// process will be started with `stdin`, `stdout` and `stderr` + /// connected. + /// + /// If `mode` is [ProcessStartMode.detached] a detached process will + /// be created. A detached process has no connection to its parent, + /// and can keep running on its own when the parent dies. The only + /// information available from a detached process is its `pid`. There + /// is no connection to its `stdin`, `stdout` or `stderr`, nor will + /// the process' exit code become available when it terminates. + /// + /// If `mode` is [ProcessStartMode.detachedWithStdio] a detached + /// process will be created where the `stdin`, `stdout` and `stderr` + /// are connected. The creator can communicate with the child through + /// these. The detached process will keep running even if these + /// communication channels are closed. The process' exit code will + /// not become available when it terminated. + /// + /// The default value for `mode` is `ProcessStartMode.normal`. + Future start( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal, + }); + + /// Starts a process and runs it non-interactively to completion. + /// + /// The first element in [command] will be treated as the executable to run, + /// with subsequent elements being passed as arguments to the executable. It + /// is left to implementations to decide what element types they support in + /// the [command] list. + /// + /// Use [workingDirectory] to set the working directory for the process. Note + /// that the change of directory occurs before executing the process on some + /// platforms, which may have impact when using relative paths for the + /// executable and the arguments. + /// + /// Use [environment] to set the environment variables for the process. If not + /// set the environment of the parent process is inherited. Currently, only + /// US-ASCII environment variables are supported and errors are likely to occur + /// if an environment variable with code-points outside the US-ASCII range is + /// passed in. + /// + /// If [includeParentEnvironment] is `true`, the process's environment will + /// include the parent process's environment, with [environment] taking + /// precedence. Default is `true`. + /// + /// If [runInShell] is true, the process will be spawned through a system + /// shell. On Linux and OS X, `/bin/sh` is used, while + /// `%WINDIR%\system32\cmd.exe` is used on Windows. + /// + /// The encoding used for decoding `stdout` and `stderr` into text is + /// controlled through [stdoutEncoding] and [stderrEncoding]. The + /// default encoding is [systemEncoding]. If `null` is used no + /// decoding will happen and the [ProcessResult] will hold binary + /// data. + /// + /// Returns a `Future` that completes with the + /// result of running the process, i.e., exit code, standard out and + /// standard in. + /// + /// The following code uses `run` to grep for `main` in the + /// file `test.dart` on Linux. + /// + /// ProcessManager mgr = new LocalProcessManager(); + /// mgr.run('grep', ['-i', 'main', 'test.dart']).then((result) { + /// stdout.write(result.stdout); + /// stderr.write(result.stderr); + /// }); + Future run( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, + }); + + /// Starts a process and runs it to completion. This is a synchronous + /// call and will block until the child process terminates. + /// + /// The arguments are the same as for [run]`. + /// + /// Returns a `ProcessResult` with the result of running the process, + /// i.e., exit code, standard out and standard in. + ProcessResult runSync( + List command, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, + }); + + /// Returns `true` if the [executable] exists and if it can be executed. + bool canRun(dynamic executable, {String? workingDirectory}); + + /// Kills the process with id [pid]. + /// + /// Where possible, sends the [signal] to the process with id + /// `pid`. This includes Linux and OS X. The default signal is + /// [ProcessSignal.sigterm] which will normally terminate the + /// process. + /// + /// On platforms without signal support, including Windows, the call + /// just terminates the process with id `pid` in a platform specific + /// way, and the `signal` parameter is ignored. + /// + /// Returns `true` if the signal is successfully delivered to the + /// process. Otherwise the signal could not be sent, usually meaning + /// that the process is already dead. + bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]); +} diff --git a/packages/process/lib/src/interface/process_wrapper.dart b/packages/process/lib/src/interface/process_wrapper.dart new file mode 100644 index 00000000000..8a58cf3e344 --- /dev/null +++ b/packages/process/lib/src/interface/process_wrapper.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; +import 'dart:io' as io; + +/// A wrapper around an [io.Process] class that adds some convenience methods. +class ProcessWrapper implements io.Process { + /// Constructs a [ProcessWrapper] object that delegates to the specified + /// underlying object. + ProcessWrapper(this._delegate) + : _stdout = StreamController>(), + _stderr = StreamController>(), + _stdoutDone = Completer(), + _stderrDone = Completer() { + _monitorStdioStream(_delegate.stdout, _stdout, _stdoutDone); + _monitorStdioStream(_delegate.stderr, _stderr, _stderrDone); + } + + final io.Process _delegate; + final StreamController> _stdout; + final StreamController> _stderr; + final Completer _stdoutDone; + final Completer _stderrDone; + + /// Listens to the specified [stream], repeating events on it via + /// [controller], and completing [completer] once the stream is done. + void _monitorStdioStream( + Stream> stream, + StreamController> controller, + Completer completer, + ) { + stream.listen( + controller.add, + onError: controller.addError, + onDone: () { + controller.close(); + completer.complete(); + }, + ); + } + + @override + Future get exitCode => _delegate.exitCode; + + /// A [Future] that completes when the process has exited and its standard + /// output and error streams have closed. + /// + /// This exists as an alternative to [exitCode], which does not guarantee + /// that the stdio streams have closed (it is possible for the exit code to + /// be available before stdout and stderr have closed). + /// + /// The future returned here will complete with the exit code of the process. + Future get done async { + late int result; + await Future.wait(>[ + _stdoutDone.future, + _stderrDone.future, + _delegate.exitCode.then((int value) { + result = value; + }), + ]); + return result; + } + + @override + bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) { + return _delegate.kill(signal); + } + + @override + int get pid => _delegate.pid; + + @override + io.IOSink get stdin => _delegate.stdin; + + @override + Stream> get stdout => _stdout.stream; + + @override + Stream> get stderr => _stderr.stream; +} diff --git a/packages/process/pubspec.yaml b/packages/process/pubspec.yaml new file mode 100644 index 00000000000..1feed658332 --- /dev/null +++ b/packages/process/pubspec.yaml @@ -0,0 +1,16 @@ +name: process +version: 5.0.0 +description: A pluggable, mockable process invocation abstraction for Dart. +homepage: https://github.com/google/process.dart + +environment: + sdk: '>=2.14.0 <4.0.0' + +dependencies: + file: '>=6.0.0 <8.0.0' + path: ^1.8.0 + platform: '^3.0.0' + +dev_dependencies: + lints: '>=1.0.1 <3.0.0' + test: ^1.16.8 diff --git a/packages/process/test/src/interface/common_test.dart b/packages/process/test/src/interface/common_test.dart new file mode 100644 index 00000000000..4f53269d082 --- /dev/null +++ b/packages/process/test/src/interface/common_test.dart @@ -0,0 +1,577 @@ +// Copyright (c) 2017, 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. + +import 'dart:io' as io; +import 'package:file/local.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; +import 'package:process/src/interface/common.dart'; +import 'package:test/test.dart'; + +void main() { + group('getExecutablePath', () { + late FileSystem fs; + late Directory workingDir, dir1, dir2, dir3; + + void initialize(FileSystemStyle style) { + setUp(() { + fs = MemoryFileSystem(style: style); + workingDir = fs.systemTempDirectory.createTempSync('work_dir_'); + dir1 = fs.systemTempDirectory.createTempSync('dir1_'); + dir2 = fs.systemTempDirectory.createTempSync('dir2_'); + dir3 = fs.systemTempDirectory.createTempSync('dir3_'); + }); + } + + tearDown(() { + for (var directory in [workingDir, dir1, dir2, dir3]) { + directory.deleteSync(recursive: true); + } + }); + + group('on windows', () { + late Platform platform; + + initialize(FileSystemStyle.windows); + + setUp(() { + platform = FakePlatform( + operatingSystem: 'windows', + environment: { + 'PATH': '${dir1.path};${dir2.path}', + 'PATHEXT': '.exe;.bat' + }, + ); + }); + + test('absolute', () { + String command = fs.path.join(dir3.path, 'bla.exe'); + String expectedPath = command; + fs.file(command).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + + command = fs.path.withoutExtension(command); + executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('in path', () { + String command = 'bla.exe'; + String expectedPath = fs.path.join(dir2.path, command); + fs.file(expectedPath).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + + command = fs.path.withoutExtension(command); + executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('in path multiple times', () { + String command = 'bla.exe'; + String expectedPath = fs.path.join(dir1.path, command); + String wrongPath = fs.path.join(dir2.path, command); + fs.file(expectedPath).createSync(); + fs.file(wrongPath).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + + command = fs.path.withoutExtension(command); + executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('in subdir of work dir', () { + String command = fs.path.join('.', 'foo', 'bla.exe'); + String expectedPath = fs.path.join(workingDir.path, command); + fs.file(expectedPath).createSync(recursive: true); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + + command = fs.path.withoutExtension(command); + executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('in work dir', () { + String command = fs.path.join('.', 'bla.exe'); + String expectedPath = fs.path.join(workingDir.path, command); + String wrongPath = fs.path.join(dir2.path, command); + fs.file(expectedPath).createSync(); + fs.file(wrongPath).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + + command = fs.path.withoutExtension(command); + executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('with multiple extensions', () { + String command = 'foo'; + String expectedPath = fs.path.join(dir1.path, '$command.exe'); + String wrongPath1 = fs.path.join(dir1.path, '$command.bat'); + String wrongPath2 = fs.path.join(dir2.path, '$command.exe'); + fs.file(expectedPath).createSync(); + fs.file(wrongPath1).createSync(); + fs.file(wrongPath2).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('not found', () { + String command = 'foo.exe'; + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + expect(executablePath, isNull); + }); + + test('not found with throwOnFailure throws exception with match state', + () { + String command = 'foo.exe'; + io.ProcessException error; + try { + getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ); + fail('Expected to throw'); + } on io.ProcessException catch (err) { + error = err; + } + + expect(error, isA()); + ProcessPackageExecutableNotFoundException notFoundException = + error as ProcessPackageExecutableNotFoundException; + expect(notFoundException.candidates, isEmpty); + expect(notFoundException.workingDirectory, equals(workingDir.path)); + expect( + error.toString(), + contains(' Working Directory: C:\\.tmp_rand0\\work_dir_rand0\n' + ' Search Path:\n' + ' C:\\.tmp_rand0\\dir1_rand0\n' + ' C:\\.tmp_rand0\\dir2_rand0\n')); + }); + + test('when path has spaces', () { + expect( + sanitizeExecutablePath(r'Program Files\bla.exe', + platform: platform), + r'"Program Files\bla.exe"'); + expect( + sanitizeExecutablePath(r'ProgramFiles\bla.exe', platform: platform), + r'ProgramFiles\bla.exe'); + expect( + sanitizeExecutablePath(r'"Program Files\bla.exe"', + platform: platform), + r'"Program Files\bla.exe"'); + expect( + sanitizeExecutablePath(r'"Program Files\bla.exe"', + platform: platform), + r'"Program Files\bla.exe"'); + expect( + sanitizeExecutablePath(r'C:\"Program Files"\bla.exe', + platform: platform), + r'C:\"Program Files"\bla.exe'); + }); + + test('with absolute path when currentDirectory getter throws', () { + FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); + String command = fs.path.join(dir3.path, 'bla.exe'); + String expectedPath = command; + fs.file(command).createSync(); + + String? executablePath = getExecutablePath( + command, + null, + platform: platform, + fs: fsNoCwd, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('with relative path when currentDirectory getter throws', () { + FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); + String command = fs.path.join('.', 'bla.exe'); + + String? executablePath = getExecutablePath( + command, + null, + platform: platform, + fs: fsNoCwd, + ); + expect(executablePath, isNull); + }); + }); + + group('on Linux', () { + late Platform platform; + + initialize(FileSystemStyle.posix); + + setUp(() { + platform = FakePlatform( + operatingSystem: 'linux', + environment: {'PATH': '${dir1.path}:${dir2.path}'}); + }); + + test('absolute', () { + String command = fs.path.join(dir3.path, 'bla'); + String expectedPath = command; + String wrongPath = fs.path.join(dir3.path, 'bla.bat'); + fs.file(command).createSync(); + fs.file(wrongPath).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('in path multiple times', () { + String command = 'xxx'; + String expectedPath = fs.path.join(dir1.path, command); + String wrongPath = fs.path.join(dir2.path, command); + fs.file(expectedPath).createSync(); + fs.file(wrongPath).createSync(); + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + _expectSamePath(executablePath, expectedPath); + }); + + test('not found', () { + String command = 'foo'; + + String? executablePath = getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + ); + expect(executablePath, isNull); + }); + + test('not found with throwOnFailure throws exception with match state', + () { + String command = 'foo'; + io.ProcessException error; + try { + getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ); + fail('Expected to throw'); + } on io.ProcessException catch (err) { + error = err; + } + + expect(error, isA()); + ProcessPackageExecutableNotFoundException notFoundException = + error as ProcessPackageExecutableNotFoundException; + expect(notFoundException.candidates, isEmpty); + expect(notFoundException.workingDirectory, equals(workingDir.path)); + expect( + error.toString(), + contains(' Working Directory: /.tmp_rand0/work_dir_rand0\n' + ' Search Path:\n' + ' /.tmp_rand0/dir1_rand0\n' + ' /.tmp_rand0/dir2_rand0\n')); + }); + + test('when path has spaces', () { + expect( + sanitizeExecutablePath('/usr/local/bin/foo bar', + platform: platform), + '/usr/local/bin/foo bar'); + }); + }); + }); + group('Real Filesystem', () { + // These tests don't use the memory filesystem because Dart can't modify file + // executable permissions, so we have to create them with actual commands. + + late Platform platform; + late Directory tmpDir; + late Directory pathDir1; + late Directory pathDir2; + late Directory pathDir3; + late Directory pathDir4; + late Directory pathDir5; + late File command1; + late File command2; + late File command3; + late File command4; + late File command5; + const Platform localPlatform = LocalPlatform(); + late FileSystem fs; + + setUp(() { + fs = LocalFileSystem(); + tmpDir = fs.systemTempDirectory.createTempSync(); + pathDir1 = tmpDir.childDirectory('path1')..createSync(); + pathDir2 = tmpDir.childDirectory('path2')..createSync(); + pathDir3 = tmpDir.childDirectory('path3')..createSync(); + pathDir4 = tmpDir.childDirectory('path4')..createSync(); + pathDir5 = tmpDir.childDirectory('path5')..createSync(); + command1 = pathDir1.childFile('command')..createSync(); + command2 = pathDir2.childFile('command')..createSync(); + command3 = pathDir3.childFile('command')..createSync(); + command4 = pathDir4.childFile('command')..createSync(); + command5 = pathDir5.childFile('command')..createSync(); + platform = FakePlatform( + operatingSystem: localPlatform.operatingSystem, + environment: { + 'PATH': [ + pathDir1, + pathDir2, + pathDir3, + pathDir4, + pathDir5, + ].map((Directory dir) => dir.absolute.path).join(':'), + }, + ); + }); + + tearDown(() { + tmpDir.deleteSync(recursive: true); + }); + + test('Only returns executables in PATH', () { + if (localPlatform.isWindows) { + // Windows doesn't check for executable-ness, and we can't run 'chmod' + // on Windows anyhow. + return; + } + + // Make the second command in the path executable, but not the first. + // No executable permissions + io.Process.runSync("chmod", ["0644", "--", command1.path]); + // Only group executable permissions + io.Process.runSync("chmod", ["0645", "--", command2.path]); + // Only other executable permissions + io.Process.runSync("chmod", ["0654", "--", command3.path]); + // All executable permissions, but not readable + io.Process.runSync("chmod", ["0311", "--", command4.path]); + // All executable permissions + io.Process.runSync("chmod", ["0755", "--", command5.path]); + + String? executablePath = getExecutablePath( + 'command', + tmpDir.path, + platform: platform, + fs: fs, + ); + + // Make sure that the path returned is for the last command, since that + // one comes last in the PATH, but is the only one executable by the + // user. + _expectSamePath(executablePath, command5.absolute.path); + }); + + test( + 'Test that finding non-executable paths throws with proper information', + () { + if (localPlatform.isWindows) { + // Windows doesn't check for executable-ness, and we can't run 'chmod' + // on Windows anyhow. + return; + } + + // Make the second command in the path executable, but not the first. + // No executable permissions + io.Process.runSync("chmod", ["0644", "--", command1.path]); + // Only group executable permissions + io.Process.runSync("chmod", ["0645", "--", command2.path]); + // Only other executable permissions + io.Process.runSync("chmod", ["0654", "--", command3.path]); + // All executable permissions, but not readable + io.Process.runSync("chmod", ["0311", "--", command4.path]); + + io.ProcessException error; + try { + getExecutablePath( + 'command', + tmpDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ); + fail('Expected to throw'); + } on io.ProcessException catch (err) { + error = err; + } + + expect(error, isA()); + ProcessPackageExecutableNotFoundException notFoundException = + error as ProcessPackageExecutableNotFoundException; + expect( + notFoundException.candidates, + equals([ + '${tmpDir.path}/path1/command', + '${tmpDir.path}/path2/command', + '${tmpDir.path}/path3/command', + '${tmpDir.path}/path4/command', + '${tmpDir.path}/path5/command', + ])); + expect( + error.toString(), + equals( + 'ProcessPackageExecutableNotFoundException: Found candidates, but lacked sufficient permissions to execute "command".\n' + ' Command: command\n' + ' Working Directory: ${tmpDir.path}\n' + ' Candidates:\n' + ' ${tmpDir.path}/path1/command\n' + ' ${tmpDir.path}/path2/command\n' + ' ${tmpDir.path}/path3/command\n' + ' ${tmpDir.path}/path4/command\n' + ' ${tmpDir.path}/path5/command\n' + ' Search Path:\n' + ' ${tmpDir.path}/path1\n' + ' ${tmpDir.path}/path2\n' + ' ${tmpDir.path}/path3\n' + ' ${tmpDir.path}/path4\n' + ' ${tmpDir.path}/path5\n', + ), + ); + }); + + test('Test that finding no executable paths throws with proper information', + () { + if (localPlatform.isWindows) { + // Windows doesn't check for executable-ness, and we can't run 'chmod' + // on Windows anyhow. + return; + } + + io.ProcessException error; + try { + getExecutablePath( + 'non-existent-command', + tmpDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ); + fail('Expected to throw'); + } on io.ProcessException catch (err) { + error = err; + } + + expect(error, isA()); + ProcessPackageExecutableNotFoundException notFoundException = + error as ProcessPackageExecutableNotFoundException; + expect(notFoundException.candidates, isEmpty); + expect( + error.toString(), + equals( + 'ProcessPackageExecutableNotFoundException: Failed to find "non-existent-command" in the search path.\n' + ' Command: non-existent-command\n' + ' Working Directory: ${tmpDir.path}\n' + ' Search Path:\n' + ' ${tmpDir.path}/path1\n' + ' ${tmpDir.path}/path2\n' + ' ${tmpDir.path}/path3\n' + ' ${tmpDir.path}/path4\n' + ' ${tmpDir.path}/path5\n'), + ); + }); + }); +} + +void _expectSamePath(String? actual, String? expected) { + expect(actual, isNotNull); + expect(actual!.toLowerCase(), expected!.toLowerCase()); +} + +class MemoryFileSystemNoCwd extends ForwardingFileSystem { + MemoryFileSystemNoCwd(FileSystem delegate) : super(delegate); + + @override + Directory get currentDirectory { + throw FileSystemException('Access denied'); + } +} diff --git a/packages/process/test/src/interface/process_wrapper_test.dart b/packages/process/test/src/interface/process_wrapper_test.dart new file mode 100644 index 00000000000..25a3f3b773f --- /dev/null +++ b/packages/process/test/src/interface/process_wrapper_test.dart @@ -0,0 +1,115 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:process/process.dart'; +import 'package:test/test.dart'; + +void main() { + group('ProcessWrapper', () { + late TestProcess delegate; + late ProcessWrapper process; + + setUp(() { + delegate = TestProcess(); + process = ProcessWrapper(delegate); + }); + + group('done', () { + late bool done; + + setUp(() { + done = false; + // ignore: unawaited_futures + process.done.then((int result) { + done = true; + }); + }); + + test('completes only when all done', () async { + expect(done, isFalse); + delegate.exitCodeCompleter.complete(0); + await Future.value(); + expect(done, isFalse); + await delegate.stdoutController.close(); + await Future.value(); + expect(done, isFalse); + await delegate.stderrController.close(); + await Future.value(); + expect(done, isTrue); + expect(await process.exitCode, 0); + }); + + test('works in conjunction with subscribers to stdio streams', () async { + process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(print); + delegate.exitCodeCompleter.complete(0); + await delegate.stdoutController.close(); + await delegate.stderrController.close(); + await Future.value(); + expect(done, isTrue); + }); + }); + + group('stdio', () { + test('streams properly close', () async { + Future testStream( + Stream> stream, + StreamController> controller, + String name, + ) async { + bool closed = false; + stream.listen( + (_) {}, + onDone: () { + closed = true; + }, + ); + await controller.close(); + await Future.value(); + expect(closed, isTrue, reason: 'for $name'); + } + + await testStream(process.stdout, delegate.stdoutController, 'stdout'); + await testStream(process.stderr, delegate.stderrController, 'stderr'); + }); + }); + }); +} + +class TestProcess implements io.Process { + TestProcess([this.pid = 123]) + : exitCodeCompleter = Completer(), + stdoutController = StreamController>(), + stderrController = StreamController>(); + + @override + final int pid; + final Completer exitCodeCompleter; + final StreamController> stdoutController; + final StreamController> stderrController; + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) { + exitCodeCompleter.complete(-1); + return true; + } + + @override + Stream> get stderr => stderrController.stream; + + @override + io.IOSink get stdin => throw UnsupportedError('Not supported'); + + @override + Stream> get stdout => stdoutController.stream; +} diff --git a/packages/process/test/utils.dart b/packages/process/test/utils.dart new file mode 100644 index 00000000000..9685cb19fdf --- /dev/null +++ b/packages/process/test/utils.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2017, 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. + +import 'dart:convert'; + +/// Decodes a UTF8-encoded byte array into a list of Strings, where each list +/// entry represents a line of text. +List decode(List data) => + const LineSplitter().convert(utf8.decode(data)); + +/// Consumes and returns an entire stream of bytes. +Future> consume(Stream> stream) => + stream.expand((List data) => data).toList(); From 52edc12b20ac5a83b732c9d0cb011506dfd7abd5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 12:50:46 -0400 Subject: [PATCH 02/11] Remove CI and repo meta-docs --- .../process/.github/workflows/process.yml | 38 ------------------- packages/process/CONTRIBUTING.md | 24 ------------ packages/process/README.md | 3 -- 3 files changed, 65 deletions(-) delete mode 100644 packages/process/.github/workflows/process.yml delete mode 100644 packages/process/CONTRIBUTING.md diff --git a/packages/process/.github/workflows/process.yml b/packages/process/.github/workflows/process.yml deleted file mode 100644 index dfcf6cdf389..00000000000 --- a/packages/process/.github/workflows/process.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Process Package - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - workflow_dispatch: - -jobs: - correctness: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 - with: - sdk: dev - - name: Install dependencies - run: dart pub upgrade - - name: Verify formatting - run: dart format --output=none --set-exit-if-changed . - - name: Analyze project source - run: dart analyze --fatal-infos - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - sdk: [stable, beta, dev] - steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 - with: - sdk: ${{ matrix.sdk }} - - name: Install dependencies - run: dart pub upgrade - - name: Run Tests - run: dart test diff --git a/packages/process/CONTRIBUTING.md b/packages/process/CONTRIBUTING.md deleted file mode 100644 index 0786fdf4346..00000000000 --- a/packages/process/CONTRIBUTING.md +++ /dev/null @@ -1,24 +0,0 @@ -# How to contribute - -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. - -## Contributor License Agreement - -Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution, -this simply gives us permission to use and redistribute your contributions as -part of the project. Head over to to see -your current agreements on file or to sign a new one. - -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. - -## Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult [GitHub Help] for more -information on using pull requests. - -[GitHub Help]: https://help.github.com/articles/about-pull-requests/ diff --git a/packages/process/README.md b/packages/process/README.md index de445e0b990..78e7c8c3fe0 100644 --- a/packages/process/README.md +++ b/packages/process/README.md @@ -1,8 +1,5 @@ # Process -[![Build Status -](https://travis-ci.org/google/process.dart.svg?branch=master)](https://travis-ci.org/google/process.dart) -[![Coverage Status -](https://coveralls.io/repos/github/google/process.dart/badge.svg?branch=master)](https://coveralls.io/github/google/process.dart?branch=master) - A generic process invocation abstraction for Dart. Like `dart:io`, `package:process` supplies a rich, Dart-idiomatic API for From 923b6672aee2a6112c41e864f8fe3f823f766e4b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 12:54:39 -0400 Subject: [PATCH 03/11] Update license format to match repo conventions --- packages/process/LICENSE | 29 +++++++++---------- packages/process/lib/process.dart | 6 ++-- .../process/lib/src/interface/common.dart | 6 ++-- .../process/lib/src/interface/exceptions.dart | 6 ++-- .../src/interface/local_process_manager.dart | 6 ++-- .../lib/src/interface/process_manager.dart | 6 ++-- .../lib/src/interface/process_wrapper.dart | 6 ++-- .../test/src/interface/common_test.dart | 6 ++-- .../src/interface/process_wrapper_test.dart | 6 ++-- packages/process/test/utils.dart | 6 ++-- 10 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/process/LICENSE b/packages/process/LICENSE index 389ce985634..c6823b81eb8 100644 --- a/packages/process/LICENSE +++ b/packages/process/LICENSE @@ -1,7 +1,7 @@ -Copyright 2017, the Dart project authors. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. @@ -13,14 +13,13 @@ met: contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/process/lib/process.dart b/packages/process/lib/process.dart index af513a0bd78..7ebedc7a812 100644 --- a/packages/process/lib/process.dart +++ b/packages/process/lib/process.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. export 'src/interface/exceptions.dart'; export 'src/interface/local_process_manager.dart'; diff --git a/packages/process/lib/src/interface/common.dart b/packages/process/lib/src/interface/common.dart index 25d52f5e04a..f8e1a29d499 100644 --- a/packages/process/lib/src/interface/common.dart +++ b/packages/process/lib/src/interface/common.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'package:file/file.dart'; import 'package:file/local.dart'; diff --git a/packages/process/lib/src/interface/exceptions.dart b/packages/process/lib/src/interface/exceptions.dart index ee407ca514c..76dc4329758 100644 --- a/packages/process/lib/src/interface/exceptions.dart +++ b/packages/process/lib/src/interface/exceptions.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:io' show ProcessException; diff --git a/packages/process/lib/src/interface/local_process_manager.dart b/packages/process/lib/src/interface/local_process_manager.dart index fc260b22d22..4d3a1495e2c 100644 --- a/packages/process/lib/src/interface/local_process_manager.dart +++ b/packages/process/lib/src/interface/local_process_manager.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:convert'; import 'dart:io' diff --git a/packages/process/lib/src/interface/process_manager.dart b/packages/process/lib/src/interface/process_manager.dart index f799dd9843a..ba1e2a1099c 100644 --- a/packages/process/lib/src/interface/process_manager.dart +++ b/packages/process/lib/src/interface/process_manager.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:convert'; import 'dart:io' diff --git a/packages/process/lib/src/interface/process_wrapper.dart b/packages/process/lib/src/interface/process_wrapper.dart index 8a58cf3e344..3b3ab3c667f 100644 --- a/packages/process/lib/src/interface/process_wrapper.dart +++ b/packages/process/lib/src/interface/process_wrapper.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:async'; import 'dart:io' as io; diff --git a/packages/process/test/src/interface/common_test.dart b/packages/process/test/src/interface/common_test.dart index 4f53269d082..6190a4a19e2 100644 --- a/packages/process/test/src/interface/common_test.dart +++ b/packages/process/test/src/interface/common_test.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:io' as io; import 'package:file/local.dart'; diff --git a/packages/process/test/src/interface/process_wrapper_test.dart b/packages/process/test/src/interface/process_wrapper_test.dart index 25a3f3b773f..a915aa09edd 100644 --- a/packages/process/test/src/interface/process_wrapper_test.dart +++ b/packages/process/test/src/interface/process_wrapper_test.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:async'; import 'dart:convert'; diff --git a/packages/process/test/utils.dart b/packages/process/test/utils.dart index 9685cb19fdf..678a8b3bca7 100644 --- a/packages/process/test/utils.dart +++ b/packages/process/test/utils.dart @@ -1,6 +1,6 @@ -// Copyright (c) 2017, 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. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:convert'; From 629503c09017c3df29cf28f624aa6a0c51da6415 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:01:47 -0400 Subject: [PATCH 04/11] Update pubspec for repo conventions --- packages/process/pubspec.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/process/pubspec.yaml b/packages/process/pubspec.yaml index 1feed658332..b204308026c 100644 --- a/packages/process/pubspec.yaml +++ b/packages/process/pubspec.yaml @@ -1,10 +1,11 @@ name: process -version: 5.0.0 description: A pluggable, mockable process invocation abstraction for Dart. -homepage: https://github.com/google/process.dart +repository: https://github.com/flutter/packages/tree/main/packages/process +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+process%22 +version: 5.0.0 environment: - sdk: '>=2.14.0 <4.0.0' + sdk: ">=2.19.0 <4.0.0" dependencies: file: '>=6.0.0 <8.0.0' @@ -12,5 +13,7 @@ dependencies: platform: '^3.0.0' dev_dependencies: - lints: '>=1.0.1 <3.0.0' test: ^1.16.8 + +topics: + - process From 97f4fe61cd911cd6f689b5463d53cf8575f8e8f9 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:02:34 -0400 Subject: [PATCH 05/11] Set tests to non-web-only --- packages/process/dart_test.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/process/dart_test.yaml diff --git a/packages/process/dart_test.yaml b/packages/process/dart_test.yaml new file mode 100644 index 00000000000..91ec220b8e2 --- /dev/null +++ b/packages/process/dart_test.yaml @@ -0,0 +1 @@ +test_on: vm From ce7730ac42d3a6b646f53b6ac17a2cc671951822 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:07:27 -0400 Subject: [PATCH 06/11] Repo metadata --- CODEOWNERS | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 68508835d07..cf4f8ffee30 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -33,6 +33,7 @@ packages/pigeon/** @tarrinneal packages/platform/** @stuartmorgan packages/plugin_platform_interface/** @stuartmorgan packages/pointer_interceptor/** @ditman +packages/process/** @stuartmorgan packages/quick_actions/** @bparrishMines packages/rfw/** @Hixie packages/shared_preferences/** @tarrinneal diff --git a/README.md b/README.md index 9f1296a90f8..4a4c03d0696 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ These are the packages hosted in this repository: | [platform](./packages/platform/) | [![pub package](https://img.shields.io/pub/v/platform.svg)](https://pub.dev/packages/platform) | [![pub points](https://img.shields.io/pub/points/platform)](https://pub.dev/packages/platform/score) | [![popularity](https://img.shields.io/pub/popularity/platform)](https://pub.dev/packages/platform/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20platform?label=)](https://github.com/flutter/flutter/labels/p%3A%20platform) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20platform?label=)](https://github.com/flutter/packages/labels/p%3A%20platform) | | [pointer\_interceptor](./packages/pointer_interceptor/) | [![pub package](https://img.shields.io/pub/v/pointer_interceptor.svg)](https://pub.dev/packages/pointer_interceptor) | [![pub points](https://img.shields.io/pub/points/pointer_interceptor)](https://pub.dev/packages/pointer_interceptor/score) | [![popularity](https://img.shields.io/pub/popularity/pointer_interceptor)](https://pub.dev/packages/pointer_interceptor/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20pointer_interceptor?label=)](https://github.com/flutter/flutter/labels/p%3A%20pointer_interceptor) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20pointer_interceptor?label=)](https://github.com/flutter/packages/labels/p%3A%20pointer_interceptor) | | [plugin\_platform\_interface](./packages/plugin_platform_interface/) | [![pub package](https://img.shields.io/pub/v/plugin_platform_interface.svg)](https://pub.dev/packages/plugin_platform_interface) | [![pub points](https://img.shields.io/pub/points/plugin_platform_interface)](https://pub.dev/packages/plugin_platform_interface/score) | [![popularity](https://img.shields.io/pub/popularity/plugin_platform_interface)](https://pub.dev/packages/plugin_platform_interface/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20plugin_platform_interface?label=)](https://github.com/flutter/flutter/labels/p%3A%20plugin_platform_interface) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20plugin_platform_interface?label=)](https://github.com/flutter/packages/labels/p%3A%20plugin_platform_interface) | +| [process](./packages/process/) | [![pub package](https://img.shields.io/pub/v/process.svg)](https://pub.dev/packages/process) | [![pub points](https://img.shields.io/pub/points/process)](https://pub.dev/packages/process/score) | [![popularity](https://img.shields.io/pub/popularity/process)](https://pub.dev/packages/process/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20process?label=)](https://github.com/flutter/flutter/labels/p%3A%20process) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20process?label=)](https://github.com/flutter/packages/labels/p%3A%20process) | | [quick\_actions](./packages/quick_actions/) | [![pub package](https://img.shields.io/pub/v/quick_actions.svg)](https://pub.dev/packages/quick_actions) | [![pub points](https://img.shields.io/pub/points/quick_actions)](https://pub.dev/packages/quick_actions/score) | [![popularity](https://img.shields.io/pub/popularity/quick_actions)](https://pub.dev/packages/quick_actions/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20quick_actions?label=)](https://github.com/flutter/flutter/labels/p%3A%20quick_actions) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20quick_actions?label=)](https://github.com/flutter/packages/labels/p%3A%20quick_actions) | | [rfw](./packages/rfw/) | [![pub package](https://img.shields.io/pub/v/rfw.svg)](https://pub.dev/packages/rfw) | [![pub points](https://img.shields.io/pub/points/rfw)](https://pub.dev/packages/rfw/score) | [![popularity](https://img.shields.io/pub/popularity/rfw)](https://pub.dev/packages/rfw/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20rfw?label=)](https://github.com/flutter/flutter/labels/p%3A%20rfw) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20rfw?label=)](https://github.com/flutter/packages/labels/p%3A%20rfw) | | [shared\_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) | [![pub points](https://img.shields.io/pub/points/shared_preferences)](https://pub.dev/packages/shared_preferences/score) | [![popularity](https://img.shields.io/pub/popularity/shared_preferences)](https://pub.dev/packages/shared_preferences/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20shared_preferences?label=)](https://github.com/flutter/flutter/labels/p%3A%20shared_preferences) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p:%20shared_preferences?label=)](https://github.com/flutter/packages/labels/p%3A%20shared_preferences) | From d4bfcf63bb1b7222595e7ab614a8e8fefabe2fca Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:11:20 -0400 Subject: [PATCH 07/11] Remove platform and process from dependency exception list --- script/configs/allowed_unpinned_deps.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index e726f4443f8..2c2345da9ae 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -59,8 +59,6 @@ - googleapis - googleapis_auth - json_annotation -- platform -- process - quiver - sanitize_html - source_helper From 6efe9392099095fb610e92cf3fcfffc6fdd824ac Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:12:22 -0400 Subject: [PATCH 08/11] Remove analysis options --- packages/process/analysis_options.yaml | 6 - .../process/lib/src/interface/common.dart | 10 +- .../process/lib/src/interface/exceptions.dart | 23 ++-- .../src/interface/local_process_manager.dart | 4 +- .../test/src/interface/common_test.dart | 105 +++++++++--------- .../src/interface/process_wrapper_test.dart | 1 + 6 files changed, 70 insertions(+), 79 deletions(-) delete mode 100644 packages/process/analysis_options.yaml diff --git a/packages/process/analysis_options.yaml b/packages/process/analysis_options.yaml deleted file mode 100644 index 8fbd2e443ac..00000000000 --- a/packages/process/analysis_options.yaml +++ /dev/null @@ -1,6 +0,0 @@ -include: package:lints/recommended.yaml - -analyzer: - errors: - # Allow having TODOs in the code - todo: ignore diff --git a/packages/process/lib/src/interface/common.dart b/packages/process/lib/src/interface/common.dart index f8e1a29d499..aefeafeb1e3 100644 --- a/packages/process/lib/src/interface/common.dart +++ b/packages/process/lib/src/interface/common.dart @@ -66,11 +66,11 @@ String? getExecutablePath( // the cwd path. In this case, fall back on '.'. workingDirectory ??= '.'; } - Context context = Context(style: fs.path.style, current: workingDirectory); + final Context context = Context(style: fs.path.style, current: workingDirectory); // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2 // is available. - String pathSeparator = platform.isWindows ? ';' : ':'; + final String pathSeparator = platform.isWindows ? ';' : ':'; List extensions = []; if (platform.isWindows && context.extension(executable).isEmpty) { @@ -87,9 +87,9 @@ String? getExecutablePath( } candidates = _getCandidatePaths(executable, searchPath, extensions, context); final List foundCandidates = []; - for (String path in candidates) { + for (final String path in candidates) { final File candidate = fs.file(path); - FileStat stat = candidate.statSync(); + final FileStat stat = candidate.statSync(); // Only return files or links that exist. if (stat.type == FileSystemEntityType.notFound || stat.type == FileSystemEntityType.directory) { @@ -149,7 +149,7 @@ List _getCandidatePaths( List extensions, Context context, ) { - List withExtensions = extensions.isNotEmpty + final List withExtensions = extensions.isNotEmpty ? extensions.map((String ext) => '$command$ext').toList() : [command]; if (context.isAbsolute(command)) { diff --git a/packages/process/lib/src/interface/exceptions.dart b/packages/process/lib/src/interface/exceptions.dart index 76dc4329758..51bc6a986ea 100644 --- a/packages/process/lib/src/interface/exceptions.dart +++ b/packages/process/lib/src/interface/exceptions.dart @@ -28,7 +28,7 @@ class ProcessPackageException extends ProcessException { const ProcessPackageException( String executable, { List arguments = const [], - String message = "", + String message = '', int errorCode = 0, this.workingDirectory, }) : super(executable, arguments, message, errorCode); @@ -67,20 +67,14 @@ class ProcessPackageExecutableNotFoundException /// /// See [ProcessPackageException] for more information. const ProcessPackageExecutableNotFoundException( - String executable, { - List arguments = const [], - String message = "", - int errorCode = 0, - String? workingDirectory, + super.executable, { + super.arguments, + super.message, + super.errorCode, + super.workingDirectory, this.candidates = const [], this.searchPath = const [], - }) : super( - executable, - arguments: arguments, - message: message, - errorCode: errorCode, - workingDirectory: workingDirectory, - ); + }); /// The list of non-viable executable candidates found. final List candidates; @@ -90,7 +84,8 @@ class ProcessPackageExecutableNotFoundException @override String toString() { - StringBuffer buffer = StringBuffer('$runtimeType: $message\n'); + final StringBuffer buffer = + StringBuffer('ProcessPackageExecutableNotFoundException: $message\n'); // Don't add an extra space if there are no arguments. final String args = arguments.isNotEmpty ? ' ${arguments.join(' ')}' : ''; buffer.writeln(' Command: $executable$args'); diff --git a/packages/process/lib/src/interface/local_process_manager.dart b/packages/process/lib/src/interface/local_process_manager.dart index 4d3a1495e2c..82dd1fb10cf 100644 --- a/packages/process/lib/src/interface/local_process_manager.dart +++ b/packages/process/lib/src/interface/local_process_manager.dart @@ -122,7 +122,7 @@ class LocalProcessManager implements ProcessManager { @override bool canRun(covariant String executable, {String? workingDirectory}) => - getExecutablePath(executable, workingDirectory, throwOnFailure: false) != + getExecutablePath(executable, workingDirectory) != null; @override @@ -133,7 +133,7 @@ class LocalProcessManager implements ProcessManager { String _getExecutable( List command, String? workingDirectory, bool runInShell) { - String commandName = command.first.toString(); + final String commandName = command.first.toString(); if (runInShell) { return commandName; } diff --git a/packages/process/test/src/interface/common_test.dart b/packages/process/test/src/interface/common_test.dart index 6190a4a19e2..c258365f600 100644 --- a/packages/process/test/src/interface/common_test.dart +++ b/packages/process/test/src/interface/common_test.dart @@ -3,8 +3,9 @@ // found in the LICENSE file. import 'dart:io' as io; -import 'package:file/local.dart'; + import 'package:file/file.dart'; +import 'package:file/local.dart'; import 'package:file/memory.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; @@ -27,7 +28,7 @@ void main() { } tearDown(() { - for (var directory in [workingDir, dir1, dir2, dir3]) { + for (final Directory directory in [workingDir, dir1, dir2, dir3]) { directory.deleteSync(recursive: true); } }); @@ -49,7 +50,7 @@ void main() { test('absolute', () { String command = fs.path.join(dir3.path, 'bla.exe'); - String expectedPath = command; + final String expectedPath = command; fs.file(command).createSync(); String? executablePath = getExecutablePath( @@ -72,7 +73,7 @@ void main() { test('in path', () { String command = 'bla.exe'; - String expectedPath = fs.path.join(dir2.path, command); + final String expectedPath = fs.path.join(dir2.path, command); fs.file(expectedPath).createSync(); String? executablePath = getExecutablePath( @@ -95,8 +96,8 @@ void main() { test('in path multiple times', () { String command = 'bla.exe'; - String expectedPath = fs.path.join(dir1.path, command); - String wrongPath = fs.path.join(dir2.path, command); + final String expectedPath = fs.path.join(dir1.path, command); + final String wrongPath = fs.path.join(dir2.path, command); fs.file(expectedPath).createSync(); fs.file(wrongPath).createSync(); @@ -120,7 +121,7 @@ void main() { test('in subdir of work dir', () { String command = fs.path.join('.', 'foo', 'bla.exe'); - String expectedPath = fs.path.join(workingDir.path, command); + final String expectedPath = fs.path.join(workingDir.path, command); fs.file(expectedPath).createSync(recursive: true); String? executablePath = getExecutablePath( @@ -143,8 +144,8 @@ void main() { test('in work dir', () { String command = fs.path.join('.', 'bla.exe'); - String expectedPath = fs.path.join(workingDir.path, command); - String wrongPath = fs.path.join(dir2.path, command); + final String expectedPath = fs.path.join(workingDir.path, command); + final String wrongPath = fs.path.join(dir2.path, command); fs.file(expectedPath).createSync(); fs.file(wrongPath).createSync(); @@ -167,15 +168,15 @@ void main() { }); test('with multiple extensions', () { - String command = 'foo'; - String expectedPath = fs.path.join(dir1.path, '$command.exe'); - String wrongPath1 = fs.path.join(dir1.path, '$command.bat'); - String wrongPath2 = fs.path.join(dir2.path, '$command.exe'); + const String command = 'foo'; + final String expectedPath = fs.path.join(dir1.path, '$command.exe'); + final String wrongPath1 = fs.path.join(dir1.path, '$command.bat'); + final String wrongPath2 = fs.path.join(dir2.path, '$command.exe'); fs.file(expectedPath).createSync(); fs.file(wrongPath1).createSync(); fs.file(wrongPath2).createSync(); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, workingDir.path, platform: platform, @@ -185,9 +186,9 @@ void main() { }); test('not found', () { - String command = 'foo.exe'; + const String command = 'foo.exe'; - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, workingDir.path, platform: platform, @@ -198,7 +199,7 @@ void main() { test('not found with throwOnFailure throws exception with match state', () { - String command = 'foo.exe'; + const String command = 'foo.exe'; io.ProcessException error; try { getExecutablePath( @@ -214,7 +215,7 @@ void main() { } expect(error, isA()); - ProcessPackageExecutableNotFoundException notFoundException = + final ProcessPackageExecutableNotFoundException notFoundException = error as ProcessPackageExecutableNotFoundException; expect(notFoundException.candidates, isEmpty); expect(notFoundException.workingDirectory, equals(workingDir.path)); @@ -249,12 +250,12 @@ void main() { }); test('with absolute path when currentDirectory getter throws', () { - FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); - String command = fs.path.join(dir3.path, 'bla.exe'); - String expectedPath = command; + final FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); + final String command = fs.path.join(dir3.path, 'bla.exe'); + final String expectedPath = command; fs.file(command).createSync(); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, null, platform: platform, @@ -264,10 +265,10 @@ void main() { }); test('with relative path when currentDirectory getter throws', () { - FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); - String command = fs.path.join('.', 'bla.exe'); + final FileSystem fsNoCwd = MemoryFileSystemNoCwd(fs); + final String command = fs.path.join('.', 'bla.exe'); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, null, platform: platform, @@ -289,13 +290,13 @@ void main() { }); test('absolute', () { - String command = fs.path.join(dir3.path, 'bla'); - String expectedPath = command; - String wrongPath = fs.path.join(dir3.path, 'bla.bat'); + final String command = fs.path.join(dir3.path, 'bla'); + final String expectedPath = command; + final String wrongPath = fs.path.join(dir3.path, 'bla.bat'); fs.file(command).createSync(); fs.file(wrongPath).createSync(); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, workingDir.path, platform: platform, @@ -305,13 +306,13 @@ void main() { }); test('in path multiple times', () { - String command = 'xxx'; - String expectedPath = fs.path.join(dir1.path, command); - String wrongPath = fs.path.join(dir2.path, command); + const String command = 'xxx'; + final String expectedPath = fs.path.join(dir1.path, command); + final String wrongPath = fs.path.join(dir2.path, command); fs.file(expectedPath).createSync(); fs.file(wrongPath).createSync(); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, workingDir.path, platform: platform, @@ -321,9 +322,9 @@ void main() { }); test('not found', () { - String command = 'foo'; + const String command = 'foo'; - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( command, workingDir.path, platform: platform, @@ -334,7 +335,7 @@ void main() { test('not found with throwOnFailure throws exception with match state', () { - String command = 'foo'; + const String command = 'foo'; io.ProcessException error; try { getExecutablePath( @@ -350,7 +351,7 @@ void main() { } expect(error, isA()); - ProcessPackageExecutableNotFoundException notFoundException = + final ProcessPackageExecutableNotFoundException notFoundException = error as ProcessPackageExecutableNotFoundException; expect(notFoundException.candidates, isEmpty); expect(notFoundException.workingDirectory, equals(workingDir.path)); @@ -390,7 +391,7 @@ void main() { late FileSystem fs; setUp(() { - fs = LocalFileSystem(); + fs = const LocalFileSystem(); tmpDir = fs.systemTempDirectory.createTempSync(); pathDir1 = tmpDir.childDirectory('path1')..createSync(); pathDir2 = tmpDir.childDirectory('path2')..createSync(); @@ -429,17 +430,17 @@ void main() { // Make the second command in the path executable, but not the first. // No executable permissions - io.Process.runSync("chmod", ["0644", "--", command1.path]); + io.Process.runSync('chmod', ['0644', '--', command1.path]); // Only group executable permissions - io.Process.runSync("chmod", ["0645", "--", command2.path]); + io.Process.runSync('chmod', ['0645', '--', command2.path]); // Only other executable permissions - io.Process.runSync("chmod", ["0654", "--", command3.path]); + io.Process.runSync('chmod', ['0654', '--', command3.path]); // All executable permissions, but not readable - io.Process.runSync("chmod", ["0311", "--", command4.path]); + io.Process.runSync('chmod', ['0311', '--', command4.path]); // All executable permissions - io.Process.runSync("chmod", ["0755", "--", command5.path]); + io.Process.runSync('chmod', ['0755', '--', command5.path]); - String? executablePath = getExecutablePath( + final String? executablePath = getExecutablePath( 'command', tmpDir.path, platform: platform, @@ -463,13 +464,13 @@ void main() { // Make the second command in the path executable, but not the first. // No executable permissions - io.Process.runSync("chmod", ["0644", "--", command1.path]); + io.Process.runSync('chmod', ['0644', '--', command1.path]); // Only group executable permissions - io.Process.runSync("chmod", ["0645", "--", command2.path]); + io.Process.runSync('chmod', ['0645', '--', command2.path]); // Only other executable permissions - io.Process.runSync("chmod", ["0654", "--", command3.path]); + io.Process.runSync('chmod', ['0654', '--', command3.path]); // All executable permissions, but not readable - io.Process.runSync("chmod", ["0311", "--", command4.path]); + io.Process.runSync('chmod', ['0311', '--', command4.path]); io.ProcessException error; try { @@ -486,7 +487,7 @@ void main() { } expect(error, isA()); - ProcessPackageExecutableNotFoundException notFoundException = + final ProcessPackageExecutableNotFoundException notFoundException = error as ProcessPackageExecutableNotFoundException; expect( notFoundException.candidates, @@ -542,7 +543,7 @@ void main() { } expect(error, isA()); - ProcessPackageExecutableNotFoundException notFoundException = + final ProcessPackageExecutableNotFoundException notFoundException = error as ProcessPackageExecutableNotFoundException; expect(notFoundException.candidates, isEmpty); expect( @@ -568,10 +569,10 @@ void _expectSamePath(String? actual, String? expected) { } class MemoryFileSystemNoCwd extends ForwardingFileSystem { - MemoryFileSystemNoCwd(FileSystem delegate) : super(delegate); + MemoryFileSystemNoCwd(super.delegate); @override Directory get currentDirectory { - throw FileSystemException('Access denied'); + throw const FileSystemException('Access denied'); } } diff --git a/packages/process/test/src/interface/process_wrapper_test.dart b/packages/process/test/src/interface/process_wrapper_test.dart index a915aa09edd..3060fd81fe9 100644 --- a/packages/process/test/src/interface/process_wrapper_test.dart +++ b/packages/process/test/src/interface/process_wrapper_test.dart @@ -48,6 +48,7 @@ void main() { process.stdout .transform(utf8.decoder) .transform(const LineSplitter()) + // ignore: avoid_print .listen(print); delegate.exitCodeCompleter.complete(0); await delegate.stdoutController.close(); From 7098edbc19bbb9b082f808ff4c5b099a69ab44a0 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:17:49 -0400 Subject: [PATCH 09/11] autoformat --- packages/process/lib/src/interface/common.dart | 3 ++- .../process/lib/src/interface/local_process_manager.dart | 3 +-- packages/process/test/src/interface/common_test.dart | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/process/lib/src/interface/common.dart b/packages/process/lib/src/interface/common.dart index aefeafeb1e3..4771c21778f 100644 --- a/packages/process/lib/src/interface/common.dart +++ b/packages/process/lib/src/interface/common.dart @@ -66,7 +66,8 @@ String? getExecutablePath( // the cwd path. In this case, fall back on '.'. workingDirectory ??= '.'; } - final Context context = Context(style: fs.path.style, current: workingDirectory); + final Context context = + Context(style: fs.path.style, current: workingDirectory); // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2 // is available. diff --git a/packages/process/lib/src/interface/local_process_manager.dart b/packages/process/lib/src/interface/local_process_manager.dart index 82dd1fb10cf..9606a4a671b 100644 --- a/packages/process/lib/src/interface/local_process_manager.dart +++ b/packages/process/lib/src/interface/local_process_manager.dart @@ -122,8 +122,7 @@ class LocalProcessManager implements ProcessManager { @override bool canRun(covariant String executable, {String? workingDirectory}) => - getExecutablePath(executable, workingDirectory) != - null; + getExecutablePath(executable, workingDirectory) != null; @override bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) { diff --git a/packages/process/test/src/interface/common_test.dart b/packages/process/test/src/interface/common_test.dart index c258365f600..f2c92552fff 100644 --- a/packages/process/test/src/interface/common_test.dart +++ b/packages/process/test/src/interface/common_test.dart @@ -28,7 +28,12 @@ void main() { } tearDown(() { - for (final Directory directory in [workingDir, dir1, dir2, dir3]) { + for (final Directory directory in [ + workingDir, + dir1, + dir2, + dir3 + ]) { directory.deleteSync(recursive: true); } }); From d39df0aa65d3d18dfa5cfbc19f55cc289d57870a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:20:28 -0400 Subject: [PATCH 10/11] Version bump --- packages/process/CHANGELOG.md | 73 +++++++++++++++++++---------------- packages/process/pubspec.yaml | 2 +- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/process/CHANGELOG.md b/packages/process/CHANGELOG.md index 57232d44240..8e7df7f410a 100644 --- a/packages/process/CHANGELOG.md +++ b/packages/process/CHANGELOG.md @@ -1,130 +1,135 @@ -#### 5.0.0 +## 5.0.1 + +* Transfers the package source from https://github.com/google/process.dart to + https://github.com/flutter/packages. + +## 5.0.0 * Remove the `covariant` keyword from `stderrEncoding` and `stdoutEncoding` parameters. * Update dependencies to work on Dart 3. * Bumped min SDK dependency to nearest non-prerelease version (2.14.0) -#### 4.2.4 +## 4.2.4 * Mark `stderrEncoding` and `stdoutEncoding` parameters as nullable again, now that the upstream SDK issue has been fixed. -#### 4.2.3 +## 4.2.3 * Rollback to version 4.2.1 (https://github.com/google/process.dart/issues/64) -#### 4.2.2 +## 4.2.2 * Mark `stderrEncoding` and `stdoutEncoding` parameters as nullable. -#### 4.2.1 +## 4.2.1 * Added custom exception types `ProcessPackageException` and `ProcessPackageExecutableNotFoundException` to provide extra information from exception conditions. -#### 4.2.0 +## 4.2.0 * Fix the signature of `ProcessManager.canRun` to be consistent with `LocalProcessManager`. -#### 4.1.1 +## 4.1.1 * Fixed `getExecutablePath()` to only return path items that are executable and readable to the user. -#### 4.1.0 +## 4.1.0 * Fix the signatures of `ProcessManager.run`, `.runSync`, and `.start` to be consistent with `LocalProcessManager`'s. * Added more details to the `ArgumentError` thrown when a command cannot be resolved to an executable. -#### 4.0.0 +## 4.0.0 * First stable null safe release. -#### 4.0.0-nullsafety.4 +## 4.0.0-nullsafety.4 * Update supported SDK range. -#### 4.0.0-nullsafety.3 +## 4.0.0-nullsafety.3 * Update supported SDK range. -#### 4.0.0-nullsafety.2 +## 4.0.0-nullsafety.2 * Update supported SDK range. -#### 4.0.0-nullsafety.1 +## 4.0.0-nullsafety.1 * Migrate to null-safety. * Remove record/replay functionality. * Remove implicit casts in preparation for null-safety. * Remove dependency on `package:intl` and `package:meta`. -#### 3.0.13 +## 3.0.13 * Handle `currentDirectory` throwing an exception in `getExecutablePath()`. -#### 3.0.12 +## 3.0.12 * Updated version constraint on intl. -#### 3.0.11 +## 3.0.11 * Fix bug: don't add quotes if the file name already has quotes. -#### 3.0.10 +## 3.0.10 * Added quoted strings to indicate where the command name ends and the arguments begin otherwise, the file name is ambiguous on Windows. -#### 3.0.9 +## 3.0.9 * Fixed bug in `ProcessWrapper` -#### 3.0.8 +## 3.0.8 * Fixed bug in `ProcessWrapper` -#### 3.0.7 +## 3.0.7 * Renamed `Process` to `ProcessWrapper` -#### 3.0.6 +## 3.0.6 * Added class `Process`, a simple wrapper around dart:io's `Process` class. -#### 3.0.5 +## 3.0.5 * Fixes for missing_return analysis errors with 2.10.0-dev.1.0. -#### 3.0.4 +## 3.0.4 * Fix unit tests * Update SDK constraint to 3. -#### 3.0.3 +## 3.0.3 * Update dependency on `package:file` -#### 3.0.2 +## 3.0.2 * Remove upper case constants. * Update SDK constraint to 2.0.0-dev.54.0. * Fix tests for Dart 2. -#### 3.0.1 +## 3.0.1 * General cleanup -#### 3.0.0 +## 3.0.0 * Cleanup getExecutablePath() to better respect the platform -#### 2.0.9 +## 2.0.9 * Bumped `package:file` dependency @@ -156,25 +161,25 @@ begin otherwise, the file name is ambiguous on Windows. * relax dependency requirement for `platform` -#### 2.0.2 +## 2.0.2 * Fix a strong mode function expression return type inference bug with Dart 1.23.0-dev.10.0. -#### 2.0.1 +## 2.0.1 * Fixed bug in `ReplayProcessManager` whereby it could try to write to `stdout` or `stderr` after the streams were closed. -#### 2.0.0 +## 2.0.0 * Bumped `package:file` dependency to 2.0.1 -#### 1.1.0 +## 1.1.0 * Added support to transparently find the right executable under Windows. -#### 1.0.1 +## 1.0.1 * The `executable` and `arguments` parameters have been merged into one `command` parameter in the `run`, `runSync`, and `start` methods of @@ -183,6 +188,6 @@ begin otherwise, the file name is ambiguous on Windows. `RecordingProcessManager` and `ReplayProcessManager` via the `CommandElement` class. -#### 1.0.0 +## 1.0.0 * Initial version diff --git a/packages/process/pubspec.yaml b/packages/process/pubspec.yaml index b204308026c..899e59ce786 100644 --- a/packages/process/pubspec.yaml +++ b/packages/process/pubspec.yaml @@ -2,7 +2,7 @@ name: process description: A pluggable, mockable process invocation abstraction for Dart. repository: https://github.com/flutter/packages/tree/main/packages/process issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+process%22 -version: 5.0.0 +version: 5.0.1 environment: sdk: ">=2.19.0 <4.0.0" From 53d4f883d60cf9fe9d1b83809af89390b65b63e2 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Oct 2023 13:59:00 -0400 Subject: [PATCH 11/11] Update tests to use throwsA --- .../test/src/interface/common_test.dart | 252 +++++++++--------- 1 file changed, 129 insertions(+), 123 deletions(-) diff --git a/packages/process/test/src/interface/common_test.dart b/packages/process/test/src/interface/common_test.dart index f2c92552fff..743b7cd34b6 100644 --- a/packages/process/test/src/interface/common_test.dart +++ b/packages/process/test/src/interface/common_test.dart @@ -205,31 +205,37 @@ void main() { test('not found with throwOnFailure throws exception with match state', () { const String command = 'foo.exe'; - io.ProcessException error; - try { - getExecutablePath( - command, - workingDir.path, - platform: platform, - fs: fs, - throwOnFailure: true, - ); - fail('Expected to throw'); - } on io.ProcessException catch (err) { - error = err; - } - - expect(error, isA()); - final ProcessPackageExecutableNotFoundException notFoundException = - error as ProcessPackageExecutableNotFoundException; - expect(notFoundException.candidates, isEmpty); - expect(notFoundException.workingDirectory, equals(workingDir.path)); expect( - error.toString(), - contains(' Working Directory: C:\\.tmp_rand0\\work_dir_rand0\n' - ' Search Path:\n' - ' C:\\.tmp_rand0\\dir1_rand0\n' - ' C:\\.tmp_rand0\\dir2_rand0\n')); + () => getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ), + throwsA(isA() + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.candidates, + 'candidates', + isEmpty) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.workingDirectory, + 'workingDirectory', + equals(workingDir.path)) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.toString(), + 'toString', + contains( + ' Working Directory: C:\\.tmp_rand0\\work_dir_rand0\n' + ' Search Path:\n' + ' C:\\.tmp_rand0\\dir1_rand0\n' + ' C:\\.tmp_rand0\\dir2_rand0\n')))); }); test('when path has spaces', () { @@ -341,31 +347,36 @@ void main() { test('not found with throwOnFailure throws exception with match state', () { const String command = 'foo'; - io.ProcessException error; - try { - getExecutablePath( - command, - workingDir.path, - platform: platform, - fs: fs, - throwOnFailure: true, - ); - fail('Expected to throw'); - } on io.ProcessException catch (err) { - error = err; - } - - expect(error, isA()); - final ProcessPackageExecutableNotFoundException notFoundException = - error as ProcessPackageExecutableNotFoundException; - expect(notFoundException.candidates, isEmpty); - expect(notFoundException.workingDirectory, equals(workingDir.path)); expect( - error.toString(), - contains(' Working Directory: /.tmp_rand0/work_dir_rand0\n' - ' Search Path:\n' - ' /.tmp_rand0/dir1_rand0\n' - ' /.tmp_rand0/dir2_rand0\n')); + () => getExecutablePath( + command, + workingDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ), + throwsA(isA() + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.candidates, + 'candidates', + isEmpty) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.workingDirectory, + 'workingDirectory', + equals(workingDir.path)) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.toString(), + 'toString', + contains(' Working Directory: /.tmp_rand0/work_dir_rand0\n' + ' Search Path:\n' + ' /.tmp_rand0/dir1_rand0\n' + ' /.tmp_rand0/dir2_rand0\n')))); }); test('when path has spaces', () { @@ -477,52 +488,48 @@ void main() { // All executable permissions, but not readable io.Process.runSync('chmod', ['0311', '--', command4.path]); - io.ProcessException error; - try { - getExecutablePath( - 'command', - tmpDir.path, - platform: platform, - fs: fs, - throwOnFailure: true, - ); - fail('Expected to throw'); - } on io.ProcessException catch (err) { - error = err; - } - - expect(error, isA()); - final ProcessPackageExecutableNotFoundException notFoundException = - error as ProcessPackageExecutableNotFoundException; - expect( - notFoundException.candidates, - equals([ - '${tmpDir.path}/path1/command', - '${tmpDir.path}/path2/command', - '${tmpDir.path}/path3/command', - '${tmpDir.path}/path4/command', - '${tmpDir.path}/path5/command', - ])); expect( - error.toString(), - equals( - 'ProcessPackageExecutableNotFoundException: Found candidates, but lacked sufficient permissions to execute "command".\n' - ' Command: command\n' - ' Working Directory: ${tmpDir.path}\n' - ' Candidates:\n' - ' ${tmpDir.path}/path1/command\n' - ' ${tmpDir.path}/path2/command\n' - ' ${tmpDir.path}/path3/command\n' - ' ${tmpDir.path}/path4/command\n' - ' ${tmpDir.path}/path5/command\n' - ' Search Path:\n' - ' ${tmpDir.path}/path1\n' - ' ${tmpDir.path}/path2\n' - ' ${tmpDir.path}/path3\n' - ' ${tmpDir.path}/path4\n' - ' ${tmpDir.path}/path5\n', - ), - ); + () => getExecutablePath( + 'command', + tmpDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ), + throwsA(isA() + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.candidates, + 'candidates', + equals([ + '${tmpDir.path}/path1/command', + '${tmpDir.path}/path2/command', + '${tmpDir.path}/path3/command', + '${tmpDir.path}/path4/command', + '${tmpDir.path}/path5/command', + ])) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.toString(), + 'toString', + contains( + 'ProcessPackageExecutableNotFoundException: Found candidates, but lacked sufficient permissions to execute "command".\n' + ' Command: command\n' + ' Working Directory: ${tmpDir.path}\n' + ' Candidates:\n' + ' ${tmpDir.path}/path1/command\n' + ' ${tmpDir.path}/path2/command\n' + ' ${tmpDir.path}/path3/command\n' + ' ${tmpDir.path}/path4/command\n' + ' ${tmpDir.path}/path5/command\n' + ' Search Path:\n' + ' ${tmpDir.path}/path1\n' + ' ${tmpDir.path}/path2\n' + ' ${tmpDir.path}/path3\n' + ' ${tmpDir.path}/path4\n' + ' ${tmpDir.path}/path5\n')))); }); test('Test that finding no executable paths throws with proper information', @@ -533,37 +540,36 @@ void main() { return; } - io.ProcessException error; - try { - getExecutablePath( - 'non-existent-command', - tmpDir.path, - platform: platform, - fs: fs, - throwOnFailure: true, - ); - fail('Expected to throw'); - } on io.ProcessException catch (err) { - error = err; - } - - expect(error, isA()); - final ProcessPackageExecutableNotFoundException notFoundException = - error as ProcessPackageExecutableNotFoundException; - expect(notFoundException.candidates, isEmpty); expect( - error.toString(), - equals( - 'ProcessPackageExecutableNotFoundException: Failed to find "non-existent-command" in the search path.\n' - ' Command: non-existent-command\n' - ' Working Directory: ${tmpDir.path}\n' - ' Search Path:\n' - ' ${tmpDir.path}/path1\n' - ' ${tmpDir.path}/path2\n' - ' ${tmpDir.path}/path3\n' - ' ${tmpDir.path}/path4\n' - ' ${tmpDir.path}/path5\n'), - ); + () => getExecutablePath( + 'non-existent-command', + tmpDir.path, + platform: platform, + fs: fs, + throwOnFailure: true, + ), + throwsA(isA() + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.candidates, + 'candidates', + isEmpty) + .having( + (ProcessPackageExecutableNotFoundException + notFoundException) => + notFoundException.toString(), + 'toString', + contains( + 'ProcessPackageExecutableNotFoundException: Failed to find "non-existent-command" in the search path.\n' + ' Command: non-existent-command\n' + ' Working Directory: ${tmpDir.path}\n' + ' Search Path:\n' + ' ${tmpDir.path}/path1\n' + ' ${tmpDir.path}/path2\n' + ' ${tmpDir.path}/path3\n' + ' ${tmpDir.path}/path4\n' + ' ${tmpDir.path}/path5\n')))); }); }); }