xref: /freebsd/usr.sbin/tzsetup/tzsetup.c (revision 380a989b3223d455375b4fae70fd0b9bdd43bafb)
1 /*
2  * Copyright 1996 Massachusetts Institute of Technology
3  *
4  * Permission to use, copy, modify, and distribute this software and
5  * its documentation for any purpose and without fee is hereby
6  * granted, provided that both the above copyright notice and this
7  * permission notice appear in all copies, that both the above
8  * copyright notice and this permission notice appear in all
9  * supporting documentation, and that the name of M.I.T. not be used
10  * in advertising or publicity pertaining to distribution of the
11  * software without specific, written prior permission.  M.I.T. makes
12  * no representations about the suitability of this software for any
13  * purpose.  It is provided "as is" without express or implied
14  * warranty.
15  *
16  * THIS SOFTWARE IS PROVIDED BY M.I.T. ``AS IS''.  M.I.T. DISCLAIMS
17  * ALL EXPRESS OR IMPLIED WARRANTIES WITH REGARD TO THIS SOFTWARE,
18  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT
20  * SHALL M.I.T. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
26  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27  * SUCH DAMAGE.
28  */
29 
30 /*
31  * Second attempt at a `tzmenu' program, using the separate description
32  * files provided in newer tzdata releases.
33  */
34 
35 #ifndef lint
36 static const char rcsid[] =
37 	"$Id: tzsetup.c,v 1.10 1998/01/10 15:55:11 steve Exp $";
38 #endif /* not lint */
39 
40 #include <sys/types.h>
41 #include <dialog.h>
42 #include <err.h>
43 #include <errno.h>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <string.h>
47 #include <unistd.h>
48 
49 #include <sys/fcntl.h>
50 #include <sys/queue.h>
51 #include <sys/stat.h>
52 
53 #include "paths.h"
54 
55 static int reallydoit = 1;
56 
57 static int continent_country_menu(dialogMenuItem *);
58 static int set_zone_multi(dialogMenuItem *);
59 static int set_zone_whole_country(dialogMenuItem *);
60 static int set_zone_menu(dialogMenuItem *);
61 
62 struct continent {
63 	dialogMenuItem *menu;
64 	int nitems;
65 	int ch;
66 	int sc;
67 };
68 
69 static struct continent africa, america, antarctica, arctic, asia, atlantic;
70 static struct continent australia, europe, indian, pacific;
71 
72 static struct continent_names {
73 	char *name;
74 	struct continent *continent;
75 } continent_names[] = {
76 	{ "Africa", &africa }, { "America", &america },
77 	{ "Antarctica", &antarctica }, { "Arctic", &arctic },
78 	{ "Asia", &asia },
79 	{ "Atlantic", &atlantic }, { "Australia", &australia },
80 	{ "Europe", &europe }, { "Indian", &indian }, { "Pacific", &pacific }
81 };
82 
83 static dialogMenuItem continents[] = {
84 	{ "1", "Africa", 0, continent_country_menu, 0, &africa },
85 	{ "2", "America -- North and South", 0, continent_country_menu, 0,
86 		  &america },
87 	{ "3", "Antarctica", 0, continent_country_menu, 0, &antarctica },
88 	{ "4", "Arctic Ocean", 0, continent_country_menu, 0, &arctic },
89 	{ "5", "Asia", 0, continent_country_menu, 0, &asia },
90 	{ "6", "Atlantic Ocean", 0, continent_country_menu, 0, &atlantic },
91 	{ "7", "Australia", 0, continent_country_menu, 0, &australia },
92 	{ "8", "Europe", 0, continent_country_menu, 0, &europe },
93 	{ "9", "Indian Ocean", 0, continent_country_menu, 0, &indian },
94 	{ "0", "Pacific Ocean", 0, continent_country_menu, 0, &pacific }
95 };
96 #define NCONTINENTS ((sizeof continents)/(sizeof continents[0]))
97 #define OCEANP(x) ((x) == 3 || (x) == 5 || (x) == 8 || (x) == 9)
98 
99 static int
100 continent_country_menu(dialogMenuItem *continent)
101 {
102 	int rv;
103 	struct continent *contp = continent->data;
104 	char title[256];
105 	int isocean = OCEANP(continent - continents);
106 	int menulen;
107 
108 	/* Short cut -- if there's only one country, don't post a menu. */
109 	if (contp->nitems == 1) {
110 		return set_zone_menu(&contp->menu[0]);
111 	}
112 
113 	/* It's amazing how much good grammar really matters... */
114 	if (!isocean)
115 		snprintf(title, sizeof title, "Countries in %s",
116 			 continent->title);
117 	else
118 		snprintf(title, sizeof title, "Islands and groups in the %s",
119 			 continent->title);
120 
121 	menulen = contp->nitems < 16 ? contp->nitems : 16;
122 	rv = dialog_menu(title, (isocean ? "Select an island or group"
123                                  : "Select a country"), -1, -1, menulen,
124 			 -contp->nitems, contp->menu, 0, &contp->ch,
125 			 &contp->sc);
126 	if (rv == 0)
127 		return DITEM_LEAVE_MENU;
128 	return DITEM_RECREATE;
129 }
130 
131 static struct continent *
132 find_continent(const char *name)
133 {
134 	int i;
135 
136 	for (i = 0; i < NCONTINENTS; i++) {
137 		if (strcmp(name, continent_names[i].name) == 0)
138 			return continent_names[i].continent;
139 	}
140 	return 0;
141 }
142 
143 struct country {
144 	char *name;
145 	char *tlc;
146 	int nzones;
147 	char *filename;		/* use iff nzones < 0 */
148 	struct continent *continent; /* use iff nzones < 0 */
149 	TAILQ_HEAD(, zone) zones; /* use iff nzones > 0 */
150 	dialogMenuItem *submenu; /* use iff nzones > 0 */
151 };
152 
153 struct zone {
154 	TAILQ_ENTRY(zone) link;
155 	char *descr;
156 	char *filename;
157 	struct continent *continent;
158 };
159 
160 /*
161  * This is the easiest organization... we use ISO 3166 country codes,
162  * of the two-letter variety, so we just size this array to suit.
163  * Beats worrying about dynamic allocation.
164  */
165 #define NCOUNTRIES	(26*26)
166 static struct country countries[NCOUNTRIES];
167 #define CODE2INT(s) ((s[0] - 'A') * 26 + (s[1] - 'A'))
168 
169 /*
170  * Read the ISO 3166 country code database in _PATH_ISO3166
171  * (/usr/share/misc/iso3166).  On error, exit via err(3).
172  */
173 static void
174 read_iso3166_table(void)
175 {
176 	FILE *fp;
177 	char *s, *t, *name;
178 	size_t len;
179 	int lineno;
180 	struct country *cp;
181 
182 	fp = fopen(_PATH_ISO3166, "r");
183 	if (!fp)
184 		err(1, _PATH_ISO3166);
185 	lineno = 0;
186 
187 	while ((s = fgetln(fp, &len)) != 0) {
188 		lineno++;
189 		if (s[len - 1] != '\n')
190 			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
191 		s[len - 1] = '\0';
192 		if (s[0] == '#' || strspn(s, " \t") == len - 1)
193 			continue;
194 
195 		/* Isolate the two-letter code. */
196 		t = strsep(&s, "\t");
197 		if (t == 0 || strlen(t) != 2)
198 			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
199 		if (t[0] < 'A' || t[0] > 'Z' || t[1] < 'A' || t[1] > 'Z')
200 			errx(1, _PATH_ISO3166 ":%d: invalid code `%s'",
201 			     lineno, t);
202 
203 		/* Now skip past the three-letter and numeric codes. */
204 		name = strsep(&s, "\t"); /* 3-let */
205 		if (name == 0 || strlen(name) != 3)
206 			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
207 		name = strsep(&s, "\t"); /* numeric */
208 		if (name == 0 || strlen(name) != 3)
209 			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
210 
211 		name = s;
212 
213 		cp = &countries[CODE2INT(t)];
214 		if (cp->name)
215 			errx(1, _PATH_ISO3166
216 			     ":%d: country code `%s' multiply defined: %s",
217 			     lineno, t, cp->name);
218 		cp->name = strdup(name);
219 		cp->tlc = strdup(t);
220 	}
221 
222 	fclose(fp);
223 }
224 
225 static void
226 add_zone_to_country(int lineno, const char *tlc, const char *descr,
227 		    const char *file, struct continent *cont)
228 {
229 	struct zone *zp;
230 	struct country *cp;
231 
232 	if (tlc[0] < 'A' || tlc[0] > 'Z' || tlc[1] < 'A' || tlc[1] > 'Z')
233 		errx(1, _PATH_ZONETAB ":%d: country code `%s' invalid",
234 		     lineno, tlc);
235 
236 	cp = &countries[CODE2INT(tlc)];
237 	if (cp->name == 0)
238 		errx(1, _PATH_ZONETAB ":%d: country code `%s' unknown",
239 		     lineno, tlc);
240 
241 	if (descr) {
242 		if (cp->nzones < 0)
243 			errx(1, _PATH_ZONETAB
244 			     ":%d: conflicting zone definition", lineno);
245 
246 		zp = malloc(sizeof *zp);
247 		if (zp == 0)
248 			err(1, "malloc(%lu)", (unsigned long)sizeof *zp);
249 
250 		if (cp->nzones == 0)
251 			TAILQ_INIT(&cp->zones);
252 
253 		zp->descr = strdup(descr);
254 		zp->filename = strdup(file);
255 		zp->continent = cont;
256 		TAILQ_INSERT_TAIL(&cp->zones, zp, link);
257 		cp->nzones++;
258 	} else {
259 		if (cp->nzones > 0)
260 			errx(1, _PATH_ZONETAB
261 			     ":%d: zone must have description", lineno);
262 		if (cp->nzones < 0)
263 			errx(1, _PATH_ZONETAB
264 			     ":%d: zone multiply defined", lineno);
265 		cp->nzones = -1;
266 		cp->filename = strdup(file);
267 		cp->continent = cont;
268 	}
269 }
270 
271 /*
272  * This comparison function intentionally sorts all of the null-named
273  * ``countries''---i.e., the codes that don't correspond to a real
274  * country---to the end.  Everything else is lexical by country name.
275  */
276 static int
277 compare_countries(const void *xa, const void *xb)
278 {
279 	const struct country *a = xa, *b = xb;
280 
281 	if (a->name == 0 && b->name == 0)
282 		return 0;
283 	if (a->name == 0 && b->name != 0)
284 		return 1;
285 	if (b->name == 0)
286 		return -1;
287 
288 	return strcmp(a->name, b->name);
289 }
290 
291 /*
292  * This must be done AFTER all zone descriptions are read, since it breaks
293  * CODE2INT().
294  */
295 static void
296 sort_countries(void)
297 {
298 	qsort(countries, NCOUNTRIES, sizeof countries[0], compare_countries);
299 }
300 
301 static void
302 read_zones(void)
303 {
304 	FILE *fp;
305 	char *line;
306 	size_t len;
307 	int lineno;
308 	char *tlc, *coord, *file, *descr, *p;
309 	char contbuf[16];
310 	struct continent *cont;
311 
312 	fp = fopen(_PATH_ZONETAB, "r");
313 	if (!fp)
314 		err(1, _PATH_ZONETAB);
315 	lineno = 0;
316 
317 	while ((line = fgetln(fp, &len)) != 0) {
318 		lineno++;
319 		if (line[len - 1] != '\n')
320 			errx(1, _PATH_ZONETAB ":%d: invalid format", lineno);
321 		line[len - 1] = '\0';
322 		if (line[0] == '#')
323 			continue;
324 
325 		tlc = strsep(&line, "\t");
326 		if (strlen(tlc) != 2)
327 			errx(1, _PATH_ZONETAB ":%d: invalid country code `%s'",
328 			     lineno, tlc);
329 		coord = strsep(&line, "\t");
330 		file = strsep(&line, "\t");
331 		p = strchr(file, '/');
332 		if (p == 0)
333 			errx(1, _PATH_ZONETAB ":%d: invalid zone name `%s'",
334 			     lineno, file);
335 		contbuf[0] = '\0';
336 		strncat(contbuf, file, p - file);
337 		cont = find_continent(contbuf);
338 		if (!cont)
339 			errx(1, _PATH_ZONETAB ":%d: invalid region `%s'",
340 			     lineno, contbuf);
341 
342 		descr = (line && *line) ? line : 0;
343 
344 		add_zone_to_country(lineno, tlc, descr, file, cont);
345 	}
346 	fclose(fp);
347 }
348 
349 static void
350 make_menus(void)
351 {
352 	struct country *cp;
353 	struct zone *zp, *zp2;
354 	struct continent *cont;
355 	dialogMenuItem *dmi;
356 	int i;
357 
358 	/*
359 	 * First, count up all the countries in each continent/ocean.
360 	 * Be careful to count those countries which have multiple zones
361 	 * only once for each.  NB: some countries are in multiple
362 	 * continents/oceans.
363 	 */
364 	for (cp = countries; cp->name; cp++) {
365 		if (cp->nzones == 0)
366 			continue;
367 		if (cp->nzones < 0) {
368 			cp->continent->nitems++;
369 		} else {
370 			for (zp = cp->zones.tqh_first; zp;
371 			     zp = zp->link.tqe_next) {
372 				cont = zp->continent;
373 				for (zp2 = cp->zones.tqh_first;
374 				     zp2->continent != cont;
375 				     zp2 = zp2->link.tqe_next)
376 					;
377 				if (zp2 == zp)
378 					zp->continent->nitems++;
379 			}
380 		}
381 	}
382 
383 	/*
384 	 * Now allocate memory for the country menus.  We set
385 	 * nitems back to zero so that we can use it for counting
386 	 * again when we actually build the menus.
387 	 */
388 	for (i = 0; i < NCONTINENTS; i++) {
389 		continent_names[i].continent->menu =
390 			malloc(sizeof(dialogMenuItem) *
391 			       continent_names[i].continent->nitems);
392 		if (continent_names[i].continent->menu == 0)
393 			err(1, "malloc for continent menu");
394 		continent_names[i].continent->nitems = 0;
395 	}
396 
397 	/*
398 	 * Now that memory is allocated, create the menu items for
399 	 * each continent.  For multiple-zone countries, also create
400 	 * the country's zone submenu.
401 	 */
402 	for (cp = countries; cp->name; cp++) {
403 		if (cp->nzones == 0)
404 			continue;
405 		if (cp->nzones < 0) {
406 			dmi = &cp->continent->menu[cp->continent->nitems];
407 			memset(dmi, 0, sizeof *dmi);
408 			asprintf(&dmi->prompt, "%d",
409 				 ++cp->continent->nitems);
410 			dmi->title = cp->name;
411 			dmi->checked = 0;
412 			dmi->fire = set_zone_whole_country;
413 			dmi->selected = 0;
414 			dmi->data = cp;
415 		} else {
416 			cp->submenu = malloc(cp->nzones * sizeof *dmi);
417 			if (cp->submenu == 0)
418 				err(1, "malloc for submenu");
419 			cp->nzones = 0;
420 			for (zp = cp->zones.tqh_first; zp;
421 			     zp = zp->link.tqe_next) {
422 				cont = zp->continent;
423 				dmi = &cp->submenu[cp->nzones];
424 				memset(dmi, 0, sizeof *dmi);
425 				asprintf(&dmi->prompt, "%d",
426 					 ++cp->nzones);
427 				dmi->title = zp->descr;
428 				dmi->checked = 0;
429 				dmi->fire = set_zone_multi;
430 				dmi->selected = 0;
431 				dmi->data = zp;
432 
433 				for (zp2 = cp->zones.tqh_first;
434 				     zp2->continent != cont;
435 				     zp2 = zp2->link.tqe_next)
436 					;
437 				if (zp2 != zp)
438 					continue;
439 
440 				dmi = &cont->menu[cont->nitems];
441 				memset(dmi, 0, sizeof *dmi);
442 				asprintf(&dmi->prompt, "%d", ++cont->nitems);
443 				dmi->title = cp->name;
444 				dmi->checked = 0;
445 				dmi->fire = set_zone_menu;
446 				dmi->selected = 0;
447 				dmi->data = cp;
448 			}
449 		}
450 	}
451 }
452 
453 static int
454 set_zone_menu(dialogMenuItem *dmi)
455 {
456 	int rv;
457 	char buf[256];
458 	struct country *cp = dmi->data;
459 	int menulen;
460 
461 	snprintf(buf, sizeof buf, "%s Time Zones", cp->name);
462 	menulen = cp->nzones < 16 ? cp->nzones : 16;
463 	rv = dialog_menu(buf, "Select a zone which observes the same time as "
464 			 "your locality.", -1, -1, menulen, -cp->nzones,
465 			 cp->submenu, 0, 0, 0);
466 	if (rv != 0)
467 		return DITEM_RECREATE;
468 	return DITEM_LEAVE_MENU;
469 }
470 
471 static int
472 install_zone_file(const char *filename)
473 {
474 	struct stat sb;
475 	int fd1, fd2;
476 	int copymode;
477 	char *msg;
478 	ssize_t len;
479 	char buf[1024];
480 
481 	if (lstat(_PATH_LOCALTIME, &sb) < 0)
482 		/* Nothing there yet... */
483 		copymode = 1;
484 	else if(S_ISLNK(sb.st_mode))
485 		copymode = 0;
486 	else
487 		copymode = 1;
488 
489 #ifdef VERBOSE
490 	if (copymode)
491 		asprintf(&msg, "Copying %s to " _PATH_LOCALTIME, filename);
492 	else
493 		asprintf(&msg, "Creating symbolic link " _PATH_LOCALTIME
494 			 " to %s", filename);
495 
496 	dialog_notify(msg);
497 	free(msg);
498 #endif
499 
500 	if (reallydoit) {
501 		if (copymode) {
502 			fd1 = open(filename, O_RDONLY, 0);
503 			if (fd1 < 0) {
504 				asprintf(&msg, "Could not open %s: %s",
505 					 filename, strerror(errno));
506 				dialog_mesgbox("Error", msg, 8, 72);
507 				free(msg);
508 				return DITEM_FAILURE | DITEM_RECREATE;
509 			}
510 
511 			unlink(_PATH_LOCALTIME);
512 			fd2 = open(_PATH_LOCALTIME,
513 				   O_CREAT | O_EXCL | O_WRONLY,
514 				   0444);
515 			if (fd2 < 0) {
516 				asprintf(&msg, "Could not open "
517 					 _PATH_LOCALTIME ": %s",
518 					 strerror(errno));
519 				dialog_mesgbox("Error", msg, 8, 72);
520 				free(msg);
521 				return DITEM_FAILURE | DITEM_RECREATE;
522 			}
523 
524 			while ((len = read(fd1, buf, sizeof buf)) > 0)
525 				len = write(fd2, buf, len);
526 
527 			if (len == -1) {
528 				asprintf(&msg, "Error copying %s to "
529 					 _PATH_LOCALTIME ": %s",
530 					 strerror(errno));
531 				dialog_mesgbox("Error", msg, 8, 72);
532 				free(msg);
533 				/* Better to leave none than a corrupt one. */
534 				unlink(_PATH_LOCALTIME);
535 				return DITEM_FAILURE | DITEM_RECREATE;
536 			}
537 			close(fd1);
538 			close(fd2);
539 		} else {
540 			if (access(filename, R_OK) != 0) {
541 				asprintf(&msg, "Cannot access %s: %s",
542 					 filename, strerror(errno));
543 				dialog_mesgbox("Error", msg, 8, 72);
544 				free(msg);
545 				return DITEM_FAILURE | DITEM_RECREATE;
546 			}
547 			unlink(_PATH_LOCALTIME);
548 			if (symlink(filename, _PATH_LOCALTIME) < 0) {
549 				asprintf(&msg, "Cannot create symbolic link "
550 					 _PATH_LOCALTIME " to %s: %s",
551 					 filename, strerror(errno));
552 				dialog_mesgbox("Error", msg, 8, 72);
553 				free(msg);
554 				return DITEM_FAILURE | DITEM_RECREATE;
555 			}
556 		}
557 	}
558 
559 #ifdef VERBOSE
560 	if (copymode)
561 		asprintf(&msg, "Copied timezone file from %s to "
562 			 _PATH_LOCALTIME, filename);
563 	else
564 		asprintf(&msg, "Created symbolic link from " _PATH_LOCALTIME
565 			 " to %s", filename);
566 
567 	dialog_mesgbox("Done", msg, 8, 72);
568 	free(msg);
569 #endif
570 	return DITEM_LEAVE_MENU;
571 }
572 
573 static int
574 confirm_zone(const char *filename)
575 {
576 	char *msg;
577 	struct tm *tm;
578 	time_t t = time(0);
579 	int rv;
580 
581 	setenv("TZ", filename, 1);
582 	tzset();
583 	tm = localtime(&t);
584 
585 	asprintf(&msg, "Does the abbreviation `%s' look reasonable?",
586 		 tm->tm_zone);
587 	rv = !dialog_yesno("Confirmation", msg, 4, 72);
588 	free(msg);
589 	return rv;
590 }
591 
592 static int
593 set_zone_multi(dialogMenuItem *dmi)
594 {
595 	char *fn;
596 	struct zone *zp = dmi->data;
597 	int rv;
598 
599 	if (!confirm_zone(zp->filename))
600 		return DITEM_FAILURE | DITEM_RECREATE;
601 
602 	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, zp->filename);
603 	rv = install_zone_file(fn);
604 	free(fn);
605 	return rv;
606 }
607 
608 static int
609 set_zone_whole_country(dialogMenuItem *dmi)
610 {
611 	char *fn;
612 	struct country *cp = dmi->data;
613 	int rv;
614 
615 	if (!confirm_zone(cp->filename))
616 		return DITEM_FAILURE | DITEM_RECREATE;
617 
618 	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, cp->filename);
619 	rv = install_zone_file(fn);
620 	free(fn);
621 	return rv;
622 }
623 
624 static void
625 usage()
626 {
627 	fprintf(stderr, "usage: tzsetup [-n]\n");
628 	exit(1);
629 }
630 
631 int
632 main(int argc, char **argv)
633 {
634 	int c, fd;
635 
636 	while ((c = getopt(argc, argv, "n")) != -1) {
637 		switch(c) {
638 		case 'n':
639 			reallydoit = 0;
640 			break;
641 
642 		default:
643 			usage();
644 		}
645 	}
646 
647 	if (optind != argc)
648 		usage();
649 
650 	read_iso3166_table();
651 	read_zones();
652 	sort_countries();
653 	make_menus();
654 
655 	init_dialog();
656 	if (!dialog_yesno("Select local or UTC (Greenwich Mean Time) clock",
657 			  "Is this machine's CMOS clock set to UTC?  If it is set to local time,\n"
658 			  "please choose NO here!", 7, 72)) {
659 		if (reallydoit)
660 			unlink(_PATH_WALL_CMOS_CLOCK);
661 	} else {
662 		if (reallydoit) {
663 			fd = open(_PATH_WALL_CMOS_CLOCK,
664 				  O_WRONLY|O_CREAT|O_TRUNC, 0666);
665 			if (fd < 0)
666 				err(1, "create %s", _PATH_WALL_CMOS_CLOCK);
667 			close(fd);
668 		}
669 	}
670 	dialog_clear_norefresh();
671 	dialog_menu("Time Zone Selector", "Select a region", -1, -1,
672 		    NCONTINENTS, -NCONTINENTS, continents, 0, NULL, NULL);
673 	dialog_clear();
674 	end_dialog();
675 	return 0;
676 }
677 
678