Unverified Commit 75a44a29 authored by Ben Konyi's avatar Ben Konyi Committed by GitHub

[ Web ] Register service extensions with DDS, not DWDS (#79479)

parent 437e32b9
...@@ -17,6 +17,7 @@ import 'package:mime/mime.dart' as mime; ...@@ -17,6 +17,7 @@ import 'package:mime/mime.dart' as mime;
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf;
import 'package:vm_service/vm_service.dart' as vm_service;
import '../artifacts.dart'; import '../artifacts.dart';
import '../asset.dart'; import '../asset.dart';
...@@ -35,6 +36,7 @@ import '../dart/package_map.dart'; ...@@ -35,6 +36,7 @@ import '../dart/package_map.dart';
import '../devfs.dart'; import '../devfs.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../vmservice.dart';
import '../web/bootstrap.dart'; import '../web/bootstrap.dart';
import '../web/chrome.dart'; import '../web/chrome.dart';
import '../web/compile.dart'; import '../web/compile.dart';
...@@ -592,10 +594,11 @@ class WebAssetServer implements AssetReader { ...@@ -592,10 +594,11 @@ class WebAssetServer implements AssetReader {
} }
class ConnectionResult { class ConnectionResult {
ConnectionResult(this.appConnection, this.debugConnection); ConnectionResult(this.appConnection, this.debugConnection, this.vmService);
final AppConnection appConnection; final AppConnection appConnection;
final DebugConnection debugConnection; final DebugConnection debugConnection;
final vm_service.VmService vmService;
} }
/// The web specific DevFS implementation. /// The web specific DevFS implementation.
...@@ -665,8 +668,12 @@ class WebDevFS implements DevFS { ...@@ -665,8 +668,12 @@ class WebDevFS implements DevFS {
if (firstConnection.isCompleted) { if (firstConnection.isCompleted) {
appConnection.runMain(); appConnection.runMain();
} else { } else {
final vm_service.VmService vmService = await createVmServiceDelegate(
Uri.parse(debugConnection.uri),
logger: globals.logger,
);
firstConnection firstConnection
.complete(ConnectionResult(appConnection, debugConnection)); .complete(ConnectionResult(appConnection, debugConnection, vmService));
} }
} on Exception catch (error, stackTrace) { } on Exception catch (error, stackTrace) {
if (!firstConnection.isCompleted) { if (!firstConnection.isCompleted) {
......
...@@ -155,7 +155,7 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -155,7 +155,7 @@ class ResidentWebRunner extends ResidentRunner {
if (_instance != null) { if (_instance != null) {
return _instance; return _instance;
} }
final vmservice.VmService service =_connectionResult?.debugConnection?.vmService; final vmservice.VmService service =_connectionResult?.vmService;
final Uri websocketUri = Uri.parse(_connectionResult.debugConnection.uri); final Uri websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
final Uri httpUri = _httpUriFromWebsocketUri(websocketUri); final Uri httpUri = _httpUriFromWebsocketUri(websocketUri);
return _instance ??= FlutterVmService(service, wsAddress: websocketUri, httpAddress: httpUri); return _instance ??= FlutterVmService(service, wsAddress: websocketUri, httpAddress: httpUri);
...@@ -835,7 +835,7 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -835,7 +835,7 @@ class ResidentWebRunner extends ResidentRunner {
_connectionResult.appConnection.runMain(); _connectionResult.appConnection.runMain();
} else { } else {
StreamSubscription<void> resumeSub; StreamSubscription<void> resumeSub;
resumeSub = _connectionResult.debugConnection.vmService.onDebugEvent resumeSub = _vmService.service.onDebugEvent
.listen((vmservice.Event event) { .listen((vmservice.Event event) {
if (event.type == vmservice.EventKind.kResume) { if (event.type == vmservice.EventKind.kResume) {
_connectionResult.appConnection.runMain(); _connectionResult.appConnection.runMain();
......
...@@ -326,6 +326,22 @@ Future<FlutterVmService> connectToVmService( ...@@ -326,6 +326,22 @@ Future<FlutterVmService> connectToVmService(
); );
} }
Future<vm_service.VmService> createVmServiceDelegate(
Uri wsUri, {
io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
@required Logger logger,
}) async {
final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger);
return vm_service.VmService(
channel,
channel.add,
log: null,
disposeHandler: () async {
await channel.close();
},
);
}
Future<FlutterVmService> _connect( Future<FlutterVmService> _connect(
Uri httpUri, { Uri httpUri, {
ReloadSources reloadSources, ReloadSources reloadSources,
...@@ -338,14 +354,8 @@ Future<FlutterVmService> _connect( ...@@ -338,14 +354,8 @@ Future<FlutterVmService> _connect(
@required Logger logger, @required Logger logger,
}) async { }) async {
final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws')); final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws'));
final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger); final vm_service.VmService delegateService = await createVmServiceDelegate(
final vm_service.VmService delegateService = vm_service.VmService( wsUri, compression: compression, logger: logger,
channel,
channel.add,
log: null,
disposeHandler: () async {
await channel.close();
},
); );
final vm_service.VmService service = await setUpVmService( final vm_service.VmService service = await setUpVmService(
......
...@@ -130,7 +130,11 @@ void main() { ...@@ -130,7 +130,11 @@ void main() {
when(mockFlutterDevice.devFS).thenReturn(mockWebDevFS); when(mockFlutterDevice.devFS).thenReturn(mockWebDevFS);
when(mockFlutterDevice.device).thenReturn(mockDevice); when(mockFlutterDevice.device).thenReturn(mockDevice);
when(mockWebDevFS.connect(any)).thenAnswer((Invocation invocation) async { when(mockWebDevFS.connect(any)).thenAnswer((Invocation invocation) async {
return ConnectionResult(mockAppConnection, mockDebugConnection); return ConnectionResult(
mockAppConnection,
mockDebugConnection,
mockDebugConnection.vmService,
);
}); });
fileSystem.file('.packages').writeAsStringSync('\n'); fileSystem.file('.packages').writeAsStringSync('\n');
}); });
......
...@@ -63,6 +63,7 @@ abstract class FlutterTestDriver { ...@@ -63,6 +63,7 @@ abstract class FlutterTestDriver {
Stream<String> get stdout => _stdout.stream; Stream<String> get stdout => _stdout.stream;
int get vmServicePort => _vmServiceWsUri.port; int get vmServicePort => _vmServiceWsUri.port;
bool get hasExited => _hasExited; bool get hasExited => _hasExited;
Uri get vmServiceWsUri => _vmServiceWsUri;
String lastTime = ''; String lastTime = '';
void _debugPrint(String message, { String topic = '' }) { void _debugPrint(String message, { String topic = '' }) {
...@@ -219,7 +220,7 @@ abstract class FlutterTestDriver { ...@@ -219,7 +220,7 @@ abstract class FlutterTestDriver {
return _flutterIsolateId; return _flutterIsolateId;
} }
Future<Isolate> _getFlutterIsolate() async { Future<Isolate> getFlutterIsolate() async {
final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId()); final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId());
return isolate; return isolate;
} }
...@@ -281,7 +282,7 @@ abstract class FlutterTestDriver { ...@@ -281,7 +282,7 @@ abstract class FlutterTestDriver {
// Cancel the subscription on either of the above. // Cancel the subscription on either of the above.
await pauseSubscription.cancel(); await pauseSubscription.cancel();
return _getFlutterIsolate(); return getFlutterIsolate();
}, },
task: 'Waiting for isolate to pause', task: 'Waiting for isolate to pause',
); );
...@@ -294,7 +295,7 @@ abstract class FlutterTestDriver { ...@@ -294,7 +295,7 @@ abstract class FlutterTestDriver {
Future<Isolate> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause); Future<Isolate> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause);
Future<bool> isAtAsyncSuspension() async { Future<bool> isAtAsyncSuspension() async {
final Isolate isolate = await _getFlutterIsolate(); final Isolate isolate = await getFlutterIsolate();
return isolate.pauseEvent.atAsyncSuspension == true; return isolate.pauseEvent.atAsyncSuspension == true;
} }
......
...@@ -13,160 +13,163 @@ import '../integration.shard/test_driver.dart'; ...@@ -13,160 +13,163 @@ import '../integration.shard/test_driver.dart';
import '../integration.shard/test_utils.dart'; import '../integration.shard/test_utils.dart';
import '../src/common.dart'; import '../src/common.dart';
void batch1() { void main() {
final BasicProject _project = BasicProject(); group('Flutter run for web', () {
final BasicProject project = BasicProject();
Directory tempDir; Directory tempDir;
FlutterRunTestDriver _flutter; FlutterRunTestDriver flutter;
Future<void> initProject() async { setUp(() async {
tempDir = createResolvedTempDirectorySync('run_expression_eval_test.'); tempDir = createResolvedTempDirectorySync('run_expression_eval_test.');
await _project.setUpIn(tempDir); await project.setUpIn(tempDir);
_flutter = FlutterRunTestDriver(tempDir); flutter = FlutterRunTestDriver(tempDir);
} });
Future<void> cleanProject() async { tearDown(() async {
await _flutter.stop(); await flutter.stop();
tryToDelete(tempDir); tryToDelete(tempDir);
} });
Future<void> start({bool expressionEvaluation}) { Future<void> start({bool expressionEvaluation}) async {
// The non-test project has a loop around its breakpoints. // The non-test project has a loop around its breakpoints.
// No need to start paused as all breakpoint would be eventually reached. // No need to start paused as all breakpoint would be eventually reached.
return _flutter.run( await flutter.run(
withDebugger: true, chrome: true, withDebugger: true, chrome: true,
expressionEvaluation: expressionEvaluation, expressionEvaluation: expressionEvaluation,
additionalCommandArgs: <String>['--verbose']); additionalCommandArgs: <String>['--verbose']);
} }
Future<void> breakInBuildMethod(FlutterTestDriver flutter) async { Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
await _flutter.breakAt( await flutter.breakAt(
_project.buildMethodBreakpointUri, project.buildMethodBreakpointUri,
_project.buildMethodBreakpointLine, project.buildMethodBreakpointLine,
); );
} }
Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async { Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
await _flutter.breakAt( await flutter.breakAt(
_project.topLevelFunctionBreakpointUri, project.topLevelFunctionBreakpointUri,
_project.topLevelFunctionBreakpointLine, project.topLevelFunctionBreakpointLine,
); );
} }
testWithoutContext('flutter run expression evaluation - error if expression evaluation disabled', () async { testWithoutContext('cannot evaluate expression if feature is disabled', () async {
await initProject();
await start(expressionEvaluation: false); await start(expressionEvaluation: false);
await breakInTopLevelFunction(_flutter); await breakInTopLevelFunction(flutter);
await failToEvaluateExpression(_flutter); await failToEvaluateExpression(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - no native javascript objects in static scope', () async { testWithoutContext('shows no native javascript objects in static scope', () async {
await initProject();
await start(expressionEvaluation: true); await start(expressionEvaluation: true);
await breakInTopLevelFunction(_flutter); await breakInTopLevelFunction(flutter);
await checkStaticScope(_flutter); await checkStaticScope(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - can handle compilation errors', () async { testWithoutContext('can handle compilation errors', () async {
await initProject();
await start(expressionEvaluation: true); await start(expressionEvaluation: true);
await breakInTopLevelFunction(_flutter); await breakInTopLevelFunction(flutter);
await evaluateErrorExpressions(_flutter); await evaluateErrorExpressions(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - can evaluate trivial expressions in top level function', () async { testWithoutContext('can evaluate trivial expressions in top level function', () async {
await initProject();
await start(expressionEvaluation: true); await start(expressionEvaluation: true);
await breakInTopLevelFunction(_flutter); await breakInTopLevelFunction(flutter);
await evaluateTrivialExpressions(_flutter); await evaluateTrivialExpressions(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - can evaluate trivial expressions in build method', () async { testWithoutContext('can evaluate trivial expressions in build method', () async {
await initProject();
await start(expressionEvaluation: true); await start(expressionEvaluation: true);
await breakInBuildMethod(_flutter); await breakInBuildMethod(flutter);
await evaluateTrivialExpressions(_flutter); await evaluateTrivialExpressions(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - can evaluate complex expressions in top level function', () async { testWithoutContext('can evaluate complex expressions in top level function', () async {
await initProject();
await start(expressionEvaluation: true); await start(expressionEvaluation: true);
await breakInTopLevelFunction(_flutter); await breakInTopLevelFunction(flutter);
await evaluateComplexExpressions(_flutter); await evaluateComplexExpressions(flutter);
await cleanProject();
}); });
testWithoutContext('flutter run expression evaluation - can evaluate complex expressions in build method', () async { testWithoutContext('can evaluate complex expressions in build method', () async {
await initProject(); await start(expressionEvaluation: true);
await _flutter.run(withDebugger: true, chrome: true); await breakInBuildMethod(flutter);
await breakInBuildMethod(_flutter); await evaluateComplexExpressions(flutter);
await evaluateComplexExpressions(_flutter); });
await cleanProject();
testWithoutContext('can evaluate trivial expressions in library without pause', () async {
await start(expressionEvaluation: true);
await evaluateTrivialExpressionsInLibrary(flutter);
}); });
}
void batch2() { testWithoutContext('can evaluate complex expressions in library without pause', () async {
final TestsProject _project = TestsProject(); await start(expressionEvaluation: true);
await evaluateComplexExpressionsInLibrary(flutter);
});
});
group('Flutter test for web', () {
final TestsProject project = TestsProject();
Directory tempDir; Directory tempDir;
FlutterRunTestDriver _flutter; FlutterRunTestDriver flutter;
Future<void> initProject() async { setUp(() async {
tempDir = createResolvedTempDirectorySync('test_expression_eval_test.'); tempDir = createResolvedTempDirectorySync('run_expression_eval_test.');
await _project.setUpIn(tempDir); await project.setUpIn(tempDir);
_flutter = FlutterRunTestDriver(tempDir); flutter = FlutterRunTestDriver(tempDir);
} });
Future<void> cleanProject() async { tearDown(() async {
await _flutter.stop(); await flutter.stop();
tryToDelete(tempDir); tryToDelete(tempDir);
} });
Future<void> breakInMethod(FlutterTestDriver flutter) async { Future<Isolate> breakInMethod(FlutterTestDriver flutter) async {
await _flutter.addBreakpoint( await flutter.addBreakpoint(
_project.breakpointAppUri, project.breakpointAppUri,
_project.breakpointLine, project.breakpointLine,
); );
await _flutter.resume(); await flutter.resume();
await _flutter.waitForPause(); return flutter.waitForPause();
} }
Future<void> startPaused({bool expressionEvaluation}) { Future<void> startPaused({bool expressionEvaluation}) {
// The test project does not have a loop around its breakpoints. // The test project does not have a loop around its breakpoints.
// Start paused so we can set a breakpoint before passing it // Start paused so we can set a breakpoint before passing it
// in the execution. // in the execution.
return _flutter.run( return flutter.run(
withDebugger: true, chrome: true, withDebugger: true, chrome: true,
expressionEvaluation: expressionEvaluation, expressionEvaluation: expressionEvaluation,
startPaused: true, script: _project.testFilePath, startPaused: true, script: project.testFilePath,
additionalCommandArgs: <String>['--verbose']); additionalCommandArgs: <String>['--verbose']);
} }
testWithoutContext('flutter test expression evaluation - error if expression evaluation disabled', () async { testWithoutContext('cannot evaluate expressions if feature is disabled', () async {
await initProject();
await startPaused(expressionEvaluation: false); await startPaused(expressionEvaluation: false);
await breakInMethod(_flutter); await breakInMethod(flutter);
await failToEvaluateExpression(_flutter); await failToEvaluateExpression(flutter);
await cleanProject(); });
testWithoutContext('can evaluate trivial expressions in a test', () async {
await startPaused(expressionEvaluation: true);
await breakInMethod(flutter);
await evaluateTrivialExpressions(flutter);
});
testWithoutContext('can evaluate complex expressions in a test', () async {
await startPaused(expressionEvaluation: true);
await breakInMethod(flutter);
await evaluateComplexExpressions(flutter);
}); });
testWithoutContext('flutter test expression evaluation - can evaluate trivial expressions in a test', () async { testWithoutContext('can evaluate trivial expressions in library without pause', () async {
await initProject();
await startPaused(expressionEvaluation: true); await startPaused(expressionEvaluation: true);
await breakInMethod(_flutter); await evaluateTrivialExpressionsInLibrary(flutter);
await evaluateTrivialExpressions(_flutter);
await cleanProject();
}); });
testWithoutContext('flutter test expression evaluation - can evaluate complex expressions in a test', () async { testWithoutContext('can evaluate complex expressions in library without pause', () async {
await initProject();
await startPaused(expressionEvaluation: true); await startPaused(expressionEvaluation: true);
await breakInMethod(_flutter); await evaluateComplexExpressionsInLibrary(flutter);
await evaluateComplexExpressions(_flutter); });
await cleanProject();
}); });
} }
...@@ -208,6 +211,28 @@ Future<void> evaluateComplexExpressions(FlutterTestDriver flutter) async { ...@@ -208,6 +211,28 @@ Future<void> evaluateComplexExpressions(FlutterTestDriver flutter) async {
expectInstance(res, InstanceKind.kDouble, DateTime.now().year.toString()); expectInstance(res, InstanceKind.kDouble, DateTime.now().year.toString());
} }
Future<void> evaluateTrivialExpressionsInLibrary(FlutterTestDriver flutter) async {
final LibraryRef library = await getRootLibrary(flutter);
final ObjRef res = await flutter.evaluate(library.id, '"test"');
expectInstance(res, InstanceKind.kString, 'test');
}
Future<void> evaluateComplexExpressionsInLibrary(FlutterTestDriver flutter) async {
final LibraryRef library = await getRootLibrary(flutter);
final ObjRef res = await flutter.evaluate(library.id, 'new DateTime.now().year');
expectInstance(res, InstanceKind.kDouble, DateTime.now().year.toString());
}
Future<LibraryRef> getRootLibrary(FlutterTestDriver flutter) async {
// `isolate.rootLib` returns incorrect library, so find the
// entrypoint manually here instead.
//
// Issue: https://github.com/dart-lang/sdk/issues/44760
final Isolate isolate = await flutter.getFlutterIsolate();
return isolate.libraries
.firstWhere((LibraryRef l) => l.uri.contains('org-dartlang-app'));
}
void expectInstance(ObjRef result, String kind, String message) { void expectInstance(ObjRef result, String kind, String message) {
expect(result, expect(result,
const TypeMatcher<InstanceRef>() const TypeMatcher<InstanceRef>()
...@@ -220,8 +245,3 @@ void expectError(ObjRef result, String message) { ...@@ -220,8 +245,3 @@ void expectError(ObjRef result, String message) {
const TypeMatcher<ErrorRef>() const TypeMatcher<ErrorRef>()
.having((ErrorRef instance) => instance.message, 'message', message)); .having((ErrorRef instance) => instance.message, 'message', message));
} }
void main() {
batch1();
batch2();
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:async';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import '../integration.shard/test_data/basic_project.dart';
import '../integration.shard/test_driver.dart';
import '../integration.shard/test_utils.dart';
import '../src/common.dart';
void main() {
Directory tempDir;
final BasicProjectWithUnaryMain project = BasicProjectWithUnaryMain();
FlutterRunTestDriver flutter;
group('Clients of flutter run on web with DDS enabled', () {
setUp(() async {
tempDir = createResolvedTempDirectorySync('run_test.');
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir, spawnDdsInstance: true);
});
tearDown(() async {
await flutter.stop();
tryToDelete(tempDir);
});
testWithoutContext('can validate flutter version', () async {
await flutter.run(
withDebugger: true, chrome: true,
additionalCommandArgs: <String>['--verbose']);
expect(flutter.vmServiceWsUri, isNotNull);
final VmService client =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
await validateFlutterVersion(client);
});
testWithoutContext('can validate flutter version in parallel', () async {
await flutter.run(
withDebugger: true, chrome: true,
additionalCommandArgs: <String>['--verbose']);
expect(flutter.vmServiceWsUri, isNotNull);
final VmService client1 =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
final VmService client2 =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
await Future.wait(<Future<void>>[
validateFlutterVersion(client1),
validateFlutterVersion(client2)]
);
}, skip: 'DDS failure: https://github.com/dart-lang/sdk/issues/45569');
});
group('Clients of flutter run on web with DDS disabled', () {
setUp(() async {
tempDir = createResolvedTempDirectorySync('run_test.');
await project.setUpIn(tempDir);
flutter = FlutterRunTestDriver(tempDir, spawnDdsInstance: false);
});
tearDown(() async {
await flutter.stop();
tryToDelete(tempDir);
});
testWithoutContext('can validate flutter version', () async {
await flutter.run(
withDebugger: true, chrome: true,
additionalCommandArgs: <String>['--verbose']);
expect(flutter.vmServiceWsUri, isNotNull);
final VmService client =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
await validateFlutterVersion(client);
});
testWithoutContext('can validate flutter version in parallel', () async {
await flutter.run(
withDebugger: true, chrome: true,
additionalCommandArgs: <String>['--verbose']);
expect(flutter.vmServiceWsUri, isNotNull);
final VmService client1 =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
final VmService client2 =
await vmServiceConnectUri('${flutter.vmServiceWsUri}');
await Future.wait(<Future<void>>[
validateFlutterVersion(client1),
validateFlutterVersion(client2)]
);
});
});
}
Future<void> validateFlutterVersion(VmService client) async {
String method;
final Future<dynamic> registration = expectLater(
client.onEvent('Service'),
emitsThrough(predicate((Event e) {
if (e.kind == EventKind.kServiceRegistered &&
e.service == 'flutterVersion') {
method = e.method;
return true;
}
return false;
}))
);
await client.streamListen('Service');
await registration;
await client.streamCancel('Service');
final dynamic version1 = await client.callServiceExtension(method);
expect(version1, const TypeMatcher<Success>()
.having((Success r) => r.type, 'type', 'Success')
.having((Success r) => r.json['frameworkVersion'], 'frameworkVersion', isNotNull));
await client.dispose();
}
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