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