/* * Copyright (C) 2010 Collabora Ltd. * * 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, either version 2.1 of the License, or * (at your option) any later version. * * 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: * Philip Withnall */ using GLib; using Gee; /* We have to declare our own wrapping of g_log so that it doesn't have the * [Diagnostics] attribute, which would cause valac to add the Vala source file * name to the message format, which we don't want. This is used in * Debug.print_line(). */ [PrintfFormat] private extern void g_log (string? log_domain, LogLevelFlags log_level, string format, ...); /** * Manage debug output and status reporting for all folks objects. All GLib * debug logging calls are passed through a log handler in this class, which * allows debug domains to be outputted according to whether they've been * enabled by being passed to {@link Debug.dup}. * * @since 0.5.1 */ public class Folks.Debug : Object { private enum Domains { /* Zero is used for "no debug spew" */ CORE = 1 << 0, TELEPATHY_BACKEND = 1 << 1, KEY_FILE_BACKEND = 1 << 2 } /* Needs to be locked when accessed: */ private static weak Debug? _instance = null; private HashSet _domains; /* needs to be locked when accessed */ private bool _all = false; /* needs _domains to be locked when accessed */ /* The current indentation level, in spaces */ private uint _indentation = 0; private string _indentation_string = ""; private bool _colour_enabled = true; private HashSet _domains_handled; /* * Whether colour output is enabled. If true, debug output may include * terminal colour escape codes. Disabled by the environment variable * FOLKS_DEBUG_NO_COLOUR being set to anything except “0”. * * This property is thread-safe. * * @since 0.5.1 */ public bool colour_enabled { get { lock (this._colour_enabled) { return this._colour_enabled; } } set { lock (this._colour_enabled) { this._colour_enabled = value; } } } private bool _debug_output_enabled = true; /** * Whether debug output is enabled. This is orthogonal to the set of enabled * debug domains; filtering of debug output as a whole is done after filtering * by enabled domains. * * @since 0.5.1 */ public bool debug_output_enabled { get { lock (this._debug_output_enabled) { return this._debug_output_enabled; } } set { lock (this._debug_output_enabled) { this._debug_output_enabled = value; } } } /** * Signal emitted in the main thread whenever objects should print their * current status. All significant objects in the library should connect * to this and print their current status in some suitable format when it's * emitted. * * Client processes should emit this signal by calling * {@link Debug.emit_print_status}. * * @since 0.5.1 */ public signal void print_status (); /** * Log domain for the status messages logged as a result of calling * {@link Debug.emit_print_status}. * * This could be used in conjunction with a log handler to redirect the * status information to a debug window or log file, for example. * * @since 0.5.1 */ public const string STATUS_LOG_DOMAIN = "folks-status"; private void _print_status_log_handler_cb (string? log_domain, LogLevelFlags log_levels, string message) { /* Print directly to stdout without any adornments */ GLib.stdout.printf ("%s\n", message); } private void _log_handler_cb (string? log_domain, LogLevelFlags log_levels, string message) { if (this.debug_output_enabled == false) { /* Don't output anything if debug output is disabled, even for * enabled debug domains. */ return; } /* Otherwise, pass through to the default log handler */ Log.default_handler (log_domain, log_levels, message); } /* turn off debug output for the given domain unless it was in the FOLKS_DEBUG * environment variable (or 'all' was set) */ internal void _register_domain (string domain) { lock (this._domains) { if (this._all || this._domains.contains (domain.down ())) { this._set_handler (domain, LogLevelFlags.LEVEL_MASK, this._log_handler_cb); return; } } /* Install a log handler which will blackhole the log message. * Other log messages will be printed out by the default log handler. */ this._set_handler (domain, LogLevelFlags.LEVEL_DEBUG, (domain_arg, flags, message) => {}); } /** * Create or return the singleton {@link Debug} class instance. * If the instance doesn't exist already, it will be created with no debug * domains enabled. * * This function is thread-safe. * * @return Singleton {@link Debug} instance * @since 0.5.1 */ public static Debug dup () { lock (Debug._instance) { Debug? _retval = Debug._instance; Debug retval; if (_retval == null) { /* use an intermediate variable to force a strong reference */ retval = new Debug (); Debug._instance = retval; } else { retval = (!) _retval; } return retval; } } /** * Create or return the singleton {@link Debug} class instance. * If the instance doesn't exist already, it will be created with the given * set of debug domains enabled. Otherwise, the existing instance will have * its set of enabled domains changed to the provided set. * * @param debug_flags A comma-separated list of debug domains to enable, or * null to disable debug output * @param colour_enabled Whether debug output should be coloured using * terminal escape sequences * @return Singleton {@link Debug} instance * @since 0.5.1 */ public static Debug dup_with_flags (string? debug_flags, bool colour_enabled) { var retval = Debug.dup (); lock (retval._domains) { retval._all = false; retval._domains = new HashSet (str_hash, str_equal); if (debug_flags != null && debug_flags != "") { var domains_split = ((!) debug_flags).split (","); foreach (var domain in domains_split) { var domain_lower = domain.down (); if (GLib.strcmp (domain_lower, "all") == 0) retval._all = true; else retval._domains.add (domain_lower); } } } retval.colour_enabled = colour_enabled; /* Unconditionally enable all G_MESSAGES_DEBUG domains, or GLib's default * log handler will drop all our output. We don't spawn any subprocesses, * so this shouldn't leak and cause problems elsewhere. */ Environment.set_variable ("G_MESSAGES_DEBUG", "all", true); return retval; } private Debug () { /* Private constructor for singleton */ Object (); } construct { this._domains_handled = new HashSet (); /* Install a log handler for log messages emitted as a result of * Debug.print-status being emitted. */ this._set_handler (Debug.STATUS_LOG_DOMAIN, LogLevelFlags.LEVEL_MASK, this._print_status_log_handler_cb); } ~Debug () { /* Remove handlers so they don't get called after we're destroyed */ foreach (var domain in this._domains_handled) this._remove_handler (domain, true); this._domains_handled.clear (); /* Manually clear the singleton _instance */ lock (Debug._instance) { Debug._instance = null; } } private void _set_handler ( string domain, LogLevelFlags flags, LogFunc log_func) { this._remove_handler (domain); Log.set_handler (domain, flags, log_func); this._domains_handled.add (domain); } private void _remove_handler (string domain, bool keep_in_map = false) { if (this._domains_handled.contains (domain)) { Log.set_handler (domain, (LogLevelFlags.LEVEL_MASK | LogLevelFlags.FLAG_RECURSION | LogLevelFlags.FLAG_FATAL), Log.default_handler); if (!keep_in_map) this._domains_handled.remove (domain); } } /** * Causes all significant objects in the library to print their current * status to standard output, obeying the options set on this * {@link Debug} instance for colouring and other formatting. * * @since 0.5.1 */ public void emit_print_status () { print ("Dumping status information…\n"); this.print_status (); } /** * Increment the indentation level used when printing output through the * object. * * This is intended to be used by backend libraries only. * * @since 0.5.1 */ public void indent () { /* We indent in increments of two spaces */ this._indentation++; this._indentation_string = string.nfill (this._indentation * 2, ' '); } /** * Decrement the indentation level used when printing output through the * object. * * This is intended to be used by backend libraries only. * * @since 0.5.1 */ public void unindent () { this._indentation--; this._indentation_string = string.nfill (this._indentation * 2, ' '); } /** * Print a debug line with the current indentation level for the specified * debug domain. * * This is intended to be used by backend libraries only. * * @param domain The debug domain name * @param level A set of log level flags for the message * @param format A printf-style format string for the heading * @param ... Arguments for the format string * @since 0.5.1 */ [PrintfFormat ()] public void print_line (string domain, LogLevelFlags level, string format, ...) { /* FIXME: store the va_list temporarily to work around bgo#638308 */ var valist = va_list (); string output = format.vprintf (valist); g_log (domain, level, "%s%s", this._indentation_string, output); } /** * Print a debug line as a heading. It will be coloured according to the * current indentation level so that different levels of headings stand out. * * This is intended to be used by backend libraries only. * * @param domain The debug domain name * @param level A set of log level flags for the message * @param format A printf-style format string for the heading * @param ... Arguments for the format string * @since 0.5.1 */ [PrintfFormat ()] public void print_heading (string domain, LogLevelFlags level, string format, ...) { /* Colour the heading according to the current indentation level. * ANSI terminal colour codes. */ const int[] heading_colours = { 31, /* red */ 32, /* green */ 34 /* blue */ }; var wrapper_format = "%s"; if (this.colour_enabled == true) { var indentation = this._indentation.clamp (0, heading_colours.length - 1); wrapper_format = "\033[1;%im%%s\033[0m".printf (heading_colours[indentation]); } /* FIXME: store the va_list temporarily to work around bgo#638308 */ var valist = va_list (); string output = format.vprintf (valist); this.print_line (domain, level, wrapper_format, output); } /* * Format a potentially null string for printing; if the string is null, * “(null)” will be outputted. If coloured output is enabled, this output * will be coloured brown. */ private string _format_nullable_string (string? input) { if (this.colour_enabled == true && input == null) { return "\033[1;36m(null)\033[0m"; /* cyan */ } else if (input == null) { return "(null)"; } return (!) input; } struct KeyValuePair { string key; string? val; } /** * Print a set of key–value pairs in a table. The width of the key column is * automatically set to the width of the longest key. The keys and values * must be provided as a null-delimited list of alternating key–value varargs. * Values may be null but keys may not. * * This is intended to be used by backend libraries only. * * The table will be printed at the current indentation level plus one. * * @param domain The debug domain name * @param level A set of log level flags for the message * @param ... Alternating keys and values, terminated with null * @since 0.5.1 */ public void print_key_value_pairs (string domain, LogLevelFlags level, ...) { var valist = va_list (); KeyValuePair[] lines = {}; uint max_key_length = 0; /* Read in the arguments and calculate the longest key for alignment * purposes */ while (true) { string? _key = valist.arg (); if (_key == null) { break; } var key = (!) _key; string? val = valist.arg (); /* Keep track of the longest key we've seen */ max_key_length = uint.max (key.length, max_key_length); lines += KeyValuePair () { key = key, val = val }; } this.indent (); /* Print out the lines */ foreach (var line in lines) { var padding = string.nfill (max_key_length - line.key.length, ' '); this.print_line (domain, level, "%s: %s%s", line.key, padding, this._format_nullable_string (line.val)); } this.unindent (); } }