added more irql checks
[reactos.git] / reactos / ntoskrnl / ex / timer.c
1 /* $Id: nttimer.c 12779 2005-01-04 04:45:00Z gdalsnes $
2 *
3 * COPYRIGHT: See COPYING in the top level directory
4 * PROJECT: ReactOS kernel
5 * FILE: ntoskrnl/ex/timer.c
6 * PURPOSE: User-mode timers
7 *
8 * PROGRAMMERS: Alex Ionescu (alex@relsoft.net) - Reimplemented
9 * David Welch (welch@mcmail.com)
10 */
11
12 /* INCLUDES *****************************************************************/
13
14 #include <ntoskrnl.h>
15 #include <internal/debug.h>
16
17 /* TYPES ********************************************************************/
18
19 /* Executive Timer Object */
20 typedef struct _ETIMER {
21 KTIMER KeTimer;
22 KAPC TimerApc;
23 KDPC TimerDpc;
24 LIST_ENTRY ActiveTimerListEntry;
25 KSPIN_LOCK Lock;
26 LONG Period;
27 BOOLEAN ApcAssociated;
28 BOOLEAN WakeTimer;
29 LIST_ENTRY WakeTimerListEntry;
30 } ETIMER, *PETIMER;
31
32 /* GLOBALS ******************************************************************/
33
34 /* Timer Object Type */
35 POBJECT_TYPE ExTimerType = NULL;
36
37 KSPIN_LOCK ExpWakeListLock;
38 LIST_ENTRY ExpWakeList;
39
40 /* Timer Mapping */
41 static GENERIC_MAPPING ExpTimerMapping = {
42 STANDARD_RIGHTS_READ | TIMER_QUERY_STATE,
43 STANDARD_RIGHTS_WRITE | TIMER_MODIFY_STATE,
44 STANDARD_RIGHTS_EXECUTE | SYNCHRONIZE,
45 TIMER_ALL_ACCESS
46 };
47
48 /* Timer Information Classes */
49 static const INFORMATION_CLASS_INFO ExTimerInfoClass[] = {
50
51 /* TimerBasicInformation */
52 ICI_SQ_SAME( sizeof(TIMER_BASIC_INFORMATION), sizeof(ULONG), ICIF_QUERY ),
53 };
54
55 /* FUNCTIONS *****************************************************************/
56
57 VOID
58 STDCALL
59 ExpDeleteTimer(PVOID ObjectBody)
60 {
61 KIRQL OldIrql;
62 PETIMER Timer = ObjectBody;
63
64 DPRINT("ExpDeleteTimer(Timer: %x)\n", Timer);
65
66 /* Lock the Wake List */
67 KeAcquireSpinLock(&ExpWakeListLock, &OldIrql);
68
69 /* Check if it has a Wait List */
70 if (!IsListEmpty(&Timer->WakeTimerListEntry)) {
71
72 /* Remove it from the Wait List */
73 DPRINT("Removing wake list\n");
74 RemoveEntryList(&Timer->WakeTimerListEntry);
75 }
76
77 /* Release the Wake List */
78 KeReleaseSpinLock(&ExpWakeListLock, OldIrql);
79
80 /* Tell the Kernel to cancel the Timer */
81 DPRINT("Cancelling Timer\n");
82 KeCancelTimer(&Timer->KeTimer);
83 }
84
85 VOID
86 STDCALL
87 ExpTimerDpcRoutine(PKDPC Dpc,
88 PVOID DeferredContext,
89 PVOID SystemArgument1,
90 PVOID SystemArgument2)
91 {
92 PETIMER Timer;
93 KIRQL OldIrql;
94
95 DPRINT("ExpTimerDpcRoutine(Dpc: %x)\n", Dpc);
96
97 /* Get the Timer Object */
98 Timer = (PETIMER)DeferredContext;
99
100 /* Lock the Timer */
101 KeAcquireSpinLock(&Timer->Lock, &OldIrql);
102
103 /* Queue the APC */
104 if(Timer->ApcAssociated) {
105
106 DPRINT("Queuing APC\n");
107 KeInsertQueueApc(&Timer->TimerApc,
108 SystemArgument1,
109 SystemArgument2,
110 IO_NO_INCREMENT);
111 }
112
113 /* Release the Timer */
114 KeReleaseSpinLock(&Timer->Lock, OldIrql);
115 }
116
117
118 VOID
119 STDCALL
120 ExpTimerApcKernelRoutine(PKAPC Apc,
121 PKNORMAL_ROUTINE* NormalRoutine,
122 PVOID* NormalContext,
123 PVOID* SystemArgument1,
124 PVOID* SystemArguemnt2)
125 {
126 PETIMER Timer;
127 PETHREAD CurrentThread = PsGetCurrentThread();
128 KIRQL OldIrql;
129
130 /* We need to find out which Timer we are */
131 Timer = CONTAINING_RECORD(Apc, ETIMER, TimerApc);
132 DPRINT("ExpTimerApcKernelRoutine(Apc: %x. Timer: %x)\n", Apc, Timer);
133
134 /* Lock the Timer */
135 KeAcquireSpinLock(&Timer->Lock, &OldIrql);
136
137 /* Lock the Thread's Active Timer List*/
138 KeAcquireSpinLockAtDpcLevel(&CurrentThread->ActiveTimerListLock);
139
140 /*
141 * Make sure that the Timer is still valid, and that it belongs to this thread
142 * Remove it if it's not periodic
143 */
144 if ((Timer->ApcAssociated) &&
145 (&CurrentThread->Tcb == Timer->TimerApc.Thread) &&
146 (!Timer->Period)) {
147
148 /* Remove it from the Active Timers List */
149 DPRINT("Removing Timer\n");
150 RemoveEntryList(&Timer->ActiveTimerListEntry);
151
152 /* Disable it */
153 Timer->ApcAssociated = FALSE;
154
155 /* Release spinlocks */
156 KeReleaseSpinLockFromDpcLevel(&CurrentThread->ActiveTimerListLock);
157 KeReleaseSpinLock(&Timer->Lock, OldIrql);
158
159 /* Dereference the Timer Object */
160 ObDereferenceObject(Timer);
161 return;
162 }
163
164 /* Release spinlocks */
165 KeReleaseSpinLockFromDpcLevel(&CurrentThread->ActiveTimerListLock);
166 KeReleaseSpinLock(&Timer->Lock, OldIrql);
167 }
168
169 VOID
170 INIT_FUNCTION
171 ExpInitializeTimerImplementation(VOID)
172 {
173 DPRINT("ExpInitializeTimerImplementation()\n");
174
175 /* Allocate Memory for the Timer */
176 ExTimerType = ExAllocatePool(NonPagedPool, sizeof(OBJECT_TYPE));
177
178 /* Create the Executive Timer Object */
179 RtlpCreateUnicodeString(&ExTimerType->TypeName, L"Timer", NonPagedPool);
180 ExTimerType->Tag = TAG('T', 'I', 'M', 'T');
181 ExTimerType->PeakObjects = 0;
182 ExTimerType->PeakHandles = 0;
183 ExTimerType->TotalObjects = 0;
184 ExTimerType->TotalHandles = 0;
185 ExTimerType->PagedPoolCharge = 0;
186 ExTimerType->NonpagedPoolCharge = sizeof(ETIMER);
187 ExTimerType->Mapping = &ExpTimerMapping;
188 ExTimerType->Dump = NULL;
189 ExTimerType->Open = NULL;
190 ExTimerType->Close = NULL;
191 ExTimerType->Delete = ExpDeleteTimer;
192 ExTimerType->Parse = NULL;
193 ExTimerType->Security = NULL;
194 ExTimerType->QueryName = NULL;
195 ExTimerType->OkayToClose = NULL;
196 ExTimerType->Create = NULL;
197 ExTimerType->DuplicationNotify = NULL;
198 ObpCreateTypeObject(ExTimerType);
199
200 /* Initialize the Wait List and Lock */
201 KeInitializeSpinLock(&ExpWakeListLock);
202 InitializeListHead(&ExpWakeList);
203 }
204
205
206 NTSTATUS
207 STDCALL
208 NtCancelTimer(IN HANDLE TimerHandle,
209 OUT PBOOLEAN CurrentState OPTIONAL)
210 {
211 PETIMER Timer;
212 KPROCESSOR_MODE PreviousMode;
213 BOOLEAN State;
214 KIRQL OldIrql;
215 PETHREAD TimerThread;
216 BOOLEAN KillTimer = FALSE;
217 NTSTATUS Status = STATUS_SUCCESS;
218
219 PAGED_CODE();
220
221 PreviousMode = ExGetPreviousMode();
222
223 DPRINT("NtCancelTimer(0x%x, 0x%x)\n", TimerHandle, CurrentState);
224
225 /* Check Parameter Validity */
226 if(CurrentState != NULL && PreviousMode != KernelMode) {
227 _SEH_TRY {
228 ProbeForWrite(CurrentState,
229 sizeof(BOOLEAN),
230 sizeof(BOOLEAN));
231 } _SEH_HANDLE {
232 Status = _SEH_GetExceptionCode();
233 } _SEH_END;
234
235 if(!NT_SUCCESS(Status)) {
236 return Status;
237 }
238 }
239
240 /* Get the Timer Object */
241 Status = ObReferenceObjectByHandle(TimerHandle,
242 TIMER_ALL_ACCESS,
243 ExTimerType,
244 PreviousMode,
245 (PVOID*)&Timer,
246 NULL);
247
248 /* Check for success */
249 if(NT_SUCCESS(Status)) {
250
251 DPRINT("Timer Referencced: %x\n", Timer);
252
253 /* Lock the Timer */
254 KeAcquireSpinLock(&Timer->Lock, &OldIrql);
255
256 /* Check if it's enabled */
257 if (Timer->ApcAssociated) {
258
259 /*
260 * First, remove it from the Thread's Active List
261 * Get the Thread.
262 */
263 TimerThread = CONTAINING_RECORD(Timer->TimerApc.Thread, ETHREAD, Tcb);
264 DPRINT("Removing from Thread: %x\n", TimerThread);
265
266 /* Lock its active list */
267 KeAcquireSpinLockAtDpcLevel(&TimerThread->ActiveTimerListLock);
268
269 /* Remove it */
270 RemoveEntryList(&TimerThread->ActiveTimerListHead);
271
272 /* Unlock the list */
273 KeReleaseSpinLockFromDpcLevel(&TimerThread->ActiveTimerListLock);
274
275 /* Cancel the Timer */
276 KeCancelTimer(&Timer->KeTimer);
277 KeRemoveQueueDpc(&Timer->TimerDpc);
278 KeRemoveQueueApc(&Timer->TimerApc);
279 Timer->ApcAssociated = FALSE;
280 KillTimer = TRUE;
281
282 } else {
283
284 /* If timer was disabled, we still need to cancel it */
285 DPRINT("APC was not Associated. Cancelling Timer\n");
286 KeCancelTimer(&Timer->KeTimer);
287 }
288
289 /* Read the old State */
290 State = KeReadStateTimer(&Timer->KeTimer);
291
292 /* Dereference the Object */
293 ObDereferenceObject(Timer);
294
295 /* Unlock the Timer */
296 KeReleaseSpinLock(&Timer->Lock, OldIrql);
297
298 /* Dereference if it was previously enabled */
299 if (KillTimer) ObDereferenceObject(Timer);
300 DPRINT1("Timer disabled\n");
301
302 /* Make sure it's safe to write to the handle */
303 if(CurrentState != NULL) {
304 _SEH_TRY {
305 *CurrentState = State;
306 } _SEH_HANDLE {
307 Status = _SEH_GetExceptionCode();
308 } _SEH_END;
309 }
310 }
311
312 /* Return to Caller */
313 return Status;
314 }
315
316
317 NTSTATUS
318 STDCALL
319 NtCreateTimer(OUT PHANDLE TimerHandle,
320 IN ACCESS_MASK DesiredAccess,
321 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
322 IN TIMER_TYPE TimerType)
323 {
324 PETIMER Timer;
325 HANDLE hTimer;
326 KPROCESSOR_MODE PreviousMode;
327 NTSTATUS Status = STATUS_SUCCESS;
328
329 PAGED_CODE();
330
331 PreviousMode = ExGetPreviousMode();
332
333 DPRINT("NtCreateTimer(Handle: %x, Type: %d)\n", TimerHandle, TimerType);
334
335 /* Check Parameter Validity */
336 if (PreviousMode != KernelMode) {
337 _SEH_TRY {
338 ProbeForWrite(TimerHandle,
339 sizeof(HANDLE),
340 sizeof(ULONG));
341 } _SEH_HANDLE {
342 Status = _SEH_GetExceptionCode();
343 } _SEH_END;
344
345 if(!NT_SUCCESS(Status)) {
346 return Status;
347 }
348 }
349
350 /* Create the Object */
351 Status = ObCreateObject(PreviousMode,
352 ExTimerType,
353 ObjectAttributes,
354 PreviousMode,
355 NULL,
356 sizeof(ETIMER),
357 0,
358 0,
359 (PVOID*)&Timer);
360
361 /* Check for Success */
362 if(NT_SUCCESS(Status)) {
363
364 /* Initialize the Kernel Timer */
365 DPRINT("Initializing Timer: %x\n", Timer);
366 KeInitializeTimerEx(&Timer->KeTimer, TimerType);
367
368 /* Initialize the Timer Lock */
369 KeInitializeSpinLock(&Timer->Lock);
370
371 /* Initialize the DPC */
372 KeInitializeDpc(&Timer->TimerDpc, ExpTimerDpcRoutine, Timer);
373
374 /* Set Initial State */
375 Timer->ApcAssociated = FALSE;
376 InitializeListHead(&Timer->WakeTimerListEntry);
377 Timer->WakeTimer = FALSE;
378
379 /* Insert the Timer */
380 Status = ObInsertObject((PVOID)Timer,
381 NULL,
382 DesiredAccess,
383 0,
384 NULL,
385 &hTimer);
386 DPRINT("Timer Inserted\n");
387
388
389 /* Make sure it's safe to write to the handle */
390 _SEH_TRY {
391 *TimerHandle = hTimer;
392 } _SEH_HANDLE {
393 Status = _SEH_GetExceptionCode();
394 } _SEH_END;
395 }
396
397 /* Return to Caller */
398 return Status;
399 }
400
401
402 NTSTATUS
403 STDCALL
404 NtOpenTimer(OUT PHANDLE TimerHandle,
405 IN ACCESS_MASK DesiredAccess,
406 IN POBJECT_ATTRIBUTES ObjectAttributes)
407 {
408 HANDLE hTimer;
409 KPROCESSOR_MODE PreviousMode;
410 NTSTATUS Status = STATUS_SUCCESS;
411
412 PAGED_CODE();
413
414 PreviousMode = ExGetPreviousMode();
415
416 DPRINT("NtOpenTimer(TimerHandle: %x)\n", TimerHandle);
417
418 /* Check Parameter Validity */
419 if (PreviousMode != KernelMode) {
420 _SEH_TRY {
421 ProbeForWrite(TimerHandle,
422 sizeof(HANDLE),
423 sizeof(ULONG));
424 } _SEH_HANDLE {
425 Status = _SEH_GetExceptionCode();
426 } _SEH_END;
427
428 if(!NT_SUCCESS(Status)) {
429 return Status;
430 }
431 }
432
433 /* Open the Timer */
434 Status = ObOpenObjectByName(ObjectAttributes,
435 ExTimerType,
436 NULL,
437 PreviousMode,
438 DesiredAccess,
439 NULL,
440 &hTimer);
441
442 /* Check for success */
443 if(NT_SUCCESS(Status)) {
444
445 /* Make sure it's safe to write to the handle */
446 _SEH_TRY {
447 *TimerHandle = hTimer;
448 } _SEH_HANDLE {
449 Status = _SEH_GetExceptionCode();
450 } _SEH_END;
451 }
452
453 /* Return to Caller */
454 return Status;
455 }
456
457
458 NTSTATUS
459 STDCALL
460 NtQueryTimer(IN HANDLE TimerHandle,
461 IN TIMER_INFORMATION_CLASS TimerInformationClass,
462 OUT PVOID TimerInformation,
463 IN ULONG TimerInformationLength,
464 OUT PULONG ReturnLength OPTIONAL)
465 {
466 PETIMER Timer;
467 KPROCESSOR_MODE PreviousMode;
468 NTSTATUS Status = STATUS_SUCCESS;
469 PTIMER_BASIC_INFORMATION BasicInfo = (PTIMER_BASIC_INFORMATION)TimerInformation;
470
471 PAGED_CODE();
472
473 PreviousMode = ExGetPreviousMode();
474
475 DPRINT("NtQueryTimer(TimerHandle: %x, Class: %d)\n", TimerHandle, TimerInformationClass);
476
477 /* Check Validity */
478 DefaultQueryInfoBufferCheck(TimerInformationClass,
479 ExTimerInfoClass,
480 TimerInformation,
481 TimerInformationLength,
482 ReturnLength,
483 PreviousMode,
484 &Status);
485 if(!NT_SUCCESS(Status)) {
486
487 DPRINT1("NtQueryTimer() failed, Status: 0x%x\n", Status);
488 return Status;
489 }
490
491 /* Get the Timer Object */
492 Status = ObReferenceObjectByHandle(TimerHandle,
493 TIMER_QUERY_STATE,
494 ExTimerType,
495 PreviousMode,
496 (PVOID*)&Timer,
497 NULL);
498
499 /* Check for Success */
500 if(NT_SUCCESS(Status)) {
501
502 switch(TimerInformationClass) {
503 case TimerBasicInformation: {
504 /* Return the Basic Information */
505 _SEH_TRY {
506
507 /* FIXME: Interrupt correction based on Interrupt Time */
508 DPRINT("Returning Information for Timer: %x. Time Remaining: %d\n", Timer, Timer->KeTimer.DueTime.QuadPart);
509 BasicInfo->TimeRemaining.QuadPart = Timer->KeTimer.DueTime.QuadPart;
510 BasicInfo->SignalState = KeReadStateTimer(&Timer->KeTimer);
511
512 if(ReturnLength != NULL) {
513 *ReturnLength = sizeof(TIMER_BASIC_INFORMATION);
514 }
515
516 } _SEH_HANDLE {
517 Status = _SEH_GetExceptionCode();
518 } _SEH_END;
519 }
520 }
521
522 ObDereferenceObject(Timer);
523 }
524
525 /* Return Status */
526 return Status;
527 }
528
529 NTSTATUS
530 STDCALL
531 NtSetTimer(IN HANDLE TimerHandle,
532 IN PLARGE_INTEGER DueTime,
533 IN PTIMER_APC_ROUTINE TimerApcRoutine OPTIONAL,
534 IN PVOID TimerContext OPTIONAL,
535 IN BOOLEAN WakeTimer,
536 IN LONG Period OPTIONAL,
537 OUT PBOOLEAN PreviousState OPTIONAL)
538 {
539 PETIMER Timer;
540 KIRQL OldIrql;
541 BOOLEAN State;
542 KPROCESSOR_MODE PreviousMode;
543 PETHREAD CurrentThread;
544 LARGE_INTEGER TimerDueTime;
545 PETHREAD TimerThread;
546 BOOLEAN KillTimer = FALSE;
547 NTSTATUS Status = STATUS_SUCCESS;
548
549 PAGED_CODE();
550
551 PreviousMode = ExGetPreviousMode();
552 CurrentThread = PsGetCurrentThread();
553
554 DPRINT("NtSetTimer(TimerHandle: %x, DueTime: %d, Apc: %x, Period: %d)\n", TimerHandle, DueTime->QuadPart, TimerApcRoutine, Period);
555
556 /* Check Parameter Validity */
557 if (PreviousMode != KernelMode) {
558 _SEH_TRY {
559 ProbeForRead(DueTime,
560 sizeof(LARGE_INTEGER),
561 sizeof(ULONG));
562 TimerDueTime = *DueTime;
563
564 if(PreviousState != NULL) {
565 ProbeForWrite(PreviousState,
566 sizeof(BOOLEAN),
567 sizeof(BOOLEAN));
568 }
569
570 } _SEH_HANDLE {
571 Status = _SEH_GetExceptionCode();
572 } _SEH_END;
573
574 if(!NT_SUCCESS(Status)) {
575 return Status;
576 }
577 }
578
579 /* Get the Timer Object */
580 Status = ObReferenceObjectByHandle(TimerHandle,
581 TIMER_ALL_ACCESS,
582 ExTimerType,
583 PreviousMode,
584 (PVOID*)&Timer,
585 NULL);
586
587 /* Check status */
588 if (NT_SUCCESS(Status)) {
589
590 /* Lock the Timer */
591 DPRINT("Timer Referencced: %x\n", Timer);
592 KeAcquireSpinLock(&Timer->Lock, &OldIrql);
593
594 /* Cancel Running Timer */
595 if (Timer->ApcAssociated) {
596
597 /*
598 * First, remove it from the Thread's Active List
599 * Get the Thread.
600 */
601 TimerThread = CONTAINING_RECORD(Timer->TimerApc.Thread, ETHREAD, Tcb);
602 DPRINT("Thread already running. Removing from Thread: %x\n", TimerThread);
603
604 /* Lock its active list */
605 KeAcquireSpinLockAtDpcLevel(&TimerThread->ActiveTimerListLock);
606
607 /* Remove it */
608 RemoveEntryList(&TimerThread->ActiveTimerListHead);
609
610 /* Unlock the list */
611 KeReleaseSpinLockFromDpcLevel(&TimerThread->ActiveTimerListLock);
612
613 /* Cancel the Timer */
614 KeCancelTimer(&Timer->KeTimer);
615 KeRemoveQueueDpc(&Timer->TimerDpc);
616 KeRemoveQueueApc(&Timer->TimerApc);
617 Timer->ApcAssociated = FALSE;
618 KillTimer = TRUE;
619
620 } else {
621
622 /* If timer was disabled, we still need to cancel it */
623 DPRINT("No APCs. Simply cancelling\n");
624 KeCancelTimer(&Timer->KeTimer);
625 }
626
627 /* Read the State */
628 State = KeReadStateTimer(&Timer->KeTimer);
629
630 /* Handle Wake Timers */
631 DPRINT("Doing Wake Semantics\n");
632 KeAcquireSpinLockAtDpcLevel(&ExpWakeListLock);
633 if (WakeTimer) {
634
635 /* Insert it into the list */
636 InsertTailList(&ExpWakeList, &Timer->WakeTimerListEntry);
637
638 } else {
639
640 /* Remove it from the list */
641 RemoveEntryList(&Timer->WakeTimerListEntry);
642 Timer->WakeTimerListEntry.Flink = NULL;
643 }
644 KeReleaseSpinLockFromDpcLevel(&ExpWakeListLock);
645
646 /* Set up the APC Routine if specified */
647 if (TimerApcRoutine) {
648
649 /* Initialize the APC */
650 DPRINT("Initializing APC: %x\n", Timer->TimerApc);
651 KeInitializeApc(&Timer->TimerApc,
652 &CurrentThread->Tcb,
653 CurrentApcEnvironment,
654 &ExpTimerApcKernelRoutine,
655 (PKRUNDOWN_ROUTINE)NULL,
656 (PKNORMAL_ROUTINE)TimerApcRoutine,
657 PreviousMode,
658 TimerContext);
659
660 /* Lock the Thread's Active List and Insert */
661 KeAcquireSpinLockAtDpcLevel(&CurrentThread->ActiveTimerListLock);
662 InsertTailList(&CurrentThread->ActiveTimerListHead,
663 &Timer->ActiveTimerListEntry);
664 KeReleaseSpinLockFromDpcLevel(&CurrentThread->ActiveTimerListLock);
665
666 }
667
668 /* Enable and Set the Timer */
669 DPRINT("Setting Kernel Timer\n");
670 KeSetTimerEx(&Timer->KeTimer,
671 TimerDueTime,
672 Period,
673 TimerApcRoutine ? &Timer->TimerDpc : 0);
674 Timer->ApcAssociated = TimerApcRoutine ? TRUE : FALSE;
675
676 /* Unlock the Timer */
677 KeReleaseSpinLock(&Timer->Lock, OldIrql);
678
679 /* Dereference the Object */
680 ObDereferenceObject(Timer);
681
682 /* Unlock the Timer */
683 KeReleaseSpinLock(&Timer->Lock, OldIrql);
684
685 /* Dereference if it was previously enabled */
686 if (!TimerApcRoutine) ObDereferenceObject(Timer);
687 if (KillTimer) ObDereferenceObject(Timer);
688 DPRINT("Finished Setting the Timer\n");
689
690 /* Make sure it's safe to write to the handle */
691 if(PreviousState != NULL) {
692 _SEH_TRY {
693 *PreviousState = State;
694 } _SEH_HANDLE {
695 Status = _SEH_GetExceptionCode();
696 } _SEH_END;
697 }
698 }
699
700 /* Return to Caller */
701 return Status;
702 }
703
704 /* EOF */