2 * last.c Re-implementation of the 'last' command, this time
3 * for Linux. Yes I know there is BSD last, but I
4 * just felt like writing this. No thanks :-).
5 * Also, this version gives lots more info (especially with -x)
7 * Author: Miquel van Smoorenburg, miquels@cistron.nl
9 * Version: @(#)last 2.85 30-Jul-2004 miquels@cistron.nl
11 * This file is part of the sysvinit suite,
12 * Copyright (C) 1991-2004 Miquel van Smoorenburg.
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 2 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
29 #include <sys/types.h>
31 #include <sys/fcntl.h>
43 #include <netinet/in.h>
45 #include <arpa/inet.h>
49 # define SHUTDOWN_TIME 254
52 char *Version
= "@(#) last 2.85 31-Apr-2004 miquels";
54 #define CHOP_DOMAIN 0 /* Define to chop off local domainname. */
55 #define NEW_UTMP 1 /* Fancy & fast utmp read code. */
56 #define UCHUNKSIZE 16384 /* How much we read at once. */
58 /* Double linked list of struct utmp's */
61 struct utmplist
*next
;
62 struct utmplist
*prev
;
64 struct utmplist
*utmplist
= NULL
;
66 /* Types of listing */
67 #define R_CRASH 1 /* No logout record, system boot in between */
68 #define R_DOWN 2 /* System brought down in decent way */
69 #define R_NORMAL 3 /* Normal */
70 #define R_NOW 4 /* Still logged in */
71 #define R_REBOOT 5 /* Reboot record. */
72 #define R_PHANTOM 6 /* No logout record but session is stale. */
73 #define R_TIMECHANGE 7 /* NEW_TIME or OLD_TIME */
75 /* Global variables */
76 int maxrecs
= 0; /* Maximum number of records to list. */
77 int recsdone
= 0; /* Number of records listed */
78 int showhost
= 1; /* Show hostname too? */
79 int name_len
= 8; /* Default print 8 characters of name */
80 int domain_len
= 16; /* Default print 16 characters of domain */
81 int oldfmt
= 0; /* Use old libc5 format? */
82 char **show
= NULL
; /* What do they want us to show */
83 char *ufile
; /* Filename of this file */
84 time_t lastdate
; /* Last date we've seen */
85 char *progname
; /* Name of this program */
87 char hostname
[256]; /* For gethostbyname() */
88 char *domainname
; /* Our domainname. */
92 * Convert old utmp format to new.
94 void uconv(struct oldutmp
*oldut
, struct utmp
*utn
)
96 memset(utn
, 0, sizeof(struct utmp
));
97 utn
->ut_type
= oldut
->ut_type
;
98 utn
->ut_pid
= oldut
->ut_pid
;
99 utn
->ut_time
= oldut
->ut_oldtime
;
100 utn
->ut_addr
= oldut
->ut_oldaddr
;
101 strncpy(utn
->ut_line
, oldut
->ut_line
, OLD_LINESIZE
);
102 strncpy(utn
->ut_user
, oldut
->ut_user
, OLD_NAMESIZE
);
103 strncpy(utn
->ut_host
, oldut
->ut_host
, OLD_HOSTSIZE
);
108 * Read one utmp entry, return in new format.
109 * Automatically reposition file pointer.
111 int uread(FILE *fp
, struct utmp
*u
, int *quit
)
114 static char buf
[UCHUNKSIZE
];
122 if (quit
== NULL
&& u
!= NULL
) {
127 r
= fread(&uto
, sizeof(uto
), 1, fp
);
130 r
= fread(u
, sizeof(struct utmp
), 1, fp
);
136 * Initialize and position.
138 utsize
= oldfmt
? sizeof(uto
) : sizeof(struct utmp
);
139 fseeko(fp
, 0, SEEK_END
);
143 o
= ((fpos
- 1) / UCHUNKSIZE
) * UCHUNKSIZE
;
144 if (fseeko(fp
, o
, SEEK_SET
) < 0) {
145 fprintf(stderr
, "%s: seek failed!\n", progname
);
148 bpos
= (int)(fpos
- o
);
149 if (fread(buf
, bpos
, 1, fp
) != 1) {
150 fprintf(stderr
, "%s: read failed!\n", progname
);
158 * Read one struct. From the buffer if possible.
163 uconv((struct oldutmp
*)(buf
+ bpos
), u
);
165 memcpy(u
, buf
+ bpos
, sizeof(struct utmp
));
170 * Oops we went "below" the buffer. We should be able to
171 * seek back UCHUNKSIZE bytes.
178 * Copy whatever is left in the buffer.
180 memcpy(tmp
+ (-bpos
), buf
, utsize
+ bpos
);
181 if (fseeko(fp
, fpos
, SEEK_SET
) < 0) {
187 * Read another UCHUNKSIZE bytes.
189 if (fread(buf
, UCHUNKSIZE
, 1, fp
) != 1) {
195 * The end of the UCHUNKSIZE byte buffer should be the first
196 * few bytes of the current struct utmp.
198 memcpy(tmp
, buf
+ UCHUNKSIZE
+ bpos
, -bpos
);
202 uconv((struct oldutmp
*)tmp
, u
);
204 memcpy(u
, tmp
, sizeof(struct utmp
));
212 * Read one utmp entry, return in new format.
213 * Automatically reposition file pointer.
215 int uread(FILE *fp
, struct utmp
*u
, int *quit
)
221 r
= oldfmt
? sizeof(struct oldutmp
) : sizeof(struct utmp
);
222 fseek(fp
, -1 * r
, SEEK_END
);
227 r
= fread(u
, sizeof(struct utmp
), 1, fp
);
229 if (fseeko(fp
, -2 * sizeof(struct utmp
), SEEK_CUR
) < 0)
234 r
= fread(&uto
, sizeof(struct oldutmp
), 1, fp
);
236 if (fseeko(fp
, -2 * sizeof(struct oldutmp
), SEEK_CUR
) < 0)
246 * Try to be smart about the location of the BTMP file
249 #define BTMP_FILE getbtmp()
252 static char btmp
[128];
255 strcpy(btmp
, WTMP_FILE
);
256 if ((p
= strrchr(btmp
, '/')) == NULL
)
261 strcat(btmp
, "btmp");
267 * Print a short date.
271 char *s
= ctime(&lastdate
);
281 printf("Interrupted %s\n", showdate());
290 printf("Interrupted %s\n", showdate());
291 signal(SIGQUIT
, quit_handler
);
295 * Get the basename of a filename
297 char *mybasename(char *s
)
301 if ((p
= strrchr(s
, '/')) != NULL
)
308 char* localtimestamp(time_t time
)
314 loc
= localtime(&time
);
322 strftime(ret
, 255, "%s", loc
);
325 /* FIXME: free(ret) */
329 * Show one line of information on screen
331 int list(struct utmp
*p
, time_t t
, int what
)
335 char ipaddress
[16] = "";
337 char utline
[UT_LINESIZE
+1];
343 * uucp and ftp have special-type entries
346 strncat(utline
, p
->ut_line
, UT_LINESIZE
);
347 if (strncmp(utline
, "ftp", 3) == 0 && isdigit(utline
[3]))
349 if (strncmp(utline
, "uucp", 4) == 0 && isdigit(utline
[4]))
353 * Is this something we wanna show?
356 for (walk
= show
; *walk
; walk
++) {
357 if (strncmp(p
->ut_name
, *walk
, UT_NAMESIZE
) == 0 ||
358 strcmp(utline
, *walk
) == 0 ||
359 (strncmp(utline
, "tty", 3) == 0 &&
360 strcmp(utline
+ 3, *walk
) == 0)) break;
362 if (*walk
== NULL
) return 0;
367 sprintf(aux2
, "crash");
370 sprintf(aux2
, "down");
373 sprintf(aux2
, "still logged in");
376 sprintf(aux2
, "gone");
379 sprintf(aux1
, "reboot");
390 if (len
>= (int)sizeof(domain
)) len
= sizeof(domain
) - 1;
392 strncat(domain
, p
->ut_host
, len
);
397 * See if this is in our domain.
399 if (s
= strchr(p
->ut_host
, '.') != NULL
&&
400 strcmp(s
+ 1, domainname
) == 0) *s
= 0;
403 sprintf(ipaddress
, "%d.%d.%d.%d", p
->ut_addr
&0xFF, p
->ut_addr
>>8&0xFF, p
->ut_addr
>>16&0xFF, p
->ut_addr
>>24&0xFF);
404 if(p
->ut_type
== RUN_LVL
)
406 sprintf(aux1
, p
->ut_name
);
408 sprintf(aux2
, utline
);
411 else if(p
->ut_type
== SHUTDOWN_TIME
)
413 sprintf(aux1
, p
->ut_name
);
416 len
= snprintf(final
, sizeof(final
),
417 "%s;%s;%s;%s;%s;%s;%s;%s;%d\n",
418 p
->ut_name
, utline
, ipaddress
, domain
, localtimestamp((time_t)p
->ut_time
), localtimestamp(t
), aux1
, aux2
, p
->ut_pid
);
420 #if defined(__GLIBC__)
421 # if (__GLIBC__ == 2) && (__GLIBC_MINOR__ == 0)
422 final
[sizeof(final
)-1] = '\0';
427 * Print out "final" string safely.
429 for (s
= final
; *s
; s
++) {
430 if (*s
== '\n' || (*s
>= 32 && (unsigned char)*s
<= 126))
436 if (len
< 0 || (size_t)len
>= sizeof(final
))
440 if (maxrecs
&& recsdone
>= maxrecs
)
452 fprintf(stderr
, "Usage: %s [-num | -n num] [-f file] "
453 "[-t YYYYMMDDHHMMSS] "
454 "[-ow] [username..] [tty..]\n", s
);
458 time_t parsetm(char *ts
)
463 memset(&tm
, 0, sizeof(tm
));
465 if (sscanf(ts
, "%4d%2d%2d%2d%2d%2d", &u
.tm_year
,
466 &u
.tm_mon
, &u
.tm_mday
, &u
.tm_hour
, &u
.tm_min
,
476 if ((tm
= mktime(&u
)) == (time_t)-1)
480 * Unfortunately mktime() is much more forgiving than
481 * it should be. For example, it'll gladly accept
482 * "30" as a valid month number. This behavior is by
483 * design, but we don't like it, so we want to detect
486 if (u
.tm_year
!= origu
.tm_year
||
487 u
.tm_mon
!= origu
.tm_mon
||
488 u
.tm_mday
!= origu
.tm_mday
||
489 u
.tm_hour
!= origu
.tm_hour
||
490 u
.tm_min
!= origu
.tm_min
||
491 u
.tm_sec
!= origu
.tm_sec
)
497 int main(int argc
, char **argv
)
499 FILE *fp
; /* Filepointer of wtmp file */
501 struct utmp ut
; /* Current utmp entry */
502 struct utmp oldut
; /* Old utmp entry to check for duplicates */
503 struct utmplist
*p
; /* Pointer into utmplist */
504 struct utmplist
*next
;/* Pointer into utmplist */
506 time_t lastboot
= 0; /* Last boottime */
507 time_t lastrch
= 0; /* Last run level change */
508 time_t lastdown
; /* Last downtime */
509 time_t begintime
; /* When wtmp begins */
510 int whydown
= 0; /* Why we went down: crash or shutdown */
512 int c
, x
; /* Scratch */
513 struct stat st
; /* To stat the [uw]tmp file */
514 int quit
= 0; /* Flag */
515 int down
= 0; /* Down flag */
516 int lastb
= 0; /* Is this 'lastb' ? */
517 char *altufile
= NULL
;/* Alternate wtmp */
519 time_t until
= 0; /* at what time to stop parsing the file */
521 progname
= mybasename(argv
[0]);
523 /* Process the arguments. */
524 while((c
= getopt(argc
, argv
, "f:n:xot:0123456789w")) != EOF
)
527 maxrecs
= atoi(optarg
);
533 if((altufile
= malloc(strlen(optarg
)+1)) == NULL
) {
534 fprintf(stderr
, "%s: out of memory\n",
538 strcpy(altufile
, optarg
);
541 if ((until
= parsetm(optarg
)) == (time_t)-1) {
542 fprintf(stderr
, "%s: Invalid time value \"%s\"\n",
548 if (UT_NAMESIZE
> name_len
)
549 name_len
= UT_NAMESIZE
;
550 if (UT_HOSTSIZE
> domain_len
)
551 domain_len
= UT_HOSTSIZE
;
553 case '0': case '1': case '2': case '3': case '4':
554 case '5': case '6': case '7': case '8': case '9':
555 maxrecs
= 10*maxrecs
+ c
- '0';
561 if (optind
< argc
) show
= argv
+ optind
;
564 * Which file do we want to read?
566 if (strncmp(progname
, "lastb", 5) == 0) {
583 * Find out domainname.
585 * This doesn't work on modern systems, where only a DNS
586 * lookup of the result from hostname() will get you the domainname.
587 * Remember that domainname() is the NIS domainname, not DNS.
588 * So basically this whole piece of code is bullshit.
591 (void) gethostname(hostname
, sizeof(hostname
));
592 if ((domainname
= strchr(hostname
, '.')) != NULL
) domainname
++;
593 if (domainname
== NULL
|| domainname
[0] == 0) {
595 (void) getdomainname(hostname
, sizeof(hostname
));
596 hostname
[sizeof(hostname
) - 1] = 0;
597 domainname
= hostname
;
598 if (strcmp(domainname
, "(none)") == 0 || domainname
[0] == 0)
604 * Install signal handlers
606 signal(SIGINT
, int_handler
);
607 signal(SIGQUIT
, quit_handler
);
612 if ((fp
= fopen(ufile
, "r")) == NULL
) {
614 fprintf(stderr
, "%s: %s: %s\n", progname
, ufile
, strerror(errno
));
615 if (altufile
== NULL
&& x
== ENOENT
)
616 fprintf(stderr
, "Perhaps this file was removed by the "
617 "operator to prevent logging %s info.\n", progname
);
622 * Optimize the buffer size.
624 setvbuf(fp
, NULL
, _IOFBF
, UCHUNKSIZE
);
627 * Read first structure to capture the time field
629 if (uread(fp
, &ut
, NULL
) == 1)
630 begintime
= ut
.ut_time
;
632 fstat(fileno(fp
), &st
);
633 begintime
= st
.st_ctime
;
637 printf(";;;;%s;;begin;\n", localtimestamp(begintime
));
641 * Go to end of file minus one structure
642 * and/or initialize utmp reading code.
644 uread(fp
, NULL
, NULL
);
647 * Read struct after struct backwards from the file.
651 if (uread(fp
, &ut
, &quit
) != 1)
654 if (until
&& until
< ut
.ut_time
)
657 if (memcmp(&ut
, &oldut
, sizeof(struct utmp
)) == 0) continue;
658 memcpy(&oldut
, &ut
, sizeof(struct utmp
));
659 lastdate
= ut
.ut_time
;
662 quit
= list(&ut
, ut
.ut_time
, R_NORMAL
);
667 * Set ut_type to the correct type.
669 if (strncmp(ut
.ut_line
, "~", 1) == 0) {
670 strcpy(ut
.ut_line
, "");
671 if (strncmp(ut
.ut_user
, "shutdown", 8) == 0)
672 ut
.ut_type
= SHUTDOWN_TIME
;
673 else if (strncmp(ut
.ut_user
, "reboot", 6) == 0)
674 ut
.ut_type
= BOOT_TIME
;
675 else if (strncmp(ut
.ut_user
, "runlevel", 8) == 0)
676 ut
.ut_type
= RUN_LVL
;
680 * For stupid old applications that don't fill in
684 if (ut
.ut_type
!= DEAD_PROCESS
&&
685 ut
.ut_name
[0] && ut
.ut_line
[0] &&
686 strcmp(ut
.ut_name
, "LOGIN") != 0)
687 ut
.ut_type
= USER_PROCESS
;
689 * Even worse, applications that write ghost
690 * entries: ut_type set to USER_PROCESS but
693 if (ut
.ut_name
[0] == 0)
694 ut
.ut_type
= DEAD_PROCESS
;
699 if (strcmp(ut
.ut_name
, "date") == 0) {
700 if (ut
.ut_line
[0] == '|') ut
.ut_type
= OLD_TIME
;
701 if (ut
.ut_line
[0] == '{') ut
.ut_type
= NEW_TIME
; /* } */
706 switch (ut
.ut_type
) {
708 quit
= list(&ut
, lastboot
, R_NORMAL
);
709 lastdown
= lastrch
= ut
.ut_time
;
714 strcpy(ut
.ut_line
, ut
.ut_type
== NEW_TIME
? "new time" : "old time");
715 quit
= list(&ut
, lastdown
, R_TIMECHANGE
);
718 quit
= list(&ut
, lastdown
, R_REBOOT
);
719 lastboot
= ut
.ut_time
;
724 sprintf(ut
.ut_line
, "%c", x
);
725 quit
= list(&ut
, lastrch
, R_NORMAL
);
726 if (x
== '0' || x
== '6') {
727 lastdown
= ut
.ut_time
;
729 ut
.ut_type
= SHUTDOWN_TIME
;
731 lastrch
= ut
.ut_time
;
736 * This was a login - show the first matching
737 * logout record and delete all records with
741 for (p
= utmplist
; p
; p
= next
) {
743 if (strncmp(p
->ut
.ut_line
, ut
.ut_line
, UT_LINESIZE
) == 0) {
746 quit
= list(&ut
, p
->ut
.ut_time
, R_NORMAL
);
749 if (p
->next
) p
->next
->prev
= p
->prev
;
751 p
->prev
->next
= p
->next
;
758 * Not found? Then crashed, down, still
759 * logged in, or missing logout record.
764 /* Is process still alive? */
766 kill(ut
.ut_pid
, 0) != 0 &&
771 quit
= list(&ut
, lastboot
, c
);
777 * Just store the data if it is
778 * interesting enough.
780 if (ut
.ut_line
[0] == 0)
782 if ((p
= malloc(sizeof(struct utmplist
))) == NULL
) {
783 fprintf(stderr
, "%s: out of memory\n", progname
);
786 memcpy(&p
->ut
, &ut
, sizeof(struct utmp
));
789 if (utmplist
) utmplist
->prev
= p
;
795 * If we saw a shutdown/reboot record we can remove
796 * the entire current utmplist.
799 lastboot
= ut
.ut_time
;
800 whydown
= (ut
.ut_type
== SHUTDOWN_TIME
) ? R_DOWN
: R_CRASH
;
801 for (p
= utmplist
; p
; p
= next
) {
813 * Should we free memory here? Nah. This is not NT :)