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
main(int argc,char * argv[])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
showtotals(struct uproc * up)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
calctotals(struct uproc * up)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 *
findhash(pid_t pid)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
prttime(time_t tim,char * tail)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
prtat(time_t * time)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
findidle(char * devname)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
clnarglist(char * arglist)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
checkampm(char * str)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 }
754