Skip to content

App Domain .callServiceExtension .restart .log #212

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

Merged
merged 13 commits into from
Mar 18, 2019
8 changes: 7 additions & 1 deletion example/web/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// 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:developer';

void main() {
print('Hello World!');
registerExtension('ext.print', (_, __) async {
print('Hello World');
return ServiceExtensionResponse.result(json.encode({'success': true}));
});
}
45 changes: 40 additions & 5 deletions webdev/lib/src/daemon/app_domain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:dwds/service.dart';
import 'package:vm_service_lib/vm_service_lib.dart';

import '../serve/chrome.dart';
import '../serve/debugger/webdev_vm_client.dart';
Expand All @@ -17,6 +19,7 @@ import 'utilites.dart';
/// A collection of method and events relevant to the running application.
class AppDomain extends Domain {
String _appId;
VmService _vmService;
WebdevVmClient _webdevVmClient;
DebugService _debugService;
bool _isShutdown = false;
Expand All @@ -35,6 +38,8 @@ class AppDomain extends Domain {
_debugService =
await devHandler.startDebugService(chrome.chromeConnection, _appId);
_webdevVmClient = await WebdevVmClient.create(_debugService);
_vmService = _webdevVmClient.client;
await _vmService.streamListen('Stdout');
sendEvent('app.debugPort', {
'appId': _appId,
'port': _debugService.port,
Expand All @@ -43,6 +48,13 @@ class AppDomain extends Domain {
sendEvent('app.started', {
'appId': _appId,
});
_vmService.onStdoutEvent.listen((log) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this next to the streamListen?

sendEvent('app.log', {
'appId': _appId,
'log': utf8.decode(base64.decode(log.bytes)),
});
});

// Shutdown could have been triggered while awaiting above.
// ignore: invariant_booleans
if (_isShutdown) dispose();
Expand All @@ -56,17 +68,40 @@ class AppDomain extends Domain {
_initialize(serverManager);
}

Future<String> _callServiceExtension(Map<String, dynamic> args) {
throw UnimplementedError();
Future<Map<String, dynamic>> _callServiceExtension(
Map<String, dynamic> args) async {
var appId = getStringArg(args, 'appId', required: true);
if (_appId != appId) throw ArgumentError.value(appId, 'appId', 'Not found');
var methodName = getStringArg(args, 'methodName', required: true);
var params = args['params'] != null
? (args['params'] as Map<String, dynamic>)
: <String, dynamic>{};
var response =
await _vmService.callServiceExtension(methodName, args: params);
return response.json;
}

Future<String> _restart(Map<String, dynamic> args) async {
throw UnimplementedError();
Future<Map<String, dynamic>> _restart(Map<String, dynamic> args) async {
var appId = getStringArg(args, 'appId', required: true);
if (_appId != appId) throw ArgumentError.value(appId, 'appId', 'Not found');
// TODO(grouma) - Figure out what fullRestart means in this context.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, full restart was the old name of hot restart. So, fullRestart==false is hot reload, and fullRestart==true is hot restart.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the details. I updated the code to reflect our current support.

// For now we will ignore.
// var fullRestart = getBoolArg(args, 'fullRestart') ?? false;
var pauseAfterRestart = getBoolArg(args, 'pause') ?? false;
if (pauseAfterRestart) {
Copy link
Member

@devoncarew devoncarew Mar 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may not want to throw here - we'd have to investigate. The IDEs will be sending it in when you start the app in a debug launch (as opposed to a 'run', w/o breakpoints enabled).

If the IDE does pass in pauseAfterRestart, it will wait until it gets a PausePostRequest event, remove all breakpoints, re-set all breakpoints, then resume the isolate. It does this to ensure that the new scripts, representing libraries that had been reloaded, have the correct breakpoints set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth seeing what happens here first before doing a lot of work however.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I'll leave it as is and wait to hear back from @jwren and @DanTup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try this out Monday, but in VS Code we do always send pause when running in debug mode (to do what Devon describes above). The reason for this is that we need to re-send breakpoints after a hot restart (they got dropped) or hot reload (the code they were attached to may have been replaced with new code), and if we don't pause then we'll have race conditions (the code may have already run before we've sent the breakpoint).

throw ArgumentError.value(
pauseAfterRestart, 'pauseAfterRestart', 'Not supported.');
}
var response = await _vmService.callServiceExtension('hotRestart');
return {
'code': response.type == 'Success' ? 0 : 1,
'message': response.toString()
};
}

Future<bool> _stop(Map<String, dynamic> args) async {
var appId = getStringArg(args, 'appId', required: true);
if (_appId != appId) throw ArgumentError("app '$appId' not found");
if (_appId != appId) throw ArgumentError.value(appId, 'appId', 'Not found');
var chrome = await Chrome.connectedInstance;
await chrome.close();
return true;
Expand Down
6 changes: 3 additions & 3 deletions webdev/lib/src/serve/debugger/webdev_vm_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import 'package:vm_service_lib/vm_service_lib.dart';
// A client of the vm service that registers some custom extensions like
// hotRestart.
class WebdevVmClient {
final VmService _client;
final VmService client;
final StreamController<Map<String, Object>> _requestController;
final StreamController<Map<String, Object>> _responseController;

WebdevVmClient(
this._client, this._requestController, this._responseController);
this.client, this._requestController, this._responseController);

Future<void> close() async {
await _requestController.close();
await _responseController.close();
_client.dispose();
client.dispose();
}

static Future<WebdevVmClient> create(DebugService debugService) async {
Expand Down
48 changes: 47 additions & 1 deletion webdev/test/daemon/app_domain_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@
// BSD-style license that can be found in the LICENSE file.

@Timeout(Duration(minutes: 2))
@Tags(['requires-edge-sdk'])

import 'dart:async';
import 'dart:convert';

@Tags(['requires-edge-sdk'])
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';

import '../test_utils.dart';
import 'utils.dart';

Future<String> _getAppId(TestProcess webdev) async {
var appId = '';
while (await webdev.stdout.hasNext) {
var line = await webdev.stdout.next;
if (line.startsWith('[{"event":"app.started"')) {
line = line.substring(1, line.length - 1);
var message = json.decode(line) as Map<String, dynamic>;
appId = message['params']['appId'] as String;
break;
}
}
assert(appId.isNotEmpty);
return appId;
}

void main() {
String exampleDirectory;

Expand Down Expand Up @@ -43,5 +62,32 @@ void main() {
await exitWebdev(webdev);
});
});

group('Methods', () {
test('.callServiceExtension', () async {
var webdev =
await runWebDev(['daemon'], workingDirectory: exampleDirectory);
var appId = await _getAppId(webdev);
var extensionCall = '[{"method":"app.callServiceExtension","id":0,'
'"params" : { "appId" : "$appId", "methodName" : "ext.print"}}]';
webdev.stdin.add(utf8.encode('$extensionCall\n'));
// The example app sets up a service extension for printing.
await expectLater(
webdev.stdout, emitsThrough(startsWith('[{"event":"app.log"')));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could easily pass for a different reason than the extension being called successfully (any log happening), I would check a bit more about the event if you can (check specifically for the expected print log line)

await exitWebdev(webdev);
});

test('.restart', () async {
var webdev =
await runWebDev(['daemon'], workingDirectory: exampleDirectory);
var appId = await _getAppId(webdev);
var extensionCall = '[{"method":"app.restart","id":0,'
'"params" : { "appId" : "$appId"}}]';
webdev.stdin.add(utf8.encode('$extensionCall\n'));
await expectLater(webdev.stdout,
emitsThrough(startsWith('[{"id":0,"result":{"code":0')));
await exitWebdev(webdev);
});
});
}, tags: ['webdriver']);
}
31 changes: 17 additions & 14 deletions webdev/test/daemon/daemon_domain_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@ void main() {
});
});

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 exitWebdev(webdev);
});
group('Methods', () {
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 exitWebdev(webdev);
});

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));
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));
});
});
}, tags: ['webdriver']);
}