mirror of
https://github.com/reactos/reactos.git
synced 2024-08-16 00:19:32 +00:00
- Detect if reparsing is being used during IRP completion and complain.
- Free MDLs in a safer way by not actually using the Irp->MdlAddress as we're looping through them. - Don't leak an event for each Asynchronous API anymore. - Handle IRP_OB_QUERY_NAME completion properly. - handle IRP_CREATE_OPERATION with a file object present. - Use deferred delete for File Object dereferences, to speed up I/O completion. - Clear the I/O Stack Location when parsing completion stacks. - Support SL_ERROR_RETURNED during completion routines. svn path=/trunk/; revision=25993
This commit is contained in:
parent
030a4abe86
commit
aafc3a967a
|
@ -3813,6 +3813,7 @@ typedef struct _IO_STACK_LOCATION {
|
||||||
/* IO_STACK_LOCATION.Control */
|
/* IO_STACK_LOCATION.Control */
|
||||||
|
|
||||||
#define SL_PENDING_RETURNED 0x01
|
#define SL_PENDING_RETURNED 0x01
|
||||||
|
#define SL_ERROR_RETURNED 0x02
|
||||||
#define SL_INVOKE_ON_CANCEL 0x20
|
#define SL_INVOKE_ON_CANCEL 0x20
|
||||||
#define SL_INVOKE_ON_SUCCESS 0x40
|
#define SL_INVOKE_ON_SUCCESS 0x40
|
||||||
#define SL_INVOKE_ON_ERROR 0x80
|
#define SL_INVOKE_ON_ERROR 0x80
|
||||||
|
|
|
@ -239,7 +239,7 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
{
|
{
|
||||||
PFILE_OBJECT FileObject;
|
PFILE_OBJECT FileObject;
|
||||||
PIRP Irp;
|
PIRP Irp;
|
||||||
PMDL Mdl;
|
PMDL Mdl, NextMdl;
|
||||||
PVOID Port = NULL, Key = NULL;
|
PVOID Port = NULL, Key = NULL;
|
||||||
BOOLEAN SignaledCreateRequest = FALSE;
|
BOOLEAN SignaledCreateRequest = FALSE;
|
||||||
|
|
||||||
|
@ -252,10 +252,26 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
Irp,
|
Irp,
|
||||||
FileObject);
|
FileObject);
|
||||||
|
|
||||||
|
/* Sanity check */
|
||||||
|
ASSERT(Irp->IoStatus.Status != 0xFFFFFFFF);
|
||||||
|
|
||||||
|
/* Check if we have a file object */
|
||||||
|
if (*SystemArgument2)
|
||||||
|
{
|
||||||
|
/* Check if we're reparsing */
|
||||||
|
if ((Irp->IoStatus.Status == STATUS_REPARSE) &&
|
||||||
|
(Irp->IoStatus.Information == IO_REPARSE_TAG_MOUNT_POINT))
|
||||||
|
{
|
||||||
|
/* We should never get this yet */
|
||||||
|
DPRINT1("Reparse support not yet present!\n");
|
||||||
|
while (TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Handle Buffered case first */
|
/* Handle Buffered case first */
|
||||||
if (Irp->Flags & IRP_BUFFERED_IO)
|
if (Irp->Flags & IRP_BUFFERED_IO)
|
||||||
{
|
{
|
||||||
/* Check if we have an input buffer and if we suceeded */
|
/* Check if we have an input buffer and if we succeeded */
|
||||||
if ((Irp->Flags & IRP_INPUT_OPERATION) &&
|
if ((Irp->Flags & IRP_INPUT_OPERATION) &&
|
||||||
(Irp->IoStatus.Status != STATUS_VERIFY_REQUIRED) &&
|
(Irp->IoStatus.Status != STATUS_VERIFY_REQUIRED) &&
|
||||||
!(NT_ERROR(Irp->IoStatus.Status)))
|
!(NT_ERROR(Irp->IoStatus.Status)))
|
||||||
|
@ -278,22 +294,25 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
Irp->Flags &= ~(IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER);
|
Irp->Flags &= ~(IRP_BUFFERED_IO | IRP_DEALLOCATE_BUFFER);
|
||||||
|
|
||||||
/* Check if there's an MDL */
|
/* Check if there's an MDL */
|
||||||
while ((Mdl = Irp->MdlAddress))
|
for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
|
||||||
{
|
{
|
||||||
/* Clear all of them */
|
/* Free it */
|
||||||
Irp->MdlAddress = Mdl->Next;
|
NextMdl = Mdl->Next;
|
||||||
IoFreeMdl(Mdl);
|
IoFreeMdl(Mdl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No MDLs left */
|
||||||
|
Irp->MdlAddress = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check if either the request was completed without any errors
|
* Check if either the request was completed without any errors
|
||||||
* (but warnings are OK!), or if it was completed with an error, but
|
* (but warnings are OK!), or if it was completed with an error, but
|
||||||
* did return from a pending I/O Operation and is not synchronous.
|
* did return from a pending I/O Operation and is not synchronous.
|
||||||
*/
|
*/
|
||||||
if ((!NT_ERROR(Irp->IoStatus.Status)) ||
|
if (!(NT_ERROR(Irp->IoStatus.Status)) ||
|
||||||
(NT_ERROR(Irp->IoStatus.Status) &&
|
(NT_ERROR(Irp->IoStatus.Status) &&
|
||||||
(Irp->PendingReturned) &&
|
(Irp->PendingReturned) &&
|
||||||
!(IsIrpSynchronous(Irp, FileObject))))
|
!(IsIrpSynchronous(Irp, FileObject))))
|
||||||
{
|
{
|
||||||
/* Get any information we need from the FO before we kill it */
|
/* Get any information we need from the FO before we kill it */
|
||||||
if ((FileObject) && (FileObject->CompletionContext))
|
if ((FileObject) && (FileObject->CompletionContext))
|
||||||
|
@ -324,12 +343,20 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
/* Check if we also have a File Object */
|
/* Check if we also have a File Object */
|
||||||
if (FileObject)
|
if (FileObject)
|
||||||
{
|
{
|
||||||
|
/* Check if this is an Asynch API */
|
||||||
|
if (!(Irp->Flags & IRP_SYNCHRONOUS_API))
|
||||||
|
{
|
||||||
|
/* Dereference the event */
|
||||||
|
ObDereferenceObject(Irp->UserEvent);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Now, if this is a Synch I/O File Object, then this event is
|
* Now, if this is a Synch I/O File Object, then this event is
|
||||||
* NOT an actual Executive Event, so we won't dereference it,
|
* NOT an actual Executive Event, so we won't dereference it,
|
||||||
* and instead, we will signal the File Object
|
* and instead, we will signal the File Object
|
||||||
*/
|
*/
|
||||||
if (FileObject->Flags & FO_SYNCHRONOUS_IO)
|
if ((FileObject->Flags & FO_SYNCHRONOUS_IO) &&
|
||||||
|
!(Irp->Flags & IRP_OB_QUERY_NAME))
|
||||||
{
|
{
|
||||||
/* Signal the file object and set the status */
|
/* Signal the file object and set the status */
|
||||||
KeSetEvent(&FileObject->Event, 0, FALSE);
|
KeSetEvent(&FileObject->Event, 0, FALSE);
|
||||||
|
@ -342,7 +369,7 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
*/
|
*/
|
||||||
if (Irp->Flags & IRP_CREATE_OPERATION)
|
if (Irp->Flags & IRP_CREATE_OPERATION)
|
||||||
{
|
{
|
||||||
/* Clear the APC Routine and remmeber this */
|
/* Clear the APC Routine and remember this */
|
||||||
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
|
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
|
||||||
SignaledCreateRequest = TRUE;
|
SignaledCreateRequest = TRUE;
|
||||||
}
|
}
|
||||||
|
@ -353,6 +380,17 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
/* Signal the file object and set the status */
|
/* Signal the file object and set the status */
|
||||||
KeSetEvent(&FileObject->Event, 0, FALSE);
|
KeSetEvent(&FileObject->Event, 0, FALSE);
|
||||||
FileObject->FinalStatus = Irp->IoStatus.Status;
|
FileObject->FinalStatus = Irp->IoStatus.Status;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This could also be a create operation, in which case we want
|
||||||
|
* to make sure there's no APC fired.
|
||||||
|
*/
|
||||||
|
if (Irp->Flags & IRP_CREATE_OPERATION)
|
||||||
|
{
|
||||||
|
/* Clear the APC Routine and remember this */
|
||||||
|
Irp->Overlay.AsynchronousParameters.UserApcRoutine = NULL;
|
||||||
|
SignaledCreateRequest = TRUE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Now that we've signaled the events, de-associate the IRP */
|
/* Now that we've signaled the events, de-associate the IRP */
|
||||||
|
@ -394,7 +432,7 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
if ((FileObject) && !(SignaledCreateRequest))
|
if ((FileObject) && !(SignaledCreateRequest))
|
||||||
{
|
{
|
||||||
/* Dereference it, since it's not needed anymore either */
|
/* Dereference it, since it's not needed anymore either */
|
||||||
ObDereferenceObject(FileObject);
|
ObDereferenceObjectDeferDelete(FileObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -408,7 +446,7 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
/* So we did return with a synch operation, was it the IRP? */
|
/* So we did return with a synch operation, was it the IRP? */
|
||||||
if (Irp->Flags & IRP_SYNCHRONOUS_API)
|
if (Irp->Flags & IRP_SYNCHRONOUS_API)
|
||||||
{
|
{
|
||||||
/* Yes, this IRP was synchronous, so retrn the I/O Status */
|
/* Yes, this IRP was synchronous, so return the I/O Status */
|
||||||
*Irp->UserIosb = Irp->IoStatus;
|
*Irp->UserIosb = Irp->IoStatus;
|
||||||
|
|
||||||
/* Now check if the user gave an event */
|
/* Now check if the user gave an event */
|
||||||
|
@ -438,7 +476,7 @@ IopCompleteRequest(IN PKAPC Apc,
|
||||||
if ((FileObject) && !(Irp->Flags & IRP_CREATE_OPERATION))
|
if ((FileObject) && !(Irp->Flags & IRP_CREATE_OPERATION))
|
||||||
{
|
{
|
||||||
/* Dereference the File Object unless this was a create */
|
/* Dereference the File Object unless this was a create */
|
||||||
ObDereferenceObject(FileObject);
|
ObDereferenceObjectDeferDelete(FileObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -520,7 +558,7 @@ IoAllocateIrp(IN CCHAR StackSize,
|
||||||
/* Did we try lookaside and fail? */
|
/* Did we try lookaside and fail? */
|
||||||
if (Flags & IRP_ALLOCATED_FIXED_SIZE) List->L.AllocateMisses++;
|
if (Flags & IRP_ALLOCATED_FIXED_SIZE) List->L.AllocateMisses++;
|
||||||
|
|
||||||
/* Check if we shoudl charge quota */
|
/* Check if we should charge quota */
|
||||||
if (ChargeQuota)
|
if (ChargeQuota)
|
||||||
{
|
{
|
||||||
/* Irp = ExAllocatePoolWithQuotaTag(NonPagedPool, Size, TAG_IRP); */
|
/* Irp = ExAllocatePoolWithQuotaTag(NonPagedPool, Size, TAG_IRP); */
|
||||||
|
@ -1087,6 +1125,19 @@ IofCallDriver(IN PDEVICE_OBJECT DeviceObject,
|
||||||
Irp);
|
Irp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FORCEINLINE
|
||||||
|
VOID
|
||||||
|
IopClearStackLocation(IN PIO_STACK_LOCATION IoStackLocation)
|
||||||
|
{
|
||||||
|
IoStackLocation->MinorFunction = 0;
|
||||||
|
IoStackLocation->Flags = 0;
|
||||||
|
IoStackLocation->Control &= SL_ERROR_RETURNED;
|
||||||
|
IoStackLocation->Parameters.Others.Argument1 = 0;
|
||||||
|
IoStackLocation->Parameters.Others.Argument2 = 0;
|
||||||
|
IoStackLocation->Parameters.Others.Argument3 = 0;
|
||||||
|
IoStackLocation->FileObject = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @implemented
|
* @implemented
|
||||||
*/
|
*/
|
||||||
|
@ -1095,33 +1146,42 @@ FASTCALL
|
||||||
IofCompleteRequest(IN PIRP Irp,
|
IofCompleteRequest(IN PIRP Irp,
|
||||||
IN CCHAR PriorityBoost)
|
IN CCHAR PriorityBoost)
|
||||||
{
|
{
|
||||||
PIO_STACK_LOCATION StackPtr;
|
PIO_STACK_LOCATION StackPtr, LastStackPtr;
|
||||||
PDEVICE_OBJECT DeviceObject;
|
PDEVICE_OBJECT DeviceObject;
|
||||||
PFILE_OBJECT FileObject;
|
PFILE_OBJECT FileObject;
|
||||||
PETHREAD Thread;
|
PETHREAD Thread;
|
||||||
NTSTATUS Status;
|
NTSTATUS Status;
|
||||||
PMDL Mdl;
|
PMDL Mdl, NextMdl;
|
||||||
ULONG MasterIrpCount;
|
ULONG MasterCount;
|
||||||
PIRP MasterIrp;
|
PIRP MasterIrp;
|
||||||
ULONG Flags;
|
ULONG Flags;
|
||||||
|
NTSTATUS ErrorCode = STATUS_SUCCESS;
|
||||||
IOTRACE(IO_IRP_DEBUG,
|
IOTRACE(IO_IRP_DEBUG,
|
||||||
"%s - Completing IRP %p\n",
|
"%s - Completing IRP %p\n",
|
||||||
__FUNCTION__,
|
__FUNCTION__,
|
||||||
Irp);
|
Irp);
|
||||||
|
|
||||||
/* Make sure this IRP isn't getting completed twice or is invalid */
|
/* Make sure this IRP isn't getting completed twice or is invalid */
|
||||||
if (((Irp->CurrentLocation) > (Irp->StackCount + 1)) ||
|
if ((Irp->CurrentLocation) > (Irp->StackCount + 1))
|
||||||
(Irp->Type != IO_TYPE_IRP))
|
|
||||||
{
|
{
|
||||||
/* Bugcheck */
|
/* Bugcheck */
|
||||||
KeBugCheckEx(MULTIPLE_IRP_COMPLETE_REQUESTS, (ULONG_PTR)Irp, 0, 0, 0);
|
KeBugCheckEx(MULTIPLE_IRP_COMPLETE_REQUESTS, (ULONG_PTR)Irp, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Some sanity checks */
|
/* Some sanity checks */
|
||||||
|
ASSERT(Irp->Type == IO_TYPE_IRP);
|
||||||
ASSERT(!Irp->CancelRoutine);
|
ASSERT(!Irp->CancelRoutine);
|
||||||
ASSERT(Irp->IoStatus.Status != STATUS_PENDING);
|
ASSERT(Irp->IoStatus.Status != STATUS_PENDING);
|
||||||
ASSERT(Irp->IoStatus.Status != 0xFFFFFFFF);
|
ASSERT(Irp->IoStatus.Status != 0xFFFFFFFF);
|
||||||
|
|
||||||
|
/* Get the last stack */
|
||||||
|
LastStackPtr = (PIO_STACK_LOCATION)(Irp + 1);
|
||||||
|
if (LastStackPtr->Control & SL_ERROR_RETURNED)
|
||||||
|
{
|
||||||
|
/* Get the error code */
|
||||||
|
ErrorCode = (NTSTATUS)LastStackPtr->Parameters.Others.Argument4;
|
||||||
|
}
|
||||||
|
|
||||||
/* Get the Current Stack and skip it */
|
/* Get the Current Stack and skip it */
|
||||||
StackPtr = IoGetCurrentIrpStackLocation(Irp);
|
StackPtr = IoGetCurrentIrpStackLocation(Irp);
|
||||||
IoSkipCurrentIrpStackLocation(Irp);
|
IoSkipCurrentIrpStackLocation(Irp);
|
||||||
|
@ -1132,13 +1192,31 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
/* Set Pending Returned */
|
/* Set Pending Returned */
|
||||||
Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;
|
Irp->PendingReturned = StackPtr->Control & SL_PENDING_RETURNED;
|
||||||
|
|
||||||
|
/* Check if we failed */
|
||||||
|
if (!NT_SUCCESS(Irp->IoStatus.Status))
|
||||||
|
{
|
||||||
|
/* Check if it was changed by a completion routine */
|
||||||
|
if (Irp->IoStatus.Status != ErrorCode)
|
||||||
|
{
|
||||||
|
/* Update the error for the current stack */
|
||||||
|
ErrorCode = Irp->IoStatus.Status;
|
||||||
|
StackPtr->Control |= SL_ERROR_RETURNED;
|
||||||
|
LastStackPtr->Parameters.Others.Argument4 = (PVOID)ErrorCode;
|
||||||
|
LastStackPtr->Control |= SL_ERROR_RETURNED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Check if there is a Completion Routine to Call */
|
/* Check if there is a Completion Routine to Call */
|
||||||
if ((NT_SUCCESS(Irp->IoStatus.Status) &&
|
if ((NT_SUCCESS(Irp->IoStatus.Status) &&
|
||||||
(StackPtr->Control & SL_INVOKE_ON_SUCCESS)) ||
|
(StackPtr->Control & SL_INVOKE_ON_SUCCESS)) ||
|
||||||
(!NT_SUCCESS(Irp->IoStatus.Status) &&
|
(!NT_SUCCESS(Irp->IoStatus.Status) &&
|
||||||
(StackPtr->Control & SL_INVOKE_ON_ERROR)) ||
|
(StackPtr->Control & SL_INVOKE_ON_ERROR)) ||
|
||||||
(Irp->Cancel && (StackPtr->Control & SL_INVOKE_ON_CANCEL)))
|
(Irp->Cancel &&
|
||||||
|
(StackPtr->Control & SL_INVOKE_ON_CANCEL)))
|
||||||
{
|
{
|
||||||
|
/* Clear the stack location */
|
||||||
|
IopClearStackLocation(StackPtr);
|
||||||
|
|
||||||
/* Check for highest-level device completion routines */
|
/* Check for highest-level device completion routines */
|
||||||
if (Irp->CurrentLocation == (Irp->StackCount + 1))
|
if (Irp->CurrentLocation == (Irp->StackCount + 1))
|
||||||
{
|
{
|
||||||
|
@ -1168,6 +1246,9 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
/* Mark it as pending */
|
/* Mark it as pending */
|
||||||
IoMarkIrpPending(Irp);
|
IoMarkIrpPending(Irp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clear the stack location */
|
||||||
|
IopClearStackLocation(StackPtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Move to next stack location and pointer */
|
/* Move to next stack location and pointer */
|
||||||
|
@ -1180,17 +1261,13 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
{
|
{
|
||||||
/* Get the master IRP and count */
|
/* Get the master IRP and count */
|
||||||
MasterIrp = Irp->AssociatedIrp.MasterIrp;
|
MasterIrp = Irp->AssociatedIrp.MasterIrp;
|
||||||
MasterIrpCount = InterlockedDecrement(&MasterIrp->
|
MasterCount = InterlockedDecrement(&MasterIrp->AssociatedIrp.IrpCount);
|
||||||
AssociatedIrp.IrpCount);
|
|
||||||
|
|
||||||
/* Set the thread of this IRP as the master's */
|
|
||||||
Irp->Tail.Overlay.Thread = MasterIrp->Tail.Overlay.Thread;
|
|
||||||
|
|
||||||
/* Free the MDLs */
|
/* Free the MDLs */
|
||||||
while ((Mdl = Irp->MdlAddress))
|
for (Mdl = Irp->MdlAddress; Mdl; Mdl = NextMdl)
|
||||||
{
|
{
|
||||||
/* Go to the next one */
|
/* Go to the next one */
|
||||||
Irp->MdlAddress = Mdl->Next;
|
NextMdl = Mdl->Next;
|
||||||
IoFreeMdl(Mdl);
|
IoFreeMdl(Mdl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1198,7 +1275,7 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
IoFreeIrp(Irp);
|
IoFreeIrp(Irp);
|
||||||
|
|
||||||
/* Complete the Master IRP */
|
/* Complete the Master IRP */
|
||||||
if (!MasterIrpCount) IofCompleteRequest(MasterIrp, PriorityBoost);
|
if (!MasterCount) IofCompleteRequest(MasterIrp, PriorityBoost);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1216,11 +1293,11 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
/* Check if this is a Paging I/O or Close Operation */
|
/* Check if this is a Paging I/O or Close Operation */
|
||||||
if (Irp->Flags & (IRP_PAGING_IO | IRP_CLOSE_OPERATION))
|
if (Irp->Flags & (IRP_PAGING_IO | IRP_CLOSE_OPERATION))
|
||||||
{
|
{
|
||||||
/* Handle a Close Operation or Sync Paging I/O (see page 165) */
|
/* Handle a Close Operation or Sync Paging I/O */
|
||||||
if (Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_CLOSE_OPERATION))
|
if (Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_CLOSE_OPERATION))
|
||||||
{
|
{
|
||||||
/* Set the I/O Status and Signal the Event */
|
/* Set the I/O Status and Signal the Event */
|
||||||
Flags = Irp->Flags & IRP_SYNCHRONOUS_PAGING_IO;
|
Flags = Irp->Flags & (IRP_SYNCHRONOUS_PAGING_IO | IRP_PAGING_IO);
|
||||||
*Irp->UserIosb = Irp->IoStatus;
|
*Irp->UserIosb = Irp->IoStatus;
|
||||||
KeSetEvent(Irp->UserEvent, PriorityBoost, FALSE);
|
KeSetEvent(Irp->UserEvent, PriorityBoost, FALSE);
|
||||||
|
|
||||||
|
@ -1245,7 +1322,8 @@ IofCompleteRequest(IN PIRP Irp,
|
||||||
PriorityBoost);
|
PriorityBoost);
|
||||||
#else
|
#else
|
||||||
/* Not implemented yet. */
|
/* Not implemented yet. */
|
||||||
ASSERT(FALSE);
|
DPRINT1("Not supported!\n");
|
||||||
|
while (TRUE);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue