/*
* Copyright 2001 by Marco d'Itri <md@linux.it>
* Copyright 2000,2001,2002,2003,2004
* Marc Lehmann <pcg@goof.com>,
* Chris Moore <chris.moore@mail.com>,
* and many others, see README
*
* Original version by Mike Baker.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/
#include "config.h"
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <locale.h>
#include <ctype.h>
#include <stdarg.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#if HAS_REGEX
#include <regex.h>
#endif
#define SHADE_X 2
#define SHADE_Y 2
/* some italic fonts still go over the margin - this margin of error cleans up the mess */
#define MARGIN_OF_ERROR 2
/* data structures */
struct logfile_entry
{
struct logfile_entry *next;
char *fname; /* name of file */
char *desc; /* alternative description */
char *buf; /* text read but not yet displayed */
const char *fontname;
XFontSet fontset;
int font_height;
int font_ascent;
FILE *fp; /* FILE struct associated with file */
ino_t inode; /* inode of the file opened */
off_t last_size; /* file size at the last check */
unsigned long color; /* color to be used for printing */
const char *colorname; /* color name/string */
int partial; /* true if the last line isn't complete */
int lastpartial; /* true if the previous output wasn't complete */
struct line_node *last; /* last line we output */
int modified; /* true if line is modified & needs displaying */
};
struct line_node
{
struct line_node *next;
struct line_node *prev;
struct logfile_entry *logfile;
char *line; /* the text of the line (so far) */
int len; /* the length of the line (in bytes) so far */
int wrapped_left; /* true if wrapped from the previous line */
int wrapped_right; /* true if wrapped to the next line */
struct breakinfo *breaks; /* array of indicies to spaces if wrapped_right */
int num_words; /* the number of words in the line */
int free_pixels; /* the number of free pixels to spread out */
};
struct breakinfo
{
int index; /* index into string of start of substring */
int width; /* width in pixels of start of substring */
int len; /* length of substring */
};
struct displaymatrix
{
char *line;
int len;
int offset;
int buffer_size;
unsigned long color;
};
/* global variables */
struct line_node *linelist = NULL, *linelist_tail = NULL;
struct displaymatrix *display;
int continuation_width = -1;
int continuation_color;
int continuation_length;
/* HACK - ideally listlen will start at however many '~'s will fit on
* the screen */
int width = STD_WIDTH, height = STD_HEIGHT, listlen = 50;
int win_x = LOC_X, win_y = LOC_Y;
int effect_x_space, effect_y_space; /* how much space does shading / outlining take up */
int effect_x_offset, effect_y_offset; /* and how does it offset the usable space */
int do_reopen;
struct timeval interval = { 2, 400000 };
/* command line options */
int opt_noinitial, opt_shade, opt_frame, opt_reverse, opt_nofilename,
opt_outline, opt_noflicker, opt_whole, opt_update, opt_wordwrap,
opt_justify, geom_mask, opt_minspace, reload;
const char *command = NULL,
*fontname = USE_FONT, *dispname = NULL, *def_color = DEF_COLOR,
*continuation = "|| ", *cont_color = DEF_CONT_COLOR;
struct logfile_entry *loglist = NULL, *loglist_tail = NULL;
Display *disp;
Window root;
GC WinGC;
#if HAS_REGEX
struct re_list
{
regex_t from;
const char *to;
struct re_list *next;
};
struct re_list *re_head, *re_tail;
char *transform_to = NULL;
regex_t *transformre;
#endif
/* prototypes */
void list_files (int);
void force_reopen (int);
void force_refresh (int);
void blank_window (int);
#ifdef USE_TOON_GET_ROOT_WINDOW
Window ToonGetRootWindow(Display *, int, Window *);
#endif /* USE_TOON_GET_ROOT_WINDOW */
void InitWindow (void);
unsigned long GetColor (const char *);
void redraw (int);
void refresh (int, int, int, int);
void transform_line (char *s);
int lineinput (struct logfile_entry *);
void reopen (void);
void check_open_files (void);
FILE *openlog (struct logfile_entry *);
static void main_loop (void);
void display_version (void);
void display_help (char *);
void install_signal (int, void (*)(int));
void *xstrdup (const char *);
void *xmalloc (size_t);
void *xrealloc (void *, size_t);
int daemonize (void);
/* signal handlers */
void
list_files (int dummy)
{
struct logfile_entry *e;
fprintf (stderr, "Files opened:\n");
for (e = loglist; e; e = e->next)
fprintf (stderr, "\t%s (%s)\n", e->fname, e->desc);
}
void
force_reopen (int dummy)
{
do_reopen = 1;
}
void
force_refresh (int dummy)
{
redraw (1);
}
void
blank_window (int dummy)
{
XClearArea (disp, root, win_x, win_y, width + MARGIN_OF_ERROR, height, False);
XFlush (disp);
exit (0);
}
/* X related functions */
unsigned long
GetColor (const char *ColorName)
{
XColor Color;
XWindowAttributes Attributes;
XGetWindowAttributes (disp, root, &Attributes);
Color.pixel = 0;
if (!XParseColor (disp, Attributes.colormap, ColorName, &Color))
fprintf (stderr, "can't parse %s\n", ColorName);
else if (!XAllocColor (disp, Attributes.colormap, &Color))
fprintf (stderr, "can't allocate %s\n", ColorName);
return Color.pixel;
}
#ifndef USE_TOON_GET_ROOT_WINDOW
static void
find_root_window (Display *display, int screen_number)
{
if (!root)
{
Window unused;
Window *windows;
unsigned int count;
Atom type;
int format;
unsigned long nitems, bytes_after_return;
unsigned char *virtual_root_window;
Atom SWM_VROOT = XInternAtom (display, "__SWM_VROOT", False);
Atom NAUTILUS_DESKTOP_WINDOW_ID = XInternAtom (display, "NAUTILUS_DESKTOP_WINDOW_ID", False);
root = RootWindow (display, screen_number);
if (XGetWindowProperty (display, root, NAUTILUS_DESKTOP_WINDOW_ID,
0, 1, False, XA_WINDOW, &type, &format,
&nitems, &bytes_after_return,
&virtual_root_window) == Success
&& type == XA_WINDOW)
{
if (XQueryTree (display, *(Window *)virtual_root_window, &unused, &unused, &windows, &count))
root = windows[count - 1];
XFree (virtual_root_window);
}
else if (XQueryTree (display, root, &unused, &unused, &windows, &count))
{
int i;
for (i = 0; i < count; i++)
{
if (XGetWindowProperty (display, windows[i], SWM_VROOT,
0, 1, False, XA_WINDOW, &type, &format,
&nitems, &bytes_after_return,
&virtual_root_window) == Success
&& type == XA_WINDOW)
{
root = *(Window *)virtual_root_window;
XFree (virtual_root_window);
break;
}
}
XFree (windows);
}
else
fprintf (stderr, "Can't query tree on root window 0x%lx", root);
}
}
#endif /* USE_TOON_GET_ROOT_WINDOW */
void
InitWindow (void)
{
XGCValues gcv;
unsigned long gcm;
int screen, ScreenWidth, ScreenHeight;
struct logfile_entry *e;
if (!(disp = XOpenDisplay (dispname)))
{
fprintf (stderr, "Can't open display %s.\n", dispname);
exit (1);
}
screen = DefaultScreen (disp);
ScreenHeight = DisplayHeight (disp, screen);
ScreenWidth = DisplayWidth (disp, screen);
find_root_window (disp, screen);
gcm = GCBackground;
gcv.graphics_exposures = True;
WinGC = XCreateGC (disp, root, gcm, &gcv);
XMapWindow (disp, root);
XSetForeground (disp, WinGC, GetColor (DEF_COLOR));
for (e = loglist; e; e = e->next)
{
char **missing_charset_list;
int missing_charset_count;
char *def_string;
e->fontset = XCreateFontSet (disp, e->fontname,
&missing_charset_list, &missing_charset_count,
&def_string);
if (missing_charset_count)
{
fprintf (stderr,
"Missing charsets in String to FontSet conversion (%s)\n",
missing_charset_list[0]);
XFreeStringList (missing_charset_list);
}
if (!e->fontset)
{
fprintf (stderr, "unable to create fontset for font '%s', exiting.\n", e->fontname);
exit (1);
}
{
XFontSetExtents *xfe = XExtentsOfFontSet (e->fontset);
e->font_height = xfe->max_logical_extent.height;
e->font_ascent = -xfe->max_logical_extent.y;
}
if (e->font_height > height - effect_y_space)
{
fprintf(stderr, "\n the display isn't tall enough to display a single line in font '%s'\n",
e->fontname);
fprintf(stderr, "\n the geometry in use is %d pixels tall\n", height);
fprintf(stderr, "\n font '%s' is %d pixels tall\n", e->fontname, e->font_height);
if (effect_y_space)
fprintf(stderr, "\n the shade or outline options need an extra %d pixel%s of vertical space\n",
effect_y_space, effect_y_space == 1 ? "" : "s");
fprintf(stderr, "\n");
exit(1);
}
}
if (geom_mask & XNegative)
win_x = win_x + ScreenWidth - width;
if (geom_mask & YNegative)
win_y = win_y + ScreenHeight - height;
{
struct logfile_entry *e;
for (e = loglist; e; e = e->next)
e->color = GetColor (e->colorname);
}
XSelectInput (disp, root, ExposureMask | FocusChangeMask);
}
/*
* if redraw () is passwd a non-zero argument, it does a complete
* redraw, rather than an update. if the argument is zero (and
* -noflicker is in effect) then only the lines which have changed
* since the last draw are redrawn.
*
* the rest is handled by regular refresh ()'es
*/
void
redraw (int redraw_all)
{
XSetClipMask (disp, WinGC, None);
refresh (0, 32768, 1, redraw_all);
}
void draw_text (Display *disp, Window root, GC WinGC, int x, int y, struct line_node *line, int foreground)
{
if (line->wrapped_right && opt_justify && line->breaks)
{
int i;
for (i = 0; i < line->num_words; i++)
XmbDrawString (disp, root, line->logfile->fontset, WinGC,
x + line->breaks[i].width + ((i * line->free_pixels) / (line->num_words - 1))
+ continuation_width * line->wrapped_left, y,
line->line + line->breaks[i].index,
line->breaks[i].len);
if (line->wrapped_left)
{
if (foreground) XSetForeground (disp, WinGC, continuation_color);
XmbDrawString (disp, root, line->logfile->fontset, WinGC, x, y, continuation, continuation_length);
}
}
else
{
XmbDrawString (disp, root, line->logfile->fontset, WinGC, x + continuation_width * line->wrapped_left,
y, line->line, line->len);
if (line->wrapped_left)
{
if (foreground) XSetForeground (disp, WinGC, continuation_color);
XmbDrawString (disp, root, line->logfile->fontset, WinGC, x, y, continuation, continuation_length);
}
}
}
/* Just redraw everything without clearing (i.e. after an EXPOSE event) */
void
refresh (int miny, int maxy, int clear, int refresh_all)
{
int lin = 0;
int space = height;
int offset;
unsigned long black_color = GetColor ("black");
struct line_node *line;
int step_per_line;
int foreground = 0;
if (opt_reverse)
offset = effect_y_offset;
else
offset = height + effect_y_offset;
miny -= win_y;
maxy -= win_y;
if (clear && !opt_noflicker)
XClearArea (disp, root, win_x, win_y, width + MARGIN_OF_ERROR, height, False);
for (line = linelist; line; line = line->next, lin++)
{
struct displaymatrix *display_line;
if (opt_noflicker && lin >= listlen)
{
int i = listlen;
listlen *= 1.5;
display = xrealloc(display, listlen * sizeof(struct displaymatrix));
for (; i < listlen; i++)
{
display[i].line = xstrdup ("");
display[i].len = 0;
display[i].offset = 0;
display[i].buffer_size = 0;
}
}
display_line = display + lin;
step_per_line = line->logfile->font_height + effect_y_space;
if (step_per_line > space)
break;
if (!opt_reverse)
offset -= step_per_line;
offset += line->logfile->font_ascent;
miny -= line->logfile->font_height;
maxy += line->logfile->font_height;
if (offset >= miny && offset <= maxy)
{
/* if this line is a different than it was, then it
* needs displaying */
if (!opt_noflicker
|| refresh_all
|| display_line->len != line->len
|| display_line->color != line->logfile->color
|| display_line->offset != offset
|| memcmp (display_line->line, line->line, line->len))
{
/* don't bother updating the record of what has been
* displayed if -noflicker isn't in effect, since we redraw
* the whole display every time anyway */
if (opt_noflicker)
{
/* update the record of what has been displayed;
* first make sure the buffer is big enough */
if (display_line->buffer_size < line->len)
{
display_line->buffer_size = line->len;
display_line->line = xrealloc (display_line->line, display_line->buffer_size);
}
display_line->len = line->len;
display_line->color = line->logfile->color;
display_line->offset = offset;
memcpy (display_line->line, line->line, line->len);
if (clear)
{
#ifdef DEBUG
static int toggle;
toggle = 1 - toggle;
XSetForeground (disp, WinGC, toggle ? GetColor ("cyan") : GetColor ("yellow"));
XFillRectangle (disp, root, WinGC, win_x, win_y + offset - line->logfile->font_ascent,
width, step_per_line);
#else /* DEBUG */
XClearArea (disp, root, win_x, win_y + offset - line->logfile->font_ascent,
width + MARGIN_OF_ERROR, step_per_line, False);
#endif /* DEBUG */
}
}
if (opt_outline)
{
int x, y;
XSetForeground (disp, WinGC, black_color);
for (x = -1; x <= 1; x += 2)
for (y = -1; y <= 1; y += 2)
draw_text (disp, root, WinGC,
win_x + effect_x_offset + x,
win_y + y + offset, line, foreground = 0);
}
else if (opt_shade)
{
XSetForeground (disp, WinGC, black_color);
draw_text (disp, root, WinGC,
win_x + effect_x_offset + SHADE_X,
win_y + offset + SHADE_Y, line, foreground = 0);
}
XSetForeground (disp, WinGC, line->logfile->color);
draw_text (disp, root, WinGC,
win_x + effect_x_offset,
win_y + offset, line, foreground = 1);
}
}
if (opt_reverse)
offset += step_per_line;
offset -= line->logfile->font_ascent;
miny += line->logfile->font_height;
maxy -= line->logfile->font_height;
space -= step_per_line;
}
if (space > 0 && clear)
{
#ifdef DEBUG
XSetForeground (disp, WinGC, GetColor ("orange"));
XFillRectangle (disp, root, WinGC, win_x, win_y + offset - (opt_reverse ? 0 : space),
width, space);
#else /* DEBUG */
XClearArea (disp, root, win_x, win_y + offset - (opt_reverse ? 0 : space),
width + MARGIN_OF_ERROR, space, False);
#endif
}
/* at least one of the lines must fit in the allocated area. we've
* already checked at initialisation time that all the fonts are small
* enough to fit at least one line in the display area, but assert it
* again here to be sure */
assert(line != linelist);
/* any lines that didn't just get looked at are never going to be, so break the chain */
if (line) line->prev->next = 0;
/* and throw them all away */
while (line)
{
struct line_node *this = line;
line = line->next;
if (this->logfile && this->logfile->last == this)
this->logfile->last = NULL;
free (this->line);
free (this->breaks);
free (this);
}
if (opt_frame)
{
XSetForeground (disp, WinGC, GetColor (def_color));
/* note that XDrawRectangle() draws a rectangle one pixel bigger
* in both dimensions than you ask for, hence the subtractions.
* XFillRectangle() doesn't suffer from this problem */
XDrawRectangle (disp, root, WinGC, win_x - 0, win_y - 0, width - 1, height - 1);
}
}
#if HAS_REGEX
void
transform_line (char *s)
{
#ifdef I_AM_Md
int i;
if (1)
{
for (i = 16; s[i]; i++)
s[i] = s[i + 11];
}
s[i + 1] = '\0';
#endif
if (transformre)
{
int i;
regmatch_t matched[16];
i = regexec (transformre, s, 16, matched, 0);
if (i == 0)
{ /* matched */
int match_start = matched[0].rm_so;
int match_end = matched[0].rm_eo;
int old_len = match_end - match_start;
int new_len = strlen (transform_to);
int old_whole_len = strlen (s);
printf ("regexp was matched by '%s' - replace with '%s'\n", s, transform_to);
printf ("match is from %d to %d\n", match_start, match_end);
if (new_len > old_len)
s = xrealloc (s, old_whole_len + new_len - old_len);
if (new_len != old_len)
{
memcpy (s + match_end + new_len - old_len,
s + match_end,
old_whole_len - match_end);
s[old_whole_len + new_len - old_len] = '\0';
}
memcpy (s + match_start,
transform_to,
new_len);
printf ("transformed to '%s'\n", s);
}
else
printf ("regexp was not matched by '%s'\n", s);
}
}
#endif
/*
* appends p2 to the end of p1, if p1 is not null
* otherwise allocates a new string and copies p2 to it
*/
char *
concat_line (char *p1, const char *p2)
{
int l1 = p1 ? strlen (p1) : 0;
int l2 = strlen (p2);
char *r;
assert (p2);
if (p1)
r = xrealloc(p1, l1 + l2 + 1);
else
r = xmalloc (l2 + 1);
memcpy (r + l1, p2, l2);
r[l1 + l2] = 0;
return r;
}
/*
* This routine can read a line of any length if it is called enough times.
*/
int
lineinput (struct logfile_entry *logfile)
{
char buff[1024], *p;
int ch;
/* HACK-2: add on the length of any partial line which we will be appending to */
int ofs = logfile->buf ? strlen (logfile->buf) : 0;
/* this loop ensures that the whole line is read, even if it's
* longer than the buffer. we need to do this because when --whole
* is in effect we don't know whether to display the line or not
* until we've seen how (ie. whether) it ends */
do
{
p = buff;
do
{
ch = fgetc (logfile->fp);
if (ch == '\n' || ch == EOF)
break;
else if (ch == '\r')
continue; /* skip */
else if (ch == '\t')
{
do
{
*p++ = ' ';
ofs++;
}
while (ofs & 7);
}
else
{
*p++ = ch;
ofs++;
}
}
while (p < buff + (sizeof buff) - 8 - 1);
if (p == buff && ch == EOF)
return 0;
*p = 0;
p = logfile->buf = concat_line (logfile->buf, buff);
}
while (ch != '\n' && ch != EOF);
logfile->lastpartial = logfile->partial;
/* there are 3 ways we could have exited the loop: reading '\n',
* reaching EOF, or filling the buffer; the 2nd and 3rd of these
* both result in a partial line */
logfile->partial = ch != '\n';
if (logfile->partial && opt_whole)
return 0;
#if HAS_REGEX
transform_line (logfile->buf);
#endif
return 1;
}
/* input: reads file->fname
* output: fills file->fp, file->inode
* returns file->fp
* in case of error, file->fp is NULL
*/
FILE *
openlog (struct logfile_entry * file)
{
struct stat stats;
if ((file->fp = fopen (file->fname, "r")) == NULL)
{
file->fp = NULL;
return NULL;
}
fstat (fileno (file->fp), &stats);
if (S_ISFIFO (stats.st_mode))
{
if (fcntl (fileno (file->fp), F_SETFL, O_NONBLOCK) < 0)
perror ("fcntl"), exit (1);
file->inode = 0;
}
else
file->inode = stats.st_ino;
if (opt_noinitial)
fseek (file->fp, 0, SEEK_END);
else /* if (stats.st_size > (listlen + 1) * width)
* HACK - 'width' is in pixels - how are we to know how much text will fit?
* fseek (file->fp, -((listlen + 2) * width/10), SEEK_END); */
fseek (file->fp, -5000, SEEK_END);
file->last_size = stats.st_size;
return file->fp;
}
void
reopen (void)
{
struct logfile_entry *e;
for (e = loglist; e; e = e->next)
{
if (!e->inode)
continue; /* skip stdin */
if (e->fp)
fclose (e->fp);
/* if fp is NULL we will try again later */
openlog (e);
}
do_reopen = 0;
}
void
check_open_files (void)
{
struct logfile_entry *e;
struct stat stats;
for (e = loglist; e; e = e->next)
{
if (!e->inode)
continue; /* skip stdin */
if (stat (e->fname, &stats) < 0)
{ /* file missing? */
sleep (1);
if (e->fp)
fclose (e->fp);
if (openlog (e) == NULL)
continue;
if (fstat (fileno (e->fp), &stats) < 0)
continue;
}
if (stats.st_ino != e->inode)
{ /* file renamed? */
if (e->fp)
fclose (e->fp);
if (openlog (e) == NULL)
continue;
if (fstat (fileno (e->fp), &stats) < 0)
continue;
}
if (stats.st_size < e->last_size)
{ /* file truncated? */
fseek (e->fp, 0, SEEK_SET);
e->last_size = stats.st_size;
}
}
}
/*
* insert a single node in the list of screen lines and return a
* pointer to the new node.
* the caller MUST then fill in ret->line and ret->len with valid
* data.
*/
static struct line_node *
new_line_node (struct logfile_entry *log)
{
struct line_node *new = xmalloc (sizeof (struct line_node));
new->logfile = log;
new->wrapped_left = 0;
new->wrapped_right = 0;
new->breaks = 0;
assert(log);
if (!log || !log->last)
{
new->next = linelist;
new->next->prev = new;
new->prev = NULL;
linelist = new;
}
else
{
/* 2 pointers from the new node */
new->next = log->last;
new->prev = log->last->prev;
/* 2 pointers back to the new node */
if (new->next) new->next->prev = new;
if (new->prev) new->prev->next = new;
/* if this is a new first entry in the list then update
* 'linelist' */
if (log->last == linelist)
linelist = new;
}
/* update the logfile record */
if (log)
log->last = new;
return new;
}
/*
* this is called after either adding a new line or appending to an
* old one. in both cases it's possible that the line no longer fits,
* and needs wrapping. this function checks the last line associated
* with the supplied logfile.
*/
static void
possibly_split_long_line (struct logfile_entry *log)
{
char *str = log->last->line;
int l = strlen (str);
char *p = str;
struct line_node *line;
int spaces;
static struct breakinfo *breaks;
static int break_buffer_size;
/* only calculate the continuation's width once */
if (continuation_width == -1)
{
continuation_length = strlen (continuation);
continuation_width = XmbTextEscapement (log->fontset, continuation, continuation_length);
continuation_color = GetColor (cont_color);
/* make an array to store information about the location of
* spaces in the line */
if (opt_justify)
{
break_buffer_size = 32;
breaks = xmalloc (break_buffer_size * sizeof (struct breakinfo));
}
}
do
{
const char *beg = p;
int start_w = log->last->wrapped_left ? continuation_width : 0;
int w = start_w;
int wrapped = 0;
char *break_p = NULL;
int width_at_break_p = 0;
int prefix_len;
spaces = 0;
if (opt_justify)
breaks[spaces].index = breaks[spaces].width = 0;
while (*p)
{
int cw, len;
/* find the length in bytes of the next multibyte character */
len = mblen (p, l);
if (len <= 0)
len = 1; /* ignore (don't skip) illegal character sequences */
/* find the width in pixels of the next character */
cw = XmbTextEscapement (log->fontset, p, len);
if (opt_wordwrap && len == 1 && p[0] == ' ' && p != break_p + 1)
{
break_p = p;
width_at_break_p = w;
spaces++;
if (opt_justify)
{
/* increase the size of the 'breaks' array when
* necessary */
if (spaces >= break_buffer_size)
{
break_buffer_size *= 1.5;
breaks = xrealloc (breaks, break_buffer_size * sizeof (struct breakinfo));
}
/* store information about (a) the location of each
* space */
breaks[spaces].index = p + 1 - beg;
/* (b) the width (in pixels) of the string up to
* this space */
breaks[spaces].width = cw + w - start_w;
/* (c) the length of each 'word' */
breaks[spaces-1].len = breaks[spaces].index - breaks[spaces-1].index;
}
}
if (cw + w > width - effect_x_space)
{
if (p == beg)
{
fprintf (stderr, "we can't even fit a single character onto the line\n");
if (len == 1) fprintf (stderr, "(the character we couldn't fit was '%c')\n", *p);
exit (1);
}
wrapped = 1;
break;
}
w += cw;
p += len;
l -= len;
}
/* if we're wrapping at spaces, and the line is long enough to
* wrap, and we've seen a space already, and the space wasn't
* the first character on the line, then wrap at the space */
if (!wrapped)
break;
/* choose where to break the line */
if (opt_wordwrap && break_p && break_p != beg)
{
prefix_len = break_p - beg;
p = break_p;
w = width_at_break_p;
/* if breaking at a space, skip all adjacent spaces */
while (*p == ' ')
{
int len = mblen (p, l);
if (len != 1) break;
p++;
}
if (opt_justify)
{
spaces--;
breaks[spaces].len--;
}
}
else
prefix_len = p - beg;
/* make a copy of the tail end of the string */
p = xstrdup (p);
/* and reduce the size of the head of the string */
log->last->line = xrealloc (log->last->line, prefix_len + 1);
log->last->len = prefix_len;
log->last->line[prefix_len] = '\0';
/* note that the head was wrapped on it's right */
log->last->wrapped_right = 1;
/* 'spaces' includes any space we broke on; we can only justify
* if there's at least one other space */
if (opt_justify && spaces &&
width - effect_x_space - width_at_break_p < spaces * log->font_height)
{
int i;
log->last->free_pixels = width - effect_x_space - w;
log->last->num_words = spaces + 1;
log->last->breaks = malloc (log->last->num_words * sizeof (struct breakinfo));
for (i = 0; i < log->last->num_words; i++)
log->last->breaks[i] = breaks[i];
}
line = new_line_node (log);
line->line = p;
l = line->len = strlen (p);
/* note that the tail end of the string is wrapped at its left */
line->wrapped_left = 1;
}
while (l);
}
static void
insert_new_line (char *str, struct logfile_entry *log)
{
struct line_node *new;
new = new_line_node (log);
new->line = str;
new->len = strlen (str);
possibly_split_long_line (log);
}
/*
* append something to an existing physical line. this is done
* by deleting the file on-screen, concatenating the new data to it
* and splitting it again.
*/
static void
append_to_existing_line (char *str, struct logfile_entry *log)
{
char *old, *new;
assert(log);
assert(log->last);
old = log->last->line;
assert(old);
new = concat_line (old, str);
free (str);
log->last->line = new;
log->last->len = strlen (new);
possibly_split_long_line (log);
}
static void
main_loop (void)
{
int lin;
time_t lastreload;
Region region = XCreateRegion ();
XEvent xev;
struct logfile_entry *lastprinted = NULL;
struct logfile_entry *current;
int need_update = 1;
display = xmalloc (sizeof (struct displaymatrix) * listlen);
lastreload = time (NULL);
/* Initialize line_node */
for (lin = 0; lin < listlen; lin++)
{
struct line_node *e = xmalloc (sizeof (struct line_node));
e->line = xstrdup ("~");
e->len = 1;
e->logfile = loglist; /* this is only needed to get a color for the '~' */
e->wrapped_left = 0;
e->wrapped_right = 0;
e->breaks = 0;
e->next = NULL;
e->prev = linelist_tail;
if (!linelist)
linelist = e;
if (linelist_tail)
linelist_tail->next = e;
linelist_tail = e;
display[lin].line = xstrdup ("");
display[lin].len = 0;
display[lin].offset = 0;
display[lin].buffer_size = 0;
}
for (;;)
{
/* read logs */
for (current = loglist; current; current = current->next)
{
if (!current->fp)
continue; /* skip missing files */
clearerr (current->fp);
while (lineinput (current))
{
need_update = 1;
/* if we're trying to update old partial lines in
* place, and the last time this file was updated the
* output was partial, and that partial line is not
* too close to the top of the screen, then update
* that partial line */
if (opt_update && current->lastpartial && current->last)
{
append_to_existing_line (current->buf, current);
current->buf = 0;
continue;
}
/* if all we just read was a newline ending a line that we've already displayed, skip it */
if (current->buf[0] == '\0' && current->lastpartial)
{
free(current->buf);
current->buf = 0;
continue;
}
/* print filename if any, and if last line was from
* different file */
if (lastprinted != current)
{
current->last = 0;
if (!opt_nofilename && current->desc[0])
{
insert_new_line (xstrdup ("["), current);
append_to_existing_line (xstrdup (current->desc), current);
append_to_existing_line (xstrdup ("]"), current);
}
}
/* if we're dealing with partial lines, and the last
* time we showed the line it wasn't finished ... */
if (!opt_whole && current->lastpartial)
{
/* if this is the same file we showed last then
append to the last line shown */
if (lastprinted == current)
append_to_existing_line (current->buf, current);
else
{
/* but if a different file has been shown in the
* mean time, make a new line, starting with the
* continuation string */
insert_new_line (current->buf, current);
current->last->wrapped_left = 1;
}
}
else
/* otherwise just make a plain and simple new line */
insert_new_line (current->buf, current);
current->buf = 0;
lastprinted = current;
}
}
if (need_update)
{
redraw (0);
need_update = 0;
}
else
{
XFlush (disp);
if (!XPending (disp))
{
fd_set fdr;
struct timeval to = interval;
FD_ZERO (&fdr);
FD_SET (ConnectionNumber (disp), &fdr);
select (ConnectionNumber (disp) + 1, &fdr, 0, 0, &to);
}
}
check_open_files ();
if (do_reopen)
reopen ();
/* we ignore possible errors due to window resizing &c */
while (XPending (disp))
{
XNextEvent (disp, &xev);
switch (xev.type)
{
case Expose:
{
XRectangle r;
r.x = xev.xexpose.x;
r.y = xev.xexpose.y;
r.width = xev.xexpose.width;
r.height = xev.xexpose.height;
XUnionRectWithRegion (&r, region, region);
}
break;
default:
#ifdef DEBUGMODE
fprintf (stderr, "PANIC! Unknown event %d\n", xev.type);
#endif
break;
}
}
/* reload if requested */
if (reload && lastreload + reload < time (NULL))
{
if (command && command[0])
system (command);
reopen ();
lastreload = time (NULL);
}
if (!XEmptyRegion (region))
{
XRectangle r;
XSetRegion (disp, WinGC, region);
XClipBox (region, &r);
refresh (r.y, r.y + r.height, 0, 1);
XDestroyRegion (region);
region = XCreateRegion ();
}
}
}
int
main (int argc, char *argv[])
{
int i;
int opt_daemonize = 0;
int opt_partial = 0, file_count = 0;
#if HAS_REGEX
char *transform = NULL;
#endif
setlocale (LC_CTYPE, ""); /* try to initialize the locale. */
for (i = 1; i < argc; i++)
{
const char *arg = argv[i];
if (arg[0] == '-' && arg[1] != '\0' && arg[1] != ',')
{
if (arg[1] == '-')
arg++;
if (!strcmp (arg, "-?") ||
!strcmp (arg, "-help") || !strcmp (arg, "-h"))
display_help (argv[0]);
else if (!strcmp (arg, "-V"))
display_version ();
else if (!strcmp (arg, "-g") || !strcmp (arg, "-geometry"))
geom_mask =
XParseGeometry (argv[++i], &win_x, &win_y, &width, &height);
else if (!strcmp (arg, "-display"))
dispname = argv[++i];
else if (!strcmp (arg, "-cont"))
continuation = argv[++i];
else if (!strcmp (arg, "-cont-color"))
cont_color = argv[++i];
else if (!strcmp (arg, "-font") || !strcmp (arg, "-fn"))
fontname = argv[++i];
#if HAS_REGEX
else if (!strcmp (arg, "-t"))
{
transform = argv[++i];
transform_to = argv[++i];
printf ("transform: '%s' to '%s'\n", transform, transform_to);
}
#endif
else if (!strcmp (arg, "-fork") || !strcmp (arg, "-f"))
opt_daemonize = 1;
else if (!strcmp (arg, "-reload"))
{
reload = atoi (argv[++i]);
command = argv[++i];
}
else if (!strcmp (arg, "-shade"))
opt_shade = 1;
else if (!strcmp (arg, "-outline"))
opt_outline = 1;
else if (!strcmp (arg, "-minspace"))
opt_minspace = 1;
else if (!strcmp (arg, "-noflicker"))
opt_noflicker = 1;
else if (!strcmp (arg, "-frame"))
opt_frame = 1;
else if (!strcmp (arg, "-no-filename"))
opt_nofilename = 1;
else if (!strcmp (arg, "-reverse"))
opt_reverse = 1;
else if (!strcmp (arg, "-whole"))
opt_whole = 1;
else if (!strcmp (arg, "-partial"))
opt_partial = 1;
else if (!strcmp (arg, "-update"))
opt_update = opt_partial = 1;
else if (!strcmp (arg, "-wordwrap"))
opt_wordwrap = 1;
else if (!strcmp (arg, "-justify"))
opt_justify = 1;
else if (!strcmp (arg, "-color"))
def_color = argv[++i];
else if (!strcmp (arg, "-noinitial"))
opt_noinitial = 1;
else if (!strcmp (arg, "-id"))
{
unsigned long id;
if (sscanf (argv[++i], "%li", &id) == 1 && id)
root = id;
}
else if (!strcmp (arg, "-interval") || !strcmp (arg, "-i"))
{
double iv = atof (argv[++i]);
interval.tv_sec = (int) iv;
interval.tv_usec = (iv - interval.tv_sec) * 1e6;
}
else
{
fprintf (stderr, "Unknown option '%s'.\n"
"Try --help for more information.\n", arg);
exit (1);
}
}
else
{ /* it must be a filename */
struct logfile_entry *e;
const char *fname, *desc, *fcolor = def_color;
char *p;
file_count++;
/* this is not foolproof yet (',' in filenames are not allowed) */
fname = desc = arg;
if ((p = strchr (arg, ',')))
{
*p = '\0';
fcolor = p + 1;
if ((p = strchr (fcolor, ',')))
{
*p = '\0';
desc = p + 1;
}
}
e = xmalloc (sizeof (struct logfile_entry));
e->partial = 0;
e->buf = 0;
if (arg[0] == '-' && arg[1] == '\0')
{
if ((e->fp = fdopen (0, "r")) == NULL)
perror ("fdopen"), exit (1);
if (fcntl (0, F_SETFL, O_NONBLOCK) < 0)
perror ("fcntl"), exit (1);
e->fname = NULL;
e->inode = 0;
if (desc == arg)
e->desc = xstrdup ("stdin");
else
e->desc = xstrdup (desc);
}
else
{
e->fname = xstrdup (fname);
if (openlog (e) == NULL)
perror (fname), exit (1);
e->desc = xstrdup (desc);
}
e->colorname = fcolor;
e->partial = 0;
e->fontname = fontname;
e->last = NULL;
e->next = NULL;
if (!loglist)
loglist = e;
if (loglist_tail)
loglist_tail->next = e;
loglist_tail = e;
}
}
if (!loglist)
{
fprintf (stderr, "You did not specify any files to tail\n"
"use %s --help for help\n", argv[0]);
exit (1);
}
if (opt_update && opt_whole)
{
fprintf (stderr, "Specify at most one of -update and -whole\n");
exit (1);
}
else if (opt_partial && opt_whole)
{
fprintf (stderr, "Specify at most one of -partial and -whole\n");
exit (1);
}
/* it doesn't make sense to justify if word wrap isn't on */
if (opt_justify)
opt_wordwrap = 1;
/* HACK-7: do we want to allow both -shade and -outline? */
if (opt_shade && opt_outline)
{
fprintf (stderr, "Specify at most one of -shade and -outline\n");
exit (1);
}
if (opt_partial)
/* if we specifically requested to see partial lines then don't insist on whole lines */
opt_whole = 0;
else if (file_count > 1)
/* otherwise, if we're viewing multiple files, default to showing whole lines */
opt_whole = 1;
#if HAS_REGEX
if (transform)
{
int i;
printf ("compiling regexp '%s'\n", transform);
transformre = xmalloc (sizeof (regex_t));
i = regcomp (transformre, transform, REG_EXTENDED);
if (i != 0)
{
char buf[512];
regerror (i, transformre, buf, sizeof (buf));
fprintf (stderr, "Cannot compile regular expression: %s\n", buf);
}
else
printf ("compiled '%s' OK to %x\n", transform, (int)transformre);
}
#endif
if (opt_outline && !opt_minspace)
{
/* adding outline increases the total width and height by 2
pixels each, and offsets the text one pixel right and one
pixel down */
effect_x_space = effect_y_space = 2;
effect_x_offset = effect_y_offset = 1;
}
else if (opt_shade && !opt_minspace)
{
/* adding a shadow increases the space used */
effect_x_space = abs (SHADE_X);
effect_y_space = abs (SHADE_Y);
/* if the shadow is to the right and below then we don't need
* to move the text to make space for it, but shadows to the left
* and above need accomodating */
effect_x_offset = SHADE_X > 0 ? 0 : -SHADE_X;
effect_y_offset = SHADE_Y > 0 ? 0 : -SHADE_Y;
}
else
{
effect_x_space = effect_y_space = 0;
effect_x_offset = effect_y_offset = 0;
}
InitWindow ();
install_signal (SIGINT, blank_window);
install_signal (SIGQUIT, blank_window);
install_signal (SIGTERM, blank_window);
install_signal (SIGHUP, force_reopen);
install_signal (SIGUSR1, list_files);
install_signal (SIGUSR2, force_refresh);
if (opt_daemonize)
daemonize ();
main_loop ();
exit (1); /* to make gcc -Wall stop complaining */
}
void
install_signal (int sig, void (*handler) (int))
{
struct sigaction action;
action.sa_handler = handler;
sigemptyset (&action.sa_mask);
action.sa_flags = SA_RESTART;
if (sigaction (sig, &action, NULL) < 0)
fprintf (stderr, "sigaction (%d): %s\n", sig, strerror (errno)), exit (1);
}
void *
xstrdup (const char *string)
{
void *p;
while ((p = strdup (string)) == NULL)
{
fprintf (stderr, "Memory exhausted in xstrdup ().\n");
sleep (10);
}
return p;
}
void *
xmalloc (size_t size)
{
void *p;
while ((p = malloc (size)) == NULL)
{
fprintf (stderr, "Memory exhausted in xmalloc ().\n");
sleep (10);
}
return p;
}
void *
xrealloc (void *ptr, size_t size)
{
void *p;
while ((p = realloc (ptr, size)) == NULL)
{
fprintf (stderr, "Memory exhausted in xrealloc ().\n");
sleep (10);
}
return p;
}
void
display_help (char *myname)
{
printf ("Usage: %s [options] file1[,color[,desc]]"
"[options] [file2[,color[,desc]] ...]\n", myname);
printf (" -g | -geometry geometry -g WIDTHxHEIGHT+X+Y\n"
" -color color use color $color as default\n"
" -reload sec command reload after $sec and run command\n"
" -id id window id to use instead of the root window\n"
" -font FONTSPEC (-fn) font to use\n"
" -f | -fork fork into background\n"
" -reverse print new lines at the top\n"
" -whole wait for \\n before showing a line\n"
" -partial show lines even if they don't end with a \\n\n"
" -update allow updates to old partial lines\n"
" -cont string to prefix continued partial lines with\n"
" defaults to \"|| \"\n"
" -wordwrap wrap long lines at spaces to avoid breaking words\n"
" -shade add shading to font\n"
" -outline add black outline to font\n"
" -minspace force minimum line spacing\n"
" -noinitial don't display the last file lines on\n"
" startup\n"
" -i | -interval seconds interval between checks (fractional\n"
" values o.k.). Default 2.4 seconds\n"
" -V display version information and exit\n"
"\n");
printf ("Example:\n%s -g 800x250+100+50 -font fixed /var/log/messages,green "
"/var/log/secure,red,'ALERT'\n", myname);
exit (0);
}
void
display_version (void)
{
printf ("root-tail version " VERSION "\n");
exit (0);
}
int
daemonize (void)
{
pid_t pid;
switch (pid = fork ())
{
case -1:
return -1;
case 0:
break;
default:
/*printf ("%d\n", pid);*/
exit (0);
}
if (setsid () == -1)
return -1;
return 0;
}
syntax highlighted by Code2HTML, v. 0.9.1