]> asedeno.scripts.mit.edu Git - 1ts-debian.git/blob - zephyr/server/main.c
d94b4686c11d2710bacabe1605060defcf6fa680
[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: zacheiss $
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: main.c,v 1.69 2001/02/27 04:50:08 zacheiss Exp $";
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 __P((void));
56 static int initialize __P((void));
57 static void usage __P((void));
58 static void do_reset __P((void));
59 static RETSIGTYPE bye __P((int));
60 static RETSIGTYPE dbug_on __P((int));
61 static RETSIGTYPE dbug_off __P((int));
62 static RETSIGTYPE sig_dump_db __P((int));
63 static RETSIGTYPE sig_dump_strings __P((int));
64 static RETSIGTYPE reset __P((int));
65 static RETSIGTYPE reap __P((int));
66 static void read_from_dump __P((char *dumpfile));
67 static void dump_db __P((void));
68 static void dump_strings __P((void));
69
70 #ifndef DEBUG
71 static void detach __P((void));
72 #endif
73
74 static short doreset = 0;               /* if it becomes 1, perform
75                                            reset functions */
76
77 int nfds;                               /* max file descriptor for select() */
78 int srv_socket;                         /* dgram socket for clients
79                                            and other servers */
80 int bdump_socket = -1;                  /* brain dump socket fd
81                                            (closed most of the time) */
82 fd_set interesting;                     /* the file descrips we are listening
83                                            to right now */
84 struct sockaddr_in srv_addr;            /* address of the socket */
85
86 Unacked *nacklist = NULL;               /* list of packets waiting for ack's */
87
88 unsigned short hm_port;                 /* host manager receiver port */
89 unsigned short hm_srv_port;             /* host manager server sending port */
90
91 char *programname;                      /* set to the basename of argv[0] */
92 char myname[MAXHOSTNAMELEN];            /* my host name */
93
94 char list_file[128];
95 #ifdef HAVE_KRB5
96 char keytab_file[128];
97 static char tkt5_file[256];
98 #endif
99 #ifdef HAVE_KRB4
100 char srvtab_file[128];
101 char my_realm[REALM_SZ];
102 static char tkt_file[128];
103 #endif
104 char acl_dir[128];
105 char subs_file[128];
106
107 int zdebug;
108 #ifdef DEBUG_MALLOC
109 int dump_malloc_stats = 0;
110 unsigned long m_size;
111 #endif
112 #ifdef DEBUG
113 int zalone;
114 #endif
115
116 struct timeval t_local;                 /* store current time for other uses */
117
118 static int dump_db_flag = 0;
119 static int dump_strings_flag = 0;
120
121 u_long npackets;                        /* number of packets processed */
122 time_t uptime;                          /* when we started operations */
123 static int nofork;
124 struct in_addr my_addr;
125 char *bdump_version = "1.2";
126
127 #ifdef HAVE_KRB5
128 krb5_ccache Z_krb5_ccache;
129 #endif
130
131 int
132 main(argc, argv)
133     int argc;
134     char **argv;
135 {
136     int nfound;                 /* #fildes ready on select */
137     fd_set readable;
138     struct timeval tv;
139     int init_from_dump = 0;
140     char *dumpfile;
141 #ifdef _POSIX_VERSION
142     struct sigaction action;
143 #endif
144     int optchar;                        /* option processing */
145     extern char *optarg;
146     extern int optind;
147
148     sprintf(list_file, "%s/zephyr/%s", SYSCONFDIR, SERVER_LIST_FILE);
149 #ifdef HAVE_KRB4
150     sprintf(srvtab_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_SRVTAB);
151     sprintf(tkt_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_TKFILE);
152 #endif
153 #ifdef HAVE_KRB5
154     sprintf(keytab_file, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_KEYTAB);
155     sprintf(tkt5_file, "FILE:%s/zephyr/%s", SYSCONFDIR, ZEPHYR_TK5FILE);
156 #endif
157     sprintf(acl_dir, "%s/zephyr/%s", SYSCONFDIR, ZEPHYR_ACL_DIR);
158     sprintf(subs_file, "%s/zephyr/%s", SYSCONFDIR, DEFAULT_SUBS_FILE);
159
160     /* set name */
161     programname = strrchr(argv[0],'/');
162     programname = (programname) ? programname + 1 : argv[0];
163
164     /* process arguments */
165     while ((optchar = getopt(argc, argv, "dsnv:f:k:")) != EOF) {
166         switch(optchar) {
167           case 'd':
168             zdebug = 1;
169             break;
170 #ifdef DEBUG
171           case 's':
172             zalone = 1;
173             break;
174 #endif
175           case 'n':
176             nofork = 1;
177             break;
178           case 'k':
179 #ifdef HAVE_KRB4
180             strncpy(my_realm, optarg, REALM_SZ);
181 #endif
182             break;
183           case 'v':
184             bdump_version = optarg;
185             break;
186           case 'f':
187             init_from_dump = 0;
188             dumpfile = optarg;
189             break;
190           case '?':
191           default:
192             usage();
193             /*NOTREACHED*/
194         }
195     }
196
197 #ifdef HAVE_KRB4
198     /* if there is no readable srvtab and we are not standalone, there
199        is no possible way we can succeed, so we exit */
200
201     if (access(srvtab_file, R_OK)
202 #ifdef DEBUG            
203         && !zalone
204 #endif /* DEBUG */
205         ) {
206         fprintf(stderr, "NO ZEPHYR SRVTAB (%s) available; exiting\n",
207                 srvtab_file);
208         exit(1);
209     }
210     /* Use local realm if not specified on command line. */
211     if (!*my_realm) {
212         if (krb_get_lrealm(my_realm, 1) != KSUCCESS) {
213             fputs("Couldn't get local Kerberos realm; exiting.\n", stderr);
214             exit(1);
215         }
216     }
217 #endif /* HAVE_KRB4 */
218
219 #ifndef DEBUG
220     if (!nofork)
221         detach();
222 #endif /* DEBUG */
223
224     /* open log */
225     OPENLOG(programname, LOG_PID, LOG_LOCAL6);
226
227 #if defined (DEBUG) && 0
228     if (zalone)
229         syslog(LOG_DEBUG, "standalone operation");
230 #endif
231 #if 0
232     if (zdebug)
233         syslog(LOG_DEBUG, "debugging on");
234 #endif
235
236     /* set up sockets & my_addr and myname, 
237        find other servers and set up server table, initialize queues
238        for retransmits, initialize error tables,
239        set up restricted classes */
240
241     /* Initialize t_local for other uses */
242     gettimeofday(&t_local, NULL);
243
244     if (initialize())
245         exit(1);
246
247     if (init_from_dump)
248         read_from_dump(dumpfile);
249
250     /* Seed random number set.  */
251     srandom(getpid() ^ time(0));
252
253     /* chdir to somewhere where a core dump will survive */
254     if (chdir(TEMP_DIRECTORY) != 0)
255         syslog(LOG_ERR, "chdir failed (%m) (execution continuing)");
256
257     FD_ZERO(&interesting);
258     FD_SET(srv_socket, &interesting);
259
260     nfds = srv_socket + 1;
261
262
263 #ifdef _POSIX_VERSION
264     action.sa_flags = 0;
265     sigemptyset(&action.sa_mask);
266
267     action.sa_handler = bye;
268     sigaction(SIGINT, &action, NULL);
269     sigaction(SIGTERM, &action, NULL);
270
271     action.sa_handler = dbug_on;
272     sigaction(SIGUSR1, &action, NULL);
273
274     action.sa_handler = dbug_off;
275     sigaction(SIGUSR2, &action, NULL);
276
277     action.sa_handler = reap;
278     sigaction(SIGCHLD, &action, NULL);
279
280     action.sa_handler = sig_dump_db;
281     sigaction(SIGFPE, &action, NULL);
282
283 #ifdef SIGEMT
284     action.sa_handler = sig_dump_strings;
285     sigaction(SIGEMT, &action, NULL);
286 #endif
287
288     action.sa_handler = reset;
289     sigaction(SIGHUP, &action, NULL);
290 #else /* !posix */
291     signal(SIGINT, bye);
292     signal(SIGTERM, bye);
293     signal(SIGUSR1, dbug_on);
294     signal(SIGUSR2, dbug_off);
295     signal(SIGCHLD, reap);
296     signal(SIGFPE, sig_dump_db);
297 #ifdef SIGEMT
298     signal(SIGEMT, sig_dump_strings);
299 #endif
300     signal(SIGHUP, reset);
301 #endif /* _POSIX_VERSION */
302
303     syslog(LOG_NOTICE, "Ready for action");
304
305     /* Reinitialize t_local now that initialization is done. */
306     gettimeofday(&t_local, NULL);
307     uptime = NOW;
308 #ifdef HAVE_KRB4
309     timer_set_rel(SWEEP_INTERVAL, sweep_ticket_hash_table, NULL);
310 #endif
311
312     realm_wakeup();
313 #ifdef DEBUG_MALLOC
314     malloc_inuse(&m_size);
315 #endif
316     for EVER {
317         if (doreset)
318             do_reset();
319
320         if (dump_db_flag)
321             dump_db();
322         if (dump_strings_flag)
323             dump_strings();
324
325         timer_process();
326
327         readable = interesting;
328         if (msgs_queued()) {
329             /* when there is input in the queue, we
330                artificially set up to pick up the input */
331             nfound = 1;
332             FD_ZERO(&readable);
333         } else  {
334             nfound = select(nfds, &readable, NULL, NULL, timer_timeout(&tv));
335         }
336
337         /* Initialize t_local for other uses */
338         gettimeofday(&t_local, (struct timezone *)0);
339                 
340         /* don't flame about EINTR, since a SIGUSR1 or SIGUSR2
341            can generate it by interrupting the select */
342         if (nfound < 0) {
343             if (errno != EINTR)
344                 syslog(LOG_WARNING, "select error: %m");
345 #ifdef DEBUG_MALLOC
346             if (dump_malloc_stats) {
347                 unsigned long foo,histid2;
348
349                 dump_malloc_stats = 0;
350                 foo = malloc_inuse(&histid2);
351                 printf("Total inuse: %d\n",foo);
352                 malloc_list(2,m_size,histid2);
353             }
354 #endif
355             continue;
356         }
357
358         if (nfound == 0) {
359             /* either we timed out or we were just
360                polling for input.  Either way we want to continue
361                the loop, and process the next timeout */
362             continue;
363         } else {
364             if (bdump_socket >= 0 && FD_ISSET(bdump_socket,&readable))
365                 bdump_send();
366             else if (msgs_queued() || FD_ISSET(srv_socket, &readable))
367                 handle_packet();
368             else
369                 syslog(LOG_ERR, "select weird?!?!");
370         }
371     }
372 }
373
374 /* Initialize net stuff.
375    Set up the server array.
376    Initialize the packet ack queues to be empty.
377    Initialize the error tables.
378    Restrict certain classes.
379    */
380
381 static int
382 initialize()
383 {
384     int zero = 0;
385     if (do_net_setup())
386         return(1);
387
388     server_init();
389
390 #ifdef HAVE_KRB4
391     krb_set_tkt_string(tkt_file);
392 #endif
393     realm_init();
394     
395     ZSetServerState(1);
396     ZInitialize();              /* set up the library */
397 #ifdef HAVE_KRB5
398     krb5_cc_resolve(Z_krb5_ctx, tkt5_file, &Z_krb5_ccache);
399 #ifdef HAVE_KRB5_CC_SET_DEFAULT_NAME
400     krb5_cc_set_default_name(Z_krb5_ctx, tkt5_file);
401 #else
402     {
403         /* Hack to make krb5_cc_default do something reasonable */
404         char *env=(char *)malloc(strlen(tkt5_file)+12);
405         if (!env) return(1);
406         sprintf(env, "KRB5CCNAME=%s", tkt5_file);
407         putenv(env);
408     }
409 #endif
410 #endif
411 #ifdef HAVE_KRB4
412     /* Override what Zinitialize set for ZGetRealm() */
413     if (*my_realm) 
414       strcpy(__Zephyr_realm, my_realm);
415 #endif
416     init_zsrv_err_tbl();        /* set up err table */
417
418     ZSetFD(srv_socket);         /* set up the socket as the input fildes */
419
420     /* set up default strings */
421
422     class_control = make_string(ZEPHYR_CTL_CLASS, 1);
423     class_admin = make_string(ZEPHYR_ADMIN_CLASS, 1);
424     class_hm = make_string(HM_CTL_CLASS, 1);
425     class_ulogin = make_string(LOGIN_CLASS, 1);
426     class_ulocate = make_string(LOCATE_CLASS, 1);
427     wildcard_instance = make_string(WILDCARD_INSTANCE, 1);
428     empty = make_string("", 0);
429
430     /* restrict certain classes */
431     access_init();
432     return 0;
433 }
434
435 /* 
436  * Set up the server and client sockets, and initialize my_addr and myname
437  */
438
439 static int
440 do_net_setup()
441 {
442     struct servent *sp;
443     struct hostent *hp;
444     char hostname[MAXHOSTNAMELEN+1];
445     int flags;
446
447     if (gethostname(hostname, MAXHOSTNAMELEN + 1)) {
448         syslog(LOG_ERR, "no hostname: %m");
449         return 1;
450     }
451     hp = gethostbyname(hostname);
452     if (!hp) {
453         syslog(LOG_ERR, "no gethostbyname repsonse");
454         strncpy(myname, hostname, MAXHOSTNAMELEN);
455         return 1;
456     }
457     strncpy(myname, hp->h_name, MAXHOSTNAMELEN);
458     memcpy(&my_addr, hp->h_addr, sizeof(hp->h_addr));
459
460     setservent(1);              /* keep file/connection open */
461
462     memset(&srv_addr, 0, sizeof(srv_addr));
463     srv_addr.sin_family = AF_INET;
464     sp = getservbyname(SERVER_SVCNAME, "udp");
465     srv_addr.sin_port = (sp) ? sp->s_port : SERVER_SVC_FALLBACK;
466
467     sp = getservbyname(HM_SVCNAME, "udp");
468     hm_port = (sp) ? sp->s_port : HM_SVC_FALLBACK;
469         
470     sp = getservbyname(HM_SRV_SVCNAME, "udp");
471     hm_srv_port = (sp) ? sp->s_port : HM_SRV_SVC_FALLBACK;
472         
473     srv_socket = socket(AF_INET, SOCK_DGRAM, 0);
474     if (srv_socket < 0) {
475         syslog(LOG_ERR, "client_sock failed: %m");
476         return 1;
477     } else {
478 #ifdef SO_BSDCOMPAT
479       int on = 1;
480
481       /* Prevent Linux from giving us socket errors we don't care about. */
482       setsockopt(srv_socket, SOL_SOCKET, SO_BSDCOMPAT, &on, sizeof(on));
483 #endif
484     }
485     if (bind(srv_socket, (struct sockaddr *) &srv_addr,
486              sizeof(srv_addr)) < 0) {
487         syslog(LOG_ERR, "client bind failed: %m");
488         return 1;
489     }
490
491     /* set not-blocking */
492 #ifdef _POSIX_VERSION
493     flags = fcntl(srv_socket, F_GETFL);
494     flags |= O_NONBLOCK;
495     fcntl(srv_socket, F_SETFL, flags);
496 #else
497     flags = 1;
498     ioctl(srv_socket, FIONBIO, &flags);
499 #endif
500
501     return 0;
502 }    
503
504
505 /*
506  * print out a usage message.
507  */
508
509 static void
510 usage()
511 {
512 #ifdef DEBUG
513         fprintf(stderr, "Usage: %s [-d] [-s] [-n] [-k realm] [-f dumpfile]\n",
514                 programname);
515 #else
516         fprintf(stderr, "Usage: %s [-d] [-n] [-k realm] [-f dumpfile]\n",
517                 programname);
518 #endif /* DEBUG */
519         exit(2);
520 }
521
522 int
523 packets_waiting()
524 {
525     fd_set readable, initial;
526     struct timeval tv;
527
528     if (msgs_queued())
529         return 1;
530     FD_ZERO(&initial);
531     FD_SET(srv_socket, &initial);
532     readable = initial;
533     tv.tv_sec = tv.tv_usec = 0;
534     return (select(srv_socket + 1, &readable, NULL, NULL, &tv) > 0);
535 }
536
537 static RETSIGTYPE
538 bye(sig)
539     int sig;
540 {
541     server_shutdown();          /* tell other servers */
542 #ifdef REALM_MGMT
543     realm_shutdown();           /* tell other realms */
544 #endif
545     hostm_shutdown();           /* tell our hosts */
546     kill_realm_pids();
547 #ifdef HAVE_KRB4
548     dest_tkt();
549 #endif
550     syslog(LOG_NOTICE, "goodbye (sig %d)", sig);
551     exit(0);
552 }
553
554 static RETSIGTYPE
555 dbug_on(sig)
556     int sig;
557 {
558     syslog(LOG_DEBUG, "debugging turned on");
559 #ifdef DEBUG_MALLOC
560     dump_malloc_stats = 1;
561 #endif
562     zdebug = 1;
563 }
564
565 static RETSIGTYPE
566 dbug_off(sig)
567     int sig;
568 {
569     syslog(LOG_DEBUG, "debugging turned off");
570 #ifdef DEBUG_MALLOC
571     malloc_inuse(&m_size);
572 #endif
573     zdebug = 0;
574 }
575
576 int fork_for_dump = 0;
577
578 static RETSIGTYPE
579 sig_dump_strings(sig)
580     int sig;
581 {
582     dump_strings_flag = 1;
583 }
584
585 static void dump_strings()
586 {
587     char filename[128];
588
589     FILE *fp;
590     int oerrno = errno;
591
592     sprintf(filename, "%szephyr.strings", TEMP_DIRECTORY);
593     fp = fopen (filename, "w");
594     if (!fp) {
595         syslog(LOG_ERR, "can't open strings dump file: %m");
596         errno = oerrno;
597         dump_strings_flag = 0;
598         return;
599     }
600     syslog(LOG_INFO, "dumping strings to disk");
601     print_string_table(fp);
602     if (fclose(fp) == EOF)
603         syslog(LOG_ERR, "error writing strings dump file");
604     else
605         syslog(LOG_INFO, "dump done");
606     oerrno = errno;
607     dump_strings_flag = 0;
608     return;
609 }
610
611 static RETSIGTYPE
612 sig_dump_db(sig)
613     int sig;
614 {
615     dump_db_flag = 1;
616 }
617
618 static void dump_db()
619 {
620     /* dump the in-core database to human-readable form on disk */
621     FILE *fp;
622     int oerrno = errno;
623     int pid;
624     char filename[128];
625
626     pid = (fork_for_dump) ? fork() : -1;
627     if (pid > 0) {
628         dump_db_flag = 0;
629         return;
630     }
631     sprintf(filename, "%szephyr.db", TEMP_DIRECTORY);
632     fp = fopen(filename, "w");
633     if (!fp) {
634         syslog(LOG_ERR, "can't open dump database");
635         errno = oerrno;
636         dump_db_flag = 0;
637         return;
638     }
639     syslog(LOG_INFO, "dumping to disk");
640     server_dump_servers(fp);
641     uloc_dump_locs(fp);
642     client_dump_clients(fp);
643     triplet_dump_subs(fp);
644     realm_dump_realms(fp);
645     syslog(LOG_INFO, "dump done");
646     if (fclose(fp) == EOF)
647         syslog(LOG_ERR, "can't close dump db");
648     if (pid == 0)
649         exit(0);
650     errno = oerrno;
651     dump_db_flag = 0;
652 }
653
654 static RETSIGTYPE
655 reset(sig)
656     int sig;
657 {
658 #if 1
659     zdbug((LOG_DEBUG,"reset()"));
660 #endif
661     doreset = 1;
662 }
663
664 static RETSIGTYPE
665 reap(sig)
666     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()
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()
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(dumpfile)
782     char *dumpfile;
783 {
784     /* Not yet implemented. */
785     return;
786 }
787