/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* e-debug-log.c: Ring buffer for logging debug messages * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * * Authors: Federico Mena-Quintero */ #include "evolution-data-server-config.h" #include #include #include #include #include #include "e-debug-log.h" #define DEFAULT_RING_BUFFER_NUM_LINES 1000 #define KEY_FILE_GROUP "debug log" #define KEY_FILE_DOMAINS_KEY "enable domains" #define KEY_FILE_MAX_LINES_KEY "max lines" static GMutex log_mutex; static GHashTable *domains_hash; static gchar **ring_buffer; static gint ring_buffer_next_index; static gint ring_buffer_num_lines; static gint ring_buffer_max_lines = DEFAULT_RING_BUFFER_NUM_LINES; static GSList *milestones_head; static GSList *milestones_tail; static void lock (void) { g_mutex_lock (&log_mutex); } static void unlock (void) { g_mutex_unlock (&log_mutex); } /** * e_debug_log: * @is_milestone: the debug information is a milestone * @domain: for which domain the debug information belongs * @format: print format * @...: arguments for the format * * Records debug information for the given @domain, if enabled, or always, * when @is_milestone is set to TRUE. * * Since: 2.32 **/ void e_debug_log (gboolean is_milestone, const gchar *domain, const gchar *format, ...) { va_list args; va_start (args, format); e_debug_logv (is_milestone, domain, format, args); va_end (args); } static gboolean is_domain_enabled (const gchar *domain) { /* User actions are always logged */ if (strcmp (domain, E_DEBUG_LOG_DOMAIN_USER) == 0) return TRUE; if (!domains_hash) return FALSE; return (g_hash_table_lookup (domains_hash, domain) != NULL); } static void ensure_ring (void) { if (ring_buffer) return; ring_buffer = g_new0 (gchar *, ring_buffer_max_lines); ring_buffer_next_index = 0; ring_buffer_num_lines = 0; } static void add_to_ring (gchar *str) { ensure_ring (); g_return_if_fail (str != NULL); if (ring_buffer_num_lines == ring_buffer_max_lines) { /* We have an overlap, and the ring_buffer_next_index pogints to * the "first" item. Free it to make room for the new item. */ g_return_if_fail (ring_buffer[ring_buffer_next_index] != NULL); g_free (ring_buffer[ring_buffer_next_index]); } else ring_buffer_num_lines++; g_return_if_fail (ring_buffer_num_lines <= ring_buffer_max_lines); ring_buffer[ring_buffer_next_index] = str; ring_buffer_next_index++; if (ring_buffer_next_index == ring_buffer_max_lines) { ring_buffer_next_index = 0; g_return_if_fail (ring_buffer_num_lines == ring_buffer_max_lines); } } static void add_to_milestones (const gchar *str) { gchar *str_copy; str_copy = g_strdup (str); if (milestones_tail) { milestones_tail = g_slist_append (milestones_tail, str_copy); milestones_tail = milestones_tail->next; } else { milestones_head = milestones_tail = g_slist_append (NULL, str_copy); } g_return_if_fail (milestones_head != NULL && milestones_tail != NULL); } /** * e_debug_logv: * @is_milestone: the debug information is a milestone * @domain: for which domain the debug information belongs * @format: print format * @args: arguments for the format * * Records debug information for the given @domain, if enabled, or always, * when @is_milestone is set to TRUE. * * Since: 2.32 **/ void e_debug_logv (gboolean is_milestone, const gchar *domain, const gchar *format, va_list args) { gchar *str; gchar *debug_str; struct timeval tv; struct tm tm; lock (); if (!(is_milestone || is_domain_enabled (domain))) goto out; str = g_strdup_vprintf (format, args); gettimeofday (&tv, NULL); tm = *localtime (&tv.tv_sec); debug_str = g_strdup_printf ( "%p;%04d/%02d/%02d;%02d:%02d:%02d.%04d;(%s);%s", g_thread_self (), tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, (gint) (tv.tv_usec / 100), domain, str); g_free (str); add_to_ring (debug_str); if (is_milestone) add_to_milestones (debug_str); out: unlock (); } /** * e_debug_log_load_configuration: * @filename: a configuration file name * @error: return location for a #GError, or %NULL * * Loads configuration for the logging from the given @filename. * * Returns: whether succeeded * * Since: 2.32 **/ gboolean e_debug_log_load_configuration (const gchar *filename, GError **error) { GKeyFile *key_file; gchar **strings; gsize num_strings; gint num; GError *my_error; g_return_val_if_fail (filename != NULL, FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); key_file = g_key_file_new (); if (!g_key_file_load_from_file ( key_file, filename, G_KEY_FILE_NONE, error)) { g_key_file_free (key_file); return FALSE; } /* Domains */ my_error = NULL; strings = g_key_file_get_string_list ( key_file, KEY_FILE_GROUP, KEY_FILE_DOMAINS_KEY, &num_strings, &my_error); if (my_error) g_error_free (my_error); else { gint i; for (i = 0; i < num_strings; i++) strings[i] = g_strstrip (strings[i]); e_debug_log_enable_domains ( (const gchar **) strings, num_strings); g_strfreev (strings); } /* Number of lines */ my_error = NULL; num = g_key_file_get_integer ( key_file, KEY_FILE_GROUP, KEY_FILE_MAX_LINES_KEY, &my_error); if (my_error) g_error_free (my_error); else e_debug_log_set_max_lines (num); g_key_file_free (key_file); return TRUE; } /** * e_debug_log_enable_domains: * @domains: (array length=n_domains): an array of domains to enable * @n_domains: legth of the @domains array * * Enables all domains from the @domains array. * * Since: 2.32 **/ void e_debug_log_enable_domains (const gchar **domains, gint n_domains) { gint i; g_return_if_fail (domains != NULL); g_return_if_fail (n_domains >= 0); lock (); if (!domains_hash) domains_hash = g_hash_table_new (g_str_hash, g_str_equal); for (i = 0; i < n_domains; i++) { g_return_if_fail (domains[i] != NULL); if (strcmp (domains[i], E_DEBUG_LOG_DOMAIN_USER) == 0) continue; /* user actions are always enabled */ if (g_hash_table_lookup (domains_hash, domains[i]) == NULL) { gchar *domain; domain = g_strdup (domains[i]); g_hash_table_insert (domains_hash, domain, domain); } } unlock (); } /** * e_debug_log_disable_domains: * @domains: (array length=n_domains): an array of domains to disable * @n_domains: legth of the @domains array * * Disables all domains from the @domains array. * * Since: 2.32 **/ void e_debug_log_disable_domains (const gchar **domains, gint n_domains) { gint i; g_return_if_fail (domains != NULL); g_return_if_fail (n_domains >= 0); lock (); if (domains_hash) { for (i = 0; i < n_domains; i++) { gchar *domain; g_return_if_fail (domains[i] != NULL); if (strcmp (domains[i], E_DEBUG_LOG_DOMAIN_USER) == 0) continue; /* user actions are always enabled */ domain = g_hash_table_lookup (domains_hash, domains[i]); if (domain) { g_hash_table_remove (domains_hash, domain); g_free (domain); } } } /* else, there is nothing to disable */ unlock (); } /** * e_debug_log_is_domain_enabled: * @domain: a log domain * * Returns: whether the given log domain is enabled, which means * that any logging to this domain is recorded. * * Since: 2.32 **/ gboolean e_debug_log_is_domain_enabled (const gchar *domain) { gboolean retval; g_return_val_if_fail (domain != NULL, FALSE); lock (); retval = is_domain_enabled (domain); unlock (); return retval; } struct domains_dump_closure { gchar **domains; gint num_domains; }; static void domains_foreach_dump_cb (gpointer key, gpointer value, gpointer data) { struct domains_dump_closure *closure; gchar *domain; closure = data; domain = key; closure->domains[closure->num_domains] = domain; closure->num_domains++; } static GKeyFile * make_key_file_from_configuration (void) { GKeyFile *key_file; struct domains_dump_closure closure; gint num_domains; key_file = g_key_file_new (); /* domains */ if (domains_hash) { num_domains = g_hash_table_size (domains_hash); if (num_domains != 0) { closure.domains = g_new (gchar *, num_domains); closure.num_domains = 0; g_hash_table_foreach ( domains_hash, domains_foreach_dump_cb, &closure); g_return_val_if_fail (num_domains == closure.num_domains, NULL); g_key_file_set_string_list ( key_file, KEY_FILE_GROUP, KEY_FILE_DOMAINS_KEY, (const gchar * const *) closure.domains, closure.num_domains); g_free (closure.domains); } } /* max lines */ g_key_file_set_integer ( key_file, KEY_FILE_GROUP, KEY_FILE_MAX_LINES_KEY, ring_buffer_max_lines); return key_file; } static gboolean write_string (const gchar *filename, FILE *file, const gchar *str, GError **error) { if (fputs (str, file) == EOF) { gint saved_errno; saved_errno = errno; g_set_error ( error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), "error when writing to log file %s", filename); return FALSE; } return TRUE; } static gboolean dump_configuration (const gchar *filename, FILE *file, GError **error) { GKeyFile *key_file; gchar *data; gsize length; gboolean success; if (!write_string ( filename, file, "\n\n" "This configuration for the debug log can be re-created\n" "by putting the following in ~/evolution-data-server-debug-log.conf\n" "(use ';' to separate domain names):\n\n", error)) { return FALSE; } success = FALSE; key_file = make_key_file_from_configuration (); data = g_key_file_to_data (key_file, &length, error); if (!data) goto out; if (!write_string (filename, file, data, error)) { goto out; } success = TRUE; out: g_key_file_free (key_file); return success; } static gboolean dump_milestones (const gchar *filename, FILE *file, GError **error) { GSList *l; if (!write_string (filename, file, "===== BEGIN MILESTONES =====\n", error)) return FALSE; for (l = milestones_head; l; l = l->next) { const gchar *str; str = l->data; if (!(write_string (filename, file, str, error) && write_string (filename, file, "\n", error))) return FALSE; } if (!write_string (filename, file, "===== END MILESTONES =====\n", error)) return FALSE; return TRUE; } static gboolean dump_ring_buffer (const gchar *filename, FILE *file, GError **error) { gint start_index; gint i; if (!write_string (filename, file, "===== BEGIN RING BUFFER =====\n", error)) return FALSE; if (ring_buffer_num_lines == ring_buffer_max_lines) start_index = ring_buffer_next_index; else start_index = 0; for (i = 0; i < ring_buffer_num_lines; i++) { gint idx; idx = (start_index + i) % ring_buffer_max_lines; if (!(write_string (filename, file, ring_buffer[idx], error) && write_string (filename, file, "\n", error))) { return FALSE; } } if (!write_string (filename, file, "===== END RING BUFFER =====\n", error)) return FALSE; return TRUE; } /** * e_debug_log_dump: * @filename: a filename to save logged information to * @error: return location for a #GError, or %NULL * * Saves current log information to the given @filename. * * Returns: whether succeeded * * Since: 2.32 **/ gboolean e_debug_log_dump (const gchar *filename, GError **error) { FILE *file; gboolean success; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); lock (); success = FALSE; file = fopen (filename, "w"); if (!file) { gint saved_errno; saved_errno = errno; g_set_error ( error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), "could not open log file %s", filename); goto out; } if (!(dump_milestones (filename, file, error) && dump_ring_buffer (filename, file, error) && dump_configuration (filename, file, error))) { goto do_close; } success = TRUE; do_close: if (fclose (file) != 0) { gint saved_errno; saved_errno = errno; if (error && *error) { g_error_free (*error); *error = NULL; } g_set_error ( error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), "error when closing log file %s", filename); success = FALSE; } out: unlock (); return success; } /** * e_debug_log_dump_to_dated_file: * @error: return location for a #GError, or %NULL * * Saves current log information to a file "e-debug-log-YYYY-MM-DD-HH-mm-ss.txt" * in the user's HOME directory. * * Returns: whether succeeded * * Since: 2.32 **/ gboolean e_debug_log_dump_to_dated_file (GError **error) { time_t t; struct tm tm; gchar *basename; gchar *filename; gboolean retval; g_return_val_if_fail (error == NULL || *error == NULL, FALSE); t = time (NULL); tm = *localtime (&t); basename = g_strdup_printf ( "e-debug-log-%04d-%02d-%02d-%02d-%02d-%02d.txt", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); filename = g_build_filename (g_get_home_dir (), basename, NULL); retval = e_debug_log_dump (filename, error); g_free (basename); g_free (filename); return retval; } /** * e_debug_log_set_max_lines: * @num_lines: number of lines * * Limits how many lines the log can have. * * Since: 2.32 **/ void e_debug_log_set_max_lines (gint num_lines) { gchar **new_buffer; gint lines_to_copy; g_return_if_fail (num_lines > 0); lock (); if (num_lines == ring_buffer_max_lines) goto out; new_buffer = g_new0 (gchar *, num_lines); lines_to_copy = MIN (num_lines, ring_buffer_num_lines); if (ring_buffer) { gint start_index; gint i; if (ring_buffer_num_lines == ring_buffer_max_lines) start_index = (ring_buffer_next_index + ring_buffer_max_lines - lines_to_copy) % ring_buffer_max_lines; else start_index = ring_buffer_num_lines - lines_to_copy; g_return_if_fail (start_index >= 0 && start_index < ring_buffer_max_lines); for (i = 0; i < lines_to_copy; i++) { gint idx; idx = (start_index + i) % ring_buffer_max_lines; new_buffer[i] = ring_buffer[idx]; ring_buffer[idx] = NULL; } for (i = 0; i < ring_buffer_max_lines; i++) g_free (ring_buffer[i]); g_free (ring_buffer); } ring_buffer = new_buffer; ring_buffer_next_index = lines_to_copy; ring_buffer_num_lines = lines_to_copy; ring_buffer_max_lines = num_lines; out: unlock (); } /** * e_debug_log_get_max_lines: * * Since: 2.32 **/ gint e_debug_log_get_max_lines (void) { gint retval; lock (); retval = ring_buffer_max_lines; unlock (); return retval; } /** * e_debug_log_clear: * * Since: 2.32 **/ void e_debug_log_clear (void) { gint i; lock (); if (!ring_buffer) goto out; for (i = 0; i < ring_buffer_max_lines; i++) { g_free (ring_buffer[i]); ring_buffer[i] = NULL; } ring_buffer_next_index = 0; ring_buffer_num_lines = 0; out: unlock (); }