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
65a79412
Unverified
Commit
65a79412
authored
Feb 26, 2020
by
Jonah Williams
Committed by
GitHub
Feb 26, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[flutter_tools] reduce globals in web validator and chrome launcher (#51443)
parent
64c5a434
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
143 additions
and
92 deletions
+143
-92
context_runner.dart
packages/flutter_tools/lib/src/context_runner.dart
+7
-1
doctor.dart
packages/flutter_tools/lib/src/doctor.dart
+5
-1
globals.dart
packages/flutter_tools/lib/src/globals.dart
+4
-0
flutter_web_platform.dart
...ages/flutter_tools/lib/src/test/flutter_web_platform.dart
+1
-1
chrome.dart
packages/flutter_tools/lib/src/web/chrome.dart
+48
-34
web_device.dart
packages/flutter_tools/lib/src/web/web_device.dart
+4
-4
web_validator.dart
packages/flutter_tools/lib/src/web/web_validator.dart
+19
-5
chrome_test.dart
...ges/flutter_tools/test/general.shard/web/chrome_test.dart
+4
-4
web_validator_test.dart
...tter_tools/test/general.shard/web/web_validator_test.dart
+51
-42
No files found.
packages/flutter_tools/lib/src/context_runner.dart
View file @
65a79412
...
...
@@ -101,7 +101,13 @@ Future<T> runInContext<T>(
logger:
globals
.
logger
,
platform:
globals
.
platform
,
),
ChromeLauncher:
()
=>
const
ChromeLauncher
(),
ChromeLauncher:
()
=>
ChromeLauncher
(
fileSystem:
globals
.
fs
,
processManager:
globals
.
processManager
,
logger:
globals
.
logger
,
operatingSystemUtils:
globals
.
os
,
platform:
globals
.
platform
,
),
CocoaPods:
()
=>
CocoaPods
(),
CocoaPodsValidator:
()
=>
const
CocoaPodsValidator
(),
Config:
()
=>
Config
(
...
...
packages/flutter_tools/lib/src/doctor.dart
View file @
65a79412
...
...
@@ -73,7 +73,11 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
if
(
iosWorkflow
.
appliesToHostPlatform
||
macOSWorkflow
.
appliesToHostPlatform
)
GroupedValidator
(<
DoctorValidator
>[
xcodeValidator
,
cocoapodsValidator
]),
if
(
webWorkflow
.
appliesToHostPlatform
)
const
WebValidator
(),
WebValidator
(
chromeLauncher:
globals
.
chromeLauncher
,
platform:
globals
.
platform
,
fileSystem:
globals
.
fs
,
),
if
(
linuxWorkflow
.
appliesToHostPlatform
)
LinuxDoctorValidator
(),
if
(
windowsWorkflow
.
appliesToHostPlatform
)
...
...
packages/flutter_tools/lib/src/globals.dart
View file @
65a79412
...
...
@@ -25,6 +25,7 @@ import 'ios/mac.dart';
import
'macos/xcode.dart'
;
import
'persistent_tool_state.dart'
;
import
'version.dart'
;
import
'web/chrome.dart'
;
Artifacts
get
artifacts
=>
context
.
get
<
Artifacts
>();
Cache
get
cache
=>
context
.
get
<
Cache
>();
...
...
@@ -148,3 +149,6 @@ final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal(
/// The global Stdio wrapper.
Stdio
get
stdio
=>
context
.
get
<
Stdio
>()
??
const
Stdio
();
/// The [ChromeLauncher] instance.
ChromeLauncher
get
chromeLauncher
=>
context
.
get
<
ChromeLauncher
>();
packages/flutter_tools/lib/src/test/flutter_web_platform.dart
View file @
65a79412
...
...
@@ -642,7 +642,7 @@ class BrowserManager {
bool
headless
=
true
,
})
async
{
final
Chrome
chrome
=
await
chromeLauncher
.
launch
(
url
.
toString
(),
headless:
headless
);
await
globals
.
chromeLauncher
.
launch
(
url
.
toString
(),
headless:
headless
);
final
Completer
<
BrowserManager
>
completer
=
Completer
<
BrowserManager
>();
...
...
packages/flutter_tools/lib/src/web/chrome.dart
View file @
65a79412
...
...
@@ -5,17 +5,16 @@
import
'dart:async'
;
import
'package:meta/meta.dart'
;
import
'package:platform/platform.dart'
;
import
'package:process/process.dart'
;
import
'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
;
import
'../base/common.dart'
;
import
'../base/context.dart'
;
import
'../base/file_system.dart'
;
import
'../base/io.dart'
;
import
'../base/logger.dart'
;
import
'../base/os.dart'
;
import
'../convert.dart'
;
import
'../globals.dart'
as
globals
;
/// The [ChromeLauncher] instance.
ChromeLauncher
get
chromeLauncher
=>
context
.
get
<
ChromeLauncher
>();
/// An environment variable used to override the location of chrome.
const
String
kChromeEnvironment
=
'CHROME_EXECUTABLE'
;
...
...
@@ -30,37 +29,36 @@ const String kMacOSExecutable =
/// The expected executable name on Windows.
const
String
kWindowsExecutable
=
r'Google\Chrome\Application\chrome.exe'
;
/// The possible locations where the chrome executable can be located on windows.
final
List
<
String
>
kWindowsPrefixes
=
<
String
>[
globals
.
platform
.
environment
[
'LOCALAPPDATA'
],
globals
.
platform
.
environment
[
'PROGRAMFILES'
],
globals
.
platform
.
environment
[
'PROGRAMFILES(X86)'
],
];
/// Find the chrome executable on the current platform.
///
/// Does not verify whether the executable exists.
String
findChromeExecutable
(
)
{
if
(
globals
.
platform
.
environment
.
containsKey
(
kChromeEnvironment
))
{
return
globals
.
platform
.
environment
[
kChromeEnvironment
];
String
findChromeExecutable
(
Platform
platform
,
FileSystem
fileSystem
)
{
if
(
platform
.
environment
.
containsKey
(
kChromeEnvironment
))
{
return
platform
.
environment
[
kChromeEnvironment
];
}
if
(
globals
.
platform
.
isLinux
)
{
if
(
platform
.
isLinux
)
{
return
kLinuxExecutable
;
}
if
(
globals
.
platform
.
isMacOS
)
{
if
(
platform
.
isMacOS
)
{
return
kMacOSExecutable
;
}
if
(
globals
.
platform
.
isWindows
)
{
if
(
platform
.
isWindows
)
{
/// The possible locations where the chrome executable can be located on windows.
final
List
<
String
>
kWindowsPrefixes
=
<
String
>[
platform
.
environment
[
'LOCALAPPDATA'
],
platform
.
environment
[
'PROGRAMFILES'
],
platform
.
environment
[
'PROGRAMFILES(X86)'
],
];
final
String
windowsPrefix
=
kWindowsPrefixes
.
firstWhere
((
String
prefix
)
{
if
(
prefix
==
null
)
{
return
false
;
}
final
String
path
=
globals
.
fs
.
path
.
join
(
prefix
,
kWindowsExecutable
);
return
globals
.
fs
.
file
(
path
).
existsSync
();
final
String
path
=
fileSystem
.
path
.
join
(
prefix
,
kWindowsExecutable
);
return
fileSystem
.
file
(
path
).
existsSync
();
},
orElse:
()
=>
'.'
);
return
globals
.
fs
.
path
.
join
(
windowsPrefix
,
kWindowsExecutable
);
return
fileSystem
.
path
.
join
(
windowsPrefix
,
kWindowsExecutable
);
}
throwToolExit
(
'Platform
${
globals.
platform.operatingSystem}
is not supported.'
);
throwToolExit
(
'Platform
${platform.operatingSystem}
is not supported.'
);
return
null
;
}
...
...
@@ -76,7 +74,23 @@ void launchChromeInstance(Chrome chrome) {
/// Responsible for launching chrome with devtools configured.
class
ChromeLauncher
{
const
ChromeLauncher
();
const
ChromeLauncher
({
@required
FileSystem
fileSystem
,
@required
Platform
platform
,
@required
ProcessManager
processManager
,
@required
OperatingSystemUtils
operatingSystemUtils
,
@required
Logger
logger
,
})
:
_fileSystem
=
fileSystem
,
_platform
=
platform
,
_processManager
=
processManager
,
_operatingSystemUtils
=
operatingSystemUtils
,
_logger
=
logger
;
final
FileSystem
_fileSystem
;
final
Platform
_platform
;
final
ProcessManager
_processManager
;
final
OperatingSystemUtils
_operatingSystemUtils
;
final
Logger
_logger
;
static
bool
get
hasChromeInstance
=>
_currentCompleter
.
isCompleted
;
...
...
@@ -84,9 +98,9 @@ class ChromeLauncher {
/// Whether we can locate the chrome executable.
bool
canFindChrome
()
{
final
String
chrome
=
findChromeExecutable
();
final
String
chrome
=
findChromeExecutable
(
_platform
,
_fileSystem
);
try
{
return
globals
.
processManager
.
canRun
(
chrome
);
return
_
processManager
.
canRun
(
chrome
);
}
on
ArgumentError
{
return
false
;
}
...
...
@@ -105,14 +119,14 @@ class ChromeLauncher {
// This is a JSON file which contains configuration from the
// browser session, such as window position. It is located
// under the Chrome data-dir folder.
final
String
preferencesPath
=
globals
.
fs
.
path
.
join
(
'Default'
,
'preferences'
);
final
String
preferencesPath
=
_fileSystem
.
path
.
join
(
'Default'
,
'preferences'
);
final
String
chromeExecutable
=
findChromeExecutable
();
final
Directory
activeDataDir
=
globals
.
fs
.
systemTempDirectory
.
createTempSync
(
'flutter_tool.'
);
final
String
chromeExecutable
=
findChromeExecutable
(
_platform
,
_fileSystem
);
final
Directory
activeDataDir
=
_fileSystem
.
systemTempDirectory
.
createTempSync
(
'flutter_tool.'
);
// Seed data dir with previous state.
final
File
savedPreferencesFile
=
globals
.
fs
.
file
(
globals
.
fs
.
path
.
join
(
dataDir
?.
path
??
''
,
preferencesPath
));
final
File
destinationFile
=
globals
.
fs
.
file
(
globals
.
fs
.
path
.
join
(
activeDataDir
.
path
,
preferencesPath
));
final
File
savedPreferencesFile
=
_fileSystem
.
file
(
_fileSystem
.
path
.
join
(
dataDir
?.
path
??
''
,
preferencesPath
));
final
File
destinationFile
=
_fileSystem
.
file
(
_fileSystem
.
path
.
join
(
activeDataDir
.
path
,
preferencesPath
));
if
(
dataDir
!=
null
)
{
if
(
savedPreferencesFile
.
existsSync
())
{
destinationFile
.
parent
.
createSync
(
recursive:
true
);
...
...
@@ -120,7 +134,7 @@ class ChromeLauncher {
}
}
final
int
port
=
debugPort
??
await
globals
.
o
s
.
findFreePort
();
final
int
port
=
debugPort
??
await
_operatingSystemUtil
s
.
findFreePort
();
final
List
<
String
>
args
=
<
String
>[
chromeExecutable
,
// Using a tmp directory ensures that a new instance of chrome launches
...
...
@@ -143,7 +157,7 @@ class ChromeLauncher {
url
,
];
final
Process
process
=
await
globals
.
processManager
.
start
(
args
);
final
Process
process
=
await
_
processManager
.
start
(
args
);
// When the process exits, copy the user settings back to the provided
// data-dir.
...
...
@@ -164,7 +178,7 @@ class ChromeLauncher {
.
transform
(
utf8
.
decoder
)
.
transform
(
const
LineSplitter
())
.
listen
((
String
line
)
{
globals
.
printTrace
(
'[CHROME]:
$line
'
);
_logger
.
printTrace
(
'[CHROME]:
$line
'
);
});
// Wait until the DevTools are listening before trying to connect.
...
...
@@ -172,7 +186,7 @@ class ChromeLauncher {
.
transform
(
utf8
.
decoder
)
.
transform
(
const
LineSplitter
())
.
map
((
String
line
)
{
globals
.
printTrace
(
'[CHROME]:
$line
'
);
_logger
.
printTrace
(
'[CHROME]:
$line
'
);
return
line
;
})
.
firstWhere
((
String
line
)
=>
line
.
startsWith
(
'DevTools listening'
),
orElse:
()
{
...
...
packages/flutter_tools/lib/src/web/web_device.dart
View file @
65a79412
...
...
@@ -80,7 +80,7 @@ class ChromeDevice extends Device {
Future
<
String
>
get
emulatorId
async
=>
null
;
@override
bool
isSupported
()
=>
featureFlags
.
isWebEnabled
&&
chromeLauncher
.
canFindChrome
();
bool
isSupported
()
=>
featureFlags
.
isWebEnabled
&&
globals
.
chromeLauncher
.
canFindChrome
();
@override
String
get
name
=>
'Chrome'
;
...
...
@@ -109,7 +109,7 @@ class ChromeDevice extends Device {
}
}
}
else
{
final
String
chrome
=
findChromeExecutable
();
final
String
chrome
=
findChromeExecutable
(
globals
.
platform
,
globals
.
fs
);
final
ProcessResult
result
=
await
globals
.
processManager
.
run
(<
String
>[
chrome
,
'--version'
,
...
...
@@ -136,7 +136,7 @@ class ChromeDevice extends Device {
final
String
url
=
platformArgs
[
'uri'
]
as
String
;
final
bool
launchChrome
=
platformArgs
[
'no-launch-chrome'
]
!=
true
;
if
(
launchChrome
)
{
_chrome
=
await
chromeLauncher
.
launch
(
_chrome
=
await
globals
.
chromeLauncher
.
launch
(
url
,
dataDir:
globals
.
fs
.
currentDirectory
.
childDirectory
(
'.dart_tool'
)
...
...
@@ -177,7 +177,7 @@ class ChromeDevice extends Device {
class
WebDevices
extends
PollingDeviceDiscovery
{
WebDevices
()
:
super
(
'chrome'
);
final
bool
_chromeIsAvailable
=
chromeLauncher
.
canFindChrome
();
final
bool
_chromeIsAvailable
=
globals
.
chromeLauncher
.
canFindChrome
();
final
ChromeDevice
_webDevice
=
ChromeDevice
();
final
WebServerDevice
_webServerDevice
=
WebServerDevice
();
...
...
packages/flutter_tools/lib/src/web/web_validator.dart
View file @
65a79412
...
...
@@ -2,20 +2,34 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:meta/meta.dart'
;
import
'package:platform/platform.dart'
;
import
'../base/file_system.dart'
;
import
'../doctor.dart'
;
import
'../globals.dart'
as
globals
;
import
'chrome.dart'
;
/// A validator that checks whether chrome is installed and can run.
class
WebValidator
extends
DoctorValidator
{
const
WebValidator
()
:
super
(
'Chrome - develop for the web'
);
const
WebValidator
({
@required
Platform
platform
,
@required
ChromeLauncher
chromeLauncher
,
@required
FileSystem
fileSystem
,
})
:
_platform
=
platform
,
_chromeLauncher
=
chromeLauncher
,
_fileSystem
=
fileSystem
,
super
(
'Chrome - develop for the web'
);
final
Platform
_platform
;
final
ChromeLauncher
_chromeLauncher
;
final
FileSystem
_fileSystem
;
@override
Future
<
ValidationResult
>
validate
()
async
{
final
String
chrome
=
findChromeExecutable
();
final
bool
canRunChrome
=
chromeLauncher
.
canFindChrome
();
final
String
chrome
=
findChromeExecutable
(
_platform
,
_fileSystem
);
final
bool
canRunChrome
=
_
chromeLauncher
.
canFindChrome
();
final
List
<
ValidationMessage
>
messages
=
<
ValidationMessage
>[
if
(
globals
.
platform
.
environment
.
containsKey
(
kChromeEnvironment
))
if
(
_
platform
.
environment
.
containsKey
(
kChromeEnvironment
))
if
(!
canRunChrome
)
ValidationMessage
.
hint
(
'
$chrome
is not executable.'
)
else
...
...
packages/flutter_tools/test/general.shard/web/chrome_test.dart
View file @
65a79412
...
...
@@ -71,7 +71,7 @@ void main() {
}
test
(
'can launch chrome and connect to the devtools'
,
()
=>
testbed
.
run
(()
async
{
await
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
);
await
globals
.
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
);
final
VerificationResult
result
=
verify
(
globals
.
processManager
.
start
(
captureAny
));
expect
(
result
.
captured
.
single
,
containsAll
(
expectChromeArgs
()));
...
...
@@ -79,7 +79,7 @@ void main() {
}));
test
(
'can launch chrome with a custom debug port'
,
()
=>
testbed
.
run
(()
async
{
await
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
debugPort:
10000
);
await
globals
.
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
debugPort:
10000
);
final
VerificationResult
result
=
verify
(
globals
.
processManager
.
start
(
captureAny
));
expect
(
result
.
captured
.
single
,
containsAll
(
expectChromeArgs
(
debugPort:
10000
)));
...
...
@@ -87,7 +87,7 @@ void main() {
}));
test
(
'can launch chrome headless'
,
()
=>
testbed
.
run
(()
async
{
await
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
headless:
true
);
await
globals
.
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
headless:
true
);
final
VerificationResult
result
=
verify
(
globals
.
processManager
.
start
(
captureAny
));
expect
(
result
.
captured
.
single
,
containsAll
(
expectChromeArgs
()));
...
...
@@ -104,7 +104,7 @@ void main() {
..
createSync
(
recursive:
true
)
..
writeAsStringSync
(
'example'
);
await
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
dataDir:
dataDir
);
await
globals
.
chromeLauncher
.
launch
(
'example_url'
,
skipCheck:
true
,
dataDir:
dataDir
);
final
VerificationResult
result
=
verify
(
globals
.
processManager
.
start
(
captureAny
));
final
String
arg
=
(
result
.
captured
.
single
as
List
<
String
>)
.
firstWhere
((
String
arg
)
=>
arg
.
startsWith
(
'--user-data-dir='
));
...
...
packages/flutter_tools/test/general.shard/web/web_validator_test.dart
View file @
65a79412
...
...
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:file/memory.dart'
;
import
'package:flutter_tools/src/base/file_system.dart'
;
import
'package:flutter_tools/src/doctor.dart'
;
import
'package:flutter_tools/src/web/chrome.dart'
;
import
'package:flutter_tools/src/web/web_validator.dart'
;
...
...
@@ -10,56 +12,63 @@ import 'package:process/process.dart';
import
'package:platform/platform.dart'
;
import
'../../src/common.dart'
;
import
'../../src/
testbed
.dart'
;
import
'../../src/
fake_process_manager
.dart'
;
void
main
(
)
{
group
(
'WebValidator'
,
()
{
Testbed
testbed
;
WebValidator
webValidato
r
;
MockPlatform
mockPlatfor
m
;
MockProcessManager
mockProcessManage
r
;
Platform
platform
;
ProcessManager
processManager
;
ChromeLauncher
chromeLaunche
r
;
FileSystem
fileSyste
m
;
WebValidator
webValidato
r
;
setUp
(()
{
mockProcessManager
=
MockProcessManager
();
testbed
=
Testbed
(
setup:
()
{
when
(
mockProcessManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
true
);
return
null
;
},
overrides:
<
Type
,
Generator
>{
Platform:
()
=>
mockPlatform
,
ProcessManager:
()
=>
mockProcessManager
,
});
webValidator
=
const
WebValidator
();
mockPlatform
=
MockPlatform
();
when
(
mockPlatform
.
isMacOS
).
thenReturn
(
true
);
when
(
mockPlatform
.
isWindows
).
thenReturn
(
false
);
when
(
mockPlatform
.
isLinux
).
thenReturn
(
false
);
});
setUp
(()
{
fileSystem
=
MemoryFileSystem
.
test
();
processManager
=
MockProcessManager
();
platform
=
FakePlatform
(
operatingSystem:
'macos'
,
environment:
<
String
,
String
>{},
);
chromeLauncher
=
ChromeLauncher
(
fileSystem:
fileSystem
,
platform:
platform
,
processManager:
processManager
,
operatingSystemUtils:
null
,
logger:
null
,
);
webValidator
=
webValidator
=
WebValidator
(
platform:
platform
,
chromeLauncher:
chromeLauncher
,
fileSystem:
fileSystem
,
);
});
test
(
'Can find macOS executable '
,
()
=>
testbed
.
run
(()
async
{
final
ValidationResult
result
=
await
webValidator
.
validate
();
expect
(
result
.
type
,
ValidationType
.
installed
);
}));
testWithoutContext
(
'WebValidator can find executable on macOS'
,
()
async
{
when
(
processManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
true
);
test
(
'Can notice missing macOS executable '
,
()
=>
testbed
.
run
(()
async
{
when
(
mockProcessManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
false
);
final
ValidationResult
result
=
await
webValidator
.
validate
();
expect
(
result
.
type
,
ValidationType
.
missing
);
}));
final
ValidationResult
result
=
await
webValidator
.
validate
();
test
(
"Doesn't warn about CHROME_EXECUTABLE unless it cant find chrome "
,
()
=>
testbed
.
run
(()
async
{
when
(
mockProcessManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
false
);
final
ValidationResult
result
=
await
webValidator
.
validate
();
expect
(
result
.
messages
,
<
ValidationMessage
>[
ValidationMessage
.
hint
(
'Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.'
),
]);
expect
(
result
.
type
,
ValidationType
.
missing
);
}));
expect
(
result
.
type
,
ValidationType
.
installed
);
});
}
class
MockPlatform
extends
Mock
implements
Platform
{
@override
Map
<
String
,
String
>
get
environment
=>
const
<
String
,
String
>{};
testWithoutContext
(
'WebValidator Can notice missing macOS executable '
,
()
async
{
when
(
processManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
false
);
final
ValidationResult
result
=
await
webValidator
.
validate
();
expect
(
result
.
type
,
ValidationType
.
missing
);
});
testWithoutContext
(
'WebValidator does not warn about CHROME_EXECUTABLE unless it cant find chrome '
,
()
async
{
when
(
processManager
.
canRun
(
kMacOSExecutable
)).
thenReturn
(
false
);
final
ValidationResult
result
=
await
webValidator
.
validate
();
expect
(
result
.
messages
,
<
ValidationMessage
>[
ValidationMessage
.
hint
(
'Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.'
),
]);
expect
(
result
.
type
,
ValidationType
.
missing
);
});
}
class
MockProcessManager
extends
Mock
implements
ProcessManager
{}
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