Commit | Line | Data |
---|---|---|
059ec3d9 PH |
1 | /************************************************* |
2 | * Exim Monitor * | |
3 | *************************************************/ | |
4 | ||
0a49a7a4 | 5 | /* Copyright (c) University of Cambridge 1995 - 2009 */ |
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 |
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. | |
f3f065bb PH |
251 | We omit the date and show only the time. Remove any time zone offset. |
252 | Take note of the presence of [pid]. */ | |
059ec3d9 PH |
253 | |
254 | if (pcre_exec(yyyymmdd_regex,NULL,CS buffer,length,0,PCRE_EOPT,NULL,0) >= 0) | |
255 | { | |
f3f065bb | 256 | int pidlength = 0; |
059ec3d9 PH |
257 | if ((buffer[20] == '+' || buffer[20] == '-') && |
258 | isdigit(buffer[21]) && buffer[25] == ' ') | |
259 | memmove(buffer + 20, buffer + 26, Ustrlen(buffer + 26) + 1); | |
f3f065bb PH |
260 | if (buffer[20] == '[') |
261 | { | |
262 | while (Ustrchr("[]0123456789", buffer[20+pidlength++]) != NULL); | |
263 | } | |
264 | id = string_copyn(buffer + 20 + pidlength, MESSAGE_ID_LENGTH); | |
059ec3d9 PH |
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 */ |