c2ecdd59c6b899c4dc07cffc49a3c52f27fd4cd9
[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 /* Return the Basic Information */
503 _SEH_TRY {
504
505 /* FIXME: Interrupt correction based on Interrupt Time */
506 DPRINT("Returning Information for Timer: %x. Time Remaining: %d\n", Timer, Timer->KeTimer.DueTime.QuadPart);
507 BasicInfo->TimeRemaining.QuadPart = Timer->KeTimer.DueTime.QuadPart;
508 BasicInfo->SignalState = KeReadStateTimer(&Timer->KeTimer);
509
510 if(ReturnLength != NULL) *ReturnLength = sizeof(TIMER_BASIC_INFORMATION);
511
512 } _SEH_HANDLE {
513
514 Status = _SEH_GetExceptionCode();
515 } _SEH_END;
516
517 /* Dereference Object */
518 ObDereferenceObject(Timer);
519 }
520
521 /* Return Status */
522 return Status;
523 }
524
525 NTSTATUS
526 STDCALL
527 NtSetTimer(IN HANDLE TimerHandle,
528 IN PLARGE_INTEGER DueTime,
529 IN PTIMER_APC_ROUTINE TimerApcRoutine OPTIONAL,
530 IN PVOID TimerContext OPTIONAL,
531 IN BOOLEAN WakeTimer,
532 IN LONG Period OPTIONAL,
533 OUT PBOOLEAN PreviousState OPTIONAL)
534 {
535 PETIMER Timer;
536 KIRQL OldIrql;
537 BOOLEAN State;
538 KPROCESSOR_MODE PreviousMode;
539 PETHREAD CurrentThread;
540 LARGE_INTEGER TimerDueTime;
541 PETHREAD TimerThread;
542 BOOLEAN KillTimer = FALSE;
543 NTSTATUS Status = STATUS_SUCCESS;
544
545 PAGED_CODE();
546
547 PreviousMode = ExGetPreviousMode();
548 CurrentThread = PsGetCurrentThread();
549
550 DPRINT("NtSetTimer(TimerHandle: %x, DueTime: %d, Apc: %x, Period: %d)\n", TimerHandle, DueTime->QuadPart, TimerApcRoutine, Period);
551
552 /* Check Parameter Validity */
553 if (PreviousMode != KernelMode) {
554 _SEH_TRY {
555 ProbeForRead(DueTime,
556 sizeof(LARGE_INTEGER),
557 sizeof(ULONG));
558 TimerDueTime = *DueTime;
559
560 if(PreviousState != NULL) {
561 ProbeForWrite(PreviousState,
562 sizeof(BOOLEAN),
563 sizeof(BOOLEAN));
564 }
565
566 } _SEH_HANDLE {
567 Status = _SEH_GetExceptionCode();
568 } _SEH_END;
569
570 if(!NT_SUCCESS(Status)) {
571 return Status;
572 }
573 }
574
575 /* Get the Timer Object */
576 Status = ObReferenceObjectByHandle(TimerHandle,
577 TIMER_ALL_ACCESS,
578 ExTimerType,
579 PreviousMode,
580 (PVOID*)&Timer,
581 NULL);
582
583 /* Check status */
584 if (NT_SUCCESS(Status)) {
585
586 /* Lock the Timer */
587 DPRINT("Timer Referencced: %x\n", Timer);
588 KeAcquireSpinLock(&Timer->Lock, &OldIrql);
589
590 /* Cancel Running Timer */
591 if (Timer->ApcAssociated) {
592
593 /*
594 * First, remove it from the Thread's Active List
595 * Get the Thread.
596 */
597 TimerThread = CONTAINING_RECORD(Timer->TimerApc.Thread, ETHREAD, Tcb);
598 DPRINT("Thread already running. Removing from Thread: %x\n", TimerThread);
599
600 /* Lock its active list */
601 KeAcquireSpinLockAtDpcLevel(&TimerThread->ActiveTimerListLock);
602
603 /* Remove it */
604 RemoveEntryList(&TimerThread->ActiveTimerListHead);
605
606 /* Unlock the list */
607 KeReleaseSpinLockFromDpcLevel(&TimerThread->ActiveTimerListLock);
608
609 /* Cancel the Timer */
610 KeCancelTimer(&Timer->KeTimer);
611 KeRemoveQueueDpc(&Timer->TimerDpc);
612 KeRemoveQueueApc(&Timer->TimerApc);
613 Timer->ApcAssociated = FALSE;
614 KillTimer = TRUE;
615
616 } else {
617
618 /* If timer was disabled, we still need to cancel it */
619 DPRINT("No APCs. Simply cancelling\n");
620 KeCancelTimer(&Timer->KeTimer);
621 }
622
623 /* Read the State */
624 State = KeReadStateTimer(&Timer->KeTimer);
625
626 /* Handle Wake Timers */
627 DPRINT("Doing Wake Semantics\n");
628 KeAcquireSpinLockAtDpcLevel(&ExpWakeListLock);
629 if (WakeTimer) {
630
631 /* Insert it into the list */
632 InsertTailList(&ExpWakeList, &Timer->WakeTimerListEntry);
633
634 } else {
635
636 /* Remove it from the list */
637 RemoveEntryList(&Timer->WakeTimerListEntry);
638 Timer->WakeTimerListEntry.Flink = NULL;
639 }
640 KeReleaseSpinLockFromDpcLevel(&ExpWakeListLock);
641
642 /* Set up the APC Routine if specified */
643 if (TimerApcRoutine) {
644
645 /* Initialize the APC */
646 DPRINT("Initializing APC: %x\n", Timer->TimerApc);
647 KeInitializeApc(&Timer->TimerApc,
648 &CurrentThread->Tcb,
649 CurrentApcEnvironment,
650 &ExpTimerApcKernelRoutine,
651 (PKRUNDOWN_ROUTINE)NULL,
652 (PKNORMAL_ROUTINE)TimerApcRoutine,
653 PreviousMode,
654 TimerContext);
655
656 /* Lock the Thread's Active List and Insert */
657 KeAcquireSpinLockAtDpcLevel(&CurrentThread->ActiveTimerListLock);
658 InsertTailList(&CurrentThread->ActiveTimerListHead,
659 &Timer->ActiveTimerListEntry);
660 KeReleaseSpinLockFromDpcLevel(&CurrentThread->ActiveTimerListLock);
661
662 }
663
664 /* Enable and Set the Timer */
665 DPRINT("Setting Kernel Timer\n");
666 KeSetTimerEx(&Timer->KeTimer,
667 TimerDueTime,
668 Period,
669 TimerApcRoutine ? &Timer->TimerDpc : 0);
670 Timer->ApcAssociated = TimerApcRoutine ? TRUE : FALSE;
671
672 /* Unlock the Timer */
673 KeReleaseSpinLock(&Timer->Lock, OldIrql);
674
675 /* Dereference the Object */
676 ObDereferenceObject(Timer);
677
678 /* Unlock the Timer */
679 KeReleaseSpinLock(&Timer->Lock, OldIrql);
680
681 /* Dereference if it was previously enabled */
682 if (!TimerApcRoutine) ObDereferenceObject(Timer);
683 if (KillTimer) ObDereferenceObject(Timer);
684 DPRINT("Finished Setting the Timer\n");
685
686 /* Make sure it's safe to write to the handle */
687 if(PreviousState != NULL) {
688 _SEH_TRY {
689 *PreviousState = State;
690 } _SEH_HANDLE {
691 Status = _SEH_GetExceptionCode();
692 } _SEH_END;
693 }
694 }
695
696 /* Return to Caller */
697 return Status;
698 }
699
700 /* EOF */