[STORPORT] Detect attached devices
[reactos.git] / drivers / storage / port / storport / fdo.c
1 /*
2 * PROJECT: ReactOS Storport Driver
3 * LICENSE: GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4 * PURPOSE: Storport FDO code
5 * COPYRIGHT: Copyright 2017 Eric Kohl (eric.kohl@reactos.org)
6 */
7
8 /* INCLUDES *******************************************************************/
9
10 #include "precomp.h"
11
12 #define NDEBUG
13 #include <debug.h>
14
15
16 /* FUNCTIONS ******************************************************************/
17
18 static
19 BOOLEAN
20 NTAPI
21 PortFdoInterruptRoutine(
22 _In_ PKINTERRUPT Interrupt,
23 _In_ PVOID ServiceContext)
24 {
25 PFDO_DEVICE_EXTENSION DeviceExtension;
26
27 DPRINT1("PortFdoInterruptRoutine(%p %p)\n",
28 Interrupt, ServiceContext);
29
30 DeviceExtension = (PFDO_DEVICE_EXTENSION)ServiceContext;
31
32 return MiniportHwInterrupt(&DeviceExtension->Miniport);
33 }
34
35
36 static
37 NTSTATUS
38 PortFdoConnectInterrupt(
39 _In_ PFDO_DEVICE_EXTENSION DeviceExtension)
40 {
41 ULONG Vector;
42 KIRQL Irql;
43 KINTERRUPT_MODE InterruptMode;
44 BOOLEAN ShareVector;
45 KAFFINITY Affinity;
46 NTSTATUS Status;
47
48 DPRINT1("PortFdoConnectInterrupt(%p)\n",
49 DeviceExtension);
50
51 /* No resources, no interrupt. Done! */
52 if (DeviceExtension->AllocatedResources == NULL ||
53 DeviceExtension->TranslatedResources == NULL)
54 {
55 DPRINT1("Checkpoint\n");
56 return STATUS_SUCCESS;
57 }
58
59 /* Get the interrupt data from the resource list */
60 Status = GetResourceListInterrupt(DeviceExtension,
61 &Vector,
62 &Irql,
63 &InterruptMode,
64 &ShareVector,
65 &Affinity);
66 if (!NT_SUCCESS(Status))
67 {
68 DPRINT1("GetResourceListInterrupt() failed (Status 0x%08lx)\n", Status);
69 return Status;
70 }
71
72 DPRINT1("Vector: %lu\n", Vector);
73 DPRINT1("Irql: %lu\n", Irql);
74
75 DPRINT1("Affinity: 0x%08lx\n", Affinity);
76
77 /* Connect the interrupt */
78 Status = IoConnectInterrupt(&DeviceExtension->Interrupt,
79 PortFdoInterruptRoutine,
80 DeviceExtension,
81 NULL,
82 Vector,
83 Irql,
84 Irql,
85 InterruptMode,
86 ShareVector,
87 Affinity,
88 FALSE);
89 if (NT_SUCCESS(Status))
90 {
91 DeviceExtension->InterruptIrql = Irql;
92 }
93 else
94 {
95 DPRINT1("IoConnectInterrupt() failed (Status 0x%08lx)\n", Status);
96 }
97
98 return Status;
99 }
100
101
102 static
103 NTSTATUS
104 PortFdoStartMiniport(
105 _In_ PFDO_DEVICE_EXTENSION DeviceExtension)
106 {
107 PHW_INITIALIZATION_DATA InitData;
108 INTERFACE_TYPE InterfaceType;
109 NTSTATUS Status;
110
111 DPRINT1("PortFdoStartDevice(%p)\n", DeviceExtension);
112
113 /* Get the interface type of the lower device */
114 InterfaceType = GetBusInterface(DeviceExtension->LowerDevice);
115 if (InterfaceType == InterfaceTypeUndefined)
116 return STATUS_NO_SUCH_DEVICE;
117
118 /* Get the driver init data for the given interface type */
119 InitData = PortGetDriverInitData(DeviceExtension->DriverExtension,
120 InterfaceType);
121 if (InitData == NULL)
122 return STATUS_NO_SUCH_DEVICE;
123
124 /* Initialize the miniport */
125 Status = MiniportInitialize(&DeviceExtension->Miniport,
126 DeviceExtension,
127 InitData);
128 if (!NT_SUCCESS(Status))
129 {
130 DPRINT1("MiniportInitialize() failed (Status 0x%08lx)\n", Status);
131 return Status;
132 }
133
134 /* Call the miniports FindAdapter function */
135 Status = MiniportFindAdapter(&DeviceExtension->Miniport);
136 if (!NT_SUCCESS(Status))
137 {
138 DPRINT1("MiniportFindAdapter() failed (Status 0x%08lx)\n", Status);
139 return Status;
140 }
141
142 /* Connect the configured interrupt */
143 Status = PortFdoConnectInterrupt(DeviceExtension);
144 if (!NT_SUCCESS(Status))
145 {
146 DPRINT1("PortFdoConnectInterrupt() failed (Status 0x%08lx)\n", Status);
147 return Status;
148 }
149
150 /* Call the miniports HwInitialize function */
151 Status = MiniportHwInitialize(&DeviceExtension->Miniport);
152 if (!NT_SUCCESS(Status))
153 {
154 DPRINT1("MiniportHwInitialize() failed (Status 0x%08lx)\n", Status);
155 return Status;
156 }
157
158 /* Call the HwPassiveInitRoutine function, if available */
159 if (DeviceExtension->HwPassiveInitRoutine != NULL)
160 {
161 DPRINT1("Calling HwPassiveInitRoutine()\n");
162 if (!DeviceExtension->HwPassiveInitRoutine(&DeviceExtension->Miniport.MiniportExtension->HwDeviceExtension))
163 {
164 DPRINT1("HwPassiveInitRoutine() failed\n");
165 return STATUS_UNSUCCESSFUL;
166 }
167 }
168
169 return STATUS_SUCCESS;
170 }
171
172
173 static
174 NTSTATUS
175 NTAPI
176 PortFdoStartDevice(
177 _In_ PFDO_DEVICE_EXTENSION DeviceExtension,
178 _In_ PIRP Irp)
179 {
180 PIO_STACK_LOCATION Stack;
181 NTSTATUS Status;
182
183 DPRINT1("PortFdoStartDevice(%p %p)\n",
184 DeviceExtension, Irp);
185
186 ASSERT(DeviceExtension->ExtensionType == FdoExtension);
187
188 /* Get the current stack location */
189 Stack = IoGetCurrentIrpStackLocation(Irp);
190
191 /* Start the lower device if the FDO is in 'stopped' state */
192 if (DeviceExtension->PnpState == dsStopped)
193 {
194 Status = ForwardIrpAndWait(DeviceExtension->LowerDevice, Irp);
195 if (!NT_SUCCESS(Status))
196 {
197 DPRINT1("ForwardIrpAndWait() failed (Status 0x%08lx)\n", Status);
198 return Status;
199 }
200 }
201
202 /* Change to the 'started' state */
203 DeviceExtension->PnpState = dsStarted;
204
205 /* Copy the raw and translated resource lists into the device extension */
206 if (Stack->Parameters.StartDevice.AllocatedResources != NULL &&
207 Stack->Parameters.StartDevice.AllocatedResourcesTranslated != NULL)
208 {
209 DeviceExtension->AllocatedResources = CopyResourceList(NonPagedPool,
210 Stack->Parameters.StartDevice.AllocatedResources);
211 if (DeviceExtension->AllocatedResources == NULL)
212 return STATUS_NO_MEMORY;
213
214 DeviceExtension->TranslatedResources = CopyResourceList(NonPagedPool,
215 Stack->Parameters.StartDevice.AllocatedResourcesTranslated);
216 if (DeviceExtension->TranslatedResources == NULL)
217 return STATUS_NO_MEMORY;
218 }
219
220 /* Get the bus interface of the lower (bus) device */
221 Status = QueryBusInterface(DeviceExtension->LowerDevice,
222 (PGUID)&GUID_BUS_INTERFACE_STANDARD,
223 sizeof(BUS_INTERFACE_STANDARD),
224 1,
225 &DeviceExtension->BusInterface,
226 NULL);
227 DPRINT1("Status: 0x%08lx\n", Status);
228 if (NT_SUCCESS(Status))
229 {
230 DPRINT1("Context: %p\n", DeviceExtension->BusInterface.Context);
231 DeviceExtension->BusInitialized = TRUE;
232 }
233
234 /* Start the miniport (FindAdapter & Initialize) */
235 Status = PortFdoStartMiniport(DeviceExtension);
236 if (!NT_SUCCESS(Status))
237 {
238 DPRINT1("FdoStartMiniport() failed (Status 0x%08lx)\n", Status);
239 DeviceExtension->PnpState = dsStopped;
240 }
241
242 return Status;
243 }
244
245
246 static NTSTATUS
247 PortSendInquiry(
248 _In_ PDEVICE_OBJECT DeviceObject,
249 _In_ ULONG Bus,
250 _In_ ULONG Target,
251 _In_ ULONG Lun)
252 {
253 PINQUIRYDATA InquiryBuffer;
254 PUCHAR /*PSENSE_DATA*/ SenseBuffer;
255 BOOLEAN KeepTrying = TRUE;
256 ULONG RetryCount = 0;
257 SCSI_REQUEST_BLOCK Srb;
258 PCDB Cdb;
259 PFDO_DEVICE_EXTENSION DeviceExtension;
260 PVOID SrbExtension = NULL;
261 BOOLEAN ret;
262 PUNIT_DATA UnitData;
263 NTSTATUS Status;
264
265 DPRINT("PortSendInquiry(%p %lu %lu %lu)\n",
266 DeviceObject, Bus, Target, Lun);
267
268 DeviceExtension = (PFDO_DEVICE_EXTENSION)DeviceObject->DeviceExtension;
269
270 InquiryBuffer = ExAllocatePoolWithTag(NonPagedPool, INQUIRYDATABUFFERSIZE, TAG_INQUIRY_DATA);
271 if (InquiryBuffer == NULL)
272 return STATUS_INSUFFICIENT_RESOURCES;
273
274 SenseBuffer = ExAllocatePoolWithTag(NonPagedPool, SENSE_BUFFER_SIZE, TAG_SENSE_DATA);
275 if (SenseBuffer == NULL)
276 {
277 ExFreePoolWithTag(InquiryBuffer, TAG_INQUIRY_DATA);
278 return STATUS_INSUFFICIENT_RESOURCES;
279 }
280
281 if (DeviceExtension->Miniport.PortConfig.SrbExtensionSize != 0)
282 {
283 SrbExtension = ExAllocatePoolWithTag(NonPagedPool,
284 DeviceExtension->Miniport.PortConfig.SrbExtensionSize,
285 TAG_SENSE_DATA);
286 if (SrbExtension == NULL)
287 {
288 ExFreePoolWithTag(SenseBuffer, TAG_SENSE_DATA);
289 ExFreePoolWithTag(InquiryBuffer, TAG_INQUIRY_DATA);
290 return STATUS_INSUFFICIENT_RESOURCES;
291 }
292 }
293
294 while (KeepTrying)
295 {
296 /* Prepare SRB */
297 RtlZeroMemory(&Srb, sizeof(SCSI_REQUEST_BLOCK));
298
299 Srb.Length = sizeof(SCSI_REQUEST_BLOCK);
300 // Srb.OriginalRequest = Irp;
301 Srb.PathId = Bus;
302 Srb.TargetId = Target;
303 Srb.Lun = Lun;
304 Srb.Function = SRB_FUNCTION_EXECUTE_SCSI;
305 Srb.SrbFlags = SRB_FLAGS_DATA_IN | SRB_FLAGS_DISABLE_SYNCH_TRANSFER;
306 Srb.TimeOutValue = 4;
307 Srb.CdbLength = 6;
308
309 Srb.SenseInfoBuffer = SenseBuffer;
310 Srb.SenseInfoBufferLength = SENSE_BUFFER_SIZE;
311
312 Srb.DataBuffer = InquiryBuffer;
313 Srb.DataTransferLength = INQUIRYDATABUFFERSIZE;
314
315 Srb.SrbExtension = SrbExtension;
316
317 /* Fill in CDB */
318 Cdb = (PCDB)Srb.Cdb;
319 Cdb->CDB6INQUIRY3.OperationCode = SCSIOP_INQUIRY;
320 Cdb->CDB6INQUIRY3.EnableVitalProductData = 1;
321 Cdb->CDB6INQUIRY3.CommandSupportData = 0;
322 Cdb->CDB6INQUIRY3.PageCode = 0; //??
323 Cdb->CDB6INQUIRY3.AllocationLength = INQUIRYDATABUFFERSIZE;
324 Cdb->CDB6INQUIRY3.Control = 0;
325
326 /* Call the miniport driver */
327 ret = MiniportStartIo(&DeviceExtension->Miniport,
328 &Srb);
329 if (ret == FALSE)
330 {
331 Status = STATUS_IO_DEVICE_ERROR;
332 KeepTrying = FALSE;
333 continue;
334 }
335
336 DPRINT("SrbStatus 0x%08lx\n", Srb.SrbStatus);
337 if (SRB_STATUS(Srb.SrbStatus) == SRB_STATUS_SUCCESS)
338 {
339 DPRINT("Found a device!\n");
340
341 UnitData = ExAllocatePool(NonPagedPool, sizeof(UNIT_DATA));
342 if (UnitData == NULL)
343 {
344 Status = STATUS_INSUFFICIENT_RESOURCES;
345 KeepTrying = FALSE;
346 continue;
347 }
348
349 /* All fine, copy data over */
350 RtlCopyMemory(&UnitData->InquiryData,
351 Srb.DataBuffer,
352 Srb.DataTransferLength);
353
354 InsertTailList(&DeviceExtension->UnitListHead,
355 &UnitData->ListEntry);
356 DeviceExtension->UnitCount++;
357
358 /* Quit the loop */
359 Status = STATUS_SUCCESS;
360 KeepTrying = FALSE;
361 continue;
362 }
363
364 DPRINT("Inquiry SRB failed with SrbStatus 0x%08X\n", Srb.SrbStatus);
365
366 /* Retry a couple of times if no timeout happened */
367 if ((RetryCount < 2) &&
368 (SRB_STATUS(Srb.SrbStatus) != SRB_STATUS_NO_DEVICE) &&
369 (SRB_STATUS(Srb.SrbStatus) != SRB_STATUS_SELECTION_TIMEOUT))
370 {
371 RetryCount++;
372 KeepTrying = TRUE;
373 }
374 else
375 {
376 /* That's all, quit the loop */
377 KeepTrying = FALSE;
378
379 /* Set status according to SRB status */
380 if (SRB_STATUS(Srb.SrbStatus) == SRB_STATUS_BAD_FUNCTION ||
381 SRB_STATUS(Srb.SrbStatus) == SRB_STATUS_BAD_SRB_BLOCK_LENGTH)
382 {
383 Status = STATUS_INVALID_DEVICE_REQUEST;
384 }
385 else
386 {
387 Status = STATUS_IO_DEVICE_ERROR;
388 }
389 }
390 }
391
392 /* Free buffers */
393 if (SrbExtension != NULL)
394 ExFreePoolWithTag(SrbExtension, TAG_SENSE_DATA);
395
396 ExFreePoolWithTag(SenseBuffer, TAG_SENSE_DATA);
397 ExFreePoolWithTag(InquiryBuffer, TAG_INQUIRY_DATA);
398
399 DPRINT("PortSendInquiry() returns 0x%08lx\n", Status);
400
401 return Status;
402 }
403
404
405 static
406 NTSTATUS
407 PortFdoScanBus(
408 _In_ PFDO_DEVICE_EXTENSION DeviceExtension)
409 {
410 ULONG Bus, Target, Lun;
411 NTSTATUS Status;
412
413 DPRINT1("PortFdoScanBus(%p)\n",
414 DeviceExtension);
415
416 DPRINT1("NumberOfBuses: %lu\n", DeviceExtension->Miniport.PortConfig.NumberOfBuses);
417 DPRINT1("MaximumNumberOfTargets: %lu\n", DeviceExtension->Miniport.PortConfig.MaximumNumberOfTargets);
418 DPRINT1("MaximumNumberOfLogicalUnits: %lu\n", DeviceExtension->Miniport.PortConfig.MaximumNumberOfLogicalUnits);
419
420 /* Scan all buses */
421 for (Bus = 0; Bus < DeviceExtension->Miniport.PortConfig.NumberOfBuses; Bus++)
422 {
423 DPRINT1("Scanning bus %ld\n", Bus);
424
425 /* Scan all targets */
426 for (Target = 0; Target < DeviceExtension->Miniport.PortConfig.MaximumNumberOfTargets; Target++)
427 {
428 DPRINT1(" Scanning target %ld:%ld\n", Bus, Target);
429
430 /* Scan all logical units */
431 for (Lun = 0; Lun < DeviceExtension->Miniport.PortConfig.MaximumNumberOfLogicalUnits; Lun++)
432 {
433 DPRINT1(" Scanning logical unit %ld:%ld:%ld\n", Bus, Target, Lun);
434
435 Status = PortSendInquiry(DeviceExtension->Device, Bus, Target, Lun);
436 DPRINT1("PortSendInquiry returned 0x%08lx\n", Status);
437 if (!NT_SUCCESS(Status))
438 break;
439 }
440 }
441 }
442
443 DPRINT("Done!\n");
444
445 return STATUS_SUCCESS;
446 }
447
448
449 static
450 NTSTATUS
451 PortFdoQueryBusRelations(
452 _In_ PFDO_DEVICE_EXTENSION DeviceExtension,
453 _Out_ PULONG_PTR Information)
454 {
455 NTSTATUS Status = STATUS_SUCCESS;;
456
457 DPRINT1("PortFdoQueryBusRelations(%p %p)\n",
458 DeviceExtension, Information);
459
460 Status = PortFdoScanBus(DeviceExtension);
461
462 DPRINT1("Units found: %lu\n", DeviceExtension->UnitCount);
463
464 *Information = 0;
465
466 return Status;
467 }
468
469
470 static
471 NTSTATUS
472 PortFdoFilterRequirements(
473 PFDO_DEVICE_EXTENSION DeviceExtension,
474 PIRP Irp)
475 {
476 PIO_RESOURCE_REQUIREMENTS_LIST RequirementsList;
477
478 DPRINT1("PortFdoFilterRequirements(%p %p)\n", DeviceExtension, Irp);
479
480 /* Get the bus number and the slot number */
481 RequirementsList =(PIO_RESOURCE_REQUIREMENTS_LIST)Irp->IoStatus.Information;
482 if (RequirementsList != NULL)
483 {
484 DeviceExtension->BusNumber = RequirementsList->BusNumber;
485 DeviceExtension->SlotNumber = RequirementsList->SlotNumber;
486 }
487
488 return STATUS_SUCCESS;
489 }
490
491
492 NTSTATUS
493 NTAPI
494 PortFdoScsi(
495 _In_ PDEVICE_OBJECT DeviceObject,
496 _In_ PIRP Irp)
497 {
498 PFDO_DEVICE_EXTENSION DeviceExtension;
499 // PIO_STACK_LOCATION Stack;
500 ULONG_PTR Information = 0;
501 NTSTATUS Status = STATUS_NOT_SUPPORTED;
502
503 DPRINT1("PortFdoScsi(%p %p)\n",
504 DeviceObject, Irp);
505
506 DeviceExtension = (PFDO_DEVICE_EXTENSION)DeviceObject->DeviceExtension;
507 ASSERT(DeviceExtension);
508 ASSERT(DeviceExtension->ExtensionType == FdoExtension);
509
510 // Stack = IoGetCurrentIrpStackLocation(Irp);
511
512
513 Irp->IoStatus.Information = Information;
514 Irp->IoStatus.Status = Status;
515 IoCompleteRequest(Irp, IO_NO_INCREMENT);
516
517 return Status;
518 }
519
520
521 NTSTATUS
522 NTAPI
523 PortFdoPnp(
524 _In_ PDEVICE_OBJECT DeviceObject,
525 _In_ PIRP Irp)
526 {
527 PFDO_DEVICE_EXTENSION DeviceExtension;
528 PIO_STACK_LOCATION Stack;
529 ULONG_PTR Information = 0;
530 NTSTATUS Status = STATUS_NOT_SUPPORTED;
531
532 DPRINT1("PortFdoPnp(%p %p)\n",
533 DeviceObject, Irp);
534
535 DeviceExtension = (PFDO_DEVICE_EXTENSION)DeviceObject->DeviceExtension;
536 ASSERT(DeviceExtension);
537 ASSERT(DeviceExtension->ExtensionType == FdoExtension);
538
539 Stack = IoGetCurrentIrpStackLocation(Irp);
540
541 switch (Stack->MinorFunction)
542 {
543 case IRP_MN_START_DEVICE: /* 0x00 */
544 DPRINT1("IRP_MJ_PNP / IRP_MN_START_DEVICE\n");
545 Status = PortFdoStartDevice(DeviceExtension, Irp);
546 break;
547
548 case IRP_MN_QUERY_REMOVE_DEVICE: /* 0x01 */
549 DPRINT1("IRP_MJ_PNP / IRP_MN_QUERY_REMOVE_DEVICE\n");
550 break;
551
552 case IRP_MN_REMOVE_DEVICE: /* 0x02 */
553 DPRINT1("IRP_MJ_PNP / IRP_MN_REMOVE_DEVICE\n");
554 break;
555
556 case IRP_MN_CANCEL_REMOVE_DEVICE: /* 0x03 */
557 DPRINT1("IRP_MJ_PNP / IRP_MN_CANCEL_REMOVE_DEVICE\n");
558 break;
559
560 case IRP_MN_STOP_DEVICE: /* 0x04 */
561 DPRINT1("IRP_MJ_PNP / IRP_MN_STOP_DEVICE\n");
562 break;
563
564 case IRP_MN_QUERY_STOP_DEVICE: /* 0x05 */
565 DPRINT1("IRP_MJ_PNP / IRP_MN_QUERY_STOP_DEVICE\n");
566 break;
567
568 case IRP_MN_CANCEL_STOP_DEVICE: /* 0x06 */
569 DPRINT1("IRP_MJ_PNP / IRP_MN_CANCEL_STOP_DEVICE\n");
570 break;
571
572 case IRP_MN_QUERY_DEVICE_RELATIONS: /* 0x07 */
573 DPRINT1("IRP_MJ_PNP / IRP_MN_QUERY_DEVICE_RELATIONS\n");
574 switch (Stack->Parameters.QueryDeviceRelations.Type)
575 {
576 case BusRelations:
577 DPRINT1(" IRP_MJ_PNP / IRP_MN_QUERY_DEVICE_RELATIONS / BusRelations\n");
578 Status = PortFdoQueryBusRelations(DeviceExtension, &Information);
579 break;
580
581 case RemovalRelations:
582 DPRINT1(" IRP_MJ_PNP / IRP_MN_QUERY_DEVICE_RELATIONS / RemovalRelations\n");
583 return ForwardIrpAndForget(DeviceExtension->LowerDevice, Irp);
584
585 default:
586 DPRINT1(" IRP_MJ_PNP / IRP_MN_QUERY_DEVICE_RELATIONS / Unknown type 0x%lx\n",
587 Stack->Parameters.QueryDeviceRelations.Type);
588 return ForwardIrpAndForget(DeviceExtension->LowerDevice, Irp);
589 }
590 break;
591
592 case IRP_MN_FILTER_RESOURCE_REQUIREMENTS: /* 0x0d */
593 DPRINT1("IRP_MJ_PNP / IRP_MN_FILTER_RESOURCE_REQUIREMENTS\n");
594 PortFdoFilterRequirements(DeviceExtension, Irp);
595 return ForwardIrpAndForget(DeviceExtension->LowerDevice, Irp);
596
597 case IRP_MN_QUERY_PNP_DEVICE_STATE: /* 0x14 */
598 DPRINT1("IRP_MJ_PNP / IRP_MN_QUERY_PNP_DEVICE_STATE\n");
599 break;
600
601 case IRP_MN_DEVICE_USAGE_NOTIFICATION: /* 0x16 */
602 DPRINT1("IRP_MJ_PNP / IRP_MN_DEVICE_USAGE_NOTIFICATION\n");
603 break;
604
605 case IRP_MN_SURPRISE_REMOVAL: /* 0x17 */
606 DPRINT1("IRP_MJ_PNP / IRP_MN_SURPRISE_REMOVAL\n");
607 break;
608
609 default:
610 DPRINT1("IRP_MJ_PNP / Unknown IOCTL 0x%lx\n", Stack->MinorFunction);
611 return ForwardIrpAndForget(DeviceExtension->LowerDevice, Irp);
612 }
613
614 Irp->IoStatus.Information = Information;
615 Irp->IoStatus.Status = Status;
616 IoCompleteRequest(Irp, IO_NO_INCREMENT);
617
618 return Status;
619 }
620
621 /* EOF */