home *** CD-ROM | disk | FTP | other *** search
/ InfoMagic Source Code 1993 July / THE_SOURCE_CODE_CD_ROM.iso / bsd_srcs / usr.sbin / amd / fsinfo / fsi_analyze.c next >
Encoding:
C/C++ Source or Header  |  1991-05-12  |  14.8 KB  |  646 lines

  1. /*
  2.  * Copyright (c) 1989 Jan-Simon Pendry
  3.  * Copyright (c) 1989 Imperial College of Science, Technology & Medicine
  4.  * Copyright (c) 1989 The Regents of the University of California.
  5.  * All rights reserved.
  6.  *
  7.  * This code is derived from software contributed to Berkeley by
  8.  * Jan-Simon Pendry at Imperial College, London.
  9.  *
  10.  * Redistribution and use in source and binary forms, with or without
  11.  * modification, are permitted provided that the following conditions
  12.  * are met:
  13.  * 1. Redistributions of source code must retain the above copyright
  14.  *    notice, this list of conditions and the following disclaimer.
  15.  * 2. Redistributions in binary form must reproduce the above copyright
  16.  *    notice, this list of conditions and the following disclaimer in the
  17.  *    documentation and/or other materials provided with the distribution.
  18.  * 3. All advertising materials mentioning features or use of this software
  19.  *    must display the following acknowledgement:
  20.  *    This product includes software developed by the University of
  21.  *    California, Berkeley and its contributors.
  22.  * 4. Neither the name of the University nor the names of its contributors
  23.  *    may be used to endorse or promote products derived from this software
  24.  *    without specific prior written permission.
  25.  *
  26.  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
  27.  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  28.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  29.  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
  30.  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  31.  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  32.  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  33.  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  34.  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  35.  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  36.  * SUCH DAMAGE.
  37.  *
  38.  *    @(#)fsi_analyze.c    5.3 (Berkeley) 5/12/91
  39.  *
  40.  * $Id: fsi_analyze.c,v 5.2.1.3 91/05/07 22:19:06 jsp Alpha $
  41.  *
  42.  */
  43.  
  44. /*
  45.  * Analyze filesystem declarations
  46.  *
  47.  * Note: most of this is magic!
  48.  */
  49.  
  50. #include "../fsinfo/fsinfo.h"
  51.  
  52. char *disk_fs_strings[] = {
  53.     "fstype", "opts", "dumpset", "passno", "freq", "mount", "log", 0,
  54. };
  55.  
  56. char *mount_strings[] = {
  57.     "volname", "exportfs", 0,
  58. };
  59.  
  60. char *fsmount_strings[] = {
  61.     "as", "volname", "fstype", "opts", "from", 0,
  62. };
  63.  
  64. char *host_strings[] = {
  65.     "host", "netif", "config", "arch", "cluster", "os", 0,
  66. };
  67.  
  68. char *ether_if_strings[] = {
  69.     "inaddr", "netmask", "hwaddr", 0,
  70. };
  71.  
  72. /*
  73.  * Strip off the trailing part of a domain
  74.  * to produce a short-form domain relative
  75.  * to the local host domain.
  76.  * Note that this has no effect if the domain
  77.  * names do not have the same number of
  78.  * components.  If that restriction proves
  79.  * to be a problem then the loop needs recoding
  80.  * to skip from right to left and do partial
  81.  * matches along the way -- ie more expensive.
  82.  */
  83. void domain_strip(otherdom, localdom)
  84. char *otherdom, *localdom;
  85. {
  86. #ifdef PARTIAL_DOMAINS
  87.         char *p1 = otherdom-1;
  88.     char *p2 = localdom-1;
  89.  
  90.         do {
  91.                 if (p1 = strchr(p1+1, '.'))
  92.                 if (p2 = strchr(p2+1, '.'))
  93.                 if (STREQ(p1+1, p2+1)) {
  94.                         *p1 = '\0';
  95.                         break;
  96.                 }
  97.         } while (p1 && p2);
  98. #else
  99.     char *p1, *p2;
  100.  
  101.     if ((p1 = strchr(otherdom, '.')) &&
  102.             (p2 = strchr(localdom, '.')) &&
  103.             (strcmp(p1+1, p2+1) == 0))
  104.         *p1 = '\0';
  105. #endif /* PARTIAL_DOMAINS */
  106. }
  107.  
  108. /*
  109.  * Take a little-endian domain name and
  110.  * transform into a big-endian Un*x pathname.
  111.  * For example: kiska.doc.ic -> ic/doc/kiska
  112.  */
  113. static char *compute_hostpath(hn)
  114. char *hn;
  115. {
  116.     char *p = strdup(hn);
  117.     char *d;
  118.     char path[MAXPATHLEN];
  119.  
  120.     domain_strip(p, hostname);
  121.     path[0] = '\0';
  122.  
  123.     do {
  124.         d = strrchr(p, '.');
  125.         if (d) {
  126.             *d = 0;
  127.             strcat(path, d+1);
  128.             strcat(path, "/");
  129.         } else {
  130.             strcat(path, p);
  131.         }
  132.     } while (d);
  133.  
  134.     log("hostpath of '%s' is '%s'", hn, path);
  135.  
  136.     strcpy(p, path);
  137.     return p;
  138. }
  139.  
  140. static dict_ent *find_volname(nn)
  141. char *nn;
  142. {
  143.     dict_ent *de;
  144.     char *p = strdup(nn);
  145.     char *q;
  146.  
  147.     do {
  148.         log("Searching for volname %s", p);
  149.         de = dict_locate(dict_of_volnames, p);
  150.         q = strrchr(p, '/');
  151.         if (q) *q = '\0';
  152.     } while (!de && q);
  153.  
  154.     free(p);
  155.     return de;
  156. }
  157.  
  158. static show_required(l, mask, info, hostname, strings)
  159. ioloc *l;
  160. int mask;
  161. char *info;
  162. char *hostname;
  163. char *strings[];
  164. {
  165.     int i;
  166.     log("mask left for %s:%s is %#x", hostname, info, mask);
  167.  
  168.     for (i = 0; strings[i]; i++)
  169.         if (ISSET(mask, i))
  170.             lerror(l, "%s:%s needs field \"%s\"", hostname, info, strings[i]);
  171. }
  172.  
  173. /*
  174.  * Check and fill in "exportfs" details.
  175.  * Make sure the m_exported field references
  176.  * the most local node with an "exportfs" entry.
  177.  */
  178. static int check_exportfs(q, e)
  179. qelem *q;
  180. mount *e;
  181. {
  182.     mount *mp;
  183.     int errors = 0;
  184.  
  185.     ITER(mp, mount, q) {
  186.         if (ISSET(mp->m_mask, DM_EXPORTFS)) {
  187.             if (e)
  188.                 lwarning(mp->m_ioloc, "%s has duplicate exportfs data", mp->m_name);
  189.             mp->m_exported = mp;
  190.             if (!ISSET(mp->m_mask, DM_VOLNAME))
  191.                 set_mount(mp, DM_VOLNAME, strdup(mp->m_name));
  192.         } else {
  193.             mp->m_exported = e;
  194.         }
  195.  
  196.         /*
  197.          * Recursively descend the mount tree
  198.          */
  199.         if (mp->m_mount)
  200.             errors += check_exportfs(mp->m_mount, mp->m_exported);
  201.  
  202.         /*
  203.          * If a volume name has been specified, but this node and none
  204.          * of its parents has been exported, report an error.
  205.          */
  206.         if (ISSET(mp->m_mask, DM_VOLNAME) && !mp->m_exported) {
  207.             lerror(mp->m_ioloc, "%s has a volname but no exportfs data", mp->m_name);
  208.             errors++;
  209.         }
  210.     }
  211.  
  212.     return errors;
  213. }
  214.  
  215. static int analyze_dkmount_tree(q, parent, dk)
  216. qelem *q;
  217. mount *parent;
  218. disk_fs *dk;
  219. {
  220.     mount *mp;
  221.     int errors = 0;
  222.  
  223.     ITER(mp, mount, q) {
  224.         log("Mount %s:", mp->m_name);
  225.         if (parent) {
  226.             char n[MAXPATHLEN];
  227.             sprintf(n, "%s/%s", parent->m_name, mp->m_name);
  228.             if (*mp->m_name == '/')
  229.                 lerror(mp->m_ioloc, "sub-directory %s of %s starts with '/'", mp->m_name, parent->m_name);
  230.             else if (STREQ(mp->m_name, "default"))
  231.                 lwarning(mp->m_ioloc, "sub-directory of %s is named \"default\"", parent->m_name);
  232.             log("Changing name %s to %s", mp->m_name, n);
  233.             free(mp->m_name);
  234.             mp->m_name = strdup(n);
  235.         }
  236.         mp->m_name_len = strlen(mp->m_name);
  237.         mp->m_parent = parent;
  238.         mp->m_dk = dk;
  239.         if (mp->m_mount)
  240.             analyze_dkmount_tree(mp->m_mount, mp, dk);
  241.     }
  242.  
  243.     return errors;
  244. }
  245.  
  246. /*
  247.  * The mount tree is a singleton list
  248.  * containing the top-level mount
  249.  * point for a disk.
  250.  */
  251. static int analyze_dkmounts(dk, q)
  252. disk_fs *dk;
  253. qelem *q;
  254. {
  255.     int errors = 0;
  256.     mount *mp, *mp2 = 0;
  257.     int i = 0;
  258.  
  259.     /*
  260.      * First scan the list of subdirs to make
  261.      * sure there is only one - and remember it
  262.      */
  263.     if (q) {
  264.         ITER(mp, mount, q) {
  265.             mp2 = mp;
  266.             i++;
  267.         }
  268.     }
  269.  
  270.     /*
  271.      * Check...
  272.      */
  273.     if (i < 1) {
  274.         lerror(dk->d_ioloc, "%s:%s has no mount point", dk->d_host->h_hostname, dk->d_dev);
  275.         return 1;
  276.     }
  277.     if (i > 1) {
  278.         lerror(dk->d_ioloc, "%s:%s has more than one mount point", dk->d_host->h_hostname, dk->d_dev);
  279.         errors++;
  280.     }
  281.     /*
  282.      * Now see if a default mount point is required
  283.      */
  284.     if (STREQ(mp2->m_name, "default")) {
  285.         if (ISSET(mp2->m_mask, DM_VOLNAME)) {
  286.             char nbuf[1024];
  287.             compute_automount_point(nbuf, dk->d_host, mp2->m_volname);
  288.             free(mp2->m_name);
  289.             mp2->m_name = strdup(nbuf);
  290.             log("%s:%s has default mount on %s", dk->d_host->h_hostname, dk->d_dev, mp2->m_name);
  291.         } else {
  292.             lerror(dk->d_ioloc, "no volname given for %s:%s", dk->d_host->h_hostname, dk->d_dev);
  293.             errors++;
  294.         }
  295.     }
  296.     /*
  297.      * Fill in the disk mount point
  298.      */
  299.     if (!errors && mp2 && mp2->m_name)
  300.         dk->d_mountpt = strdup(mp2->m_name);
  301.     else
  302.         dk->d_mountpt = strdup("error");
  303.  
  304.     /*
  305.      * Analyze the mount tree
  306.      */
  307.     errors += analyze_dkmount_tree(q, 0, dk);
  308.  
  309.     /*
  310.      * Analyze the export tree
  311.      */
  312.     errors += check_exportfs(q, 0);
  313.  
  314.     return errors;
  315. }
  316.  
  317. static void fixup_required_disk_info(dp)
  318. disk_fs *dp;
  319. {
  320.     /*
  321.      * "fstype"
  322.      */
  323.     if (ISSET(dp->d_mask, DF_FSTYPE)) {
  324.         if (STREQ(dp->d_fstype, "swap")) {
  325.             /*
  326.              * Fixup for a swap device
  327.              */
  328.             if (!ISSET(dp->d_mask, DF_PASSNO)) {
  329.                 dp->d_passno = 0;
  330.                 BITSET(dp->d_mask, DF_PASSNO);
  331.             } else if (dp->d_freq != 0) {
  332.                 lwarning(dp->d_ioloc,
  333.                     "Pass number for %s:%s is non-zero",
  334.                     dp->d_host->h_hostname, dp->d_dev);
  335.             }
  336.  
  337.             /*
  338.              * "freq"
  339.              */
  340.             if (!ISSET(dp->d_mask, DF_FREQ)) {
  341.                 dp->d_freq = 0;
  342.                 BITSET(dp->d_mask, DF_FREQ);
  343.             } else if (dp->d_freq != 0) {
  344.                 lwarning(dp->d_ioloc,
  345.                     "dump frequency for %s:%s is non-zero",
  346.                     dp->d_host->h_hostname, dp->d_dev);
  347.             }
  348.  
  349.             /*
  350.              * "opts"
  351.              */
  352.             if (!ISSET(dp->d_mask, DF_OPTS))
  353.                 set_disk_fs(dp, DF_OPTS, strdup("swap"));
  354.  
  355.             /*
  356.              * "mount"
  357.              */
  358.             if (!ISSET(dp->d_mask, DF_MOUNT)) {
  359.                 qelem *q = new_que();
  360.                 mount *m = new_mount();
  361.                 m->m_name = strdup("swap");
  362.                 m->m_mount = new_que();
  363.                 ins_que(&m->m_q, q->q_back);
  364.                 dp->d_mount = q;
  365.                 BITSET(dp->d_mask, DF_MOUNT);
  366.             } else {
  367.                 lerror(dp->d_ioloc, "%s: mount field specified for swap partition", dp->d_host->h_hostname);
  368.             }
  369.         } else if (STREQ(dp->d_fstype, "export")) {
  370.             /*
  371.              * "passno"
  372.              */
  373.             if (!ISSET(dp->d_mask, DF_PASSNO)) {
  374.                 dp->d_passno = 0;
  375.                 BITSET(dp->d_mask, DF_PASSNO);
  376.             } else if (dp->d_passno != 0) {
  377.                 lwarning(dp->d_ioloc,
  378.                     "pass number for %s:%s is non-zero",
  379.                     dp->d_host->h_hostname, dp->d_dev);
  380.             }
  381.  
  382.             /*
  383.              * "freq"
  384.              */
  385.             if (!ISSET(dp->d_mask, DF_FREQ)) {
  386.                 dp->d_freq = 0;
  387.                 BITSET(dp->d_mask, DF_FREQ);
  388.             } else if (dp->d_freq != 0) {
  389.                 lwarning(dp->d_ioloc,
  390.                     "dump frequency for %s:%s is non-zero",
  391.                     dp->d_host->h_hostname, dp->d_dev);
  392.             }
  393.  
  394.             /*
  395.              * "opts"
  396.              */
  397.             if (!ISSET(dp->d_mask, DF_OPTS))
  398.                 set_disk_fs(dp, DF_OPTS, strdup("rw,defaults"));
  399.  
  400.         }
  401.     }
  402. }
  403.  
  404. static void fixup_required_mount_info(fp, de)
  405. fsmount *fp;
  406. dict_ent *de;
  407. {
  408.     if (!ISSET(fp->f_mask, FM_FROM)) {
  409.         if (de->de_count != 1) {
  410.             lerror(fp->f_ioloc, "ambiguous mount: %s is a replicated filesystem", fp->f_volname);
  411.         } else {
  412.             dict_data *dd;
  413.             mount *mp = 0;
  414.             ITER(dd, dict_data, &de->de_q) {
  415.                 mp = (mount *) dd->dd_data;
  416.                 break;
  417.             }
  418.             if (!mp)
  419.                 abort();
  420.             fp->f_ref = mp;
  421.             set_fsmount(fp, FM_FROM, mp->m_dk->d_host->h_hostname);
  422.             log("set: %s comes from %s", fp->f_volname, fp->f_from);
  423.         }
  424.     }
  425.  
  426.     if (!ISSET(fp->f_mask, FM_FSTYPE)) {
  427.         set_fsmount(fp, FM_FSTYPE, strdup("nfs"));
  428.         log("set: fstype is %s", fp->f_fstype);
  429.     }
  430.  
  431.     if (!ISSET(fp->f_mask, FM_OPTS)) {
  432.         set_fsmount(fp, FM_OPTS, strdup("rw,nosuid,grpid,defaults"));
  433.         log("set: opts are %s", fp->f_opts);
  434.     }
  435.  
  436.     if (!ISSET(fp->f_mask, FM_LOCALNAME)) {
  437.         if (fp->f_ref) {
  438.             set_fsmount(fp, FM_LOCALNAME, strdup(fp->f_volname));
  439.             log("set: localname is %s", fp->f_localname);
  440.         } else {
  441.             lerror(fp->f_ioloc, "cannot determine localname since volname %s is not uniquely defined", fp->f_volname);
  442.         }
  443.     }
  444. }
  445.  
  446. /*
  447.  * For each disk on a host
  448.  * analyze the mount information
  449.  * and fill in any derivable
  450.  * details.
  451.  */
  452. static void analyze_drives(hp)
  453. host *hp;
  454. {
  455.     qelem *q = hp->h_disk_fs;
  456.     disk_fs *dp;
  457.  
  458.     ITER(dp, disk_fs, q) {
  459.         int req;
  460.         log("Disk %s:", dp->d_dev);
  461.         dp->d_host = hp;
  462.         fixup_required_disk_info(dp);
  463.         req = ~dp->d_mask & DF_REQUIRED;
  464.         if (req)
  465.             show_required(dp->d_ioloc, req, dp->d_dev, hp->h_hostname, disk_fs_strings);
  466.         analyze_dkmounts(dp, dp->d_mount);
  467.     }
  468. }
  469.  
  470. /*
  471.  * Check that all static mounts make sense and
  472.  * that the source volumes exist.
  473.  */
  474. static void analyze_mounts(hp)
  475. host *hp;
  476. {
  477.     qelem *q = hp->h_mount;
  478.     fsmount *fp;
  479.     int netbootp = 0;
  480.  
  481.     ITER(fp, fsmount, q) {
  482.         char *p;
  483.         char *nn = strdup(fp->f_volname);
  484.         int req;
  485.         dict_ent *de;
  486.         int found = 0;
  487.         int matched = 0;
  488.         do {
  489.             p = 0;
  490.             de = find_volname(nn);
  491.             log("Mount: %s (trying %s)", fp->f_volname, nn);
  492.  
  493.             if (de) {
  494.                 found = 1;
  495.                 /*
  496.                  * Check that the from field is really exporting
  497.                  * the filesystem requested.
  498.                  */
  499.                 if (ISSET(fp->f_mask, FM_FROM)) {
  500.                     dict_data *dd;
  501.                     mount *mp2 = 0;
  502.                     ITER(dd, dict_data, &de->de_q) {
  503.                         mount *mp = (mount *) dd->dd_data;
  504.                         if (STREQ(mp->m_dk->d_host->h_hostname, fp->f_from)) {
  505.                             mp2 = mp;
  506.                             break;
  507.                         }
  508.                     }
  509.  
  510.                     if (mp2) {
  511.                         fp->f_ref = mp2;
  512.                         matched = 1;
  513.                         break;
  514.                     }
  515.                 } else {
  516.                     matched = 1;
  517.                     break;
  518.                 }
  519.             }
  520.             p = strrchr(nn, '/');
  521.             if (p)
  522.                 *p = 0;
  523.         } while (de && p);
  524.         free(nn);
  525.  
  526.         if (!found) {
  527.             lerror(fp->f_ioloc, "volname %s unknown", fp->f_volname);
  528.         } else if (matched) {
  529.             fixup_required_mount_info(fp, de);
  530.             req = ~fp->f_mask & FM_REQUIRED;
  531.             if (req) {
  532.                 show_required(fp->f_ioloc, req, fp->f_volname, hp->h_hostname,
  533.                     fsmount_strings);
  534.             } else if (strcmp(fp->f_localname, "/") == 0) {
  535.                 hp->h_netroot = fp;
  536.                 netbootp |= FM_NETROOT;
  537.             } else if (strcmp(fp->f_localname, "swap") == 0) {
  538.                 hp->h_netswap = fp;
  539.                 netbootp |= FM_NETSWAP;
  540.             }
  541.         } else {
  542.             lerror(fp->f_ioloc, "volname %s not exported from %s", fp->f_volname,
  543.                 fp->f_from ? fp->f_from : "anywhere");
  544.         }
  545.     }
  546.  
  547.     if (netbootp && (netbootp != FM_NETBOOT))
  548.         lerror(hp->h_ioloc, "network booting requires both root and swap areas");
  549. }
  550.  
  551. void analyze_hosts(q)
  552. qelem *q;
  553. {
  554.     host *hp;
  555.  
  556.     show_area_being_processed("analyze hosts", 5);
  557.  
  558.     /*
  559.      * Check all drives
  560.      */
  561.     ITER(hp, host, q) {
  562.         log("disks on host %s", hp->h_hostname);
  563.         show_new("ana-host");
  564.         hp->h_hostpath = compute_hostpath(hp->h_hostname);
  565.  
  566.         if (hp->h_disk_fs)
  567.             analyze_drives(hp);
  568.  
  569.     }
  570.  
  571.     show_area_being_processed("analyze mounts", 5);
  572.  
  573.     /*
  574.      * Check static mounts
  575.      */
  576.     ITER(hp, host, q) {
  577.         log("mounts on host %s", hp->h_hostname);
  578.         show_new("ana-mount");
  579.         if (hp->h_mount)
  580.             analyze_mounts(hp);
  581.  
  582.     }
  583. }
  584.  
  585. /*
  586.  * Check an automount request
  587.  */
  588. static void analyze_automount(ap)
  589. automount *ap;
  590. {
  591.     dict_ent *de = find_volname(ap->a_volname);
  592.     if (de) {
  593.         ap->a_mounted = de;
  594.     } else {
  595.         if (STREQ(ap->a_volname, ap->a_name))
  596.             lerror(ap->a_ioloc, "unknown volname %s automounted", ap->a_volname);
  597.         else
  598.             lerror(ap->a_ioloc, "unknown volname %s automounted on %s", ap->a_volname, ap->a_name);
  599.     }
  600. }
  601.  
  602. static void analyze_automount_tree(q, pref, lvl)
  603. qelem *q;
  604. char *pref;
  605. int lvl;
  606. {
  607.     automount *ap;
  608.  
  609.     ITER(ap, automount, q) {
  610.         char nname[1024];
  611.         if (lvl > 0 || ap->a_mount)
  612.             if (ap->a_name[1] && strchr(ap->a_name+1, '/'))
  613.                 lerror(ap->a_ioloc, "not allowed '/' in a directory name");
  614.         sprintf(nname, "%s/%s", pref, ap->a_name);
  615.         free(ap->a_name);
  616.         ap->a_name = strdup(nname[1] == '/' ? nname+1 : nname);
  617.         log("automount point %s:", ap->a_name);
  618.         show_new("ana-automount");
  619.         if (ap->a_mount) {
  620.             analyze_automount_tree(ap->a_mount, ap->a_name, lvl+1);
  621.         } else if (ap->a_volname) {
  622.             log("\tautomount from %s", ap->a_volname);
  623.             analyze_automount(ap);
  624.         } else if (ap->a_symlink) {
  625.             log("\tsymlink to %s", ap->a_symlink);
  626.         } else {
  627.             ap->a_volname = strdup(ap->a_name);
  628.             log("\timplicit automount from %s", ap->a_volname);
  629.             analyze_automount(ap);
  630.         }
  631.     }
  632. }
  633.  
  634. void analyze_automounts(q)
  635. qelem *q;
  636. {
  637.     auto_tree *tp;
  638.  
  639.     show_area_being_processed("analyze automount", 5);
  640.     /*
  641.      * q is a list of automounts
  642.      */
  643.     ITER(tp, auto_tree, q)
  644.         analyze_automount_tree(tp->t_mount, "", 0);
  645. }
  646.