Make LocalProcessManager portable (#3)
LocalProcessManager now uses the same algorithm on Windows and Posix systems to find the executable for a command.
Previously, manager.run(['pub']) would work on Linux, but not on Windows because 'pub.bat' could not be located.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 720d352..f16fa65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+#### 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
diff --git a/lib/src/interface/common.dart b/lib/src/interface/common.dart
new file mode 100644
index 0000000..dc7e9bb
--- /dev/null
+++ b/lib/src/interface/common.dart
@@ -0,0 +1,61 @@
+// 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 File, Directory;
+
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:platform/platform.dart';
+
+/// Searches the `PATH` for the actual executable that [commandName] is supposed
+/// to launch.
+///
+/// Return `null` if the executable cannot be found.
+@visibleForTesting
+String getExecutablePath(String commandName, String workingDirectory,
+ {Platform platform}) {
+ platform ??= new LocalPlatform();
+ workingDirectory ??= Directory.current.path;
+ // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2
+ // is available.
+ String pathSeparator = platform.isWindows ? ';' : ':';
+
+ List<String> extensions = <String>[];
+ if (platform.isWindows && p.extension(commandName).isEmpty) {
+ extensions = platform.environment['PATHEXT'].split(pathSeparator);
+ }
+
+ List<String> candidates = <String>[];
+ if (commandName.contains(p.separator)) {
+ candidates =
+ _getCandidatePaths(commandName, <String>[workingDirectory], extensions);
+ } else {
+ List<String> searchPath = platform.environment['PATH'].split(pathSeparator);
+ candidates = _getCandidatePaths(commandName, searchPath, extensions);
+ }
+ return candidates.firstWhere((String path) => new File(path).existsSync(),
+ orElse: () => null);
+}
+
+/// Returns all possible combinations of `$searchPath\$commandName.$ext` for
+/// `searchPath` in [searchPaths] and `ext` in [extensions].
+///
+/// If [extensions] is empty, it will just enumerate all
+/// `$searchPath\$commandName`.
+/// If [commandName] is an absolute path, it will just enumerate
+/// `$commandName.$ext`.
+Iterable<String> _getCandidatePaths(
+ String commandName, List<String> searchPaths, List<String> extensions) {
+ List<String> withExtensions = extensions.isNotEmpty
+ ? extensions.map((String ext) => '$commandName$ext').toList()
+ : <String>[commandName];
+ if (p.isAbsolute(commandName)) {
+ return withExtensions;
+ }
+ return searchPaths
+ .map((String path) =>
+ withExtensions.map((String command) => p.join(path, command)))
+ .expand((Iterable<String> e) => e)
+ .toList();
+}
diff --git a/lib/src/interface/local_process_manager.dart b/lib/src/interface/local_process_manager.dart
index a5509a5..9719c01 100644
--- a/lib/src/interface/local_process_manager.dart
+++ b/lib/src/interface/local_process_manager.dart
@@ -14,13 +14,9 @@
import 'package:meta/meta.dart';
+import 'common.dart';
import 'process_manager.dart';
-String _getExecutable(List<dynamic> command) => command.first.toString();
-
-List<String> _getArguments(List<dynamic> command) =>
- command.skip(1).map((dynamic element) => element.toString()).toList();
-
/// Local implementation of the `ProcessManager` interface.
///
/// This implementation delegates directly to the corresponding static methods
@@ -44,7 +40,7 @@
ProcessStartMode mode: ProcessStartMode.NORMAL,
}) {
return Process.start(
- _getExecutable(command),
+ _getExecutable(command, workingDirectory, runInShell),
_getArguments(command),
workingDirectory: workingDirectory,
environment: environment,
@@ -65,7 +61,7 @@
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
return Process.run(
- _getExecutable(command),
+ _getExecutable(command, workingDirectory, runInShell),
_getArguments(command),
workingDirectory: workingDirectory,
environment: environment,
@@ -87,7 +83,7 @@
Encoding stderrEncoding: SYSTEM_ENCODING,
}) {
return Process.runSync(
- _getExecutable(command),
+ _getExecutable(command, workingDirectory, runInShell),
_getArguments(command),
workingDirectory: workingDirectory,
environment: environment,
@@ -99,7 +95,27 @@
}
@override
+ bool canRun(@checked Object executable, {String workingDirectory}) =>
+ getExecutablePath(executable, workingDirectory) != null;
+
+ @override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.SIGTERM]) {
return Process.killPid(pid, signal);
}
}
+
+String _getExecutable(
+ List<dynamic> command, String workingDirectory, bool runInShell) {
+ String commandName = command.first.toString();
+ if (runInShell) {
+ return commandName;
+ }
+ String exe = getExecutablePath(commandName, workingDirectory);
+ if (exe == null) {
+ throw new ArgumentError('Cannot find executable for $commandName.');
+ }
+ return exe;
+}
+
+List<String> _getArguments(List<dynamic> command) =>
+ command.skip(1).map((dynamic element) => element.toString()).toList();
diff --git a/lib/src/interface/process_manager.dart b/lib/src/interface/process_manager.dart
index 9630dce..8375970 100644
--- a/lib/src/interface/process_manager.dart
+++ b/lib/src/interface/process_manager.dart
@@ -163,6 +163,9 @@
Encoding stderrEncoding: SYSTEM_ENCODING,
});
+ /// 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
diff --git a/lib/src/record_replay/can_run_manifest_entry.dart b/lib/src/record_replay/can_run_manifest_entry.dart
new file mode 100644
index 0000000..095a667
--- /dev/null
+++ b/lib/src/record_replay/can_run_manifest_entry.dart
@@ -0,0 +1,36 @@
+import 'manifest_entry.dart';
+
+/// An entry in the process invocation manifest for `canRun`.
+class CanRunManifestEntry extends ManifestEntry {
+ @override
+ final String type = 'can_run';
+
+ /// The name of the executable for which the run-ability is checked.
+ final String executable;
+
+ /// The result of the check.
+ final bool result;
+
+ /// Creates a new manifest entry with the given properties.
+ CanRunManifestEntry({this.executable, this.result});
+
+ /// Creates a new manifest entry populated with the specified JSON [data].
+ ///
+ /// If any required fields are missing from the JSON data, this will throw
+ /// a [FormatException].
+ factory CanRunManifestEntry.fromJson(Map<String, dynamic> data) {
+ checkRequiredField(data, 'executable');
+ checkRequiredField(data, 'result');
+ CanRunManifestEntry entry = new CanRunManifestEntry(
+ executable: data['executable'],
+ result: data['result'],
+ );
+ return entry;
+ }
+
+ @override
+ Map<String, dynamic> toJson() => new JsonBuilder()
+ .add('executable', executable)
+ .add('result', result)
+ .entry;
+}
diff --git a/lib/src/record_replay/manifest.dart b/lib/src/record_replay/manifest.dart
index ffac2fb..45c1177 100644
--- a/lib/src/record_replay/manifest.dart
+++ b/lib/src/record_replay/manifest.dart
@@ -5,7 +5,9 @@
import 'dart:convert';
import 'dart:io' show ProcessStartMode;
+import 'can_run_manifest_entry.dart';
import 'manifest_entry.dart';
+import 'run_manifest_entry.dart';
/// Tests if two lists contain pairwise equal elements.
bool _areListsEqual/*<T>*/(
@@ -48,7 +50,18 @@
List<Map<String, dynamic>> decoded = new JsonDecoder().convert(json);
Manifest manifest = new Manifest();
decoded.forEach((Map<String, dynamic> entry) {
- manifest._entries.add(new ManifestEntry.fromJson(entry));
+ switch (entry['type']) {
+ case 'run':
+ manifest._entries.add(new RunManifestEntry.fromJson(entry['body']));
+ break;
+ case 'can_run':
+ manifest._entries
+ .add(new CanRunManifestEntry.fromJson(entry['body']));
+ break;
+ default:
+ throw new UnsupportedError(
+ 'Manifest type ${entry['type']} is unkown.');
+ }
});
return manifest;
}
@@ -59,15 +72,16 @@
/// The number of entries currently in the manifest.
int get length => _entries.length;
- /// Gets the entry whose [ManifestEntry.pid] matches the specified [pid].
- ManifestEntry getEntry(int pid) {
- return _entries.firstWhere((ManifestEntry entry) => entry.pid == pid);
+ /// Gets the entry whose [RunManifestEntry.pid] matches the specified [pid].
+ ManifestEntry getRunEntry(int pid) {
+ return _entries.firstWhere(
+ (ManifestEntry entry) => entry is RunManifestEntry && entry.pid == pid);
}
/// Finds the first manifest entry that has not been invoked and whose
/// metadata matches the specified criteria. If no arguments are specified,
/// this will simply return the first entry that has not yet been invoked.
- ManifestEntry findPendingEntry({
+ ManifestEntry findPendingRunEntry({
List<String> command,
ProcessStartMode mode,
Encoding stdoutEncoding,
@@ -75,14 +89,28 @@
}) {
return _entries.firstWhere(
(ManifestEntry entry) {
- bool hit = !entry.invoked;
- // Ignore workingDirectory & environment, as they could
- // yield false negatives.
- hit = hit && _isHit(entry.command, command, _areListsEqual);
- hit = hit && _isHit(entry.mode, mode);
- hit = hit && _isHit(entry.stdoutEncoding, stdoutEncoding);
- hit = hit && _isHit(entry.stderrEncoding, stderrEncoding);
- return hit;
+ return entry is RunManifestEntry &&
+ !entry.invoked &&
+ _isHit(entry.command, command, _areListsEqual) &&
+ _isHit(entry.mode, mode) &&
+ _isHit(entry.stdoutEncoding, stdoutEncoding) &&
+ _isHit(entry.stderrEncoding, stderrEncoding);
+ },
+ orElse: () => null,
+ );
+ }
+
+ /// Finds the first manifest entry that has not been invoked and whose
+ /// metadata matches the specified criteria. If no arguments are specified,
+ /// this will simply return the first entry that has not yet been invoked.
+ ManifestEntry findPendingCanRunEntry({
+ String executable,
+ }) {
+ return _entries.firstWhere(
+ (ManifestEntry entry) {
+ return entry is CanRunManifestEntry &&
+ !entry.invoked &&
+ _isHit(entry.executable, executable);
},
orElse: () => null,
);
@@ -91,7 +119,10 @@
/// Returns a JSON-encoded representation of this manifest.
String toJson() {
List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
- _entries.forEach((ManifestEntry entry) => list.add(entry.toJson()));
+ _entries.forEach((ManifestEntry entry) => list.add(new JsonBuilder()
+ .add('type', entry.type)
+ .add('body', entry.toJson())
+ .entry));
return const JsonEncoder.withIndent(' ').convert(list);
}
}
diff --git a/lib/src/record_replay/manifest_entry.dart b/lib/src/record_replay/manifest_entry.dart
index db568ad..11d66f6 100644
--- a/lib/src/record_replay/manifest_entry.dart
+++ b/lib/src/record_replay/manifest_entry.dart
@@ -1,140 +1,11 @@
-// 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 ProcessStartMode, SYSTEM_ENCODING;
-
import 'manifest.dart';
import 'replay_process_manager.dart';
-/// Throws a [FormatException] if [data] does not contain [key].
-void _checkRequiredField(Map<String, dynamic> data, String key) {
- if (!data.containsKey(key))
- throw new FormatException('Required field missing: $key');
-}
-
-/// Gets a `ProcessStartMode` value by its string name.
-ProcessStartMode _getProcessStartMode(String value) {
- if (value != null) {
- for (ProcessStartMode mode in ProcessStartMode.values) {
- if (mode.toString() == value) {
- return mode;
- }
- }
- throw new FormatException('Invalid value for mode: $value');
- }
- return null;
-}
-
-/// Gets an `Encoding` instance by the encoding name.
-Encoding _getEncoding(String encoding) {
- if (encoding == 'system') {
- return SYSTEM_ENCODING;
- } else if (encoding != null) {
- return Encoding.getByName(encoding);
- }
- return null;
-}
-
/// An entry in the process invocation manifest.
///
/// Each entry in the [Manifest] represents a single recorded process
/// invocation.
-class ManifestEntry {
- /// The process id.
- final int pid;
-
- /// The base file name for this entry. `stdout` and `stderr` files for this
- /// process will be serialized in the recording directory as
- /// `$basename.stdout` and `$basename.stderr`, respectively.
- final String basename;
-
- /// The command that was run. The first element is the executable, and the
- /// remaining elements are the arguments to the executable.
- final List<String> command;
-
- /// The process' working directory when it was spawned.
- final String workingDirectory;
-
- /// The environment variables that were passed to the process.
- final Map<String, String> environment;
-
- /// Whether the invoker's environment was made available to the process.
- final bool includeParentEnvironment;
-
- /// Whether the process was spawned through a system shell.
- final bool runInShell;
-
- /// The mode with which the process was spawned.
- final ProcessStartMode mode;
-
- /// The encoding used for the `stdout` of the process.
- final Encoding stdoutEncoding;
-
- /// The encoding used for the `stderr` of the process.
- final Encoding stderrEncoding;
-
- /// The exit code of the process.
- int exitCode;
-
- /// Creates a new manifest entry with the given properties.
- ManifestEntry({
- this.pid,
- this.basename,
- this.command,
- this.workingDirectory,
- this.environment,
- this.includeParentEnvironment,
- this.runInShell,
- this.mode,
- this.stdoutEncoding,
- this.stderrEncoding,
- this.exitCode,
- });
-
- /// Creates a new manifest entry populated with the specified JSON [data].
- ///
- /// If any required fields are missing from the JSON data, this will throw
- /// a [FormatException].
- factory ManifestEntry.fromJson(Map<String, dynamic> data) {
- _checkRequiredField(data, 'pid');
- _checkRequiredField(data, 'basename');
- _checkRequiredField(data, 'command');
- ManifestEntry entry = new ManifestEntry(
- pid: data['pid'],
- basename: data['basename'],
- command: data['command'],
- workingDirectory: data['workingDirectory'],
- environment: data['environment'],
- includeParentEnvironment: data['includeParentEnvironment'],
- runInShell: data['runInShell'],
- mode: _getProcessStartMode(data['mode']),
- stdoutEncoding: _getEncoding(data['stdoutEncoding']),
- stderrEncoding: _getEncoding(data['stderrEncoding']),
- exitCode: data['exitCode'],
- );
- entry.daemon = data['daemon'];
- entry.notResponding = data['notResponding'];
- return entry;
- }
-
- /// The executable that was invoked.
- String get executable => command.first;
-
- /// The arguments that were passed to [executable].
- List<String> get arguments => command.skip(1).toList();
-
- /// Indicates that the process is a daemon.
- bool get daemon => _daemon;
- bool _daemon = false;
- set daemon(bool value) => _daemon = value ?? false;
-
- /// Indicates that the process did not respond to `SIGTERM`.
- bool get notResponding => _notResponding;
- bool _notResponding = false;
- set notResponding(bool value) => _notResponding = value ?? false;
-
+abstract class ManifestEntry {
/// Whether this entry has been "invoked" by [ReplayProcessManager].
bool get invoked => _invoked;
bool _invoked = false;
@@ -144,36 +15,32 @@
_invoked = true;
}
+ /// The type of this [ManifestEntry].
+ String get type;
+
/// Returns a JSON-encodable representation of this manifest entry.
- Map<String, dynamic> toJson() => new _JsonBuilder()
- .add('pid', pid)
- .add('basename', basename)
- .add('command', command)
- .add('workingDirectory', workingDirectory)
- .add('environment', environment)
- .add('includeParentEnvironment', includeParentEnvironment)
- .add('runInShell', runInShell)
- .add('mode', mode, () => mode.toString())
- .add('stdoutEncoding', stdoutEncoding, () => stdoutEncoding.name)
- .add('stderrEncoding', stderrEncoding, () => stderrEncoding.name)
- .add('daemon', daemon)
- .add('notResponding', notResponding)
- .add('exitCode', exitCode)
- .entry;
+ Map<String, dynamic> toJson();
}
/// A lightweight class that provides a means of building a manifest entry
/// JSON object.
-class _JsonBuilder {
+class JsonBuilder {
+ /// The JSON-encodable object.
final Map<String, dynamic> entry = <String, dynamic>{};
/// Adds the specified key/value pair to the manifest entry iff the value
/// is non-null. If [jsonValue] is specified, its value will be used instead
/// of the raw value.
- _JsonBuilder add(String name, dynamic value, [dynamic jsonValue()]) {
+ JsonBuilder add(String name, dynamic value, [dynamic jsonValue()]) {
if (value != null) {
entry[name] = jsonValue == null ? value : jsonValue();
}
return this;
}
}
+
+/// Throws a [FormatException] if [data] does not contain [key].
+void checkRequiredField(Map<String, dynamic> data, String key) {
+ if (!data.containsKey(key))
+ throw new FormatException('Required field missing: $key');
+}
diff --git a/lib/src/record_replay/recording_process_manager.dart b/lib/src/record_replay/recording_process_manager.dart
index 51ed57f..c1f337c 100644
--- a/lib/src/record_replay/recording_process_manager.dart
+++ b/lib/src/record_replay/recording_process_manager.dart
@@ -18,11 +18,12 @@
import 'package:path/path.dart' as path;
import '../interface/process_manager.dart';
+import 'can_run_manifest_entry.dart';
import 'common.dart';
import 'constants.dart';
import 'manifest.dart';
-import 'manifest_entry.dart';
import 'replay_process_manager.dart';
+import 'run_manifest_entry.dart';
/// Records process invocation activity and serializes it to disk.
///
@@ -97,7 +98,7 @@
List<String> sanitizedCommand = sanitize(command);
String basename = _getBasename(process.pid, sanitizedCommand);
- ManifestEntry entry = new ManifestEntry(
+ RunManifestEntry entry = new RunManifestEntry(
pid: process.pid,
basename: basename,
command: sanitizedCommand,
@@ -145,7 +146,7 @@
List<String> sanitizedCommand = sanitize(command);
String basename = _getBasename(result.pid, sanitizedCommand);
- _manifest.add(new ManifestEntry(
+ _manifest.add(new RunManifestEntry(
pid: result.pid,
basename: basename,
command: sanitizedCommand,
@@ -201,7 +202,7 @@
List<String> sanitizedCommand = sanitize(command);
String basename = _getBasename(result.pid, sanitizedCommand);
- _manifest.add(new ManifestEntry(
+ _manifest.add(new RunManifestEntry(
pid: result.pid,
basename: basename,
command: sanitizedCommand,
@@ -229,6 +230,15 @@
}
@override
+ bool canRun(dynamic executable, {String workingDirectory}) {
+ bool result =
+ delegate.canRun(executable, workingDirectory: workingDirectory);
+ _manifest.add(new CanRunManifestEntry(
+ executable: executable.toString(), result: result));
+ return result;
+ }
+
+ @override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.SIGTERM]) {
return delegate.killPid(pid, signal);
}
@@ -283,13 +293,13 @@
/// Waits for all running processes to exit, and records their exit codes in
/// the process manifest. Any process that doesn't exit within [timeout]
- /// will be marked as a [ManifestEntry.daemon] and be signalled with
+ /// will be marked as a [RunManifestEntry.daemon] and be signalled with
/// `SIGTERM`. If such processes *still* don't exit within [timeout] after
- /// being signalled, they'll be marked as [ManifestEntry.notResponding].
+ /// being signalled, they'll be marked as [RunManifestEntry.notResponding].
Future<Null> _waitForRunningProcessesToExit(Duration timeout) async {
await _waitForRunningProcessesToExitWithTimeout(
timeout: timeout,
- onTimeout: (ManifestEntry entry) {
+ onTimeout: (RunManifestEntry entry) {
entry.daemon = true;
delegate.killPid(entry.pid);
});
@@ -297,16 +307,16 @@
// them to shutdown, wait one more time for those processes to exit.
await _waitForRunningProcessesToExitWithTimeout(
timeout: timeout,
- onTimeout: (ManifestEntry entry) {
+ onTimeout: (RunManifestEntry entry) {
entry.notResponding = true;
});
}
Future<Null> _waitForRunningProcessesToExitWithTimeout({
Duration timeout,
- void onTimeout(ManifestEntry entry),
+ void onTimeout(RunManifestEntry entry),
}) async {
- void callOnTimeout(int pid) => onTimeout(_manifest.getEntry(pid));
+ void callOnTimeout(int pid) => onTimeout(_manifest.getRunEntry(pid));
await Future
.wait(new List<Future<int>>.from(_runningProcesses.values))
.timeout(timeout,
diff --git a/lib/src/record_replay/replay_process_manager.dart b/lib/src/record_replay/replay_process_manager.dart
index 3c2362a..bef2343 100644
--- a/lib/src/record_replay/replay_process_manager.dart
+++ b/lib/src/record_replay/replay_process_manager.dart
@@ -18,10 +18,11 @@
import 'package:path/path.dart' as path;
import '../interface/process_manager.dart';
+import 'can_run_manifest_entry.dart';
import 'common.dart';
import 'constants.dart';
import 'manifest.dart';
-import 'manifest_entry.dart';
+import 'run_manifest_entry.dart';
import 'recording_process_manager.dart';
/// Fakes all process invocations by replaying a previously-recorded series
@@ -95,7 +96,7 @@
bool runInShell: false,
io.ProcessStartMode mode: io.ProcessStartMode.NORMAL,
}) async {
- ManifestEntry entry = _popEntry(command, mode: mode);
+ RunManifestEntry entry = _popRunEntry(command, mode: mode);
_ReplayResult result = await _ReplayResult.create(this, entry);
return result.asProcess(entry.daemon);
}
@@ -110,7 +111,7 @@
Encoding stdoutEncoding: io.SYSTEM_ENCODING,
Encoding stderrEncoding: io.SYSTEM_ENCODING,
}) async {
- ManifestEntry entry = _popEntry(command,
+ RunManifestEntry entry = _popRunEntry(command,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
return await _ReplayResult.create(this, entry);
}
@@ -125,7 +126,7 @@
Encoding stdoutEncoding: io.SYSTEM_ENCODING,
Encoding stderrEncoding: io.SYSTEM_ENCODING,
}) {
- ManifestEntry entry = _popEntry(command,
+ RunManifestEntry entry = _popRunEntry(command,
stdoutEncoding: stdoutEncoding, stderrEncoding: stderrEncoding);
return _ReplayResult.createSync(this, entry);
}
@@ -133,14 +134,14 @@
/// Finds and returns the next entry in the process manifest that matches
/// the specified process arguments. Once found, it marks the manifest entry
/// as having been invoked and thus not eligible for invocation again.
- ManifestEntry _popEntry(
+ RunManifestEntry _popRunEntry(
List<dynamic> command, {
io.ProcessStartMode mode,
Encoding stdoutEncoding,
Encoding stderrEncoding,
}) {
List<String> sanitizedCommand = sanitize(command);
- ManifestEntry entry = _manifest.findPendingEntry(
+ RunManifestEntry entry = _manifest.findPendingRunEntry(
command: sanitizedCommand,
mode: mode,
stdoutEncoding: stdoutEncoding,
@@ -157,6 +158,18 @@
}
@override
+ bool canRun(dynamic executable, {String workingDirectory}) {
+ CanRunManifestEntry entry = _manifest.findPendingCanRunEntry(
+ executable: executable.toString(),
+ );
+ if (entry == null) {
+ throw new ArgumentError('No matching invocation found for $executable');
+ }
+ entry.setInvoked();
+ return entry.result;
+ }
+
+ @override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.SIGTERM]) {
throw new UnsupportedError(
"$runtimeType.killPid() has not been implemented because at the time "
@@ -192,7 +205,7 @@
static Future<_ReplayResult> create(
ReplayProcessManager manager,
- ManifestEntry entry,
+ RunManifestEntry entry,
) async {
FileSystem fs = manager.location.fileSystem;
String basePath = path.join(manager.location.path, entry.basename);
@@ -220,7 +233,7 @@
static _ReplayResult createSync(
ReplayProcessManager manager,
- ManifestEntry entry,
+ RunManifestEntry entry,
) {
FileSystem fs = manager.location.fileSystem;
String basePath = path.join(manager.location.path, entry.basename);
diff --git a/lib/src/record_replay/run_manifest_entry.dart b/lib/src/record_replay/run_manifest_entry.dart
new file mode 100644
index 0000000..c9e3103
--- /dev/null
+++ b/lib/src/record_replay/run_manifest_entry.dart
@@ -0,0 +1,148 @@
+// 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 ProcessStartMode, SYSTEM_ENCODING;
+
+import 'manifest_entry.dart';
+
+/// Gets a `ProcessStartMode` value by its string name.
+ProcessStartMode _getProcessStartMode(String value) {
+ if (value != null) {
+ for (ProcessStartMode mode in ProcessStartMode.values) {
+ if (mode.toString() == value) {
+ return mode;
+ }
+ }
+ throw new FormatException('Invalid value for mode: $value');
+ }
+ return null;
+}
+
+/// Gets an `Encoding` instance by the encoding name.
+Encoding _getEncoding(String encoding) {
+ if (encoding == 'system') {
+ return SYSTEM_ENCODING;
+ } else if (encoding != null) {
+ return Encoding.getByName(encoding);
+ }
+ return null;
+}
+
+/// An entry in the process invocation manifest for running an executable.
+class RunManifestEntry extends ManifestEntry {
+ @override
+ final String type = 'run';
+
+ /// The process id.
+ final int pid;
+
+ /// The base file name for this entry. `stdout` and `stderr` files for this
+ /// process will be serialized in the recording directory as
+ /// `$basename.stdout` and `$basename.stderr`, respectively.
+ final String basename;
+
+ /// The command that was run. The first element is the executable, and the
+ /// remaining elements are the arguments to the executable.
+ final List<String> command;
+
+ /// The process' working directory when it was spawned.
+ final String workingDirectory;
+
+ /// The environment variables that were passed to the process.
+ final Map<String, String> environment;
+
+ /// Whether the invoker's environment was made available to the process.
+ final bool includeParentEnvironment;
+
+ /// Whether the process was spawned through a system shell.
+ final bool runInShell;
+
+ /// The mode with which the process was spawned.
+ final ProcessStartMode mode;
+
+ /// The encoding used for the `stdout` of the process.
+ final Encoding stdoutEncoding;
+
+ /// The encoding used for the `stderr` of the process.
+ final Encoding stderrEncoding;
+
+ /// The exit code of the process.
+ int exitCode;
+
+ /// Creates a new manifest entry with the given properties.
+ RunManifestEntry({
+ this.pid,
+ this.basename,
+ this.command,
+ this.workingDirectory,
+ this.environment,
+ this.includeParentEnvironment,
+ this.runInShell,
+ this.mode,
+ this.stdoutEncoding,
+ this.stderrEncoding,
+ this.exitCode,
+ });
+
+ /// Creates a new manifest entry populated with the specified JSON [data].
+ ///
+ /// If any required fields are missing from the JSON data, this will throw
+ /// a [FormatException].
+ factory RunManifestEntry.fromJson(Map<String, dynamic> data) {
+ checkRequiredField(data, 'pid');
+ checkRequiredField(data, 'basename');
+ checkRequiredField(data, 'command');
+ RunManifestEntry entry = new RunManifestEntry(
+ pid: data['pid'],
+ basename: data['basename'],
+ command: data['command'],
+ workingDirectory: data['workingDirectory'],
+ environment: data['environment'],
+ includeParentEnvironment: data['includeParentEnvironment'],
+ runInShell: data['runInShell'],
+ mode: _getProcessStartMode(data['mode']),
+ stdoutEncoding: _getEncoding(data['stdoutEncoding']),
+ stderrEncoding: _getEncoding(data['stderrEncoding']),
+ exitCode: data['exitCode'],
+ );
+ entry.daemon = data['daemon'];
+ entry.notResponding = data['notResponding'];
+ return entry;
+ }
+
+ /// The executable that was invoked.
+ String get executable => command.first;
+
+ /// The arguments that were passed to [executable].
+ List<String> get arguments => command.skip(1).toList();
+
+ /// Indicates that the process is a daemon.
+ bool get daemon => _daemon;
+ bool _daemon = false;
+ set daemon(bool value) => _daemon = value ?? false;
+
+ /// Indicates that the process did not respond to `SIGTERM`.
+ bool get notResponding => _notResponding;
+ bool _notResponding = false;
+ set notResponding(bool value) => _notResponding = value ?? false;
+
+ /// Returns a JSON-encodable representation of this manifest entry.
+ @override
+ Map<String, dynamic> toJson() => new JsonBuilder()
+ .add('pid', pid)
+ .add('basename', basename)
+ .add('command', command)
+ .add('workingDirectory', workingDirectory)
+ .add('environment', environment)
+ .add('includeParentEnvironment', includeParentEnvironment)
+ .add('runInShell', runInShell)
+ .add('mode', mode, () => mode.toString())
+ .add('stdoutEncoding', stdoutEncoding, () => stdoutEncoding.name)
+ .add('stderrEncoding', stderrEncoding, () => stderrEncoding.name)
+ .add('daemon', daemon)
+ .add('notResponding', notResponding)
+ .add('exitCode', exitCode)
+ .entry;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 4acfa0c..2065165 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,15 +1,16 @@
name: process
-version: 1.0.1
+version: 1.1.0
authors:
- Todd Volkert <tvolkert@google.com>
description: A pluggable, mockable process invocation abstraction for Dart.
-homepage: https://github.com/tvolkert/process
+homepage: https://github.com/google/process.dart
dependencies:
file: ^1.0.1
intl: '>=0.14.0 <0.15.0'
meta: ^1.0.4
path: ^1.4.0
+ platform: ^1.0.1
dev_dependencies:
test: ^0.12.10
diff --git a/test/data/replay/MANIFEST.txt b/test/data/replay/MANIFEST.txt
index eda5bea..978ec3d 100644
--- a/test/data/replay/MANIFEST.txt
+++ b/test/data/replay/MANIFEST.txt
@@ -1,23 +1,36 @@
[
{
- "pid": 100,
- "basename": "001.sing.100",
- "command": [
- "sing",
- "ppap"
- ],
- "mode": "ProcessStartMode.NORMAL",
- "exitCode": 0
+ "type": "run",
+ "body": {
+ "pid": 100,
+ "basename": "001.sing.100",
+ "command": [
+ "sing",
+ "ppap"
+ ],
+ "mode": "ProcessStartMode.NORMAL",
+ "exitCode": 0
+ }
},
{
- "pid": 101,
- "basename": "002.dance.101",
- "command": [
- "dance",
- "gangnam-style"
- ],
- "stdoutEncoding": "system",
- "stderrEncoding": "system",
- "exitCode": 2
+ "type": "run",
+ "body": {
+ "pid": 101,
+ "basename": "002.dance.101",
+ "command": [
+ "dance",
+ "gangnam-style"
+ ],
+ "stdoutEncoding": "system",
+ "stderrEncoding": "system",
+ "exitCode": 2
+ }
+ },
+ {
+ "type": "can_run",
+ "body": {
+ "executable": "marathon",
+ "result": true
+ }
}
]
diff --git a/test/record_test.dart b/test/record_test.dart
index b340bb2..a94984d 100644
--- a/test/record_test.dart
+++ b/test/record_test.dart
@@ -3,10 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
-import 'dart:io' show Process, ProcessResult, SYSTEM_ENCODING;
+import 'dart:io' show Platform, Process, ProcessResult, SYSTEM_ENCODING;
import 'package:file/file.dart';
import 'package:file/local.dart';
+import 'package:path/path.dart' as p;
import 'package:process/process.dart';
import 'package:process/record_replay.dart';
import 'package:test/test.dart';
@@ -15,6 +16,9 @@
void main() {
FileSystem fs = new LocalFileSystem();
+ // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/1
+ // is available.
+ String newline = Platform.isWindows ? '\r\n' : '\n';
group('RecordingProcessManager', () {
Directory tmp;
@@ -30,7 +34,8 @@
});
test('start', () async {
- Process process = await manager.start(<String>['echo', 'foo']);
+ Process process =
+ await manager.start(<String>['echo', 'foo'], runInShell: true);
int pid = process.pid;
int exitCode = await process.exitCode;
List<int> stdout = await consume(process.stdout);
@@ -45,22 +50,25 @@
_Recording recording = new _Recording(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
- expect(entry['pid'], pid);
- expect(entry['command'], <String>['echo', 'foo']);
- expect(entry['mode'], 'ProcessStartMode.NORMAL');
- expect(entry['exitCode'], exitCode);
+ expect(entry['type'], 'run');
+ Map<String, dynamic> body = entry['body'];
+ expect(body['pid'], pid);
+ expect(body['command'], <String>['echo', 'foo']);
+ expect(body['mode'], 'ProcessStartMode.NORMAL');
+ expect(body['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
test('run', () async {
- ProcessResult result = await manager.run(<String>['echo', 'bar']);
+ ProcessResult result =
+ await manager.run(<String>['echo', 'bar'], runInShell: true);
int pid = result.pid;
int exitCode = result.exitCode;
String stdout = result.stdout;
String stderr = result.stderr;
expect(exitCode, 0);
- expect(stdout, 'bar\n');
+ expect(stdout, 'bar$newline');
expect(stderr, isEmpty);
// Force the recording to be written to disk.
@@ -69,23 +77,26 @@
_Recording recording = new _Recording(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
- expect(entry['pid'], pid);
- expect(entry['command'], <String>['echo', 'bar']);
- expect(entry['stdoutEncoding'], 'system');
- expect(entry['stderrEncoding'], 'system');
- expect(entry['exitCode'], exitCode);
+ expect(entry['type'], 'run');
+ Map<String, dynamic> body = entry['body'];
+ expect(body['pid'], pid);
+ expect(body['command'], <String>['echo', 'bar']);
+ expect(body['stdoutEncoding'], 'system');
+ expect(body['stderrEncoding'], 'system');
+ expect(body['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
test('runSync', () async {
- ProcessResult result = manager.runSync(<String>['echo', 'baz']);
+ ProcessResult result =
+ manager.runSync(<String>['echo', 'baz'], runInShell: true);
int pid = result.pid;
int exitCode = result.exitCode;
String stdout = result.stdout;
String stderr = result.stderr;
expect(exitCode, 0);
- expect(stdout, 'baz\n');
+ expect(stdout, 'baz$newline');
expect(stderr, isEmpty);
// Force the recording to be written to disk.
@@ -94,14 +105,34 @@
_Recording recording = new _Recording(tmp);
expect(recording.manifest, hasLength(1));
Map<String, dynamic> entry = recording.manifest.first;
- expect(entry['pid'], pid);
- expect(entry['command'], <String>['echo', 'baz']);
- expect(entry['stdoutEncoding'], 'system');
- expect(entry['stderrEncoding'], 'system');
- expect(entry['exitCode'], exitCode);
+ expect(entry['type'], 'run');
+ Map<String, dynamic> body = entry['body'];
+ expect(body['pid'], pid);
+ expect(body['command'], <String>['echo', 'baz']);
+ expect(body['stdoutEncoding'], 'system');
+ expect(body['stderrEncoding'], 'system');
+ expect(body['exitCode'], exitCode);
expect(recording.stdoutForEntryAt(0), stdout);
expect(recording.stderrForEntryAt(0), stderr);
});
+
+ test('canRun', () async {
+ String executable = p.join(tmp.path, 'bla.exe');
+ fs.file(executable).createSync();
+
+ bool result = manager.canRun(executable);
+
+ // Force the recording to be written to disk.
+ await manager.flush(finishRunningProcesses: true);
+
+ _Recording recording = new _Recording(tmp);
+ expect(recording.manifest, hasLength(1));
+ Map<String, dynamic> entry = recording.manifest.first;
+ expect(entry['type'], 'can_run');
+ Map<String, dynamic> body = entry['body'];
+ expect(body['executable'], executable);
+ expect(body['result'], result);
+ });
});
}
@@ -116,10 +147,10 @@
}
dynamic stdoutForEntryAt(int index) =>
- _getStdioContent(manifest[index], 'stdout');
+ _getStdioContent(manifest[index]['body'], 'stdout');
dynamic stderrForEntryAt(int index) =>
- _getStdioContent(manifest[index], 'stderr');
+ _getStdioContent(manifest[index]['body'], 'stderr');
dynamic _getFileContent(String name, Encoding encoding) {
File file = dir.fileSystem.file('${dir.path}/$name');
diff --git a/test/replay_test.dart b/test/replay_test.dart
index 8b84b1a..ff9fb49 100644
--- a/test/replay_test.dart
+++ b/test/replay_test.dart
@@ -51,5 +51,10 @@
expect(result.stdout, '');
expect(result.stderr, 'No one can dance like Psy\n');
});
+
+ test('canRun', () {
+ bool result = manager.canRun('marathon');
+ expect(result, true);
+ });
});
}
diff --git a/test/src/interface/common_test.dart b/test/src/interface/common_test.dart
new file mode 100644
index 0000000..1c82252
--- /dev/null
+++ b/test/src/interface/common_test.dart
@@ -0,0 +1,191 @@
+// 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' as p;
+import 'package:platform/platform.dart';
+import 'package:process/src/interface/common.dart';
+import 'package:test/test.dart';
+
+void main() {
+ FileSystem fs = new LocalFileSystem();
+
+ group('getExecutablePath', () {
+ Directory workingDir, dir1, dir2, dir3;
+
+ setUp(() {
+ workingDir = fs.systemTempDirectory.createTempSync('work_dir_');
+ dir1 = fs.systemTempDirectory.createTempSync('dir1_');
+ dir2 = fs.systemTempDirectory.createTempSync('dir2_');
+ dir3 = fs.systemTempDirectory.createTempSync('dir3_');
+ });
+
+ tearDown(() {
+ <Directory>[workingDir, dir1, dir2, dir3]
+ .forEach((Directory d) => d.deleteSync(recursive: true));
+ });
+
+ group('on windows', () {
+ Platform platform;
+
+ setUp(() {
+ platform = new FakePlatform(
+ operatingSystem: 'windows',
+ environment: <String, String>{
+ 'PATH': '${dir1.path};${dir2.path}',
+ 'PATHEXT': '.exe;.bat'
+ });
+ });
+
+ test('absolute', () {
+ String command = p.join(dir3.path, 'bla.exe');
+ String expectedPath = command;
+ fs.file(command).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+
+ command = p.withoutExtension(command);
+ executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('in path', () {
+ String command = 'bla.exe';
+ String expectedPath = p.join(dir2.path, command);
+ fs.file(expectedPath).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+
+ command = p.withoutExtension(command);
+ executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('in path multiple times', () {
+ String command = 'bla.exe';
+ String expectedPath = p.join(dir1.path, command);
+ String wrongPath = p.join(dir2.path, command);
+ fs.file(expectedPath).createSync();
+ fs.file(wrongPath).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+
+ command = p.withoutExtension(command);
+ executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('in subdir of work dir', () {
+ String command = p.join('.', 'foo', 'bla.exe');
+ String expectedPath = p.join(workingDir.path, command);
+ fs.file(expectedPath).createSync(recursive: true);
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+
+ command = p.withoutExtension(command);
+ executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('in work dir', () {
+ String command = p.join('.', 'bla.exe');
+ String expectedPath = p.join(workingDir.path, command);
+ String wrongPath = p.join(dir2.path, command);
+ fs.file(expectedPath).createSync();
+ fs.file(wrongPath).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+
+ command = p.withoutExtension(command);
+ executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('with multiple extensions', () {
+ String command = 'foo';
+ String expectedPath = p.join(dir1.path, '$command.exe');
+ String wrongPath1 = p.join(dir1.path, '$command.bat');
+ String wrongPath2 = p.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);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('not found', () {
+ String command = 'foo.exe';
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ expect(executablePath, isNull);
+ });
+ });
+
+ group('on Linux', () {
+ Platform platform;
+
+ setUp(() {
+ platform = new FakePlatform(
+ operatingSystem: 'linux',
+ environment: <String, String>{'PATH': '${dir1.path}:${dir2.path}'});
+ });
+
+ test('absolute', () {
+ String command = p.join(dir3.path, 'bla');
+ String expectedPath = command;
+ String wrongPath = p.join(dir3.path, 'bla.bat');
+ fs.file(command).createSync();
+ fs.file(wrongPath).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('in path multiple times', () {
+ String command = 'xxx';
+ String expectedPath = p.join(dir1.path, command);
+ String wrongPath = p.join(dir2.path, command);
+ fs.file(expectedPath).createSync();
+ fs.file(wrongPath).createSync();
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ _expectSamePath(executablePath, expectedPath);
+ });
+
+ test('not found', () {
+ String command = 'foo';
+
+ String executablePath =
+ getExecutablePath(command, workingDir.path, platform: platform);
+ expect(executablePath, isNull);
+ });
+ });
+ });
+}
+
+void _expectSamePath(String actual, String expected) {
+ expect(actual, isNotNull);
+ expect(actual.toLowerCase(), expected.toLowerCase());
+}