1 /**
2 * @file
3 * NeoMutt Logging
4 *
5 * @authors
6 * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
7 *
8 * @copyright
9 * This program is free software: you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 2 of the License, or (at your option) any later
12 * version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 /**
24 * @page neo_mutt_logging NeoMutt Logging
25 *
26 * NeoMutt Logging
27 */
28
29 #include "config.h"
30 #include <errno.h>
31 #include <stdarg.h>
32 #include <stdbool.h>
33 #include <stdint.h>
34 #include <stdio.h>
35 #include <string.h>
36 #include <time.h>
37 #include "mutt/lib.h"
38 #include "config/lib.h"
39 #include "core/lib.h"
40 #include "gui/lib.h"
41 #include "mutt_logging.h"
42 #include "color/lib.h"
43 #include "mutt_globals.h"
44 #include "muttlib.h"
45 #include "options.h"
46
47 uint64_t LastError = 0; ///< Time of the last error message (in milliseconds since the Unix epoch)
48
49 char *CurrentFile = NULL; ///< The previous log file name
50 const int NumOfLogs = 5; ///< How many log files to rotate
51
52 #define S_TO_MS 1000L
53
54 /**
55 * error_pause - Wait for an error message to be read
56 *
57 * If '$sleep_time' seconds hasn't elapsed since LastError, then wait
58 */
error_pause(void)59 static void error_pause(void)
60 {
61 const short c_sleep_time = cs_subset_number(NeoMutt->sub, "sleep_time");
62 const uint64_t elapsed = mutt_date_epoch_ms() - LastError;
63 const uint64_t sleep = c_sleep_time * S_TO_MS;
64 if ((LastError == 0) || (elapsed >= sleep))
65 return;
66
67 mutt_refresh();
68 mutt_date_sleep_ms(sleep - elapsed);
69 }
70
71 /**
72 * rotate_logs - Rotate a set of numbered files
73 * @param file Template filename
74 * @param count Maximum number of files
75 * @retval ptr Name of the 0'th file
76 *
77 * Given a template 'temp', rename files numbered 0 to (count-1).
78 *
79 * Rename:
80 * - ...
81 * - temp1 -> temp2
82 * - temp0 -> temp1
83 */
rotate_logs(const char * file,int count)84 static const char *rotate_logs(const char *file, int count)
85 {
86 if (!file)
87 return NULL;
88
89 struct Buffer *old_file = mutt_buffer_pool_get();
90 struct Buffer *new_file = mutt_buffer_pool_get();
91
92 /* rotate the old debug logs */
93 for (count -= 2; count >= 0; count--)
94 {
95 mutt_buffer_printf(old_file, "%s%d", file, count);
96 mutt_buffer_printf(new_file, "%s%d", file, count + 1);
97
98 mutt_buffer_expand_path(old_file);
99 mutt_buffer_expand_path(new_file);
100 (void) rename(mutt_buffer_string(old_file), mutt_buffer_string(new_file));
101 }
102
103 file = mutt_buffer_strdup(old_file);
104 mutt_buffer_pool_release(&old_file);
105 mutt_buffer_pool_release(&new_file);
106
107 return file;
108 }
109
110 /**
111 * mutt_clear_error - Clear the message line (bottom line of screen)
112 */
mutt_clear_error(void)113 void mutt_clear_error(void)
114 {
115 /* Make sure the error message has had time to be read */
116 if (OptMsgErr)
117 error_pause();
118
119 ErrorBufMessage = false;
120 if (!OptNoCurses)
121 msgwin_clear_text();
122 }
123
124 /**
125 * log_disp_curses - Display a log line in the message line - Implements ::log_dispatcher_t - @ingroup logging_api
126 */
log_disp_curses(time_t stamp,const char * file,int line,const char * function,enum LogLevel level,...)127 int log_disp_curses(time_t stamp, const char *file, int line,
128 const char *function, enum LogLevel level, ...)
129 {
130 const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
131 if (level > c_debug_level)
132 return 0;
133
134 char buf[1024];
135
136 va_list ap;
137 va_start(ap, level);
138 const char *fmt = va_arg(ap, const char *);
139 int ret = vsnprintf(buf, sizeof(buf), fmt, ap);
140 va_end(ap);
141
142 if (level == LL_PERROR)
143 {
144 char *buf2 = buf + ret;
145 int len = sizeof(buf) - ret;
146 const char *p = strerror(errno);
147 if (!p)
148 p = _("unknown error");
149
150 ret += snprintf(buf2, len, ": %s (errno = %d)", p, errno);
151 }
152
153 const bool dupe = (strcmp(buf, ErrorBuf) == 0);
154 if (!dupe)
155 {
156 /* Only log unique messages */
157 log_disp_file(stamp, file, line, function, level, "%s", buf);
158 if (stamp == 0)
159 log_disp_queue(stamp, file, line, function, level, "%s", buf);
160 }
161
162 /* Don't display debugging message on screen */
163 if (level > LL_MESSAGE)
164 return 0;
165
166 /* Only pause if this is a message following an error */
167 if ((level > LL_ERROR) && OptMsgErr && !dupe)
168 error_pause();
169
170 size_t width = msgwin_get_width();
171 mutt_simple_format(ErrorBuf, sizeof(ErrorBuf), 0, width ? width : sizeof(ErrorBuf),
172 JUSTIFY_LEFT, 0, buf, sizeof(buf), false);
173 ErrorBufMessage = true;
174
175 if (!OptKeepQuiet)
176 {
177 enum ColorId color = MT_COLOR_NORMAL;
178 switch (level)
179 {
180 case LL_ERROR:
181 mutt_beep(false);
182 color = MT_COLOR_ERROR;
183 break;
184 case LL_WARNING:
185 color = MT_COLOR_WARNING;
186 break;
187 default:
188 color = MT_COLOR_MESSAGE;
189 break;
190 }
191
192 msgwin_set_text(color, ErrorBuf);
193 }
194
195 if ((level <= LL_ERROR) && !dupe)
196 {
197 OptMsgErr = true;
198 LastError = mutt_date_epoch_ms();
199 }
200 else
201 {
202 OptMsgErr = false;
203 LastError = 0;
204 }
205
206 return ret;
207 }
208
209 /**
210 * mutt_log_prep - Prepare to log
211 */
mutt_log_prep(void)212 void mutt_log_prep(void)
213 {
214 char ver[64];
215 snprintf(ver, sizeof(ver), "-%s%s", PACKAGE_VERSION, GitVer);
216 log_file_set_version(ver);
217 }
218
219 /**
220 * mutt_log_stop - Close the log file
221 */
mutt_log_stop(void)222 void mutt_log_stop(void)
223 {
224 log_file_close(false);
225 FREE(&CurrentFile);
226 }
227
228 /**
229 * mutt_log_set_file - Change the logging file
230 * @param file Name to use
231 * @retval 0 Success, file opened
232 * @retval -1 Error, see errno
233 *
234 * Close the old log, rotate the new logs and open the new log.
235 */
mutt_log_set_file(const char * file)236 int mutt_log_set_file(const char *file)
237 {
238 const char *const c_debug_file = cs_subset_path(NeoMutt->sub, "debug_file");
239 if (!mutt_str_equal(CurrentFile, c_debug_file))
240 {
241 const char *name = rotate_logs(c_debug_file, NumOfLogs);
242 if (!name)
243 return -1;
244
245 log_file_set_filename(name, false);
246 FREE(&name);
247 mutt_str_replace(&CurrentFile, c_debug_file);
248 }
249
250 cs_subset_str_string_set(NeoMutt->sub, "debug_file", file, NULL);
251
252 return 0;
253 }
254
255 /**
256 * mutt_log_set_level - Change the logging level
257 * @param level Logging level
258 * @param verbose If true, then log the event
259 * @retval 0 Success
260 * @retval -1 Error, level is out of range
261 */
mutt_log_set_level(enum LogLevel level,bool verbose)262 int mutt_log_set_level(enum LogLevel level, bool verbose)
263 {
264 if (!CurrentFile)
265 {
266 const char *const c_debug_file = cs_subset_path(NeoMutt->sub, "debug_file");
267 mutt_log_set_file(c_debug_file);
268 }
269
270 if (log_file_set_level(level, verbose) != 0)
271 return -1;
272
273 cs_subset_str_native_set(NeoMutt->sub, "debug_level", level, NULL);
274 return 0;
275 }
276
277 /**
278 * mutt_log_start - Enable file logging
279 * @retval 0 Success, or already running
280 * @retval -1 Failed to start
281 *
282 * This also handles file rotation.
283 */
mutt_log_start(void)284 int mutt_log_start(void)
285 {
286 const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
287 if (c_debug_level < 1)
288 return 0;
289
290 if (log_file_running())
291 return 0;
292
293 const char *const c_debug_file = cs_subset_path(NeoMutt->sub, "debug_file");
294 mutt_log_set_file(c_debug_file);
295
296 /* This will trigger the file creation */
297 if (log_file_set_level(c_debug_level, true) < 0)
298 return -1;
299
300 return 0;
301 }
302
303 /**
304 * level_validator - Validate the "debug_level" config variable - Implements ConfigDef::validator() - @ingroup cfg_def_validator
305 */
level_validator(const struct ConfigSet * cs,const struct ConfigDef * cdef,intptr_t value,struct Buffer * err)306 int level_validator(const struct ConfigSet *cs, const struct ConfigDef *cdef,
307 intptr_t value, struct Buffer *err)
308 {
309 if ((value < 0) || (value >= LL_MAX))
310 {
311 mutt_buffer_printf(err, _("Invalid value for option %s: %ld"), cdef->name, value);
312 return CSR_ERR_INVALID;
313 }
314
315 return CSR_SUCCESS;
316 }
317
318 /**
319 * main_log_observer - Notification that a Config Variable has changed - Implements ::observer_t - @ingroup observer_api
320 */
main_log_observer(struct NotifyCallback * nc)321 int main_log_observer(struct NotifyCallback *nc)
322 {
323 if ((nc->event_type != NT_CONFIG) || !nc->event_data)
324 return -1;
325
326 struct EventConfig *ev_c = nc->event_data;
327
328 if (mutt_str_equal(ev_c->name, "debug_file"))
329 {
330 const char *const c_debug_file = cs_subset_path(NeoMutt->sub, "debug_file");
331 mutt_log_set_file(c_debug_file);
332 }
333 else if (mutt_str_equal(ev_c->name, "debug_level"))
334 {
335 const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
336 mutt_log_set_level(c_debug_level, true);
337 }
338 else
339 {
340 return 0;
341 }
342
343 mutt_debug(LL_DEBUG5, "log done\n");
344 return 0;
345 }
346