Unverified Commit 980880e2 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] remove package HTTP and add pub.dev head check tests (#75871)

parent af0ea8c3
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
import 'dart:async'; import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -19,8 +18,8 @@ import 'resident_runner.dart'; ...@@ -19,8 +18,8 @@ import 'resident_runner.dart';
/// An implementation of the devtools launcher that uses the server package. /// An implementation of the devtools launcher that uses the server package.
/// ///
/// This is implemented in isolated to prevent the flutter_tool from needing /// This is implemented in `isolated/` to prevent the flutter_tool from needing
/// a devtools dep in google3. /// a devtools dependency in google3.
class DevtoolsServerLauncher extends DevtoolsLauncher { class DevtoolsServerLauncher extends DevtoolsLauncher {
DevtoolsServerLauncher({ DevtoolsServerLauncher({
@required Platform platform, @required Platform platform,
...@@ -28,22 +27,26 @@ class DevtoolsServerLauncher extends DevtoolsLauncher { ...@@ -28,22 +27,26 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
@required String pubExecutable, @required String pubExecutable,
@required Logger logger, @required Logger logger,
@required PersistentToolState persistentToolState, @required PersistentToolState persistentToolState,
@visibleForTesting io.HttpClient httpClient,
}) : _processManager = processManager, }) : _processManager = processManager,
_pubExecutable = pubExecutable, _pubExecutable = pubExecutable,
_logger = logger, _logger = logger,
_platform = platform, _platform = platform,
_persistentToolState = persistentToolState; _persistentToolState = persistentToolState,
_httpClient = httpClient ?? io.HttpClient();
final ProcessManager _processManager; final ProcessManager _processManager;
final String _pubExecutable; final String _pubExecutable;
final Logger _logger; final Logger _logger;
final Platform _platform; final Platform _platform;
final PersistentToolState _persistentToolState; final PersistentToolState _persistentToolState;
final io.HttpClient _httpClient;
io.Process _devToolsProcess; io.Process _devToolsProcess;
static final RegExp _serveDevToolsPattern = static final RegExp _serveDevToolsPattern =
RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)'); RegExp(r'Serving DevTools at ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
static const String _pubHostedUrlKey = 'PUB_HOSTED_URL';
@override @override
Future<void> launch(Uri vmServiceUri) async { Future<void> launch(Uri vmServiceUri) async {
...@@ -51,15 +54,33 @@ class DevtoolsServerLauncher extends DevtoolsLauncher { ...@@ -51,15 +54,33 @@ class DevtoolsServerLauncher extends DevtoolsLauncher {
// this method is guaranteed not to return a Future that throws. // this method is guaranteed not to return a Future that throws.
try { try {
bool offline = false; bool offline = false;
bool useOverrideUrl = false;
try { try {
const String pubHostedUrlKey = 'PUB_HOSTED_URL'; Uri uri;
if (_platform.environment.containsKey(pubHostedUrlKey)) { if (_platform.environment.containsKey(_pubHostedUrlKey)) {
await http.head(Uri.parse(_platform.environment[pubHostedUrlKey])); useOverrideUrl = true;
uri = Uri.parse(_platform.environment[_pubHostedUrlKey]);
} else { } else {
await http.head(Uri.https('pub.dev', '')); uri = Uri.https('pub.dev', '');
}
final io.HttpClientRequest request = await _httpClient.headUrl(uri);
final io.HttpClientResponse response = await request.close();
await response.drain<void>();
if (response.statusCode != io.HttpStatus.ok) {
offline = true;
} }
} on Exception { } on Exception {
offline = true; offline = true;
} on ArgumentError {
if (!useOverrideUrl) {
rethrow;
}
// The user supplied a custom pub URL that was invalid, pretend to be offline
// and inform them that the URL was invalid.
offline = true;
_logger.printError(
'PUB_HOSTED_URL was set to an invalid URL: "${_platform.environment[_pubHostedUrlKey]}".'
);
} }
if (offline) { if (offline) {
......
...@@ -40,7 +40,7 @@ void main() { ...@@ -40,7 +40,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -67,7 +67,7 @@ void main() { ...@@ -67,7 +67,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -88,7 +88,7 @@ void main() { ...@@ -88,7 +88,7 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse( FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
headers: <String, List<String>>{ headers: <String, List<String>>{
'x-goog-hash': <String>[], 'x-goog-hash': <String>[],
} }
...@@ -100,7 +100,7 @@ void main() { ...@@ -100,7 +100,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -119,7 +119,7 @@ void main() { ...@@ -119,7 +119,7 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse( FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
body: <int>[0], body: <int>[0],
headers: <String, List<String>>{ headers: <String, List<String>>{
'x-goog-hash': <String>[ 'x-goog-hash': <String>[
...@@ -135,7 +135,7 @@ void main() { ...@@ -135,7 +135,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -154,7 +154,7 @@ void main() { ...@@ -154,7 +154,7 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse( FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
body: <int>[0], body: <int>[0],
headers: <String, List<String>>{ headers: <String, List<String>>{
'x-goog-hash': <String>[ 'x-goog-hash': <String>[
...@@ -163,7 +163,7 @@ void main() { ...@@ -163,7 +163,7 @@ void main() {
], ],
} }
)), )),
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse( FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
headers: <String, List<String>>{ headers: <String, List<String>>{
'x-goog-hash': <String>[ 'x-goog-hash': <String>[
'foo-bar-baz', 'foo-bar-baz',
...@@ -178,7 +178,7 @@ void main() { ...@@ -178,7 +178,7 @@ void main() {
await expectLater(() async => await artifactUpdater.downloadZipArchive( await expectLater(() async => await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
), throwsToolExit(message: 'k7iFrf4SQT9WfcQ==')); // validate that the hash mismatch message is included. ), throwsToolExit(message: 'k7iFrf4SQT9WfcQ==')); // validate that the hash mismatch message is included.
}); });
...@@ -197,8 +197,8 @@ void main() { ...@@ -197,8 +197,8 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), responseError: const HttpException('')), FakeRequest(Uri.parse('http://test.zip'), responseError: const HttpException('')),
FakeRequest(Uri.parse('http:///test.zip')), FakeRequest(Uri.parse('http://test.zip')),
]), ]),
tempStorage: fileSystem.currentDirectory.childDirectory('temp') tempStorage: fileSystem.currentDirectory.childDirectory('temp')
..createSync(), ..createSync(),
...@@ -206,7 +206,7 @@ void main() { ...@@ -206,7 +206,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
...@@ -224,8 +224,8 @@ void main() { ...@@ -224,8 +224,8 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)), FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)),
FakeRequest(Uri.parse('http:///test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)), FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)),
]), ]),
tempStorage: fileSystem.currentDirectory.childDirectory('temp') tempStorage: fileSystem.currentDirectory.childDirectory('temp')
..createSync(), ..createSync(),
...@@ -233,7 +233,7 @@ void main() { ...@@ -233,7 +233,7 @@ void main() {
await expectLater(() async => await artifactUpdater.downloadZipArchive( await expectLater(() async => await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
), throwsToolExit()); ), throwsToolExit());
...@@ -281,7 +281,7 @@ void main() { ...@@ -281,7 +281,7 @@ void main() {
operatingSystemUtils: operatingSystemUtils, operatingSystemUtils: operatingSystemUtils,
platform: testPlatform, platform: testPlatform,
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse('http:///test.zip'), responseError: ArgumentError()), FakeRequest(Uri.parse('http://test.zip'), responseError: ArgumentError()),
]), ]),
tempStorage: fileSystem.currentDirectory.childDirectory('temp') tempStorage: fileSystem.currentDirectory.childDirectory('temp')
..createSync(), ..createSync(),
...@@ -289,7 +289,7 @@ void main() { ...@@ -289,7 +289,7 @@ void main() {
await expectLater(() async => await artifactUpdater.downloadZipArchive( await expectLater(() async => await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
), throwsA(isA<ArgumentError>())); ), throwsA(isA<ArgumentError>()));
...@@ -314,7 +314,7 @@ void main() { ...@@ -314,7 +314,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -338,7 +338,7 @@ void main() { ...@@ -338,7 +338,7 @@ void main() {
await artifactUpdater.downloadZipArchive( await artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(logger.statusText, contains('test message')); expect(logger.statusText, contains('test message'));
...@@ -362,7 +362,7 @@ void main() { ...@@ -362,7 +362,7 @@ void main() {
expect(artifactUpdater.downloadZipArchive( expect(artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
), throwsA(isA<ToolExit>())); ), throwsA(isA<ToolExit>()));
expect(fileSystem.file('te,[/test'), isNot(exists)); expect(fileSystem.file('te,[/test'), isNot(exists));
...@@ -386,7 +386,7 @@ void main() { ...@@ -386,7 +386,7 @@ void main() {
expect(artifactUpdater.downloadZipArchive( expect(artifactUpdater.downloadZipArchive(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
), throwsA(isA<ToolExit>())); ), throwsA(isA<ToolExit>()));
expect(fileSystem.file('te,[/test'), isNot(exists)); expect(fileSystem.file('te,[/test'), isNot(exists));
...@@ -409,7 +409,7 @@ void main() { ...@@ -409,7 +409,7 @@ void main() {
await artifactUpdater.downloadZippedTarball( await artifactUpdater.downloadZippedTarball(
'test message', 'test message',
Uri.parse('http:///test.zip'), Uri.parse('http://test.zip'),
fileSystem.currentDirectory.childDirectory('out'), fileSystem.currentDirectory.childDirectory('out'),
); );
expect(fileSystem.file('out/test'), exists); expect(fileSystem.file('out/test'), exists);
......
...@@ -259,6 +259,7 @@ void main() { ...@@ -259,6 +259,7 @@ void main() {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest], requests: <VmServiceExpectation>[createDevFSRequest],
); );
setHttpAddress(Uri.parse('http://localhost'), fakeVmServiceHost.vmService);
final DevFS devFS = DevFS( final DevFS devFS = DevFS(
fakeVmServiceHost.vmService, fakeVmServiceHost.vmService,
......
...@@ -17,6 +17,7 @@ import 'package:flutter_tools/src/resident_runner.dart'; ...@@ -17,6 +17,7 @@ import 'package:flutter_tools/src/resident_runner.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/fake_http_client.dart';
void main() { void main() {
BufferLogger logger; BufferLogger logger;
...@@ -34,6 +35,65 @@ void main() { ...@@ -34,6 +35,65 @@ void main() {
); );
}); });
testWithoutContext('DevtoolsLauncher does not launch devtools if unable to reach pub.dev', () async {
final DevtoolsLauncher launcher = DevtoolsServerLauncher(
pubExecutable: 'pub',
logger: logger,
platform: platform,
persistentToolState: persistentToolState,
httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(
Uri.https('pub.dev', ''),
method: HttpMethod.head,
response: const FakeResponse(statusCode: HttpStatus.internalServerError),
),
]),
processManager: FakeProcessManager.list(<FakeCommand>[]),
);
final DevToolsServerAddress address = await launcher.serve();
expect(address, isNull);
});
testWithoutContext('DevtoolsLauncher pings PUB_HOSTED_URL instead of pub.dev for online check', () async {
final DevtoolsLauncher launcher = DevtoolsServerLauncher(
pubExecutable: 'pub',
logger: logger,
platform: FakePlatform(environment: <String, String>{
'PUB_HOSTED_URL': 'https://pub2.dev'
}),
persistentToolState: persistentToolState,
httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(
Uri.https('pub2.dev', ''),
method: HttpMethod.head,
response: const FakeResponse(statusCode: HttpStatus.internalServerError),
),
]),
processManager: FakeProcessManager.list(<FakeCommand>[]),
);
final DevToolsServerAddress address = await launcher.serve();
expect(address, isNull);
});
testWithoutContext('DevtoolsLauncher handles an invalid PUB_HOSTED_URL', () async {
final DevtoolsLauncher launcher = DevtoolsServerLauncher(
pubExecutable: 'pub',
logger: logger,
platform: FakePlatform(environment: <String, String>{
'PUB_HOSTED_URL': r'not_an_http_url'
}),
persistentToolState: persistentToolState,
httpClient: FakeHttpClient.list(<FakeRequest>[]),
processManager: FakeProcessManager.list(<FakeCommand>[]),
);
final DevToolsServerAddress address = await launcher.serve();
expect(address, isNull);
expect(logger.errorText, contains('PUB_HOSTED_URL was set to an invalid URL: "not_an_http_url".'));
});
testWithoutContext('DevtoolsLauncher launches DevTools through pub and saves the URI', () async { testWithoutContext('DevtoolsLauncher launches DevTools through pub and saves the URI', () async {
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
final DevtoolsLauncher launcher = DevtoolsServerLauncher( final DevtoolsLauncher launcher = DevtoolsServerLauncher(
...@@ -41,6 +101,7 @@ void main() { ...@@ -41,6 +101,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
...@@ -85,6 +146,7 @@ void main() { ...@@ -85,6 +146,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
...@@ -129,6 +191,7 @@ void main() { ...@@ -129,6 +191,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
...@@ -178,6 +241,7 @@ void main() { ...@@ -178,6 +241,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
...@@ -209,6 +273,7 @@ void main() { ...@@ -209,6 +273,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
...@@ -253,6 +318,7 @@ void main() { ...@@ -253,6 +318,7 @@ void main() {
logger: logger, logger: logger,
platform: platform, platform: platform,
persistentToolState: persistentToolState, persistentToolState: persistentToolState,
httpClient: FakeHttpClient.any(),
processManager: FakeProcessManager.list(<FakeCommand>[ processManager: FakeProcessManager.list(<FakeCommand>[
const FakeCommand( const FakeCommand(
command: <String>[ command: <String>[
......
...@@ -66,7 +66,7 @@ void main() { ...@@ -66,7 +66,7 @@ void main() {
final Testbed testbed = Testbed(); final Testbed testbed = Testbed();
await testbed.run(() async { await testbed.run(() async {
final HttpClient client = HttpClient(); final HttpClient client = HttpClient();
final HttpClientRequest request = await client.getUrl(null); final HttpClientRequest request = await client.getUrl(Uri.parse('http://foo.dev'));
final HttpClientResponse response = await request.close(); final HttpClientResponse response = await request.close();
expect(response.statusCode, HttpStatus.ok); expect(response.statusCode, HttpStatus.ok);
......
...@@ -263,6 +263,12 @@ class FakeHttpClient implements HttpClient { ...@@ -263,6 +263,12 @@ class FakeHttpClient implements HttpClient {
int _requestCount = 0; int _requestCount = 0;
_FakeHttpClientRequest _findRequest(HttpMethod method, Uri uri) { _FakeHttpClientRequest _findRequest(HttpMethod method, Uri uri) {
// Ensure the fake client throws similar errors to the real client.
if (uri.host.isEmpty) {
throw ArgumentError('No host specified in URI $uri');
} else if (uri.scheme != 'http' && uri.scheme != 'https') {
throw ArgumentError("Unsupported scheme '${uri.scheme}' in URI $uri");
}
final String methodString = _toMethodString(method); final String methodString = _toMethodString(method);
if (_any) { if (_any) {
return _FakeHttpClientRequest( return _FakeHttpClientRequest(
......
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