NT4/private/ntos/config/cmnotify.c

973 lines
26 KiB
C
Raw Normal View History

2001-01-01 00:00:00 +01:00
/*++
Copyright (c) 1991 Microsoft Corporation
Module Name:
cmnotify.c
Abstract:
This module contains support for NtNotifyChangeKey.
Author:
Bryan M. Willman (bryanwi) 03-Feb-1992
Revision History:
--*/
#include "cmp.h"
//
// "Back Side" of notify
//
extern PCMHIVE CmpMasterHive;
VOID
CmpReportNotifyHelper(
IN PUNICODE_STRING Name,
IN PHHIVE SearchHive,
IN PHHIVE Hive,
IN PCM_KEY_NODE Node,
IN ULONG Filter
);
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE,CmpReportNotify)
#pragma alloc_text(PAGE,CmpReportNotifyHelper)
#pragma alloc_text(PAGE,CmpPostNotify)
#pragma alloc_text(PAGE,CmpPostApc)
#pragma alloc_text(PAGE,CmpPostApcRunDown)
#pragma alloc_text(PAGE,CmNotifyRunDown)
#pragma alloc_text(PAGE,CmpFlushNotify)
#pragma alloc_text(PAGE,CmpNotifyChangeKey)
#endif
VOID
CmpReportNotify(
UNICODE_STRING Name,
PHHIVE Hive,
HCELL_INDEX Cell,
ULONG Filter
)
/*++
Routine Description:
This routine is called when a notifiable event occurs. It will
apply CmpReportNotifyHelper to the hive the event occured in,
and the master hive if different.
Arguments:
Name - canonical path name (as in a key control block) of the key
at which the event occured. For create or delete this is
the created or deleted key.
WARNING: Name's length field may be edited, though the
buffer will not be.
Hive - pointer to hive containing cell of Key at which event occured.
Cell - cell of Key at which event occured
(hive and cell correspond with name.)
Filter - event to be reported
Return Value:
NONE.
--*/
{
PCM_KEY_NODE pcell;
ULONG flags;
ULONG i;
PAGED_CODE();
CMLOG(CML_WORKER, CMS_NOTIFY) {
KdPrint(("CmpReportNotify:\n"));
KdPrint(("\tName = %wZ\n", &Name));
KdPrint(("\tHive:%08lx Cell:%08lx Filter:%08lx\n", Hive, Cell, Filter));
}
pcell = (PCM_KEY_NODE)HvGetCell(Hive, Cell);
//
// If the operation was create or delete, treat it as a change
// to the parent.
//
if (Filter == REG_NOTIFY_CHANGE_NAME) {
flags = pcell->Flags;
Cell = pcell->Parent;
if (flags & KEY_HIVE_ENTRY) {
Hive = &(CmpMasterHive->Hive);
pcell = (PCM_KEY_NODE)HvGetCell(Hive, Cell);
}
for ( i = (Name.Length/sizeof(WCHAR))-1;
Name.Buffer[i] != OBJ_NAME_PATH_SEPARATOR;
i--
)
{}
ASSERT(i > 1);
Name.Length = (USHORT)(i * sizeof(WCHAR));
//
// if we're at an exit/link node, back up the real node
// that MUST be it's parent.
//
if (pcell->Flags & KEY_HIVE_EXIT) {
Cell = pcell->Parent;
}
pcell = (PCM_KEY_NODE)HvGetCell(Hive, Cell);
}
//
// Report to notifies waiting on the event's hive
//
CmpReportNotifyHelper(&Name, Hive, Hive, pcell, Filter);
//
// If containging hive is not the master hive, apply to master hive
//
if (Hive != &(CmpMasterHive->Hive)) {
CmpReportNotifyHelper(&Name,
&(CmpMasterHive->Hive),
Hive,
pcell,
Filter);
}
return;
}
VOID
CmpReportNotifyHelper(
IN PUNICODE_STRING Name,
IN PHHIVE SearchHive,
IN PHHIVE Hive,
IN PCM_KEY_NODE Node,
IN ULONG Filter
)
/*++
Routine Description:
Scan the list of active notifies for the specified hive. For
any with scope including KeyControlBlock and filter matching
Filter, and with proper security access, post the notify.
Arguments:
Name - canonical path name (as in a key control block) of the key
at which the event occured. (This is the name for
reporting purposes.)
SearchHive - hive to search for matches (which notify list to check)
Hive - Supplies hive containing node to match with.
Node - pointer to key to match with (and check access to)
Filter - type of event
Return Value:
NONE.
--*/
{
PLIST_ENTRY NotifyPtr;
PCM_NOTIFY_BLOCK NotifyBlock;
PCMHIVE CmSearchHive;
PUNICODE_STRING NotifyName;
PAGED_CODE();
CmSearchHive = CONTAINING_RECORD(SearchHive, CMHIVE, Hive);
NotifyPtr = &(CmSearchHive->NotifyList);
while (NotifyPtr->Flink != NULL) {
NotifyPtr = NotifyPtr->Flink;
NotifyBlock = CONTAINING_RECORD(NotifyPtr, CM_NOTIFY_BLOCK, HiveList);
NotifyName = &(NotifyBlock->KeyControlBlock->FullName);
if (NotifyName->Length > Name->Length) {
//
// list is length sorted, we're past all shorter entries
//
break;
}
if ( (
(NotifyName->Length == Name->Length) ||
(Name->Buffer[NotifyName->Length/sizeof(WCHAR)] ==
OBJ_NAME_PATH_SEPARATOR)
)
&&
( RtlPrefixString((PSTRING)NotifyName, (PSTRING)Name, TRUE) )
&&
( NotifyBlock->Filter & Filter )
&&
(
(NotifyBlock->WatchTree == TRUE) ||
(
Node == NotifyBlock->KeyControlBlock->KeyNode
)
)
)
{
//
// Name lengths match, or notifyname is proper length for
// component point prefix (proper prefix) match of name
// AND
// Prefix characters actually match
// AND
// Filter matches, this event is relevent to this notify
// AND
// Either the notify spans the whole subtree, or the cell
// (key) of interest is the one it applies to
//
// THEREFORE: The notify is relevent.
//
//
// Correct scope, does caller have access?
//
if (CmpCheckNotifyAccess(NotifyBlock,Hive,Node)) {
//
// Notify block has KEY_NOTIFY access to the node
// the event occured at. It is relevent. Therefore,
// it gets to see this event. Post and be done.
//
CmpPostNotify(
NotifyBlock,
Name,
Filter,
STATUS_NOTIFY_ENUM_DIR
);
} // else no KEY_NOTIFY access to node event occured at
} // else not relevent (wrong scope, filter, etc)
}
return;
}
VOID
CmpPostNotify(
PCM_NOTIFY_BLOCK NotifyBlock,
PUNICODE_STRING Name OPTIONAL,
ULONG Filter,
NTSTATUS Status
)
/*++
Routine Description:
Actually report the notify event by signalling events, enqueing
APCs, and so forth.
Arguments:
NotifyBlock - pointer to structure that describes the notify
operation. (Where to post to)
Name - name of key at which event occurred.
Filter - nature of event
Status - completion status to report
Return Value:
NONE.
--*/
{
PCM_POST_BLOCK PostBlock;
Filter;
Name;
PAGED_CODE();
CMLOG(CML_MAJOR, CMS_NOTIFY) {
KdPrint(("CmpPostNotify:\n"));
KdPrint(("\tNotifyBlock:%08lx ", NotifyBlock));
KdPrint(("\tName = %wZ\n", Name));
KdPrint(("\tFilter:%08lx Status=%08lx\n", Filter, Status));
}
ASSERT_CM_LOCK_OWNED();
if (IsListEmpty(&(NotifyBlock->PostList)) == TRUE) {
//
// Nothing to post, set a mark and return
//
NotifyBlock->NotifyPending = TRUE;
return;
}
NotifyBlock->NotifyPending = FALSE;
//
// IMPLEMENTATION NOTE:
// If we ever want to actually implement the code that returns
// names of things that changed, this is the place to add the
// name and operation type to the buffer.
//
//
// Pull and post all the entries in the post list
//
while (IsListEmpty(&(NotifyBlock->PostList)) == FALSE) {
//
// Remove from the notify block list, and enqueue the apc.
// The apc will remove itself from the thread list
//
PostBlock = (PCM_POST_BLOCK)RemoveHeadList(&(NotifyBlock->PostList));
PostBlock = CONTAINING_RECORD(PostBlock,
CM_POST_BLOCK,
NotifyList);
switch (PostBlock->NotifyType) {
case PostSynchronous:
//
// This is a SYNC notify call. There will be no user event,
// and no user apc routine. Quick exit here, just fill in
// the Status and poke the event.
//
// Holder of the systemevent will wake up and free the
// postblock. If we free it here, we get a race & bugcheck.
//
PostBlock->u.Sync.Status = Status;
KeSetEvent(PostBlock->u.Sync.SystemEvent,
0,
FALSE);
break;
case PostAsyncUser:
//
// Insert the APC into the queue
//
KeInsertQueueApc(PostBlock->u.AsyncUser.Apc,
(PVOID)Status,
(PVOID)PostBlock,
0);
break;
case PostAsyncKernel:
//
// Queue the work item, then free the post block.
//
if (PostBlock->u.AsyncKernel.WorkItem != NULL) {
ExQueueWorkItem(PostBlock->u.AsyncKernel.WorkItem,
PostBlock->u.AsyncKernel.QueueType);
}
//
// Signal Event if present, and deref it.
//
if (PostBlock->u.AsyncKernel.Event != NULL) {
KeSetEvent(PostBlock->u.AsyncKernel.Event,
0,
FALSE);
ObDereferenceObject(PostBlock->u.AsyncKernel.Event);
}
//
// remove the post block from the thread list, and free it
//
RemoveEntryList(&(PostBlock->ThreadList));
CmpFreePostBlock(PostBlock);
break;
}
}
return;
}
VOID
CmpPostApc(
struct _KAPC *Apc,
PKNORMAL_ROUTINE *NormalRoutine,
PVOID *NormalContext,
PVOID *SystemArgument1,
PVOID *SystemArgument2
)
/*++
Routine Description:
This is the kernel apc routine. It is called for all notifies,
regardless of what form of notification the caller requested.
We compute the postblock address from the apc object address.
IoStatus is set. SystemEvent and UserEvent will be signalled
as appropriate. If the user requested an APC, then NormalRoutine
will be set at entry and executed when we exit. The PostBlock
is freed here.
Arguments:
Apc - pointer to apc object
NormalRoutine - Will be called when we return
NormalContext - will be 1st argument to normal routine, ApcContext
passed in when NtNotifyChangeKey was called
SystemArgument1 - IN: Status value for IoStatusBlock
OUT: Ptr to IoStatusBlock (2nd arg to user apc routine)
SystemArgument2 - Pointer to the PostBlock
Return Value:
NONE.
--*/
{
PCM_POST_BLOCK PostBlock;
PAGED_CODE();
CMLOG(CML_MAJOR, CMS_NOTIFY) {
KdPrint(("CmpPostApc:\n"));
KdPrint(("\tApc:%08lx ", Apc));
KdPrint(("NormalRoutine:%08lx\n", NormalRoutine));
KdPrint(("\tNormalContext:%08lx", NormalContext));
KdPrint(("\tSystemArgument1=IoStatusBlock:%08lx\n", SystemArgument1));
}
ASSERT(KeGetCurrentIrql() >= APC_LEVEL);
PostBlock = *(PCM_POST_BLOCK *)SystemArgument2;
//
// Fill in IO Status Block
//
// IMPLEMENTATION NOTE:
// If we ever want to actually implement the code that returns
// names of things that changed, this is the place to copy the
// buffer into the caller's buffer.
//
try {
PostBlock->u.AsyncUser.IoStatusBlock->Status = *((ULONG *)SystemArgument1);
PostBlock->u.AsyncUser.IoStatusBlock->Information = 0L;
} except (EXCEPTION_EXECUTE_HANDLER) {
NOTHING;
}
*SystemArgument1 = PostBlock->u.AsyncUser.IoStatusBlock;
//
// This is an Async notify, do all work here, including
// cleaning up the post block
//
//
// Signal UserEvent if present, and deref it.
//
if (PostBlock->u.AsyncUser.UserEvent != NULL) {
KeSetEvent(PostBlock->u.AsyncUser.UserEvent,
0,
FALSE);
ObDereferenceObject(PostBlock->u.AsyncUser.UserEvent);
}
//
// remove the post block from the thread list, and free it
//
RemoveEntryList(&(PostBlock->ThreadList));
CmpFreePostBlock(PostBlock);
return;
}
VOID
CmpPostApcRunDown(
struct _KAPC *Apc
)
/*++
Routine Description:
This routine is called to clear away apcs in the apc queue
of a thread that has been terminated.
Since the apc is in the apc queue, we know that it is NOT in
any NotifyBlock's post list. It is, however, in the threads's
PostBlockList.
Therefore, poke any user events so that waiters are not stuck,
drop the references so the event can be cleaned up, delist the
PostBlock and free it.
Since we are cleaning up the thread, SystemEvents are not interesting.
Arguments:
Apc - pointer to apc object
Return Value:
NONE.
--*/
{
PCM_POST_BLOCK PostBlock;
KIRQL OldIrql;
PAGED_CODE();
CMLOG(CML_MAJOR, CMS_NOTIFY) {
KdPrint(("CmpApcRunDown:\n"));
KdPrint(("\tApc:%08lx ", Apc));
}
KeRaiseIrql(APC_LEVEL, &OldIrql);
PostBlock = (PCM_POST_BLOCK)Apc->SystemArgument2;
//
// report status and wake up any threads that might otherwise
// be stuck. also drop any event references we hold
//
try {
PostBlock->u.AsyncUser.IoStatusBlock->Status = STATUS_NOTIFY_CLEANUP;
PostBlock->u.AsyncUser.IoStatusBlock->Information = 0L;
} except (EXCEPTION_EXECUTE_HANDLER) {
NOTHING;
}
if (PostBlock->u.AsyncUser.UserEvent != NULL) {
KeSetEvent(
PostBlock->u.AsyncUser.UserEvent,
0,
FALSE
);
ObDereferenceObject(PostBlock->u.AsyncUser.UserEvent);
}
//
// delist the post block
//
RemoveEntryList(&(PostBlock->ThreadList));
//
// Free the post block. Use Ex call because PostBlocks are NOT
// part of the global registry pool computation, but are instead
// part of NonPagedPool with Quota.
//
CmpFreePostBlock(PostBlock);
KeLowerIrql(OldIrql);
return;
}
//
// Cleanup procedure
//
VOID
CmNotifyRunDown(
PETHREAD Thread
)
/*++
Routine Description:
This routine is called from PspExitThread to clean up any pending
notify requests.
It will traverse the thread's PostBlockList, for each PostBlock it
finds, it will:
1. Remove it from the relevent NotifyBlock. This requires
that we hold the Registry mutex.
2. Remove it from the thread's PostBlockList. This requires
that we run at APC level.
3. By the time this procedure runs, user apcs are not interesting
and neither are SystemEvents, so do not bother processing
them.
UserEvents and IoStatusBlocks could be refered to by other
threads in the same process, or even a different process,
so process them so those threads know what happened, use
status code of STATUS_NOTIFY_CLEANUP.
4. Free the post block.
Arguments:
Thread - pointer to the executive thread object for the thread
we wish to do rundown on.
Return Value:
NONE.
--*/
{
PCM_POST_BLOCK PostBlock;
KIRQL OldIrql;
PAGED_CODE();
if ( IsListEmpty(&(Thread->PostBlockList)) == TRUE ) {
return;
}
CMLOG(CML_API, CMS_NTAPI) {
KdPrint(("CmNotifyRunDown: ethread:%08lx\n", Thread));
}
CmpLockRegistryExclusive();
KeRaiseIrql(APC_LEVEL, &OldIrql);
while (IsListEmpty(&(Thread->PostBlockList)) == FALSE) {
//
// remove from thread list
//
PostBlock = (PCM_POST_BLOCK)RemoveHeadList(&(Thread->PostBlockList));
PostBlock = CONTAINING_RECORD(
PostBlock,
CM_POST_BLOCK,
ThreadList
);
//
// at this point, CmpReportNotify and friends will no longer
// attempt to post this post block.
//
if (PostBlock->NotifyType == PostAsyncUser) {
//
// report status and wake up any threads that might otherwise
// be stuck. also drop any event references we hold
//
try {
PostBlock->u.AsyncUser.IoStatusBlock->Status = STATUS_NOTIFY_CLEANUP;
PostBlock->u.AsyncUser.IoStatusBlock->Information = 0L;
} except (EXCEPTION_EXECUTE_HANDLER) {
CMLOG(CML_API, CMS_EXCEPTION) {
KdPrint(("!!CmNotifyRundown: code:%08lx\n", GetExceptionCode()));
}
NOTHING;
}
if (PostBlock->u.AsyncUser.UserEvent != NULL) {
KeSetEvent(
PostBlock->u.AsyncUser.UserEvent,
0,
FALSE
);
ObDereferenceObject(PostBlock->u.AsyncUser.UserEvent);
}
//
// Cancel the APC. Otherwise the rundown routine will also
// free the post block if the APC happens to be queued at
// this point. If the APC is queued, then the post block has
// already been removed from the notify list, so don't remove
// it again.
//
if (!KeRemoveQueueApc(PostBlock->u.AsyncUser.Apc)) {
//
// remove from notify block's list
//
RemoveEntryList(&(PostBlock->NotifyList));
}
} else {
//
// remove from notify block's list
//
RemoveEntryList(&(PostBlock->NotifyList));
}
//
// Free the post block. Use Ex call because PostBlocks are NOT
// part of the global registry pool computation, but are instead
// part of NonPagedPool with Quota.
//
CmpFreePostBlock(PostBlock);
}
KeLowerIrql(OldIrql);
CmpUnlockRegistry();
return;
}
VOID
CmpFlushNotify(
PCM_KEY_BODY KeyBody
)
/*++
Routine Description:
Clean up notifyblock when a handle is closed or the key it refers
to is deleted.
Arguments:
KeyBody - supplies pointer to key object body for handle we
are cleaning up.
Return Value:
NONE
--*/
{
PCM_NOTIFY_BLOCK NotifyBlock;
PHHIVE Hive;
PAGED_CODE();
ASSERT_CM_LOCK_OWNED();
NotifyBlock = KeyBody->NotifyBlock;
if (NotifyBlock == NULL) {
return;
}
//
// Clean up all PostBlocks waiting on the NotifyBlock
//
if (IsListEmpty(&(NotifyBlock->PostList)) == FALSE) {
CmpPostNotify(
NotifyBlock,
NULL,
0,
STATUS_NOTIFY_CLEANUP
);
}
//
// Release the subject context
//
SeReleaseSubjectContext(&NotifyBlock->SubjectContext);
//
// IMPLEMENTATION NOTE:
// If we ever do code to report names and types of events,
// this is the place to free the buffer.
//
//
// Remove the NotifyBlock from the hive chain
//
NotifyBlock->HiveList.Blink->Flink = NotifyBlock->HiveList.Flink;
if (NotifyBlock->HiveList.Flink != NULL) {
NotifyBlock->HiveList.Flink->Blink = NotifyBlock->HiveList.Blink;
}
//
// decrement the notify count
//
Hive = KeyBody->KeyControlBlock->KeyHive;
//
// Free the block, clean up the KeyBody
//
CmpFree(NotifyBlock,sizeof(CM_NOTIFY_BLOCK));
KeyBody->NotifyBlock = NULL;
return;
}
//
// "Front Side" of notify. See also Ntapi.c: ntnotifychangekey
//
NTSTATUS
CmpNotifyChangeKey(
IN PCM_KEY_BODY KeyBody,
IN PCM_POST_BLOCK PostBlock,
IN ULONG CompletionFilter,
IN BOOLEAN WatchTree,
IN PVOID Buffer,
IN ULONG BufferSize
)
/*++
Routine Description:
This routine sets up the NotifyBlock, and attaches the PostBlock
to it. When it returns, the Notify is visible to the system,
and will receive event reports.
If there is already an event report pending, then the notify
call will be satisified at once.
Arguments:
KeyBody - pointer to key object that handle refers to, allows access
to key control block, notify block, etc.
PostBlock - pointer to structure that describes how/where the caller
is to be notified.
WARNING: PostBlock must come from Pool, THIS routine
will keep it, back side will free it. This
routine WILL free it in case of error.
CompletionFilter - what types of events the caller wants to see
WatchTree - TRUE to watch whole subtree, FALSE to watch only immediate
key the notify is applied to
Buffer - pointer to area to recieve notify data
BufferSize - size of buffer, also size user would like to allocate
for internal buffer
Return Value:
Status.
--*/
{
PCM_NOTIFY_BLOCK NotifyBlock;
PCM_NOTIFY_BLOCK node;
PLIST_ENTRY ptr;
PCMHIVE Hive;
KIRQL OldIrql;
PAGED_CODE();
CMLOG(CML_WORKER, CMS_NOTIFY) {
KdPrint(("CmpNotifyChangeKey:\n"));
KdPrint(("\tKeyBody:%08lx PostBlock:%08lx ", KeyBody, PostBlock));
KdPrint(("Filter:%08lx WatchTree:%08lx\n", CompletionFilter, WatchTree));
}
CmpLockRegistryExclusive();
if (KeyBody->KeyControlBlock->Delete) {
CmpFreePostBlock(PostBlock);
CmpUnlockRegistry();
return STATUS_KEY_DELETED;
}
Hive = (PCMHIVE)KeyBody->KeyControlBlock->KeyHive;
Hive = CONTAINING_RECORD(Hive, CMHIVE, Hive);
NotifyBlock = KeyBody->NotifyBlock;
if (NotifyBlock == NULL) {
//
// Set up new notify session
//
NotifyBlock = CmpAllocateTag(sizeof(CM_NOTIFY_BLOCK),FALSE,CM_NOTIFYBLOCK_TAG);
CMLOG(CML_MINOR, CMS_POOL) {
KdPrint(("**CmpNotifyChangeKey: allocate:%08lx, ", sizeof(CM_NOTIFY_BLOCK)));
KdPrint(("type:%d, at:%08lx\n", PagedPool, NotifyBlock));
}
if (NotifyBlock == NULL) {
CmpFreePostBlock(PostBlock);
CmpUnlockRegistry();
return STATUS_INSUFFICIENT_RESOURCES;
}
NotifyBlock->KeyControlBlock = KeyBody->KeyControlBlock;
NotifyBlock->Filter = CompletionFilter;
NotifyBlock->WatchTree = WatchTree;
NotifyBlock->NotifyPending = FALSE;
InitializeListHead(&(NotifyBlock->PostList));
KeyBody->NotifyBlock = NotifyBlock;
NotifyBlock->KeyBody = KeyBody;
//
// IMPLEMENTATION NOTE:
// If we ever want to actually return the buffers full of
// data, the buffer should be allocated and its address
// stored in the notify block here.
//
//
// Capture the subject context so we can do checking once the
// notify goes off.
//
SeCaptureSubjectContext(&NotifyBlock->SubjectContext);
//
// Attach notify block to hive in properly sorted order
//
ptr = &(Hive->NotifyList);
while (TRUE) {
if (ptr->Flink == NULL) {
//
// End of list, add self after ptr.
//
ptr->Flink = &(NotifyBlock->HiveList);
NotifyBlock->HiveList.Flink = NULL;
NotifyBlock->HiveList.Blink = ptr;
break;
}
ptr = ptr->Flink;
node = CONTAINING_RECORD(ptr, CM_NOTIFY_BLOCK, HiveList);
if (node->KeyControlBlock->FullName.Length >
KeyBody->KeyControlBlock->FullName.Length)
{
//
// ptr -> notify with longer name than us, insert in FRONT
//
NotifyBlock->HiveList.Flink = ptr;
ptr->Blink->Flink = &(NotifyBlock->HiveList);
NotifyBlock->HiveList.Blink = ptr->Blink;
ptr->Blink = &(NotifyBlock->HiveList);
break;
}
}
}
//
// Add post block to front of notify block's list, and add it to thread list.
//
InsertHeadList(
&(NotifyBlock->PostList),
&(PostBlock->NotifyList)
);
KeRaiseIrql(APC_LEVEL, &OldIrql);
InsertHeadList(
&(PsGetCurrentThread()->PostBlockList),
&(PostBlock->ThreadList)
);
KeLowerIrql(OldIrql);
//
// If there is a notify pending (will not be if we just created
// the notify block) then post it at once. Note that this call
// ALWAYS returns STATUS_PENDING unless it fails. Caller must
// ALWAYS look in IoStatusBlock to see what happened.
//
if (NotifyBlock->NotifyPending == TRUE) {
CmpPostNotify(
NotifyBlock,
NULL,
0,
STATUS_NOTIFY_ENUM_DIR
);
}
CmpUnlockRegistry();
return STATUS_PENDING;
}