Commit 8303fff8 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Run pub in interactive mode in flutter packages pub (#11700)

parent f430a45a
...@@ -118,6 +118,7 @@ Future<int> run(List<String> args, List<FlutterCommand> subCommands, { ...@@ -118,6 +118,7 @@ Future<int> run(List<String> args, List<FlutterCommand> subCommands, {
// in those locations as well to see if you need a similar update there. // in those locations as well to see if you need a similar update there.
// Seed these context entries first since others depend on them // Seed these context entries first since others depend on them
context.putIfAbsent(Stdio, () => const Stdio());
context.putIfAbsent(Platform, () => const LocalPlatform()); context.putIfAbsent(Platform, () => const LocalPlatform());
context.putIfAbsent(FileSystem, () => const LocalFileSystem()); context.putIfAbsent(FileSystem, () => const LocalFileSystem());
context.putIfAbsent(ProcessManager, () => const LocalProcessManager()); context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
......
...@@ -26,10 +26,11 @@ ...@@ -26,10 +26,11 @@
/// increase the API surface that we have to test in Flutter tools, and the APIs /// increase the API surface that we have to test in Flutter tools, and the APIs
/// in `dart:io` can sometimes be hard to use in tests. /// in `dart:io` can sometimes be hard to use in tests.
import 'dart:async'; import 'dart:async';
import 'dart:io' as io show exit, ProcessSignal; import 'dart:io' as io show exit, IOSink, ProcessSignal, stderr, stdin, stdout;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'context.dart';
import 'platform.dart'; import 'platform.dart';
import 'process.dart'; import 'process.dart';
...@@ -62,10 +63,11 @@ export 'dart:io' ...@@ -62,10 +63,11 @@ export 'dart:io'
ProcessStartMode, ProcessStartMode,
// RandomAccessFile NO! Use `file_system.dart` // RandomAccessFile NO! Use `file_system.dart`
ServerSocket, ServerSocket,
stderr, // stderr, NO! Use `io.dart`
stdin, // stdin, NO! Use `io.dart`
Stdin,
StdinException, StdinException,
stdout, // stdout, NO! Use `io.dart`
Socket, Socket,
SocketException, SocketException,
SYSTEM_ENCODING, SYSTEM_ENCODING,
...@@ -143,3 +145,32 @@ class _PosixProcessSignal extends ProcessSignal { ...@@ -143,3 +145,32 @@ class _PosixProcessSignal extends ProcessSignal {
return super.watch(); return super.watch();
} }
} }
class Stdio {
const Stdio();
Stream<List<int>> get stdin => io.stdin;
io.IOSink get stdout => io.stdout;
io.IOSink get stderr => io.stderr;
}
io.IOSink get stderr {
if (context == null)
return io.stderr;
final Stdio contextStreams = context[Stdio];
return contextStreams.stderr;
}
Stream<List<int>> get stdin {
if (context == null)
return io.stdin;
final Stdio contextStreams = context[Stdio];
return contextStreams.stdin;
}
io.IOSink get stdout {
if (context == null)
return io.stdout;
final Stdio contextStreams = context[Stdio];
return contextStreams.stdout;
}
...@@ -166,6 +166,30 @@ Future<int> runCommandAndStreamOutput(List<String> cmd, { ...@@ -166,6 +166,30 @@ Future<int> runCommandAndStreamOutput(List<String> cmd, {
return await process.exitCode; return await process.exitCode;
} }
/// Runs the [command] interactively, connecting the stdin/stdout/stderr
/// streams of this process to those of the child process. Completes with
/// the exit code of the child process.
Future<int> runInteractively(List<String> command, {
String workingDirectory,
bool allowReentrantFlutter: false,
Map<String, String> environment
}) async {
final Process process = await runCommand(
command,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment,
);
process.stdin.addStream(stdin);
// Wait for stdout and stderr to be fully processed, because process.exitCode
// may complete first.
Future.wait<dynamic>(<Future<dynamic>>[
stdout.addStream(process.stdout),
stderr.addStream(process.stderr),
]);
return await process.exitCode;
}
Future<Null> runAndKill(List<String> cmd, Duration timeout) { Future<Null> runAndKill(List<String> cmd, Duration timeout) {
final Future<Process> proc = runDetached(cmd); final Future<Process> proc = runDetached(cmd);
return new Future<Null>.delayed(timeout, () async { return new Future<Null>.delayed(timeout, () async {
......
...@@ -9,7 +9,7 @@ import 'package:quiver/strings.dart'; ...@@ -9,7 +9,7 @@ import 'package:quiver/strings.dart';
import '../globals.dart'; import '../globals.dart';
import 'context.dart'; import 'context.dart';
import 'io.dart'; import 'io.dart' as io;
import 'platform.dart'; import 'platform.dart';
final AnsiTerminal _kAnsiTerminal = new AnsiTerminal(); final AnsiTerminal _kAnsiTerminal = new AnsiTerminal();
...@@ -61,18 +61,21 @@ class AnsiTerminal { ...@@ -61,18 +61,21 @@ class AnsiTerminal {
// [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is // [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is
// connected to a terminal or not. // connected to a terminal or not.
// (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.) // (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.)
try { final Stream<List<int>> stdin = io.stdin;
// The order of setting lineMode and echoMode is important on Windows. if (stdin is io.Stdin) {
if (value) { try {
stdin.echoMode = false; // The order of setting lineMode and echoMode is important on Windows.
stdin.lineMode = false; if (value) {
} else { stdin.echoMode = false;
stdin.lineMode = true; stdin.lineMode = false;
stdin.echoMode = true; } else {
stdin.lineMode = true;
stdin.echoMode = true;
}
} on io.StdinException catch (error) {
if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode))
rethrow;
} }
} on StdinException catch (error) {
if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode))
rethrow;
} }
} }
...@@ -83,7 +86,7 @@ class AnsiTerminal { ...@@ -83,7 +86,7 @@ class AnsiTerminal {
/// Useful when the console is in [singleCharMode]. /// Useful when the console is in [singleCharMode].
Stream<String> get onCharInput { Stream<String> get onCharInput {
if (_broadcastStdInString == null) if (_broadcastStdInString == null)
_broadcastStdInString = stdin.transform(ASCII.decoder).asBroadcastStream(); _broadcastStdInString = io.stdin.transform(ASCII.decoder).asBroadcastStream();
return _broadcastStdInString; return _broadcastStdInString;
} }
......
...@@ -124,5 +124,5 @@ class PackagesPassthroughCommand extends FlutterCommand { ...@@ -124,5 +124,5 @@ class PackagesPassthroughCommand extends FlutterCommand {
} }
@override @override
Future<Null> runCommand() => pub(argResults.rest, retry: false); Future<Null> runCommand() => pubInteractively(argResults.rest);
} }
...@@ -78,23 +78,25 @@ Future<Null> pubGet({ ...@@ -78,23 +78,25 @@ Future<Null> pubGet({
typedef String MessageFilter(String message); typedef String MessageFilter(String message);
/// Runs pub in 'batch' mode, forwarding complete lines written by pub to its
/// stdout/stderr streams to the corresponding stream of this process, optionally
/// applying filtering. The pub process will not receive anything on its stdin stream.
Future<Null> pub(List<String> arguments, { Future<Null> pub(List<String> arguments, {
String directory, String directory,
MessageFilter filter, MessageFilter filter,
String failureMessage: 'pub failed', String failureMessage: 'pub failed',
@required bool retry, @required bool retry,
}) async { }) async {
final List<String> command = <String>[ sdkBinaryName('pub') ]..addAll(arguments);
int attempts = 0; int attempts = 0;
int duration = 1; int duration = 1;
int code; int code;
while (true) { while (true) {
attempts += 1; attempts += 1;
code = await runCommandAndStreamOutput( code = await runCommandAndStreamOutput(
command, _pubCommand(arguments),
workingDirectory: directory, workingDirectory: directory,
mapFunction: filter, mapFunction: filter,
environment: <String, String>{ 'FLUTTER_ROOT': Cache.flutterRoot, _pubEnvironmentKey: _getPubEnvironmentValue() } environment: _pubEnvironment,
); );
if (code != 69) // UNAVAILABLE in https://github.com/dart-lang/pub/blob/master/lib/src/exit_codes.dart if (code != 69) // UNAVAILABLE in https://github.com/dart-lang/pub/blob/master/lib/src/exit_codes.dart
break; break;
...@@ -108,6 +110,32 @@ Future<Null> pub(List<String> arguments, { ...@@ -108,6 +110,32 @@ Future<Null> pub(List<String> arguments, {
throwToolExit('$failureMessage ($code)', exitCode: code); throwToolExit('$failureMessage ($code)', exitCode: code);
} }
/// Runs pub in 'interactive' mode, directly piping the stdin stream of this
/// process to that of pub, and the stdout/stderr stream of pub to the corresponding
/// streams of this process.
Future<Null> pubInteractively(List<String> arguments, {
String directory,
}) async {
final int code = await runInteractively(
_pubCommand(arguments),
workingDirectory: directory,
environment: _pubEnvironment,
);
if (code != 0)
throwToolExit('pub finished with exit code $code', exitCode: code);
}
/// The command used for running pub.
List<String> _pubCommand(List<String> arguments) {
return <String>[ sdkBinaryName('pub') ]..addAll(arguments);
}
/// The full environment used when running pub.
Map<String, String> get _pubEnvironment => <String, String>{
'FLUTTER_ROOT': Cache.flutterRoot,
_pubEnvironmentKey: _getPubEnvironmentValue(),
};
final RegExp _analyzerWarning = new RegExp(r'^! \w+ [^ ]+ from path \.\./\.\./bin/cache/dart-sdk/lib/\w+$'); final RegExp _analyzerWarning = new RegExp(r'^! \w+ [^ ]+ from path \.\./\.\./bin/cache/dart-sdk/lib/\w+$');
/// The console environment key used by the pub tool. /// The console environment key used by the pub tool.
......
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io' show IOSink;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart' hide IOSink;
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/packages.dart'; import 'package:flutter_tools/src/commands/packages.dart';
...@@ -69,47 +71,72 @@ void main() { ...@@ -69,47 +71,72 @@ void main() {
}); });
group('packages test/pub', () { group('packages test/pub', () {
final List<List<dynamic>> log = <List<dynamic>>[]; MockProcessManager mockProcessManager;
MockStdio mockStdio;
setUp(() {
mockProcessManager = new MockProcessManager();
mockStdio = new MockStdio();
});
testUsingContext('test', () async { testUsingContext('test', () async {
log.clear();
await createTestCommandRunner(new PackagesCommand()).run(<String>['packages', 'test']); await createTestCommandRunner(new PackagesCommand()).run(<String>['packages', 'test']);
expect(log, hasLength(1)); final List<String> commands = mockProcessManager.commands;
expect(log[0], hasLength(3)); expect(commands, hasLength(3));
expect(log[0][0], matches(r'dart-sdk[\\/]bin[\\/]pub')); expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
expect(log[0][1], 'run'); expect(commands[1], 'run');
expect(log[0][2], 'test'); expect(commands[2], 'test');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () { ProcessManager: () => mockProcessManager,
return new MockProcessManager((List<dynamic> command) { Stdio: () => mockStdio,
log.add(command);
});
},
}); });
testUsingContext('run', () async { testUsingContext('run', () async {
log.clear();
await createTestCommandRunner(new PackagesCommand()).run(<String>['packages', '--verbose', 'pub', 'run', '--foo', 'bar']); await createTestCommandRunner(new PackagesCommand()).run(<String>['packages', '--verbose', 'pub', 'run', '--foo', 'bar']);
expect(log, hasLength(1)); final List<String> commands = mockProcessManager.commands;
expect(log[0], hasLength(4)); expect(commands, hasLength(4));
expect(log[0][0], matches(r'dart-sdk[\\/]bin[\\/]pub')); expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
expect(log[0][1], 'run'); expect(commands[1], 'run');
expect(log[0][2], '--foo'); expect(commands[2], '--foo');
expect(log[0][3], 'bar'); expect(commands[3], 'bar');
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Stdio: () => mockStdio,
});
testUsingContext('publish', () async {
final PromptingProcess process = new PromptingProcess();
mockProcessManager.processFactory = (List<String> commands) => process;
final Future<Null> runPackages = createTestCommandRunner(new PackagesCommand()).run(<String>['packages', 'pub', 'publish']);
final Future<Null> runPrompt = process.showPrompt('Proceed (y/n)? ', <String>['hello', 'world']);
final Future<Null> simulateUserInput = new Future<Null>(() {
mockStdio.simulateStdin('y');
});
await Future.wait(<Future<Null>>[runPackages, runPrompt, simulateUserInput]);
final List<String> commands = mockProcessManager.commands;
expect(commands, hasLength(2));
expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
expect(commands[1], 'publish');
final List<String> stdout = mockStdio.writtenToStdout;
expect(stdout, hasLength(4));
expect(stdout.sublist(0, 2), contains('Proceed (y/n)? '));
expect(stdout.sublist(0, 2), contains('y\n'));
expect(stdout[2], 'hello\n');
expect(stdout[3], 'world\n');
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () { ProcessManager: () => mockProcessManager,
return new MockProcessManager((List<dynamic> command) { Stdio: () => mockStdio,
log.add(command);
});
},
}); });
}); });
} }
typedef void StartCallback(List<dynamic> command); /// A strategy for creating Process objects from a list of commands.
typedef Process ProcessFactory(List<String> command);
/// A ProcessManager that starts Processes by delegating to a ProcessFactory.
class MockProcessManager implements ProcessManager { class MockProcessManager implements ProcessManager {
MockProcessManager(this.onStart); ProcessFactory processFactory = (List<String> commands) => new MockProcess();
List<String> commands;
final StartCallback onStart;
@override @override
Future<Process> start( Future<Process> start(
...@@ -120,20 +147,63 @@ class MockProcessManager implements ProcessManager { ...@@ -120,20 +147,63 @@ class MockProcessManager implements ProcessManager {
bool runInShell: false, bool runInShell: false,
ProcessStartMode mode: ProcessStartMode.NORMAL, ProcessStartMode mode: ProcessStartMode.NORMAL,
}) { }) {
onStart(command); commands = command;
return new Future<Process>.value(new MockProcess()); return new Future<Process>.value(processFactory(command));
}
@override
dynamic noSuchMethod(Invocation invocation) => null;
}
/// A process that prompts the user to proceed, then asynchronously writes
/// some lines to stdout before it exits.
class PromptingProcess implements Process {
Future<Null> showPrompt(String prompt, List<String> outputLines) async {
_stdoutController.add(UTF8.encode(prompt));
final List<int> bytesOnStdin = await _stdin.future;
// Echo stdin to stdout.
_stdoutController.add(bytesOnStdin);
if (bytesOnStdin[0] == UTF8.encode('y')[0]) {
for (final String line in outputLines)
_stdoutController.add(UTF8.encode('$line\n'));
}
await _stdoutController.close();
}
final StreamController<List<int>> _stdoutController = new StreamController<List<int>>();
final CompleterIOSink _stdin = new CompleterIOSink();
@override
Stream<List<int>> get stdout => _stdoutController.stream;
@override
Stream<List<int>> get stderr => const Stream<List<int>>.empty();
@override
IOSink get stdin => _stdin;
@override
Future<int> get exitCode async {
await _stdoutController.done;
return 0;
} }
@override @override
dynamic noSuchMethod(Invocation invocation) => null; dynamic noSuchMethod(Invocation invocation) => null;
} }
/// An inactive process that collects stdin and produces no output.
class MockProcess implements Process { class MockProcess implements Process {
final IOSink _stdin = new MemoryIOSink();
@override
Stream<List<int>> get stdout => const Stream<List<int>>.empty();
@override @override
Stream<List<int>> get stdout => new MockStream<List<int>>(); Stream<List<int>> get stderr => const Stream<List<int>>.empty();
@override @override
Stream<List<int>> get stderr => new MockStream<List<int>>(); IOSink get stdin => _stdin;
@override @override
Future<int> get exitCode => new Future<int>.value(0); Future<int> get exitCode => new Future<int>.value(0);
...@@ -142,29 +212,97 @@ class MockProcess implements Process { ...@@ -142,29 +212,97 @@ class MockProcess implements Process {
dynamic noSuchMethod(Invocation invocation) => null; dynamic noSuchMethod(Invocation invocation) => null;
} }
class MockStream<T> implements Stream<T> { /// An IOSink that completes a future with the first line written to it.
class CompleterIOSink extends MemoryIOSink {
final Completer<List<int>> _completer = new Completer<List<int>>();
Future<List<int>> get future => _completer.future;
@override @override
Stream<S> transform<S>(StreamTransformer<T, S> streamTransformer) => new MockStream<S>(); void add(List<int> data) {
if (!_completer.isCompleted)
_completer.complete(data);
super.add(data);
}
}
/// A Stdio that collects stdout and supports simulated stdin.
class MockStdio extends Stdio {
final MemoryIOSink _stdout = new MemoryIOSink();
final StreamController<List<int>> _stdin = new StreamController<List<int>>();
@override @override
Stream<T> where(bool test(T event)) => new MockStream<T>(); IOSink get stdout => _stdout;
@override @override
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}) { Stream<List<int>> get stdin => _stdin.stream;
return new MockStreamSubscription<T>();
void simulateStdin(String line) {
_stdin.add(UTF8.encode('$line\n'));
} }
@override List<String> get writtenToStdout => _stdout.writes.map(_stdout.encoding.decode).toList();
dynamic noSuchMethod(Invocation invocation) => null;
} }
class MockStreamSubscription<T> implements StreamSubscription<T> { /// An IOSink that collects whatever is written to it.
class MemoryIOSink implements IOSink {
@override @override
Future<E> asFuture<E>([E futureValue]) => new Future<E>.value(); Encoding encoding = UTF8;
final List<List<int>> writes = <List<int>>[];
@override @override
Future<Null> cancel() => null; void add(List<int> data) {
writes.add(data);
}
@override @override
dynamic noSuchMethod(Invocation invocation) => null; Future<Null> addStream(Stream<List<int>> stream) {
final Completer<Null> completer = new Completer<Null>();
stream.listen((List<int> data) {
add(data);
}).onDone(() => completer.complete(null));
return completer.future;
}
@override
void writeCharCode(int charCode) {
add(<int>[charCode]);
}
@override
void write(Object obj) {
add(encoding.encode('$obj'));
}
@override
void writeln([Object obj = ""]) {
add(encoding.encode('$obj\n'));
}
@override
void writeAll(Iterable<dynamic> objects, [String separator = ""]) {
bool addSeparator = false;
for (dynamic object in objects) {
if (addSeparator) {
write(separator);
}
write(object);
addSeparator = true;
}
}
@override
void addError(dynamic error, [StackTrace stackTrace]) {
throw new UnimplementedError();
}
@override
Future<Null> get done => close();
@override
Future<Null> close() async => null;
@override
Future<Null> flush() async => null;
} }
...@@ -8,6 +8,7 @@ import 'package:flutter_tools/src/artifacts.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
...@@ -73,6 +74,7 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -73,6 +74,7 @@ void testUsingContext(String description, dynamic testMethod(), {
// The context always starts with these value since others depend on them. // The context always starts with these value since others depend on them.
testContext testContext
..putIfAbsent(Stdio, () => const Stdio())
..putIfAbsent(Platform, () => const LocalPlatform()) ..putIfAbsent(Platform, () => const LocalPlatform())
..putIfAbsent(FileSystem, () => const LocalFileSystem()) ..putIfAbsent(FileSystem, () => const LocalFileSystem())
..putIfAbsent(ProcessManager, () => const LocalProcessManager()) ..putIfAbsent(ProcessManager, () => const LocalProcessManager())
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment