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
c1c12aa3
Unverified
Commit
c1c12aa3
authored
May 11, 2021
by
Jenn Magder
Committed by
GitHub
May 11, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add posix permission chown suggestion to io error handling (#81942)
parent
0581c05c
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
357 additions
and
92 deletions
+357
-92
error_handling_io.dart
packages/flutter_tools/lib/src/base/error_handling_io.dart
+46
-11
error_handling_io_test.dart
...tools/test/general.shard/base/error_handling_io_test.dart
+310
-79
config_test.dart
packages/flutter_tools/test/general.shard/config_test.dart
+1
-2
No files found.
packages/flutter_tools/lib/src/base/error_handling_io.dart
View file @
c1c12aa3
...
...
@@ -212,6 +212,7 @@ class ErrorHandlingFile
)),
platform:
_platform
,
failureMessage:
'Flutter failed to write to a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -221,6 +222,7 @@ class ErrorHandlingFile
()
=>
delegate
.
readAsStringSync
(),
platform:
_platform
,
failureMessage:
'Flutter failed to read a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -234,6 +236,7 @@ class ErrorHandlingFile
()
=>
delegate
.
writeAsBytesSync
(
bytes
,
mode:
mode
,
flush:
flush
),
platform:
_platform
,
failureMessage:
'Flutter failed to write to a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -253,6 +256,7 @@ class ErrorHandlingFile
)),
platform:
_platform
,
failureMessage:
'Flutter failed to write to a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -272,6 +276,7 @@ class ErrorHandlingFile
),
platform:
_platform
,
failureMessage:
'Flutter failed to write to a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -283,6 +288,7 @@ class ErrorHandlingFile
),
platform:
_platform
,
failureMessage:
'Flutter failed to create file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
recursive
?
null
:
_posixPermissionSuggestion
(<
String
>[
delegate
.
parent
.
path
]),
);
}
...
...
@@ -294,6 +300,7 @@ class ErrorHandlingFile
),
platform:
_platform
,
failureMessage:
'Flutter failed to open a file at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
delegate
.
path
]),
);
}
...
...
@@ -307,7 +314,8 @@ class ErrorHandlingFile
_runSync
<
void
>(
()
=>
delegate
.
openSync
(
mode:
FileMode
.
read
).
closeSync
(),
platform:
_platform
,
failureMessage:
'Flutter failed to copy
$path
to
$newPath
due to source location error'
failureMessage:
'Flutter failed to copy
$path
to
$newPath
due to source location error'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
path
]),
);
// Next check if the destination file can be written. If not, bail through
// error handling.
...
...
@@ -347,11 +355,17 @@ class ErrorHandlingFile
source
?.
closeSync
();
sink
?.
closeSync
();
}
},
platform:
_platform
,
failureMessage:
'Flutter failed to copy
$path
to
$newPath
due to unknown error'
);
},
platform:
_platform
,
failureMessage:
'Flutter failed to copy
$path
to
$newPath
due to unknown error'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(<
String
>[
path
,
resultFile
.
parent
.
path
]),
);
// The original copy failed, but the manual copy worked.
return
wrapFile
(
resultFile
);
}
String
_posixPermissionSuggestion
(
List
<
String
>
paths
)
=>
'Try running:
\n
'
' sudo chown -R
\$
(whoami)
${paths.map(fileSystem.path.absolute).join(' ')}
'
;
@override
String
toString
()
=>
delegate
.
toString
();
}
...
...
@@ -420,6 +434,7 @@ class ErrorHandlingDirectory
platform:
_platform
,
failureMessage:
'Flutter failed to create a directory at "
${delegate.path}
"'
,
posixPermissionSuggestion:
recursive
?
null
:
_posixPermissionSuggestion
(
delegate
.
parent
.
path
),
);
}
...
...
@@ -450,6 +465,7 @@ class ErrorHandlingDirectory
platform:
_platform
,
failureMessage:
'Flutter failed to create a directory at "
${delegate.path}
"'
,
posixPermissionSuggestion:
recursive
?
null
:
_posixPermissionSuggestion
(
delegate
.
parent
.
path
),
);
}
...
...
@@ -460,6 +476,7 @@ class ErrorHandlingDirectory
platform:
_platform
,
failureMessage:
'Flutter failed to delete a directory at "
${delegate.path}
"'
,
posixPermissionSuggestion:
recursive
?
null
:
_posixPermissionSuggestion
(
delegate
.
path
),
);
}
...
...
@@ -470,6 +487,7 @@ class ErrorHandlingDirectory
platform:
_platform
,
failureMessage:
'Flutter failed to delete a directory at "
${delegate.path}
"'
,
posixPermissionSuggestion:
recursive
?
null
:
_posixPermissionSuggestion
(
delegate
.
path
),
);
}
...
...
@@ -480,9 +498,13 @@ class ErrorHandlingDirectory
platform:
_platform
,
failureMessage:
'Flutter failed to check for directory existence at "
${delegate.path}
"'
,
posixPermissionSuggestion:
_posixPermissionSuggestion
(
delegate
.
parent
.
path
),
);
}
String
_posixPermissionSuggestion
(
String
path
)
=>
'Try running:
\n
'
' sudo chown -R
\$
(whoami)
${fileSystem.path.absolute(path)}
'
;
@override
String
toString
()
=>
delegate
.
toString
();
}
...
...
@@ -538,6 +560,7 @@ const String _kNoExecutableFound = 'The Flutter tool could not locate an executa
Future
<
T
>
_run
<
T
>(
Future
<
T
>
Function
()
op
,
{
required
Platform
platform
,
String
?
failureMessage
,
String
?
posixPermissionSuggestion
,
})
async
{
assert
(
platform
!=
null
);
try
{
...
...
@@ -551,14 +574,14 @@ Future<T> _run<T>(Future<T> Function() op, {
if
(
platform
.
isWindows
)
{
_handleWindowsException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
);
}
else
if
(
platform
.
isLinux
||
platform
.
isMacOS
)
{
_handlePosixException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
);
_handlePosixException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
,
posixPermissionSuggestion
);
}
rethrow
;
}
on
io
.
ProcessException
catch
(
e
)
{
if
(
platform
.
isWindows
)
{
_handleWindowsException
(
e
,
failureMessage
,
e
.
errorCode
);
}
else
if
(
platform
.
isLinux
||
platform
.
isMacOS
)
{
_handlePosixException
(
e
,
failureMessage
,
e
.
errorCode
);
_handlePosixException
(
e
,
failureMessage
,
e
.
errorCode
,
posixPermissionSuggestion
);
}
rethrow
;
}
...
...
@@ -567,6 +590,7 @@ Future<T> _run<T>(Future<T> Function() op, {
T
_runSync
<
T
>(
T
Function
()
op
,
{
required
Platform
platform
,
String
?
failureMessage
,
String
?
posixPermissionSuggestion
,
})
{
assert
(
platform
!=
null
);
try
{
...
...
@@ -580,14 +604,14 @@ T _runSync<T>(T Function() op, {
if
(
platform
.
isWindows
)
{
_handleWindowsException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
);
}
else
if
(
platform
.
isLinux
||
platform
.
isMacOS
)
{
_handlePosixException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
);
_handlePosixException
(
e
,
failureMessage
,
e
.
osError
?.
errorCode
??
0
,
posixPermissionSuggestion
);
}
rethrow
;
}
on
io
.
ProcessException
catch
(
e
)
{
if
(
platform
.
isWindows
)
{
_handleWindowsException
(
e
,
failureMessage
,
e
.
errorCode
);
}
else
if
(
platform
.
isLinux
||
platform
.
isMacOS
)
{
_handlePosixException
(
e
,
failureMessage
,
e
.
errorCode
);
_handlePosixException
(
e
,
failureMessage
,
e
.
errorCode
,
posixPermissionSuggestion
);
}
rethrow
;
}
...
...
@@ -617,6 +641,9 @@ class ErrorHandlingProcessManager extends ProcessManager {
return
_runSync
(
()
=>
_delegate
.
canRun
(
executable
,
workingDirectory:
workingDirectory
),
platform:
_platform
,
failureMessage:
'Flutter failed to run "
$executable
"'
,
posixPermissionSuggestion:
'Try running:
\n
'
' sudo chown -R
\$
(whoami)
$executable
&& chmod u+rx
$executable
'
,
);
}
...
...
@@ -695,7 +722,7 @@ class ErrorHandlingProcessManager extends ProcessManager {
}
}
void
_handlePosixException
(
Exception
e
,
String
?
message
,
int
errorCode
)
{
void
_handlePosixException
(
Exception
e
,
String
?
message
,
int
errorCode
,
String
?
posixPermissionSuggestion
)
{
// From:
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
...
...
@@ -714,10 +741,18 @@ void _handlePosixException(Exception e, String? message, int errorCode) {
break
;
case
eperm:
case
eacces:
errorMessage
=
'
$message
. The flutter tool cannot access the file or directory.
\n
'
'Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.'
;
final
StringBuffer
errorBuffer
=
StringBuffer
();
if
(
message
!=
null
&&
message
.
isNotEmpty
)
{
errorBuffer
.
writeln
(
'
$message
.'
);
}
else
{
errorBuffer
.
writeln
(
'The flutter tool cannot access the file or directory.'
);
}
errorBuffer
.
writeln
(
'Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.'
);
if
(
posixPermissionSuggestion
!=
null
&&
posixPermissionSuggestion
.
isNotEmpty
)
{
errorBuffer
.
writeln
(
posixPermissionSuggestion
);
}
errorMessage
=
errorBuffer
.
toString
();
break
;
default
:
// Caller must rethrow the exception.
...
...
packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart
View file @
c1c12aa3
...
...
@@ -15,7 +15,6 @@ import 'package:flutter_tools/src/base/file_system.dart';
import
'package:flutter_tools/src/base/io.dart'
;
import
'package:flutter_tools/src/base/platform.dart'
;
import
'package:mockito/mockito.dart'
;
import
'package:path/path.dart'
as
path
;
// flutter_ignore: package_path_import
import
'package:process/process.dart'
;
import
'../../src/common.dart'
;
...
...
@@ -23,7 +22,6 @@ import '../../src/fake_process_manager.dart';
class
MockFile
extends
Mock
implements
File
{}
class
MockFileSystem
extends
Mock
implements
FileSystem
{}
class
MockPathContext
extends
Mock
implements
path
.
Context
{}
class
MockDirectory
extends
Mock
implements
Directory
{}
class
MockRandomAccessFile
extends
Mock
implements
RandomAccessFile
{}
...
...
@@ -48,7 +46,11 @@ void setupWriteMocks({
int
errorCode
,
})
{
final
MockFile
mockFile
=
MockFile
();
final
MockDirectory
mockParentDirectory
=
MockDirectory
();
when
(
mockFileSystem
.
file
(
any
)).
thenReturn
(
mockFile
);
when
(
mockFile
.
path
).
thenReturn
(
'parent/file'
);
when
(
mockFile
.
parent
).
thenReturn
(
mockParentDirectory
);
when
(
mockParentDirectory
.
path
).
thenReturn
(
'parent'
);
when
(
mockFile
.
writeAsBytes
(
any
,
mode:
anyNamed
(
'mode'
),
...
...
@@ -88,7 +90,11 @@ void setupReadMocks({
int
errorCode
,
})
{
final
MockFile
mockFile
=
MockFile
();
final
MockDirectory
mockParentDirectory
=
MockDirectory
();
when
(
mockFileSystem
.
file
(
any
)).
thenReturn
(
mockFile
);
when
(
mockFile
.
path
).
thenReturn
(
'parent/file'
);
when
(
mockFile
.
parent
).
thenReturn
(
mockParentDirectory
);
when
(
mockParentDirectory
.
path
).
thenReturn
(
'parent'
);
when
(
mockFileSystem
.
currentDirectory
).
thenThrow
(
FileSystemException
(
''
,
''
,
OSError
(
''
,
errorCode
)));
when
(
mockFile
.
readAsStringSync
(
encoding:
anyNamed
(
'encoding'
),
...
...
@@ -101,7 +107,12 @@ void setupDirectoryMocks({
int
errorCode
,
})
{
final
MockDirectory
mockDirectory
=
MockDirectory
();
final
MockDirectory
mockParentDirectory
=
MockDirectory
();
when
(
mockDirectory
.
parent
).
thenReturn
(
mockParentDirectory
);
when
(
mockFileSystem
.
directory
(
any
)).
thenReturn
(
mockDirectory
);
when
(
mockDirectory
.
path
).
thenReturn
(
'parent/directory'
);
when
(
mockDirectory
.
parent
).
thenReturn
(
mockParentDirectory
);
when
(
mockParentDirectory
.
path
).
thenReturn
(
'parent'
);
when
(
mockDirectory
.
createTemp
(
any
)).
thenAnswer
((
_
)
async
{
throw
FileSystemException
(
''
,
''
,
OSError
(
''
,
errorCode
));
});
...
...
@@ -188,7 +199,8 @@ void main() {
delegate:
mockFileSystem
,
platform:
windowsPlatform
,
);
when
(
mockFileSystem
.
path
).
thenReturn
(
MockPathContext
());
// For fs.path.absolute usage.
when
(
mockFileSystem
.
path
).
thenReturn
(
MemoryFileSystem
.
test
().
path
);
});
testWithoutContext
(
'bypasses error handling when withAllowedFailure is used'
,
()
{
...
...
@@ -372,6 +384,7 @@ void main() {
MockFileSystem
mockFileSystem
;
ErrorHandlingFileSystem
fs
;
FileExceptionHandler
exceptionHandler
;
setUp
(()
{
mockFileSystem
=
MockFileSystem
();
...
...
@@ -379,51 +392,113 @@ void main() {
delegate:
mockFileSystem
,
platform:
linuxPlatform
,
);
when
(
mockFileSystem
.
path
).
thenReturn
(
MockPathContext
());
// For fs.path.absolute usage.
when
(
mockFileSystem
.
path
).
thenReturn
(
MemoryFileSystem
.
test
().
path
);
exceptionHandler
=
FileExceptionHandler
();
});
testWithoutContext
(
'when access is denied'
,
()
async
{
setupWriteMocks
(
mockFileSystem:
mockFileSystem
,
fs:
fs
,
errorCode:
eacces
,
final
ErrorHandlingFileSystem
fileSystem
=
ErrorHandlingFileSystem
(
delegate:
MemoryFileSystem
.
test
(
opHandle:
exceptionHandler
.
opHandle
),
platform:
linuxPlatform
,
);
final
Directory
directory
=
fileSystem
.
directory
(
'dir'
)..
createSync
();
final
File
file
=
directory
.
childFile
(
'file'
);
final
File
file
=
fs
.
file
(
'file'
);
const
String
expectedMessage
=
'The flutter tool cannot access the file or directory'
;
expect
(()
async
=>
file
.
writeAsBytes
(<
int
>[
0
]),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
async
=>
file
.
writeAsString
(
''
),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
writeAsBytesSync
(<
int
>[
0
]),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
writeAsStringSync
(
''
),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
openSync
(),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
createSync
(),
throwsToolExit
(
message:
expectedMessage
));
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
create
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
write
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
read
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
delete
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
const
String
writeMessage
=
'Flutter failed to write to a file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir/file'
;
expect
(()
async
=>
file
.
writeAsBytes
(<
int
>[
0
]),
throwsToolExit
(
message:
writeMessage
));
expect
(()
async
=>
file
.
writeAsString
(
''
),
throwsToolExit
(
message:
writeMessage
));
expect
(()
=>
file
.
writeAsBytesSync
(<
int
>[
0
]),
throwsToolExit
(
message:
writeMessage
));
expect
(()
=>
file
.
writeAsStringSync
(
''
),
throwsToolExit
(
message:
writeMessage
));
const
String
createMessage
=
'Flutter failed to create file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir'
;
expect
(()
=>
file
.
createSync
(),
throwsToolExit
(
message:
createMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
file
.
createSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
const
String
readMessage
=
'Flutter failed to read a file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir/file'
;
expect
(()
=>
file
.
readAsStringSync
(),
throwsToolExit
(
message:
readMessage
));
});
testWithoutContext
(
'when access is denied for directories'
,
()
async
{
setupDirectoryMocks
(
mockFileSystem:
mockFileSystem
,
fs:
fs
,
errorCode:
eperm
,
final
ErrorHandlingFileSystem
fileSystem
=
ErrorHandlingFileSystem
(
delegate:
MemoryFileSystem
.
test
(
opHandle:
exceptionHandler
.
opHandle
),
platform:
linuxPlatform
,
);
final
Directory
parent
=
fileSystem
.
directory
(
'parent'
)..
createSync
();
final
Directory
directory
=
parent
.
childDirectory
(
'childDir'
);
final
Directory
directory
=
fs
.
directory
(
'file'
);
exceptionHandler
.
addError
(
directory
,
FileSystemOp
.
create
,
FileSystemException
(
''
,
directory
.
path
,
const
OSError
(
''
,
eperm
)),
);
exceptionHandler
.
addError
(
directory
,
FileSystemOp
.
delete
,
FileSystemException
(
''
,
directory
.
path
,
const
OSError
(
''
,
eperm
)),
);
const
String
expectedMessage
=
'The flutter tool cannot access the file or directory'
;
const
String
createMessage
=
'Flutter failed to create a directory at "parent/childDir".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /parent'
;
expect
(()
async
=>
directory
.
create
(),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
async
=>
directory
.
delete
(),
throwsToolExit
(
message:
expectedMessage
));
throwsToolExit
(
message:
createMessage
));
expect
(()
=>
directory
.
createSync
(),
throwsToolExit
(
message:
expectedMessage
));
throwsToolExit
(
message:
createMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
directory
.
createSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
const
String
deleteMessage
=
'Flutter failed to delete a directory at "parent/childDir".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /parent'
;
expect
(()
=>
directory
.
deleteSync
(),
throwsToolExit
(
message:
expectedMessage
));
throwsToolExit
(
message:
deleteMessage
));
expect
(()
async
=>
directory
.
delete
(),
throwsToolExit
(
message:
deleteMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
directory
.
deleteSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
});
testWithoutContext
(
'when writing to a full device'
,
()
async
{
...
...
@@ -496,6 +571,7 @@ void main() {
const
int
eacces
=
13
;
MockFileSystem
mockFileSystem
;
ErrorHandlingFileSystem
fs
;
FileExceptionHandler
exceptionHandler
;
setUp
(()
{
mockFileSystem
=
MockFileSystem
();
...
...
@@ -503,49 +579,113 @@ void main() {
delegate:
mockFileSystem
,
platform:
macOSPlatform
,
);
when
(
mockFileSystem
.
path
).
thenReturn
(
MockPathContext
());
// For fs.path.absolute usage.
when
(
mockFileSystem
.
path
).
thenReturn
(
MemoryFileSystem
.
test
().
path
);
exceptionHandler
=
FileExceptionHandler
();
});
testWithoutContext
(
'when access is denied'
,
()
async
{
setupWriteMocks
(
mockFileSystem:
mockFileSystem
,
fs:
fs
,
errorCode:
eacces
,
final
ErrorHandlingFileSystem
fileSystem
=
ErrorHandlingFileSystem
(
delegate:
MemoryFileSystem
.
test
(
opHandle:
exceptionHandler
.
opHandle
),
platform:
macOSPlatform
,
);
final
Directory
directory
=
fileSystem
.
directory
(
'dir'
)..
createSync
();
final
File
file
=
directory
.
childFile
(
'file'
);
final
File
file
=
fs
.
file
(
'file'
);
const
String
expectedMessage
=
'The flutter tool cannot access the file'
;
expect
(()
async
=>
file
.
writeAsBytes
(<
int
>[
0
]),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
async
=>
file
.
writeAsString
(
''
),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
writeAsBytesSync
(<
int
>[
0
]),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
writeAsStringSync
(
''
),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
file
.
openSync
(),
throwsToolExit
(
message:
expectedMessage
));
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
create
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
write
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
read
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
exceptionHandler
.
addError
(
file
,
FileSystemOp
.
delete
,
FileSystemException
(
''
,
file
.
path
,
const
OSError
(
''
,
eacces
)),
);
const
String
writeMessage
=
'Flutter failed to write to a file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir/file'
;
expect
(()
async
=>
file
.
writeAsBytes
(<
int
>[
0
]),
throwsToolExit
(
message:
writeMessage
));
expect
(()
async
=>
file
.
writeAsString
(
''
),
throwsToolExit
(
message:
writeMessage
));
expect
(()
=>
file
.
writeAsBytesSync
(<
int
>[
0
]),
throwsToolExit
(
message:
writeMessage
));
expect
(()
=>
file
.
writeAsStringSync
(
''
),
throwsToolExit
(
message:
writeMessage
));
const
String
createMessage
=
'Flutter failed to create file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir'
;
expect
(()
=>
file
.
createSync
(),
throwsToolExit
(
message:
createMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
file
.
createSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
const
String
readMessage
=
'Flutter failed to read a file at "dir/file".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /dir/file'
;
expect
(()
=>
file
.
readAsStringSync
(),
throwsToolExit
(
message:
readMessage
));
});
testWithoutContext
(
'when access is denied for directories'
,
()
async
{
setupDirectoryMocks
(
mockFileSystem:
mockFileSystem
,
fs:
fs
,
errorCode:
eperm
,
final
ErrorHandlingFileSystem
fileSystem
=
ErrorHandlingFileSystem
(
delegate:
MemoryFileSystem
.
test
(
opHandle:
exceptionHandler
.
opHandle
),
platform:
macOSPlatform
,
);
final
Directory
parent
=
fileSystem
.
directory
(
'parent'
)..
createSync
();
final
Directory
directory
=
parent
.
childDirectory
(
'childDir'
);
final
Directory
directory
=
fs
.
directory
(
'file'
);
exceptionHandler
.
addError
(
directory
,
FileSystemOp
.
create
,
FileSystemException
(
''
,
directory
.
path
,
const
OSError
(
''
,
eperm
)),
);
exceptionHandler
.
addError
(
directory
,
FileSystemOp
.
delete
,
FileSystemException
(
''
,
directory
.
path
,
const
OSError
(
''
,
eperm
)),
);
const
String
expectedMessage
=
'The flutter tool cannot access the file or directory'
;
const
String
createMessage
=
'Flutter failed to create a directory at "parent/childDir".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /parent'
;
expect
(()
async
=>
directory
.
create
(),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
async
=>
directory
.
delete
(),
throwsToolExit
(
message:
expectedMessage
));
expect
(()
=>
directory
.
createSync
(),
throwsToolExit
(
message:
expectedMessage
));
throwsToolExit
(
message:
createMessage
));
expect
(()
=>
directory
.
createSync
(),
throwsToolExit
(
message:
createMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
directory
.
createSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
const
String
deleteMessage
=
'Flutter failed to delete a directory at "parent/childDir".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /parent'
;
expect
(()
=>
directory
.
deleteSync
(),
throwsToolExit
(
message:
expectedMessage
));
throwsToolExit
(
message:
deleteMessage
));
expect
(()
async
=>
directory
.
delete
(),
throwsToolExit
(
message:
deleteMessage
));
// Recursive does not contain the "sudo chown" suggestion.
expect
(()
async
=>
directory
.
deleteSync
(
recursive:
true
),
throwsA
(
isA
<
ToolExit
>().
having
((
ToolExit
e
)
=>
e
.
message
,
'message'
,
isNot
(
contains
(
'sudo chown'
)))));
});
testWithoutContext
(
'when writing to a full device'
,
()
async
{
...
...
@@ -635,7 +775,8 @@ void main() {
final
Object
firstPath
=
fs
.
path
;
fs
.
currentDirectory
=
null
;
when
(
mockFileSystem
.
path
).
thenReturn
(
MockPathContext
());
// For fs.path.absolute usage.
when
(
mockFileSystem
.
path
).
thenReturn
(
MemoryFileSystem
.
test
().
path
);
expect
(
identical
(
firstPath
,
fs
.
path
),
false
);
});
...
...
@@ -689,7 +830,7 @@ void main() {
const
int
kUserMappedSectionOpened
=
1224
;
const
int
kUserPermissionDenied
=
5
;
test
(
'when PackageProcess throws an exception containg non-executable bits'
,
()
{
test
WithoutContext
(
'when PackageProcess throws an exception containg non-executable bits'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessPackageExecutableNotFoundException
(
''
,
candidates:
<
String
>[
'not-empty'
])),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessPackageExecutableNotFoundException
(
''
,
candidates:
<
String
>[
'not-empty'
])),
...
...
@@ -708,7 +849,7 @@ void main() {
throwsToolExit
(
message:
expectedMessage
));
});
test
(
'when PackageProcess throws an exception without containing non-executable bits'
,
()
{
test
WithoutContext
(
'when PackageProcess throws an exception without containing non-executable bits'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessPackageExecutableNotFoundException
(
''
,
candidates:
<
String
>[])),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessPackageExecutableNotFoundException
(
''
,
candidates:
<
String
>[])),
...
...
@@ -725,7 +866,7 @@ void main() {
expect
(()
async
=>
processManager
.
runSync
(<
String
>[
'foo'
]),
throwsProcessException
());
});
test
(
'when the device is full'
,
()
{
test
WithoutContext
(
'when the device is full'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kDeviceFull
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kDeviceFull
)),
...
...
@@ -747,7 +888,7 @@ void main() {
throwsToolExit
(
message:
expectedMessage
));
});
test
(
'when the file is being used by another program'
,
()
{
test
WithoutContext
(
'when the file is being used by another program'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kUserMappedSectionOpened
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kUserMappedSectionOpened
)),
...
...
@@ -768,7 +909,7 @@ void main() {
throwsToolExit
(
message:
expectedMessage
));
});
test
(
'when permissions are denied'
,
()
{
test
WithoutContext
(
'when permissions are denied'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kUserPermissionDenied
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
kUserPermissionDenied
)),
...
...
@@ -788,13 +929,25 @@ void main() {
expect
(()
=>
processManager
.
runSync
(<
String
>[
'foo'
]),
throwsToolExit
(
message:
expectedMessage
));
});
testWithoutContext
(
'when cannot run executable'
,
()
{
final
ThrowingFakeProcessManager
throwingFakeProcessManager
=
ThrowingFakeProcessManager
(
const
ProcessException
(
''
,
<
String
>[],
''
,
kUserPermissionDenied
));
final
ProcessManager
processManager
=
ErrorHandlingProcessManager
(
delegate:
throwingFakeProcessManager
,
platform:
windowsPlatform
,
);
const
String
expectedMessage
=
r'Flutter failed to run "C:\path\to\dart". The flutter tool cannot access the file or directory.'
;
expect
(()
async
=>
processManager
.
canRun
(
r'C:\path\to\dart'
),
throwsToolExit
(
message:
expectedMessage
));
});
});
group
(
'ProcessManager on linux throws tool exit'
,
()
{
const
int
enospc
=
28
;
const
int
eacces
=
13
;
test
(
'when writing to a full device'
,
()
{
test
WithoutContext
(
'when writing to a full device'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
enospc
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
enospc
)),
...
...
@@ -815,7 +968,7 @@ void main() {
throwsToolExit
(
message:
expectedMessage
));
});
test
(
'when permissions are denied'
,
()
{
test
WithoutContext
(
'when permissions are denied'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
eacces
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
eacces
)),
...
...
@@ -835,13 +988,29 @@ void main() {
expect
(()
=>
processManager
.
runSync
(<
String
>[
'foo'
]),
throwsToolExit
(
message:
expectedMessage
));
});
testWithoutContext
(
'when cannot run executable'
,
()
{
final
ThrowingFakeProcessManager
throwingFakeProcessManager
=
ThrowingFakeProcessManager
(
const
ProcessException
(
''
,
<
String
>[],
''
,
eacces
));
final
ProcessManager
processManager
=
ErrorHandlingProcessManager
(
delegate:
throwingFakeProcessManager
,
platform:
linuxPlatform
,
);
const
String
expectedMessage
=
'Flutter failed to run "/path/to/dart".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /path/to/dart && chmod u+rx /path/to/dart'
;
expect
(()
async
=>
processManager
.
canRun
(
'/path/to/dart'
),
throwsToolExit
(
message:
expectedMessage
));
});
});
group
(
'ProcessManager on macOS throws tool exit'
,
()
{
const
int
enospc
=
28
;
const
int
eacces
=
13
;
test
(
'when writing to a full device'
,
()
{
test
WithoutContext
(
'when writing to a full device'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
enospc
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
enospc
)),
...
...
@@ -862,7 +1031,7 @@ void main() {
throwsToolExit
(
message:
expectedMessage
));
});
test
(
'when permissions are denied'
,
()
{
test
WithoutContext
(
'when permissions are denied'
,
()
{
final
FakeProcessManager
fakeProcessManager
=
FakeProcessManager
.
list
(<
FakeCommand
>[
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
eacces
)),
const
FakeCommand
(
command:
<
String
>[
'foo'
],
exception:
ProcessException
(
''
,
<
String
>[],
''
,
eacces
)),
...
...
@@ -870,7 +1039,7 @@ void main() {
]);
final
ProcessManager
processManager
=
ErrorHandlingProcessManager
(
delegate:
fakeProcessManager
,
platform:
linux
Platform
,
platform:
macOS
Platform
,
);
const
String
expectedMessage
=
'The flutter tool cannot access the file'
;
...
...
@@ -882,6 +1051,22 @@ void main() {
expect
(()
=>
processManager
.
runSync
(<
String
>[
'foo'
]),
throwsToolExit
(
message:
expectedMessage
));
});
testWithoutContext
(
'when cannot run executable'
,
()
{
final
ThrowingFakeProcessManager
throwingFakeProcessManager
=
ThrowingFakeProcessManager
(
const
ProcessException
(
''
,
<
String
>[],
''
,
eacces
));
final
ProcessManager
processManager
=
ErrorHandlingProcessManager
(
delegate:
throwingFakeProcessManager
,
platform:
macOSPlatform
,
);
const
String
expectedMessage
=
'Flutter failed to run "/path/to/dart".
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /path/to/dart && chmod u+rx /path/to/dart'
;
expect
(()
async
=>
processManager
.
canRun
(
'/path/to/dart'
),
throwsToolExit
(
message:
expectedMessage
));
});
});
testWithoutContext
(
'ErrorHandlingProcessManager delegates killPid correctly'
,
()
async
{
...
...
@@ -910,21 +1095,32 @@ void main() {
delegate:
mockFileSystem
,
platform:
linuxPlatform
,
);
when
(
mockFileSystem
.
path
).
thenReturn
(
MockPathContext
());
// For fs.path.absolute usage.
when
(
mockFileSystem
.
path
).
thenReturn
(
MemoryFileSystem
.
test
().
path
);
});
testWithoutContext
(
'copySync handles error if openSync on source file fails'
,
()
{
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
when
(
source
.
openSync
(
mode:
anyNamed
(
'mode'
)))
.
thenThrow
(
const
FileSystemException
(
''
,
''
,
OSError
(
''
,
eaccess
)));
when
(
mockFileSystem
.
file
(
'source'
)).
thenReturn
(
source
);
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
());
const
String
expectedMessage
=
'Flutter failed to copy source to dest due to source location error.
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /source'
;
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
(
message:
expectedMessage
));
});
testWithoutContext
(
'copySync handles error if createSync on destination file fails'
,
()
{
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
final
MockDirectory
parent
=
MockDirectory
();
when
(
parent
.
path
).
thenReturn
(
'destParent'
);
final
MockFile
dest
=
MockFile
();
when
(
dest
.
parent
).
thenReturn
(
parent
);
when
(
source
.
openSync
(
mode:
anyNamed
(
'mode'
)))
.
thenReturn
(
MockRandomAccessFile
());
when
(
dest
.
createSync
(
recursive:
anyNamed
(
'recursive'
)))
...
...
@@ -932,13 +1128,20 @@ void main() {
when
(
mockFileSystem
.
file
(
'source'
)).
thenReturn
(
source
);
when
(
mockFileSystem
.
file
(
'dest'
)).
thenReturn
(
dest
);
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
());
const
String
expectedMessage
=
'Flutter failed to copy source to dest due to destination location error.
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.'
;
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
(
message:
expectedMessage
));
});
// dart:io is able to clobber read-only files.
testWithoutContext
(
'copySync will copySync even if the destination is not writable'
,
()
{
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
final
MockDirectory
parent
=
MockDirectory
();
when
(
parent
.
path
).
thenReturn
(
'destParent'
);
final
MockFile
dest
=
MockFile
();
when
(
dest
.
parent
).
thenReturn
(
parent
);
when
(
source
.
copySync
(
any
)).
thenReturn
(
dest
);
when
(
mockFileSystem
.
file
(
'source'
)).
thenReturn
(
source
);
...
...
@@ -955,7 +1158,11 @@ void main() {
testWithoutContext
(
'copySync will copySync if there are no exceptions'
,
()
{
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
final
MockDirectory
parent
=
MockDirectory
();
when
(
parent
.
path
).
thenReturn
(
'destParent'
);
final
MockFile
dest
=
MockFile
();
when
(
dest
.
parent
).
thenReturn
(
parent
);
when
(
source
.
copySync
(
any
)).
thenReturn
(
dest
);
when
(
mockFileSystem
.
file
(
'source'
)).
thenReturn
(
source
);
...
...
@@ -973,7 +1180,11 @@ void main() {
testWithoutContext
(
'copySync can directly copy bytes if both files can be opened but copySync fails'
,
()
{
final
MemoryFileSystem
memoryFileSystem
=
MemoryFileSystem
.
test
();
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
final
MockDirectory
parent
=
MockDirectory
();
when
(
parent
.
path
).
thenReturn
(
'destParent'
);
final
MockFile
dest
=
MockFile
();
when
(
dest
.
parent
).
thenReturn
(
parent
);
final
List
<
int
>
expectedBytes
=
List
<
int
>.
generate
(
64
*
1024
+
3
,
(
int
i
)
=>
i
.
isEven
?
0
:
1
);
final
File
memorySource
=
memoryFileSystem
.
file
(
'source'
)
..
writeAsBytesSync
(
expectedBytes
);
...
...
@@ -997,7 +1208,11 @@ void main() {
testWithoutContext
(
'copySync deletes the result file if the fallback fails'
,
()
{
final
MemoryFileSystem
memoryFileSystem
=
MemoryFileSystem
.
test
();
final
MockFile
source
=
MockFile
();
when
(
source
.
path
).
thenReturn
(
'source'
);
final
MockDirectory
parent
=
MockDirectory
();
when
(
parent
.
path
).
thenReturn
(
'destParent'
);
final
MockFile
dest
=
MockFile
();
when
(
dest
.
parent
).
thenReturn
(
parent
);
final
File
memorySource
=
memoryFileSystem
.
file
(
'source'
)
..
createSync
();
final
File
memoryDest
=
memoryFileSystem
.
file
(
'dest'
)
...
...
@@ -1020,7 +1235,12 @@ void main() {
when
(
mockFileSystem
.
file
(
'source'
)).
thenReturn
(
source
);
when
(
mockFileSystem
.
file
(
'dest'
)).
thenReturn
(
dest
);
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
());
const
String
expectedMessage
=
'Flutter failed to copy source to dest due to unknown error.
\n
'
'Please ensure that the SDK and/or project is installed in a location that has read/write permissions for the current user.
\n
'
'Try running:
\n
'
r' sudo chown -R $(whoami) /source /destParent'
;
expect
(()
=>
fileSystem
.
file
(
'source'
).
copySync
(
'dest'
),
throwsToolExit
(
message:
expectedMessage
));
verify
(
dest
.
deleteSync
(
recursive:
true
)).
called
(
1
);
});
...
...
@@ -1036,3 +1256,14 @@ class FakeSignalProcessManager extends Fake implements ProcessManager {
return
true
;
}
}
class
ThrowingFakeProcessManager
extends
Fake
implements
ProcessManager
{
ThrowingFakeProcessManager
(
Exception
exception
)
:
_exception
=
exception
;
final
Exception
_exception
;
@override
bool
canRun
(
dynamic
executable
,
{
String
workingDirectory
})
{
throw
_exception
;
}
}
packages/flutter_tools/test/general.shard/config_test.dart
View file @
c1c12aa3
...
...
@@ -104,8 +104,7 @@ void main() {
config
=
Config
.
createForTesting
(
file
,
bufferLogger
);
expect
(
bufferLogger
.
errorText
,
contains
(
'Could not read preferences in testfile'
));
// Also contains original error message:
expect
(
bufferLogger
.
errorText
,
contains
(
'The flutter tool cannot access the file or directory'
));
expect
(
bufferLogger
.
errorText
,
contains
(
r'sudo chown -R $(whoami) /testfile'
));
});
testWithoutContext
(
'Config in home dir is used if it exists'
,
()
{
...
...
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