-
Notifications
You must be signed in to change notification settings - Fork 82
Daemon command boilerplate #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// Copyright (c) 2019, 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'; | ||
|
||
import 'package:args/command_runner.dart'; | ||
|
||
import '../daemon/daemon.dart'; | ||
|
||
Stream<Map<String, dynamic>> get _stdinCommandStream => stdin | ||
.transform<String>(utf8.decoder) | ||
.transform<String>(const LineSplitter()) | ||
.where((String line) => line.startsWith('[{') && line.endsWith('}]')) | ||
.map<Map<String, dynamic>>((String line) { | ||
line = line.substring(1, line.length - 1); | ||
return json.decode(line) as Map<String, dynamic>; | ||
}); | ||
|
||
void _stdoutCommandResponse(Map<String, dynamic> command) { | ||
stdout.writeln('[${json.encode(command)}]'); | ||
} | ||
|
||
/// A mode for running WebDev from command-line tools. | ||
/// | ||
/// Communication happens over STDIO using JSON-RPC. | ||
/// | ||
/// This supports a subset of: | ||
/// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/doc/daemon.md | ||
class DaemonCommand extends Command<int> { | ||
@override | ||
final name = 'daemon'; | ||
|
||
@override | ||
final hidden = true; | ||
|
||
@override | ||
String get description => | ||
'A mode for running WebDev from command-line tools.'; | ||
|
||
@override | ||
Future<int> run() async { | ||
var daemon = Daemon(_stdinCommandStream, _stdoutCommandResponse); | ||
await daemon.onExit; | ||
return 0; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Copyright (c) 2019, 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'; | ||
|
||
import 'daemon_domain.dart'; | ||
import 'domain.dart'; | ||
import 'utilites.dart'; | ||
|
||
/// A collection of domains. | ||
/// | ||
/// Listens for commands, routes them to the corresponding domain and provides | ||
/// the result. | ||
class Daemon { | ||
Daemon(Stream<Map<String, dynamic>> commandStream, this._sendCommand) { | ||
_registerDomain(DaemonDomain(this)); | ||
|
||
// TODO(grouma) - complete these other domains. | ||
//_registerDomain(appDomain = AppDomain(this)); | ||
//_registerDomain(deviceDomain = DeviceDomain(this)); | ||
//_registerDomain(emulatorDomain = EmulatorDomain(this)); | ||
|
||
_commandSubscription = commandStream.listen( | ||
_handleRequest, | ||
onDone: () { | ||
if (!_onExitCompleter.isCompleted) _onExitCompleter.complete(0); | ||
}, | ||
); | ||
} | ||
|
||
StreamSubscription<Map<String, dynamic>> _commandSubscription; | ||
|
||
final void Function(Map<String, dynamic>) _sendCommand; | ||
|
||
final Completer<int> _onExitCompleter = Completer<int>(); | ||
final Map<String, Domain> _domainMap = <String, Domain>{}; | ||
|
||
void _registerDomain(Domain domain) { | ||
_domainMap[domain.name] = domain; | ||
} | ||
|
||
Future<int> get onExit => _onExitCompleter.future; | ||
|
||
void _handleRequest(Map<String, dynamic> request) { | ||
// {id, method, params} | ||
|
||
// [id] is an opaque type to us. | ||
var id = request['id']; | ||
|
||
if (id == null) { | ||
stderr.writeln('no id for request: $request'); | ||
return; | ||
} | ||
|
||
try { | ||
var method = request['method'] as String ?? ''; | ||
if (!method.contains('.')) { | ||
throw ArgumentError('method not understood: $method'); | ||
} | ||
|
||
var domain = method.substring(0, method.indexOf('.')); | ||
var name = method.substring(method.indexOf('.') + 1); | ||
if (_domainMap[domain] == null) { | ||
throw ArgumentError('no domain for method: $method'); | ||
} | ||
|
||
_domainMap[domain].handleCommand( | ||
name, id, request['params'] as Map<String, dynamic> ?? {}); | ||
} catch (error, trace) { | ||
send(<String, dynamic>{ | ||
'id': id, | ||
'error': toJsonable(error), | ||
'trace': '$trace', | ||
}); | ||
} | ||
} | ||
|
||
void send(Map<String, dynamic> map) => _sendCommand(map); | ||
|
||
void shutdown({dynamic error}) { | ||
_commandSubscription?.cancel(); | ||
for (var domain in _domainMap.values) domain.dispose(); | ||
if (!_onExitCompleter.isCompleted) { | ||
if (error == null) { | ||
_onExitCompleter.complete(0); | ||
} else { | ||
_onExitCompleter.completeError(error); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright (c) 2019, 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'; | ||
|
||
import 'daemon.dart'; | ||
import 'domain.dart'; | ||
|
||
const String protocolVersion = '0.4.2'; | ||
|
||
/// A collection of method and events relevant to the daemon command. | ||
class DaemonDomain extends Domain { | ||
DaemonDomain(Daemon daemon) : super(daemon, 'daemon') { | ||
registerHandler('version', _version); | ||
registerHandler('shutdown', _shutdown); | ||
|
||
sendEvent( | ||
'daemon.connected', | ||
{ | ||
'version': protocolVersion, | ||
'pid': pid, | ||
}, | ||
); | ||
} | ||
|
||
Future<String> _version(Map<String, dynamic> args) { | ||
return Future<String>.value(protocolVersion); | ||
} | ||
|
||
Future<void> _shutdown(Map<String, dynamic> args) { | ||
// Schedule shutdown after we return the result. | ||
Timer.run(daemon.shutdown); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see if this can run right synchronously... my guess is its cruft from the flutter tools impl? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does also seem a bit sketchy that this is the thing shutting down the daemon even though it isn't creating it... but that part is probably OK. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to run it after we return the value. I'll add a comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could chain this off the future then with a cascade? Timer.run is a weird way to handle this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do in a follow up PR. |
||
return Future<void>.value(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) 2019, 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 'daemon.dart'; | ||
import 'utilites.dart'; | ||
|
||
/// Hosts a set of commands and events to be used with the Daemon. | ||
abstract class Domain { | ||
Domain(this.daemon, this.name); | ||
|
||
final Daemon daemon; | ||
final String name; | ||
final Map<String, CommandHandler> _handlers = <String, CommandHandler>{}; | ||
|
||
void registerHandler(String name, CommandHandler handler) { | ||
_handlers[name] = handler; | ||
} | ||
|
||
@override | ||
String toString() => name; | ||
|
||
void handleCommand(String command, dynamic id, Map<String, dynamic> args) { | ||
jakemac53 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Future<dynamic>.sync(() { | ||
if (_handlers.containsKey(command)) return _handlers[command](args); | ||
throw ArgumentError('command not understood: $name.$command'); | ||
}).then<dynamic>((dynamic result) { | ||
if (result == null) { | ||
_send(<String, dynamic>{'id': id}); | ||
} else { | ||
_send(<String, dynamic>{'id': id, 'result': toJsonable(result)}); | ||
} | ||
}).catchError((dynamic error, dynamic trace) { | ||
_send(<String, dynamic>{ | ||
'id': id, | ||
'error': toJsonable(error), | ||
'trace': '$trace', | ||
}); | ||
}); | ||
} | ||
|
||
void sendEvent(String name, [dynamic args]) { | ||
var map = <String, dynamic>{'event': name}; | ||
if (args != null) map['params'] = toJsonable(args); | ||
_send(map); | ||
} | ||
|
||
void _send(Map<String, dynamic> map) => daemon.send(map); | ||
|
||
void dispose() {} | ||
} | ||
|
||
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Copyright (c) 2019, 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. | ||
|
||
dynamic toJsonable(dynamic obj) { | ||
if (obj is String || | ||
obj is int || | ||
obj is bool || | ||
obj is Map<dynamic, dynamic> || | ||
obj is List<dynamic> || | ||
obj == null) return obj; | ||
return '$obj'; | ||
grouma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
String getStringArg(Map<String, dynamic> args, String name, | ||
{bool required = false}) { | ||
if (required && !args.containsKey(name)) | ||
throw ArgumentError('$name is required'); | ||
grouma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var val = args[name]; | ||
if (val != null && val is! String) { | ||
throw ArgumentError('$name is not a String'); | ||
} | ||
return val as String; | ||
} | ||
|
||
bool getBoolArg(Map<String, dynamic> args, String name, | ||
{bool required = false}) { | ||
if (required && !args.containsKey(name)) { | ||
throw ArgumentError('$name is required'); | ||
} | ||
var val = args[name]; | ||
if (val != null && val is! bool) throw ArgumentError('$name is not a bool'); | ||
return val as bool; | ||
} | ||
|
||
int getIntArg(Map<String, dynamic> args, String name, {bool required = false}) { | ||
if (required && !args.containsKey(name)) | ||
throw ArgumentError('$name is required'); | ||
var val = args[name]; | ||
if (val != null && val is! int) throw ArgumentError('$name is not an int'); | ||
return val as int; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright (c) 2019, 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 'package:path/path.dart' as p; | ||
import 'package:test/test.dart'; | ||
import 'package:test_process/test_process.dart'; | ||
import 'package:webdev/src/util.dart'; | ||
|
||
import '../test_utils.dart'; | ||
|
||
void main() { | ||
String exampleDirectory; | ||
|
||
setUpAll(() async { | ||
exampleDirectory = p.absolute(p.join(p.current, '..', 'example')); | ||
|
||
var process = await TestProcess.start(pubPath, ['upgrade'], | ||
workingDirectory: exampleDirectory, environment: getPubEnvironment()); | ||
|
||
await process.shouldExit(0); | ||
}); | ||
|
||
group('Daemon', () { | ||
group('Events', () { | ||
test('.connected', () async { | ||
var webdev = | ||
await runWebDev(['daemon'], workingDirectory: exampleDirectory); | ||
await expectLater( | ||
webdev.stdout, emits(startsWith('[{"event":"daemon.connected"'))); | ||
await webdev.kill(); | ||
}); | ||
}); | ||
|
||
test('.version', () async { | ||
var webdev = | ||
await runWebDev(['daemon'], workingDirectory: exampleDirectory); | ||
webdev.stdin.add(utf8.encode('[{"method":"daemon.version","id":0}]\n')); | ||
await expectLater( | ||
webdev.stdout, emitsThrough(equals('[{"id":0,"result":"0.4.2"}]'))); | ||
await webdev.kill(); | ||
}); | ||
|
||
test('.shutdown', () async { | ||
var webdev = | ||
await runWebDev(['daemon'], workingDirectory: exampleDirectory); | ||
webdev.stdin.add(utf8.encode('[{"method":"daemon.shutdown","id":0}]\n')); | ||
await expectLater(webdev.stdout, emitsThrough(equals('[{"id":0}]'))); | ||
expect(await webdev.exitCode, equals(0)); | ||
}); | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.