test for .git dir with -d not -e; more portable
[users/jgh/exim.git] / src / exim_monitor / em_log.c
1 /*************************************************
2 *                 Exim Monitor                   *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2009 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8 /* This module contains code for scanning the main log,
9 extracting information from it, and displaying a "tail". */
10
11 #include "em_hdr.h"
12
13 #define log_buffer_len 4096      /* For each log entry */
14
15 /* If anonymizing, don't alter these strings (this is all an ad hoc hack). */
16
17 #ifdef ANONYMIZE
18 static char *oklist[] = {
19   "Completed",
20   "defer",
21   "from",
22   "Connection timed out",
23   "Start queue run: pid=",
24   "End queue run: pid=",
25   "host lookup did not complete",
26   "unexpected disconnection while reading SMTP command from",
27   "verify failed for SMTP recipient",
28   "H=",
29   "U=",
30   "id=",
31   "<",
32   ">",
33   "(",
34   ")",
35   "[",
36   "]",
37   "@",
38   "=",
39   "*",
40   ".",
41   "-",
42   "\"",
43   " ",
44   "\n"};
45 static int oklist_size = sizeof(oklist) / sizeof(uschar *);
46 #endif
47
48
49
50 /*************************************************
51 *             Write to the log display           *
52 *************************************************/
53
54 static int visible = 0;
55 static int scrolled = FALSE;
56 static int size = 0;
57 static int top = 0;
58
59 static void show_log(char *s, ...)
60 {
61 int length, newtop;
62 va_list ap;
63 XawTextBlock b;
64 uschar buffer[log_buffer_len + 24];
65
66 /* Do nothing if not tailing a log */
67
68 if (log_widget == NULL) return;
69
70 /* Initialize the text block structure */
71
72 b.firstPos = 0;
73 b.ptr = CS buffer;
74 b.format = FMT8BIT;
75
76 /* We want to know whether the window has been scrolled back or not,
77 so that we can cease automatically scrolling with new text. This turns
78 out to be tricky with the text widget. We can detect whether the
79 scroll bar has been operated by checking on the "top" value, but it's
80 harder to detect that it has been returned to the bottom. The following
81 heuristic does its best. */
82
83 newtop = XawTextTopPosition(log_widget);
84 if (newtop != top)
85   {
86   if (!scrolled)
87     {
88     visible = size - top;      /* save size of window */
89     scrolled = newtop < top;
90     }
91   else if (newtop > size - visible) scrolled = FALSE;
92   top = newtop;
93   }
94
95 /* Format the text that is to be written. */
96
97 va_start(ap, s);
98 vsprintf(CS buffer, s, ap);
99 va_end(ap);
100 length = Ustrlen(buffer);
101
102 /* If we are anonymizing for screen shots, flatten various things. */
103
104 #ifdef ANONYMIZE
105   {
106   uschar *p = buffer + 9;
107   if (p[6] == '-' && p[13] == '-') p += 17;
108
109   while (p < buffer + length)
110     {
111     int i;
112
113     /* Check for strings to be left alone */
114
115     for (i = 0; i < oklist_size; i++)
116       {
117       int len = Ustrlen(oklist[i]);
118       if (Ustrncmp(p, oklist[i], len) == 0)
119         {
120         p += len;
121         break;
122         }
123       }
124     if (i < oklist_size) continue;
125
126     /* Leave driver names, size, protocol, alone */
127
128     if ((*p == 'D' || *p == 'P' || *p == 'T' || *p == 'S' || *p == 'R') &&
129         p[1] == '=')
130       {
131       p += 2;
132       while (*p != ' ' && *p != 0) p++;
133       continue;
134       }
135
136     /* Leave C= text alone */
137
138     if (Ustrncmp(p, "C=\"", 3) == 0)
139       {
140       p += 3;
141       while (*p != 0 && *p != '"') p++;
142       continue;
143       }
144
145     /* Flatten remaining chars */
146
147     if (isdigit(*p)) *p++ = 'x';
148     else if (isalpha(*p)) *p++ = 'x';
149     else *p++ = '$';
150     }
151   }
152 #endif
153
154 /* If this would overflow the buffer, throw away 50% of the
155 current stuff in the buffer. Code defensively against odd
156 extreme cases that shouldn't actually arise. */
157
158 if (size + length > log_buffer_size)
159   {
160   if (size == 0) length = log_buffer_size/2; else
161     {
162     int cutcount = log_buffer_size/2;
163     if (cutcount > size) cutcount = size; else
164       {
165       while (cutcount < size && log_display_buffer[cutcount] != '\n')
166         cutcount++;
167       cutcount++;
168       }
169     b.length = 0;
170     XawTextReplace(log_widget, 0, cutcount, &b);
171     size -= cutcount;
172     top -= cutcount;
173     if (top < 0) top = 0;
174     if (top < cutcount) XawTextInvalidate(log_widget, 0, 999999);
175     xs_SetValues(log_widget, 1, "displayPosition", top);
176     }
177   }
178
179 /* Insert the new text at the end of the buffer. */
180
181 b.length = length;
182 XawTextReplace(log_widget, 999999, 999999, &b);
183 size += length;
184
185 /* When not scrolled back, we want to keep the bottom line
186 always visible. Put the insert point at the start of it because
187 this stops left/right scrolling with some X libraries. */
188
189 if (!scrolled)
190   {
191   XawTextSetInsertionPoint(log_widget, size - length);
192   top = XawTextTopPosition(log_widget);
193   }
194 }
195
196
197
198
199 /*************************************************
200 *            Function to read the log            *
201 *************************************************/
202
203 /* We read any new log entries, and use their data to
204 updated total counts for the configured stripcharts.
205 The count for the queue chart is handled separately.
206 We also munge the log entries and display a one-line
207 version in the log window. */
208
209 void read_log(void)
210 {
211 struct stat statdata;
212 uschar buffer[log_buffer_len];
213
214 /* If log is not yet open, skip all of this. */
215
216 if (LOG != NULL)
217   {
218   fseek(LOG, log_position, SEEK_SET);
219
220   while (Ufgets(buffer, log_buffer_len, LOG) != NULL)
221     {
222     uschar *id;
223     uschar *p = buffer;
224     void *reset_point;
225     int length = Ustrlen(buffer);
226     int i;
227
228     /* Skip totally blank lines (paranoia: there shouldn't be any) */
229
230     while (*p == ' ' || *p == '\t') p++;
231     if (*p == '\n') continue;
232
233     /* We should now have a complete log entry in the buffer; check
234     it for various regular expression matches and take appropriate
235     action. Get the current store point so we can reset to it. */
236
237     reset_point = store_get(0);
238
239     /* First, update any stripchart data values, noting that the zeroth
240     stripchart is the queue length, which is handled elsewhere, and the
241     1st may the a size monitor. */
242
243     for (i = stripchart_varstart; i < stripchart_number; i++)
244       {
245       if (pcre_exec(stripchart_regex[i], NULL, CS buffer, length, 0, PCRE_EOPT,
246             NULL, 0) >= 0)
247         stripchart_total[i]++;
248       }
249
250     /* Munge the log entry and display shortened form on one line.
251     We omit the date and show only the time. Remove any time zone offset.
252     Take note of the presence of [pid]. */
253
254     if (pcre_exec(yyyymmdd_regex,NULL,CS buffer,length,0,PCRE_EOPT,NULL,0) >= 0)
255       {
256       int pidlength = 0;
257       if ((buffer[20] == '+' || buffer[20] == '-') &&
258           isdigit(buffer[21]) && buffer[25] == ' ')
259         memmove(buffer + 20, buffer + 26, Ustrlen(buffer + 26) + 1);
260       if (buffer[20] == '[')
261         {
262         while (Ustrchr("[]0123456789", buffer[20+pidlength++]) != NULL);
263         }
264       id = string_copyn(buffer + 20 + pidlength, MESSAGE_ID_LENGTH);
265       show_log("%s", buffer+11);
266       }
267     else
268       {
269       id = US"";
270       show_log("%s", buffer);
271       }
272
273     /* Deal with frozen and unfrozen messages */
274
275     if (strstric(buffer, US"frozen", FALSE) != NULL)
276       {
277       queue_item *qq = find_queue(id, queue_noop, 0);
278       if (qq != NULL)
279         {
280         if (strstric(buffer, US"unfrozen", FALSE) != NULL)
281           qq->frozen = FALSE;
282         else qq->frozen = TRUE;
283         }
284       }
285
286     /* Notice defer messages, and add the destination if it
287     isn't already on the list for this message, with a pointer
288     to the parent if we can. */
289
290     if ((p = Ustrstr(buffer, "==")) != NULL)
291       {
292       queue_item *qq = find_queue(id, queue_noop, 0);
293       if (qq != NULL)
294         {
295         dest_item *d;
296         uschar *q, *r;
297         p += 2;
298         while (isspace(*p)) p++;
299         q = p;
300         while (*p != 0 && !isspace(*p))
301           {
302           if (*p++ != '\"') continue;
303           while (*p != 0)
304             {
305             if (*p == '\\') p += 2;
306               else if (*p++ == '\"') break;
307             }
308           }
309         *p++ = 0;
310         if ((r = strstric(q, qualify_domain, FALSE)) != NULL &&
311           *(--r) == '@') *r = 0;
312
313         /* If we already have this destination, as tested case-insensitively,
314         do not add it to the destinations list. */
315
316         d = find_dest(qq, q, dest_add, TRUE);
317
318         if (d->parent == NULL)
319           {
320           while (isspace(*p)) p++;
321           if (*p == '<')
322             {
323             dest_item *dd;
324             q = ++p;
325             while (*p != 0 && *p != '>') p++;
326             *p = 0;
327             if ((p = strstric(q, qualify_domain, FALSE)) != NULL &&
328               *(--p) == '@') *p = 0;
329             dd = find_dest(qq, q, dest_noop, FALSE);
330             if (dd != NULL && dd != d) d->parent = dd;
331             }
332           }
333         }
334       }
335
336     store_reset(reset_point);
337     }
338   }
339
340
341 /* We have to detect when the log file is changed, and switch to the new file.
342 In practice, for non-datestamped files, this means that some deliveries might
343 go unrecorded, since they'll be written to the old file, but this usually
344 happens in the middle of the night, and I don't think the hassle of keeping
345 track of two log files is worth it.
346
347 First we check the datestamped name of the log file if necessary; if it is
348 different to the file we currently have open, go for the new file. As happens
349 in Exim itself, we leave in the following inode check, even when datestamping
350 because it does no harm and will cope should a file actually be renamed for
351 some reason.
352
353 The test for a changed log file is to look up the inode of the file by name and
354 compare it with the saved inode of the file we currently are processing. This
355 accords with the usual interpretation of POSIX and other Unix specs that imply
356 "one file, one inode". However, it appears that on some Digital systems, if an
357 open file is unlinked, a new file may be created with the same inode while the
358 old file remains in existence. This can happen if the old log file is renamed,
359 processed in some way, and then deleted. To work round this, also test for a
360 link count of zero on the currently open file. */
361
362 if (log_datestamping)
363   {
364   uschar log_file_wanted[256];
365   string_format(log_file_wanted, sizeof(log_file_wanted), CS log_file);
366   if (Ustrcmp(log_file_wanted, log_file_open) != 0)
367     {
368     if (LOG != NULL)
369       {
370       fclose(LOG);
371       LOG = NULL;
372       }
373     Ustrcpy(log_file_open, log_file_wanted);
374     }
375   }
376
377 if (LOG == NULL ||
378     (fstat(fileno(LOG), &statdata) == 0 && statdata.st_nlink == 0) ||
379     (Ustat(log_file, &statdata) == 0 && log_inode != statdata.st_ino))
380   {
381   FILE *TEST;
382
383   /* Experiment shows that sometimes you can't immediately open
384   the new log file - presumably immediately after the old one
385   is renamed and before the new one exists. Therefore do a
386   trial open first to be sure. */
387
388   if ((TEST = fopen(CS log_file_open, "r")) != NULL)
389     {
390     if (LOG != NULL) fclose(LOG);
391     LOG = TEST;
392     fstat(fileno(LOG), &statdata);
393     log_inode = statdata.st_ino;
394     }
395   }
396
397 /* Save the position we have got to in the log. */
398
399 if (LOG != NULL) log_position = ftell(LOG);
400 }
401
402 /* End of em_log.c */