1 /*************************************************
3 *************************************************/
5 /* Copyright (c) The Exim Maintainers 2021 - 2024 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-or-later */
10 /* This module contains code for scanning the main log,
11 extracting information from it, and displaying a "tail". */
15 #define log_buffer_len 4096 /* For each log entry */
17 /* If anonymizing, don't alter these strings (this is all an ad hoc hack). */
20 static char *oklist[] = {
24 "Connection timed out",
25 "Start queue run: pid=",
26 "End queue run: pid=",
27 "host lookup did not complete",
28 "unexpected disconnection while reading SMTP command from",
29 "verify failed for SMTP recipient",
47 static int oklist_size = sizeof(oklist) / sizeof(uschar *);
52 /*************************************************
53 * Write to the log display *
54 *************************************************/
56 static int visible = 0;
57 static int scrolled = FALSE;
61 static void show_log(char *s, ...) PRINTF_FUNCTION(1,2);
63 static void show_log(char *s, ...)
68 uschar buffer[log_buffer_len + 24];
70 /* Do nothing if not tailing a log */
72 if (log_widget == NULL) return;
74 /* Initialize the text block structure */
80 /* We want to know whether the window has been scrolled back or not,
81 so that we can cease automatically scrolling with new text. This turns
82 out to be tricky with the text widget. We can detect whether the
83 scroll bar has been operated by checking on the "top" value, but it's
84 harder to detect that it has been returned to the bottom. The following
85 heuristic does its best. */
87 newtop = XawTextTopPosition(log_widget);
92 visible = size - top; /* save size of window */
93 scrolled = newtop < top;
95 else if (newtop > size - visible) scrolled = FALSE;
99 /* Format the text that is to be written. */
102 vsprintf(CS buffer, s, ap);
104 length = Ustrlen(buffer);
106 /* If we are anonymizing for screen shots, flatten various things. */
110 uschar *p = buffer + 9;
111 if ( p[MESSAGE_ID_TIME_LEN] == '-'
112 && p[MESSAGE_ID_TIME_LEN + MESSAGE_ID_PID_LEN + 1] == '-')
113 p += MESSAGE_ID_LENGTH + 1;
115 while (p < buffer + length)
119 /* Check for strings to be left alone */
121 for (i = 0; i < oklist_size; i++)
123 int len = Ustrlen(oklist[i]);
124 if (Ustrncmp(p, oklist[i], len) == 0)
130 if (i < oklist_size) continue;
132 /* Leave driver names, size, protocol, alone */
134 if ((*p == 'D' || *p == 'P' || *p == 'T' || *p == 'S' || *p == 'R') &&
138 while (*p != ' ' && *p != 0) p++;
142 /* Leave C= text alone */
144 if (Ustrncmp(p, "C=\"", 3) == 0)
147 while (*p != 0 && *p != '"') p++;
151 /* Flatten remaining chars */
153 if (isdigit(*p)) *p++ = 'x';
154 else if (isalpha(*p)) *p++ = 'x';
160 /* If this would overflow the buffer, throw away 50% of the
161 current stuff in the buffer. Code defensively against odd
162 extreme cases that shouldn't actually arise. */
164 if (size + length > log_buffer_size)
166 if (size == 0) length = log_buffer_size/2; else
168 int cutcount = log_buffer_size/2;
169 if (cutcount > size) cutcount = size; else
171 while (cutcount < size && log_display_buffer[cutcount] != '\n')
176 XawTextReplace(log_widget, 0, cutcount, &b);
179 if (top < 0) top = 0;
180 if (top < cutcount) XawTextInvalidate(log_widget, 0, 999999);
181 xs_SetValues(log_widget, 1, "displayPosition", top);
185 /* Insert the new text at the end of the buffer. */
188 XawTextReplace(log_widget, 999999, 999999, &b);
191 /* When not scrolled back, we want to keep the bottom line
192 always visible. Put the insert point at the start of it because
193 this stops left/right scrolling with some X libraries. */
197 XawTextSetInsertionPoint(log_widget, size - length);
198 top = XawTextTopPosition(log_widget);
205 /*************************************************
206 * Function to read the log *
207 *************************************************/
209 /* We read any new log entries, and use their data to
210 updated total counts for the configured stripcharts.
211 The count for the queue chart is handled separately.
212 We also munge the log entries and display a one-line
213 version in the log window. */
217 struct stat statdata;
218 uschar buffer[log_buffer_len];
220 /* If log is not yet open, skip all of this. */
224 if (fseek(LOG, log_position, SEEK_SET))
226 perror("logfile fseek");
230 while (Ufgets(buffer, log_buffer_len, LOG) != NULL)
235 int length = Ustrlen(buffer);
236 pcre2_match_data * md = pcre2_match_data_create(1, NULL);
238 /* Skip totally blank lines (paranoia: there shouldn't be any) */
240 while (*p == ' ' || *p == '\t') p++;
241 if (*p == '\n') continue;
243 /* We should now have a complete log entry in the buffer; check
244 it for various regular expression matches and take appropriate
245 action. Get the current store point so we can reset to it. */
247 reset_point = store_mark();
249 /* First, update any stripchart data values, noting that the zeroth
250 stripchart is the queue length, which is handled elsewhere, and the
251 1st may the a size monitor. */
253 for (int i = stripchart_varstart; i < stripchart_number; i++)
254 if (pcre2_match(stripchart_regex[i], (PCRE2_SPTR)buffer, length,
255 0, PCRE_EOPT, md, NULL) >= 0)
256 stripchart_total[i]++;
258 /* Munge the log entry and display shortened form on one line.
259 We omit the date and show only the time. Remove any time zone offset.
260 Take note of the presence of [pid]. */
262 if (pcre2_match(yyyymmdd_regex, (PCRE2_SPTR) buffer, length, 0, PCRE_EOPT,
266 if ( (buffer[20] == '+' || buffer[20] == '-')
267 && isdigit(buffer[21]) && buffer[25] == ' ')
268 memmove(buffer + 20, buffer + 26, Ustrlen(buffer + 26) + 1);
269 if (buffer[20] == '[')
270 while (Ustrchr("[]0123456789", buffer[20+pidlength++]) != NULL)
272 id = string_copyn(buffer + 20 + pidlength, MESSAGE_ID_LENGTH);
273 show_log("%s", buffer+11);
278 show_log("%s", buffer);
280 pcre2_match_data_free(md);
282 /* Deal with frozen and unfrozen messages */
284 if (strstric(buffer, US"frozen", FALSE) != NULL)
286 queue_item *qq = find_queue(id, queue_noop, 0);
288 qq->frozen = strstric(buffer, US"unfrozen", FALSE) == NULL;
291 /* Notice defer messages, and add the destination if it
292 isn't already on the list for this message, with a pointer
293 to the parent if we can. */
295 if ((p = Ustrstr(buffer, "==")) != NULL)
297 queue_item * qq = find_queue(id, queue_noop, 0);
303 while (isspace(*p)) p++;
305 while (*p && !isspace(*p))
307 if (*p++ != '\"') continue;
309 if (*p == '\\') p += 2;
310 else if (*p++ == '\"') break;
313 if ((r = strstric(q, qualify_domain, FALSE)) != NULL &&
314 *(--r) == '@') *r = 0;
316 /* If we already have this destination, as tested case-insensitively,
317 do not add it to the destinations list. */
319 d = find_dest(qq, q, dest_add, TRUE);
321 if (d->parent == NULL)
323 while (isspace(*p)) p++;
328 while (*p != 0 && *p != '>') p++;
330 if ((p = strstric(q, qualify_domain, FALSE)) != NULL &&
331 *(--p) == '@') *p = 0;
332 dd = find_dest(qq, q, dest_noop, FALSE);
333 if (dd != NULL && dd != d) d->parent = dd;
339 store_reset(reset_point);
344 /* We have to detect when the log file is changed, and switch to the new file.
345 In practice, for non-datestamped files, this means that some deliveries might
346 go unrecorded, since they'll be written to the old file, but this usually
347 happens in the middle of the night, and I don't think the hassle of keeping
348 track of two log files is worth it.
350 First we check the datestamped name of the log file if necessary; if it is
351 different to the file we currently have open, go for the new file. As happens
352 in Exim itself, we leave in the following inode check, even when datestamping
353 because it does no harm and will cope should a file actually be renamed for
356 The test for a changed log file is to look up the inode of the file by name and
357 compare it with the saved inode of the file we currently are processing. This
358 accords with the usual interpretation of POSIX and other Unix specs that imply
359 "one file, one inode". However, it appears that on some Digital systems, if an
360 open file is unlinked, a new file may be created with the same inode while the
361 old file remains in existence. This can happen if the old log file is renamed,
362 processed in some way, and then deleted. To work round this, also test for a
363 link count of zero on the currently open file. */
365 if (log_datestamping)
367 uschar log_file_wanted[256];
368 /* Do *not* use "%s" here, we need the %D datestamp in the log_file string to
369 be expanded. The trailing NULL arg is to quieten preprocessors that need at
370 least one arg for a variadic set in a macro. */
371 string_format(log_file_wanted, sizeof(log_file_wanted), CS log_file, NULL);
372 if (Ustrcmp(log_file_wanted, log_file_open) != 0)
379 Ustrcpy(log_file_open, log_file_wanted);
384 (fstat(fileno(LOG), &statdata) == 0 && statdata.st_nlink == 0) ||
385 (Ustat(log_file, &statdata) == 0 && log_inode != statdata.st_ino))
389 /* Experiment shows that sometimes you can't immediately open
390 the new log file - presumably immediately after the old one
391 is renamed and before the new one exists. Therefore do a
392 trial open first to be sure. */
394 if ((TEST = fopen(CS log_file_open, "r")) != NULL)
396 if (LOG != NULL) fclose(LOG);
398 if (fstat(fileno(LOG), &statdata))
400 fprintf(stderr, "fstat %s: %s\n", log_file_open, strerror(errno));
403 log_inode = statdata.st_ino;
407 /* Save the position we have got to in the log. */
409 if (LOG != NULL) log_position = ftell(LOG);
412 /* End of em_log.c */