xref: /titanic_50/usr/src/cmd/vntsd/console.c (revision 0b46ffbad80838700776674f5dea3ad9b16dd6e4)
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 #pragma ident	"%Z%%M%	%I%	%E% SMI"
26 
27 /*
28  * Listen thread creates a console thread whenever there is a tcp client
29  * made a conection to its port. In the console thread, if there are
30  * multiple consoles in the group, client will be asked for a console selection.
31  * a write thread for a console is created when first client connects to a
32  * selected console and console thread becomes read thread for the client.
33  */
34 
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <unistd.h>
39 #include <sys/types.h>
40 #include <sys/socket.h>
41 #include <netinet/in.h>
42 #include <thread.h>
43 #include <synch.h>
44 #include <signal.h>
45 #include <assert.h>
46 #include <ctype.h>
47 #include <syslog.h>
48 #include <libintl.h>
49 #include <netdb.h>
50 #include "vntsd.h"
51 #include "chars.h"
52 
53 /*  display domain names in the group */
54 static boolean_t
55 display_domain_name(vntsd_cons_t *consp,  int  *fd)
56 {
57 	char	buf[VNTSD_LINE_LEN];
58 	char	*status;
59 
60 
61 	if (consp->clientpq != NULL) {
62 		status = gettext("connected");
63 	} else if (consp->status & VNTSD_CONS_DELETED) {
64 		status = gettext("removing...");
65 	} else {
66 		status = gettext("online");
67 	}
68 
69 	(void) snprintf(buf, sizeof (buf), "%-20d%-30s%-25s%s",
70 	    consp->cons_no, consp->domain_name, status, vntsd_eol);
71 
72 	return (vntsd_write_fd(*fd, buf, strlen(buf)) != VNTSD_SUCCESS);
73 }
74 
75 /* output connected message to tcp client */
76 static int
77 write_connect_msg(vntsd_client_t *clientp, char *group_name,
78     char *domain_name)
79 {
80 
81 	int	rv = VNTSD_SUCCESS;
82 	char	buf[VNTSD_LINE_LEN];
83 
84 	if ((rv = vntsd_write_client(clientp, vntsd_eol, VNTSD_EOL_LEN)) !=
85 	    VNTSD_SUCCESS) {
86 		return (rv);
87 	}
88 
89 	(void) snprintf(buf, sizeof (buf),
90 	    gettext("Connecting to console \"%s\" in group \"%s\" ...."),
91 	    domain_name, group_name);
92 
93 	if ((rv = vntsd_write_line(clientp, buf)) != VNTSD_SUCCESS) {
94 		return (rv);
95 	}
96 
97 	if ((rv = vntsd_write_line(clientp,
98 			    gettext("Press ~? for control options .."))) !=
99 	    VNTSD_SUCCESS) {
100 		return (rv);
101 	}
102 
103 	return (VNTSD_SUCCESS);
104 }
105 
106 static int
107 create_write_thread(vntsd_cons_t *consp)
108 {
109 
110 	assert(consp);
111 
112 	/* create write thread for the console */
113 	(void) mutex_lock(&consp->lock);
114 	if (thr_create(NULL, 0, (thr_func_t)vntsd_write_thread,
115 		    (void *)consp, NULL, &consp->wr_tid)) {
116 
117 		DERR(stderr, "t@%d create_rd_wr_thread@%d: "
118 		    "create write thread failed\n",
119 		    thr_self(), consp->cons_no);
120 		(void) close(consp->vcc_fd);
121 		consp->vcc_fd = -1;
122 		(void) mutex_unlock(&consp->lock);
123 
124 		return (VNTSD_ERR_CREATE_WR_THR);
125 	}
126 	(void) mutex_unlock(&consp->lock);
127 	return (VNTSD_SUCCESS);
128 }
129 
130 /* Display all domain consoles in a group. */
131 static int
132 list_all_domains(vntsd_group_t *groupp, vntsd_client_t *clientp)
133 {
134 	char	    vntsd_line[VNTSD_LINE_LEN];
135 	int	    rv = VNTSD_SUCCESS;
136 
137 	if ((rv = vntsd_write_client(clientp, vntsd_eol, VNTSD_EOL_LEN))
138 	    != VNTSD_SUCCESS) {
139 		return (rv);
140 	}
141 
142 	/*
143 	 * TRANSLATION_NOTE
144 	 * The following three strings of the form "DOMAIN .." are table
145 	 * headers and should be all uppercase.
146 	 */
147 	(void) snprintf(vntsd_line, sizeof (vntsd_line),
148 	    "%-20s%-30s%-25s",
149 	    gettext("DOMAIN ID"), gettext("DOMAIN NAME"),
150 	    gettext("DOMAIN STATE"));
151 
152 	if ((rv = vntsd_write_line(clientp, vntsd_line)) != VNTSD_SUCCESS) {
153 		return (rv);
154 	}
155 
156 	(void) mutex_lock(&groupp->lock);
157 
158 	if (vntsd_que_find(groupp->conspq, (compare_func_t)display_domain_name,
159 		    &(clientp->sockfd)) != NULL) {
160 		rv = VNTSD_ERR_WRITE_CLIENT;
161 	}
162 
163 	(void) mutex_unlock(&groupp->lock);
164 
165 	return (rv);
166 }
167 
168 /* display help */
169 static int
170 display_help(vntsd_client_t *clientp)
171 {
172 	int	rv = VNTSD_SUCCESS;
173 	char	*bufp;
174 
175 	if ((rv = vntsd_write_client(clientp, vntsd_eol, VNTSD_EOL_LEN))
176 		!= VNTSD_SUCCESS) {
177 		return (rv);
178 	}
179 
180 	/*
181 	 * TRANSLATION_NOTE
182 	 * The following three strings of the form ". -- ..." are help
183 	 * messages for single character commands. Do not translate the
184 	 * character before the --.
185 	 */
186 	bufp = gettext("h -- this help)");
187 
188 	if ((rv = vntsd_write_line(clientp, bufp)) != VNTSD_SUCCESS) {
189 		return (rv);
190 	}
191 
192 	bufp = gettext("l -- list of consoles");
193 
194 	if ((rv = vntsd_write_line(clientp, bufp)) != VNTSD_SUCCESS) {
195 		return (rv);
196 	}
197 
198 	bufp = gettext("q -- quit");
199 
200 	if ((rv = vntsd_write_line(clientp, bufp)) != VNTSD_SUCCESS) {
201 		return (rv);
202 	}
203 
204 	/*
205 	 * TRANSLATION_NOTE
206 	 * In the following string, "id" is a short mnemonic for
207 	 * "identifier" and both occurrences should be translated.
208 	 */
209 
210 	bufp = gettext("[c[c ]]{id} -- connect to console of domain {id}");
211 
212 	if ((rv = vntsd_write_line(clientp, bufp)) != VNTSD_SUCCESS) {
213 		return (rv);
214 	}
215 
216 	return (VNTSD_SUCCESS);
217 }
218 
219 /* select a console to connect */
220 static int
221 select_cons(vntsd_group_t *groupp, int num_cons, vntsd_cons_t **consp,
222     vntsd_client_t *clientp, char c)
223 {
224 	int	    cons_no = -2;
225 	int	    n;
226 	int	    i;
227 	char	    buf[VNTSD_LINE_LEN];
228 	int	    rv;
229 
230 
231 
232 	(void) mutex_lock(&groupp->lock);
233 	if (groupp->num_cons == 0) {
234 		(void) mutex_unlock(&groupp->lock);
235 		/* no console in this group */
236 		return (VNTSD_STATUS_NO_CONS);
237 	}
238 	(void) mutex_unlock(&groupp->lock);
239 
240 	if (num_cons == 1) {
241 		/* by pass selecting console */
242 		*consp = (vntsd_cons_t *)(groupp->conspq->handle);
243 		return (VNTSD_SUCCESS);
244 	}
245 
246 
247 	if (isdigit(c)) {
248 		/* {id} input */
249 		cons_no = c - '0';
250 	} else if (c == 'c') {
251 		/* c{id} or c {id} input */
252 		cons_no = -1;
253 	} else if (!isspace(c)) {
254 		return (VNTSD_ERR_INVALID_INPUT);
255 	}
256 
257 	/* get client selections */
258 	n = VNTSD_LINE_LEN;
259 
260 	if ((rv = vntsd_read_line(clientp, buf, &n)) != VNTSD_SUCCESS) {
261 		return (rv);
262 	}
263 
264 	/* parse command */
265 	for (i = 0; i < n; i++) {
266 		if (cons_no == -1) {
267 			/* c{id} */
268 			cons_no = atoi(buf + i);
269 			break;
270 		}
271 
272 		if (isspace(buf[i]) && cons_no == -2) {
273 			/* skip space */
274 			continue;
275 		}
276 
277 		if (buf[i] == 'c') {
278 			/* c{id} or c {id} */
279 			cons_no = -1;
280 		} else if (buf[i] == CR) {
281 			break;
282 		} else {
283 			return (VNTSD_ERR_INVALID_INPUT);
284 		}
285 	}
286 
287 	if (cons_no < 0) {
288 		return (VNTSD_ERR_INVALID_INPUT);
289 	}
290 
291 	/* get selected console */
292 	(void) mutex_lock(&groupp->lock);
293 
294 	*consp = (vntsd_cons_t *)vntsd_que_find(groupp->conspq,
295 		    (compare_func_t)vntsd_cons_by_consno, &cons_no);
296 
297 	if (*consp == NULL) {
298 		/* during console selection, the console has been  deleted */
299 		(void) mutex_unlock(&groupp->lock);
300 
301 		return (VNTSD_ERR_INVALID_INPUT);
302 	}
303 	if ((*consp)->status & VNTSD_CONS_DELETED) {
304 		return (VNTSD_ERR_INVALID_INPUT);
305 	}
306 
307 	(void) mutex_unlock(&groupp->lock);
308 
309 	return (VNTSD_SUCCESS);
310 }
311 
312 /* compare if there is a match console in the gorup */
313 static boolean_t
314 find_cons_in_group(vntsd_cons_t *consp_in_group, vntsd_cons_t *consp)
315 {
316 	if (consp_in_group == consp) {
317 		return (B_TRUE);
318 	} else {
319 		return (B_FALSE);
320 	}
321 }
322 
323 /* connect a client to a console */
324 static int
325 connect_cons(vntsd_cons_t *consp, vntsd_client_t *clientp)
326 {
327 	int	rv, rv1;
328 	vntsd_group_t *groupp;
329 
330 	assert(consp);
331 	groupp = consp->group;
332 	assert(groupp);
333 	assert(clientp);
334 
335 	(void) mutex_lock(&groupp->lock);
336 
337 	/* check if console is valid */
338 	consp = vntsd_que_find(groupp->conspq,
339 	    (compare_func_t)find_cons_in_group, consp);
340 
341 	if (consp == NULL) {
342 		(void) mutex_unlock(&groupp->lock);
343 		return (VNTSD_STATUS_NO_CONS);
344 	}
345 	if (consp->status & VNTSD_CONS_DELETED) {
346 		(void) mutex_unlock(&groupp->lock);
347 		return (VNTSD_STATUS_NO_CONS);
348 	}
349 
350 	(void) mutex_lock(&consp->lock);
351 	(void) mutex_lock(&clientp->lock);
352 
353 
354 	clientp->cons = consp;
355 
356 	/* enable daemon cmd */
357 	clientp->status &= ~VNTSD_CLIENT_DISABLE_DAEMON_CMD;
358 
359 	if (consp->clientpq == NULL) {
360 		/* first connect to console - a writer */
361 		assert(consp->vcc_fd == -1);
362 		/* open vcc */
363 		consp->vcc_fd = vntsd_open_vcc(consp->dev_name, consp->cons_no);
364 		if (consp->vcc_fd < 0) {
365 			(void) mutex_unlock(&clientp->lock);
366 			(void) mutex_unlock(&consp->lock);
367 			(void) mutex_unlock(&groupp->lock);
368 			assert(consp->group);
369 			return (vntsd_vcc_err(consp));
370 		}
371 	}
372 
373 	(void) mutex_unlock(&clientp->lock);
374 
375 	/*
376 	 * move the client from group's no console selected queue
377 	 * to cons queue
378 	 */
379 
380 	rv = vntsd_que_rm(&groupp->no_cons_clientpq, clientp);
381 	assert(rv == VNTSD_SUCCESS);
382 
383 	rv = vntsd_que_append(&consp->clientpq, clientp);
384 	(void) mutex_unlock(&groupp->lock);
385 
386 	if (rv != VNTSD_SUCCESS) {
387 		if (consp->clientpq->handle == clientp) {
388 			/* writer */
389 			(void) close(consp->vcc_fd);
390 			consp->vcc_fd = -1;
391 		}
392 
393 		(void) mutex_unlock(&consp->lock);
394 		return (rv);
395 	}
396 
397 	(void) mutex_unlock(&consp->lock);
398 
399 	if (consp->clientpq->handle == clientp) {
400 		/* create a write thread */
401 		rv = create_write_thread(consp);
402 		if (rv != VNTSD_SUCCESS) {
403 			return (rv);
404 		}
405 	}
406 
407 	/* write connecting message */
408 	if ((rv = write_connect_msg(clientp, consp->group->group_name,
409 	    consp->domain_name)) != VNTSD_SUCCESS) {
410 			return (rv);
411 	}
412 
413 	/* process input from client */
414 	rv = vntsd_read(clientp);
415 
416 	/* client disconnected from the console */
417 	(void) mutex_lock(&groupp->lock);
418 
419 	/* remove client from console queue */
420 	(void) mutex_lock(&consp->lock);
421 	rv1 = vntsd_que_rm(&consp->clientpq, clientp);
422 	assert(rv1 == VNTSD_SUCCESS);
423 
424 	/* append client to group's no console selected  queue */
425 	rv1 = vntsd_que_append(&groupp->no_cons_clientpq, clientp);
426 	(void) mutex_unlock(&groupp->lock);
427 
428 	if (consp->clientpq == NULL) {
429 		/* clean up console since there is no client connected to it */
430 		assert(consp->vcc_fd != -1);
431 
432 		/* close vcc port */
433 		(void) close(consp->vcc_fd);
434 		consp->vcc_fd = -1;
435 
436 		/* force write thread to exit */
437 		assert(consp->wr_tid != (thread_t)-1);
438 		(void) thr_kill(consp->wr_tid, SIGUSR1);
439 		(void) mutex_unlock(&consp->lock);
440 		(void) thr_join(consp->wr_tid, NULL, NULL);
441 		(void) mutex_lock(&consp->lock);
442 	}
443 
444 	if (consp->status & VNTSD_CONS_SIG_WAIT) {
445 		/* console is waiting for client to disconnect */
446 		(void) cond_signal(&consp->cvp);
447 	}
448 
449 	(void) mutex_unlock(&consp->lock);
450 
451 	return (rv1 == VNTSD_SUCCESS ? rv : rv1);
452 
453 }
454 
455 /* read command line input */
456 static int
457 read_cmd(vntsd_client_t *clientp, char *prompt, char *cmd)
458 {
459 	int		rv;
460 
461 	/* disable daemon special command */
462 	(void) mutex_lock(&clientp->lock);
463 	clientp->status |= VNTSD_CLIENT_DISABLE_DAEMON_CMD;
464 	(void) mutex_unlock(&clientp->lock);
465 
466 	if ((rv = vntsd_write_client(clientp, vntsd_eol, VNTSD_EOL_LEN))
467 	    != VNTSD_SUCCESS) {
468 		return (rv);
469 	}
470 
471 	if ((rv = vntsd_write_client(clientp, prompt, strlen(prompt)))
472 		!= VNTSD_SUCCESS) {
473 		return (rv);
474 	}
475 
476 	if ((rv = vntsd_read_data(clientp, cmd)) != VNTSD_SUCCESS) {
477 		return (rv);
478 	}
479 	if (*cmd == BS) {
480 		return (VNTSD_SUCCESS);
481 	}
482 
483 	rv = vntsd_write_client(clientp, cmd, 1);
484 
485 	*cmd = tolower(*cmd);
486 
487 	return (rv);
488 }
489 
490 /* reset client for selecting a console in the group */
491 static void
492 client_init(vntsd_client_t *clientp)
493 {
494 	(void) mutex_lock(&clientp->lock);
495 	clientp->cons = NULL;
496 	clientp->status = 0;
497 	(void) mutex_unlock(&clientp->lock);
498 }
499 
500 /* clean up client and exit the thread */
501 static void
502 client_fini(vntsd_group_t *groupp, vntsd_client_t *clientp)
503 {
504 
505 	assert(groupp);
506 	assert(clientp);
507 
508 	/* disconnct client from tcp port */
509 	assert(clientp->sockfd != -1);
510 	(void) close(clientp->sockfd);
511 
512 	(void) mutex_lock(&groupp->lock);
513 	(void) vntsd_que_rm(&groupp->no_cons_clientpq, clientp);
514 
515 	if ((groupp->no_cons_clientpq == NULL) &&
516 	    (groupp->status & VNTSD_GROUP_SIG_WAIT)) {
517 		/* group is waiting to be deleted */
518 		groupp->status &= ~VNTSD_GROUP_SIG_WAIT;
519 		(void) cond_signal(&groupp->cvp);
520 	}
521 	(void) mutex_unlock(&groupp->lock);
522 
523 	(void) mutex_destroy(&clientp->lock);
524 	free(clientp);
525 
526 	thr_exit(0);
527 }
528 
529 /*  check client's status. exit if client quits or fatal errors */
530 static void
531 console_chk_status(vntsd_group_t *groupp, vntsd_client_t *clientp, int status)
532 {
533 	char    err_msg[VNTSD_LINE_LEN];
534 
535 	D1(stderr, "t@%d console_chk_status() status=%d "
536 	    "client status=%x num consoles=%d \n",
537 	    thr_self(), status, clientp->status, groupp->num_cons);
538 
539 	(void) snprintf(err_msg, VNTSD_LINE_LEN, "console_chk_status client%d"
540 	    " num_cos=%d", clientp->sockfd, groupp->num_cons);
541 
542 	if (groupp->num_cons == 0) {
543 		/* no more console in the group */
544 		client_fini(groupp, clientp);
545 	}
546 
547 	if (status == VNTSD_STATUS_INTR) {
548 		/* reason for signal? */
549 		status = vntsd_cons_chk_intr(clientp);
550 	}
551 
552 	switch (status) {
553 
554 	case VNTSD_STATUS_CLIENT_QUIT:
555 		client_fini(groupp, clientp);
556 		return;
557 
558 	case VNTSD_STATUS_RESELECT_CONS:
559 		assert(clientp->cons);
560 		if ((groupp->num_cons == 1) &&
561 		    (groupp->conspq->handle == clientp->cons)) {
562 			/* no other selection available */
563 			client_fini(groupp, clientp);
564 		} else {
565 			client_init(clientp);
566 		}
567 		return;
568 
569 	case VNTSD_STATUS_VCC_IO_ERR:
570 		if ((clientp->status & VNTSD_CLIENT_CONS_DELETED) == 0) {
571 			/* check if console was deleted  */
572 			status = vntsd_vcc_err(clientp->cons);
573 		}
574 
575 		if (status != VNTSD_STATUS_CONTINUE) {
576 			/* console was deleted */
577 			if (groupp->num_cons == 1) {
578 				client_fini(groupp, clientp);
579 			}
580 		}
581 
582 		/* console is ok */
583 		client_init(clientp);
584 		return;
585 
586 	case VNTSD_STATUS_MOV_CONS_FORWARD:
587 	case VNTSD_STATUS_MOV_CONS_BACKWARD:
588 		if (groupp->num_cons == 1) {
589 			/* same console */
590 			return;
591 		}
592 
593 		/* get selected console */
594 		(void) mutex_lock(&(clientp->cons->group->lock));
595 		clientp->cons = vntsd_que_pos(clientp->cons->group->conspq,
596 		    clientp->cons,
597 		    (status == VNTSD_STATUS_MOV_CONS_FORWARD)?(1):(-1));
598 		(void) mutex_unlock(&(clientp->cons->group->lock));
599 		return;
600 
601 	case VNTSD_SUCCESS:
602 	case VNTSD_STATUS_CONTINUE:
603 	case VNTSD_STATUS_NO_CONS:
604 		client_init(clientp);
605 		return;
606 
607 	case VNTSD_ERR_INVALID_INPUT:
608 		return;
609 
610 	default:
611 		/* fatal error */
612 		vntsd_log(status, err_msg);
613 		client_fini(groupp, clientp);
614 		return;
615 	}
616 }
617 
618 /* console thread */
619 void *
620 vntsd_console_thread(vntsd_thr_arg_t *argp)
621 {
622 	vntsd_group_t	    *groupp;
623 	vntsd_cons_t	    *consp;
624 	vntsd_client_t	    *clientp;
625 
626 	char		    buf[MAXHOSTNAMELEN];
627 	char		    prompt[72];
628 	char		    cmd;
629 	int		    rv = VNTSD_SUCCESS;
630 	int		    num_cons;
631 
632 
633 	groupp = (vntsd_group_t *)argp->handle;
634 	clientp = (vntsd_client_t *)argp->arg;
635 
636 	assert(groupp);
637 	assert(clientp);
638 
639 	/* check if group is removed */
640 
641 	D1(stderr, "t@%d get_client_sel@%lld:client@%d\n", thr_self(),
642 	    groupp->tcp_port, clientp->sockfd);
643 
644 	bzero(buf, MAXHOSTNAMELEN);
645 
646 	/* host name */
647 	if (gethostname(buf, MAXHOSTNAMELEN)) {
648 		vntsd_log(VNTSD_STATUS_NO_HOST_NAME, "vntsd_console_thread()");
649 		(void) snprintf(buf, sizeof (buf), "unkown host");
650 	}
651 
652 	if (snprintf(prompt, sizeof (prompt),
653 		    "%s-vnts-%s: h,l,{id},c{id},c {id},q:",
654 	    buf, groupp->group_name) >= sizeof (prompt)) {
655 		/* long prompt doesn't fit, use short one */
656 		(void) snprintf(prompt, sizeof (prompt),
657 				"vnts: h,l,{id},c{id},c {id}, q:");
658 	}
659 
660 
661 	for (;;) {
662 		cmd = ' ';
663 		D1(stderr, "t@%d console_thread()@%lld:client@%d\n", thr_self(),
664 		    groupp->tcp_port, clientp->sockfd);
665 
666 		num_cons = vntsd_chk_group_total_cons(groupp);
667 
668 		if ((num_cons > 1) && (clientp->cons == NULL)) {
669 			/*  console to connect to */
670 			rv = read_cmd(clientp, prompt, &cmd);
671 			/* check error and may exit */
672 			console_chk_status(groupp, clientp, rv);
673 		}
674 
675 		switch (cmd) {
676 
677 		case 'l':
678 
679 			/* list domain names */
680 			rv = list_all_domains(groupp, clientp);
681 			break;
682 
683 
684 		case 'q':
685 
686 			rv = VNTSD_STATUS_CLIENT_QUIT;
687 			break;
688 
689 		case 'h':
690 			rv = display_help(clientp);
691 			break;
692 
693 		default:
694 			/* select console */
695 			if (clientp->cons == NULL) {
696 				rv = select_cons(groupp, num_cons,
697 				    &consp, clientp, cmd);
698 				if (rv == VNTSD_ERR_INVALID_INPUT) {
699 					rv = display_help(clientp);
700 					break;
701 				}
702 			} else {
703 				consp = clientp->cons;
704 			}
705 			assert(consp);
706 
707 			/* connect to console */
708 			rv = connect_cons(consp, clientp);
709 			D1(stderr, "t@%d console_thread()"
710 			    "connect_cons returns %d\n",
711 			    thr_self(), rv);
712 			break;
713 
714 		}
715 		/* check error and may  exit */
716 		console_chk_status(groupp, clientp, rv);
717 	}
718 
719 	/*NOTREACHED*/
720 	return (NULL);
721 }
722