1 /*************************************************
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainters 2021 */
7 /* See the file NOTICE for conditions of use and distribution. */
9 /* This module contains code for scanning the main log,
10 extracting information from it, and displaying a "tail". */
14 #define log_buffer_len 4096 /* For each log entry */
16 /* If anonymizing, don't alter these strings (this is all an ad hoc hack). */
19 static char *oklist[] = {
23 "Connection timed out",
24 "Start queue run: pid=",
25 "End queue run: pid=",
26 "host lookup did not complete",
27 "unexpected disconnection while reading SMTP command from",
28 "verify failed for SMTP recipient",
46 static int oklist_size = sizeof(oklist) / sizeof(uschar *);
51 /*************************************************
52 * Write to the log display *
53 *************************************************/
55 static int visible = 0;
56 static int scrolled = FALSE;
60 static void show_log(char *s, ...) PRINTF_FUNCTION(1,2);
62 static void show_log(char *s, ...)
67 uschar buffer[log_buffer_len + 24];
69 /* Do nothing if not tailing a log */
71 if (log_widget == NULL) return;
73 /* Initialize the text block structure */
79 /* We want to know whether the window has been scrolled back or not,
80 so that we can cease automatically scrolling with new text. This turns
81 out to be tricky with the text widget. We can detect whether the
82 scroll bar has been operated by checking on the "top" value, but it's
83 harder to detect that it has been returned to the bottom. The following
84 heuristic does its best. */
86 newtop = XawTextTopPosition(log_widget);
91 visible = size - top; /* save size of window */
92 scrolled = newtop < top;
94 else if (newtop > size - visible) scrolled = FALSE;
98 /* Format the text that is to be written. */
101 vsprintf(CS buffer, s, ap);
103 length = Ustrlen(buffer);
105 /* If we are anonymizing for screen shots, flatten various things. */
109 uschar *p = buffer + 9;
110 if (p[6] == '-' && p[13] == '-') p += 17;
112 while (p < buffer + length)
116 /* Check for strings to be left alone */
118 for (i = 0; i < oklist_size; i++)
120 int len = Ustrlen(oklist[i]);
121 if (Ustrncmp(p, oklist[i], len) == 0)
127 if (i < oklist_size) continue;
129 /* Leave driver names, size, protocol, alone */
131 if ((*p == 'D' || *p == 'P' || *p == 'T' || *p == 'S' || *p == 'R') &&
135 while (*p != ' ' && *p != 0) p++;
139 /* Leave C= text alone */
141 if (Ustrncmp(p, "C=\"", 3) == 0)
144 while (*p != 0 && *p != '"') p++;
148 /* Flatten remaining chars */
150 if (isdigit(*p)) *p++ = 'x';
151 else if (isalpha(*p)) *p++ = 'x';
157 /* If this would overflow the buffer, throw away 50% of the
158 current stuff in the buffer. Code defensively against odd
159 extreme cases that shouldn't actually arise. */
161 if (size + length > log_buffer_size)
163 if (size == 0) length = log_buffer_size/2; else
165 int cutcount = log_buffer_size/2;
166 if (cutcount > size) cutcount = size; else
168 while (cutcount < size && log_display_buffer[cutcount] != '\n')
173 XawTextReplace(log_widget, 0, cutcount, &b);
176 if (top < 0) top = 0;
177 if (top < cutcount) XawTextInvalidate(log_widget, 0, 999999);
178 xs_SetValues(log_widget, 1, "displayPosition", top);
182 /* Insert the new text at the end of the buffer. */
185 XawTextReplace(log_widget, 999999, 999999, &b);
188 /* When not scrolled back, we want to keep the bottom line
189 always visible. Put the insert point at the start of it because
190 this stops left/right scrolling with some X libraries. */
194 XawTextSetInsertionPoint(log_widget, size - length);
195 top = XawTextTopPosition(log_widget);
202 /*************************************************
203 * Function to read the log *
204 *************************************************/
206 /* We read any new log entries, and use their data to
207 updated total counts for the configured stripcharts.
208 The count for the queue chart is handled separately.
209 We also munge the log entries and display a one-line
210 version in the log window. */
214 struct stat statdata;
215 uschar buffer[log_buffer_len];
217 /* If log is not yet open, skip all of this. */
221 if (fseek(LOG, log_position, SEEK_SET))
223 perror("logfile fseek");
227 while (Ufgets(buffer, log_buffer_len, LOG) != NULL)
232 int length = Ustrlen(buffer);
233 pcre2_match_data * md = pcre2_match_data_create(1, NULL);
235 /* Skip totally blank lines (paranoia: there shouldn't be any) */
237 while (*p == ' ' || *p == '\t') p++;
238 if (*p == '\n') continue;
240 /* We should now have a complete log entry in the buffer; check
241 it for various regular expression matches and take appropriate
242 action. Get the current store point so we can reset to it. */
244 reset_point = store_mark();
246 /* First, update any stripchart data values, noting that the zeroth
247 stripchart is the queue length, which is handled elsewhere, and the
248 1st may the a size monitor. */
250 for (int i = stripchart_varstart; i < stripchart_number; i++)
251 if (pcre2_match(stripchart_regex[i], (PCRE2_SPTR)buffer, length,
252 0, PCRE_EOPT, md, NULL) >= 0)
253 stripchart_total[i]++;
255 /* Munge the log entry and display shortened form on one line.
256 We omit the date and show only the time. Remove any time zone offset.
257 Take note of the presence of [pid]. */
259 if (pcre2_match(yyyymmdd_regex, (PCRE2_SPTR) buffer, length, 0, PCRE_EOPT,
263 if ( (buffer[20] == '+' || buffer[20] == '-')
264 && isdigit(buffer[21]) && buffer[25] == ' ')
265 memmove(buffer + 20, buffer + 26, Ustrlen(buffer + 26) + 1);
266 if (buffer[20] == '[')
267 while (Ustrchr("[]0123456789", buffer[20+pidlength++]) != NULL)
269 id = string_copyn(buffer + 20 + pidlength, MESSAGE_ID_LENGTH);
270 show_log("%s", buffer+11);
275 show_log("%s", buffer);
277 pcre2_match_data_free(md);
279 /* Deal with frozen and unfrozen messages */
281 if (strstric(buffer, US"frozen", FALSE) != NULL)
283 queue_item *qq = find_queue(id, queue_noop, 0);
285 qq->frozen = strstric(buffer, US"unfrozen", FALSE) == NULL;
288 /* Notice defer messages, and add the destination if it
289 isn't already on the list for this message, with a pointer
290 to the parent if we can. */
292 if ((p = Ustrstr(buffer, "==")) != NULL)
294 queue_item *qq = find_queue(id, queue_noop, 0);
300 while (isspace(*p)) p++;
302 while (*p != 0 && !isspace(*p))
304 if (*p++ != '\"') continue;
307 if (*p == '\\') p += 2;
308 else if (*p++ == '\"') break;
312 if ((r = strstric(q, qualify_domain, FALSE)) != NULL &&
313 *(--r) == '@') *r = 0;
315 /* If we already have this destination, as tested case-insensitively,
316 do not add it to the destinations list. */
318 d = find_dest(qq, q, dest_add, TRUE);
320 if (d->parent == NULL)
322 while (isspace(*p)) p++;
327 while (*p != 0 && *p != '>') p++;
329 if ((p = strstric(q, qualify_domain, FALSE)) != NULL &&
330 *(--p) == '@') *p = 0;
331 dd = find_dest(qq, q, dest_noop, FALSE);
332 if (dd != NULL && dd != d) d->parent = dd;
338 store_reset(reset_point);
343 /* We have to detect when the log file is changed, and switch to the new file.
344 In practice, for non-datestamped files, this means that some deliveries might
345 go unrecorded, since they'll be written to the old file, but this usually
346 happens in the middle of the night, and I don't think the hassle of keeping
347 track of two log files is worth it.
349 First we check the datestamped name of the log file if necessary; if it is
350 different to the file we currently have open, go for the new file. As happens
351 in Exim itself, we leave in the following inode check, even when datestamping
352 because it does no harm and will cope should a file actually be renamed for
355 The test for a changed log file is to look up the inode of the file by name and
356 compare it with the saved inode of the file we currently are processing. This
357 accords with the usual interpretation of POSIX and other Unix specs that imply
358 "one file, one inode". However, it appears that on some Digital systems, if an
359 open file is unlinked, a new file may be created with the same inode while the
360 old file remains in existence. This can happen if the old log file is renamed,
361 processed in some way, and then deleted. To work round this, also test for a
362 link count of zero on the currently open file. */
364 if (log_datestamping)
366 uschar log_file_wanted[256];
367 /* Do *not* use "%s" here, we need the %D datestamp in the log_file string to
368 be expanded. The trailing NULL arg is to quieten preprocessors that need at
369 least one arg for a variadic set in a macro. */
370 string_format(log_file_wanted, sizeof(log_file_wanted), CS log_file, NULL);
371 if (Ustrcmp(log_file_wanted, log_file_open) != 0)
378 Ustrcpy(log_file_open, log_file_wanted);
383 (fstat(fileno(LOG), &statdata) == 0 && statdata.st_nlink == 0) ||
384 (Ustat(log_file, &statdata) == 0 && log_inode != statdata.st_ino))
388 /* Experiment shows that sometimes you can't immediately open
389 the new log file - presumably immediately after the old one
390 is renamed and before the new one exists. Therefore do a
391 trial open first to be sure. */
393 if ((TEST = fopen(CS log_file_open, "r")) != NULL)
395 if (LOG != NULL) fclose(LOG);
397 if (fstat(fileno(LOG), &statdata))
399 fprintf(stderr, "fstat %s: %s\n", log_file_open, strerror(errno));
402 log_inode = statdata.st_ino;
406 /* Save the position we have got to in the log. */
408 if (LOG != NULL) log_position = ftell(LOG);
411 /* End of em_log.c */