[SHELL32_APITEST] Follow-up to #6796 (25e2f5f)
[reactos.git] / win32ss / user / winsrv / consrv / handle.c
1 /*
2 * LICENSE: GPL - See COPYING in the top level directory
3 * PROJECT: ReactOS Console Server DLL
4 * FILE: win32ss/user/winsrv/consrv/handle.c
5 * PURPOSE: Console I/O Handles functions
6 * PROGRAMMERS: David Welch
7 * Jeffrey Morlan
8 * Hermes Belusca-Maito (hermes.belusca@sfr.fr)
9 */
10
11 /* INCLUDES *******************************************************************/
12
13 #include "consrv.h"
14
15 #include <win/console.h>
16
17 #define NDEBUG
18 #include <debug.h>
19
20 /* GLOBALS ********************************************************************/
21
22 /* Console handle */
23 typedef struct _CONSOLE_IO_HANDLE
24 {
25 PCONSOLE_IO_OBJECT Object; /* The object on which the handle points to */
26 ULONG Access;
27 ULONG ShareMode;
28 BOOLEAN Inheritable;
29 } CONSOLE_IO_HANDLE, *PCONSOLE_IO_HANDLE;
30
31
32 /* PRIVATE FUNCTIONS **********************************************************/
33
34 static LONG
35 AdjustHandleCounts(IN PCONSOLE_IO_HANDLE Handle,
36 IN LONG Change)
37 {
38 PCONSOLE_IO_OBJECT Object = Handle->Object;
39
40 DPRINT("AdjustHandleCounts(0x%p, %d), Object = 0x%p\n",
41 Handle, Change, Object);
42 DPRINT("\tAdjustHandleCounts(0x%p, %d), Object = 0x%p, Object->ReferenceCount = %d, Object->Type = %lu\n",
43 Handle, Change, Object, Object->ReferenceCount, Object->Type);
44
45 if (Handle->Access & GENERIC_READ) Object->AccessRead += Change;
46 if (Handle->Access & GENERIC_WRITE) Object->AccessWrite += Change;
47 if (!(Handle->ShareMode & FILE_SHARE_READ)) Object->ExclusiveRead += Change;
48 if (!(Handle->ShareMode & FILE_SHARE_WRITE)) Object->ExclusiveWrite += Change;
49
50 Object->ReferenceCount += Change;
51
52 return Object->ReferenceCount;
53 }
54
55 static VOID
56 ConSrvCloseHandle(IN PCONSOLE_IO_HANDLE Handle)
57 {
58 PCONSOLE_IO_OBJECT Object = Handle->Object;
59 if (Object != NULL)
60 {
61 /*
62 * If this is a input handle, notify and dereference
63 * all the waits related to this handle.
64 */
65 if (Object->Type == INPUT_BUFFER)
66 {
67 // PCONSOLE_INPUT_BUFFER InputBuffer = (PCONSOLE_INPUT_BUFFER)Object;
68 PCONSRV_CONSOLE Console = (PCONSRV_CONSOLE)Object->Console;
69
70 /*
71 * Wake up all the writing waiters related to this handle for this
72 * input buffer, if any, then dereference them and purge them all
73 * from the list.
74 * To select them amongst all the waiters for this input buffer,
75 * pass the handle pointer to the waiters, then they will check
76 * whether or not they are related to this handle and if so, they
77 * return.
78 */
79 CsrNotifyWait(&Console->ReadWaitQueue,
80 TRUE,
81 NULL,
82 (PVOID)Handle);
83 if (!IsListEmpty(&Console->ReadWaitQueue))
84 {
85 CsrDereferenceWait(&Console->ReadWaitQueue);
86 }
87 }
88
89 /* If the last handle to a screen buffer is closed, delete it... */
90 if (AdjustHandleCounts(Handle, -1) == 0)
91 {
92 if (Object->Type == TEXTMODE_BUFFER || Object->Type == GRAPHICS_BUFFER)
93 {
94 PCONSOLE_SCREEN_BUFFER Buffer = (PCONSOLE_SCREEN_BUFFER)Object;
95 /* ...unless it's the only buffer left. Windows allows deletion
96 * even of the last buffer, but having to deal with a lack of
97 * any active buffer might be error-prone. */
98 if (Buffer->ListEntry.Flink != Buffer->ListEntry.Blink)
99 ConDrvDeleteScreenBuffer(Buffer);
100 }
101 else if (Object->Type == INPUT_BUFFER)
102 {
103 DPRINT("Closing the input buffer\n");
104 }
105 else
106 {
107 DPRINT1("Invalid object type %d\n", Object->Type);
108 }
109 }
110
111 /* Invalidate (zero-out) this handle entry */
112 // Handle->Object = NULL;
113 // RtlZeroMemory(Handle, sizeof(*Handle));
114 }
115 RtlZeroMemory(Handle, sizeof(*Handle)); // Be sure the whole entry is invalidated.
116 }
117
118
119 NTSTATUS
120 ConSrvInheritHandlesTable(IN PCONSOLE_PROCESS_DATA SourceProcessData,
121 IN PCONSOLE_PROCESS_DATA TargetProcessData)
122 {
123 NTSTATUS Status = STATUS_SUCCESS;
124 ULONG i, j;
125
126 RtlEnterCriticalSection(&SourceProcessData->HandleTableLock);
127
128 /* Inherit a handles table only if there is no already */
129 if (TargetProcessData->HandleTable != NULL /* || TargetProcessData->HandleTableSize != 0 */)
130 {
131 Status = STATUS_UNSUCCESSFUL;
132 goto Quit;
133 }
134
135 /* Allocate a new handle table for the child process */
136 TargetProcessData->HandleTable = ConsoleAllocHeap(HEAP_ZERO_MEMORY,
137 SourceProcessData->HandleTableSize
138 * sizeof(CONSOLE_IO_HANDLE));
139 if (TargetProcessData->HandleTable == NULL)
140 {
141 Status = STATUS_NO_MEMORY;
142 goto Quit;
143 }
144
145 TargetProcessData->HandleTableSize = SourceProcessData->HandleTableSize;
146
147 /*
148 * Parse the parent process' handles table and, for each handle,
149 * do a copy of it and reference it, if the handle is inheritable.
150 */
151 for (i = 0, j = 0; i < SourceProcessData->HandleTableSize; i++)
152 {
153 if (SourceProcessData->HandleTable[i].Object != NULL &&
154 SourceProcessData->HandleTable[i].Inheritable)
155 {
156 /*
157 * Copy the handle data and increment the reference count of the
158 * pointed object (via the call to ConSrvCreateHandleEntry == AdjustHandleCounts).
159 */
160 TargetProcessData->HandleTable[j] = SourceProcessData->HandleTable[i];
161 AdjustHandleCounts(&TargetProcessData->HandleTable[j], +1);
162 ++j;
163 }
164 }
165
166 Quit:
167 RtlLeaveCriticalSection(&SourceProcessData->HandleTableLock);
168 return Status;
169 }
170
171 VOID
172 ConSrvFreeHandlesTable(IN PCONSOLE_PROCESS_DATA ProcessData)
173 {
174 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
175
176 if (ProcessData->HandleTable != NULL)
177 {
178 ULONG i;
179
180 /*
181 * ProcessData->ConsoleHandle is NULL (and the assertion fails) when
182 * ConSrvFreeHandlesTable is called in ConSrvConnect during the
183 * allocation of a new console.
184 */
185 // ASSERT(ProcessData->ConsoleHandle);
186 if (ProcessData->ConsoleHandle != NULL)
187 {
188 /* Close all the console handles */
189 for (i = 0; i < ProcessData->HandleTableSize; i++)
190 {
191 ConSrvCloseHandle(&ProcessData->HandleTable[i]);
192 }
193 }
194 /* Free the handles table memory */
195 ConsoleFreeHeap(ProcessData->HandleTable);
196 ProcessData->HandleTable = NULL;
197 }
198
199 ProcessData->HandleTableSize = 0;
200
201 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
202 }
203
204
205
206
207
208
209 // ConSrvCreateObject
210 VOID
211 ConSrvInitObject(IN OUT PCONSOLE_IO_OBJECT Object,
212 IN CONSOLE_IO_OBJECT_TYPE Type,
213 IN PCONSOLE Console)
214 {
215 ASSERT(Object);
216 // if (!Object) return;
217
218 Object->Type = Type;
219 Object->Console = Console;
220 Object->ReferenceCount = 0;
221
222 Object->AccessRead = Object->AccessWrite = 0;
223 Object->ExclusiveRead = Object->ExclusiveWrite = 0;
224 }
225
226 NTSTATUS
227 ConSrvInsertObject(IN PCONSOLE_PROCESS_DATA ProcessData,
228 OUT PHANDLE Handle,
229 IN PCONSOLE_IO_OBJECT Object,
230 IN ULONG Access,
231 IN BOOLEAN Inheritable,
232 IN ULONG ShareMode)
233 {
234 #define IO_HANDLES_INCREMENT 2 * 3
235
236 ULONG i = 0;
237 PCONSOLE_IO_HANDLE Block;
238
239 // NOTE: Commented out because calling code always lock HandleTableLock before.
240 // RtlEnterCriticalSection(&ProcessData->HandleTableLock);
241
242 ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
243 (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
244
245 if (ProcessData->HandleTable)
246 {
247 for (i = 0; i < ProcessData->HandleTableSize; i++)
248 {
249 if (ProcessData->HandleTable[i].Object == NULL)
250 break;
251 }
252 }
253
254 if (i >= ProcessData->HandleTableSize)
255 {
256 /* Allocate a new handles table */
257 Block = ConsoleAllocHeap(HEAP_ZERO_MEMORY,
258 (ProcessData->HandleTableSize +
259 IO_HANDLES_INCREMENT) * sizeof(CONSOLE_IO_HANDLE));
260 if (Block == NULL)
261 {
262 // RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
263 return STATUS_UNSUCCESSFUL;
264 }
265
266 /* If we previously had a handles table, free it and use the new one */
267 if (ProcessData->HandleTable)
268 {
269 /* Copy the handles from the old table to the new one */
270 RtlCopyMemory(Block,
271 ProcessData->HandleTable,
272 ProcessData->HandleTableSize * sizeof(CONSOLE_IO_HANDLE));
273 ConsoleFreeHeap(ProcessData->HandleTable);
274 }
275 ProcessData->HandleTable = Block;
276 ProcessData->HandleTableSize += IO_HANDLES_INCREMENT;
277 }
278
279 ProcessData->HandleTable[i].Object = Object;
280 ProcessData->HandleTable[i].Access = Access;
281 ProcessData->HandleTable[i].Inheritable = Inheritable;
282 ProcessData->HandleTable[i].ShareMode = ShareMode;
283 AdjustHandleCounts(&ProcessData->HandleTable[i], +1);
284 *Handle = ULongToHandle((i << 2) | 0x3);
285
286 // RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
287
288 return STATUS_SUCCESS;
289 }
290
291 NTSTATUS
292 ConSrvRemoveObject(IN PCONSOLE_PROCESS_DATA ProcessData,
293 IN HANDLE Handle)
294 {
295 ULONG Index = HandleToULong(Handle) >> 2;
296
297 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
298
299 ASSERT(ProcessData->HandleTable);
300 // ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
301 // (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
302
303 if (Index >= ProcessData->HandleTableSize ||
304 ProcessData->HandleTable[Index].Object == NULL)
305 {
306 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
307 return STATUS_INVALID_HANDLE;
308 }
309
310 ASSERT(ProcessData->ConsoleHandle);
311 ConSrvCloseHandle(&ProcessData->HandleTable[Index]);
312
313 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
314 return STATUS_SUCCESS;
315 }
316
317 NTSTATUS
318 ConSrvGetObject(IN PCONSOLE_PROCESS_DATA ProcessData,
319 IN HANDLE Handle,
320 OUT PCONSOLE_IO_OBJECT* Object,
321 OUT PVOID* Entry OPTIONAL,
322 IN ULONG Access,
323 IN BOOLEAN LockConsole,
324 IN CONSOLE_IO_OBJECT_TYPE Type)
325 {
326 // NTSTATUS Status;
327 ULONG Index = HandleToULong(Handle) >> 2;
328 PCONSOLE_IO_HANDLE HandleEntry = NULL;
329 PCONSOLE_IO_OBJECT ObjectEntry = NULL;
330 // PCONSRV_CONSOLE ObjectConsole;
331
332 ASSERT(Object);
333 if (Entry) *Entry = NULL;
334
335 DPRINT("ConSrvGetObject -- Object: 0x%x, Handle: 0x%x\n", Object, Handle);
336
337 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
338
339 if ( IsConsoleHandle(Handle) &&
340 Index < ProcessData->HandleTableSize )
341 {
342 HandleEntry = &ProcessData->HandleTable[Index];
343 ObjectEntry = HandleEntry->Object;
344 }
345
346 if ( HandleEntry == NULL ||
347 ObjectEntry == NULL ||
348 (HandleEntry->Access & Access) == 0 ||
349 /*(Type != 0 && ObjectEntry->Type != Type)*/
350 (Type != 0 && (ObjectEntry->Type & Type) == 0) )
351 {
352 DPRINT("ConSrvGetObject -- Invalid handle 0x%x of type %lu with access %lu ; retrieved object 0x%x (handle 0x%x) of type %lu with access %lu\n",
353 Handle, Type, Access, ObjectEntry, HandleEntry, (ObjectEntry ? ObjectEntry->Type : 0), (HandleEntry ? HandleEntry->Access : 0));
354
355 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
356 return STATUS_INVALID_HANDLE;
357 }
358
359 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
360
361 // Status = ConSrvGetConsole(ProcessData, &ObjectConsole, LockConsole);
362 // if (NT_SUCCESS(Status))
363 if (ConDrvValidateConsoleUnsafe(ObjectEntry->Console, CONSOLE_RUNNING, LockConsole))
364 {
365 _InterlockedIncrement(&ObjectEntry->Console->ReferenceCount);
366
367 /* Return the objects to the caller */
368 *Object = ObjectEntry;
369 if (Entry) *Entry = HandleEntry;
370
371 // RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
372 return STATUS_SUCCESS;
373 }
374 else
375 {
376 // RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
377 return STATUS_INVALID_HANDLE;
378 }
379 }
380
381 VOID
382 ConSrvReleaseObject(IN PCONSOLE_IO_OBJECT Object,
383 IN BOOLEAN IsConsoleLocked)
384 {
385 PCONSRV_CONSOLE ObjectConsole = (PCONSRV_CONSOLE)Object->Console;
386 ConSrvReleaseConsole(ObjectConsole, IsConsoleLocked);
387 }
388
389
390 /* PUBLIC SERVER APIS *********************************************************/
391
392 /* API_NUMBER: ConsolepOpenConsole */
393 CON_API(SrvOpenConsole,
394 CONSOLE_OPENCONSOLE, OpenConsoleRequest)
395 {
396 /*
397 * This API opens a handle to either the input buffer or to
398 * a screen-buffer of the console of the current process.
399 */
400
401 NTSTATUS Status;
402 DWORD DesiredAccess = OpenConsoleRequest->DesiredAccess;
403 DWORD ShareMode = OpenConsoleRequest->ShareMode;
404 PCONSOLE_IO_OBJECT Object;
405
406 OpenConsoleRequest->Handle = INVALID_HANDLE_VALUE;
407
408 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
409
410 /*
411 * Open a handle to either the active screen buffer or the input buffer.
412 */
413 if (OpenConsoleRequest->HandleType == HANDLE_OUTPUT)
414 {
415 Object = &Console->ActiveBuffer->Header;
416 }
417 else // HANDLE_INPUT
418 {
419 Object = &Console->InputBuffer.Header;
420 }
421
422 if (((DesiredAccess & GENERIC_READ) && Object->ExclusiveRead != 0) ||
423 ((DesiredAccess & GENERIC_WRITE) && Object->ExclusiveWrite != 0) ||
424 (!(ShareMode & FILE_SHARE_READ) && Object->AccessRead != 0) ||
425 (!(ShareMode & FILE_SHARE_WRITE) && Object->AccessWrite != 0))
426 {
427 DPRINT1("Sharing violation\n");
428 Status = STATUS_SHARING_VIOLATION;
429 }
430 else
431 {
432 Status = ConSrvInsertObject(ProcessData,
433 &OpenConsoleRequest->Handle,
434 Object,
435 DesiredAccess,
436 OpenConsoleRequest->InheritHandle,
437 ShareMode);
438 }
439
440 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
441
442 return Status;
443 }
444
445 /* API_NUMBER: ConsolepDuplicateHandle */
446 CON_API(SrvDuplicateHandle,
447 CONSOLE_DUPLICATEHANDLE, DuplicateHandleRequest)
448 {
449 NTSTATUS Status;
450 HANDLE SourceHandle = DuplicateHandleRequest->SourceHandle;
451 ULONG Index = HandleToULong(SourceHandle) >> 2;
452 PCONSOLE_IO_HANDLE Entry;
453 DWORD DesiredAccess;
454
455 DuplicateHandleRequest->TargetHandle = INVALID_HANDLE_VALUE;
456
457 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
458
459 // ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
460 // (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
461
462 if ( /** !IsConsoleHandle(SourceHandle) || **/
463 Index >= ProcessData->HandleTableSize ||
464 (Entry = &ProcessData->HandleTable[Index])->Object == NULL)
465 {
466 DPRINT1("Couldn't duplicate invalid handle 0x%p\n", SourceHandle);
467 Status = STATUS_INVALID_HANDLE;
468 goto Quit;
469 }
470
471 if (DuplicateHandleRequest->Options & DUPLICATE_SAME_ACCESS)
472 {
473 DesiredAccess = Entry->Access;
474 }
475 else
476 {
477 DesiredAccess = DuplicateHandleRequest->DesiredAccess;
478 /* Make sure the source handle has all the desired flags */
479 if ((Entry->Access & DesiredAccess) == 0)
480 {
481 DPRINT1("Handle 0x%p only has access %X; requested %X\n",
482 SourceHandle, Entry->Access, DesiredAccess);
483 Status = STATUS_INVALID_PARAMETER;
484 goto Quit;
485 }
486 }
487
488 /* Insert the new handle inside the process handles table */
489 Status = ConSrvInsertObject(ProcessData,
490 &DuplicateHandleRequest->TargetHandle,
491 Entry->Object,
492 DesiredAccess,
493 DuplicateHandleRequest->InheritHandle,
494 Entry->ShareMode);
495 if (NT_SUCCESS(Status) &&
496 (DuplicateHandleRequest->Options & DUPLICATE_CLOSE_SOURCE))
497 {
498 /* Close the original handle if needed */
499 ConSrvCloseHandle(Entry);
500 }
501
502 Quit:
503 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
504
505 return Status;
506 }
507
508 /* API_NUMBER: ConsolepGetHandleInformation */
509 CON_API(SrvGetHandleInformation,
510 CONSOLE_GETHANDLEINFO, GetHandleInfoRequest)
511 {
512 NTSTATUS Status;
513 HANDLE Handle = GetHandleInfoRequest->Handle;
514 ULONG Index = HandleToULong(Handle) >> 2;
515 PCONSOLE_IO_HANDLE Entry;
516
517 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
518
519 ASSERT(ProcessData->HandleTable);
520 // ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
521 // (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
522
523 if (!IsConsoleHandle(Handle) ||
524 Index >= ProcessData->HandleTableSize ||
525 (Entry = &ProcessData->HandleTable[Index])->Object == NULL)
526 {
527 Status = STATUS_INVALID_HANDLE;
528 goto Quit;
529 }
530
531 /*
532 * Retrieve the handle information flags. The console server
533 * doesn't support HANDLE_FLAG_PROTECT_FROM_CLOSE.
534 */
535 GetHandleInfoRequest->Flags = 0;
536 if (Entry->Inheritable) GetHandleInfoRequest->Flags |= HANDLE_FLAG_INHERIT;
537
538 Status = STATUS_SUCCESS;
539
540 Quit:
541 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
542
543 return Status;
544 }
545
546 /* API_NUMBER: ConsolepSetHandleInformation */
547 CON_API(SrvSetHandleInformation,
548 CONSOLE_SETHANDLEINFO, SetHandleInfoRequest)
549 {
550 NTSTATUS Status;
551 HANDLE Handle = SetHandleInfoRequest->Handle;
552 ULONG Index = HandleToULong(Handle) >> 2;
553 PCONSOLE_IO_HANDLE Entry;
554
555 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
556
557 ASSERT(ProcessData->HandleTable);
558 // ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
559 // (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
560
561 if (!IsConsoleHandle(Handle) ||
562 Index >= ProcessData->HandleTableSize ||
563 (Entry = &ProcessData->HandleTable[Index])->Object == NULL)
564 {
565 Status = STATUS_INVALID_HANDLE;
566 goto Quit;
567 }
568
569 /*
570 * Modify the handle information flags. The console server
571 * doesn't support HANDLE_FLAG_PROTECT_FROM_CLOSE.
572 */
573 if (SetHandleInfoRequest->Mask & HANDLE_FLAG_INHERIT)
574 {
575 Entry->Inheritable = ((SetHandleInfoRequest->Flags & HANDLE_FLAG_INHERIT) != 0);
576 }
577
578 Status = STATUS_SUCCESS;
579
580 Quit:
581 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
582
583 return Status;
584 }
585
586 /* API_NUMBER: ConsolepCloseHandle */
587 CON_API(SrvCloseHandle,
588 CONSOLE_CLOSEHANDLE, CloseHandleRequest)
589 {
590 return ConSrvRemoveObject(ProcessData, CloseHandleRequest->Handle);
591 }
592
593 /* API_NUMBER: ConsolepVerifyIoHandle */
594 CON_API(SrvVerifyConsoleIoHandle,
595 CONSOLE_VERIFYHANDLE, VerifyHandleRequest)
596 {
597 HANDLE IoHandle = VerifyHandleRequest->Handle;
598 ULONG Index = HandleToULong(IoHandle) >> 2;
599
600 VerifyHandleRequest->IsValid = FALSE;
601
602 RtlEnterCriticalSection(&ProcessData->HandleTableLock);
603
604 // ASSERT( (ProcessData->HandleTable == NULL && ProcessData->HandleTableSize == 0) ||
605 // (ProcessData->HandleTable != NULL && ProcessData->HandleTableSize != 0) );
606
607 if (!IsConsoleHandle(IoHandle) ||
608 Index >= ProcessData->HandleTableSize ||
609 ProcessData->HandleTable[Index].Object == NULL)
610 {
611 DPRINT("SrvVerifyConsoleIoHandle failed\n");
612 }
613 else
614 {
615 VerifyHandleRequest->IsValid = TRUE;
616 }
617
618 RtlLeaveCriticalSection(&ProcessData->HandleTableLock);
619
620 return STATUS_SUCCESS;
621 }
622
623 /* EOF */