Unverified Commit 296eeb59 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Migrate custom_device to null safety (#92955)

parent f4d1bfed
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.8
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../application_package.dart'; import '../application_package.dart';
...@@ -103,12 +100,12 @@ class CustomDeviceLogReader extends DeviceLogReader { ...@@ -103,12 +100,12 @@ class CustomDeviceLogReader extends DeviceLogReader {
/// A [DevicePortForwarder] that uses commands to forward / unforward a port. /// A [DevicePortForwarder] that uses commands to forward / unforward a port.
class CustomDevicePortForwarder extends DevicePortForwarder { class CustomDevicePortForwarder extends DevicePortForwarder {
CustomDevicePortForwarder({ CustomDevicePortForwarder({
@required String deviceName, required String deviceName,
@required List<String> forwardPortCommand, required List<String> forwardPortCommand,
@required RegExp forwardPortSuccessRegex, required RegExp forwardPortSuccessRegex,
this.numTries/*?*/, this.numTries,
@required ProcessManager processManager, required ProcessManager processManager,
@required Logger logger, required Logger logger,
Map<String, String> additionalReplacementValues = const <String, String>{} Map<String, String> additionalReplacementValues = const <String, String>{}
}) : _deviceName = deviceName, }) : _deviceName = deviceName,
_forwardPortCommand = forwardPortCommand, _forwardPortCommand = forwardPortCommand,
...@@ -125,17 +122,17 @@ class CustomDevicePortForwarder extends DevicePortForwarder { ...@@ -125,17 +122,17 @@ class CustomDevicePortForwarder extends DevicePortForwarder {
final RegExp _forwardPortSuccessRegex; final RegExp _forwardPortSuccessRegex;
final ProcessManager _processManager; final ProcessManager _processManager;
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
final int numTries; final int? numTries;
final Map<String, String> _additionalReplacementValues; final Map<String, String> _additionalReplacementValues;
final List<ForwardedPort> _forwardedPorts = <ForwardedPort>[]; final List<ForwardedPort> _forwardedPorts = <ForwardedPort>[];
@override @override
Future<void> dispose() async { Future<void> dispose() async {
// copy the list so we don't modify it concurrently // copy the list so we don't modify it concurrently
return Future.wait(List<ForwardedPort>.of(_forwardedPorts).map(unforward)); await Future.wait(List<ForwardedPort>.of(_forwardedPorts).map(unforward));
} }
Future<ForwardedPort> tryForward(int devicePort, int hostPort) async { Future<ForwardedPort?> tryForward(int devicePort, int hostPort) async {
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
_forwardPortCommand, _forwardPortCommand,
<String, String>{ <String, String>{
...@@ -148,7 +145,7 @@ class CustomDevicePortForwarder extends DevicePortForwarder { ...@@ -148,7 +145,7 @@ class CustomDevicePortForwarder extends DevicePortForwarder {
// launch the forwarding command // launch the forwarding command
final Process process = await _processUtils.start(interpolated); final Process process = await _processUtils.start(interpolated);
final Completer<ForwardedPort> completer = Completer<ForwardedPort>(); final Completer<ForwardedPort?> completer = Completer<ForwardedPort?>();
// read the outputs of the process, if we find a line that matches // read the outputs of the process, if we find a line that matches
// the configs forwardPortSuccessRegex, we complete with a successfully // the configs forwardPortSuccessRegex, we complete with a successfully
...@@ -181,18 +178,18 @@ class CustomDevicePortForwarder extends DevicePortForwarder { ...@@ -181,18 +178,18 @@ class CustomDevicePortForwarder extends DevicePortForwarder {
} }
@override @override
Future<int> forward(int devicePort, {int hostPort}) async { Future<int> forward(int devicePort, {int? hostPort}) async {
int actualHostPort = (hostPort == 0 || hostPort == null) ? devicePort : hostPort; int actualHostPort = (hostPort == 0 || hostPort == null) ? devicePort : hostPort;
int tries = 0; int tries = 0;
while ((numTries == null) || (tries < numTries)) { while ((numTries == null) || (tries < numTries!)) {
// when the desired host port is already forwarded by this Forwarder, // when the desired host port is already forwarded by this Forwarder,
// choose another one // choose another one
while (_forwardedPorts.any((ForwardedPort port) => port.hostPort == actualHostPort)) { while (_forwardedPorts.any((ForwardedPort port) => port.hostPort == actualHostPort)) {
actualHostPort += 1; actualHostPort += 1;
} }
final ForwardedPort port = await tryForward(devicePort, actualHostPort); final ForwardedPort? port = await tryForward(devicePort, actualHostPort);
if (port != null) { if (port != null) {
_forwardedPorts.add(port); _forwardedPorts.add(port);
...@@ -217,7 +214,10 @@ class CustomDevicePortForwarder extends DevicePortForwarder { ...@@ -217,7 +214,10 @@ class CustomDevicePortForwarder extends DevicePortForwarder {
// since a forwarded port represents a running process launched with // since a forwarded port represents a running process launched with
// the forwardPortCommand, unforwarding is as easy as killing the process // the forwardPortCommand, unforwarding is as easy as killing the process
_processManager.killPid(forwardedPort.context.pid); final int? pid = forwardedPort.context?.pid;
if (pid != null) {
_processManager.killPid(pid);
}
_forwardedPorts.remove(forwardedPort); _forwardedPorts.remove(forwardedPort);
} }
} }
...@@ -229,11 +229,11 @@ class CustomDevicePortForwarder extends DevicePortForwarder { ...@@ -229,11 +229,11 @@ class CustomDevicePortForwarder extends DevicePortForwarder {
/// kill to stop the app, maybe other things in the future. /// kill to stop the app, maybe other things in the future.
class CustomDeviceAppSession { class CustomDeviceAppSession {
CustomDeviceAppSession({ CustomDeviceAppSession({
@required this.name, required this.name,
@required CustomDevice device, required CustomDevice device,
@required ApplicationPackage appPackage, required ApplicationPackage appPackage,
@required Logger logger, required Logger logger,
@required ProcessManager processManager required ProcessManager processManager
}) : _appPackage = appPackage, }) : _appPackage = appPackage,
_device = device, _device = device,
_logger = logger, _logger = logger,
...@@ -252,8 +252,8 @@ class CustomDeviceAppSession { ...@@ -252,8 +252,8 @@ class CustomDeviceAppSession {
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
final CustomDeviceLogReader logReader; final CustomDeviceLogReader logReader;
Process _process; Process? _process;
int _forwardedHostPort; int? _forwardedHostPort;
/// Get the engine options for the given [debuggingOptions], /// Get the engine options for the given [debuggingOptions],
/// [traceStartup] and [route]. /// [traceStartup] and [route].
...@@ -262,7 +262,7 @@ class CustomDeviceAppSession { ...@@ -262,7 +262,7 @@ class CustomDeviceAppSession {
/// ///
/// For example, `_getEngineOptions(null, false, null)` will return /// For example, `_getEngineOptions(null, false, null)` will return
/// `['enable-dart-profiling=true', 'enable-background-compilation=true']` /// `['enable-dart-profiling=true', 'enable-background-compilation=true']`
List<String> _getEngineOptions(DebuggingOptions debuggingOptions, bool traceStartup, String route) { List<String> _getEngineOptions(DebuggingOptions debuggingOptions, bool traceStartup, String? route) {
final List<String> options = <String>[]; final List<String> options = <String>[];
void addFlag(String value) { void addFlag(String value) {
...@@ -345,7 +345,7 @@ class CustomDeviceAppSession { ...@@ -345,7 +345,7 @@ class CustomDeviceAppSession {
/// ///
/// For example, `_getEngineOptionsForCmdline(null, false, null)` will return /// For example, `_getEngineOptionsForCmdline(null, false, null)` will return
/// `--enable-dart-profiling=true --enable-background-compilation=true` /// `--enable-dart-profiling=true --enable-background-compilation=true`
String _getEngineOptionsForCmdline(DebuggingOptions debuggingOptions, bool traceStartup, String route) { String _getEngineOptionsForCmdline(DebuggingOptions debuggingOptions, bool traceStartup, String? route) {
return _getEngineOptions(debuggingOptions, traceStartup, route).map((String e) => '--$e').join(' '); return _getEngineOptions(debuggingOptions, traceStartup, route).map((String e) => '--$e').join(' ');
} }
...@@ -358,22 +358,24 @@ class CustomDeviceAppSession { ...@@ -358,22 +358,24 @@ class CustomDeviceAppSession {
/// [ipv6] may not be respected since it depends on the device config whether /// [ipv6] may not be respected since it depends on the device config whether
/// it uses ipv6 or ipv4 /// it uses ipv6 or ipv4
Future<LaunchResult> start({ Future<LaunchResult> start({
String mainPath, String? mainPath,
String route, String? route,
DebuggingOptions debuggingOptions, required DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs = const <String, dynamic>{}, Map<String, Object?> platformArgs = const <String, Object>{},
bool prebuiltApplication = false, bool prebuiltApplication = false,
bool ipv6 = false, bool ipv6 = false,
String userIdentifier String? userIdentifier
}) async { }) async {
platformArgs ??= <String, dynamic>{}; final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
final String? packageName = _appPackage.name;
final bool traceStartup = platformArgs['trace-startup'] as bool ?? false; if (packageName == null) {
throw ToolExit('Could not start app, name for $_appPackage is unknown.');
}
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
_device._config.runDebugCommand, _device._config.runDebugCommand,
<String, String>{ <String, String>{
'remotePath': '/tmp/', 'remotePath': '/tmp/',
'appName': _appPackage.name, 'appName': packageName,
'engineOptions': _getEngineOptionsForCmdline(debuggingOptions, traceStartup, route) 'engineOptions': _getEngineOptionsForCmdline(debuggingOptions, traceStartup, route)
} }
); );
...@@ -396,11 +398,11 @@ class CustomDeviceAppSession { ...@@ -396,11 +398,11 @@ class CustomDeviceAppSession {
// in the same microtask AFAICT but this way we're on the safe side. // in the same microtask AFAICT but this way we're on the safe side.
logReader.listenToProcessOutput(process); logReader.listenToProcessOutput(process);
final Uri observatoryUri = await discovery.uri; final Uri? observatoryUri = await discovery.uri;
await discovery.cancel(); await discovery.cancel();
if (_device._config.usesPortForwarding) { if (_device._config.usesPortForwarding) {
_forwardedHostPort = observatoryUri.port; _forwardedHostPort = observatoryUri?.port;
} }
return LaunchResult.succeeded(observatoryUri: observatoryUri); return LaunchResult.succeeded(observatoryUri: observatoryUri);
...@@ -426,7 +428,7 @@ class CustomDeviceAppSession { ...@@ -426,7 +428,7 @@ class CustomDeviceAppSession {
} }
_maybeUnforwardPort(); _maybeUnforwardPort();
final bool result = _processManager.killPid(_process.pid); final bool result = _processManager.killPid(_process!.pid);
_process = null; _process = null;
return result; return result;
} }
...@@ -434,7 +436,7 @@ class CustomDeviceAppSession { ...@@ -434,7 +436,7 @@ class CustomDeviceAppSession {
void dispose() { void dispose() {
if (_process != null) { if (_process != null) {
_maybeUnforwardPort(); _maybeUnforwardPort();
_processManager.killPid(_process.pid); _processManager.killPid(_process!.pid);
_process = null; _process = null;
} }
...@@ -447,9 +449,9 @@ class CustomDeviceAppSession { ...@@ -447,9 +449,9 @@ class CustomDeviceAppSession {
/// used to construct it. /// used to construct it.
class CustomDevice extends Device { class CustomDevice extends Device {
CustomDevice({ CustomDevice({
@required CustomDeviceConfig config, required CustomDeviceConfig config,
@required Logger logger, required Logger logger,
@required ProcessManager processManager, required ProcessManager processManager,
}) : _config = config, }) : _config = config,
_logger = logger, _logger = logger,
_processManager = processManager, _processManager = processManager,
...@@ -461,8 +463,8 @@ class CustomDevice extends Device { ...@@ -461,8 +463,8 @@ class CustomDevice extends Device {
portForwarder = config.usesPortForwarding ? portForwarder = config.usesPortForwarding ?
CustomDevicePortForwarder( CustomDevicePortForwarder(
deviceName: config.label, deviceName: config.label,
forwardPortCommand: config.forwardPortCommand, forwardPortCommand: config.forwardPortCommand!,
forwardPortSuccessRegex: config.forwardPortSuccessRegex, forwardPortSuccessRegex: config.forwardPortSuccessRegex!,
processManager: processManager, processManager: processManager,
logger: logger, logger: logger,
) : const NoOpDevicePortForwarder(), ) : const NoOpDevicePortForwarder(),
...@@ -517,7 +519,7 @@ class CustomDevice extends Device { ...@@ -517,7 +519,7 @@ class CustomDevice extends Device {
/// will be reported in the log using [_logger.printError]. If [timeout] /// will be reported in the log using [_logger.printError]. If [timeout]
/// is null, it's treated as if it's an infinite timeout. /// is null, it's treated as if it's an infinite timeout.
Future<bool> tryPing({ Future<bool> tryPing({
Duration timeout, Duration? timeout,
Map<String, String> replacementValues = const <String, String>{} Map<String, String> replacementValues = const <String, String>{}
}) async { }) async {
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
...@@ -537,9 +539,10 @@ class CustomDevice extends Device { ...@@ -537,9 +539,10 @@ class CustomDevice extends Device {
// If the user doesn't configure a ping success regex, any ping with exitCode zero // If the user doesn't configure a ping success regex, any ping with exitCode zero
// is good enough. Otherwise we check if either stdout or stderr have a match of // is good enough. Otherwise we check if either stdout or stderr have a match of
// the pingSuccessRegex. // the pingSuccessRegex.
return _config.pingSuccessRegex == null final RegExp? pingSuccessRegex = _config.pingSuccessRegex;
|| _config.pingSuccessRegex.hasMatch(result.stdout) return pingSuccessRegex == null
|| _config.pingSuccessRegex.hasMatch(result.stderr); || pingSuccessRegex.hasMatch(result.stdout)
|| pingSuccessRegex.hasMatch(result.stderr);
} }
/// Tries to execute the configs postBuild command using [appName] for the /// Tries to execute the configs postBuild command using [appName] for the
...@@ -555,15 +558,15 @@ class CustomDevice extends Device { ...@@ -555,15 +558,15 @@ class CustomDevice extends Device {
/// will be reported in the log using [_logger.printError]. If [timeout] /// will be reported in the log using [_logger.printError]. If [timeout]
/// is null, it's treated as if it's an infinite timeout. /// is null, it's treated as if it's an infinite timeout.
Future<bool> _tryPostBuild({ Future<bool> _tryPostBuild({
@required String appName, required String appName,
@required String localPath, required String localPath,
Duration timeout, Duration? timeout,
Map<String, String> additionalReplacementValues = const <String, String>{} Map<String, String> additionalReplacementValues = const <String, String>{}
}) async { }) async {
assert(_config.postBuildCommand != null); assert(_config.postBuildCommand != null);
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
_config.postBuildCommand, _config.postBuildCommand!,
<String, String>{ <String, String>{
'appName': appName, 'appName': appName,
'localPath': localPath 'localPath': localPath
...@@ -593,8 +596,8 @@ class CustomDevice extends Device { ...@@ -593,8 +596,8 @@ class CustomDevice extends Device {
/// will be reported in the log using [_logger.printError]. If [timeout] /// will be reported in the log using [_logger.printError]. If [timeout]
/// is null, it's treated as if it's an infinite timeout. /// is null, it's treated as if it's an infinite timeout.
Future<bool> tryUninstall({ Future<bool> tryUninstall({
@required String appName, required String appName,
Duration timeout, Duration? timeout,
Map<String, String> additionalReplacementValues = const <String, String>{} Map<String, String> additionalReplacementValues = const <String, String>{}
}) async { }) async {
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
...@@ -627,9 +630,9 @@ class CustomDevice extends Device { ...@@ -627,9 +630,9 @@ class CustomDevice extends Device {
/// [appName] is the name of the app to be installed. Substituted for any occurrence /// [appName] is the name of the app to be installed. Substituted for any occurrence
/// of `${appName}` in the custom device configs `install` command. /// of `${appName}` in the custom device configs `install` command.
Future<bool> tryInstall({ Future<bool> tryInstall({
@required String localPath, required String localPath,
@required String appName, required String appName,
Duration timeout, Duration? timeout,
Map<String, String> additionalReplacementValues = const <String, String>{} Map<String, String> additionalReplacementValues = const <String, String>{}
}) async { }) async {
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
...@@ -666,11 +669,11 @@ class CustomDevice extends Device { ...@@ -666,11 +669,11 @@ class CustomDevice extends Device {
} }
@override @override
Future<String> get emulatorId async => null; Future<String?> get emulatorId async => null;
@override @override
FutureOr<DeviceLogReader> getLogReader({ FutureOr<DeviceLogReader> getLogReader({
covariant ApplicationPackage app, covariant ApplicationPackage? app,
bool includePastLogs = false bool includePastLogs = false
}) { }) {
if (app != null) { if (app != null) {
...@@ -681,21 +684,22 @@ class CustomDevice extends Device { ...@@ -681,21 +684,22 @@ class CustomDevice extends Device {
} }
@override @override
Future<bool> installApp(covariant ApplicationPackage app, {String userIdentifier}) async { Future<bool> installApp(covariant ApplicationPackage app, {String? userIdentifier}) async {
if (!await tryUninstall(appName: app.name)) { final String? appName = app.name;
if (appName == null || !await tryUninstall(appName: appName)) {
return false; return false;
} }
final bool result = await tryInstall( final bool result = await tryInstall(
localPath: getAssetBuildDirectory(), localPath: getAssetBuildDirectory(),
appName: app.name appName: appName,
); );
return result; return result;
} }
@override @override
Future<bool> isAppInstalled(covariant ApplicationPackage app, {String userIdentifier}) async { Future<bool> isAppInstalled(covariant ApplicationPackage app, {String? userIdentifier}) async {
return false; return false;
} }
...@@ -717,7 +721,7 @@ class CustomDevice extends Device { ...@@ -717,7 +721,7 @@ class CustomDevice extends Device {
} }
final List<String> interpolated = interpolateCommand( final List<String> interpolated = interpolateCommand(
_config.screenshotCommand, _config.screenshotCommand!,
<String, String>{}, <String, String>{},
); );
...@@ -749,14 +753,14 @@ class CustomDevice extends Device { ...@@ -749,14 +753,14 @@ class CustomDevice extends Device {
@override @override
Future<LaunchResult> startApp( Future<LaunchResult> startApp(
covariant ApplicationPackage package, { covariant ApplicationPackage package, {
String mainPath, String? mainPath,
String route, String? route,
DebuggingOptions debuggingOptions, required DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs, Map<String, Object?> platformArgs = const <String, Object>{},
bool prebuiltApplication = false, bool prebuiltApplication = false,
bool ipv6 = false, bool ipv6 = false,
String userIdentifier, String? userIdentifier,
BundleBuilder bundleBuilder BundleBuilder? bundleBuilder,
}) async { }) async {
if (!prebuiltApplication) { if (!prebuiltApplication) {
final String assetBundleDir = getAssetBuildDirectory(); final String assetBundleDir = getAssetBuildDirectory();
...@@ -774,8 +778,12 @@ class CustomDevice extends Device { ...@@ -774,8 +778,12 @@ class CustomDevice extends Device {
// if we have a post build step (needed for some embedders), execute it // if we have a post build step (needed for some embedders), execute it
if (_config.postBuildCommand != null) { if (_config.postBuildCommand != null) {
final String? packageName = package.name;
if (packageName == null) {
throw ToolExit('Could not start app, name for $package is unknown.');
}
await _tryPostBuild( await _tryPostBuild(
appName: package.name, appName: packageName,
localPath: assetBundleDir, localPath: assetBundleDir,
); );
} }
...@@ -798,7 +806,7 @@ class CustomDevice extends Device { ...@@ -798,7 +806,7 @@ class CustomDevice extends Device {
} }
@override @override
Future<bool> stopApp(covariant ApplicationPackage app, {String userIdentifier}) { Future<bool> stopApp(covariant ApplicationPackage app, {String? userIdentifier}) {
return _getOrCreateAppSession(app).stop(); return _getOrCreateAppSession(app).stop();
} }
...@@ -806,8 +814,12 @@ class CustomDevice extends Device { ...@@ -806,8 +814,12 @@ class CustomDevice extends Device {
Future<TargetPlatform> get targetPlatform async => _config.platform ?? TargetPlatform.linux_arm64; Future<TargetPlatform> get targetPlatform async => _config.platform ?? TargetPlatform.linux_arm64;
@override @override
Future<bool> uninstallApp(covariant ApplicationPackage app, {String userIdentifier}) { Future<bool> uninstallApp(covariant ApplicationPackage app, {String? userIdentifier}) async {
return tryUninstall(appName: app.name); final String? appName = app.name;
if (appName == null) {
return false;
}
return tryUninstall(appName: appName);
} }
} }
...@@ -817,10 +829,10 @@ class CustomDevices extends PollingDeviceDiscovery { ...@@ -817,10 +829,10 @@ class CustomDevices extends PollingDeviceDiscovery {
/// Create a custom device discovery that pings all enabled devices in the /// Create a custom device discovery that pings all enabled devices in the
/// given [CustomDevicesConfig]. /// given [CustomDevicesConfig].
CustomDevices({ CustomDevices({
@required FeatureFlags featureFlags, required FeatureFlags featureFlags,
@required ProcessManager processManager, required ProcessManager processManager,
@required Logger logger, required Logger logger,
@required CustomDevicesConfig config required CustomDevicesConfig config
}) : _customDeviceWorkflow = CustomDeviceWorkflow( }) : _customDeviceWorkflow = CustomDeviceWorkflow(
featureFlags: featureFlags, featureFlags: featureFlags,
), ),
...@@ -855,7 +867,7 @@ class CustomDevices extends PollingDeviceDiscovery { ...@@ -855,7 +867,7 @@ class CustomDevices extends PollingDeviceDiscovery {
} }
@override @override
Future<List<Device>> pollingGetDevices({Duration timeout}) async { Future<List<Device>> pollingGetDevices({Duration? timeout}) async {
if (!canListAnything) { if (!canListAnything) {
return const <Device>[]; return const <Device>[];
} }
......
...@@ -391,7 +391,7 @@ void main() { ...@@ -391,7 +391,7 @@ void main() {
processManager: processManager, processManager: processManager,
); );
final LaunchResult launchResult = await appSession.start(); final LaunchResult launchResult = await appSession.start(debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug));
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.observatoryUri, Uri.parse('http://127.0.0.1:12345/abcd/')); expect(launchResult.observatoryUri, Uri.parse('http://127.0.0.1:12345/abcd/'));
...@@ -428,7 +428,7 @@ void main() { ...@@ -428,7 +428,7 @@ void main() {
processManager: processManager processManager: processManager
); );
final LaunchResult launchResult = await appSession.start(); final LaunchResult launchResult = await appSession.start(debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug));
expect(launchResult.started, true); expect(launchResult.started, true);
expect(launchResult.observatoryUri, Uri.parse('http://192.168.178.123:12345/abcd/')); expect(launchResult.observatoryUri, Uri.parse('http://192.168.178.123:12345/abcd/'));
......
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