lastlogin.c revision 1.20 1 1.20 wiz /* $NetBSD: lastlogin.c,v 1.20 2020/05/07 12:52:40 wiz Exp $ */
2 1.1 phil /*
3 1.1 phil * Copyright (c) 1996 John M. Vinopal
4 1.1 phil * All rights reserved.
5 1.1 phil *
6 1.1 phil * Redistribution and use in source and binary forms, with or without
7 1.1 phil * modification, are permitted provided that the following conditions
8 1.1 phil * are met:
9 1.1 phil * 1. Redistributions of source code must retain the above copyright
10 1.1 phil * notice, this list of conditions and the following disclaimer.
11 1.1 phil * 2. Redistributions in binary form must reproduce the above copyright
12 1.1 phil * notice, this list of conditions and the following disclaimer in the
13 1.1 phil * documentation and/or other materials provided with the distribution.
14 1.1 phil * 3. All advertising materials mentioning features or use of this software
15 1.1 phil * must display the following acknowledgement:
16 1.1 phil * This product includes software developed for the NetBSD Project
17 1.1 phil * by John M. Vinopal.
18 1.1 phil * 4. The name of the author may not be used to endorse or promote products
19 1.1 phil * derived from this software without specific prior written permission.
20 1.1 phil *
21 1.1 phil * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 1.1 phil * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 1.1 phil * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 1.1 phil * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 1.1 phil * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 1.1 phil * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 1.1 phil * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 1.1 phil * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 1.1 phil * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 1.1 phil * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 1.1 phil * SUCH DAMAGE.
32 1.1 phil */
33 1.1 phil
34 1.3 lukem #include <sys/cdefs.h>
35 1.3 lukem #ifndef lint
36 1.20 wiz __RCSID("$NetBSD: lastlogin.c,v 1.20 2020/05/07 12:52:40 wiz Exp $");
37 1.3 lukem #endif
38 1.3 lukem
39 1.1 phil #include <sys/types.h>
40 1.3 lukem #include <err.h>
41 1.9 christos #include <db.h>
42 1.1 phil #include <errno.h>
43 1.1 phil #include <pwd.h>
44 1.1 phil #include <stdio.h>
45 1.5 matt #include <stdlib.h>
46 1.7 elric #include <string.h>
47 1.9 christos #include <fcntl.h>
48 1.3 lukem #include <time.h>
49 1.9 christos #include <arpa/inet.h>
50 1.9 christos #include <sys/socket.h>
51 1.9 christos #ifdef SUPPORT_UTMP
52 1.3 lukem #include <utmp.h>
53 1.9 christos #endif
54 1.9 christos #ifdef SUPPORT_UTMPX
55 1.9 christos #include <utmpx.h>
56 1.9 christos #endif
57 1.4 perry #include <unistd.h>
58 1.11 christos #include <util.h>
59 1.1 phil
60 1.16 kim #ifndef UT_NAMESIZE
61 1.16 kim # define UT_NAMESIZE 8
62 1.16 kim #endif
63 1.16 kim #ifndef UT_LINESIZE
64 1.16 kim # define UT_LINESIZE 8
65 1.16 kim #endif
66 1.16 kim #ifndef UT_HOSTSIZE
67 1.16 kim # define UT_HOSTSIZE 16
68 1.16 kim #endif
69 1.16 kim
70 1.16 kim #ifndef UTX_USERSIZE
71 1.16 kim # define UTX_USERSIZE 64
72 1.16 kim #endif
73 1.16 kim #ifndef UTX_LINESIZE
74 1.16 kim # define UTX_LINESIZE 64
75 1.16 kim #endif
76 1.16 kim #ifndef UTX_HOSTSIZE
77 1.16 kim # define UTX_HOSTSIZE 256
78 1.16 kim #endif
79 1.16 kim
80 1.18 kim /*
81 1.18 kim * Fields in the structure below are 1 byte longer than the maximum possible
82 1.18 kim * for NUL-termination.
83 1.18 kim */
84 1.7 elric struct output {
85 1.9 christos struct timeval o_tv;
86 1.16 kim char o_name[UTX_USERSIZE+1];
87 1.16 kim char o_line[UTX_LINESIZE+1];
88 1.16 kim char o_host[UTX_HOSTSIZE+1];
89 1.7 elric struct output *next;
90 1.7 elric };
91 1.7 elric
92 1.7 elric #define SORT_NONE 0x0000
93 1.7 elric #define SORT_REVERSE 0x0001
94 1.7 elric #define SORT_TIME 0x0002
95 1.7 elric #define DOSORT(x) ((x) & (SORT_TIME))
96 1.7 elric static int sortlog = SORT_NONE;
97 1.7 elric static struct output *outstack = NULL;
98 1.19 kim static struct output *outstack_p = NULL;
99 1.7 elric
100 1.16 kim static int fixed = 0;
101 1.16 kim #define FIXED_NAMELEN UT_NAMESIZE
102 1.16 kim #define FIXED_LINELEN UT_LINESIZE
103 1.16 kim /*
104 1.16 kim * This makes the "fixed" output fit in 79 columns.
105 1.16 kim * Using UT_HOSTSIZE (16) seems too conservative.
106 1.16 kim */
107 1.16 kim #define FIXED_HOSTLEN 32
108 1.16 kim
109 1.9 christos static int numeric = 0;
110 1.16 kim static size_t namelen = 0;
111 1.16 kim static size_t linelen = 0;
112 1.16 kim static size_t hostlen = 0;
113 1.16 kim #define SIZECOLUMNS (!(namelen && linelen && hostlen))
114 1.9 christos
115 1.16 kim static int comparelog(const void *, const void *);
116 1.16 kim static void output_record(struct output *);
117 1.9 christos #ifdef SUPPORT_UTMP
118 1.16 kim static void process_entry(struct passwd *, struct lastlog *);
119 1.16 kim static void dolastlog(const char *, int, char *[]);
120 1.9 christos #endif
121 1.9 christos #ifdef SUPPORT_UTMPX
122 1.16 kim static void process_entryx(struct passwd *, struct lastlogx *);
123 1.16 kim static void dolastlogx(const char *, int, char *[]);
124 1.9 christos #endif
125 1.19 kim static void append_record(struct output *);
126 1.16 kim static void sizecolumns(struct output *);
127 1.16 kim static void output_stack(struct output *);
128 1.16 kim static void sort_and_output_stack(struct output *);
129 1.15 joerg __dead static void usage(void);
130 1.1 phil
131 1.1 phil int
132 1.15 joerg main(int argc, char *argv[])
133 1.1 phil {
134 1.14 lukem const char *logfile =
135 1.14 lukem #if defined(SUPPORT_UTMPX)
136 1.14 lukem _PATH_LASTLOGX;
137 1.14 lukem #elif defined(SUPPORT_UTMP)
138 1.14 lukem _PATH_LASTLOG;
139 1.14 lukem #else
140 1.14 lukem #error "either SUPPORT_UTMP or SUPPORT_UTMPX must be defined"
141 1.14 lukem #endif
142 1.9 christos int ch;
143 1.9 christos size_t len;
144 1.1 phil
145 1.16 kim while ((ch = getopt(argc, argv, "f:FH:L:nN:rt")) != -1) {
146 1.7 elric switch (ch) {
147 1.9 christos case 'H':
148 1.9 christos hostlen = atoi(optarg);
149 1.9 christos break;
150 1.9 christos case 'f':
151 1.9 christos logfile = optarg;
152 1.9 christos break;
153 1.16 kim case 'F':
154 1.16 kim fixed++;
155 1.16 kim break;
156 1.9 christos case 'L':
157 1.9 christos linelen = atoi(optarg);
158 1.9 christos break;
159 1.9 christos case 'n':
160 1.9 christos numeric++;
161 1.9 christos break;
162 1.9 christos case 'N':
163 1.9 christos namelen = atoi(optarg);
164 1.9 christos break;
165 1.7 elric case 'r':
166 1.7 elric sortlog |= SORT_REVERSE;
167 1.7 elric break;
168 1.7 elric case 't':
169 1.7 elric sortlog |= SORT_TIME;
170 1.7 elric break;
171 1.7 elric default:
172 1.7 elric usage();
173 1.7 elric }
174 1.1 phil }
175 1.7 elric argc -= optind;
176 1.7 elric argv += optind;
177 1.1 phil
178 1.16 kim if (fixed) {
179 1.16 kim if (!namelen)
180 1.16 kim namelen = FIXED_NAMELEN;
181 1.16 kim if (!linelen)
182 1.16 kim linelen = FIXED_LINELEN;
183 1.16 kim if (!hostlen)
184 1.16 kim hostlen = FIXED_HOSTLEN;
185 1.16 kim }
186 1.16 kim
187 1.9 christos len = strlen(logfile);
188 1.9 christos
189 1.9 christos setpassent(1); /* Keep passwd file pointers open */
190 1.9 christos
191 1.9 christos #if defined(SUPPORT_UTMPX)
192 1.9 christos if (len > 0 && logfile[len - 1] == 'x')
193 1.9 christos dolastlogx(logfile, argc, argv);
194 1.9 christos else
195 1.9 christos #endif
196 1.9 christos #if defined(SUPPORT_UTMP)
197 1.9 christos dolastlog(logfile, argc, argv);
198 1.9 christos #endif
199 1.9 christos
200 1.9 christos setpassent(0); /* Close passwd file pointers */
201 1.9 christos
202 1.16 kim if (outstack) {
203 1.16 kim if (SIZECOLUMNS)
204 1.16 kim sizecolumns(outstack);
205 1.16 kim
206 1.16 kim if (DOSORT(sortlog))
207 1.16 kim sort_and_output_stack(outstack);
208 1.16 kim else
209 1.16 kim output_stack(outstack);
210 1.16 kim }
211 1.9 christos
212 1.9 christos return 0;
213 1.9 christos }
214 1.9 christos
215 1.9 christos #ifdef SUPPORT_UTMP
216 1.9 christos static void
217 1.9 christos dolastlog(const char *logfile, int argc, char **argv)
218 1.9 christos {
219 1.9 christos int i;
220 1.9 christos FILE *fp = fopen(logfile, "r");
221 1.9 christos struct passwd *passwd;
222 1.9 christos struct lastlog l;
223 1.9 christos
224 1.1 phil if (fp == NULL)
225 1.1 phil err(1, "%s", logfile);
226 1.1 phil
227 1.1 phil /* Process usernames given on the command line. */
228 1.8 wulf if (argc > 0) {
229 1.9 christos off_t offset;
230 1.8 wulf for (i = 0; i < argc; i++) {
231 1.1 phil if ((passwd = getpwnam(argv[i])) == NULL) {
232 1.1 phil warnx("user '%s' not found", argv[i]);
233 1.1 phil continue;
234 1.1 phil }
235 1.1 phil /* Calculate the offset into the lastlog file. */
236 1.9 christos offset = passwd->pw_uid * sizeof(l);
237 1.9 christos if (fseeko(fp, offset, SEEK_SET)) {
238 1.1 phil warn("fseek error");
239 1.1 phil continue;
240 1.1 phil }
241 1.9 christos if (fread(&l, sizeof(l), 1, fp) != 1) {
242 1.1 phil warnx("fread error on '%s'", passwd->pw_name);
243 1.1 phil clearerr(fp);
244 1.1 phil continue;
245 1.1 phil }
246 1.9 christos process_entry(passwd, &l);
247 1.1 phil }
248 1.1 phil }
249 1.1 phil /* Read all lastlog entries, looking for active ones */
250 1.1 phil else {
251 1.9 christos for (i = 0; fread(&l, sizeof(l), 1, fp) == 1; i++) {
252 1.9 christos if (l.ll_time == 0)
253 1.2 christos continue;
254 1.13 atatat if ((passwd = getpwuid(i)) == NULL) {
255 1.13 atatat static struct passwd p;
256 1.13 atatat static char n[32];
257 1.13 atatat snprintf(n, sizeof(n), "(%d)", i);
258 1.13 atatat p.pw_uid = i;
259 1.13 atatat p.pw_name = n;
260 1.13 atatat passwd = &p;
261 1.13 atatat }
262 1.13 atatat process_entry(passwd, &l);
263 1.1 phil }
264 1.1 phil if (ferror(fp))
265 1.1 phil warnx("fread error");
266 1.1 phil }
267 1.1 phil
268 1.9 christos (void)fclose(fp);
269 1.9 christos }
270 1.9 christos
271 1.9 christos static void
272 1.9 christos process_entry(struct passwd *p, struct lastlog *l)
273 1.9 christos {
274 1.9 christos struct output o;
275 1.9 christos
276 1.18 kim memset(&o, 0, sizeof(o));
277 1.17 kim if (numeric > 1)
278 1.17 kim (void)snprintf(o.o_name, sizeof(o.o_name), "%d", p->pw_uid);
279 1.17 kim else
280 1.17 kim (void)strlcpy(o.o_name, p->pw_name, sizeof(o.o_name));
281 1.18 kim (void)memcpy(o.o_line, l->ll_line, sizeof(l->ll_line));
282 1.18 kim (void)memcpy(o.o_host, l->ll_host, sizeof(l->ll_host));
283 1.9 christos o.o_tv.tv_sec = l->ll_time;
284 1.9 christos o.o_tv.tv_usec = 0;
285 1.9 christos o.next = NULL;
286 1.1 phil
287 1.9 christos /*
288 1.16 kim * If we are dynamically sizing the columns or sorting the log,
289 1.16 kim * we need all the entries in memory so push the current entry
290 1.16 kim * onto a stack. Otherwise, we can just output it.
291 1.9 christos */
292 1.16 kim if (SIZECOLUMNS || DOSORT(sortlog))
293 1.19 kim append_record(&o);
294 1.9 christos else
295 1.16 kim output_record(&o);
296 1.9 christos }
297 1.9 christos #endif
298 1.9 christos
299 1.9 christos #ifdef SUPPORT_UTMPX
300 1.9 christos static void
301 1.9 christos dolastlogx(const char *logfile, int argc, char **argv)
302 1.9 christos {
303 1.9 christos int i = 0;
304 1.9 christos DB *db = dbopen(logfile, O_RDONLY|O_SHLOCK, 0, DB_HASH, NULL);
305 1.9 christos DBT key, data;
306 1.9 christos struct lastlogx l;
307 1.9 christos struct passwd *passwd;
308 1.9 christos
309 1.9 christos if (db == NULL)
310 1.9 christos err(1, "%s", logfile);
311 1.9 christos
312 1.9 christos if (argc > 0) {
313 1.9 christos for (i = 0; i < argc; i++) {
314 1.9 christos if ((passwd = getpwnam(argv[i])) == NULL) {
315 1.9 christos warnx("User `%s' not found", argv[i]);
316 1.9 christos continue;
317 1.9 christos }
318 1.9 christos key.data = &passwd->pw_uid;
319 1.9 christos key.size = sizeof(passwd->pw_uid);
320 1.9 christos
321 1.9 christos switch ((*db->get)(db, &key, &data, 0)) {
322 1.9 christos case 0:
323 1.9 christos break;
324 1.9 christos case 1:
325 1.9 christos warnx("User `%s' not found", passwd->pw_name);
326 1.9 christos continue;
327 1.9 christos case -1:
328 1.9 christos warn("Error looking up `%s'", passwd->pw_name);
329 1.9 christos continue;
330 1.9 christos default:
331 1.9 christos abort();
332 1.9 christos }
333 1.9 christos
334 1.9 christos if (data.size != sizeof(l)) {
335 1.9 christos errno = EFTYPE;
336 1.9 christos err(1, "%s", logfile);
337 1.9 christos }
338 1.9 christos (void)memcpy(&l, data.data, sizeof(l));
339 1.9 christos
340 1.9 christos process_entryx(passwd, &l);
341 1.9 christos }
342 1.9 christos }
343 1.9 christos /* Read all lastlog entries, looking for active ones */
344 1.9 christos else {
345 1.9 christos switch ((*db->seq)(db, &key, &data, R_FIRST)) {
346 1.9 christos case 0:
347 1.9 christos break;
348 1.9 christos case 1:
349 1.9 christos warnx("No entries found");
350 1.9 christos (*db->close)(db);
351 1.9 christos return;
352 1.9 christos case -1:
353 1.9 christos warn("Error seeking to first entry");
354 1.9 christos (*db->close)(db);
355 1.9 christos return;
356 1.9 christos default:
357 1.9 christos abort();
358 1.9 christos }
359 1.9 christos
360 1.9 christos do {
361 1.9 christos uid_t uid;
362 1.9 christos
363 1.9 christos if (key.size != sizeof(uid) || data.size != sizeof(l)) {
364 1.9 christos errno = EFTYPE;
365 1.9 christos err(1, "%s", logfile);
366 1.9 christos }
367 1.9 christos (void)memcpy(&uid, key.data, sizeof(uid));
368 1.9 christos
369 1.9 christos if ((passwd = getpwuid(uid)) == NULL) {
370 1.17 kim static struct passwd p;
371 1.17 kim static char n[32];
372 1.17 kim snprintf(n, sizeof(n), "(%d)", i);
373 1.17 kim p.pw_uid = i;
374 1.17 kim p.pw_name = n;
375 1.17 kim passwd = &p;
376 1.9 christos }
377 1.9 christos (void)memcpy(&l, data.data, sizeof(l));
378 1.9 christos process_entryx(passwd, &l);
379 1.9 christos } while ((i = (*db->seq)(db, &key, &data, R_NEXT)) == 0);
380 1.9 christos
381 1.9 christos switch (i) {
382 1.9 christos case 1:
383 1.9 christos break;
384 1.9 christos case -1:
385 1.9 christos warn("Error seeking to last entry");
386 1.9 christos break;
387 1.9 christos case 0:
388 1.9 christos default:
389 1.9 christos abort();
390 1.9 christos }
391 1.9 christos }
392 1.7 elric
393 1.9 christos (*db->close)(db);
394 1.1 phil }
395 1.1 phil
396 1.7 elric static void
397 1.9 christos process_entryx(struct passwd *p, struct lastlogx *l)
398 1.7 elric {
399 1.7 elric struct output o;
400 1.7 elric
401 1.18 kim memset(&o, 0, sizeof(o));
402 1.13 atatat if (numeric > 1)
403 1.13 atatat (void)snprintf(o.o_name, sizeof(o.o_name), "%d", p->pw_uid);
404 1.13 atatat else
405 1.13 atatat (void)strlcpy(o.o_name, p->pw_name, sizeof(o.o_name));
406 1.18 kim (void)memcpy(o.o_line, l->ll_line, sizeof(l->ll_line));
407 1.17 kim if (numeric)
408 1.16 kim (void)sockaddr_snprintf(o.o_host, sizeof(o.o_host), "%a",
409 1.16 kim (struct sockaddr *)&l->ll_ss);
410 1.17 kim else
411 1.18 kim (void)memcpy(o.o_host, l->ll_host, sizeof(l->ll_host));
412 1.9 christos o.o_tv = l->ll_tv;
413 1.7 elric o.next = NULL;
414 1.7 elric
415 1.7 elric /*
416 1.16 kim * If we are dynamically sizing the columns or sorting the log,
417 1.16 kim * we need all the entries in memory so push the current entry
418 1.16 kim * onto a stack. Otherwise, we can just output it.
419 1.7 elric */
420 1.16 kim if (SIZECOLUMNS || DOSORT(sortlog))
421 1.19 kim append_record(&o);
422 1.7 elric else
423 1.16 kim output_record(&o);
424 1.7 elric }
425 1.9 christos #endif
426 1.7 elric
427 1.7 elric static void
428 1.19 kim append_record(struct output *o)
429 1.7 elric {
430 1.7 elric struct output *out;
431 1.7 elric
432 1.7 elric out = malloc(sizeof(*out));
433 1.7 elric if (!out)
434 1.7 elric err(EXIT_FAILURE, "malloc failed");
435 1.9 christos (void)memcpy(out, o, sizeof(*out));
436 1.7 elric out->next = NULL;
437 1.7 elric
438 1.19 kim if (outstack_p)
439 1.19 kim outstack_p = outstack_p->next = out;
440 1.19 kim else
441 1.19 kim outstack = outstack_p = out;
442 1.7 elric }
443 1.7 elric
444 1.7 elric static void
445 1.16 kim sizecolumns(struct output *stack)
446 1.16 kim {
447 1.16 kim struct output *o;
448 1.16 kim size_t len;
449 1.16 kim
450 1.16 kim if (!namelen)
451 1.16 kim for (o = stack; o; o = o->next) {
452 1.16 kim len = strlen(o->o_name);
453 1.16 kim if (namelen < len)
454 1.16 kim namelen = len;
455 1.16 kim }
456 1.16 kim
457 1.16 kim if (!linelen)
458 1.16 kim for (o = stack; o; o = o->next) {
459 1.16 kim len = strlen(o->o_line);
460 1.16 kim if (linelen < len)
461 1.16 kim linelen = len;
462 1.16 kim }
463 1.16 kim
464 1.16 kim if (!hostlen)
465 1.16 kim for (o = stack; o; o = o->next) {
466 1.16 kim len = strlen(o->o_host);
467 1.16 kim if (hostlen < len)
468 1.16 kim hostlen = len;
469 1.16 kim }
470 1.16 kim }
471 1.16 kim
472 1.16 kim static void
473 1.16 kim output_stack(struct output *stack)
474 1.16 kim {
475 1.16 kim struct output *o;
476 1.16 kim
477 1.16 kim for (o = stack; o; o = o->next)
478 1.16 kim output_record(o);
479 1.16 kim }
480 1.16 kim
481 1.16 kim static void
482 1.16 kim sort_and_output_stack(struct output *o)
483 1.7 elric {
484 1.7 elric struct output **outs;
485 1.7 elric struct output *tmpo;
486 1.7 elric int num;
487 1.7 elric int i;
488 1.7 elric
489 1.7 elric /* count the number of entries to display */
490 1.16 kim for (num=0, tmpo = o; tmpo; tmpo = tmpo->next, num++)
491 1.7 elric ;
492 1.7 elric
493 1.7 elric outs = malloc(sizeof(*outs) * num);
494 1.7 elric if (!outs)
495 1.7 elric err(EXIT_FAILURE, "malloc failed");
496 1.7 elric for (i=0, tmpo = o; i < num; tmpo=tmpo->next, i++)
497 1.7 elric outs[i] = tmpo;
498 1.7 elric
499 1.7 elric mergesort(outs, num, sizeof(*outs), comparelog);
500 1.7 elric
501 1.7 elric for (i=0; i < num; i++)
502 1.16 kim output_record(outs[i]);
503 1.7 elric }
504 1.7 elric
505 1.7 elric static int
506 1.7 elric comparelog(const void *left, const void *right)
507 1.7 elric {
508 1.14 lukem const struct output *l = *(const struct output * const *)left;
509 1.14 lukem const struct output *r = *(const struct output * const *)right;
510 1.7 elric int order = (sortlog&SORT_REVERSE)?-1:1;
511 1.7 elric
512 1.9 christos if (l->o_tv.tv_sec < r->o_tv.tv_sec)
513 1.7 elric return 1 * order;
514 1.9 christos if (l->o_tv.tv_sec == r->o_tv.tv_sec)
515 1.7 elric return 0;
516 1.7 elric return -1 * order;
517 1.7 elric }
518 1.7 elric
519 1.1 phil /* Duplicate the output of last(1) */
520 1.1 phil static void
521 1.16 kim output_record(struct output *o)
522 1.1 phil {
523 1.9 christos time_t t = (time_t)o->o_tv.tv_sec;
524 1.16 kim printf("%-*.*s %-*.*s %-*.*s %s",
525 1.10 he (int)namelen, (int)namelen, o->o_name,
526 1.10 he (int)linelen, (int)linelen, o->o_line,
527 1.16 kim (int)hostlen, (int)hostlen, o->o_host,
528 1.9 christos t ? ctime(&t) : "Never logged in\n");
529 1.1 phil }
530 1.1 phil
531 1.1 phil static void
532 1.9 christos usage(void)
533 1.1 phil {
534 1.20 wiz (void)fprintf(stderr, "Usage: %s [-Fnrt] [-f filename] "
535 1.20 wiz "[-H hostsize] [-L linesize] [-N namesize] [user ...]\n",
536 1.9 christos getprogname());
537 1.1 phil exit(1);
538 1.1 phil }
539