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