Override stdin, stdout, stderr in Dart - dart

I'm looking to build the equivalent of the bash 'expect' command which is able to
capture output to stdout as well as inject data into stdin.
Dart provides the IOOverrides class that looks like it allows you to override stdout, stdin and stderr.
The ZoneSpecification that you pass to IOOverrides expects a instance of type StdIn to override stdin.
IOOverrides.runZoned(() => action,
stdin: () => Stdin._(mystream), // this won't work as there is no public ctor
);
As such I was hoping I could instantiate my own copy of StdIn and then inject data into it.
The problem is that StdIn only has a private constructor.
So it would appear that there is no way to actually override Stdin using a ZoneSpecification.
Am I missing something here?
The stdin getter actually has the code to allow it to be overriden:
/// The standard input stream of data read by this program.
Stdin get stdin {
return IOOverrides.current?.stdin ?? _stdin;
}
Are there other ways to achieve this?
Ultimately this is what I'm trying to achieve:
Interact(spawn: () {
final age = ask(
'How old are you',
defaultValue: '5',
customPrompt: (prompt, defaultValue, {hidden = false}) =>
'AAA$prompt:$defaultValue',
);
print('You are $age years old');
})
..expect('AAAHow old ar you:5')
..send('6')
..expect('You are 6 years old');

The Process class exposes a stdin member. See https://api.dart.dev/stable/2.10.5/dart-io/Process-class.html#standard-io-streams.

So this is not even close to a working solution, but based on #jamesdlin
comment the basic implementation looks like this.
Currently all of the puppet wrapper classes just forward calls to the real stdin/out/err. In reality these will need to intercept and process the calls rather than passing them through.
void test() {
Puppet(spawn: () {
final age = ask(
'How old are you',
defaultValue: '5',
customPrompt: (prompt, defaultValue, {hidden = false}) =>
'AAA$prompt:$defaultValue',
);
print('You are $age years old');
})
..expect('AAAHow old ar you:5')
..send('6')
..expect('You are 6 years old');
}
The Puppet class:
class Puppet<T> {
Puppet({required this.spawn});
T Function() spawn;
void _run() {
IOOverrides.runZoned(() => spawn,
stdin: PuppetStdin.new,
stdout: () => PuppetStdout(stdout),
stderr: () => PuppetStdout(stderr));
}
void expect(String expected) {}
void send(String s) {}
}
PuppetStdin
import 'dart:async';
import 'dart:convert';
import 'dart:io';
class PuppetStdin extends Stream<List<int>> implements Stdin {
PuppetStdin(
{this.echoMode = true,
this.echoNewlineMode = true,
this.lineMode = true});
StreamController<List<int>> controller = StreamController();
#override
bool echoMode;
#override
bool echoNewlineMode;
#override
bool lineMode;
#override
bool get hasTerminal => stdin.hasTerminal;
#override
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
throw UnimplementedError();
}
#override
int readByteSync() => stdin.readByteSync();
#override
String? readLineSync(
{Encoding encoding = systemEncoding, bool retainNewlines = false}) =>
stdin.readLineSync(encoding: encoding, retainNewlines: retainNewlines);
#override
bool get supportsAnsiEscapes => stdin.supportsAnsiEscapes;
}
PuppetStdout which will probably also serve the needs of overloading stderr
import 'dart:convert';
import 'dart:io';
class PuppetStdout implements Stdout {
PuppetStdout(this.stdout);
Stdout stdout;
IOSink? _nonBlocking;
/// Whether there is a terminal attached to stdout.
#override
bool get hasTerminal => stdout.hasTerminal;
/// The number of columns of the terminal.
///
/// If no terminal is attached to stdout, a [StdoutException] is thrown. See
/// [hasTerminal] for more info.
#override
int get terminalColumns => stdout.terminalColumns;
/// The number of lines of the terminal.
///
/// If no terminal is attached to stdout, a [StdoutException] is thrown. See
/// [hasTerminal] for more info.
#override
int get terminalLines => stdout.terminalLines;
/// Whether connected to a terminal that supports ANSI escape sequences.
///
/// Not all terminals are recognized, and not all recognized terminals can
/// report whether they support ANSI escape sequences, so this value is a
/// best-effort attempt at detecting the support.
///
/// The actual escape sequence support may differ between terminals,
/// with some terminals supporting more escape sequences than others,
/// and some terminals even differing in behavior for the same escape
/// sequence.
///
/// The ANSI color selection is generally supported.
///
/// Currently, a `TERM` environment variable containing the string `xterm`
/// will be taken as evidence that ANSI escape sequences are supported.
/// On Windows, only versions of Windows 10 after v.1511
/// ("TH2", OS build 10586) will be detected as supporting the output of
/// ANSI escape sequences, and only versions after v.1607 ("Anniversary
/// Update", OS build 14393) will be detected as supporting the input of
/// ANSI escape sequences.
#override
bool get supportsAnsiEscapes => stdout.supportsAnsiEscapes;
/// A non-blocking `IOSink` for the same output.
#override
IOSink get nonBlocking => _nonBlocking ??= stdout.nonBlocking;
#override
Encoding get encoding => stdout.encoding;
#override
set encoding(Encoding encoding) {
stdout.encoding = encoding;
}
#override
void add(List<int> data) {
stdout.add(data);
}
#override
void addError(Object error, [StackTrace? stackTrace]) {
stdout.addError(error, stackTrace);
}
#override
// ignore: strict_raw_type
Future addStream(Stream<List<int>> stream) => stdout.addStream(stream);
#override
// ignore: strict_raw_type
Future close() => stdout.close();
#override
// ignore: strict_raw_type
Future get done => stdout.done;
#override
// ignore: strict_raw_type
Future flush() => stdout.flush();
#override
void write(Object? object) {
stdout.write(object);
}
#override
// ignore: strict_raw_type
void writeAll(Iterable objects, [String sep = '']) {
stdout.writeAll(objects, sep);
}
#override
void writeCharCode(int charCode) {
stdout.writeCharCode(charCode);
}
#override
void writeln([Object? object = '']) {
stdout.writeln(object);
}
}

Related

NoSuchMethodEror: tried to call a non-function, such as null:

I have this code which works well on android emulator but gives error on web.
import 'package:parivaar/components/screens/home/Home.dart';
typedef T Constructor<T>();
final Map<String, Constructor<Object>> _constructors =
<String, Constructor<Object>>{};
void register<T>(Constructor<T> constructor) {
_constructors[T.toString()] = constructor;
}
class ClassBuilder {
static void registerClasses() {
register<Home>(() => Home());
}
static dynamic fromString(String type) {
return _constructors[type]();
}
}
And i am calling that function as follows:
class _MyHomePageState extends State {
KFDrawerController _drawerController;
#override
void initState() {
super.initState();
_drawerController = KFDrawerController(
initialPage: ClassBuilder.fromString('Home'),
.
..
...
....
You are probably assuming that T.toString() returns the source name of the type as a string. Nobody ever promised that.
It works on native, but on the web you often optimize for size and "minification" turns all the long names into shorter names. With that, the name of the type Home is no longer the string "Home".
I generally do not recommend depending on the string representation of types (or Type objects for that matter).
Consider changing register and fromString to:
void register<T>(Constructor<T> constructor) {
_constructors[T] = constructor;
}
and
static T fromType<T>() => _constructors[T]();
That relies on Type object equality, which is a well-defined operation.
Not perfect, but still better than going through strings.
If you need to create the objects dynamically from strings, where you don't know the type, then I'd instead require you to provide the key string on registration, changing register to:
void register<T>(String key, Constructor<T> constructor) {
_constructors[key] = constructor;
}
and register types like:
static void registerClasses() {
register<Home>("Home", () => Home());
}

What's the equivalent to this[x] in Dart?

For instance, in Javascript I can do something like:
class Foo {
x = 'baz';
bar() {
const someVar = 'x';
console.log(this[someVar]);
// Output: 'baz';
}
}
Hopefully that's relatively clear - it boils down to accessing a member variable by another variable's contents. How is this achieved in Dart?
This is not trivial in Dart. Dart doesn't have a syntax to access class properties with [].
There are a couple of approaches though:
Mirrors:
https://api.dartlang.org/stable/2.6.1/dart-mirrors/dart-mirrors-library.html
Basically you have access to everything and offers the biggest freedom. You can check what properties a class has, access them via names and so on. Big disadvantage is that the generated JS (if targeting web) will be huge. Flutter doesn't support it at all.
Reflectable
To deal with the large generated JS, you can use package:reflectable. Never tried it with Flutter. It's a bit more to set up and start using bit it works.
Dart only solution 1
You can overload [] operator on a class:
class Foo {
final _backing = <String, String>{
'foo': 'bar'
};
operator [](String val) {
return _backing[val];
}
}
void main() {
final inst = Foo();
print(inst['foo']);
}
Dart only solution 2
Just use a map :) Well sort of... If you are dealing with complex types and you want to add some extra functionality to your map, you can do something like this:
import 'dart:collection';
class StringMap extends Object with MapMixin<String, String> {
final _backing = <String, String>{};
#override
String operator [](Object key) {
return _backing[key];
}
#override
void operator []=(String key, String value) {
_backing[key] = value;
}
#override
void clear() {
_backing.clear();
}
#override
Iterable<String> get keys => _backing.keys;
#override
String remove(Object key) {
return _backing.remove(key);
}
}

How to transform a Sink<T> into a Sink<V>?

I have a method that can emit its output into a given Sink<Node>.
I wanted to pipe that into stdout which is a Sink<List<int>>.
Supposing I have a function convert that converts Node to List<int>, how can I transform stdout into a Sink<Node>, so that it will print my Tree to the console?
I have made this example showing how you can do it with a StreamController:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
class Message {
String text;
Message(this.text);
}
void main() {
final controller = StreamController<Message>();
stdout.addStream(controller.stream
.map((var msg) => msg.text)
.transform(const Utf8Encoder()));
var messageSink = controller.sink;
messageSink.add(Message('Hello World'));
}
The StreamController in this example takes Message objects and converts them into List<int> by first using map to convert the Message to String object and then use a transformer to convert the String into a List of UTF8 bytes.
I've filed: https://github.com/dart-lang/sdk/issues/50607
Here is how I solved this:
class _MappedSink<From, To> implements Sink<From> {
final To Function(From) _transform;
final Sink<To> _sink;
const _MappedSink(this._sink, this._transform);
#override
void add(From data) => _sink.add(_transform(data));
#override
void close() => _sink.close();
}
extension SinkMap<To> on Sink<To> {
Sink<From> map<From>(To Function(From) transform) =>
_MappedSink(this, transform);
}

Connecting a sink of BLoC with another BLoC

I am using the BLoC pattern as described at the Google IO talk.
I have a simple BLoC which is used to display a alert in the UI whenever a string is added to messageSink:
class AlertBloc {
final _message = BehaviorSubject<String>();
AlertBloc() {}
Stream<String> get message => _message.stream;
Sink<String> get messageSink => _message.sink;
void dispose() {
_message.close(); }
}
Elsewhere in the app, I have another BLoC which needs to add a string to messageSink, when a certain condition is met.
I noticed it is not a good idea to provide the whole BLoC from the Google I/O repo for the talk, and they provide advice for connecting a stream from a BLoC to another BLoC sink:
Note that we are not providing [CartBloc] to the
[ProductSquareBloc] directly, although it would be easier to
implement. BLoCs should not depend on other BLoCs (separation of
concerns). They can only communicate with each other using
streams. In this case, the [CartBloc.items] output plugs into the
[ProductSquareBloc.cartItems] input.
My question is how to connect a sink from a BLoC to another BLoC stream?
Here is a simple example for you. Imagine the following two BLoCs:
The first one exposes a Stream and populates it with some values:
class ProducerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _productionController = StreamController<int>();
//Instead, you expose a stream
Stream<int> get production => _productionController.stream;
//This method generates some values and puts them to stream
void produceValue() {
_productionController.sink.add(1);
_productionController.sink.add(2);
_productionController.sink.add(3);
}
//Don't forget to close your controllers
void dispose() {
_productionController.close();
}
}
The other one exposes a Sink and processes values that are put into it.
class ConsumerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _consumptionController = StreamController<int>();
//Instead, you expose a sink
StreamSink<int> get consumption => _consumptionController.sink;
//In class constructor we start listening to the stream of values
ConsumerBLoC() {
_consumptionController.listen((value) {_consumeValue(value);} );
//or simply: _consumptionController.listen(_consumeValue); //theese are the same
}
//This method generates some values and puts them to stream
void consumeValue(int value) {
//Do something with the value
print('Value processed: $value');
}
//Don't forget to close your controllers
void dispose() {
_consumptionController.close();
}
}
Now, the task is to connect production stream to consumption sink. As you have correctly noticed, you do not want for any of two BLoCs to know anything about existence of the other one. So none of the two should hold references to the other one or even create instances of another one. Instead, you connect them using your Widget class:
//Define some widget to represent main screen of your application
class MainScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainScreenState();
}
//And define a state for this widget (state does not need to be public)
class _MainScreenState extends State<MainScreen> {
//You define both blocks here
ProducerBLoC _producer = new ProducerBLoC();
ConsumerBLoC _consumer = new ConsumerBLoC();
//Now, either do it in _MainScreenState constructor, or in the initState() method
#override
void initState() {
super.initState();
//Connect production stream with consumption sink
_producer.production.listen((value) => _consumer.consumption.add(value));
//Or, beautifully: _producer.production.pipe(_consumer.consumption);
}
#override
Widget build(BuildContext context) {
//The exact implementation does not matter in current context
}
//And don't forget to close your controllers
#override
dispose() {
super.dispose();
_producer.dispose();
_consumer.dispose();
}
}
This way, any value generated by ProducerBLoC will immediately be consumed by ConsumerBLoC. And, what's the most important, - both BLoCs are completely independent from one another!
The exact same way as you'd do with streams: Passing it as parameter
class Bloc {
final Sink<int> _external;
Bloc(this._external);
}

What is the difference between Stream<List<int>> and Stream<int> in Dart

I am trying to wrap my head around Dart Streams. In particular this example of the command line utility cat has the following lines of code:
Stream<List<int>> stream = new File(path).openRead();
// Transform the stream using a `StreamTransformer`. The transformers
// used here convert the data to UTF8 and split string values into
// individual lines.
return stream
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (showLineNumbers) {
stdout.write('${lineNumber++} ');
}
stdout.writeln(line);
}).asFuture().catchError((_) => _handleError(path));
The declaration of the Stream<T> as Stream<List<int>> has me a bit confused. Why is it not declared as a Stream<int>. How does the List<> type make this different. Are the subscriber events buffered in some way if it is a List?
What Type (as in <T>) is passed to the first transform? Is it an int or a List<int>?
What type is passed to each of the next transforms and what determines their type.
Does this example read the entire file before passing the results of the transform to the next transform? If so, is there an example somewhere of how to Stream very large files similar to this Node question Parsing huge logfiles in Node.js - read in line-by-line
Good question.
UTF8 is a Utf8Codec that extends Codec<String, List<int>>. So UTF8.decoder is a Converter<List<int>, String> that takes List<int> as parameter.
LineSplitter is a Converter<String, List<String>>. So it takes String as parameter. The resulting stream of .transform(const LineSplitter()) is a Stream<String> where each line is sent.
File.openRead doesn't read the entire file before writing the first bytes to the stream. So there's no problem to deal with large files.
Alexandre Ardhuin has the first three questions right. The 4th question however is not. After taking this apart and stubbing out my own version of the code I determined the following:
Even on a 37Mb file, the the transforms only get called once.
Here is the code I used to figure it out.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
void main(List<String> arguments) {
Stream<List<int>> stream = new File('Data.txt').openRead();
stream
.transform(const Utf8InterceptDecoder())
.transform(const LineSplitterIntercept())
.listen((line) {
// stdout.writeln(line);
}).asFuture().catchError((_) => print(_));
}
int lineSplitCount = 0;
class LineSplitterIntercept extends LineSplitter {
const LineSplitterIntercept() : super();
// Never gets called
List<String> convert(String data) {
stdout.writeln("LineSplitterIntercept.convert : Data:" + data);
return super.convert(data);
}
StringConversionSink startChunkedConversion(ChunkedConversionSink<String> sink) {
stdout.writeln("LineSplitterIntercept.startChunkedConversion Count:"+lineSplitCount.toString()+ " Sink: " + sink.toString());
lineSplitCount++;
return super.startChunkedConversion(sink);
}
}
int utfCount = 0;
class Utf8InterceptDecoder extends Utf8Decoder {
const Utf8InterceptDecoder() : super();
//never gets called
String convert(List<int> codeUnits) {
stdout.writeln("Utf8InterceptDecoder.convert : codeUnits.length:" + codeUnits.length.toString());
return super.convert(codeUnits);
}
ByteConversionSink startChunkedConversion(ChunkedConversionSink<String> sink) {
stdout.writeln("Utf8InterceptDecoder.startChunkedConversion Count:"+ utfCount.toString() + " Sink: "+ sink.toString());
utfCount++;
return super.startChunkedConversion(sink);
}
}

Resources