Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
a204f038
Unverified
Commit
a204f038
authored
Jan 27, 2021
by
Dan Field
Committed by
GitHub
Jan 27, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Null safe migration for fuchsia_remote_debug_protocol (#74762)
parent
d546e1d3
Changes
7
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
115 additions
and
142 deletions
+115
-142
dart_vm.dart
...s/fuchsia_remote_debug_protocol/lib/src/dart/dart_vm.dart
+23
-35
fuchsia_remote_connection.dart
...ote_debug_protocol/lib/src/fuchsia_remote_connection.dart
+38
-40
ssh_command_runner.dart
...te_debug_protocol/lib/src/runners/ssh_command_runner.dart
+4
-4
pubspec.yaml
packages/fuchsia_remote_debug_protocol/pubspec.yaml
+1
-1
fuchsia_remote_connection_test.dart
...e_debug_protocol/test/fuchsia_remote_connection_test.dart
+22
-22
dart_vm_test.dart
...sia_remote_debug_protocol/test/src/dart/dart_vm_test.dart
+18
-31
ssh_command_runner_test.dart
...ug_protocol/test/src/runners/ssh_command_runner_test.dart
+9
-9
No files found.
packages/fuchsia_remote_debug_protocol/lib/src/dart/dart_vm.dart
View file @
a204f038
...
...
@@ -10,14 +10,13 @@ import 'package:vm_service/vm_service.dart' as vms;
import
'../common/logging.dart'
;
const
Duration
_kConnectTimeout
=
Duration
(
seconds:
3
);
const
Duration
_kRpcTimeout
=
Duration
(
seconds:
5
);
final
Logger
_log
=
Logger
(
'DartVm'
);
/// Signature of an asynchronous function for establishing a [vms.VmService]
/// connection to a [Uri].
typedef
RpcPeerConnectionFunction
=
Future
<
vms
.
VmService
>
Function
(
Uri
uri
,
{
Duration
timeout
,
required
Duration
timeout
,
});
/// [DartVm] uses this function to connect to the Dart VM on Fuchsia.
...
...
@@ -34,7 +33,7 @@ Future<vms.VmService> _waitAndConnect(
Duration
timeout
=
_kConnectTimeout
,
})
async
{
int
attempts
=
0
;
WebSocket
socket
;
late
WebSocket
socket
;
while
(
true
)
{
try
{
socket
=
await
WebSocket
.
connect
(
uri
.
toString
());
...
...
@@ -56,7 +55,7 @@ Future<vms.VmService> _waitAndConnect(
await
service
.
getVersion
();
return
service
;
}
catch
(
e
)
{
await
socket
?
.
close
();
await
socket
.
close
();
if
(
attempts
>
5
)
{
_log
.
warning
(
'It is taking an unusually long time to connect to the VM...'
);
}
...
...
@@ -112,9 +111,6 @@ class DartVm {
}
final
vms
.
VmService
service
=
await
fuchsiaVmServiceConnectionFunction
(
uri
,
timeout:
timeout
);
if
(
service
==
null
)
{
return
null
;
}
return
DartVm
.
_
(
service
,
uri
);
}
...
...
@@ -123,16 +119,13 @@ class DartVm {
/// This is not limited to Isolates running Flutter, but to any Isolate on the
/// VM. Therefore, the [pattern] argument should be written to exclude
/// matching unintended isolates.
Future
<
List
<
IsolateRef
>>
getMainIsolatesByPattern
(
Pattern
pattern
,
{
Duration
timeout
=
_kRpcTimeout
,
})
async
{
Future
<
List
<
IsolateRef
>>
getMainIsolatesByPattern
(
Pattern
pattern
)
async
{
final
vms
.
VM
vmRef
=
await
_vmService
.
getVM
();
final
List
<
IsolateRef
>
result
=
<
IsolateRef
>[];
for
(
final
vms
.
IsolateRef
isolateRef
in
vmRef
.
isolates
)
{
if
(
pattern
.
matchAsPrefix
(
isolateRef
.
name
)
!=
null
)
{
for
(
final
vms
.
IsolateRef
isolateRef
in
vmRef
.
isolates
!
)
{
if
(
pattern
.
matchAsPrefix
(
isolateRef
.
name
!
)
!=
null
)
{
_log
.
fine
(
'Found Isolate matching "
$pattern
": "
${isolateRef.name}
"'
);
result
.
add
(
IsolateRef
.
_fromJson
(
isolateRef
.
json
,
this
));
result
.
add
(
IsolateRef
.
_fromJson
(
isolateRef
.
json
!
,
this
));
}
}
return
result
;
...
...
@@ -145,16 +138,11 @@ class DartVm {
/// the flutter view's name), then the flutter view's ID will be added
/// instead. If none of these things can be found (isolate has no name or the
/// flutter view has no ID), then the result will not be added to the list.
Future
<
List
<
FlutterView
>>
getAllFlutterViews
({
Duration
timeout
=
_kRpcTimeout
,
})
async
{
Future
<
List
<
FlutterView
>>
getAllFlutterViews
()
async
{
final
List
<
FlutterView
>
views
=
<
FlutterView
>[];
final
vms
.
Response
rpcResponse
=
await
_vmService
.
callMethod
(
'_flutter.listViews'
);
for
(
final
Map
<
String
,
dynamic
>
jsonView
in
(
rpcResponse
.
json
[
'views'
]
as
List
<
dynamic
>).
cast
<
Map
<
String
,
dynamic
>>())
{
final
FlutterView
flutterView
=
FlutterView
.
_fromJson
(
jsonView
);
if
(
flutterView
!=
null
)
{
views
.
add
(
flutterView
);
}
for
(
final
Map
<
String
,
dynamic
>
jsonView
in
(
rpcResponse
.
json
![
'views'
]
as
List
<
dynamic
>).
cast
<
Map
<
String
,
dynamic
>>())
{
views
.
add
(
FlutterView
.
_fromJson
(
jsonView
));
}
return
views
;
}
...
...
@@ -190,25 +178,25 @@ class FlutterView {
/// All other cases return a [FlutterView] instance. The name of the
/// view may be null, but the id will always be set.
factory
FlutterView
.
_fromJson
(
Map
<
String
,
dynamic
>
json
)
{
final
Map
<
String
,
dynamic
>
isolate
=
json
[
'isolate'
]
as
Map
<
String
,
dynamic
>;
final
String
id
=
json
[
'id'
]
as
String
;
String
name
;
final
Map
<
String
,
dynamic
>?
isolate
=
json
[
'isolate'
]
as
Map
<
String
,
dynamic
>?;
final
String
?
id
=
json
[
'id'
]
as
String
?;
String
?
name
;
if
(
id
==
null
)
{
throw
RpcFormatError
(
'Unable to find view name for the following JSON structure "
$json
"'
);
}
if
(
isolate
!=
null
)
{
name
=
isolate
[
'name'
]
as
String
;
name
=
isolate
[
'name'
]
as
String
?
;
if
(
name
==
null
)
{
throw
RpcFormatError
(
'Unable to find name for isolate "
$isolate
"'
);
}
}
if
(
id
==
null
)
{
throw
RpcFormatError
(
'Unable to find view name for the following JSON structure "
$json
"'
);
}
return
FlutterView
.
_
(
name
,
id
);
}
/// Determines the name of the isolate associated with this view. If there is
/// no associated isolate, this will be set to the view's ID.
final
String
_name
;
final
String
?
_name
;
/// The ID of the Flutter view.
final
String
_id
;
...
...
@@ -219,7 +207,7 @@ class FlutterView {
/// Returns the name of the [FlutterView].
///
/// May be null if there is no associated isolate.
String
get
name
=>
_name
;
String
?
get
name
=>
_name
;
}
/// This is a wrapper class for the `@Isolate` RPC object.
...
...
@@ -233,9 +221,9 @@ class IsolateRef {
IsolateRef
.
_
(
this
.
name
,
this
.
number
,
this
.
dartVm
);
factory
IsolateRef
.
_fromJson
(
Map
<
String
,
dynamic
>
json
,
DartVm
dartVm
)
{
final
String
number
=
json
[
'number'
]
as
String
;
final
String
name
=
json
[
'name'
]
as
String
;
final
String
type
=
json
[
'type'
]
as
String
;
final
String
?
number
=
json
[
'number'
]
as
String
?
;
final
String
?
name
=
json
[
'name'
]
as
String
?
;
final
String
?
type
=
json
[
'type'
]
as
String
?
;
if
(
type
==
null
)
{
throw
RpcFormatError
(
'Unable to find type within JSON "
$json
"'
);
}
...
...
packages/fuchsia_remote_debug_protocol/lib/src/fuchsia_remote_connection.dart
View file @
a204f038
This diff is collapsed.
Click to expand it.
packages/fuchsia_remote_debug_protocol/lib/src/runners/ssh_command_runner.dart
View file @
a204f038
...
...
@@ -44,7 +44,7 @@ class SshCommandRunner {
/// IPv4 nor IPv6. When connecting to a link local address (`fe80::` is
/// usually at the start of the address), an interface should be supplied.
SshCommandRunner
({
this
.
address
,
required
this
.
address
,
this
.
interface
=
''
,
this
.
sshConfigPath
,
})
:
_processManager
=
const
LocalProcessManager
()
{
...
...
@@ -55,7 +55,7 @@ class SshCommandRunner {
@visibleForTesting
SshCommandRunner
.
withProcessManager
(
this
.
_processManager
,
{
this
.
address
,
required
this
.
address
,
this
.
interface
=
''
,
this
.
sshConfigPath
,
})
{
...
...
@@ -70,7 +70,7 @@ class SshCommandRunner {
final
String
address
;
/// The path to the SSH config (optional).
final
String
sshConfigPath
;
final
String
?
sshConfigPath
;
/// The name of the machine's network interface (for use with IPv6
/// connections. Ignored otherwise).
...
...
@@ -84,7 +84,7 @@ class SshCommandRunner {
final
List
<
String
>
args
=
<
String
>[
'ssh'
,
if
(
sshConfigPath
!=
null
)
...<
String
>[
'-F'
,
sshConfigPath
],
...<
String
>[
'-F'
,
sshConfigPath
!
],
if
(
isIpV6Address
(
address
))
...<
String
>[
'-6'
,
if
(
interface
.
isEmpty
)
address
else
'
$address
%
$interface
'
]
else
...
...
packages/fuchsia_remote_debug_protocol/pubspec.yaml
View file @
a204f038
...
...
@@ -5,7 +5,7 @@ homepage: http://flutter.dev
author
:
Flutter Authors <flutter-dev@googlegroups.com>
environment
:
sdk
:
"
>=2.2.2
<3.0.0"
sdk
:
'
>=2.12.0-0
<3.0.0'
dependencies
:
process
:
4.0.0-nullsafety.4
...
...
packages/fuchsia_remote_debug_protocol/test/fuchsia_remote_connection_test.dart
View file @
a204f038
...
...
@@ -11,9 +11,9 @@ import 'common.dart';
void
main
(
)
{
group
(
'FuchsiaRemoteConnection.connect'
,
()
{
List
<
FakePortForwarder
>
forwardedPorts
;
late
List
<
FakePortForwarder
>
forwardedPorts
;
List
<
FakeVmService
>
fakeVmServices
;
List
<
Uri
>
uriConnections
;
late
List
<
Uri
>
uriConnections
;
setUp
(()
{
final
List
<
Map
<
String
,
dynamic
>>
flutterViewCannedResponses
=
...
...
@@ -63,7 +63,7 @@ void main() {
uriConnections
=
<
Uri
>[];
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
return
Future
<
vms
.
VmService
>(()
async
{
final
FakeVmService
service
=
FakeVmService
();
...
...
@@ -89,8 +89,8 @@ void main() {
Future
<
PortForwarder
>
fakePortForwardingFunction
(
String
address
,
int
remotePort
,
[
String
interface
=
'',
String
configFile
,
String
?
interface
=
'',
String
?
configFile
,
])
{
return
Future
<
PortForwarder
>(()
{
final
FakePortForwarder
pf
=
FakePortForwarder
();
...
...
@@ -156,8 +156,8 @@ void main() {
Future
<
PortForwarder
>
fakePortForwardingFunction
(
String
address
,
int
remotePort
,
[
String
interface
=
'',
String
configFile
,
String
?
interface
=
'',
String
?
configFile
,
])
{
return
Future
<
PortForwarder
>(()
{
final
FakePortForwarder
pf
=
FakePortForwarder
();
...
...
@@ -223,8 +223,8 @@ void main() {
Future
<
PortForwarder
>
fakePortForwardingFunction
(
String
address
,
int
remotePort
,
[
String
interface
=
'',
String
configFile
,
String
?
interface
=
'',
String
?
configFile
,
])
{
return
Future
<
PortForwarder
>(()
{
final
FakePortForwarder
pf
=
FakePortForwarder
();
...
...
@@ -296,24 +296,24 @@ void main() {
}
class
FakeSshCommandRunner
extends
Fake
implements
SshCommandRunner
{
List
<
String
>
findResponse
;
List
<
String
>
lsResponse
;
List
<
String
>
?
findResponse
;
List
<
String
>
?
lsResponse
;
@override
Future
<
List
<
String
>>
run
(
String
command
)
async
{
if
(
command
.
startsWith
(
'/bin/find'
))
{
return
findResponse
;
return
findResponse
!
;
}
if
(
command
.
startsWith
(
'/bin/ls'
))
{
return
lsResponse
;
return
lsResponse
!
;
}
throw
UnimplementedError
(
command
);
}
@override
String
interface
;
String
interface
=
''
;
@
override
String
address
;
String
address
=
''
;
@override
String
get
sshConfigPath
=>
'~/.ssh'
;
...
...
@@ -321,13 +321,13 @@ class FakeSshCommandRunner extends Fake implements SshCommandRunner {
class
FakePortForwarder
extends
Fake
implements
PortForwarder
{
@override
int
port
;
int
port
=
0
;
@override
int
remotePort
;
int
remotePort
=
0
;
@override
String
openPortAddress
;
String
?
openPortAddress
;
bool
stopped
=
false
;
@override
...
...
@@ -338,7 +338,7 @@ class FakePortForwarder extends Fake implements PortForwarder {
class
FakeVmService
extends
Fake
implements
vms
.
VmService
{
bool
disposed
=
false
;
vms
.
Response
flutterListViews
;
vms
.
Response
?
flutterListViews
;
@override
Future
<
void
>
dispose
()
async
{
...
...
@@ -346,15 +346,15 @@ class FakeVmService extends Fake implements vms.VmService {
}
@override
Future
<
vms
.
Response
>
callMethod
(
String
method
,
{
String
isolateId
,
Map
<
String
,
dynamic
>
args
})
async
{
Future
<
vms
.
Response
>
callMethod
(
String
method
,
{
String
?
isolateId
,
Map
<
String
,
dynamic
>?
args
})
async
{
if
(
method
==
'_flutter.listViews'
)
{
return
flutterListViews
;
return
flutterListViews
!
;
}
throw
UnimplementedError
(
method
);
}
@override
Future
<
void
>
onDone
;
Future
<
void
>
onDone
=
Future
<
void
>.
value
()
;
@override
Future
<
vms
.
Version
>
getVersion
()
async
{
...
...
packages/fuchsia_remote_debug_protocol/test/src/dart/dart_vm_test.dart
View file @
a204f038
...
...
@@ -16,24 +16,11 @@ void main() {
restoreVmServiceConnectionFunction
();
});
test
(
'null connector'
,
()
async
{
Future
<
vms
.
VmService
>
fakeServiceFunction
(
Uri
uri
,
{
Duration
timeout
,
})
{
return
Future
<
vms
.
VmService
>(()
=>
null
);
}
fuchsiaVmServiceConnectionFunction
=
fakeServiceFunction
;
expect
(
await
DartVm
.
connect
(
Uri
.
parse
(
'http://this.whatever/ws'
)),
equals
(
null
));
});
test
(
'disconnect closes peer'
,
()
async
{
final
FakeVmService
service
=
FakeVmService
();
Future
<
vms
.
VmService
>
fakeServiceFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
return
Future
<
vms
.
VmService
>(()
=>
service
);
}
...
...
@@ -47,7 +34,7 @@ void main() {
});
group
(
'DartVm.getAllFlutterViews'
,
()
{
FakeVmService
fakeService
;
late
FakeVmService
fakeService
;
setUp
(()
{
fakeService
=
FakeVmService
();
...
...
@@ -91,7 +78,7 @@ void main() {
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
fakeService
.
flutterListViews
=
vms
.
Response
.
parse
(
flutterViewCannedResponses
);
return
Future
<
vms
.
VmService
>(()
=>
fakeService
);
...
...
@@ -147,7 +134,7 @@ void main() {
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
fakeService
.
flutterListViews
=
vms
.
Response
.
parse
(
flutterViewCannedResponses
);
return
Future
<
vms
.
VmService
>(()
=>
fakeService
);
...
...
@@ -194,7 +181,7 @@ void main() {
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
fakeService
.
flutterListViews
=
vms
.
Response
.
parse
(
flutterViewCannedResponseMissingId
);
return
Future
<
vms
.
VmService
>(()
=>
fakeService
);
...
...
@@ -219,33 +206,33 @@ void main() {
'id'
:
'isolates/1'
,
'name'
:
'file://thingThatWillNotMatch:main()'
,
'number'
:
'1'
,
}),
})
!
,
vms
.
IsolateRef
.
parse
(<
String
,
dynamic
>{
'type'
:
'@Isolate'
,
'fixedId'
:
'true'
,
'id'
:
'isolates/2'
,
'name'
:
'0:dart_name_pattern()'
,
'number'
:
'2'
,
}),
})
!
,
vms
.
IsolateRef
.
parse
(<
String
,
dynamic
>{
'type'
:
'@Isolate'
,
'fixedId'
:
'true'
,
'id'
:
'isolates/3'
,
'name'
:
'flutterBinary.cmx'
,
'number'
:
'3'
,
}),
})
!
,
vms
.
IsolateRef
.
parse
(<
String
,
dynamic
>{
'type'
:
'@Isolate'
,
'fixedId'
:
'true'
,
'id'
:
'isolates/4'
,
'name'
:
'0:some_other_dart_name_pattern()'
,
'number'
:
'4'
,
}),
})
!
,
];
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
fakeService
.
vm
=
FakeVM
(
isolates:
isolates
);
return
Future
<
vms
.
VmService
>(()
=>
fakeService
);
...
...
@@ -279,7 +266,7 @@ void main() {
Future
<
vms
.
VmService
>
fakeVmConnectionFunction
(
Uri
uri
,
{
Duration
timeout
,
Duration
?
timeout
,
})
{
fakeService
.
flutterListViews
=
vms
.
Response
.
parse
(
flutterViewCannedResponseMissingIsolateName
);
return
Future
<
vms
.
VmService
>(()
=>
fakeService
);
...
...
@@ -300,11 +287,11 @@ void main() {
class
FakeVmService
extends
Fake
implements
vms
.
VmService
{
bool
disposed
=
false
;
vms
.
Response
flutterListViews
;
vms
.
VM
vm
;
vms
.
Response
?
flutterListViews
;
vms
.
VM
?
vm
;
@override
Future
<
vms
.
VM
>
getVM
()
async
=>
vm
;
Future
<
vms
.
VM
>
getVM
()
async
=>
vm
!
;
@override
Future
<
void
>
dispose
()
async
{
...
...
@@ -312,15 +299,15 @@ class FakeVmService extends Fake implements vms.VmService {
}
@override
Future
<
vms
.
Response
>
callMethod
(
String
method
,
{
String
isolateId
,
Map
<
String
,
dynamic
>
args
})
async
{
Future
<
vms
.
Response
>
callMethod
(
String
method
,
{
String
?
isolateId
,
Map
<
String
,
dynamic
>?
args
})
async
{
if
(
method
==
'_flutter.listViews'
)
{
return
flutterListViews
;
return
flutterListViews
!
;
}
throw
UnimplementedError
(
method
);
}
@override
Future
<
void
>
onDone
;
Future
<
void
>
onDone
=
Future
<
void
>.
value
()
;
}
class
FakeVM
extends
Fake
implements
vms
.
VM
{
...
...
@@ -329,5 +316,5 @@ class FakeVM extends Fake implements vms.VM {
});
@override
List
<
vms
.
IsolateRef
>
isolates
;
List
<
vms
.
IsolateRef
>
?
isolates
;
}
packages/fuchsia_remote_debug_protocol/test/src/runners/ssh_command_runner_test.dart
View file @
a204f038
...
...
@@ -33,8 +33,8 @@ void main() {
});
group
(
'SshCommandRunner.run'
,
()
{
FakeProcessManager
fakeProcessManager
;
FakeProcessResult
fakeProcessResult
;
late
FakeProcessManager
fakeProcessManager
;
late
FakeProcessResult
fakeProcessResult
;
SshCommandRunner
runner
;
setUp
(()
{
...
...
@@ -103,10 +103,10 @@ void main() {
);
fakeProcessResult
.
stdout
=
'somestuff'
;
await
runner
.
run
(
'ls /whatever'
);
final
List
<
String
>
passedCommand
=
fakeProcessManager
.
runCommands
.
single
as
List
<
String
>;
final
List
<
String
?>
passedCommand
=
fakeProcessManager
.
runCommands
.
single
as
List
<
String
?
>;
expect
(
passedCommand
,
contains
(
'-F'
));
final
int
indexOfFlag
=
passedCommand
.
indexOf
(
'-F'
);
final
String
passedConfig
=
passedCommand
[
indexOfFlag
+
1
];
final
String
?
passedConfig
=
passedCommand
[
indexOfFlag
+
1
];
expect
(
passedConfig
,
config
);
});
...
...
@@ -118,7 +118,7 @@ void main() {
);
fakeProcessResult
.
stdout
=
'somestuff'
;
await
runner
.
run
(
'ls /whatever'
);
final
List
<
String
>
passedCommand
=
fakeProcessManager
.
runCommands
.
single
as
List
<
String
>;
final
List
<
String
?>
passedCommand
=
fakeProcessManager
.
runCommands
.
single
as
List
<
String
?
>;
final
int
indexOfFlag
=
passedCommand
.
indexOf
(
'-F'
);
expect
(
indexOfFlag
,
equals
(-
1
));
});
...
...
@@ -126,21 +126,21 @@ void main() {
}
class
FakeProcessManager
extends
Fake
implements
ProcessManager
{
FakeProcessResult
fakeResult
;
FakeProcessResult
?
fakeResult
;
List
<
List
<
dynamic
>>
runCommands
=
<
List
<
dynamic
>>[];
@override
Future
<
ProcessResult
>
run
(
List
<
dynamic
>
command
,
{
String
workingDirectory
,
Map
<
String
,
String
>
environment
,
String
?
workingDirectory
,
Map
<
String
,
String
>
?
environment
,
bool
includeParentEnvironment
=
true
,
bool
runInShell
=
false
,
Encoding
stdoutEncoding
=
systemEncoding
,
Encoding
stderrEncoding
=
systemEncoding
,
})
async
{
runCommands
.
add
(
command
);
return
fakeResult
;
return
fakeResult
!
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment