551 lines
16 KiB
C
551 lines
16 KiB
C
|
#include "mpath.h"
|
|||
|
#include <wmistr.h>
|
|||
|
#include <wmidata.h>
|
|||
|
#include <stdio.h>
|
|||
|
|
|||
|
|
|||
|
#ifdef USE_BINARY_MOF_QUERY
|
|||
|
//
|
|||
|
// MOF data can be reported by a device driver via a resource attached to
|
|||
|
// the device drivers image file or in response to a query on the binary
|
|||
|
// mof data guid. Here we define global variables containing the binary mof
|
|||
|
// data to return in response to a binary mof guid query. Note that this
|
|||
|
// data is defined to be in a PAGED data segment since it does not need to
|
|||
|
// be in nonpaged memory. Note that instead of a single large mof file
|
|||
|
// we could have broken it into multiple individual files. Each file would
|
|||
|
// have its own binary mof data buffer and get reported via a different
|
|||
|
// instance of the binary mof guid. By mixing and matching the different
|
|||
|
// sets of binary mof data buffers a "dynamic" composite mof would be created.
|
|||
|
|
|||
|
#ifdef ALLOC_DATA_PRAGMA
|
|||
|
#pragma data_seg("PAGED")
|
|||
|
#endif
|
|||
|
|
|||
|
#pragma message ("USE_BINARY defined ChuckP")
|
|||
|
UCHAR MPathBinaryMofData[] =
|
|||
|
{
|
|||
|
#include "mpathwmi.x"
|
|||
|
};
|
|||
|
#ifdef ALLOC_DATA_PRAGMA
|
|||
|
#pragma data_seg()
|
|||
|
#endif
|
|||
|
#endif
|
|||
|
|
|||
|
|
|||
|
WMIGUIDREGINFO MPathWmiGuidList[] = {
|
|||
|
{
|
|||
|
&MPath_Disk_Info_GUID,
|
|||
|
1,
|
|||
|
0
|
|||
|
},
|
|||
|
{
|
|||
|
&MPath_Test_GUID,
|
|||
|
1,
|
|||
|
0
|
|||
|
},
|
|||
|
|
|||
|
{
|
|||
|
&MSWmi_MofData_GUID,
|
|||
|
1,
|
|||
|
#ifdef USE_BINARY_MOF_QUERY
|
|||
|
0
|
|||
|
#else
|
|||
|
WMIREG_FLAG_REMOVE_GUID
|
|||
|
#endif
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
#define DiskInformation 0
|
|||
|
#define MPathTest 1
|
|||
|
#define BinaryMofGuid 2
|
|||
|
|
|||
|
#define MPathGuidCount (sizeof(MPathWmiGuidList) / sizeof(WMIGUIDREGINFO))
|
|||
|
|
|||
|
//#define MPATH_GUID_PDISK_INFO 0
|
|||
|
//#define MPATH_BINARY_MOF 1
|
|||
|
//#define MPATH_GUID_FAILOVER_INFO 1
|
|||
|
//#define MPATH_GUID_CONFIG_INFO 2
|
|||
|
//#define MPATH_GUID_DSM_NAME 3
|
|||
|
//#define MPATH_GUID_PATH_MAINTENACE 4
|
|||
|
//
|
|||
|
// Always update this for any guid additions.
|
|||
|
// This indicates the dividing line between our and DSM
|
|||
|
// guids.
|
|||
|
//
|
|||
|
//#define MPATH_MAX_GUID_INDEX MPATH_BINARY_MOF
|
|||
|
//
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathFdoWmi(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp
|
|||
|
)
|
|||
|
{
|
|||
|
PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
|
|||
|
|
|||
|
IoSkipCurrentIrpStackLocation(Irp);
|
|||
|
|
|||
|
return IoCallDriver(deviceExtension->TargetObject, Irp);
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathQueryWmiRegInfo(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
OUT PULONG RegFlags,
|
|||
|
OUT PUNICODE_STRING InstanceName,
|
|||
|
OUT PUNICODE_STRING *RegistryPath,
|
|||
|
OUT PUNICODE_STRING MofResourceName,
|
|||
|
OUT PDEVICE_OBJECT *Pdo
|
|||
|
)
|
|||
|
{
|
|||
|
PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
|
|||
|
|
|||
|
*RegFlags = WMIREG_FLAG_INSTANCE_PDO;
|
|||
|
*RegistryPath = &deviceExtension->RegistryPath;
|
|||
|
*Pdo = deviceExtension->DeviceObject;
|
|||
|
MPDebugPrint((0,
|
|||
|
"MPathQueryWmiRegInfo: *Pdo (%x), DeviceObject (%x)\n",
|
|||
|
*Pdo,
|
|||
|
DeviceObject));
|
|||
|
#ifndef USE_BINARY_MOF_QUERY
|
|||
|
#pragma message ("BIN NOT - ChuckP")
|
|||
|
//
|
|||
|
// Return the name specified in the .rc file of the resource which
|
|||
|
// contains the bianry mof data. By default WMI will look for this
|
|||
|
// resource in the driver image (.sys) file, however if the value
|
|||
|
// MofImagePath is specified in the driver's registry key
|
|||
|
// then WMI will look for the resource in the file specified there.
|
|||
|
RtlInitUnicodeString(MofResourceName, L"MofResourceName");
|
|||
|
#endif
|
|||
|
|
|||
|
return STATUS_SUCCESS;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathQueryWmiDataBlock(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp,
|
|||
|
IN ULONG GuidIndex,
|
|||
|
IN ULONG InstanceIndex,
|
|||
|
IN ULONG InstanceCount,
|
|||
|
IN OUT PULONG InstanceLengthArray,
|
|||
|
IN ULONG BufferAvail,
|
|||
|
OUT PUCHAR Buffer
|
|||
|
)
|
|||
|
{
|
|||
|
PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
|
|||
|
PPSEUDO_DISK_EXTENSION diskExtension = &deviceExtension->PseudoDiskExtension;
|
|||
|
NTSTATUS status;
|
|||
|
ULONG i;
|
|||
|
ULONG bytesReturned = 0;
|
|||
|
|
|||
|
if (TRUE) { //(GuidIndex <= MPATH_MAX_GUID_INDEX) {
|
|||
|
|
|||
|
//
|
|||
|
// This is for the pdisk
|
|||
|
// Ensure that this is a QUERY_SINGLE_INSTANCE, as these data blocks
|
|||
|
// are defined as having only one instance.
|
|||
|
//
|
|||
|
if (InstanceIndex != 0 || InstanceCount != 1) {
|
|||
|
status = STATUS_INVALID_DEVICE_REQUEST;
|
|||
|
} else {
|
|||
|
switch (GuidIndex) {
|
|||
|
case MPathTest:
|
|||
|
bytesReturned = sizeof(ULONG);
|
|||
|
if (bytesReturned > BufferAvail) {
|
|||
|
status = STATUS_BUFFER_TOO_SMALL;
|
|||
|
} else {
|
|||
|
ULONG theValue = 0xaa55aa55;
|
|||
|
|
|||
|
RtlCopyMemory(Buffer,
|
|||
|
&theValue,
|
|||
|
sizeof(ULONG));
|
|||
|
status = STATUS_SUCCESS;
|
|||
|
|
|||
|
}
|
|||
|
break;
|
|||
|
case DiskInformation: {
|
|||
|
PMPath_Disk_Info diskInfo = (PMPath_Disk_Info)Buffer;
|
|||
|
PSTORAGE_DEVICE_DESCRIPTOR storageDescriptor;
|
|||
|
ULONG serialNumberLength;
|
|||
|
ULONG totalStringLength;
|
|||
|
PUCHAR serialNumber;
|
|||
|
ANSI_STRING ansiSerialNumber;
|
|||
|
UNICODE_STRING unicodeString;
|
|||
|
|
|||
|
bytesReturned = sizeof(MPath_Disk_Info);
|
|||
|
|
|||
|
//
|
|||
|
// Determine the serial number length.
|
|||
|
//
|
|||
|
storageDescriptor =
|
|||
|
diskExtension->DeviceDescriptors[0].StorageDescriptor;
|
|||
|
serialNumber = (PUCHAR)storageDescriptor;
|
|||
|
|
|||
|
//
|
|||
|
// Move serialNumber to the correct position in RawData.
|
|||
|
//
|
|||
|
(ULONG_PTR)serialNumber += storageDescriptor->SerialNumberOffset;
|
|||
|
|
|||
|
//
|
|||
|
// Get it's length.
|
|||
|
//
|
|||
|
serialNumberLength = strlen(serialNumber);
|
|||
|
totalStringLength = serialNumberLength * sizeof(WCHAR);
|
|||
|
totalStringLength += sizeof(USHORT);
|
|||
|
totalStringLength *= diskExtension->NumberPhysicalPaths;
|
|||
|
|
|||
|
|
|||
|
bytesReturned += totalStringLength;
|
|||
|
bytesReturned += (diskExtension->NumberPhysicalPaths - 1) *
|
|||
|
sizeof(PHYSICAL_DISK_INFO);
|
|||
|
|
|||
|
|
|||
|
if (bytesReturned > BufferAvail) {
|
|||
|
status = STATUS_BUFFER_TOO_SMALL;
|
|||
|
} else {
|
|||
|
PWCHAR index;
|
|||
|
PDEVICE_DESCRIPTOR deviceDescriptor;
|
|||
|
PPHYSICAL_DISK_INFO deviceInfo = diskInfo->DeviceInfo;
|
|||
|
|
|||
|
diskInfo->NumberDrives = diskExtension->NumberPhysicalPaths;
|
|||
|
diskInfo->IsLoadBalance = FALSE;
|
|||
|
|
|||
|
RtlInitAnsiString(&ansiSerialNumber, serialNumber);
|
|||
|
RtlAnsiStringToUnicodeString(&unicodeString, &ansiSerialNumber, TRUE);
|
|||
|
|
|||
|
for (i = 0; i < diskExtension->NumberPhysicalPaths; i++) {
|
|||
|
|
|||
|
deviceDescriptor =
|
|||
|
&diskExtension->DeviceDescriptors[i];
|
|||
|
deviceInfo->BusType = 1;
|
|||
|
deviceInfo->PortNumber =
|
|||
|
deviceDescriptor->ScsiAddress->PortNumber;
|
|||
|
deviceInfo->TargetId =
|
|||
|
deviceDescriptor->ScsiAddress->TargetId;
|
|||
|
deviceInfo->Lun =
|
|||
|
deviceDescriptor->ScsiAddress->Lun;
|
|||
|
|
|||
|
deviceInfo->CorrespondingPathId =
|
|||
|
(ULONG)deviceDescriptor->PhysicalPath->PhysicalPathId;
|
|||
|
index = (PWCHAR)deviceInfo->VariableData;
|
|||
|
*index++ = unicodeString.Length;
|
|||
|
|
|||
|
RtlCopyMemory(index,
|
|||
|
unicodeString.Buffer,
|
|||
|
unicodeString.Length);
|
|||
|
(ULONG_PTR)deviceInfo +=
|
|||
|
sizeof(PHYSICAL_DISK_INFO) + unicodeString.Length;
|
|||
|
}
|
|||
|
|
|||
|
status = STATUS_SUCCESS;
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
#ifdef USE_BINARY_MOF_QUERY
|
|||
|
case BinaryMofGuid:
|
|||
|
{
|
|||
|
bytesReturned = sizeof(MPathBinaryMofData);
|
|||
|
MPDebugPrint((0,
|
|||
|
"MPathQueryDataBlock: BinaryMofGuid\n"));
|
|||
|
DbgBreakPoint();
|
|||
|
if (BufferAvail < bytesReturned)
|
|||
|
{
|
|||
|
status = STATUS_BUFFER_TOO_SMALL;
|
|||
|
} else {
|
|||
|
RtlCopyMemory(Buffer, MPathBinaryMofData, bytesReturned);
|
|||
|
status = STATUS_SUCCESS;
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
#endif
|
|||
|
default:
|
|||
|
status = STATUS_WMI_GUID_NOT_FOUND;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
|
|||
|
//
|
|||
|
// This is for the DSM
|
|||
|
//
|
|||
|
status = STATUS_WMI_GUID_NOT_FOUND;
|
|||
|
}
|
|||
|
MPDebugPrint((0,
|
|||
|
"MpathQueryDataBlock: Buffer (%x) BytesReturned (%x). Status (%x)\n",
|
|||
|
Buffer,
|
|||
|
bytesReturned,
|
|||
|
status));
|
|||
|
|
|||
|
//
|
|||
|
// Indicate the data length.
|
|||
|
//
|
|||
|
*InstanceLengthArray = bytesReturned;
|
|||
|
|
|||
|
status = WmiCompleteRequest(DeviceObject,
|
|||
|
Irp,
|
|||
|
status,
|
|||
|
bytesReturned,
|
|||
|
IO_NO_INCREMENT);
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathSetWmiDataBlock(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp,
|
|||
|
IN ULONG GuidIndex,
|
|||
|
IN ULONG InstanceIndex,
|
|||
|
IN ULONG BufferSize,
|
|||
|
IN PUCHAR Buffer
|
|||
|
)
|
|||
|
{
|
|||
|
NTSTATUS status;
|
|||
|
ULONG bytesReturned = 0;
|
|||
|
|
|||
|
if (FALSE) { // (GuidIndex <= MPATH_MAX_GUID_INDEX) {
|
|||
|
status = STATUS_WMI_READ_ONLY;
|
|||
|
} else {
|
|||
|
//
|
|||
|
// DSM Guid
|
|||
|
// If there is a GUID match, call the dsm. It will have to call
|
|||
|
// DsmCompleteWMIRequest to finish the request.
|
|||
|
// DONOT complete it here, rather return the status from the DSM routine
|
|||
|
// TODO
|
|||
|
status = STATUS_WMI_GUID_NOT_FOUND;
|
|||
|
}
|
|||
|
status = WmiCompleteRequest(DeviceObject,
|
|||
|
Irp,
|
|||
|
status,
|
|||
|
bytesReturned,
|
|||
|
IO_NO_INCREMENT);
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathSetWmiDataItem(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp,
|
|||
|
IN ULONG GuidIndex,
|
|||
|
IN ULONG InstanceIndex,
|
|||
|
IN ULONG DataItemId,
|
|||
|
IN ULONG BufferSize,
|
|||
|
IN PUCHAR Buffer
|
|||
|
)
|
|||
|
{
|
|||
|
NTSTATUS status;
|
|||
|
ULONG bytesReturned = 0;
|
|||
|
|
|||
|
if (TRUE) { // (GuidIndex <= MPATH_MAX_GUID_INDEX) {
|
|||
|
status = STATUS_WMI_READ_ONLY;
|
|||
|
} else {
|
|||
|
//
|
|||
|
// DSM Guid
|
|||
|
// TODO
|
|||
|
status = STATUS_WMI_GUID_NOT_FOUND;
|
|||
|
}
|
|||
|
status = WmiCompleteRequest(DeviceObject,
|
|||
|
Irp,
|
|||
|
status,
|
|||
|
bytesReturned,
|
|||
|
IO_NO_INCREMENT);
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathExecuteWmiMethod(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp,
|
|||
|
IN ULONG GuidIndex,
|
|||
|
IN ULONG InstanceIndex,
|
|||
|
IN ULONG MethodId,
|
|||
|
IN ULONG InBuffersize,
|
|||
|
IN ULONG OutBufferSize,
|
|||
|
IN OUT PUCHAR Buffer
|
|||
|
)
|
|||
|
{
|
|||
|
NTSTATUS status;
|
|||
|
ULONG bytesReturned = 0;
|
|||
|
|
|||
|
if (TRUE) { //(GuidIndex <= MPATH_MAX_GUID_INDEX ) {
|
|||
|
switch (GuidIndex) {
|
|||
|
//case MPATH_GUID_PATH_MAINTENACE: {
|
|||
|
|
|||
|
|
|||
|
//
|
|||
|
// Need to verify the parameters.
|
|||
|
// TODO: Need data block that lists adapter
|
|||
|
// (path) ids.
|
|||
|
//
|
|||
|
//status = MPathFailOver(id1, id2);
|
|||
|
// status = STATUS_WMI_ITEMID_NOT_FOUND;
|
|||
|
// }
|
|||
|
// break;
|
|||
|
|
|||
|
default:
|
|||
|
status = STATUS_WMI_ITEMID_NOT_FOUND;
|
|||
|
break;
|
|||
|
|
|||
|
}
|
|||
|
} else {
|
|||
|
|
|||
|
status = STATUS_WMI_ITEMID_NOT_FOUND;
|
|||
|
}
|
|||
|
status = WmiCompleteRequest(DeviceObject,
|
|||
|
Irp,
|
|||
|
status,
|
|||
|
bytesReturned,
|
|||
|
IO_NO_INCREMENT);
|
|||
|
return status;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathWmiFunctionControl(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp,
|
|||
|
IN ULONG GuidIndex,
|
|||
|
IN WMIENABLEDISABLECONTROL Function,
|
|||
|
IN BOOLEAN Enable
|
|||
|
)
|
|||
|
{
|
|||
|
PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
|
|||
|
PPSEUDO_DISK_EXTENSION diskExtension = &deviceExtension->PseudoDiskExtension;
|
|||
|
NTSTATUS status;
|
|||
|
|
|||
|
//
|
|||
|
// Handle the enabling/disabling of the datablocks/events
|
|||
|
//
|
|||
|
#if 0
|
|||
|
if (GuidIndex == MPATH_GUID_FAILOVER_INFO) {
|
|||
|
if (Enable) {
|
|||
|
diskExtension->WmiEventsEnabled = TRUE;
|
|||
|
} else {
|
|||
|
|
|||
|
diskExtension->WmiEventsEnabled = FALSE;
|
|||
|
}
|
|||
|
|
|||
|
} else {
|
|||
|
#endif
|
|||
|
status = STATUS_WMI_GUID_NOT_FOUND;
|
|||
|
//}
|
|||
|
status = WmiCompleteRequest(DeviceObject,
|
|||
|
Irp,
|
|||
|
status,
|
|||
|
0,
|
|||
|
IO_NO_INCREMENT);
|
|||
|
return status;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
VOID
|
|||
|
MPathSetupWmi(
|
|||
|
IN PDEVICE_EXTENSION DeviceExtension
|
|||
|
)
|
|||
|
{
|
|||
|
if (DeviceExtension->Type == DEV_MPATH_CONTROL) {
|
|||
|
MPDebugPrint((0,
|
|||
|
"MPathSetWmi: Control Object\n"));
|
|||
|
DbgBreakPoint();
|
|||
|
} else {
|
|||
|
PPSEUDO_DISK_EXTENSION diskExtension = &DeviceExtension->PseudoDiskExtension;
|
|||
|
|
|||
|
diskExtension->WmiInfo.GuidCount = MPathGuidCount;
|
|||
|
diskExtension->WmiInfo.GuidList = MPathWmiGuidList;
|
|||
|
diskExtension->WmiInfo.QueryWmiRegInfo = MPathQueryWmiRegInfo;
|
|||
|
diskExtension->WmiInfo.QueryWmiDataBlock = MPathQueryWmiDataBlock;
|
|||
|
diskExtension->WmiInfo.SetWmiDataBlock = MPathSetWmiDataBlock;
|
|||
|
diskExtension->WmiInfo.SetWmiDataItem = MPathSetWmiDataItem;
|
|||
|
diskExtension->WmiInfo.ExecuteWmiMethod = MPathExecuteWmiMethod;
|
|||
|
diskExtension->WmiInfo.WmiFunctionControl = MPathWmiFunctionControl;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
NTSTATUS
|
|||
|
MPathWmi(
|
|||
|
IN PDEVICE_OBJECT DeviceObject,
|
|||
|
IN PIRP Irp
|
|||
|
)
|
|||
|
{
|
|||
|
PDEVICE_EXTENSION deviceExtension = DeviceObject->DeviceExtension;
|
|||
|
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
|
|||
|
PUCHAR wmiBuffer = irpStack->Parameters.WMI.Buffer;
|
|||
|
ULONG bufferSize = irpStack->Parameters.WMI.BufferSize;
|
|||
|
ULONG savedLength = Irp->IoStatus.Information;
|
|||
|
PPSEUDO_DISK_EXTENSION diskExtension;
|
|||
|
SYSCTL_IRP_DISPOSITION disposition;
|
|||
|
NTSTATUS status;
|
|||
|
|
|||
|
MPDebugPrint((0,
|
|||
|
"MPathWmi: Irp (%x), MJ (%x), MN (%x)",
|
|||
|
Irp,
|
|||
|
irpStack->MajorFunction,
|
|||
|
irpStack->MinorFunction));
|
|||
|
|
|||
|
|
|||
|
diskExtension = &deviceExtension->PseudoDiskExtension;
|
|||
|
|
|||
|
status = WmiSystemControl(&diskExtension->WmiInfo,
|
|||
|
DeviceObject,
|
|||
|
Irp,
|
|||
|
&disposition);
|
|||
|
|
|||
|
MPDebugPrint((0,
|
|||
|
"Disposition is (%x). Status (%x)\n",
|
|||
|
disposition,
|
|||
|
status));
|
|||
|
switch (disposition) {
|
|||
|
case IrpProcessed:
|
|||
|
|
|||
|
//
|
|||
|
// Already processed and the DpWmiXXX routines specified in WmiInfo will
|
|||
|
// complete it.
|
|||
|
//
|
|||
|
break;
|
|||
|
|
|||
|
case IrpNotCompleted:
|
|||
|
|
|||
|
IoCompleteRequest(Irp, IO_NO_INCREMENT);
|
|||
|
break;
|
|||
|
|
|||
|
case IrpNotWmi:
|
|||
|
case IrpForward:
|
|||
|
default:
|
|||
|
//
|
|||
|
// Need to ensure this is the pseudodisk
|
|||
|
//
|
|||
|
if (deviceExtension->Type == DEV_MPATH_CONTROL) {
|
|||
|
|
|||
|
MPDebugPrint((0,
|
|||
|
" for Control\n"));
|
|||
|
//
|
|||
|
// Call the 'control' (FDO) handler.
|
|||
|
//
|
|||
|
return MPathFdoWmi(DeviceObject, Irp);
|
|||
|
} else {
|
|||
|
//
|
|||
|
// This is the PDO, so just complete it without touching anything.
|
|||
|
//
|
|||
|
IoCompleteRequest(Irp, IO_NO_INCREMENT);
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return status;
|
|||
|
|
|||
|
}
|
|||
|
|