Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wine-winehq
Project
Project
Details
Activity
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
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wine
wine-winehq
Commits
bd2b8980
Commit
bd2b8980
authored
Jan 26, 2024
by
Rémi Bernon
Committed by
Alexandre Julliard
Feb 02, 2024
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dinput/tests: Add a helper to wait on HID input reads.
parent
a4d2e62a
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
119 additions
and
61 deletions
+119
-61
device8.c
dlls/dinput/tests/device8.c
+1
-0
dinput_test.h
dlls/dinput/tests/dinput_test.h
+5
-0
driver_bus.c
dlls/dinput/tests/driver_bus.c
+92
-61
driver_hid.c
dlls/dinput/tests/driver_hid.c
+1
-0
driver_hid.h
dlls/dinput/tests/driver_hid.h
+2
-0
hid.c
dlls/dinput/tests/hid.c
+18
-0
No files found.
dlls/dinput/tests/device8.c
View file @
bd2b8980
...
...
@@ -1738,6 +1738,7 @@ static void test_hid_mouse(void)
mouse_move_count
=
0
;
bus_send_hid_input
(
file
,
&
desc
,
single_move
,
sizeof
(
single_move
)
);
bus_wait_hid_input
(
file
,
&
desc
,
100
);
res
=
MsgWaitForMultipleObjects
(
0
,
NULL
,
FALSE
,
500
,
QS_MOUSEMOVE
);
todo_wine
...
...
dlls/dinput/tests/dinput_test.h
View file @
bd2b8980
...
...
@@ -108,6 +108,11 @@ void wait_hid_expect_( const char *file, int line, HANDLE device, struct hid_dev
void
send_hid_input_
(
const
char
*
file
,
int
line
,
HANDLE
device
,
struct
hid_device_desc
*
desc
,
struct
hid_expect
*
expect
,
DWORD
expect_size
);
#define wait_hid_input( a, b ) wait_hid_input_( __FILE__, __LINE__, a, NULL, b, FALSE )
#define bus_wait_hid_input( a, b, c ) wait_hid_input_( __FILE__, __LINE__, a, b, c, FALSE )
void
wait_hid_input_
(
const
char
*
file
,
int
line
,
HANDLE
device
,
struct
hid_device_desc
*
desc
,
DWORD
timeout
,
BOOL
todo
);
#define msg_wait_for_events( a, b, c ) msg_wait_for_events_( __FILE__, __LINE__, a, b, c )
DWORD
msg_wait_for_events_
(
const
char
*
file
,
int
line
,
DWORD
count
,
HANDLE
*
events
,
DWORD
timeout
);
...
...
dlls/dinput/tests/driver_bus.c
View file @
bd2b8980
...
...
@@ -63,27 +63,13 @@ static void check_buffer_( int line, HID_XFER_PACKET *packet, struct hid_expect
}
}
struct
expec
t_queue
struct
wai
t_queue
{
KSPIN_LOCK
lock
;
struct
hid_expect
*
pos
;
struct
hid_expect
*
end
;
struct
hid_expect
spurious
;
struct
hid_expect
*
buffer
;
IRP
*
pending_wait
;
char
context
[
64
];
};
static
void
expect_queue_init
(
struct
expect_queue
*
queue
)
{
KeInitializeSpinLock
(
&
queue
->
lock
);
queue
->
buffer
=
ExAllocatePool
(
PagedPool
,
EXPECT_QUEUE_BUFFER_SIZE
);
RtlSecureZeroMemory
(
queue
->
buffer
,
EXPECT_QUEUE_BUFFER_SIZE
);
queue
->
pos
=
queue
->
buffer
;
queue
->
end
=
queue
->
buffer
;
}
static
void
expect_queue_cleanup
(
struct
expect_queue
*
queue
)
static
void
wait_queue_cleanup
(
struct
wait_queue
*
queue
)
{
KIRQL
irql
;
IRP
*
irp
;
...
...
@@ -101,7 +87,50 @@ static void expect_queue_cleanup( struct expect_queue *queue )
irp
->
IoStatus
.
Status
=
STATUS_DELETE_PENDING
;
IoCompleteRequest
(
irp
,
IO_NO_INCREMENT
);
}
}
static
void
wait_queue_unlock
(
struct
wait_queue
*
queue
,
KIRQL
irql
,
BOOL
empty
)
{
IRP
*
irp
;
/* complete the pending wait IRP if the queue is now empty */
if
((
irp
=
empty
?
queue
->
pending_wait
:
NULL
))
{
queue
->
pending_wait
=
NULL
;
if
(
!
IoSetCancelRoutine
(
irp
,
NULL
))
irp
=
NULL
;
}
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
if
(
irp
)
{
irp
->
IoStatus
.
Status
=
STATUS_SUCCESS
;
IoCompleteRequest
(
irp
,
IO_NO_INCREMENT
);
}
}
struct
expect_queue
{
struct
wait_queue
base
;
struct
hid_expect
*
pos
;
struct
hid_expect
*
end
;
struct
hid_expect
spurious
;
struct
hid_expect
*
buffer
;
char
context
[
64
];
};
static
void
expect_queue_init
(
struct
expect_queue
*
queue
)
{
KeInitializeSpinLock
(
&
queue
->
base
.
lock
);
queue
->
buffer
=
ExAllocatePool
(
PagedPool
,
EXPECT_QUEUE_BUFFER_SIZE
);
RtlSecureZeroMemory
(
queue
->
buffer
,
EXPECT_QUEUE_BUFFER_SIZE
);
queue
->
pos
=
queue
->
buffer
;
queue
->
end
=
queue
->
buffer
;
}
static
void
expect_queue_cleanup
(
struct
expect_queue
*
queue
)
{
wait_queue_cleanup
(
&
queue
->
base
);
ExFreePool
(
queue
->
buffer
);
}
...
...
@@ -115,7 +144,7 @@ static void expect_queue_reset( struct expect_queue *queue, void *buffer, unsign
RtlSecureZeroMemory
(
missing
,
EXPECT_QUEUE_BUFFER_SIZE
);
missing_end
=
missing
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
tmp
=
queue
->
pos
;
while
(
tmp
<
queue
->
end
)
*
missing_end
++
=
*
tmp
++
;
...
...
@@ -125,7 +154,7 @@ static void expect_queue_reset( struct expect_queue *queue, void *buffer, unsign
if
(
size
)
memcpy
(
queue
->
end
,
buffer
,
size
);
queue
->
end
=
queue
->
end
+
size
/
sizeof
(
struct
hid_expect
);
memcpy
(
context
,
queue
->
context
,
sizeof
(
context
)
);
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
KeReleaseSpinLock
(
&
queue
->
base
.
lock
,
irql
);
tmp
=
missing
;
while
(
tmp
!=
missing_end
)
...
...
@@ -150,7 +179,7 @@ static void expect_queue_reset( struct expect_queue *queue, void *buffer, unsign
static
void
WINAPI
wait_cancel_routine
(
DEVICE_OBJECT
*
device
,
IRP
*
irp
)
{
struct
expec
t_queue
*
queue
=
irp
->
Tail
.
Overlay
.
DriverContext
[
0
];
struct
wai
t_queue
*
queue
=
irp
->
Tail
.
Overlay
.
DriverContext
[
0
];
KIRQL
irql
;
IoReleaseCancelSpinLock
(
irp
->
CancelIrql
);
...
...
@@ -164,7 +193,7 @@ static void WINAPI wait_cancel_routine( DEVICE_OBJECT *device, IRP *irp )
IoCompleteRequest
(
irp
,
IO_NO_INCREMENT
);
}
static
NTSTATUS
expect_queue_add_pending_locked
(
struct
expec
t_queue
*
queue
,
IRP
*
irp
)
static
NTSTATUS
wait_queue_add_pending_locked
(
struct
wai
t_queue
*
queue
,
IRP
*
irp
)
{
if
(
queue
->
pending_wait
)
return
STATUS_INVALID_PARAMETER
;
...
...
@@ -184,9 +213,9 @@ static NTSTATUS expect_queue_add_pending( struct expect_queue *queue, IRP *irp )
NTSTATUS
status
;
KIRQL
irql
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
status
=
expect_queue_add_pending_locked
(
queu
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
status
=
wait_queue_add_pending_locked
(
&
queue
->
bas
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
base
.
lock
,
irql
);
return
status
;
}
...
...
@@ -198,16 +227,16 @@ static NTSTATUS expect_queue_wait_pending( struct expect_queue *queue, IRP *irp
IRP
*
pending
;
KIRQL
irql
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
if
((
pending
=
queue
->
pending_wait
))
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
if
((
pending
=
queue
->
base
.
pending_wait
))
{
queue
->
pending_wait
=
NULL
;
queue
->
base
.
pending_wait
=
NULL
;
if
(
!
IoSetCancelRoutine
(
pending
,
NULL
))
pending
=
NULL
;
}
if
(
pending
&&
queue
->
pos
==
queue
->
end
)
status
=
STATUS_SUCCESS
;
else
status
=
expect_queue_add_pending_locked
(
queu
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
else
status
=
wait_queue_add_pending_locked
(
&
queue
->
bas
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
base
.
lock
,
irql
);
if
(
pending
)
{
...
...
@@ -225,10 +254,10 @@ static NTSTATUS expect_queue_wait( struct expect_queue *queue, IRP *irp )
KIRQL
irql
;
irp
->
IoStatus
.
Information
=
0
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
if
(
queue
->
pos
==
queue
->
end
)
status
=
STATUS_SUCCESS
;
else
status
=
expect_queue_add_pending_locked
(
queu
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
else
status
=
wait_queue_add_pending_locked
(
&
queue
->
bas
e
,
irp
);
KeReleaseSpinLock
(
&
queue
->
base
.
lock
,
irql
);
return
status
;
}
...
...
@@ -240,14 +269,13 @@ static void expect_queue_next( struct expect_queue *queue, ULONG code, HID_XFER_
ULONG
len
=
packet
->
reportBufferLen
;
BYTE
*
buf
=
packet
->
reportBuffer
;
BYTE
id
=
packet
->
reportId
;
IRP
*
irp
=
NULL
;
KIRQL
irql
;
missing
=
ExAllocatePool
(
PagedPool
,
EXPECT_QUEUE_BUFFER_SIZE
);
RtlSecureZeroMemory
(
missing
,
EXPECT_QUEUE_BUFFER_SIZE
);
missing_end
=
missing
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
tmp
=
queue
->
pos
;
while
(
tmp
<
queue
->
end
)
{
...
...
@@ -270,28 +298,12 @@ static void expect_queue_next( struct expect_queue *queue, ULONG code, HID_XFER_
queue
->
pos
++
;
}
if
((
irp
=
queue
->
pending_wait
))
{
/* don't mark the IRP as pending if someone's already waiting */
if
(
expect
->
ret_status
==
STATUS_PENDING
)
expect
->
ret_status
=
STATUS_SUCCESS
;
/* complete the pending wait IRP if the queue is now empty */
if
(
queue
->
pos
!=
queue
->
end
)
irp
=
NULL
;
else
{
queue
->
pending_wait
=
NULL
;
if
(
!
IoSetCancelRoutine
(
irp
,
NULL
))
irp
=
NULL
;
}
}
/* don't mark the IRP as pending if someone's already waiting */
if
(
expect
->
ret_status
==
STATUS_PENDING
&&
queue
->
base
.
pending_wait
)
expect
->
ret_status
=
STATUS_SUCCESS
;
memcpy
(
context
,
queue
->
context
,
context_size
);
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
if
(
irp
)
{
irp
->
IoStatus
.
Status
=
STATUS_SUCCESS
;
IoCompleteRequest
(
irp
,
IO_NO_INCREMENT
);
}
wait_queue_unlock
(
&
queue
->
base
,
irql
,
queue
->
pos
==
queue
->
end
);
ok
(
tmp
!=
&
queue
->
spurious
,
"%s got spurious packet
\n
"
,
context
);
...
...
@@ -368,7 +380,7 @@ static void irp_queue_init( struct irp_queue *queue )
struct
input_queue
{
KSPIN_LOCK
lock
;
struct
wait_queue
base
;
BOOL
is_polled
;
struct
hid_expect
*
pos
;
struct
hid_expect
*
end
;
...
...
@@ -379,7 +391,7 @@ struct input_queue
static
void
input_queue_init
(
struct
input_queue
*
queue
,
BOOL
is_polled
)
{
KeInitializeSpinLock
(
&
queue
->
lock
);
KeInitializeSpinLock
(
&
queue
->
base
.
lock
);
queue
->
is_polled
=
is_polled
;
queue
->
buffer
=
ExAllocatePool
(
PagedPool
,
EXPECT_QUEUE_BUFFER_SIZE
);
RtlSecureZeroMemory
(
queue
->
buffer
,
EXPECT_QUEUE_BUFFER_SIZE
);
...
...
@@ -391,6 +403,7 @@ static void input_queue_init( struct input_queue *queue, BOOL is_polled )
static
void
input_queue_cleanup
(
struct
input_queue
*
queue
)
{
wait_queue_cleanup
(
&
queue
->
base
);
ExFreePool
(
queue
->
buffer
);
}
...
...
@@ -418,7 +431,7 @@ static NTSTATUS input_queue_read( struct input_queue *queue, IRP *irp )
NTSTATUS
status
;
KIRQL
irql
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
if
(
input_queue_read_locked
(
queue
,
irp
))
{
irp_queue_push
(
&
queue
->
completed
,
irp
);
...
...
@@ -430,7 +443,7 @@ static NTSTATUS input_queue_read( struct input_queue *queue, IRP *irp )
irp_queue_push
(
&
queue
->
pending
,
irp
);
status
=
STATUS_PENDING
;
}
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
wait_queue_unlock
(
&
queue
->
base
,
irql
,
queue
->
pos
==
queue
->
end
);
return
status
;
}
...
...
@@ -441,7 +454,7 @@ static void input_queue_reset( struct input_queue *queue, void *in_buf, ULONG in
KIRQL
irql
;
IRP
*
irp
;
KeAcquireSpinLock
(
&
queue
->
lock
,
&
irql
);
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
remaining
=
queue
->
end
-
queue
->
pos
;
queue
->
pos
=
queue
->
buffer
;
queue
->
end
=
queue
->
buffer
;
...
...
@@ -453,13 +466,28 @@ static void input_queue_reset( struct input_queue *queue, void *in_buf, ULONG in
input_queue_read_locked
(
queue
,
irp
);
irp_queue_push
(
&
queue
->
completed
,
irp
);
}
KeReleaseSpinLock
(
&
queue
->
lock
,
irql
);
wait_queue_unlock
(
&
queue
->
base
,
irql
,
queue
->
pos
==
queue
->
end
);
if
(
!
queue
->
is_polled
)
ok
(
!
remaining
,
"unread input
\n
"
);
irp_queue_complete
(
&
queue
->
completed
,
FALSE
);
}
/* wait for the input queue to empty */
static
NTSTATUS
input_queue_wait
(
struct
input_queue
*
queue
,
IRP
*
irp
)
{
NTSTATUS
status
;
KIRQL
irql
;
irp
->
IoStatus
.
Information
=
0
;
KeAcquireSpinLock
(
&
queue
->
base
.
lock
,
&
irql
);
if
(
queue
->
pos
==
queue
->
end
)
status
=
STATUS_SUCCESS
;
else
status
=
wait_queue_add_pending_locked
(
&
queue
->
base
,
irp
);
KeReleaseSpinLock
(
&
queue
->
base
.
lock
,
irql
);
return
status
;
}
struct
device
{
KSPIN_LOCK
lock
;
...
...
@@ -1277,11 +1305,13 @@ static NTSTATUS pdo_handle_ioctl( struct phys_device *impl, IRP *irp, ULONG code
if
(
in_size
>
EXPECT_QUEUE_BUFFER_SIZE
)
return
STATUS_BUFFER_OVERFLOW
;
input_queue_reset
(
&
impl
->
input_queue
,
in_buffer
,
in_size
);
return
STATUS_SUCCESS
;
case
IOCTL_WINETEST_HID_WAIT_INPUT
:
return
input_queue_wait
(
&
impl
->
input_queue
,
irp
);
case
IOCTL_WINETEST_HID_SET_CONTEXT
:
if
(
in_size
>
sizeof
(
impl
->
expect_queue
.
context
))
return
STATUS_BUFFER_OVERFLOW
;
KeAcquireSpinLock
(
&
impl
->
expect_queue
.
lock
,
&
irql
);
KeAcquireSpinLock
(
&
impl
->
expect_queue
.
base
.
lock
,
&
irql
);
memcpy
(
impl
->
expect_queue
.
context
,
in_buffer
,
in_size
);
KeReleaseSpinLock
(
&
impl
->
expect_queue
.
lock
,
irql
);
KeReleaseSpinLock
(
&
impl
->
expect_queue
.
base
.
lock
,
irql
);
return
STATUS_SUCCESS
;
case
IOCTL_WINETEST_REMOVE_DEVICE
:
KeAcquireSpinLock
(
&
impl
->
base
.
lock
,
&
irql
);
...
...
@@ -1356,6 +1386,7 @@ static NTSTATUS fdo_ioctl( DEVICE_OBJECT *device, IRP *irp )
case
IOCTL_WINETEST_HID_WAIT_EXPECT
:
case
IOCTL_WINETEST_HID_SEND_INPUT
:
case
IOCTL_WINETEST_HID_SET_CONTEXT
:
case
IOCTL_WINETEST_HID_WAIT_INPUT
:
if
(
in_size
<
sizeof
(
*
desc
))
status
=
STATUS_INVALID_PARAMETER
;
else
if
(
!
(
device
=
find_child_device
(
impl
,
desc
))
||
!
(
pdo
=
pdo_from_DEVICE_OBJECT
(
device
)))
...
...
dlls/dinput/tests/driver_hid.c
View file @
bd2b8980
...
...
@@ -188,6 +188,7 @@ static NTSTATUS WINAPI driver_ioctl( DEVICE_OBJECT *device, IRP *irp )
case
IOCTL_WINETEST_HID_WAIT_EXPECT
:
case
IOCTL_WINETEST_HID_SEND_INPUT
:
case
IOCTL_WINETEST_HID_SET_CONTEXT
:
case
IOCTL_WINETEST_HID_WAIT_INPUT
:
IoSkipCurrentIrpStackLocation
(
irp
);
return
IoCallDriver
(
ext
->
PhysicalDeviceObject
,
irp
);
...
...
dlls/dinput/tests/driver_hid.h
View file @
bd2b8980
...
...
@@ -49,6 +49,7 @@ DEFINE_GUID(control_class,0xdeadbeef,0x29ef,0x4538,0xa5,0xfd,0xb6,0x95,0x73,0xa3
#define IOCTL_WINETEST_HID_SET_CONTEXT CTL_CODE(FILE_DEVICE_KEYBOARD, 0x803, METHOD_IN_DIRECT, FILE_ANY_ACCESS)
#define IOCTL_WINETEST_CREATE_DEVICE CTL_CODE(FILE_DEVICE_KEYBOARD, 0x804, METHOD_IN_DIRECT, FILE_ANY_ACCESS)
#define IOCTL_WINETEST_REMOVE_DEVICE CTL_CODE(FILE_DEVICE_KEYBOARD, 0x805, METHOD_IN_DIRECT, FILE_ANY_ACCESS)
#define IOCTL_WINETEST_HID_WAIT_INPUT CTL_CODE(FILE_DEVICE_KEYBOARD, 0x806, METHOD_IN_DIRECT, FILE_ANY_ACCESS)
struct
hid_expect
{
...
...
@@ -173,6 +174,7 @@ static inline const char *debugstr_ioctl( ULONG code )
case
IOCTL_WINETEST_HID_SET_EXPECT
:
return
"IOCTL_WINETEST_HID_SET_EXPECT"
;
case
IOCTL_WINETEST_HID_WAIT_EXPECT
:
return
"IOCTL_WINETEST_HID_WAIT_EXPECT"
;
case
IOCTL_WINETEST_HID_SEND_INPUT
:
return
"IOCTL_WINETEST_HID_SEND_INPUT"
;
case
IOCTL_WINETEST_HID_WAIT_INPUT
:
return
"IOCTL_WINETEST_HID_WAIT_INPUT"
;
case
IOCTL_WINETEST_HID_SET_CONTEXT
:
return
"IOCTL_WINETEST_HID_SET_CONTEXT"
;
case
IOCTL_WINETEST_CREATE_DEVICE
:
return
"IOCTL_WINETEST_CREATE_DEVICE"
;
case
IOCTL_WINETEST_REMOVE_DEVICE
:
return
"IOCTL_WINETEST_REMOVE_DEVICE"
;
...
...
dlls/dinput/tests/hid.c
View file @
bd2b8980
...
...
@@ -1001,6 +1001,24 @@ void send_hid_input_( const char *file, int line, HANDLE device, struct hid_devi
ok_
(
file
,
line
)(
ret
,
"IOCTL_WINETEST_HID_SEND_INPUT failed, last error %lu
\n
"
,
GetLastError
()
);
}
void
wait_hid_input_
(
const
char
*
file
,
int
line
,
HANDLE
device
,
struct
hid_device_desc
*
desc
,
DWORD
timeout
,
BOOL
todo
)
{
char
buffer
[
sizeof
(
*
desc
)];
SIZE_T
size
;
if
(
desc
)
memcpy
(
buffer
,
desc
,
sizeof
(
*
desc
)
);
else
memset
(
buffer
,
0
,
sizeof
(
*
desc
)
);
size
=
sizeof
(
*
desc
);
todo_wine_if
(
todo
)
{
BOOL
ret
=
sync_ioctl_
(
file
,
line
,
device
,
IOCTL_WINETEST_HID_WAIT_INPUT
,
buffer
,
size
,
NULL
,
0
,
timeout
);
ok_
(
file
,
line
)(
ret
,
"IOCTL_WINETEST_HID_WAIT_INPUT failed, last error %lu
\n
"
,
GetLastError
()
);
}
set_hid_expect_
(
file
,
line
,
device
,
desc
,
NULL
,
0
);
}
static
void
test_hidp_get_input
(
HANDLE
file
,
int
report_id
,
ULONG
report_len
,
PHIDP_PREPARSED_DATA
preparsed
)
{
struct
hid_expect
expect
[]
=
...
...
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