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