]> asedeno.scripts.mit.edu Git - 1ts-debian.git/blob - zephyr/server/main.c
r4275@bucket (orig r265): kcr | 2008-01-21 02:57:32 -0500
[1ts-debian.git] / zephyr / server / main.c
1 /* This file is part of the Project Athena Zephyr Notification System.
2  * It contains the main loop of the Zephyr server
3  *
4  *      Created by:     John T. Kohl
5  *
6  *      $Source: /afs/dev.mit.edu/source/repository/athena/lib/zephyr/server/main.c,v $
7  *      $Author$
8  *
9  *      Copyright (c) 1987,1988,1991 by the Massachusetts Institute of Technology.
10  *      For copying and distribution information, see the file
11  *      "mit-copyright.h". 
12  */
13
14 #include <zephyr/mit-copyright.h>
15 #include "zserver.h"
16 #include <sys/socket.h>
17 #include <sys/resource.h>
18
19 #ifndef lint
20 #ifndef SABER
21 static const char rcsid_main_c[] =
22     "$Id$";
23 #endif
24 #endif
25
26 /*
27  * Server loop for Zephyr.
28  */
29
30 /*
31   The Zephyr server maintains several linked lists of information.
32
33   There is an array of servers (otherservers) initialized and maintained
34   by server_s.c.
35
36   Each server descriptor contains a pointer to a linked list of hosts
37   which are ``owned'' by that server.  The first server is the ``limbo''
38   server which owns any host which was formerly owned by a dead server.
39
40   Each of these host list entries has an IP address and a pointer to a
41   linked list of clients on that host.
42
43   Each client has a sockaddr_in, a list of subscriptions, and possibly
44   a session key.
45
46   In addition, the class manager has copies of the pointers to the
47   clients which are registered with a particular class, the
48   not-yet-acknowledged list has copies of pointers to some clients,
49   and the hostm manager may have copies of pointers to some clients
50   (if the client has not acknowledged a packet after a given timeout).
51 */
52
53 #define EVER            (;;)            /* don't stop looping */
54
55 static int do_net_setup(void);
56 static int initialize(void);
57 static void usage(void);
58 static void do_reset(void);
59 static RETSIGTYPE bye(int);
60 static RETSIGTYPE dbug_on(int);
61 static RETSIGTYPE dbug_off(int);
62 static RETSIGTYPE sig_dump_db(int);
63 static RETSIGTYPE reset(int);
64 static RETSIGTYPE reap(int);
65 static void read_from_dump(char *dumpfile);
66 static void dump_db(void);
67 static void dump_strings(void);
68
69 #ifndef DEBUG
70 static void detach(void);
71 #endif
72
73 static short doreset = 0;               /* if it becomes 1, perform
74                                            reset functions */
75
76 int nfds;                               /* max file descriptor for select() */
77 int srv_socket;                         /* dgram socket for clients
78                                            and other servers */
79 int bdump_socket = -1;                  /* brain dump socket fd
80                                            (closed most of the time) */
81 fd_set interesting;                     /* the file descrips we are listening
82                                            to right now */
83 struct sockaddr_in srv_addr;            /* address of the socket */
84
85 Unacked *nacklist = NULL;               /* list of packets waiting for ack's */
86
87 unsigned short hm_port;                 /* host manager receiver port */
88 unsigned short hm_srv_port;             /* host manager server sending port */
89
90 char *programname;                      /* set to the basename of argv[0] */
91 char myname[MAXHOSTNAMELEN];            /* my host name */
92
93 char list_file[128];
94 #ifdef HAVE_KRB5
95 char keytab_file[128];
96 static char tkt5_file[256];
97 #endif
98 #ifdef HAVE_KRB4
99 char srvtab_file[128];
100 char my_realm[REALM_SZ];
101 static char tkt_file[128];
102 #endif
103 char acl_dir[128];
104 char subs_file[128];
105
106 int zdebug;
107 #ifdef DEBUG_MALLOC
108 int dump_malloc_stats = 0;
109 unsigned long m_size;
110 #endif
111 #ifdef DEBUG
112 int zalone;
113 #endif
114
115 struct timeval t_local;                 /* store current time for other uses */
116
117 static int dump_db_flag = 0;
118 static int dump_strings_flag = 0;
119
120 u_long npackets;                        /* number of packets processed */
121 time_t uptime;                          /* when we started operations */
122 static int nofork;
123 struct in_addr my_addr;
124 char *bdump_version = "1.2";
125
126 #ifdef HAVE_KRB5
127 int bdump_auth_proto = 5;
128 #else /* HAVE_KRB5 */
129 #ifdef HAVE_KRB4
130 int bdump_auth_proto = 4;
131 #else /* HAVE_KRB4 */
132 int bdump_auth_proto = 0;
133 #endif /* HAVE_KRB4 */
134 #endif /* HAVE_KRB5 */
135
136 #ifdef HAVE_KRB5
137 krb5_ccache Z_krb5_ccache;
138 krb5_keyblock *__Zephyr_keyblock;
139 #else
140 #ifdef HAVE_KRB4
141 C_Block __Zephyr_session;
142 #endif
143 #endif
144
145 int
146 main(int argc,
147      char **argv)
148 {
149     int nfound;                 /* #fildes ready on select */
150     fd_set readable;
151     struct timeval tv;
152     int init_from_dump = 0;
153     char *dumpfile;
154 #ifdef _POSIX_VERSION
155     struct sigaction action;
156 #endif
157     int optchar;                        /* option processing */
158     extern char *optarg;
159     extern int optind;
160
161     sprintf(list_file, "%s/zephyr/%s", SYSCONFDIR, SERVER_LIST_FILE);
162 #ifdef HAVE_KRB4
163     sprintf(srvtab_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_SRVTAB);
164     sprintf(tkt_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_TKFILE);
165 #endif
166 #ifdef HAVE_KRB5
167     sprintf(keytab_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_KEYTAB);
168     sprintf(tkt5_file, "FILE:%s/zephyr/%s", SYSCONFDIR, ZEPHYR_TK5FILE);
169 #endif
170     sprintf(acl_dir, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_ACL_DIR);
171     sprintf(subs_file, "%s/zephyr/%s", SYSCONFDIR, DEFAULT_SUBS_FILE);
172
173     /* set name */
174     programname = strrchr(argv[0],'/');
175     programname = (programname) ? programname + 1 : argv[0];
176
177     /* process arguments */
178     while ((optchar = getopt(argc, argv, "dsnv4f:k:")) != EOF) {
179         switch(optchar) {
180           case 'd':
181             zdebug = 1;
182             break;
183 #ifdef DEBUG
184           case 's':
185             zalone = 1;
186             break;
187 #endif
188           case 'n':
189             nofork = 1;
190             break;
191           case 'k':
192 #ifdef HAVE_KRB4
193             strncpy(my_realm, optarg, REALM_SZ);
194 #endif
195             break;
196           case 'v':
197             bdump_version = optarg;
198             break;
199           case 'f':
200             init_from_dump = 0;
201             dumpfile = optarg;
202             break;
203         case '4':
204             bdump_auth_proto = 4;
205             break;
206           case '?':
207           default:
208             usage();
209             /*NOTREACHED*/
210         }
211     }
212
213 #ifdef HAVE_KRB4
214     /* if there is no readable srvtab and we are not standalone, there
215        is no possible way we can succeed, so we exit */
216
217     if (access(srvtab_file, R_OK)
218 #ifdef DEBUG            
219         && !zalone
220 #endif /* DEBUG */
221         ) {
222         fprintf(stderr, "NO ZEPHYR SRVTAB (%s) available; exiting\n",
223                 srvtab_file);
224         exit(1);
225     }
226     /* Use local realm if not specified on command line. */
227     if (!*my_realm) {
228         if (krb_get_lrealm(my_realm, 1) != KSUCCESS) {
229             fputs("Couldn't get local Kerberos realm; exiting.\n", stderr);
230             exit(1);
231         }
232     }
233 #endif /* HAVE_KRB4 */
234
235 #ifndef DEBUG
236     if (!nofork)
237         detach();
238 #endif /* DEBUG */
239
240     /* open log */
241     OPENLOG(programname, LOG_PID, LOG_LOCAL6);
242
243 #if defined (DEBUG) && 0
244     if (zalone)
245         syslog(LOG_DEBUG, "standalone operation");
246 #endif
247 #if 0
248     if (zdebug)
249         syslog(LOG_DEBUG, "debugging on");
250 #endif
251
252     /* set up sockets & my_addr and myname, 
253        find other servers and set up server table, initialize queues
254        for retransmits, initialize error tables,
255        set up restricted classes */
256
257     /* Initialize t_local for other uses */
258     gettimeofday(&t_local, NULL);
259
260     if (initialize())
261         exit(1);
262
263     if (init_from_dump)
264         read_from_dump(dumpfile);
265
266     /* Seed random number set.  */
267     srandom(getpid() ^ time(0));
268
269     /* chdir to somewhere where a core dump will survive */
270     if (chdir(TEMP_DIRECTORY) != 0)
271         syslog(LOG_ERR, "chdir failed (%m) (execution continuing)");
272
273     FD_ZERO(&interesting);
274     FD_SET(srv_socket, &interesting);
275
276     nfds = srv_socket + 1;
277
278
279 #ifdef _POSIX_VERSION
280     action.sa_flags = 0;
281     sigemptyset(&action.sa_mask);
282
283     action.sa_handler = bye;
284     sigaction(SIGINT, &action, NULL);
285     sigaction(SIGTERM, &action, NULL);
286
287     action.sa_handler = dbug_on;
288     sigaction(SIGUSR1, &action, NULL);
289
290     action.sa_handler = dbug_off;
291     sigaction(SIGUSR2, &action, NULL);
292
293     action.sa_handler = reap;
294     sigaction(SIGCHLD, &action, NULL);
295
296     action.sa_handler = sig_dump_db;
297     sigaction(SIGFPE, &action, NULL);
298
299 #ifdef SIGEMT
300     action.sa_handler = sig_dump_strings;
301     sigaction(SIGEMT, &action, NULL);
302 #endif
303
304     action.sa_handler = reset;
305     sigaction(SIGHUP, &action, NULL);
306 #else /* !posix */
307     signal(SIGINT, bye);
308     signal(SIGTERM, bye);
309     signal(SIGUSR1, dbug_on);
310     signal(SIGUSR2, dbug_off);
311     signal(SIGCHLD, reap);
312     signal(SIGFPE, sig_dump_db);
313 #ifdef SIGEMT
314     signal(SIGEMT, sig_dump_strings);
315 #endif
316     signal(SIGHUP, reset);
317 #endif /* _POSIX_VERSION */
318
319     syslog(LOG_NOTICE, "Ready for action");
320
321     /* Reinitialize t_local now that initialization is done. */
322     gettimeofday(&t_local, NULL);
323     uptime = NOW;
324
325     realm_wakeup();
326 #ifdef DEBUG_MALLOC
327     malloc_inuse(&m_size);
328 #endif
329     for EVER {
330         if (doreset)
331             do_reset();
332
333         if (dump_db_flag)
334             dump_db();
335         if (dump_strings_flag)
336             dump_strings();
337
338         timer_process();
339
340         readable = interesting;
341         if (msgs_queued()) {
342             /* when there is input in the queue, we
343                artificially set up to pick up the input */
344             nfound = 1;
345             FD_ZERO(&readable);
346         } else  {
347             nfound = select(nfds, &readable, NULL, NULL, timer_timeout(&tv));
348         }
349
350         /* Initialize t_local for other uses */
351         gettimeofday(&t_local, (struct timezone *)0);
352                 
353         /* don't flame about EINTR, since a SIGUSR1 or SIGUSR2
354            can generate it by interrupting the select */
355         if (nfound < 0) {
356             if (errno != EINTR)
357                 syslog(LOG_WARNING, "select error: %m");
358 #ifdef DEBUG_MALLOC
359             if (dump_malloc_stats) {
360                 unsigned long foo,histid2;
361
362                 dump_malloc_stats = 0;
363                 foo = malloc_inuse(&histid2);
364                 printf("Total inuse: %d\n",foo);
365                 malloc_list(2,m_size,histid2);
366             }
367 #endif
368             continue;
369         }
370
371         if (nfound == 0) {
372             /* either we timed out or we were just
373                polling for input.  Either way we want to continue
374                the loop, and process the next timeout */
375             continue;
376         } else {
377             if (bdump_socket >= 0 && FD_ISSET(bdump_socket,&readable))
378                 bdump_send();
379             else if (msgs_queued() || FD_ISSET(srv_socket, &readable))
380                 handle_packet();
381             else
382                 syslog(LOG_ERR, "select weird?!?!");
383         }
384     }
385 }
386
387 /* Initialize net stuff.
388    Set up the server array.
389    Initialize the packet ack queues to be empty.
390    Initialize the error tables.
391    Restrict certain classes.
392    */
393
394 static int
395 initialize(void)
396 {
397     if (do_net_setup())
398         return(1);
399
400     server_init();
401
402 #ifdef HAVE_KRB4
403     krb_set_tkt_string(tkt_file);
404 #endif
405     realm_init();
406     
407     ZSetServerState(1);
408     ZInitialize();              /* set up the library */
409 #ifdef HAVE_KRB5
410     krb5_cc_resolve(Z_krb5_ctx, tkt5_file, &Z_krb5_ccache);
411 #ifdef HAVE_KRB5_CC_SET_DEFAULT_NAME
412     krb5_cc_set_default_name(Z_krb5_ctx, tkt5_file);
413 #else
414     {
415         /* Hack to make krb5_cc_default do something reasonable */
416         char *env=(char *)malloc(strlen(tkt5_file)+12);
417         if (!env) return(1);
418         sprintf(env, "KRB5CCNAME=%s", tkt5_file);
419         putenv(env);
420     }
421 #endif
422 #endif
423 #ifdef HAVE_KRB4
424     /* Override what Zinitialize set for ZGetRealm() */
425     if (*my_realm) 
426       strcpy(__Zephyr_realm, my_realm);
427 #endif
428     init_zsrv_err_tbl();        /* set up err table */
429
430     ZSetFD(srv_socket);         /* set up the socket as the input fildes */
431
432     /* set up default strings */
433
434     class_control = make_string(ZEPHYR_CTL_CLASS, 1);
435     class_admin = make_string(ZEPHYR_ADMIN_CLASS, 1);
436     class_hm = make_string(HM_CTL_CLASS, 1);
437     class_ulogin = make_string(LOGIN_CLASS, 1);
438     class_ulocate = make_string(LOCATE_CLASS, 1);
439     wildcard_instance = make_string(WILDCARD_INSTANCE, 1);
440     empty = make_string("", 0);
441
442     /* restrict certain classes */
443     access_init();
444     return 0;
445 }
446
447 /* 
448  * Set up the server and client sockets, and initialize my_addr and myname
449  */
450
451 static int
452 do_net_setup(void)
453 {
454     struct servent *sp;
455     struct hostent *hp;
456     char hostname[MAXHOSTNAMELEN+1];
457     int flags;
458
459     if (gethostname(hostname, MAXHOSTNAMELEN + 1)) {
460         syslog(LOG_ERR, "no hostname: %m");
461         return 1;
462     }
463     hp = gethostbyname(hostname);
464     if (!hp) {
465         syslog(LOG_ERR, "no gethostbyname repsonse");
466         strncpy(myname, hostname, MAXHOSTNAMELEN);
467         return 1;
468     }
469     strncpy(myname, hp->h_name, MAXHOSTNAMELEN);
470     memcpy(&my_addr, hp->h_addr, sizeof(hp->h_addr));
471
472     setservent(1);              /* keep file/connection open */
473
474     memset(&srv_addr, 0, sizeof(srv_addr));
475     srv_addr.sin_family = AF_INET;
476     sp = getservbyname(SERVER_SVCNAME, "udp");
477     srv_addr.sin_port = (sp) ? sp->s_port : SERVER_SVC_FALLBACK;
478
479     sp = getservbyname(HM_SVCNAME, "udp");
480     hm_port = (sp) ? sp->s_port : HM_SVC_FALLBACK;
481         
482     sp = getservbyname(HM_SRV_SVCNAME, "udp");
483     hm_srv_port = (sp) ? sp->s_port : HM_SRV_SVC_FALLBACK;
484         
485     srv_socket = socket(AF_INET, SOCK_DGRAM, 0);
486     if (srv_socket < 0) {
487         syslog(LOG_ERR, "client_sock failed: %m");
488         return 1;
489     } else {
490 #ifdef SO_BSDCOMPAT
491       int on = 1;
492
493       /* Prevent Linux from giving us socket errors we don't care about. */
494       setsockopt(srv_socket, SOL_SOCKET, SO_BSDCOMPAT, &on, sizeof(on));
495 #endif
496     }
497     if (bind(srv_socket, (struct sockaddr *) &srv_addr,
498              sizeof(srv_addr)) < 0) {
499         syslog(LOG_ERR, "client bind failed: %m");
500         return 1;
501     }
502
503     /* set not-blocking */
504 #ifdef _POSIX_VERSION
505     flags = fcntl(srv_socket, F_GETFL);
506     flags |= O_NONBLOCK;
507     fcntl(srv_socket, F_SETFL, flags);
508 #else
509     flags = 1;
510     ioctl(srv_socket, FIONBIO, &flags);
511 #endif
512
513     return 0;
514 }    
515
516
517 /*
518  * print out a usage message.
519  */
520
521 static void
522 usage(void)
523 {
524 #ifdef DEBUG
525         fprintf(stderr, "Usage: %s [-d] [-s] [-n] [-k realm] [-f dumpfile]\n",
526                 programname);
527 #else
528         fprintf(stderr, "Usage: %s [-d] [-n] [-k realm] [-f dumpfile]\n",
529                 programname);
530 #endif /* DEBUG */
531         exit(2);
532 }
533
534 int
535 packets_waiting(void)
536 {
537     fd_set readable, initial;
538     struct timeval tv;
539
540     if (msgs_queued())
541         return 1;
542     FD_ZERO(&initial);
543     FD_SET(srv_socket, &initial);
544     readable = initial;
545     tv.tv_sec = tv.tv_usec = 0;
546     return (select(srv_socket + 1, &readable, NULL, NULL, &tv) > 0);
547 }
548
549 static RETSIGTYPE
550 bye(int sig)
551 {
552     server_shutdown();          /* tell other servers */
553 #ifdef REALM_MGMT
554     realm_shutdown();           /* tell other realms */
555 #endif
556     hostm_shutdown();           /* tell our hosts */
557     kill_realm_pids();
558 #ifdef HAVE_KRB4
559     dest_tkt();
560 #endif
561     syslog(LOG_NOTICE, "goodbye (sig %d)", sig);
562     exit(0);
563 }
564
565 static RETSIGTYPE
566 dbug_on(int sig)
567 {
568     syslog(LOG_DEBUG, "debugging turned on");
569 #ifdef DEBUG_MALLOC
570     dump_malloc_stats = 1;
571 #endif
572     zdebug = 1;
573 }
574
575 static RETSIGTYPE
576 dbug_off(int sig)
577 {
578     syslog(LOG_DEBUG, "debugging turned off");
579 #ifdef DEBUG_MALLOC
580     malloc_inuse(&m_size);
581 #endif
582     zdebug = 0;
583 }
584
585 int fork_for_dump = 0;
586
587 static void dump_strings(void)
588 {
589     char filename[128];
590
591     FILE *fp;
592     int oerrno = errno;
593
594     sprintf(filename, "%szephyr.strings", TEMP_DIRECTORY);
595     fp = fopen (filename, "w");
596     if (!fp) {
597         syslog(LOG_ERR, "can't open strings dump file: %m");
598         errno = oerrno;
599         dump_strings_flag = 0;
600         return;
601     }
602     syslog(LOG_INFO, "dumping strings to disk");
603     print_string_table(fp);
604     if (fclose(fp) == EOF)
605         syslog(LOG_ERR, "error writing strings dump file");
606     else
607         syslog(LOG_INFO, "dump done");
608     oerrno = errno;
609     dump_strings_flag = 0;
610     return;
611 }
612
613 static RETSIGTYPE
614 sig_dump_db(int sig)
615 {
616     dump_db_flag = 1;
617 }
618
619 static void
620 dump_db(void)
621 {
622     /* dump the in-core database to human-readable form on disk */
623     FILE *fp;
624     int oerrno = errno;
625     int pid;
626     char filename[128];
627
628     pid = (fork_for_dump) ? fork() : -1;
629     if (pid > 0) {
630         dump_db_flag = 0;
631         return;
632     }
633     sprintf(filename, "%szephyr.db", TEMP_DIRECTORY);
634     fp = fopen(filename, "w");
635     if (!fp) {
636         syslog(LOG_ERR, "can't open dump database");
637         errno = oerrno;
638         dump_db_flag = 0;
639         return;
640     }
641     syslog(LOG_INFO, "dumping to disk");
642     server_dump_servers(fp);
643     uloc_dump_locs(fp);
644     client_dump_clients(fp);
645     triplet_dump_subs(fp);
646     realm_dump_realms(fp);
647     syslog(LOG_INFO, "dump done");
648     if (fclose(fp) == EOF)
649         syslog(LOG_ERR, "can't close dump db");
650     if (pid == 0)
651         exit(0);
652     errno = oerrno;
653     dump_db_flag = 0;
654 }
655
656 static RETSIGTYPE
657 reset(int sig)
658 {
659 #if 1
660     zdbug((LOG_DEBUG,"reset()"));
661 #endif
662     doreset = 1;
663 }
664
665 static RETSIGTYPE
666 reap(int sig)
667 {
668     int pid, i = 0;
669     int oerrno = errno;
670     ZRealm *rlm;
671 #ifdef _POSIX_VERSION
672     int waitb;
673 #else
674     union wait waitb;
675 #endif
676 #if 1
677     zdbug((LOG_DEBUG,"reap()"));
678 #endif
679 #ifdef _POSIX_VERSION
680     while ((pid = waitpid(-1, &waitb, WNOHANG)) == 0) 
681       { i++; if (i > 10) break; }
682 #else
683     while ((pid = wait3 (&waitb, WNOHANG, (struct rusage*) 0)) == 0) 
684       { i++; if (i > 10) break; }
685 #endif
686
687     errno = oerrno;
688  
689     if (pid) {
690       if (WIFSIGNALED(waitb) == 0) {
691         if (WIFEXITED(waitb) != 0) {
692           rlm = realm_get_realm_by_pid(pid);
693           if (rlm) {
694             rlm->child_pid = 0;
695             rlm->have_tkt = 1;
696           }
697         }
698       } else {
699         rlm = realm_get_realm_by_pid(pid);
700         if (rlm) {
701           rlm->child_pid = 0;
702         }
703       }
704     }
705 }
706
707 static void
708 do_reset(void)
709 {
710     int oerrno = errno;
711 #ifdef _POSIX_VERSION
712     sigset_t mask, omask;
713 #else
714     int omask;
715 #endif
716 #if 0
717     zdbug((LOG_DEBUG,"do_reset()"));
718 #endif
719 #ifdef _POSIX_VERSION
720     sigemptyset(&mask);
721     sigaddset(&mask, SIGHUP);
722     sigprocmask(SIG_BLOCK, &mask, &omask);
723 #else
724     omask = sigblock(sigmask(SIGHUP));
725 #endif
726
727     /* reset various things in the server's state */
728     subscr_reset();
729     server_reset();
730     access_reinit();
731     syslog(LOG_INFO, "restart completed");
732     doreset = 0;
733     errno = oerrno;
734 #ifdef _POSIX_VERSION
735     sigprocmask(SIG_SETMASK, &omask, (sigset_t *)0);
736 #else
737     sigsetmask(omask);
738 #endif
739 }
740
741 #ifndef DEBUG
742 /*
743  * detach from the terminal
744  */
745
746 static void
747 detach(void)
748 {
749     /* detach from terminal and fork. */
750     int i;
751     long size;
752
753 #ifdef _POSIX_VERSION
754     size = sysconf(_SC_OPEN_MAX);
755 #else
756     size = getdtablesize();
757 #endif
758     /* profiling seems to get confused by fork() */
759     i = fork ();
760     if (i) {
761         if (i < 0)
762             perror("fork");
763         exit(0);
764     }
765
766     for (i = 0; i < size; i++)
767         close(i);
768
769     i = open("/dev/tty", O_RDWR, 666);
770 #ifdef TIOCNOTTY /* Only necessary on old systems. */
771     ioctl(i, TIOCNOTTY, NULL);
772 #endif
773     close(i);
774 #ifdef _POSIX_VERSION
775     setsid();
776 #endif
777 }
778 #endif /* not DEBUG */
779
780 static void
781 read_from_dump(char *dumpfile)
782 {
783     /* Not yet implemented. */
784     return;
785 }
786