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