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