xref: /illumos-gate/usr/src/cmd/fs.d/ufs/quota/quota.c (revision 03831d35)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 /*	Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T	*/
27 /*	  All Rights Reserved  	*/
28 
29 /*
30  * University Copyright- Copyright (c) 1982, 1986, 1988
31  * The Regents of the University of California
32  * All Rights Reserved
33  *
34  * University Acknowledgment- Portions of this document are derived from
35  * software developed by the University of California, Berkeley, and its
36  * contributors.
37  */
38 
39 #pragma ident	"%Z%%M%	%I%	%E% SMI"
40 
41 /*
42  * Disk quota reporting program.
43  */
44 #include <stdio.h>
45 #include <sys/mnttab.h>
46 #include <ctype.h>
47 #include <pwd.h>
48 #include <errno.h>
49 #include <fcntl.h>
50 #include <memory.h>
51 #include <sys/time.h>
52 #include <sys/param.h>
53 #include <sys/types.h>
54 #include <sys/sysmacros.h>
55 #include <sys/mntent.h>
56 #include <sys/file.h>
57 #include <sys/stat.h>
58 #include <sys/fs/ufs_quota.h>
59 
60 int	vflag;
61 int	nolocalquota;
62 
63 extern int	optind;
64 extern char	*optarg;
65 
66 #define	QFNAME	"quotas"
67 
68 #if DEV_BSIZE < 1024
69 #define	kb(x)	((x) / (1024 / DEV_BSIZE))
70 #else
71 #define	kb(x)	((x) * (DEV_BSIZE / 1024))
72 #endif
73 
74 static int getnfsquota(char *, char *, uid_t, struct dqblk *);
75 static void showuid(uid_t);
76 static void showquotas(uid_t, char *);
77 static void warn(struct mnttab *, struct dqblk *);
78 static void heading(uid_t, char *);
79 static void prquota(struct mnttab *, struct dqblk *);
80 static void fmttime(char *, long);
81 
82 int
83 main(int argc, char *argv[])
84 {
85 	int	opt;
86 	int	i;
87 	int	status = 0;
88 
89 	while ((opt = getopt(argc, argv, "vV")) != EOF) {
90 		switch (opt) {
91 
92 		case 'v':
93 			vflag++;
94 			break;
95 
96 		case 'V':		/* Print command line */
97 			{
98 			char	*opt_text;
99 			int	opt_count;
100 
101 			(void) fprintf(stdout, "quota -F UFS ");
102 			for (opt_count = 1; opt_count < argc; opt_count++) {
103 				opt_text = argv[opt_count];
104 				if (opt_text)
105 				    (void) fprintf(stdout, " %s ", opt_text);
106 			}
107 			(void) fprintf(stdout, "\n");
108 			}
109 			break;
110 
111 		case '?':
112 			fprintf(stderr, "ufs usage: quota [-v] [username]\n");
113 			exit(32);
114 		}
115 	}
116 	if (quotactl(Q_ALLSYNC, NULL, (uid_t)0, NULL) < 0 && errno == EINVAL) {
117 		if (vflag)
118 			fprintf(stderr, "There are no quotas on this system\n");
119 		nolocalquota++;
120 	}
121 	if (argc == optind) {
122 		showuid(getuid());
123 		exit(0);
124 	}
125 	for (i = optind; i < argc; i++) {
126 		if (alldigits(argv[i])) {
127 			showuid((uid_t)atoi(argv[i]));
128 		} else
129 			status |= showname(argv[i]);
130 	}
131 	return (status);
132 }
133 
134 static void
135 showuid(uid_t uid)
136 {
137 	struct passwd *pwd = getpwuid(uid);
138 
139 	if (uid == 0) {
140 		if (vflag)
141 			printf("no disk quota for uid 0\n");
142 		return;
143 	}
144 	if (pwd == NULL)
145 		showquotas(uid, "(no account)");
146 	else
147 		showquotas(uid, pwd->pw_name);
148 }
149 
150 int
151 showname(char *name)
152 {
153 	struct passwd *pwd = getpwnam(name);
154 
155 	if (pwd == NULL) {
156 		fprintf(stderr, "quota: %s: unknown user\n", name);
157 		return (32);
158 	}
159 	if (pwd->pw_uid == 0) {
160 		if (vflag)
161 			printf("no disk quota for %s (uid 0)\n", name);
162 		return (0);
163 	}
164 	showquotas(pwd->pw_uid, name);
165 	return (0);
166 }
167 
168 #include "../../nfs/lib/replica.h"
169 
170 static void
171 showquotas(uid_t uid, char *name)
172 {
173 	struct mnttab mnt;
174 	FILE *mtab;
175 	struct dqblk dqblk;
176 	uid_t myuid;
177 
178 	myuid = getuid();
179 	if (uid != myuid && myuid != 0) {
180 		printf("quota: %s (uid %d): permission denied\n", name, uid);
181 		exit(32);
182 	}
183 	if (vflag)
184 		heading(uid, name);
185 	mtab = fopen(MNTTAB, "r");
186 	while (getmntent(mtab, &mnt) == NULL) {
187 		if (strcmp(mnt.mnt_fstype, MNTTYPE_UFS) == 0) {
188 			if (nolocalquota ||
189 			    (quotactl(Q_GETQUOTA,
190 				mnt.mnt_mountp, uid, &dqblk) != 0 &&
191 				!(vflag && getdiskquota(&mnt, uid, &dqblk))))
192 					continue;
193 		} else if (strcmp(mnt.mnt_fstype, MNTTYPE_NFS) == 0) {
194 
195 			struct replica *rl;
196 			int count;
197 
198 			if (hasopt(MNTOPT_NOQUOTA, mnt.mnt_mntopts))
199 				continue;
200 
201 			/*
202 			 * Skip quota processing if mounted with public
203 			 * option. We are not likely to be able to pierce
204 			 * a fire wall to contact the quota server.
205 			 */
206 			if (hasopt(MNTOPT_PUBLIC, mnt.mnt_mntopts))
207 				continue;
208 
209 			rl = parse_replica(mnt.mnt_special, &count);
210 
211 			if (rl == NULL) {
212 
213 				if (count < 0)
214 					fprintf(stderr, "cannot find hostname "
215 					    "and/or pathname for %s\n",
216 					    mnt.mnt_mountp);
217 				else
218 					fprintf(stderr, "no memory to parse "
219 					    "mnttab entry for %s\n",
220 					    mnt.mnt_mountp);
221 				continue;
222 			}
223 
224 			/*
225 			 * We skip quota reporting on mounts with replicas
226 			 * for the following reasons:
227 			 *
228 			 * (1) Very little point in reporting quotas on
229 			 * a set of read-only replicas ... how will the
230 			 * user correct the problem?
231 			 *
232 			 * (2) Which replica would we report the quota
233 			 * for? If we pick the current replica, what
234 			 * happens when a fail over event occurs? The
235 			 * next time quota is run, the quota will look
236 			 * all different, or there won't even be one.
237 			 * This has the potential to break scripts.
238 			 *
239 			 * If we prnt quouta for all replicas, how do
240 			 * we present the output without breaking scripts?
241 			 */
242 
243 			if (count > 1) {
244 				free_replica(rl, count);
245 				continue;
246 			}
247 
248 			/*
249 			 * Skip file systems mounted using public fh.
250 			 * We are not likely to be able to pierce
251 			 * a fire wall to contact the quota server.
252 			 */
253 			if (strcmp(rl[0].host, "nfs") == 0 &&
254 			    strncmp(rl[0].path, "//", 2) == 0) {
255 				free_replica(rl, count);
256 				continue;
257 			}
258 
259 			if (!getnfsquota(rl[0].host, rl[0].path, uid, &dqblk)) {
260 				free_replica(rl, count);
261 				continue;
262 			}
263 
264 			free_replica(rl, count);
265 
266 		} else {
267 			continue;
268 		}
269 		if (dqblk.dqb_bsoftlimit == 0 && dqblk.dqb_bhardlimit == 0 &&
270 		    dqblk.dqb_fsoftlimit == 0 && dqblk.dqb_fhardlimit == 0)
271 			continue;
272 		if (vflag)
273 			prquota(&mnt, &dqblk);
274 		else
275 			warn(&mnt, &dqblk);
276 	}
277 	fclose(mtab);
278 }
279 
280 static void
281 warn(struct mnttab *mntp, struct dqblk *dqp)
282 {
283 	struct timeval tv;
284 
285 	time(&(tv.tv_sec));
286 	tv.tv_usec = 0;
287 	if (dqp->dqb_bhardlimit &&
288 		dqp->dqb_curblocks >= dqp->dqb_bhardlimit) {
289 		printf("Block limit reached on %s\n", mntp->mnt_mountp);
290 	} else if (dqp->dqb_bsoftlimit &&
291 		dqp->dqb_curblocks >= dqp->dqb_bsoftlimit) {
292 		if (dqp->dqb_btimelimit == 0) {
293 			printf("Over disk quota on %s, remove %luK\n",
294 			    mntp->mnt_mountp,
295 			    kb(dqp->dqb_curblocks - dqp->dqb_bsoftlimit + 1));
296 		} else if (dqp->dqb_btimelimit > tv.tv_sec) {
297 			char btimeleft[80];
298 
299 			fmttime(btimeleft, dqp->dqb_btimelimit - tv.tv_sec);
300 			printf("Over disk quota on %s, remove %luK within %s\n",
301 			    mntp->mnt_mountp,
302 			    kb(dqp->dqb_curblocks - dqp->dqb_bsoftlimit + 1),
303 			    btimeleft);
304 		} else {
305 			printf(
306 		"Over disk quota on %s, time limit has expired, remove %luK\n",
307 			    mntp->mnt_mountp,
308 			    kb(dqp->dqb_curblocks - dqp->dqb_bsoftlimit + 1));
309 		}
310 	}
311 	if (dqp->dqb_fhardlimit &&
312 	    dqp->dqb_curfiles >= dqp->dqb_fhardlimit) {
313 		printf("File count limit reached on %s\n", mntp->mnt_mountp);
314 	} else if (dqp->dqb_fsoftlimit &&
315 	    dqp->dqb_curfiles >= dqp->dqb_fsoftlimit) {
316 		if (dqp->dqb_ftimelimit == 0) {
317 			printf("Over file quota on %s, remove %lu file%s\n",
318 			    mntp->mnt_mountp,
319 			    dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1,
320 			    ((dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1) > 1 ?
321 				"s" : ""));
322 		} else if (dqp->dqb_ftimelimit > tv.tv_sec) {
323 			char ftimeleft[80];
324 
325 			fmttime(ftimeleft, dqp->dqb_ftimelimit - tv.tv_sec);
326 			printf(
327 "Over file quota on %s, remove %lu file%s within %s\n",
328 			    mntp->mnt_mountp,
329 			    dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1,
330 			    ((dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1) > 1 ?
331 				"s" : ""), ftimeleft);
332 		} else {
333 			printf(
334 "Over file quota on %s, time limit has expired, remove %lu file%s\n",
335 			    mntp->mnt_mountp,
336 			    dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1,
337 			    ((dqp->dqb_curfiles - dqp->dqb_fsoftlimit + 1) > 1 ?
338 				"s" : ""));
339 		}
340 	}
341 }
342 
343 static void
344 heading(uid_t uid, char *name)
345 {
346 	printf("Disk quotas for %s (uid %ld):\n", name, (long)uid);
347 	printf("%-12s %7s%7s%7s%12s%7s%7s%7s%12s\n",
348 		"Filesystem",
349 		"usage",
350 		"quota",
351 		"limit",
352 		"timeleft",
353 		"files",
354 		"quota",
355 		"limit",
356 		"timeleft");
357 }
358 
359 static void
360 prquota(struct mnttab *mntp, struct dqblk *dqp)
361 {
362 	struct timeval tv;
363 	char ftimeleft[80], btimeleft[80];
364 	char *cp;
365 
366 	time(&(tv.tv_sec));
367 	tv.tv_usec = 0;
368 	if (dqp->dqb_bsoftlimit && dqp->dqb_curblocks >= dqp->dqb_bsoftlimit) {
369 		if (dqp->dqb_btimelimit == 0) {
370 			strcpy(btimeleft, "NOT STARTED");
371 		} else if (dqp->dqb_btimelimit > tv.tv_sec) {
372 			fmttime(btimeleft, dqp->dqb_btimelimit - tv.tv_sec);
373 		} else {
374 			strcpy(btimeleft, "EXPIRED");
375 		}
376 	} else {
377 		btimeleft[0] = '\0';
378 	}
379 	if (dqp->dqb_fsoftlimit && dqp->dqb_curfiles >= dqp->dqb_fsoftlimit) {
380 		if (dqp->dqb_ftimelimit == 0) {
381 			strcpy(ftimeleft, "NOT STARTED");
382 		} else if (dqp->dqb_ftimelimit > tv.tv_sec) {
383 			fmttime(ftimeleft, dqp->dqb_ftimelimit - tv.tv_sec);
384 		} else {
385 			strcpy(ftimeleft, "EXPIRED");
386 		}
387 	} else {
388 		ftimeleft[0] = '\0';
389 	}
390 	if (strlen(mntp->mnt_mountp) > 12) {
391 		printf("%s\n", mntp->mnt_mountp);
392 		cp = "";
393 	} else {
394 		cp = mntp->mnt_mountp;
395 	}
396 	printf("%-12.12s %7d %6d %6d %11s %6d %6d %6d %11s\n",
397 	    cp,
398 	    kb(dqp->dqb_curblocks),
399 	    kb(dqp->dqb_bsoftlimit),
400 	    kb(dqp->dqb_bhardlimit),
401 	    btimeleft,
402 	    dqp->dqb_curfiles,
403 	    dqp->dqb_fsoftlimit,
404 	    dqp->dqb_fhardlimit,
405 	    ftimeleft);
406 }
407 
408 static void
409 fmttime(char *buf, long time)
410 {
411 	int i;
412 	static struct {
413 		int c_secs;		/* conversion units in secs */
414 		char *c_str;		/* unit string */
415 	} cunits [] = {
416 		{60*60*24*28, "months"},
417 		{60*60*24*7, "weeks"},
418 		{60*60*24, "days"},
419 		{60*60, "hours"},
420 		{60, "mins"},
421 		{1, "secs"}
422 	};
423 
424 	if (time <= 0) {
425 		strcpy(buf, "EXPIRED");
426 		return;
427 	}
428 	for (i = 0; i < sizeof (cunits)/sizeof (cunits[0]); i++) {
429 		if (time >= cunits[i].c_secs)
430 			break;
431 	}
432 	sprintf(buf, "%.1f %s", (double)time/cunits[i].c_secs, cunits[i].c_str);
433 }
434 
435 int
436 alldigits(char *s)
437 {
438 	int c;
439 
440 	c = *s++;
441 	do {
442 		if (!isdigit(c))
443 			return (0);
444 	} while (c = *s++);
445 	return (1);
446 }
447 
448 int
449 getdiskquota(struct mnttab *mntp, uid_t uid, struct dqblk *dqp)
450 {
451 	int fd;
452 	dev_t fsdev;
453 	struct stat64 statb;
454 	char qfilename[MAXPATHLEN];
455 
456 	if (stat64(mntp->mnt_special, &statb) < 0 ||
457 	    (statb.st_mode & S_IFMT) != S_IFBLK)
458 		return (0);
459 	fsdev = statb.st_rdev;
460 	(void) snprintf(qfilename, sizeof (qfilename), "%s/%s",
461 		mntp->mnt_mountp, QFNAME);
462 	if (stat64(qfilename, &statb) < 0 || statb.st_dev != fsdev)
463 		return (0);
464 	if ((fd = open64(qfilename, O_RDONLY)) < 0)
465 		return (0);
466 	(void) llseek(fd, (offset_t)dqoff(uid), L_SET);
467 	switch (read(fd, dqp, sizeof (struct dqblk))) {
468 	case 0:				/* EOF */
469 		/*
470 		 * Convert implicit 0 quota (EOF)
471 		 * into an explicit one (zero'ed dqblk).
472 		 */
473 		memset((caddr_t)dqp, 0, sizeof (struct dqblk));
474 		break;
475 
476 	case sizeof (struct dqblk):	/* OK */
477 		break;
478 
479 	default:			/* ERROR */
480 		close(fd);
481 		return (0);
482 	}
483 	close(fd);
484 	return (1);
485 }
486 
487 int
488 quotactl(int cmd, char *mountp, uid_t uid, caddr_t addr)
489 {
490 	int		fd;
491 	int		status;
492 	struct quotctl	quota;
493 	char		qfile[MAXPATHLEN];
494 
495 	FILE		*fstab;
496 	struct mnttab	mnt;
497 
498 
499 	if ((mountp == NULL) && (cmd == Q_ALLSYNC)) {
500 	/*
501 	 * Find the mount point of any mounted file system. This is
502 	 * because the ioctl that implements the quotactl call has
503 	 * to go to a real file, and not to the block device.
504 	 */
505 		if ((fstab = fopen(MNTTAB, "r")) == NULL) {
506 			fprintf(stderr, "%s: ", MNTTAB);
507 			perror("open");
508 			exit(32);
509 		}
510 		fd = -1;
511 		while ((status = getmntent(fstab, &mnt)) == NULL) {
512 			if (strcmp(mnt.mnt_fstype, MNTTYPE_UFS) != 0 ||
513 				hasopt(MNTOPT_RO, mnt.mnt_mntopts))
514 				continue;
515 			if ((strlcpy(qfile, mnt.mnt_mountp,
516 				sizeof (qfile)) >= sizeof (qfile)) ||
517 			    (strlcat(qfile, "/" QFNAME, sizeof (qfile)) >=
518 				sizeof (qfile))) {
519 				continue;
520 			}
521 			if ((fd = open64(qfile, O_RDONLY)) != -1)
522 				break;
523 		}
524 		fclose(fstab);
525 		if (fd == -1) {
526 			errno = ENOENT;
527 			return (-1);
528 		}
529 	} else {
530 		if (mountp == NULL || mountp[0] == '\0') {
531 			errno = ENOENT;
532 			return (-1);
533 		}
534 		if ((strlcpy(qfile, mountp, sizeof (qfile)) >= sizeof
535 			(qfile)) ||
536 		    (strlcat(qfile, "/" QFNAME, sizeof (qfile)) >= sizeof
537 			(qfile))) {
538 			errno = ENOENT;
539 			return (-1);
540 		}
541 		if ((fd = open64(qfile, O_RDONLY)) < 0)
542 			return (-1);
543 	}	/* else */
544 	quota.op = cmd;
545 	quota.uid = uid;
546 	quota.addr = addr;
547 	status = ioctl(fd, Q_QUOTACTL, &quota);
548 	if (fd != 0)
549 		close(fd);
550 	return (status);
551 }
552 
553 
554 /*
555  * Return 1 if opt appears in optlist
556  */
557 int
558 hasopt(char *opt, char *optlist)
559 {
560 	char *value;
561 	char *opts[2];
562 
563 	opts[0] = opt;
564 	opts[1] = NULL;
565 
566 	if (optlist == NULL)
567 		return (0);
568 	while (*optlist != '\0') {
569 		if (getsubopt(&optlist, opts, &value) == 0)
570 			return (1);
571 	}
572 	return (0);
573 }
574 
575 #include <rpc/rpc.h>
576 #include <netdb.h>
577 #include <rpcsvc/rquota.h>
578 
579 static int
580 getnfsquota(char *hostp, char *path, uid_t uid, struct dqblk *dqp)
581 {
582 	struct getquota_args gq_args;
583 	struct getquota_rslt gq_rslt;
584 	struct rquota *rquota;
585 	extern char *strchr();
586 
587 	gq_args.gqa_pathp = path;
588 	gq_args.gqa_uid = uid;
589 	if (callaurpc(hostp, RQUOTAPROG, RQUOTAVERS,
590 	    (vflag? RQUOTAPROC_GETQUOTA: RQUOTAPROC_GETACTIVEQUOTA),
591 	    xdr_getquota_args, &gq_args, xdr_getquota_rslt, &gq_rslt) != 0) {
592 		return (0);
593 	}
594 	switch (gq_rslt.status) {
595 	case Q_OK:
596 		{
597 		struct timeval tv;
598 		u_longlong_t limit;
599 
600 		rquota = &gq_rslt.getquota_rslt_u.gqr_rquota;
601 
602 		if (!vflag && rquota->rq_active == FALSE)
603 			return (0);
604 		gettimeofday(&tv, NULL);
605 		limit = (u_longlong_t)(rquota->rq_bhardlimit) *
606 		    rquota->rq_bsize / DEV_BSIZE;
607 		dqp->dqb_bhardlimit = limit;
608 		limit = (u_longlong_t)(rquota->rq_bsoftlimit) *
609 		    rquota->rq_bsize / DEV_BSIZE;
610 		dqp->dqb_bsoftlimit = limit;
611 		limit = (u_longlong_t)(rquota->rq_curblocks) *
612 		    rquota->rq_bsize / DEV_BSIZE;
613 		dqp->dqb_curblocks = limit;
614 		dqp->dqb_fhardlimit = rquota->rq_fhardlimit;
615 		dqp->dqb_fsoftlimit = rquota->rq_fsoftlimit;
616 		dqp->dqb_curfiles = rquota->rq_curfiles;
617 		dqp->dqb_btimelimit =
618 		    tv.tv_sec + rquota->rq_btimeleft;
619 		dqp->dqb_ftimelimit =
620 		    tv.tv_sec + rquota->rq_ftimeleft;
621 		return (1);
622 		}
623 
624 	case Q_NOQUOTA:
625 		break;
626 
627 	case Q_EPERM:
628 		fprintf(stderr, "quota permission error, host: %s\n", hostp);
629 		break;
630 
631 	default:
632 		fprintf(stderr, "bad rpc result, host: %s\n",  hostp);
633 		break;
634 	}
635 	return (0);
636 }
637 
638 int
639 callaurpc(char *host, int prognum, int versnum, int procnum,
640 		xdrproc_t inproc, char *in, xdrproc_t outproc, char *out)
641 {
642 	static enum clnt_stat clnt_stat;
643 	struct timeval tottimeout;
644 
645 	static CLIENT *cl = NULL;
646 	static int oldprognum, oldversnum;
647 	static char oldhost[MAXHOSTNAMELEN+1];
648 
649 	/*
650 	 * Cache the client handle in case there are lots
651 	 * of entries in the /etc/mnttab for the same
652 	 * server. If the server returns an error, don't
653 	 * make further calls.
654 	 */
655 	if (cl == NULL || oldprognum != prognum || oldversnum != versnum ||
656 		strcmp(oldhost, host) != 0) {
657 		if (cl) {
658 			clnt_destroy(cl);
659 			cl = NULL;
660 		}
661 		cl = clnt_create(host, prognum, versnum, "udp");
662 		if (cl == NULL)
663 			return ((int)RPC_TIMEDOUT);
664 		if ((cl->cl_auth = authunix_create_default()) == NULL) {
665 			clnt_destroy(cl);
666 			return (RPC_CANTSEND);
667 		}
668 		oldprognum = prognum;
669 		oldversnum = versnum;
670 		(void) strcpy(oldhost, host);
671 		clnt_stat = RPC_SUCCESS;
672 	}
673 
674 	if (clnt_stat != RPC_SUCCESS)
675 		return ((int)clnt_stat);	/* don't bother retrying */
676 
677 	tottimeout.tv_sec  = 5;
678 	tottimeout.tv_usec = 0;
679 	clnt_stat = clnt_call(cl, procnum, inproc, in,
680 	    outproc, out, tottimeout);
681 
682 	return ((int)clnt_stat);
683 }
684