/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- caja-debug-log.c: Ring buffer for logging debug messages Copyright (C) 2006 Novell, Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. Author: Federico Mena-Quintero <federico@novell.com> */ #include <config.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <time.h> #include <sys/time.h> #include "caja-debug-log.h" #include "caja-file.h" #include <src/glibcompat.h> /* for g_list_free_full */ #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" #define MAX_URI_COUNT 20 #if GLIB_CHECK_VERSION(2, 31, 0) static GMutex log_mutex; #else static GStaticMutex log_mutex = G_STATIC_MUTEX_INIT; #endif static GHashTable *domains_hash; static char **ring_buffer; static int ring_buffer_next_index; static int ring_buffer_num_lines; static int ring_buffer_max_lines = DEFAULT_RING_BUFFER_NUM_LINES; static GSList *milestones_head; static GSList *milestones_tail; static void lock (void) { #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&log_mutex); #else g_static_mutex_lock (&log_mutex); #endif } static void unlock (void) { #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&log_mutex); #else g_static_mutex_unlock (&log_mutex); #endif } void caja_debug_log (gboolean is_milestone, const char *domain, const char *format, ...) { va_list args; va_start (args, format); caja_debug_logv (is_milestone, domain, NULL, format, args); va_end (args); } static gboolean is_domain_enabled (const char *domain) { /* User actions are always logged */ if (strcmp (domain, CAJA_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 (char *, ring_buffer_max_lines); ring_buffer_next_index = 0; ring_buffer_num_lines = 0; } static void add_to_ring (char *str) { ensure_ring (); g_assert (str != NULL); if (ring_buffer_num_lines == ring_buffer_max_lines) { /* We have an overlap, and the ring_buffer_next_index points to * the "first" item. Free it to make room for the new item. */ g_assert (ring_buffer[ring_buffer_next_index] != NULL); g_free (ring_buffer[ring_buffer_next_index]); } else ring_buffer_num_lines++; g_assert (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_assert (ring_buffer_num_lines == ring_buffer_max_lines); } } static void add_to_milestones (const char *str) { char *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_assert (milestones_head != NULL && milestones_tail != NULL); } void caja_debug_logv (gboolean is_milestone, const char *domain, const GList *uris, const char *format, va_list args) { char *str; char *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, (int) (tv.tv_usec / 100), domain, str); g_free (str); if (uris) { int debug_str_len; int uris_len; const GList *l; char *new_str; char *p; int count; uris_len = 0; count = 0; for (l = uris; l; l = l->next) { const char *uri; uri = l->data; uris_len += strlen (uri) + 2; /* plus 2 for a tab and the newline */ if (count++ > MAX_URI_COUNT) { uris_len += 4; /* "...\n" */ break; } } debug_str_len = strlen (debug_str); new_str = g_new (char, debug_str_len + 1 + uris_len + 1); /* plus 1 for newline & zero */ p = g_stpcpy (new_str, debug_str); *p++ = '\n'; count = 0; for (l = uris; l; l = l->next) { const char *uri; uri = l->data; *p++ = '\t'; p = g_stpcpy (p, uri); if (l->next) *p++ = '\n'; if (count++ > MAX_URI_COUNT) { p = g_stpcpy (p, "...\n"); break; } } g_free (debug_str); debug_str = new_str; } add_to_ring (debug_str); if (is_milestone) add_to_milestones (debug_str); out: unlock (); } void caja_debug_log_with_uri_list (gboolean is_milestone, const char *domain, const GList *uris, const char *format, ...) { va_list args; va_start (args, format); caja_debug_logv (is_milestone, domain, uris, format, args); va_end (args); } void caja_debug_log_with_file_list (gboolean is_milestone, const char *domain, GList *files, const char *format, ...) { va_list args; GList *uris; GList *l; int count; /* Avoid conversion if debugging disabled */ if (!(is_milestone || caja_debug_log_is_domain_enabled (domain))) { return; } uris = NULL; count = 0; for (l = files; l; l = l->next) { CajaFile *file; char *uri; file = CAJA_FILE (l->data); uri = caja_file_get_uri (file); if (caja_file_is_gone (file)) { char *new_uri; /* Hack: this will create an invalid URI, but it's for * display purposes only. */ new_uri = g_strconcat (uri ? uri : "", " (gone)", NULL); g_free (uri); uri = new_uri; } uris = g_list_prepend (uris, uri); /* Avoid doing to much work, more than MAX_URI_COUNT uris won't be shown anyway */ if (count++ > MAX_URI_COUNT + 1) { break; } } uris = g_list_reverse (uris); va_start (args, format); caja_debug_logv (is_milestone, domain, uris, format, args); va_end (args); g_list_free_full (uris, g_free); } gboolean caja_debug_log_load_configuration (const char *filename, GError **error) { GKeyFile *key_file; char **strings; gsize num_strings; int num; GError *my_error; g_assert (filename != NULL); g_assert (error == NULL || *error == NULL); 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 { int i; for (i = 0; i < num_strings; i++) strings[i] = g_strstrip (strings[i]); caja_debug_log_enable_domains ((const char **) 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 caja_debug_log_set_max_lines (num); g_key_file_free (key_file); return TRUE; } void caja_debug_log_enable_domains (const char **domains, int n_domains) { int i; g_assert (domains != NULL); g_assert (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_assert (domains[i] != NULL); if (strcmp (domains[i], CAJA_DEBUG_LOG_DOMAIN_USER) == 0) continue; /* user actions are always enabled */ if (g_hash_table_lookup (domains_hash, domains[i]) == NULL) { char *domain; domain = g_strdup (domains[i]); g_hash_table_insert (domains_hash, domain, domain); } } unlock (); } void caja_debug_log_disable_domains (const char **domains, int n_domains) { int i; g_assert (domains != NULL); g_assert (n_domains >= 0); lock (); if (domains_hash) { for (i = 0; i < n_domains; i++) { char *domain; g_assert (domains[i] != NULL); if (strcmp (domains[i], CAJA_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 (); } gboolean caja_debug_log_is_domain_enabled (const char *domain) { gboolean retval; g_assert (domain != NULL); lock (); retval = is_domain_enabled (domain); unlock (); return retval; } struct domains_dump_closure { char **domains; int num_domains; }; static void domains_foreach_dump_cb (gpointer key, gpointer value, gpointer data) { struct domains_dump_closure *closure; char *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; int 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 (char *, num_domains); closure.num_domains = 0; g_hash_table_foreach (domains_hash, domains_foreach_dump_cb, &closure); g_assert (num_domains == closure.num_domains); 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 char *filename, FILE *file, const char *str, GError **error) { if (fputs (str, file) == EOF) { int 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 char *filename, FILE *file, GError **error) { GKeyFile *key_file; char *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 ~/caja-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 char *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 char *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 char *filename, FILE *file, GError **error) { int start_index; int 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++) { int 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; } gboolean caja_debug_log_dump (const char *filename, GError **error) { FILE *file; gboolean success; g_assert (error == NULL || *error == NULL); lock (); success = FALSE; file = fopen (filename, "w"); if (!file) { int 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) { int 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; } void caja_debug_log_set_max_lines (int num_lines) { char **new_buffer; int lines_to_copy; g_assert (num_lines > 0); lock (); if (num_lines == ring_buffer_max_lines) goto out; new_buffer = g_new0 (char *, num_lines); lines_to_copy = MIN (num_lines, ring_buffer_num_lines); if (ring_buffer) { int start_index; int 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_assert (start_index >= 0 && start_index < ring_buffer_max_lines); for (i = 0; i < lines_to_copy; i++) { int 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 (); } int caja_debug_log_get_max_lines (void) { int retval; lock (); retval = ring_buffer_max_lines; unlock (); return retval; } void caja_debug_log_clear (void) { int 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 (); }