ProcessorScheduler.st
branchjv
changeset 23073 7e7d5e29738c
parent 23072 0402b3e0d43b
parent 21387 e3865533e6a6
child 23074 07a6d8d23ffd
equal deleted inserted replaced
23072:0402b3e0d43b 23073:7e7d5e29738c
   575     ^ currentPriority
   575     ^ currentPriority
   576 
   576 
   577     "Processor currentPriority"
   577     "Processor currentPriority"
   578 !
   578 !
   579 
   579 
       
   580 exitWhenNoMoreUserProcesses:aBoolean
       
   581     "set/clear the flag, which controls if the scheduler should exit and return
       
   582      when the last user process finishes (and therefore exit the smalltalk system).
       
   583      A userProcess is defined as a process with a non-zero processGroup.
       
   584      This flag is typically set for standAlone operation, to terminate the (Unix-)
       
   585      process, when the last thread terminates."
       
   586 
       
   587     exitWhenNoMoreUserProcesses := aBoolean
       
   588 !
       
   589 
   580 interruptCounter
   590 interruptCounter
   581     "for statistics: counts the overall number of interrupts"
   591     "for statistics: counts the overall number of interrupts"
   582 
   592 
   583     ^ interruptCounter
   593     ^ interruptCounter
   584 
   594 
   896 
   906 
   897     'Processor [info]: finish dispatch (no more processes)' infoPrintCR.
   907     'Processor [info]: finish dispatch (no more processes)' infoPrintCR.
   898 
   908 
   899     "Modified: / 23-09-1996 / 14:19:56 / stefan"
   909     "Modified: / 23-09-1996 / 14:19:56 / stefan"
   900     "Modified: / 20-07-2012 / 18:34:48 / cg"
   910     "Modified: / 20-07-2012 / 18:34:48 / cg"
   901 !
       
   902 
       
   903 exitWhenNoMoreUserProcesses:aBoolean
       
   904     "set/clear the flag, which controls if the scheduler should exit and return
       
   905      when the last user process finishes (and therefore exit the smalltalk system).
       
   906      A userProcess is defined as a process with a non-zero processGroup.
       
   907      This flag is typically set for standAlone operation, to terminate the (Unix-)
       
   908      process, when the last thread terminates."
       
   909 
       
   910     exitWhenNoMoreUserProcesses := aBoolean
       
   911 ! !
   911 ! !
   912 
   912 
   913 !ProcessorScheduler methodsFor:'initialization'!
   913 !ProcessorScheduler methodsFor:'initialization'!
   914 
   914 
   915 initialize
   915 initialize
  1206      within the block.
  1206      within the block.
  1207      ActionBlock will be called with an OSProcessStatus as arg if the
  1207      ActionBlock will be called with an OSProcessStatus as arg if the
  1208      status of the OS process changes (e.g. the process terminates).
  1208      status of the OS process changes (e.g. the process terminates).
  1209      The method returns the value from aBlockReturningPid (i.e. a pid or nil)."
  1209      The method returns the value from aBlockReturningPid (i.e. a pid or nil)."
  1210 
  1210 
  1211     |pid wasBlocked|
  1211     |pid wasBlocked exitStatus|
  1212 
  1212 
  1213     "/ aBlock will be evaluated:
  1213     "/ aBlock will be evaluated:
  1214     "/   on unix: as soon as a SIGCHLD interrupt for pid has been received.
  1214     "/   on unix: as soon as a SIGCHLD interrupt for pid has been received.
  1215     "/   on win:  as soon as a select for the pid handle returns
  1215     "/   on win:  as soon as a select for the pid handle returns
  1216 
  1216 
  1217     OperatingSystem enableChildSignalInterrupts.        "/ no-op in windows
  1217     OperatingSystem enableChildSignalInterrupts.        "/ no-op in windows
  1218     wasBlocked := OperatingSystem blockInterrupts.
  1218     wasBlocked := OperatingSystem blockInterrupts.
  1219     "/ start the OS-Process
  1219     "/ start the OS-Process
  1220     pid := aBlockReturningPid value.
  1220     pid := aBlockReturningPid value.
  1221     pid notNil ifTrue:[
  1221     pid notNil ifTrue:[
  1222 	osChildExitActions at:pid put:actionBlock.
  1222         osChildExitActions at:pid put:actionBlock.
       
  1223     ].
       
  1224     "check for a race, that SIGCHILD was received before we could register the actionBlock"
       
  1225     exitStatus := OperatingSystem childProcessWait:false pid:pid.
       
  1226     exitStatus notNil ifTrue:[
       
  1227         self unmonitorPid:pid.
       
  1228         wasBlocked ifFalse:[OperatingSystem unblockInterrupts].
       
  1229         actionBlock value:exitStatus.
  1223     ].
  1230     ].
  1224     wasBlocked ifFalse:[OperatingSystem unblockInterrupts].
  1231     wasBlocked ifFalse:[OperatingSystem unblockInterrupts].
  1225     ^ pid
  1232     ^ pid
  1226 
  1233 
  1227     "Created: / 25.3.1997 / 10:54:56 / stefan"
  1234     "Created: / 25.3.1997 / 10:54:56 / stefan"
  1346                self terminateNoSignal:p.
  1353                self terminateNoSignal:p.
  1347             ]
  1354             ]
  1348         ]
  1355         ]
  1349     ].
  1356     ].
  1350     zombie notNil ifTrue:[
  1357     zombie notNil ifTrue:[
       
  1358         "delayed processing of terminated process (see #terminateNoSignal)"
  1351         self class threadDestroy:zombie.
  1359         self class threadDestroy:zombie.
  1352         zombie := nil
  1360         zombie := nil
  1353     ].
  1361     ].
  1354     wasBlocked ifFalse:[OperatingSystem unblockInterrupts].
  1362     wasBlocked ifFalse:[OperatingSystem unblockInterrupts].
  1355 
  1363 
  1356     "Modified: / 23-07-2010 / 10:32:11 / cg"
  1364     "Modified: / 23-07-2010 / 10:32:11 / cg"
       
  1365     "Modified (format): / 24-01-2017 / 17:50:12 / stefan"
  1357 ! !
  1366 ! !
  1358 
  1367 
  1359 !ProcessorScheduler methodsFor:'priority constants'!
  1368 !ProcessorScheduler methodsFor:'priority constants'!
  1360 
  1369 
  1361 highIOPriority
  1370 highIOPriority
  1731     ^ process.
  1740     ^ process.
  1732 
  1741 
  1733     "
  1742     "
  1734 	Processor processWithId:4
  1743 	Processor processWithId:4
  1735 	Processor processWithId:4711
  1744 	Processor processWithId:4711
       
  1745     "
       
  1746 !
       
  1747 
       
  1748 processesWithGroupId:anInteger
       
  1749     "answer a collection of processes with processGroupId, anInteger"
       
  1750 
       
  1751     |wasBlocked coll|
       
  1752 
       
  1753     coll := OrderedCollection new.
       
  1754 
       
  1755     wasBlocked := OperatingSystem blockInterrupts.
       
  1756     KnownProcesses validElementsDo:[:eachProcess| 
       
  1757         eachProcess processGroupId = anInteger ifTrue:[
       
  1758             coll add:eachProcess.
       
  1759         ].
       
  1760     ].
       
  1761 
       
  1762     wasBlocked ifFalse:[
       
  1763         OperatingSystem unblockInterrupts.
       
  1764     ].
       
  1765 
       
  1766     ^ coll.
       
  1767 
       
  1768     "
       
  1769         Processor processesWithGroupId:0
       
  1770         Processor processesWithGroupId:4711
  1736     "
  1771     "
  1737 ! !
  1772 ! !
  1738 
  1773 
  1739 !ProcessorScheduler methodsFor:'scheduling'!
  1774 !ProcessorScheduler methodsFor:'scheduling'!
  1740 
  1775 
  2686     "arrange for a semaphore to be triggered when input on aStream arrives.
  2721     "arrange for a semaphore to be triggered when input on aStream arrives.
  2687      This will do a select, if the OS supports selecting on that filedescriptor,
  2722      This will do a select, if the OS supports selecting on that filedescriptor,
  2688      otherwise, it will be polled every few milliseconds (MSDOS)."
  2723      otherwise, it will be polled every few milliseconds (MSDOS)."
  2689 
  2724 
  2690     aStream canBeSelected ifTrue:[
  2725     aStream canBeSelected ifTrue:[
  2691 	"/ can this stream be selected on ?
  2726         "/ can this stream be selected on ?
  2692 	self signal:aSemaphore onInput:aStream fileDescriptor orCheck:nil
  2727         self signal:aSemaphore onInput:aStream fileHandle orCheck:nil
  2693     ] ifFalse:[
  2728     ] ifFalse:[
  2694 	"/ nope - must poll ...
  2729         "/ nope - must poll ...
  2695 	self signal:aSemaphore onInput:nil orCheck:[aStream canReadWithoutBlocking]
  2730         self signal:aSemaphore onInput:nil orCheck:[aStream canReadWithoutBlocking]
  2696     ]
  2731     ]
  2697 
  2732 
  2698     "Modified: / 14.12.1999 / 23:58:50 / cg"
  2733     "Modified: / 14.12.1999 / 23:58:50 / cg"
  2699 !
  2734 !
  2700 
  2735 
  2787     "arrange for a semaphore to be triggered when output on aStream is possible.
  2822     "arrange for a semaphore to be triggered when output on aStream is possible.
  2788      This will do a select, if the OS supports selecting on that filedescriptor,
  2823      This will do a select, if the OS supports selecting on that filedescriptor,
  2789      otherwise, it will be polled every few milliseconds (MSDOS)."
  2824      otherwise, it will be polled every few milliseconds (MSDOS)."
  2790 
  2825 
  2791     aStream canBeSelected ifTrue:[
  2826     aStream canBeSelected ifTrue:[
  2792 	"/ can this stream be selected on ?
  2827         "/ can this stream be selected on ?
  2793 	self signal:aSemaphore onOutput:aStream fileDescriptor orCheck:nil
  2828         self signal:aSemaphore onOutput:aStream fileHandle orCheck:nil
  2794     ] ifFalse:[
  2829     ] ifFalse:[
  2795 	"/ nope - must poll ...
  2830         "/ nope - must poll ...
  2796 	self signal:aSemaphore onOutput:nil orCheck:[aStream canWriteWithoutBlocking]
  2831         self signal:aSemaphore onOutput:nil orCheck:[aStream canWriteWithoutBlocking]
  2797     ]
  2832     ]
  2798 
  2833 
  2799     "Modified: / 14.12.1999 / 23:59:19 / cg"
  2834     "Modified: / 14.12.1999 / 23:59:19 / cg"
  2800 ! !
  2835 ! !
  2801 
  2836 
  3404 
  3439 
  3405     wasBlocked := OperatingSystem unblockInterrupts.
  3440     wasBlocked := OperatingSystem unblockInterrupts.
  3406 
  3441 
  3407     newProcessMaybeReady := false.
  3442     newProcessMaybeReady := false.
  3408     readableResultFdArray size < readFdArray size ifTrue:[
  3443     readableResultFdArray size < readFdArray size ifTrue:[
  3409 	readableResultFdArray := Array new:(40 max:readFdArray size).
  3444         readableResultFdArray := Array new:(40 max:readFdArray size).
  3410     ].
  3445     ].
  3411     writableResultFdArray size < writeFdArray size ifTrue:[
  3446     writableResultFdArray size < writeFdArray size ifTrue:[
  3412 	writableResultFdArray := Array new:(40 max:writeFdArray size).
  3447         writableResultFdArray := Array new:(40 max:writeFdArray size).
  3413     ].
  3448     ].
  3414 
  3449 
  3415     exceptArray := exceptFdArray.
  3450     exceptArray := exceptFdArray.
  3416 
  3451 
  3417     OperatingSystem isMSWINDOWSlike ifTrue:[
  3452     OperatingSystem isMSWINDOWSlike ifTrue:[
  3418 	"/
  3453         "/
  3419 	"/ win32 does a WaitForMultipleObjects in select...
  3454         "/ win32 does a WaitForMultipleObjects in select...
  3420 	"/ unix waits for SIGCHLD
  3455         "/ unix waits for SIGCHLD
  3421 	"/
  3456         "/
  3422 	|hasPids|
  3457         |hasPids|
  3423 
  3458 
  3424 	hasPids := false.
  3459         hasPids := false.
  3425 	osChildExitActions keysDo:[:eachPid|
  3460         osChildExitActions keysDo:[:eachPid|
  3426 	    eachPid address = 0 ifTrue:[
  3461             eachPid address = 0 ifTrue:[
  3427 		'Processor: remove 0-handle pid: ' infoPrint. eachPid infoPrintCR.
  3462                 Logger warning:'Processor: remove 0-handle pid: %1' with:eachPid.
  3428 		osChildExitActions safeRemoveKey:eachPid.
  3463                 osChildExitActions safeRemoveKey:eachPid.
  3429 	    ] ifFalse:[
  3464             ] ifFalse:[
  3430 		hasPids := true.
  3465                 hasPids := true.
  3431 	    ].
  3466             ].
  3432 	].
  3467         ].
  3433 	hasPids ifTrue:[
  3468         hasPids ifTrue:[
  3434 	    exceptArray := (exceptArray upTo:nil), osChildExitActions keys asArray.
  3469             exceptArray := (exceptArray upTo:nil), osChildExitActions keys asArray.
  3435 "/'exceptArray: ' print. exceptArray printCR.
  3470 "/'exceptArray: ' print. exceptArray printCR.
  3436 	].
  3471         ].
  3437     ].
  3472     ].
  3438 
  3473 
  3439     exceptResultFdArray size < exceptArray size ifTrue:[
  3474     exceptResultFdArray size < exceptArray size ifTrue:[
  3440 	exceptResultFdArray := Array new:(40 max:exceptArray size).
  3475         exceptResultFdArray := Array new:(40 max:exceptArray size).
  3441     ].
  3476     ].
  3442 
  3477 
  3443     nReady := OperatingSystem
  3478     nReady := OperatingSystem
  3444 		selectOnAnyReadable:readFdArray
  3479                 selectOnAnyReadable:readFdArray
  3445 		writable:writeFdArray
  3480                 writable:writeFdArray
  3446 		exception:exceptArray
  3481                 exception:exceptArray
  3447 		readableInto:readableResultFdArray
  3482                 readableInto:readableResultFdArray
  3448 		writableInto:writableResultFdArray
  3483                 writableInto:writableResultFdArray
  3449 		exceptionInto:exceptResultFdArray
  3484                 exceptionInto:exceptResultFdArray
  3450 		withTimeOut:millis.
  3485                 withTimeOut:millis.
  3451 
  3486 
  3452     wasBlocked ifTrue:[
  3487     wasBlocked ifTrue:[
  3453 	OperatingSystem blockInterrupts.
  3488         OperatingSystem blockInterrupts.
  3454     ].
  3489     ].
  3455 
  3490 
  3456     nReady <= 0 ifTrue:[
  3491     nReady <= 0 ifTrue:[
  3457 	"/ either still nothing to do,
  3492         "/ either still nothing to do,
  3458 	"/ or error (which should not happen)
  3493         "/ or error (which should not happen)
  3459 
  3494 
  3460 	(nReady < 0 and:[(err := OperatingSystem lastErrorSymbol) notNil]) ifTrue:[
  3495         (nReady < 0 and:[(err := OperatingSystem lastErrorSymbol) notNil]) ifTrue:[
  3461 	    err == #EBADF ifTrue:[
  3496             err == #EBADF ifTrue:[
  3462 		"/ mhmh - one of the fd's given to me is corrupt.
  3497                 "/ mhmh - one of the fd's given to me is corrupt.
  3463 		"/ find out which one .... and remove it
  3498                 "/ find out which one .... and remove it
  3464 		self removeCorruptedFds
  3499                 self removeCorruptedFds
  3465 	    ] ifFalse:[
  3500             ] ifFalse:[
  3466 		err == #ENOENT ifTrue:[
  3501                 err == #ENOENT ifTrue:[
  3467 		    'Processor [warning]: ENOENT in select; rd=' infoPrint.
  3502                     'Processor [warning]: ENOENT in select; rd=' infoPrint.
  3468 		    readFdArray infoPrint. ' wr=' infoPrint. writeFdArray infoPrintCR.
  3503                     readFdArray infoPrint. ' wr=' infoPrint. writeFdArray infoPrintCR.
  3469 		] ifFalse:[
  3504                 ] ifFalse:[
  3470 		    'Processor [warning]: error in select: ' infoPrint. err infoPrintCR.
  3505                     'Processor [warning]: error in select: ' infoPrint. err infoPrintCR.
  3471 		]
  3506                 ]
  3472 	    ].
  3507             ].
  3473 	]
  3508         ]
  3474     ] ifFalse:[
  3509     ] ifFalse:[
  3475 	readyIndex := 1.
  3510         readyIndex := 1.
  3476 	[nReady > 0
  3511         [nReady > 0
  3477 	     and:[ readyIndex <= readableResultFdArray size
  3512              and:[ readyIndex <= readableResultFdArray size
  3478 	     and:[ (fd := readableResultFdArray at:readyIndex) notNil ]]
  3513              and:[ (fd := readableResultFdArray at:readyIndex) notNil ]]
  3479 	] whileTrue:[
  3514         ] whileTrue:[
  3480 	    index := readFdArray identityIndexOf:fd.
  3515             index := readFdArray identityIndexOf:fd.
  3481 	    index ~~ 0 ifTrue:[
  3516             index ~~ 0 ifTrue:[
  3482 		action := readCheckArray at:index.
  3517                 action := readCheckArray at:index.
  3483 		sema := readSemaphoreArray at:index.
  3518                 sema := readSemaphoreArray at:index.
  3484 		sema notNil ifTrue:[
  3519                 sema notNil ifTrue:[
  3485 		    sema signalOnce.
  3520                     sema signalOnce.
  3486 		    newProcessMaybeReady := true.
  3521                     newProcessMaybeReady := true.
  3487 		    action isNil ifTrue:[
  3522                     action isNil ifTrue:[
  3488 			"before May 2014 we disabled the sema in the caller after wakeup.
  3523                         "before May 2014 we disabled the sema in the caller after wakeup.
  3489 			 This caused ST/X to consume 100% cpu, when the caller didn't read
  3524                          This caused ST/X to consume 100% cpu, when the caller didn't read
  3490 			 the data (e.g. because his process was stopped)."
  3525                          the data (e.g. because his process was stopped)."
  3491 			"disable possible write side and timeouts as well"
  3526                         "disable possible write side and timeouts as well"
  3492 			self disableSemaphore:sema.
  3527                         self disableSemaphore:sema.
  3493 		    ].
  3528                     ].
  3494 		].
  3529                 ].
  3495 		(action notNil and:[action value]) ifTrue:[
  3530                 (action notNil and:[action value]) ifTrue:[
  3496 		    newProcessMaybeReady := true.
  3531                     newProcessMaybeReady := true.
  3497 		].
  3532                 ].
  3498 	    ].
  3533             ].
  3499 	    nReady := nReady - 1.
  3534             nReady := nReady - 1.
  3500 	    readyIndex := readyIndex + 1.
  3535             readyIndex := readyIndex + 1.
  3501 	].
  3536         ].
  3502 
  3537 
  3503 	readyIndex := 1.
  3538         readyIndex := 1.
  3504 	[nReady > 0
  3539         [nReady > 0
  3505 	     and:[ readyIndex <= writableResultFdArray size
  3540              and:[ readyIndex <= writableResultFdArray size
  3506 	     and:[ (fd := writableResultFdArray at:readyIndex) notNil ]]
  3541              and:[ (fd := writableResultFdArray at:readyIndex) notNil ]]
  3507 	] whileTrue:[
  3542         ] whileTrue:[
  3508 	    index := writeFdArray identityIndexOf:fd.
  3543             index := writeFdArray identityIndexOf:fd.
  3509 	    index ~~ 0 ifTrue:[
  3544             index ~~ 0 ifTrue:[
  3510 		action := writeCheckArray at:index.
  3545                 action := writeCheckArray at:index.
  3511 		sema := writeSemaphoreArray at:index.
  3546                 sema := writeSemaphoreArray at:index.
  3512 		sema notNil ifTrue:[
  3547                 sema notNil ifTrue:[
  3513 		    sema signalOnce.
  3548                     sema signalOnce.
  3514 		    newProcessMaybeReady := true.
  3549                     newProcessMaybeReady := true.
  3515 		    action isNil ifTrue:[
  3550                     action isNil ifTrue:[
  3516 			"now this is a one shot operation - see the input above"
  3551                         "now this is a one shot operation - see the input above"
  3517 			"disable possible read side and timeouts as well"
  3552                         "disable possible read side and timeouts as well"
  3518 			self disableSemaphore:sema.
  3553                         self disableSemaphore:sema.
  3519 		    ].
  3554                     ].
  3520 		].
  3555                 ].
  3521 		(action notNil and:[action value]) ifTrue:[
  3556                 (action notNil and:[action value]) ifTrue:[
  3522 		    newProcessMaybeReady := true.
  3557                     newProcessMaybeReady := true.
  3523 		].
  3558                 ].
  3524 	    ].
  3559             ].
  3525 	    nReady := nReady - 1.
  3560             nReady := nReady - 1.
  3526 	    readyIndex := readyIndex + 1.
  3561             readyIndex := readyIndex + 1.
  3527 	].
  3562         ].
  3528 
  3563 
  3529 "/'except result got: ' print. exceptArray printCR. exceptResultFdArray printCR.
  3564 "/'except result got: ' print. exceptArray printCR. exceptResultFdArray printCR.
  3530 	readyIndex := 1.
  3565         readyIndex := 1.
  3531 	[nReady > 0
  3566         [nReady > 0
  3532 	     and:[ readyIndex <= exceptResultFdArray size
  3567              and:[ readyIndex <= exceptResultFdArray size
  3533 	     and:[ (fdOrPid := exceptResultFdArray at:readyIndex) notNil ]]
  3568              and:[ (fdOrPid := exceptResultFdArray at:readyIndex) notNil ]]
  3534 	] whileTrue:[
  3569         ] whileTrue:[
  3535 "/'except got: ' print. fdOrPid printCR.
  3570 "/'except got: ' print. fdOrPid printCR.
  3536 	    index := exceptFdArray identityIndexOf:fdOrPid.
  3571             index := exceptFdArray identityIndexOf:fdOrPid.
  3537 	    index ~~ 0 ifTrue:[
  3572             index ~~ 0 ifTrue:[
  3538 		sema := exceptSemaphoreArray at:index.
  3573                 sema := exceptSemaphoreArray at:index.
  3539 		sema notNil ifTrue:[
  3574                 sema notNil ifTrue:[
  3540 		    sema signalOnce.
  3575                     sema signalOnce.
  3541 		    newProcessMaybeReady := true.
  3576                     newProcessMaybeReady := true.
  3542 		    "disable possible read/write side and timeouts as well"
  3577                     "disable possible read/write side and timeouts as well"
  3543 		    self disableSemaphore:sema.
  3578                     self disableSemaphore:sema.
  3544 		].
  3579                 ].
  3545 	    ] ifFalse:[ "may be a PID?"
  3580             ] ifFalse:[ "may be a PID?"
  3546 		|osProcessStatus actionBlock|
  3581                 |osProcessStatus actionBlock|
  3547 
  3582 
  3548 		actionBlock := osChildExitActions removeKey:fdOrPid ifAbsent:nil.
  3583                 actionBlock := osChildExitActions removeKey:fdOrPid ifAbsent:nil.
  3549 "/'pid signaled: ' print. fdOrPid printCR.
  3584 "/'pid signaled: ' print. fdOrPid printCR.
  3550 		actionBlock notNil ifTrue:[
  3585                 actionBlock notNil ifTrue:[
  3551 		    osProcessStatus := OperatingSystem childProcessWait:false pid:fdOrPid.
  3586                     osProcessStatus := OperatingSystem childProcessWait:false pid:fdOrPid.
  3552 		    (osProcessStatus notNil and:[osProcessStatus pid = fdOrPid]) ifTrue:[
  3587                     (osProcessStatus notNil and:[osProcessStatus pid = fdOrPid]) ifTrue:[
  3553 			actionBlock value:osProcessStatus.
  3588                         actionBlock value:osProcessStatus.
  3554 			newProcessMaybeReady := true.
  3589                         newProcessMaybeReady := true.
  3555 		    ].
  3590                     ].
  3556 		].
  3591                 ].
  3557 	    ].
  3592             ].
  3558 	    nReady := nReady - 1.
  3593             nReady := nReady - 1.
  3559 	    readyIndex := readyIndex + 1.
  3594             readyIndex := readyIndex + 1.
  3560 	].
  3595         ].
  3561     ].
  3596     ].
  3562     ^ newProcessMaybeReady
  3597     ^ newProcessMaybeReady
  3563 
  3598 
  3564     "Modified: / 12-04-1996 / 09:31:22 / stefan"
  3599     "Modified: / 12-04-1996 / 09:31:22 / stefan"
  3565     "Modified: / 07-12-2006 / 19:48:17 / cg"
  3600     "Modified: / 07-12-2006 / 19:48:17 / cg"