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 (c) 2013 Gary Mills 23 * 24 * Copyright 2009 Sun Microsystems, Inc. All rights reserved. 25 * Use is subject to license terms. 26 */ 27 28 /* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */ 29 /* All Rights Reserved */ 30 31 /* 32 * University Copyright- Copyright (c) 1982, 1986, 1988 33 * The Regents of the University of California 34 * All Rights Reserved 35 * 36 * University Acknowledgment- Portions of this document are derived from 37 * software developed by the University of California, Berkeley, and its 38 * contributors. 39 */ 40 41 /* 42 * This is the new w command which takes advantage of 43 * the /proc interface to gain access to the information 44 * of all the processes currently on the system. 45 * 46 * This program also implements 'uptime'. 47 * 48 * Maintenance note: 49 * 50 * Much of this code is replicated in whodo.c. If you're 51 * fixing bugs here, then you should probably fix 'em there too. 52 */ 53 54 #include <stdio.h> 55 #include <string.h> 56 #include <stdarg.h> 57 #include <stdlib.h> 58 #include <ctype.h> 59 #include <fcntl.h> 60 #include <time.h> 61 #include <errno.h> 62 #include <sys/types.h> 63 #include <utmpx.h> 64 #include <sys/stat.h> 65 #include <dirent.h> 66 #include <procfs.h> /* /proc header file */ 67 #include <locale.h> 68 #include <unistd.h> 69 #include <sys/loadavg.h> 70 #include <limits.h> 71 #include <priv_utils.h> 72 73 /* 74 * Use the full lengths from utmpx for user and line. 75 */ 76 static struct utmpx dummy; 77 #define NMAX (sizeof (dummy.ut_user)) 78 #define LMAX (sizeof (dummy.ut_line)) 79 80 /* Print minimum field widths. */ 81 #define LOGIN_WIDTH 8 82 #define LINE_WIDTH 8 83 84 #define DIV60(t) ((t+30)/60) /* x/60 rounded */ 85 86 #ifdef ERR 87 #undef ERR 88 #endif 89 #define ERR (-1) 90 91 #define HSIZE 256 /* size of process hash table */ 92 #define PROCDIR "/proc" 93 #define INITPROCESS (pid_t)1 /* init process pid */ 94 #define NONE 'n' /* no state */ 95 #define RUNNING 'r' /* runnable process */ 96 #define ZOMBIE 'z' /* zombie process */ 97 #define VISITED 'v' /* marked node as visited */ 98 #define PRINTF(a) if (printf a < 0) { \ 99 perror((gettext("%s: printf failed"), prog)); \ 100 exit(1); } 101 102 struct uproc { 103 pid_t p_upid; /* process id */ 104 char p_state; /* numeric value of process state */ 105 dev_t p_ttyd; /* controlling tty of process */ 106 time_t p_time; /* seconds of user & system time */ 107 time_t p_ctime; /* seconds of child user & sys time */ 108 int p_igintr; /* 1 = ignores SIGQUIT and SIGINT */ 109 char p_comm[PRARGSZ+1]; /* command */ 110 char p_args[PRARGSZ+1]; /* command line arguments */ 111 struct uproc *p_child, /* first child pointer */ 112 *p_sibling, /* sibling pointer */ 113 *p_pgrpl, /* pgrp link */ 114 *p_link; /* hash table chain pointer */ 115 }; 116 117 /* 118 * define hash table for struct uproc 119 * Hash function uses process id 120 * and the size of the hash table(HSIZE) 121 * to determine process index into the table. 122 */ 123 static struct uproc pr_htbl[HSIZE]; 124 125 static struct uproc *findhash(pid_t); 126 static time_t findidle(char *); 127 static void clnarglist(char *); 128 static void showtotals(struct uproc *); 129 static void calctotals(struct uproc *); 130 static void prttime(time_t, int); 131 static void prtat(time_t *time); 132 133 static char *prog; /* pointer to invocation name */ 134 static int header = 1; /* true if -h flag: don't print heading */ 135 static int lflag = 1; /* set if -l flag; 0 for -s flag: short form */ 136 static char *sel_user; /* login of particular user selected */ 137 static char firstchar; /* first char of name of prog invoked as */ 138 static int login; /* true if invoked as login shell */ 139 static time_t now; /* current time of day */ 140 static time_t uptime; /* time of last reboot & elapsed time since */ 141 static int nusers; /* number of users logged in now */ 142 static time_t idle; /* number of minutes user is idle */ 143 static time_t jobtime; /* total cpu time visible */ 144 static char doing[520]; /* process attached to terminal */ 145 static time_t proctime; /* cpu time of process in doing */ 146 static pid_t curpid, empty; 147 static int add_times; /* boolean: add the cpu times or not */ 148 149 #if SIGQUIT > SIGINT 150 #define ACTSIZE SIGQUIT 151 #else 152 #define ACTSIZE SIGINT 153 #endif 154 155 int 156 main(int argc, char *argv[]) 157 { 158 struct utmpx *ut; 159 struct utmpx *utmpbegin; 160 struct utmpx *utmpend; 161 struct utmpx *utp; 162 struct uproc *up, *parent, *pgrp; 163 struct psinfo info; 164 struct sigaction actinfo[ACTSIZE]; 165 struct pstatus statinfo; 166 size_t size; 167 struct stat sbuf; 168 DIR *dirp; 169 struct dirent *dp; 170 char pname[64]; 171 char *fname; 172 int procfd; 173 char *cp; 174 int i; 175 int days, hrs, mins; 176 int entries; 177 double loadavg[3]; 178 179 /* 180 * This program needs the proc_owner privilege 181 */ 182 (void) __init_suid_priv(PU_CLEARLIMITSET, PRIV_PROC_OWNER, 183 (char *)NULL); 184 185 (void) setlocale(LC_ALL, ""); 186 #if !defined(TEXT_DOMAIN) 187 #define TEXT_DOMAIN "SYS_TEST" 188 #endif 189 (void) textdomain(TEXT_DOMAIN); 190 191 login = (argv[0][0] == '-'); 192 cp = strrchr(argv[0], '/'); 193 firstchar = login ? argv[0][1] : (cp == 0) ? argv[0][0] : cp[1]; 194 prog = argv[0]; 195 196 while (argc > 1) { 197 if (argv[1][0] == '-') { 198 for (i = 1; argv[1][i]; i++) { 199 switch (argv[1][i]) { 200 201 case 'h': 202 header = 0; 203 break; 204 205 case 'l': 206 lflag++; 207 break; 208 case 's': 209 lflag = 0; 210 break; 211 212 case 'u': 213 case 'w': 214 firstchar = argv[1][i]; 215 break; 216 217 default: 218 (void) fprintf(stderr, gettext( 219 "%s: bad flag %s\n"), 220 prog, argv[1]); 221 exit(1); 222 } 223 } 224 } else { 225 if (!isalnum(argv[1][0]) || argc > 2) { 226 (void) fprintf(stderr, gettext( 227 "usage: %s [ -hlsuw ] [ user ]\n"), prog); 228 exit(1); 229 } else 230 sel_user = argv[1]; 231 } 232 argc--; argv++; 233 } 234 235 /* 236 * read the UTMP_FILE (contains information about each logged in user) 237 */ 238 if (stat(UTMPX_FILE, &sbuf) == ERR) { 239 (void) fprintf(stderr, gettext("%s: stat error of %s: %s\n"), 240 prog, UTMPX_FILE, strerror(errno)); 241 exit(1); 242 } 243 entries = sbuf.st_size / sizeof (struct futmpx); 244 size = sizeof (struct utmpx) * entries; 245 if ((ut = malloc(size)) == NULL) { 246 (void) fprintf(stderr, gettext("%s: malloc error of %s: %s\n"), 247 prog, UTMPX_FILE, strerror(errno)); 248 exit(1); 249 } 250 251 (void) utmpxname(UTMPX_FILE); 252 253 utmpbegin = ut; 254 utmpend = (struct utmpx *)((char *)utmpbegin + size); 255 256 setutxent(); 257 while ((ut < utmpend) && ((utp = getutxent()) != NULL)) 258 (void) memcpy(ut++, utp, sizeof (*ut)); 259 endutxent(); 260 261 (void) time(&now); /* get current time */ 262 263 if (header) { /* print a header */ 264 prtat(&now); 265 for (ut = utmpbegin; ut < utmpend; ut++) { 266 if (ut->ut_type == USER_PROCESS) { 267 if (!nonuser(*ut)) 268 nusers++; 269 } else if (ut->ut_type == BOOT_TIME) { 270 uptime = now - ut->ut_xtime; 271 uptime += 30; 272 days = uptime / (60*60*24); 273 uptime %= (60*60*24); 274 hrs = uptime / (60*60); 275 uptime %= (60*60); 276 mins = uptime / 60; 277 278 PRINTF((gettext(" up"))); 279 if (days > 0) 280 PRINTF((gettext( 281 " %d day(s),"), days)); 282 if (hrs > 0 && mins > 0) { 283 PRINTF((" %2d:%02d,", hrs, mins)); 284 } else { 285 if (hrs > 0) 286 PRINTF((gettext( 287 " %d hr(s),"), hrs)); 288 if (mins > 0) 289 PRINTF((gettext( 290 " %d min(s),"), mins)); 291 } 292 } 293 } 294 295 ut = utmpbegin; /* rewind utmp data */ 296 PRINTF((((nusers == 1) ? 297 gettext(" %d user") : gettext(" %d users")), nusers)); 298 /* 299 * Print 1, 5, and 15 minute load averages. 300 */ 301 (void) getloadavg(loadavg, 3); 302 PRINTF((gettext(", load average: %.2f, %.2f, %.2f\n"), 303 loadavg[LOADAVG_1MIN], loadavg[LOADAVG_5MIN], 304 loadavg[LOADAVG_15MIN])); 305 306 if (firstchar == 'u') /* uptime command */ 307 exit(0); 308 309 if (lflag) { 310 PRINTF((dcgettext(NULL, "User tty " 311 "login@ idle JCPU PCPU what\n", 312 LC_TIME))); 313 } else { 314 PRINTF((dcgettext(NULL, 315 "User tty idle what\n", 316 LC_TIME))); 317 } 318 319 if (fflush(stdout) == EOF) { 320 perror((gettext("%s: fflush failed\n"), prog)); 321 exit(1); 322 } 323 } 324 325 /* 326 * loop through /proc, reading info about each process 327 * and build the parent/child tree 328 */ 329 if (!(dirp = opendir(PROCDIR))) { 330 (void) fprintf(stderr, gettext("%s: could not open %s: %s\n"), 331 prog, PROCDIR, strerror(errno)); 332 exit(1); 333 } 334 335 while ((dp = readdir(dirp)) != NULL) { 336 if (dp->d_name[0] == '.') 337 continue; 338 retry: 339 (void) sprintf(pname, "%s/%s/", PROCDIR, dp->d_name); 340 fname = pname + strlen(pname); 341 (void) strcpy(fname, "psinfo"); 342 if ((procfd = open(pname, O_RDONLY)) < 0) 343 continue; 344 if (read(procfd, &info, sizeof (info)) != sizeof (info)) { 345 int err = errno; 346 (void) close(procfd); 347 if (err == EAGAIN) 348 goto retry; 349 if (err != ENOENT) 350 (void) fprintf(stderr, gettext( 351 "%s: read() failed on %s: %s \n"), 352 prog, pname, strerror(err)); 353 continue; 354 } 355 (void) close(procfd); 356 357 up = findhash(info.pr_pid); 358 up->p_ttyd = info.pr_ttydev; 359 up->p_state = (info.pr_nlwp == 0? ZOMBIE : RUNNING); 360 up->p_time = 0; 361 up->p_ctime = 0; 362 up->p_igintr = 0; 363 (void) strncpy(up->p_comm, info.pr_fname, 364 sizeof (info.pr_fname)); 365 up->p_args[0] = 0; 366 367 if (up->p_state != NONE && up->p_state != ZOMBIE) { 368 (void) strcpy(fname, "status"); 369 370 /* now we need the proc_owner privilege */ 371 (void) __priv_bracket(PRIV_ON); 372 373 procfd = open(pname, O_RDONLY); 374 375 /* drop proc_owner privilege after open */ 376 (void) __priv_bracket(PRIV_OFF); 377 378 if (procfd < 0) 379 continue; 380 381 if (read(procfd, &statinfo, sizeof (statinfo)) 382 != sizeof (statinfo)) { 383 int err = errno; 384 (void) close(procfd); 385 if (err == EAGAIN) 386 goto retry; 387 if (err != ENOENT) 388 (void) fprintf(stderr, gettext( 389 "%s: read() failed on %s: %s \n"), 390 prog, pname, strerror(err)); 391 continue; 392 } 393 (void) close(procfd); 394 395 up->p_time = statinfo.pr_utime.tv_sec + 396 statinfo.pr_stime.tv_sec; /* seconds */ 397 up->p_ctime = statinfo.pr_cutime.tv_sec + 398 statinfo.pr_cstime.tv_sec; 399 400 (void) strcpy(fname, "sigact"); 401 402 /* now we need the proc_owner privilege */ 403 (void) __priv_bracket(PRIV_ON); 404 405 procfd = open(pname, O_RDONLY); 406 407 /* drop proc_owner privilege after open */ 408 (void) __priv_bracket(PRIV_OFF); 409 410 if (procfd < 0) 411 continue; 412 413 if (read(procfd, actinfo, sizeof (actinfo)) 414 != sizeof (actinfo)) { 415 int err = errno; 416 (void) close(procfd); 417 if (err == EAGAIN) 418 goto retry; 419 if (err != ENOENT) 420 (void) fprintf(stderr, gettext( 421 "%s: read() failed on %s: %s \n"), 422 prog, pname, strerror(err)); 423 continue; 424 } 425 (void) close(procfd); 426 427 up->p_igintr = 428 actinfo[SIGINT-1].sa_handler == SIG_IGN && 429 actinfo[SIGQUIT-1].sa_handler == SIG_IGN; 430 431 /* 432 * Process args. 433 */ 434 up->p_args[0] = 0; 435 clnarglist(info.pr_psargs); 436 (void) strcat(up->p_args, info.pr_psargs); 437 if (up->p_args[0] == 0 || 438 up->p_args[0] == '-' && up->p_args[1] <= ' ' || 439 up->p_args[0] == '?') { 440 (void) strcat(up->p_args, " ("); 441 (void) strcat(up->p_args, up->p_comm); 442 (void) strcat(up->p_args, ")"); 443 } 444 } 445 446 /* 447 * link pgrp together in case parents go away 448 * Pgrp chain is a single linked list originating 449 * from the pgrp leader to its group member. 450 */ 451 if (info.pr_pgid != info.pr_pid) { /* not pgrp leader */ 452 pgrp = findhash(info.pr_pgid); 453 up->p_pgrpl = pgrp->p_pgrpl; 454 pgrp->p_pgrpl = up; 455 } 456 parent = findhash(info.pr_ppid); 457 458 /* if this is the new member, link it in */ 459 if (parent->p_upid != INITPROCESS) { 460 if (parent->p_child) { 461 up->p_sibling = parent->p_child; 462 up->p_child = 0; 463 } 464 parent->p_child = up; 465 } 466 } 467 468 /* revert to non-privileged user after opening */ 469 (void) __priv_relinquish(); 470 471 (void) closedir(dirp); 472 (void) time(&now); /* get current time */ 473 474 /* 475 * loop through utmpx file, printing process info 476 * about each logged in user 477 */ 478 for (ut = utmpbegin; ut < utmpend; ut++) { 479 if (ut->ut_type != USER_PROCESS) 480 continue; 481 if (sel_user && strncmp(ut->ut_name, sel_user, NMAX) != 0) 482 continue; /* we're looking for somebody else */ 483 484 /* print login name of the user */ 485 PRINTF(("%-*.*s ", LOGIN_WIDTH, NMAX, ut->ut_name)); 486 487 /* print tty user is on */ 488 if (lflag) { 489 PRINTF(("%-*.*s ", LINE_WIDTH, LMAX, ut->ut_line)); 490 } else { 491 if (ut->ut_line[0] == 'p' && ut->ut_line[1] == 't' && 492 ut->ut_line[2] == 's' && ut->ut_line[3] == '/') { 493 PRINTF(("%-*.*s ", LINE_WIDTH, LMAX, 494 &ut->ut_line[4])); 495 } else { 496 PRINTF(("%-*.*s ", LINE_WIDTH, LMAX, 497 ut->ut_line)); 498 } 499 } 500 501 /* print when the user logged in */ 502 if (lflag) { 503 time_t tim = ut->ut_xtime; 504 prtat(&tim); 505 } 506 507 /* print idle time */ 508 idle = findidle(ut->ut_line); 509 prttime(idle, 8); 510 showtotals(findhash(ut->ut_pid)); 511 } 512 if (fclose(stdout) == EOF) { 513 perror((gettext("%s: fclose failed"), prog)); 514 exit(1); 515 } 516 return (0); 517 } 518 519 /* 520 * Prints the CPU time for all processes & children, 521 * and the cpu time for interesting process, 522 * and what the user is doing. 523 */ 524 static void 525 showtotals(struct uproc *up) 526 { 527 jobtime = 0; 528 proctime = 0; 529 empty = 1; 530 curpid = -1; 531 add_times = 1; 532 533 calctotals(up); 534 535 if (lflag) { 536 /* print CPU time for all processes & children */ 537 /* and need to convert clock ticks to seconds first */ 538 prttime((time_t)jobtime, 8); 539 540 /* print cpu time for interesting process */ 541 /* and need to convert clock ticks to seconds first */ 542 prttime((time_t)proctime, 8); 543 } 544 /* what user is doing, current process */ 545 PRINTF(("%-.32s\n", doing)); 546 } 547 548 /* 549 * This recursive routine descends the process 550 * tree starting from the given process pointer(up). 551 * It used depth-first search strategy and also marked 552 * each node as visited as it traversed down the tree. 553 * It calculates the process time for all processes & 554 * children. It also finds the interesting process 555 * and determines its cpu time and command. 556 */ 557 static void 558 calctotals(struct uproc *up) 559 { 560 struct uproc *zp; 561 562 /* 563 * Once a node has been visited, stop adding cpu times 564 * for its children so they don't get totalled twice. 565 * Still look for the interesting job for this utmp 566 * entry, however. 567 */ 568 if (up->p_state == VISITED) 569 add_times = 0; 570 up->p_state = VISITED; 571 if (up->p_state == NONE || up->p_state == ZOMBIE) 572 return; 573 574 if (empty && !up->p_igintr) { 575 empty = 0; 576 curpid = -1; 577 } 578 579 if (up->p_upid > curpid && (!up->p_igintr || empty)) { 580 curpid = up->p_upid; 581 if (lflag) 582 (void) strcpy(doing, up->p_args); 583 else 584 (void) strcpy(doing, up->p_comm); 585 } 586 587 if (add_times == 1) { 588 jobtime += up->p_time + up->p_ctime; 589 proctime += up->p_time; 590 } 591 592 /* descend for its children */ 593 if (up->p_child) { 594 calctotals(up->p_child); 595 for (zp = up->p_child->p_sibling; zp; zp = zp->p_sibling) 596 calctotals(zp); 597 } 598 } 599 600 /* 601 * Findhash finds the appropriate entry in the process 602 * hash table (pr_htbl) for the given pid in case that 603 * pid exists on the hash chain. It returns back a pointer 604 * to that uproc structure. If this is a new pid, it allocates 605 * a new node, initializes it, links it into the chain (after 606 * head) and returns a structure pointer. 607 */ 608 static struct uproc * 609 findhash(pid_t pid) 610 { 611 struct uproc *up, *tp; 612 613 tp = up = &pr_htbl[pid % HSIZE]; 614 if (up->p_upid == 0) { /* empty slot */ 615 up->p_upid = pid; 616 up->p_state = NONE; 617 up->p_child = up->p_sibling = up->p_pgrpl = up->p_link = 0; 618 return (up); 619 } 620 if (up->p_upid == pid) { /* found in hash table */ 621 return (up); 622 } 623 for (tp = up->p_link; tp; tp = tp->p_link) { /* follow chain */ 624 if (tp->p_upid == pid) 625 return (tp); 626 } 627 tp = malloc(sizeof (*tp)); /* add new node */ 628 if (!tp) { 629 (void) fprintf(stderr, gettext("%s: out of memory!: %s\n"), 630 prog, strerror(errno)); 631 exit(1); 632 } 633 (void) memset(tp, 0, sizeof (*tp)); 634 tp->p_upid = pid; 635 tp->p_state = NONE; 636 tp->p_child = tp->p_sibling = tp->p_pgrpl = 0; 637 tp->p_link = up->p_link; /* insert after head */ 638 up->p_link = tp; 639 return (tp); 640 } 641 642 #define HR (60 * 60) 643 #define DAY (24 * HR) 644 #define MON (30 * DAY) 645 646 /* 647 * prttime prints a time in days, hours, minutes, or seconds. 648 * The second argument is the field width. 649 */ 650 static void 651 prttime(time_t tim, int width) 652 { 653 char value[12]; 654 char *unit; 655 656 if (tim >= 36 * HR) { 657 (void) snprintf(value, sizeof (value), "%d", 658 (tim + (DAY / 2)) / (DAY)); 659 unit = dcgettext(NULL, "days", LC_TIME); 660 } else if (tim >= 36 * 60) { 661 (void) snprintf(value, sizeof (value), "%d", 662 (tim + (HR / 2)) / (HR)); 663 unit = dcgettext(NULL, "hours", LC_TIME); 664 } else if (tim >= 60) { 665 (void) snprintf(value, sizeof (value), "%d", 666 (tim + 30) / 60); 667 unit = dcgettext(NULL, "mins", LC_TIME); 668 } else if (tim > 0) { 669 (void) snprintf(value, sizeof (value), "%d", (int)tim); 670 unit = dcgettext(NULL, "secs", LC_TIME); 671 } else { 672 (void) strcpy(value, "0"); 673 unit = " "; 674 } 675 width -= 2 + strlen(value); 676 width = (width > 1) ? width : 1; 677 PRINTF(("%s %-*s ", value, width, unit)); 678 } 679 680 /* 681 * prints a locale-specific time given a pointer to a time of day 682 */ 683 static void 684 prtat(time_t *time) 685 { 686 struct tm *p; 687 688 p = localtime(time); 689 if (now - *time <= 18 * HR) { 690 char timestr[50]; 691 692 (void) strftime(timestr, sizeof (timestr), 693 "%X", p); 694 PRINTF(("%-11s ", timestr)); 695 } else if (now - *time <= 7 * DAY) { 696 char weekdaytime[20]; 697 698 (void) strftime(weekdaytime, sizeof (weekdaytime), 699 "%d %b", p); 700 PRINTF(("%-11s ", weekdaytime)); 701 } else { 702 char monthtime[20]; 703 704 (void) strftime(monthtime, sizeof (monthtime), 705 "%b %Y", p); 706 PRINTF(("%-11s ", monthtime)); 707 } 708 } 709 710 /* 711 * find & return number of minutes current tty has been idle 712 */ 713 static time_t 714 findidle(char *devname) 715 { 716 struct stat stbuf; 717 time_t lastaction, diff; 718 char ttyname[64]; 719 720 (void) strcpy(ttyname, "/dev/"); 721 (void) strcat(ttyname, devname); 722 if (stat(ttyname, &stbuf) != -1) { 723 lastaction = stbuf.st_atime; 724 diff = now - lastaction; 725 diff = DIV60(diff); 726 if (diff < 0) 727 diff = 0; 728 } else 729 diff = 0; 730 return (diff); 731 } 732 733 /* 734 * given a pointer to the argument string get rid of unsavory characters. 735 */ 736 static void 737 clnarglist(char *arglist) 738 { 739 char *c; 740 int err = 0; 741 742 /* get rid of unsavory characters */ 743 for (c = arglist; *c != NULL; c++) { 744 if ((*c < ' ') || (*c > 0176)) { 745 if (err++ > 5) { 746 *arglist = NULL; 747 break; 748 } 749 *c = '?'; 750 } 751 } 752 }