Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
wine
Manage
Activity
Members
Labels
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Package registry
Container Registry
Model registry
Operate
Terraform modules
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Lorenzo Ferrillo
wine
Commits
e0f54d1b
Commit
e0f54d1b
authored
13 years ago
by
Bernhard Loos
Committed by
Alexandre Julliard
13 years ago
Browse files
Options
Downloads
Patches
Plain Diff
ntdll/tests: Add a number of named pipe tests using the nt api and ioctls.
parent
8286bfee
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
dlls/ntdll/tests/Makefile.in
+1
-0
1 addition, 0 deletions
dlls/ntdll/tests/Makefile.in
dlls/ntdll/tests/pipe.c
+430
-0
430 additions, 0 deletions
dlls/ntdll/tests/pipe.c
with
431 additions
and
0 deletions
dlls/ntdll/tests/Makefile.in
+
1
−
0
View file @
e0f54d1b
...
...
@@ -14,6 +14,7 @@ C_SRCS = \
large_int.c
\
om.c
\
path.c
\
pipe.c
\
port.c
\
reg.c
\
rtl.c
\
...
...
This diff is collapsed.
Click to expand it.
dlls/ntdll/tests/pipe.c
0 → 100644
+
430
−
0
View file @
e0f54d1b
/* Unit test suite for Ntdll NamedPipe API functions
*
* Copyright 2011 Bernhard Loos
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include
<stdio.h>
#include
<stdarg.h>
#include
"ntstatus.h"
#define WIN32_NO_STATUS
#include
"windef.h"
#include
"winbase.h"
#include
"winuser.h"
#include
"winreg.h"
#include
"winnls.h"
#include
"wine/test.h"
#include
"winternl.h"
#include
"winioctl.h"
#ifndef __WINE_WINTERNL_H
typedef
struct
{
ULONG
NamedPipeType
;
ULONG
NamedPipeConfiguration
;
ULONG
MaximumInstances
;
ULONG
CurrentInstances
;
ULONG
InboundQuota
;
ULONG
ReadDataAvailable
;
ULONG
OutboundQuota
;
ULONG
WriteQuotaAvailable
;
ULONG
NamedPipeState
;
ULONG
NamedPipeEnd
;
}
FILE_PIPE_LOCAL_INFORMATION
;
#ifndef FILE_SYNCHRONOUS_IO_ALERT
#define FILE_SYNCHRONOUS_IO_ALERT 0x10
#endif
#ifndef FILE_SYNCHRONOUS_IO_NONALERT
#define FILE_SYNCHRONOUS_IO_NONALERT 0x20
#endif
#ifndef FSCTL_PIPE_LISTEN
#define FSCTL_PIPE_LISTEN CTL_CODE(FILE_DEVICE_NAMED_PIPE, 2, METHOD_BUFFERED, FILE_ANY_ACCESS)
#endif
#endif
static
NTSTATUS
(
WINAPI
*
pNtFsControlFile
)
(
HANDLE
handle
,
HANDLE
event
,
PIO_APC_ROUTINE
apc
,
PVOID
apc_context
,
PIO_STATUS_BLOCK
io
,
ULONG
code
,
PVOID
in_buffer
,
ULONG
in_size
,
PVOID
out_buffer
,
ULONG
out_size
);
static
NTSTATUS
(
WINAPI
*
pNtCreateNamedPipeFile
)
(
PHANDLE
handle
,
ULONG
access
,
POBJECT_ATTRIBUTES
attr
,
PIO_STATUS_BLOCK
iosb
,
ULONG
sharing
,
ULONG
dispo
,
ULONG
options
,
ULONG
pipe_type
,
ULONG
read_mode
,
ULONG
completion_mode
,
ULONG
max_inst
,
ULONG
inbound_quota
,
ULONG
outbound_quota
,
PLARGE_INTEGER
timeout
);
static
NTSTATUS
(
WINAPI
*
pNtQueryInformationFile
)
(
IN
HANDLE
FileHandle
,
OUT
PIO_STATUS_BLOCK
IoStatusBlock
,
OUT
PVOID
FileInformation
,
IN
ULONG
Length
,
IN
FILE_INFORMATION_CLASS
FileInformationClass
);
static
NTSTATUS
(
WINAPI
*
pNtCancelIoFile
)
(
HANDLE
hFile
,
PIO_STATUS_BLOCK
io_status
);
static
void
(
WINAPI
*
pRtlInitUnicodeString
)
(
PUNICODE_STRING
target
,
PCWSTR
source
);
static
HANDLE
(
WINAPI
*
pOpenThread
)(
DWORD
dwDesiredAccess
,
BOOL
bInheritHandle
,
DWORD
dwThreadId
);
static
DWORD
(
WINAPI
*
pQueueUserAPC
)(
PAPCFUNC
pfnAPC
,
HANDLE
hThread
,
ULONG_PTR
dwData
);
static
BOOL
init_func_ptrs
(
void
)
{
HMODULE
module
=
GetModuleHandle
(
"ntdll.dll"
);
#define loadfunc(name) if (!(p##name = (void *)GetProcAddress(module, #name))) { \
trace("GetProcAddress(%s) failed\n", #name); \
return FALSE; \
}
loadfunc
(
NtFsControlFile
)
loadfunc
(
NtCreateNamedPipeFile
)
loadfunc
(
NtQueryInformationFile
)
loadfunc
(
NtCancelIoFile
)
loadfunc
(
RtlInitUnicodeString
)
/* not fatal */
module
=
GetModuleHandle
(
"kernel32.dll"
);
pOpenThread
=
(
void
*
)
GetProcAddress
(
module
,
"OpenThread"
);
pQueueUserAPC
=
(
void
*
)
GetProcAddress
(
module
,
"QueueUserAPC"
);
return
TRUE
;
}
static
const
WCHAR
testpipe
[]
=
{
'\\'
,
'\\'
,
'.'
,
'\\'
,
'p'
,
'i'
,
'p'
,
'e'
,
'\\'
,
't'
,
'e'
,
's'
,
't'
,
'p'
,
'i'
,
'p'
,
'e'
,
0
};
static
const
WCHAR
testpipe_nt
[]
=
{
'\\'
,
'?'
,
'?'
,
'\\'
,
'p'
,
'i'
,
'p'
,
'e'
,
'\\'
,
't'
,
'e'
,
's'
,
't'
,
'p'
,
'i'
,
'p'
,
'e'
,
0
};
static
NTSTATUS
create_pipe
(
PHANDLE
handle
,
ULONG
sharing
,
ULONG
options
)
{
IO_STATUS_BLOCK
iosb
;
OBJECT_ATTRIBUTES
attr
;
UNICODE_STRING
name
;
LARGE_INTEGER
timeout
;
NTSTATUS
res
;
pRtlInitUnicodeString
(
&
name
,
testpipe_nt
);
attr
.
Length
=
sizeof
(
attr
);
attr
.
RootDirectory
=
0
;
attr
.
ObjectName
=
&
name
;
attr
.
Attributes
=
0x40
;
/*case insensitive */
attr
.
SecurityDescriptor
=
NULL
;
attr
.
SecurityQualityOfService
=
NULL
;
timeout
.
QuadPart
=
-
100000000000ll
;
res
=
pNtCreateNamedPipeFile
(
handle
,
FILE_READ_ATTRIBUTES
|
SYNCHRONIZE
,
&
attr
,
&
iosb
,
sharing
,
2
/*FILE_CREATE*/
,
options
,
1
,
0
,
0
,
0xFFFFFFFF
,
500
,
500
,
&
timeout
);
return
res
;
}
static
void
test_create_invalid
(
void
)
{
IO_STATUS_BLOCK
iosb
;
OBJECT_ATTRIBUTES
attr
;
UNICODE_STRING
name
;
LARGE_INTEGER
timeout
;
NTSTATUS
res
;
HANDLE
handle
,
handle2
;
FILE_PIPE_LOCAL_INFORMATION
info
;
pRtlInitUnicodeString
(
&
name
,
testpipe_nt
);
attr
.
Length
=
sizeof
(
attr
);
attr
.
RootDirectory
=
0
;
attr
.
ObjectName
=
&
name
;
attr
.
Attributes
=
0x40
;
/*case insensitive */
attr
.
SecurityDescriptor
=
NULL
;
attr
.
SecurityQualityOfService
=
NULL
;
timeout
.
QuadPart
=
-
100000000000ll
;
/* create a pipe with sharing = 0 */
res
=
pNtCreateNamedPipeFile
(
&
handle
,
FILE_READ_ATTRIBUTES
|
SYNCHRONIZE
,
&
attr
,
&
iosb
,
0
,
2
/*FILE_CREATE*/
,
0
,
1
,
0
,
0
,
0xFFFFFFFF
,
500
,
500
,
&
timeout
);
todo_wine
ok
(
res
==
STATUS_INVALID_PARAMETER
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
if
(
!
res
)
CloseHandle
(
handle
);
/* create a pipe without r/w access */
res
=
pNtCreateNamedPipeFile
(
&
handle
,
SYNCHRONIZE
,
&
attr
,
&
iosb
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
2
/*FILE_CREATE*/
,
0
,
1
,
0
,
0
,
0xFFFFFFFF
,
500
,
500
,
&
timeout
);
ok
(
!
res
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
res
=
pNtQueryInformationFile
(
handle
,
&
iosb
,
&
info
,
sizeof
(
info
),
(
FILE_INFORMATION_CLASS
)
24
);
todo_wine
ok
(
res
==
STATUS_ACCESS_DENIED
,
"NtQueryInformationFile returned %x
\n
"
,
res
);
/* test FILE_CREATE creation disposition */
res
=
pNtCreateNamedPipeFile
(
&
handle2
,
SYNCHRONIZE
,
&
attr
,
&
iosb
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
2
/*FILE_CREATE*/
,
0
,
1
,
0
,
0
,
0xFFFFFFFF
,
500
,
500
,
&
timeout
);
todo_wine
ok
(
res
==
STATUS_ACCESS_DENIED
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
if
(
!
res
)
CloseHandle
(
handle2
);
CloseHandle
(
handle
);
}
static
BOOL
ioapc_called
;
static
void
CALLBACK
ioapc
(
void
*
arg
,
PIO_STATUS_BLOCK
io
,
ULONG
reserved
)
{
ioapc_called
=
TRUE
;
}
static
NTSTATUS
listen_pipe
(
HANDLE
hPipe
,
HANDLE
hEvent
,
PIO_STATUS_BLOCK
iosb
,
BOOL
use_apc
)
{
int
dummy
;
ioapc_called
=
FALSE
;
return
pNtFsControlFile
(
hPipe
,
hEvent
,
use_apc
?
&
ioapc
:
NULL
,
use_apc
?
&
dummy
:
NULL
,
iosb
,
FSCTL_PIPE_LISTEN
,
0
,
0
,
0
,
0
);
}
static
void
test_overlapped
(
void
)
{
IO_STATUS_BLOCK
iosb
;
HANDLE
hEvent
;
HANDLE
hPipe
;
HANDLE
hClient
;
NTSTATUS
res
;
hEvent
=
CreateEventW
(
NULL
,
TRUE
,
FALSE
,
NULL
);
ok
(
hEvent
!=
INVALID_HANDLE_VALUE
,
"can't create event, GetLastError: %x
\n
"
,
GetLastError
());
res
=
create_pipe
(
&
hPipe
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
0
/* OVERLAPPED */
);
ok
(
!
res
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
memset
(
&
iosb
,
0x55
,
sizeof
(
iosb
));
/* try with event and apc */
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
ok
(
res
==
STATUS_PENDING
,
"NtFsControlFile returned %x
\n
"
,
res
);
hClient
=
CreateFileW
(
testpipe
,
GENERIC_READ
|
GENERIC_WRITE
,
0
,
0
,
OPEN_EXISTING
,
0
,
0
);
ok
(
hClient
!=
INVALID_HANDLE_VALUE
,
"can't open pipe, GetLastError: %x
\n
"
,
GetLastError
());
ok
(
iosb
.
Status
==
0
,
"Wrong iostatus %x
\n
"
,
iosb
.
Status
);
ok
(
WaitForSingleObject
(
hEvent
,
0
)
==
0
,
"hEvent not signaled
\n
"
);
ok
(
!
ioapc_called
,
"IOAPC ran too early
\n
"
);
SleepEx
(
0
,
TRUE
);
/* alertable wait state */
ok
(
ioapc_called
,
"IOAPC didn't run
\n
"
);
CloseHandle
(
hEvent
);
CloseHandle
(
hPipe
);
CloseHandle
(
hClient
);
}
static
BOOL
userapc_called
;
static
void
CALLBACK
userapc
(
ULONG_PTR
dwParam
)
{
userapc_called
=
TRUE
;
}
static
BOOL
open_succeded
;
static
DWORD
WINAPI
thread
(
PVOID
main_thread
)
{
HANDLE
h
;
Sleep
(
400
);
if
(
main_thread
)
{
userapc_called
=
FALSE
;
ok
(
pQueueUserAPC
(
&
userapc
,
main_thread
,
0
),
"can't queue user apc, GetLastError: %x
\n
"
,
GetLastError
());
CloseHandle
(
main_thread
);
}
Sleep
(
400
);
h
=
CreateFileW
(
testpipe
,
GENERIC_WRITE
|
GENERIC_READ
,
0
,
0
,
OPEN_EXISTING
,
0
,
0
);
if
(
h
!=
INVALID_HANDLE_VALUE
)
{
open_succeded
=
TRUE
;
Sleep
(
100
);
CloseHandle
(
h
);
}
else
open_succeded
=
FALSE
;
return
0
;
}
static
void
test_alertable
(
void
)
{
IO_STATUS_BLOCK
iosb
;
HANDLE
hEvent
;
HANDLE
hPipe
;
NTSTATUS
res
;
HANDLE
hThread
;
memset
(
&
iosb
,
0x55
,
sizeof
(
iosb
));
hEvent
=
CreateEventW
(
NULL
,
TRUE
,
FALSE
,
NULL
);
ok
(
hEvent
!=
INVALID_HANDLE_VALUE
,
"can't create event, GetLastError: %x
\n
"
,
GetLastError
());
res
=
create_pipe
(
&
hPipe
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
FILE_SYNCHRONOUS_IO_ALERT
);
ok
(
!
res
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
/* queue an user apc before calling listen */
userapc_called
=
FALSE
;
ok
(
pQueueUserAPC
(
&
userapc
,
GetCurrentThread
(),
0
),
"can't queue user apc, GetLastError: %x
\n
"
,
GetLastError
());
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
todo_wine
ok
(
res
==
STATUS_CANCELLED
,
"NtFsControlFile returned %x
\n
"
,
res
);
todo_wine
ok
(
userapc_called
,
"user apc didn't run
\n
"
);
ok
(
iosb
.
Status
==
0x55555555
,
"iosb.Status got changed to %x
\n
"
,
iosb
.
Status
);
todo_wine
ok
(
WaitForSingleObjectEx
(
hEvent
,
0
,
TRUE
)
==
WAIT_TIMEOUT
,
"hEvent signaled
\n
"
);
ok
(
!
ioapc_called
,
"IOAPC ran
\n
"
);
/* queue an user apc from a different thread */
hThread
=
CreateThread
(
NULL
,
0
,
&
thread
,
pOpenThread
(
MAXIMUM_ALLOWED
,
FALSE
,
GetCurrentThreadId
()),
0
,
0
);
ok
(
hThread
!=
INVALID_HANDLE_VALUE
,
"can't create thread, GetLastError: %x
\n
"
,
GetLastError
());
/* wine_todo: the earlier NtFsControlFile call gets cancelled after the pipe gets set into listen state
instead of before, so this NtFsControlFile will fail STATUS_INVALID_HANDLE */
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
todo_wine
ok
(
res
==
STATUS_CANCELLED
,
"NtFsControlFile returned %x
\n
"
,
res
);
ok
(
userapc_called
,
"user apc didn't run
\n
"
);
todo_wine
ok
(
iosb
.
Status
==
0x55555555
,
"iosb.Status got changed to %x
\n
"
,
iosb
.
Status
);
ok
(
WaitForSingleObjectEx
(
hEvent
,
0
,
TRUE
)
==
WAIT_TIMEOUT
,
"hEvent signaled
\n
"
);
ok
(
!
ioapc_called
,
"IOAPC ran
\n
"
);
WaitForSingleObject
(
hThread
,
INFINITE
);
SleepEx
(
0
,
TRUE
);
/* get rid of the userapc, if NtFsControlFile failed */
ok
(
open_succeded
,
"couldn't open client side pipe
\n
"
);
CloseHandle
(
hThread
);
DisconnectNamedPipe
(
hPipe
);
/* finally try without an apc */
hThread
=
CreateThread
(
NULL
,
0
,
&
thread
,
0
,
0
,
0
);
ok
(
hThread
!=
INVALID_HANDLE_VALUE
,
"can't create thread, GetLastError: %x
\n
"
,
GetLastError
());
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
todo_wine
ok
(
!
res
,
"NtFsControlFile returned %x
\n
"
,
res
);
ok
(
open_succeded
,
"couldn't open client side pipe
\n
"
);
ok
(
!
iosb
.
Status
,
"Wrong iostatus %x
\n
"
,
iosb
.
Status
);
todo_wine
ok
(
WaitForSingleObject
(
hEvent
,
0
)
==
0
,
"hEvent not signaled
\n
"
);
WaitForSingleObject
(
hThread
,
INFINITE
);
CloseHandle
(
hThread
);
CloseHandle
(
hEvent
);
CloseHandle
(
hPipe
);
}
static
void
test_nonalertable
(
void
)
{
IO_STATUS_BLOCK
iosb
;
HANDLE
hEvent
;
HANDLE
hPipe
;
NTSTATUS
res
;
HANDLE
hThread
;
memset
(
&
iosb
,
0x55
,
sizeof
(
iosb
));
hEvent
=
CreateEventW
(
NULL
,
TRUE
,
FALSE
,
NULL
);
ok
(
hEvent
!=
INVALID_HANDLE_VALUE
,
"can't create event, GetLastError: %x
\n
"
,
GetLastError
());
res
=
create_pipe
(
&
hPipe
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
FILE_SYNCHRONOUS_IO_NONALERT
);
ok
(
!
res
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
hThread
=
CreateThread
(
NULL
,
0
,
&
thread
,
0
,
0
,
0
);
ok
(
hThread
!=
INVALID_HANDLE_VALUE
,
"can't create thread, GetLastError: %x
\n
"
,
GetLastError
());
userapc_called
=
FALSE
;
ok
(
pQueueUserAPC
(
&
userapc
,
GetCurrentThread
(),
0
),
"can't queue user apc, GetLastError: %x
\n
"
,
GetLastError
());
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
todo_wine
ok
(
!
res
,
"NtFsControlFile returned %x
\n
"
,
res
);
ok
(
open_succeded
,
"couldn't open client side pipe
\n
"
);
todo_wine
ok
(
!
iosb
.
Status
,
"Wrong iostatus %x
\n
"
,
iosb
.
Status
);
todo_wine
ok
(
WaitForSingleObject
(
hEvent
,
0
)
==
0
,
"hEvent not signaled
\n
"
);
ok
(
!
ioapc_called
,
"IOAPC ran too early
\n
"
);
ok
(
!
userapc_called
,
"user apc ran too early
\n
"
);
SleepEx
(
0
,
TRUE
);
/* alertable wait state */
ok
(
ioapc_called
,
"IOAPC didn't run
\n
"
);
ok
(
userapc_called
,
"user apc didn't run
\n
"
);
WaitForSingleObject
(
hThread
,
INFINITE
);
CloseHandle
(
hThread
);
CloseHandle
(
hEvent
);
CloseHandle
(
hPipe
);
}
static
void
test_cancelio
(
void
)
{
IO_STATUS_BLOCK
iosb
;
IO_STATUS_BLOCK
cancel_sb
;
HANDLE
hEvent
;
HANDLE
hPipe
;
NTSTATUS
res
;
hEvent
=
CreateEventW
(
NULL
,
TRUE
,
FALSE
,
NULL
);
ok
(
hEvent
!=
INVALID_HANDLE_VALUE
,
"can't create event, GetLastError: %x
\n
"
,
GetLastError
());
res
=
create_pipe
(
&
hPipe
,
FILE_SHARE_READ
|
FILE_SHARE_WRITE
,
0
/* OVERLAPPED */
);
ok
(
!
res
,
"NtCreateNamedPipeFile returned %x
\n
"
,
res
);
memset
(
&
iosb
,
0x55
,
sizeof
(
iosb
));
res
=
listen_pipe
(
hPipe
,
hEvent
,
&
iosb
,
TRUE
);
ok
(
res
==
STATUS_PENDING
,
"NtFsControlFile returned %x
\n
"
,
res
);
res
=
pNtCancelIoFile
(
hPipe
,
&
cancel_sb
);
todo_wine
ok
(
!
res
,
"NtCancelIoFile returned %x
\n
"
,
res
);
todo_wine
{
ok
(
iosb
.
Status
==
STATUS_CANCELLED
,
"Wrong iostatus %x
\n
"
,
iosb
.
Status
);
ok
(
WaitForSingleObject
(
hEvent
,
0
)
==
0
,
"hEvent not signaled
\n
"
);
}
ok
(
!
ioapc_called
,
"IOAPC ran too early
\n
"
);
SleepEx
(
0
,
TRUE
);
/* alertable wait state */
ok
(
ioapc_called
,
"IOAPC didn't run
\n
"
);
CloseHandle
(
hEvent
);
CloseHandle
(
hPipe
);
}
START_TEST
(
pipe
)
{
if
(
!
init_func_ptrs
())
return
;
trace
(
"starting invalid create tests
\n
"
);
test_create_invalid
();
trace
(
"starting overlapped tests
\n
"
);
test_overlapped
();
if
(
!
pOpenThread
||
!
pQueueUserAPC
)
return
;
trace
(
"starting alertable tests
\n
"
);
test_alertable
();
trace
(
"starting nonalertable tests
\n
"
);
test_nonalertable
();
trace
(
"starting cancelio tests
\n
"
);
test_cancelio
();
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment