source: rtems/c/src/exec/score/src/thread.c @ 5250ff39

4.104.114.84.95
Last change on this file since 5250ff39 was 5250ff39, checked in by Joel Sherrill <joel.sherrill@…>, on 08/23/95 at 21:06:31

Moved _Thread_Information -> _RTEMS_tasks_Information.

Added a table of object information control blocks.

Modified _Thread_Get so it looks up a thread regardless of which
thread management "entity" (manager, internal, etc) actually "owns" it.

  • Property mode set to 100644
File size: 19.4 KB
Line 
1/*
2 *  Thread Handler
3 *
4 *
5 *  COPYRIGHT (c) 1989, 1990, 1991, 1992, 1993, 1994.
6 *  On-Line Applications Research Corporation (OAR).
7 *  All rights assigned to U.S. Government, 1994.
8 *
9 *  This material may be reproduced by or for the U.S. Government pursuant
10 *  to the copyright license under the clause at DFARS 252.227-7013.  This
11 *  notice must appear in all copies of this file and its derivatives.
12 *
13 *  $Id$
14 */
15
16#include <rtems/system.h>
17#include <rtems/config.h>
18#include <rtems/context.h>
19#include <rtems/fatal.h>
20#include <rtems/init.h>
21#include <rtems/intthrd.h>
22#include <rtems/isr.h>
23#include <rtems/modes.h>
24#include <rtems/object.h>
25#include <rtems/priority.h>
26#include <rtems/states.h>
27#include <rtems/thread.h>
28#include <rtems/userext.h>
29#include <rtems/wkspace.h>
30
31/*PAGE
32 *
33 *  _Thread_Handler_initialization
34 *
35 *  This routine initializes all thread manager related data structures.
36 *
37 *  Input parameters:
38 *    ticks_per_timeslice - clock ticks per quantum
39 *    maximum_proxies     - number of proxies to initialize
40 *
41 *  Output parameters:  NONE
42 */
43
44void _Thread_Handler_initialization(
45  unsigned32   ticks_per_timeslice,
46  unsigned32   maximum_proxies
47)
48{
49  unsigned32 index;
50
51  _Context_Switch_necessary = FALSE;
52  _Thread_Executing         = NULL;
53  _Thread_Heir              = NULL;
54  _Thread_Allocated_fp      = NULL;
55
56  _Thread_Ticks_remaining_in_timeslice = ticks_per_timeslice;
57  _Thread_Ticks_per_timeslice          = ticks_per_timeslice;
58
59  _Thread_Ready_chain = _Workspace_Allocate_or_fatal_error(
60    (RTEMS_MAXIMUM_PRIORITY + 1) * sizeof(Chain_Control)
61  );
62
63  for ( index=0; index <= RTEMS_MAXIMUM_PRIORITY ; index++ )
64    _Chain_Initialize_empty( &_Thread_Ready_chain[ index ] );
65
66  _Thread_MP_Handler_initialization( maximum_proxies );
67}
68
69/*PAGE
70 *
71 *  _Thread_Start_multitasking
72 *
73 *  This kernel routine readies the requested thread, the thread chain
74 *  is adjusted.  A new heir thread may be selected.
75 *
76 *  Input parameters:
77 *    system_thread - pointer to system initialization thread control block
78 *    idle_thread   - pointer to idle thread control block
79 *
80 *  Output parameters:  NONE
81 *
82 *  NOTE:  This routine uses the "blocking" heir selection mechanism.
83 *         This insures the correct heir after a thread restart.
84 *
85 *  INTERRUPT LATENCY:
86 *    ready chain
87 *    select heir
88 */
89
90void _Thread_Start_multitasking(
91  Thread_Control *system_thread,
92  Thread_Control *idle_thread
93)
94{
95
96   _Thread_Executing  =
97   _Thread_Heir       =
98   _Thread_MP_Receive = system_thread;
99
100   /*
101    *  Scheduling will not work "correctly" until the above
102    *  statements have been executed.
103    */
104
105   _Thread_Ready( system_thread );
106   _Thread_Ready( idle_thread );
107
108   _Context_Switch_necessary = FALSE;
109
110   _Context_Switch( &_Thread_BSP_context, &system_thread->Registers );
111
112}
113
114/*PAGE
115 *
116 *  _Thread_Dispatch
117 *
118 *  This kernel routine determines if a dispatch is needed, and if so
119 *  dispatches to the heir thread.  Once the heir is running an attempt
120 *  is made to dispatch any ASRs.
121 *
122 *  ALTERNATE ENTRY POINTS:
123 *    void _Thread_Enable_dispatch();
124 *
125 *  Input parameters:  NONE
126 *
127 *  Output parameters:  NONE
128 *
129 *  INTERRUPT LATENCY:
130 *    dispatch thread
131 *    no dispatch thread
132 */
133
134#if ( CPU_INLINE_ENABLE_DISPATCH == FALSE )
135void _Thread_Enable_dispatch( void )
136{
137  if ( --_Thread_Dispatch_disable_level )
138    return;
139  _Thread_Dispatch();
140}
141#endif
142
143void _Thread_Dispatch( void )
144{
145  Thread_Control  *executing;
146  Thread_Control  *heir;
147  ISR_Level               level;
148  rtems_signal_set  signal_set;
149  rtems_mode           previous_mode;
150
151  executing   = _Thread_Executing;
152  _ISR_Disable( level );
153  while ( _Context_Switch_necessary == TRUE ) {
154    heir = _Thread_Heir;
155    _Thread_Dispatch_disable_level = 1;
156    _Context_Switch_necessary = FALSE;
157    _Thread_Executing = heir;
158    _ISR_Enable( level );
159
160    _User_extensions_Task_switch( executing, heir );
161
162    _Thread_Ticks_remaining_in_timeslice = _Thread_Ticks_per_timeslice;
163
164    /*
165     *  If the CPU has hardware floating point, then we must address saving
166     *  and restoring it as part of the context switch.
167     *
168     *  The second conditional compilation section selects the algorithm used
169     *  to context switch between floating point tasks.  The deferred algorithm
170     *  can be significantly better in a system with few floating point tasks
171     *  because it reduces the total number of save and restore FP context
172     *  operations.  However, this algorithm can not be used on all CPUs due
173     *  to unpredictable use of FP registers by some compilers for integer
174     *  operations.
175     */
176
177#if ( CPU_HARDWARE_FP == TRUE )
178#if ( CPU_USE_DEFERRED_FP_SWITCH == TRUE )
179    if ( (heir->fp_context != NULL) && !_Thread_Is_allocated_fp( heir ) ) {
180      if ( _Thread_Allocated_fp != NULL )
181        _Context_Save_fp( &_Thread_Allocated_fp->fp_context );
182      _Context_Restore_fp( &heir->fp_context );
183      _Thread_Allocated_fp = heir;
184    }
185#else
186    if ( executing->fp_context != NULL )
187      _Context_Save_fp( &executing->fp_context );
188
189    if ( heir->fp_context != NULL )
190      _Context_Restore_fp( &heir->fp_context );
191#endif
192#endif
193
194    _Context_Switch( &executing->Registers, &heir->Registers );
195
196    executing = _Thread_Executing;
197    _ISR_Disable( level );
198  }
199
200  _Thread_Dispatch_disable_level = 0;
201
202  if ( _ASR_Are_signals_pending( &executing->Signal ) ) {
203    signal_set                       = executing->Signal.signals_posted;
204    executing->Signal.signals_posted = 0;
205    _ISR_Enable( level );
206
207    executing->Signal.nest_level += 1;
208    if (_Thread_Change_mode( executing->Signal.mode_set,
209                                RTEMS_ALL_MODE_MASKS, &previous_mode ))
210        _Thread_Dispatch();
211
212    (*executing->Signal.handler)( signal_set );
213
214    executing->Signal.nest_level -= 1;
215    if (_Thread_Change_mode( previous_mode,
216                                RTEMS_ALL_MODE_MASKS, &previous_mode ))
217        _Thread_Dispatch();
218  }
219  else
220    _ISR_Enable( level );
221}
222
223/*PAGE
224 *
225 *  _Thread_Ready
226 *
227 *  This kernel routine readies the requested thread, the thread chain
228 *  is adjusted.  A new heir thread may be selected.
229 *
230 *  Input parameters:
231 *    the_thread - pointer to thread control block
232 *
233 *  Output parameters:  NONE
234 *
235 *  NOTE:  This routine uses the "blocking" heir selection mechanism.
236 *         This insures the correct heir after a thread restart.
237 *
238 *  INTERRUPT LATENCY:
239 *    ready chain
240 *    select heir
241 */
242
243void _Thread_Ready(
244  Thread_Control *the_thread
245)
246{
247  ISR_Level              level;
248  Thread_Control *heir;
249
250  _ISR_Disable( level );
251
252  the_thread->current_state = STATES_READY;
253
254  _Priority_Add_to_bit_map( &the_thread->Priority_map );
255
256  _Chain_Append_unprotected( the_thread->ready, &the_thread->Object.Node );
257
258  _ISR_Flash( level );
259
260  _Thread_Calculate_heir();
261
262  heir = _Thread_Heir;
263
264  if ( !_Thread_Is_executing( heir ) &&
265        _Modes_Is_preempt( _Thread_Executing->current_modes ) )
266    _Context_Switch_necessary = TRUE;
267
268  _ISR_Enable( level );
269}
270
271/*PAGE
272 *
273 *  _Thread_Clear_state
274 *
275 *  This kernel routine clears the appropriate states in the
276 *  requested thread.  The thread ready chain is adjusted if
277 *  necessary and the Heir thread is set accordingly.
278 *
279 *  Input parameters:
280 *    the_thread - pointer to thread control block
281 *    state      - state set to clear
282 *
283 *  Output parameters:  NONE
284 *
285 *  INTERRUPT LATENCY:
286 *    priority map
287 *    select heir
288 */
289
290void _Thread_Clear_state(
291  Thread_Control *the_thread,
292  States_Control  state
293)
294{
295  ISR_Level level;
296
297  _ISR_Disable( level );
298    the_thread->current_state =
299      _States_Clear( state, the_thread->current_state );
300
301    if ( _States_Is_ready( the_thread->current_state ) ) {
302
303      _Priority_Add_to_bit_map( &the_thread->Priority_map );
304
305      _Chain_Append_unprotected( the_thread->ready, &the_thread->Object.Node );
306
307      _ISR_Flash( level );
308
309      if ( the_thread->current_priority < _Thread_Heir->current_priority ) {
310        _Thread_Heir = the_thread;
311        if ( _Modes_Is_preempt( _Thread_Executing->current_modes ) ||
312             the_thread->current_priority == 0 )
313          _Context_Switch_necessary = TRUE;
314      }
315    }
316  _ISR_Enable( level );
317}
318
319/*PAGE
320 *
321 * _Thread_Set_state
322 *
323 * This kernel routine sets the requested state in the THREAD.  The
324 * THREAD chain is adjusted if necessary.
325 *
326 * Input parameters:
327 *   the_thread   - pointer to thread control block
328 *   state - state to be set
329 *
330 * Output parameters:  NONE
331 *
332 *  INTERRUPT LATENCY:
333 *    ready chain
334 *    select map
335 */
336
337void _Thread_Set_state(
338  Thread_Control *the_thread,
339  States_Control         state
340)
341{
342  ISR_Level             level;
343  Chain_Control *ready;
344
345  ready = the_thread->ready;
346  _ISR_Disable( level );
347  if ( !_States_Is_ready( the_thread->current_state ) ) {
348    the_thread->current_state =
349       _States_Set( state, the_thread->current_state );
350    _ISR_Enable( level );
351    return;
352  }
353
354  the_thread->current_state = state;
355
356  if ( _Chain_Has_only_one_node( ready ) ) {
357
358    _Chain_Initialize_empty( ready );
359    _Priority_Remove_from_bit_map( &the_thread->Priority_map );
360
361  } else
362    _Chain_Extract_unprotected( &the_thread->Object.Node );
363
364  _ISR_Flash( level );
365
366  if ( _Thread_Is_heir( the_thread ) )
367     _Thread_Calculate_heir();
368
369  if ( _Thread_Is_executing( the_thread ) )
370    _Context_Switch_necessary = TRUE;
371
372  _ISR_Enable( level );
373}
374
375/*PAGE
376 *
377 *  _Thread_Set_transient
378 *
379 *  This kernel routine places the requested thread in the transient state
380 *  which will remove it from the ready queue, if necessary.  No
381 *  rescheduling is necessary because it is assumed that the transient
382 *  state will be cleared before dispatching is enabled.
383 *
384 *  Input parameters:
385 *    the_thread - pointer to thread control block
386 *
387 *  Output parameters:  NONE
388 *
389 *  INTERRUPT LATENCY:
390 *    only case
391 */
392
393void _Thread_Set_transient(
394  Thread_Control *the_thread
395)
396{
397  ISR_Level             level;
398  unsigned32            old_state;
399  Chain_Control *ready;
400
401  ready = the_thread->ready;
402  _ISR_Disable( level );
403
404  old_state = the_thread->current_state;
405  the_thread->current_state = _States_Set( STATES_TRANSIENT, old_state );
406
407  if ( _States_Is_ready( old_state ) ) {
408    if ( _Chain_Has_only_one_node( ready ) ) {
409
410      _Chain_Initialize_empty( ready );
411      _Priority_Remove_from_bit_map( &the_thread->Priority_map );
412
413    } else
414      _Chain_Extract_unprotected( &the_thread->Object.Node );
415  }
416
417  _ISR_Enable( level );
418
419}
420
421/*PAGE
422 *
423 *  _Thread_Reset_timeslice
424 *
425 *  This routine will remove the running thread from the ready chain
426 *  and place it immediately at the rear of this chain and then the
427 *  timeslice counter is reset.  The heir THREAD will be updated if
428 *  the running is also the currently the heir.
429 *
430 *  Input parameters:   NONE
431 *
432 *  Output parameters:  NONE
433 *
434 *  INTERRUPT LATENCY:
435 *    ready chain
436 *    select heir
437 */
438
439void _Thread_Reset_timeslice( void )
440{
441  ISR_Level              level;
442  Thread_Control *executing;
443  Chain_Control  *ready;
444
445  executing = _Thread_Executing;
446  ready     = executing->ready;
447  _ISR_Disable( level );
448    if ( _Chain_Has_only_one_node( ready ) ) {
449      _Thread_Ticks_remaining_in_timeslice = _Thread_Ticks_per_timeslice;
450      _ISR_Enable( level );
451      return;
452    }
453    _Chain_Extract_unprotected( &executing->Object.Node );
454    _Chain_Append_unprotected( ready, &executing->Object.Node );
455
456  _ISR_Flash( level );
457
458    if ( _Thread_Is_heir( executing ) )
459      _Thread_Heir = (Thread_Control *) ready->first;
460
461    _Context_Switch_necessary = TRUE;
462
463  _ISR_Enable( level );
464}
465
466/*PAGE
467 *
468 *  _Thread_Tickle_timeslice
469 *
470 *  This scheduler routine determines if timeslicing is enabled
471 *  for the currently executing thread and, if so, updates the
472 *  timeslice count and checks for timeslice expiration.
473 *
474 *  Input parameters:   NONE
475 *
476 *  Output parameters:  NONE
477 */
478
479void _Thread_Tickle_timeslice( void )
480{
481  if ( ( _Modes_Is_timeslice(_Thread_Executing->current_modes) )  &&
482       ( _States_Is_ready( _Thread_Executing->current_state ) ) &&
483       ( --_Thread_Ticks_remaining_in_timeslice == 0 ) ) {
484      _Thread_Reset_timeslice();
485  }
486}
487
488/*PAGE
489 *
490 *  _Thread_Yield_processor
491 *
492 *  This kernel routine will remove the running THREAD from the ready chain
493 *  and place it immediatly at the rear of this chain.  Reset timeslice
494 *  and yield the processor functions both use this routine, therefore if
495 *  reset is TRUE and this is the only thread on the chain then the
496 *  timeslice counter is reset.  The heir THREAD will be updated if the
497 *  running is also the currently the heir.
498 *
499 *  Input parameters:   NONE
500 *
501 *  Output parameters:  NONE
502 *
503 *  INTERRUPT LATENCY:
504 *    ready chain
505 *    select heir
506 */
507
508void _Thread_Yield_processor( void )
509{
510  ISR_Level       level;
511  Thread_Control *executing;
512  Chain_Control  *ready;
513
514  executing = _Thread_Executing;
515  ready     = executing->ready;
516  _ISR_Disable( level );
517    if ( !_Chain_Has_only_one_node( ready ) ) {
518      _Chain_Extract_unprotected( &executing->Object.Node );
519      _Chain_Append_unprotected( ready, &executing->Object.Node );
520
521      _ISR_Flash( level );
522
523      if ( _Thread_Is_heir( executing ) )
524        _Thread_Heir = (Thread_Control *) ready->first;
525      _Context_Switch_necessary = TRUE;
526    }
527    else if ( !_Thread_Is_heir( executing ) )
528      _Context_Switch_necessary = TRUE;
529
530  _ISR_Enable( level );
531}
532
533/*PAGE
534 *
535 *  _Thread_Load_environment
536 *
537 *  Load starting environment for another thread from its start area in the
538 *  thread.  Only called from t_restart and t_start.
539 *
540 *  Input parameters:
541 *    the_thread - thread control block pointer
542 *
543 *  Output parameters:  NONE
544 */
545
546void _Thread_Load_environment(
547  Thread_Control *the_thread
548)
549{
550  if ( the_thread->Start.fp_context ) {
551    the_thread->fp_context = the_thread->Start.fp_context;
552    _Context_Initialize_fp( &the_thread->fp_context );
553  }
554
555  _Context_Initialize(
556    &the_thread->Registers,
557    the_thread->Start.Initial_stack.area,
558    the_thread->Start.Initial_stack.size,
559    _Modes_Get_interrupt_level( the_thread->Start.initial_modes ),
560    _Thread_Handler
561  );
562
563}
564
565/*PAGE
566 *
567 *  _Thread_Handler
568 *
569 *  This routine is the default thread exitted error handler.  It is
570 *  returned to when a thread exits.  The configured fatal error handler
571 *  is invoked to process the exit.
572 *
573 *  Input parameters:   NONE
574 *
575 *  Output parameters:  NONE
576 */
577
578void _Thread_Handler( void )
579{
580  Thread_Control *executing;
581
582  executing = _Thread_Executing;
583
584  /*
585   * Take care that 'begin' extensions get to complete before
586   * 'switch' extensions can run.  This means must keep dispatch
587   * disabled until all 'begin' extensions complete.
588   */
589 
590  _User_extensions_Task_begin( executing );
591 
592  /*
593   *  At this point, the dispatch disable level BETTER be 1.
594   */
595
596  _Thread_Enable_dispatch();
597 
598  (*executing->Start.entry_point)( executing->Start.initial_argument );
599
600  _User_extensions_Task_exitted( executing );
601
602  rtems_fatal_error_occurred( RTEMS_TASK_EXITTED );
603}
604
605/*PAGE
606 *
607 *  _Thread_Delay_ended
608 *
609 *  This routine processes a thread whose delay period has ended.
610 *  It is called by the watchdog handler.
611 *
612 *  Input parameters:
613 *    id - thread id
614 *
615 *  Output parameters: NONE
616 */
617
618void _Thread_Delay_ended(
619  Objects_Id  id,
620  void       *ignored
621)
622{
623  Thread_Control    *the_thread;
624  Objects_Locations  location;
625
626  the_thread = _Thread_Get( id, &location );
627  switch ( location ) {
628    case OBJECTS_ERROR:
629    case OBJECTS_REMOTE:  /* impossible */
630      break;
631    case OBJECTS_LOCAL:
632      _Thread_Unblock( the_thread );
633      _Thread_Unnest_dispatch();
634      break;
635  }
636}
637
638/*PAGE
639 *
640 *  _Thread_Change_priority
641 *
642 *  This kernel routine changes the priority of the thread.  The
643 *  thread chain is adjusted if necessary.
644 *
645 *  Input parameters:
646 *    the_thread   - pointer to thread control block
647 *    new_priority - ultimate priority
648 *
649 *  Output parameters:  NONE
650 *
651 *  INTERRUPT LATENCY:
652 *    ready chain
653 *    select heir
654 */
655
656void _Thread_Change_priority(
657  Thread_Control *the_thread,
658  rtems_task_priority       new_priority
659)
660{
661  ISR_Level level;
662
663  _Thread_Set_transient( the_thread );
664
665  if ( the_thread->current_priority != new_priority )
666    _Thread_Set_priority( the_thread, new_priority );
667
668  _ISR_Disable( level );
669
670  the_thread->current_state =
671    _States_Clear( STATES_TRANSIENT, the_thread->current_state );
672
673  if ( ! _States_Is_ready( the_thread->current_state ) ) {
674    _ISR_Enable( level );
675    return;
676  }
677
678  _Priority_Add_to_bit_map( &the_thread->Priority_map );
679  _Chain_Append_unprotected( the_thread->ready, &the_thread->Object.Node );
680
681  _ISR_Flash( level );
682
683  _Thread_Calculate_heir();
684
685  if ( !_Thread_Is_executing_also_the_heir() &&
686       _Modes_Is_preempt(_Thread_Executing->current_modes) )
687    _Context_Switch_necessary = TRUE;
688
689  _ISR_Enable( level );
690}
691
692/*PAGE
693 *
694 * _Thread_Set_priority
695 *
696 * This directive enables and disables several modes of
697 * execution for the requesting thread.
698 *
699 *  Input parameters:
700 *    the_thread   - pointer to thread priority
701 *    new_priority - new priority
702 *
703 *  Output: NONE
704 */
705
706void _Thread_Set_priority(
707  Thread_Control *the_thread,
708  rtems_task_priority       new_priority
709)
710{
711  the_thread->current_priority = new_priority;
712  the_thread->ready            = &_Thread_Ready_chain[ new_priority ];
713
714  _Priority_Initialize_information( &the_thread->Priority_map, new_priority );
715}
716
717/*PAGE
718 *
719 *  _Thread_Change_mode
720 *
721 *  This routine enables and disables several modes of
722 *  execution for the requesting thread.
723 *
724 *  Input parameters:
725 *    mode         - new mode
726 *    mask         - mask
727 *    old_mode_set - address of previous mode
728 *
729 *  Output:
730 *    *old_mode_set - previous mode
731 *     returns TRUE if scheduling necessary
732 *
733 *  INTERRUPT LATENCY:
734 *    only one case
735 */
736
737boolean _Thread_Change_mode(
738  unsigned32  new_mode_set,
739  unsigned32  mask,
740  unsigned32 *old_mode_set
741)
742{
743  rtems_mode   changed;
744  rtems_mode   threads_new_mode_set;
745  Thread_Control *executing;
746  boolean         need_dispatch;
747
748  executing     = _Thread_Executing;
749  *old_mode_set = executing->current_modes;
750
751  _Modes_Change( executing->current_modes,
752                   new_mode_set, mask, &threads_new_mode_set, &changed );
753
754  _Modes_Set_interrupt_level( threads_new_mode_set );
755
756  if ( _Modes_Mask_changed( changed, RTEMS_ASR_MASK ) )
757    _ASR_Swap_signals( &executing->Signal );
758
759  executing->current_modes = threads_new_mode_set;
760  need_dispatch = TRUE;
761
762  if ( !_States_Is_ready( executing->current_state ) ||
763       ( !_Thread_Is_heir( executing ) &&
764         _Modes_Is_preempt(threads_new_mode_set) ) )
765
766     _Context_Switch_necessary = TRUE;
767
768  else if ( !_ASR_Are_signals_pending( &executing->Signal ) )
769
770    need_dispatch = FALSE;
771
772  return need_dispatch;
773}
774
775/*PAGE
776 *
777 *  _Thread_Get
778 *
779 *  NOTE:  If we are not using static inlines, this must be a real
780 *         subroutine call.
781 *
782 *  NOTE:  XXX... This routine may be able to be optimized.
783 */
784
785#ifndef USE_INLINES
786
787STATIC INLINE Thread_Control *_Thread_Get (
788  Objects_Id           id,
789  Objects_Locations   *location
790)
791{
792  Objects_Classes      the_class;
793  Objects_Information *information;
794 
795  if ( _Objects_Are_ids_equal( id, OBJECTS_ID_OF_SELF ) ) {
796    _Thread_Disable_dispatch();
797    *location = OBJECTS_LOCAL;
798    return( _Thread_Executing );
799  }
800 
801  the_class = rtems_get_class( id );
802 
803  if ( the_class > OBJECTS_CLASSES_LAST ) {
804    *location = OBJECTS_ERROR;
805    return (Thread_Control *) 0;
806  }
807 
808  information = _Objects_Information_table[ the_class ];
809 
810  if ( !information || !information->is_thread ) {
811    *location = OBJECTS_ERROR;
812    return (Thread_Control *) 0;
813  }
814 
815  return (Thread_Control *) _Objects_Get( information, id, location );
816}
817#endif
Note: See TracBrowser for help on using the repository browser.