/* LIBGIMP - The GIMP Library * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis * * gimputils.c * Copyright (C) 2003 Sven Neumann * * 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 3 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 * . */ #include "config.h" #include #include #include #ifdef PLATFORM_OSX #include #endif #ifdef HAVE_EXECINFO_H /* Allowing backtrace() API. */ #include #endif #include #include #if defined(G_OS_WIN32) # include # include #else /* G_OS_WIN32 */ /* For waitpid() */ #include #include #include /* For thread IDs. */ #include #include #ifdef HAVE_SYS_PRCTL_H #include #endif #ifdef HAVE_SYS_THR_H #include #endif #endif /* G_OS_WIN32 */ #include "gimpbasetypes.h" #include "gimputils.h" #include "libgimp/libgimp-intl.h" /** * SECTION: gimputils * @title: gimputils * @short_description: Utilities of general interest * * Utilities of general interest **/ static gboolean gimp_utils_generic_available (const gchar *program, gint major, gint minor); static gboolean gimp_utils_gdb_available (gint major, gint minor); /** * gimp_utf8_strtrim: * @str: an UTF-8 encoded string (or %NULL) * @max_chars: the maximum number of characters before the string get * trimmed * * Creates a (possibly trimmed) copy of @str. The string is cut if it * exceeds @max_chars characters or on the first newline. The fact * that the string was trimmed is indicated by appending an ellipsis. * * Returns: A (possibly trimmed) copy of @str which should be freed * using g_free() when it is not needed any longer. **/ gchar * gimp_utf8_strtrim (const gchar *str, gint max_chars) { /* FIXME: should we make this translatable? */ const gchar ellipsis[] = "..."; const gint e_len = strlen (ellipsis); if (str) { const gchar *p; const gchar *newline = NULL; gint chars = 0; gunichar unichar; for (p = str; *p; p = g_utf8_next_char (p)) { if (++chars > max_chars) break; unichar = g_utf8_get_char (p); switch (g_unichar_break_type (unichar)) { case G_UNICODE_BREAK_MANDATORY: case G_UNICODE_BREAK_LINE_FEED: newline = p; break; default: continue; } break; } if (*p) { gsize len = p - str; gchar *trimmed = g_new (gchar, len + e_len + 2); memcpy (trimmed, str, len); if (newline) trimmed[len++] = ' '; g_strlcpy (trimmed + len, ellipsis, e_len + 1); return trimmed; } return g_strdup (str); } return NULL; } /** * gimp_any_to_utf8: * @str: The string to be converted to UTF-8. * @len: The length of the string, or -1 if the string * is nul-terminated. * @warning_format: The message format for the warning message if conversion * to UTF-8 fails. See the printf() * documentation. * @...: The parameters to insert into the format string. * * This function takes any string (UTF-8 or not) and always returns a valid * UTF-8 string. * * If @str is valid UTF-8, a copy of the string is returned. * * If UTF-8 validation fails, g_locale_to_utf8() is tried and if it * succeeds the resulting string is returned. * * Otherwise, the portion of @str that is UTF-8, concatenated * with "(invalid UTF-8 string)" is returned. If not even the start * of @str is valid UTF-8, only "(invalid UTF-8 string)" is returned. * * Return value: The UTF-8 string as described above. **/ gchar * gimp_any_to_utf8 (const gchar *str, gssize len, const gchar *warning_format, ...) { const gchar *start_invalid; gchar *utf8; g_return_val_if_fail (str != NULL, NULL); if (g_utf8_validate (str, len, &start_invalid)) { if (len < 0) utf8 = g_strdup (str); else utf8 = g_strndup (str, len); } else { utf8 = g_locale_to_utf8 (str, len, NULL, NULL, NULL); } if (! utf8) { if (warning_format) { va_list warning_args; va_start (warning_args, warning_format); g_logv (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, warning_format, warning_args); va_end (warning_args); } if (start_invalid > str) { gchar *tmp; tmp = g_strndup (str, start_invalid - str); utf8 = g_strconcat (tmp, " ", _("(invalid UTF-8 string)"), NULL); g_free (tmp); } else { utf8 = g_strdup (_("(invalid UTF-8 string)")); } } return utf8; } /** * gimp_filename_to_utf8: * @filename: The filename to be converted to UTF-8. * * Convert a filename in the filesystem's encoding to UTF-8 * temporarily. The return value is a pointer to a string that is * guaranteed to be valid only during the current iteration of the * main loop or until the next call to gimp_filename_to_utf8(). * * The only purpose of this function is to provide an easy way to pass * a filename in the filesystem encoding to a function that expects an * UTF-8 encoded filename. * * Return value: A temporarily valid UTF-8 representation of @filename. * This string must not be changed or freed. **/ const gchar * gimp_filename_to_utf8 (const gchar *filename) { /* Simpleminded implementation, but at least allocates just one copy * of each translation. Could check if already UTF-8, and if so * return filename as is. Could perhaps (re)use a suitably large * cyclic buffer, but then would have to verify that all calls * really need the return value just for a "short" time. */ static GHashTable *ht = NULL; gchar *filename_utf8; if (! filename) return NULL; if (! ht) ht = g_hash_table_new (g_str_hash, g_str_equal); filename_utf8 = g_hash_table_lookup (ht, filename); if (! filename_utf8) { filename_utf8 = g_filename_display_name (filename); g_hash_table_insert (ht, g_strdup (filename), filename_utf8); } return filename_utf8; } /** * gimp_file_get_utf8_name: * @file: a #GFile * * This function works like gimp_filename_to_utf8() and returns * a UTF-8 encoded string that does not need to be freed. * * It converts a #GFile's path or uri to UTF-8 temporarily. The * return value is a pointer to a string that is guaranteed to be * valid only during the current iteration of the main loop or until * the next call to gimp_file_get_utf8_name(). * * The only purpose of this function is to provide an easy way to pass * a #GFile's name to a function that expects an UTF-8 encoded string. * * See g_file_get_parse_name(). * * Since: 2.10 * * Return value: A temporarily valid UTF-8 representation of @file's name. * This string must not be changed or freed. **/ const gchar * gimp_file_get_utf8_name (GFile *file) { gchar *name; g_return_val_if_fail (G_IS_FILE (file), NULL); name = g_file_get_parse_name (file); g_object_set_data_full (G_OBJECT (file), "gimp-parse-name", name, (GDestroyNotify) g_free); return name; } /** * gimp_file_has_extension: * @file: a #GFile * @extension: an ASCII extension * * This function checks if @file's URI ends with @extension. It behaves * like g_str_has_suffix() on g_file_get_uri(), except that the string * comparison is done case-insensitively using g_ascii_strcasecmp(). * * Since: 2.10 * * Return value: %TRUE if @file's URI ends with @extension, * %FALSE otherwise. **/ gboolean gimp_file_has_extension (GFile *file, const gchar *extension) { gchar *uri; gint uri_len; gint ext_len; gboolean result = FALSE; g_return_val_if_fail (G_IS_FILE (file), FALSE); g_return_val_if_fail (extension != NULL, FALSE); uri = g_file_get_uri (file); uri_len = strlen (uri); ext_len = strlen (extension); if (uri_len && ext_len && (uri_len > ext_len)) { if (g_ascii_strcasecmp (uri + uri_len - ext_len, extension) == 0) result = TRUE; } g_free (uri); return result; } /** * gimp_file_show_in_file_manager: * @file: a #GFile * @error: return location for a #GError * * Shows @file in the system file manager. * * Since: 2.10 * * Return value: %TRUE on success, %FALSE otherwise. On %FALSE, @error * is set. **/ gboolean gimp_file_show_in_file_manager (GFile *file, GError **error) { g_return_val_if_fail (G_IS_FILE (file), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); #if defined(G_OS_WIN32) { gboolean ret; char *filename; int n; LPWSTR w_filename = NULL; ITEMIDLIST *pidl = NULL; ret = FALSE; /* Calling this function multiple times should do no harm, but it is easier to put this here as it needs linking against ole32. */ CoInitialize (NULL); filename = g_file_get_path (file); if (!filename) { g_set_error_literal (error, G_FILE_ERROR, 0, _("File path is NULL")); goto out; } n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, filename, -1, NULL, 0); if (n == 0) { g_set_error_literal (error, G_FILE_ERROR, 0, _("Error converting UTF-8 filename to wide char")); goto out; } w_filename = g_malloc_n (n + 1, sizeof (wchar_t)); n = MultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, filename, -1, w_filename, (n + 1) * sizeof (wchar_t)); if (n == 0) { g_set_error_literal (error, G_FILE_ERROR, 0, _("Error converting UTF-8 filename to wide char")); goto out; } pidl = ILCreateFromPathW (w_filename); if (!pidl) { g_set_error_literal (error, G_FILE_ERROR, 0, _("ILCreateFromPath() failed")); goto out; } SHOpenFolderAndSelectItems (pidl, 0, NULL, 0); ret = TRUE; out: if (pidl) ILFree (pidl); g_free (w_filename); g_free (filename); return ret; } #elif defined(PLATFORM_OSX) { gchar *uri; NSString *filename; NSURL *url; gboolean retval = TRUE; uri = g_file_get_uri (file); filename = [NSString stringWithUTF8String:uri]; url = [NSURL URLWithString:filename]; if (url) { NSArray *url_array = [NSArray arrayWithObject:url]; [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:url_array]; } else { g_set_error (error, G_FILE_ERROR, 0, _("Cannot convert '%s' into a valid NSURL."), uri); retval = FALSE; } g_free (uri); return retval; } #else /* UNIX */ { GDBusProxy *proxy; GVariant *retval; GVariantBuilder *builder; gchar *uri; proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", NULL, error); if (! proxy) { g_prefix_error (error, _("Connecting to org.freedesktop.FileManager1 failed: ")); return FALSE; } uri = g_file_get_uri (file); builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); g_variant_builder_add (builder, "s", uri); g_free (uri); retval = g_dbus_proxy_call_sync (proxy, "ShowItems", g_variant_new ("(ass)", builder, ""), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); g_variant_builder_unref (builder); g_object_unref (proxy); if (! retval) { g_prefix_error (error, _("Calling ShowItems failed: ")); return FALSE; } g_variant_unref (retval); return TRUE; } #endif } /** * gimp_strip_uline: * @str: underline infested string (or %NULL) * * This function returns a copy of @str stripped of underline * characters. This comes in handy when needing to strip mnemonics * from menu paths etc. * * In some languages, mnemonics are handled by adding the mnemonic * character in brackets (like "File (_F)"). This function recognizes * this construct and removes the whole bracket construction to get * rid of the mnemonic (see bug 157561). * * Return value: A (possibly stripped) copy of @str which should be * freed using g_free() when it is not needed any longer. **/ gchar * gimp_strip_uline (const gchar *str) { gchar *escaped; gchar *p; gboolean past_bracket = FALSE; if (! str) return NULL; p = escaped = g_strdup (str); while (*str) { if (*str == '_') { /* "__" means a literal "_" in the menu path */ if (str[1] == '_') { *p++ = *str++; str++; continue; } /* find the "(_X)" construct and remove it entirely */ if (past_bracket && str[1] && *(g_utf8_next_char (str + 1)) == ')') { str = g_utf8_next_char (str + 1) + 1; p--; } else { str++; } } else { past_bracket = (*str == '('); *p++ = *str++; } } *p = '\0'; return escaped; } /** * gimp_escape_uline: * @str: Underline infested string (or %NULL) * * This function returns a copy of @str with all underline converted * to two adjacent underlines. This comes in handy when needing to display * strings with underlines (like filenames) in a place that would convert * them to mnemonics. * * Return value: A (possibly escaped) copy of @str which should be * freed using g_free() when it is not needed any longer. * * Since: 2.2 **/ gchar * gimp_escape_uline (const gchar *str) { gchar *escaped; gchar *p; gint n_ulines = 0; if (! str) return NULL; for (p = (gchar *) str; *p; p++) if (*p == '_') n_ulines++; p = escaped = g_malloc (strlen (str) + n_ulines + 1); while (*str) { if (*str == '_') *p++ = '_'; *p++ = *str++; } *p = '\0'; return escaped; } /** * gimp_canonicalize_identifier: * @identifier: The identifier string to canonicalize. * * Turns any input string into a canonicalized string. * * Canonical identifiers are e.g. expected by the PDB for procedure * and parameter names. Every character of the input string that is * not either '-', 'a-z', 'A-Z' or '0-9' will be replaced by a '-'. * * Return value: The canonicalized identifier. This is a newly * allocated string that should be freed with g_free() * when no longer needed. * * Since: 2.4 **/ gchar * gimp_canonicalize_identifier (const gchar *identifier) { gchar *canonicalized = NULL; if (identifier) { gchar *p; canonicalized = g_strdup (identifier); for (p = canonicalized; *p != 0; p++) { gchar c = *p; if (c != '-' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) *p = '-'; } } return canonicalized; } /** * gimp_enum_get_desc: * @enum_class: a #GEnumClass * @value: a value from @enum_class * * Retrieves #GimpEnumDesc associated with the given value, or %NULL. * * Return value: the value's #GimpEnumDesc. * * Since: 2.2 **/ GimpEnumDesc * gimp_enum_get_desc (GEnumClass *enum_class, gint value) { const GimpEnumDesc *value_desc; g_return_val_if_fail (G_IS_ENUM_CLASS (enum_class), NULL); value_desc = gimp_enum_get_value_descriptions (G_TYPE_FROM_CLASS (enum_class)); if (value_desc) { while (value_desc->value_desc) { if (value_desc->value == value) return (GimpEnumDesc *) value_desc; value_desc++; } } return NULL; } /** * gimp_enum_get_value: * @enum_type: the #GType of a registered enum * @value: an integer value * @value_name: return location for the value's name (or %NULL) * @value_nick: return location for the value's nick (or %NULL) * @value_desc: return location for the value's translated description (or %NULL) * @value_help: return location for the value's translated help (or %NULL) * * Checks if @value is valid for the enum registered as @enum_type. * If the value exists in that enum, its name, nick and its translated * description and help are returned (if @value_name, @value_nick, * @value_desc and @value_help are not %NULL). * * Return value: %TRUE if @value is valid for the @enum_type, * %FALSE otherwise * * Since: 2.2 **/ gboolean gimp_enum_get_value (GType enum_type, gint value, const gchar **value_name, const gchar **value_nick, const gchar **value_desc, const gchar **value_help) { GEnumClass *enum_class; GEnumValue *enum_value; gboolean success = FALSE; g_return_val_if_fail (G_TYPE_IS_ENUM (enum_type), FALSE); enum_class = g_type_class_ref (enum_type); enum_value = g_enum_get_value (enum_class, value); if (enum_value) { if (value_name) *value_name = enum_value->value_name; if (value_nick) *value_nick = enum_value->value_nick; if (value_desc || value_help) { GimpEnumDesc *enum_desc; enum_desc = gimp_enum_get_desc (enum_class, value); if (value_desc) { if (enum_desc && enum_desc->value_desc) { const gchar *context; context = gimp_type_get_translation_context (enum_type); if (context) /* the new way, using NC_() */ *value_desc = g_dpgettext2 (gimp_type_get_translation_domain (enum_type), context, enum_desc->value_desc); else /* for backward compatibility */ *value_desc = g_strip_context (enum_desc->value_desc, dgettext (gimp_type_get_translation_domain (enum_type), enum_desc->value_desc)); } else { *value_desc = NULL; } } if (value_help) { *value_help = ((enum_desc && enum_desc->value_help) ? dgettext (gimp_type_get_translation_domain (enum_type), enum_desc->value_help) : NULL); } } success = TRUE; } g_type_class_unref (enum_class); return success; } /** * gimp_enum_value_get_desc: * @enum_class: a #GEnumClass * @enum_value: a #GEnumValue from @enum_class * * Retrieves the translated description for a given @enum_value. * * Return value: the translated description of the enum value * * Since: 2.2 **/ const gchar * gimp_enum_value_get_desc (GEnumClass *enum_class, GEnumValue *enum_value) { GType type = G_TYPE_FROM_CLASS (enum_class); GimpEnumDesc *enum_desc; enum_desc = gimp_enum_get_desc (enum_class, enum_value->value); if (enum_desc && enum_desc->value_desc) { const gchar *context; context = gimp_type_get_translation_context (type); if (context) /* the new way, using NC_() */ return g_dpgettext2 (gimp_type_get_translation_domain (type), context, enum_desc->value_desc); else /* for backward compatibility */ return g_strip_context (enum_desc->value_desc, dgettext (gimp_type_get_translation_domain (type), enum_desc->value_desc)); } return enum_value->value_name; } /** * gimp_enum_value_get_help: * @enum_class: a #GEnumClass * @enum_value: a #GEnumValue from @enum_class * * Retrieves the translated help for a given @enum_value. * * Return value: the translated help of the enum value * * Since: 2.2 **/ const gchar * gimp_enum_value_get_help (GEnumClass *enum_class, GEnumValue *enum_value) { GType type = G_TYPE_FROM_CLASS (enum_class); GimpEnumDesc *enum_desc; enum_desc = gimp_enum_get_desc (enum_class, enum_value->value); if (enum_desc && enum_desc->value_help) return dgettext (gimp_type_get_translation_domain (type), enum_desc->value_help); return NULL; } /** * gimp_enum_value_get_abbrev: * @enum_class: a #GEnumClass * @enum_value: a #GEnumValue from @enum_class * * Retrieves the translated abbreviation for a given @enum_value. * * Return value: the translated abbreviation of the enum value * * Since: 2.10 **/ const gchar * gimp_enum_value_get_abbrev (GEnumClass *enum_class, GEnumValue *enum_value) { GType type = G_TYPE_FROM_CLASS (enum_class); GimpEnumDesc *enum_desc; enum_desc = gimp_enum_get_desc (enum_class, enum_value->value); if (enum_desc && enum_desc[1].value == enum_desc->value && enum_desc[1].value_desc) { return g_dpgettext2 (gimp_type_get_translation_domain (type), gimp_type_get_translation_context (type), enum_desc[1].value_desc); } return NULL; } /** * gimp_flags_get_first_desc: * @flags_class: a #GFlagsClass * @value: a value from @flags_class * * Retrieves the first #GimpFlagsDesc that matches the given value, or %NULL. * * Return value: the value's #GimpFlagsDesc. * * Since: 2.2 **/ GimpFlagsDesc * gimp_flags_get_first_desc (GFlagsClass *flags_class, guint value) { const GimpFlagsDesc *value_desc; g_return_val_if_fail (G_IS_FLAGS_CLASS (flags_class), NULL); value_desc = gimp_flags_get_value_descriptions (G_TYPE_FROM_CLASS (flags_class)); if (value_desc) { while (value_desc->value_desc) { if ((value_desc->value & value) == value_desc->value) return (GimpFlagsDesc *) value_desc; value_desc++; } } return NULL; } /** * gimp_flags_get_first_value: * @flags_type: the #GType of registered flags * @value: an integer value * @value_name: return location for the value's name (or %NULL) * @value_nick: return location for the value's nick (or %NULL) * @value_desc: return location for the value's translated description (or %NULL) * @value_help: return location for the value's translated help (or %NULL) * * Checks if @value is valid for the flags registered as @flags_type. * If the value exists in that flags, its name, nick and its * translated description and help are returned (if @value_name, * @value_nick, @value_desc and @value_help are not %NULL). * * Return value: %TRUE if @value is valid for the @flags_type, * %FALSE otherwise * * Since: 2.2 **/ gboolean gimp_flags_get_first_value (GType flags_type, guint value, const gchar **value_name, const gchar **value_nick, const gchar **value_desc, const gchar **value_help) { GFlagsClass *flags_class; GFlagsValue *flags_value; g_return_val_if_fail (G_TYPE_IS_FLAGS (flags_type), FALSE); flags_class = g_type_class_peek (flags_type); flags_value = g_flags_get_first_value (flags_class, value); if (flags_value) { if (value_name) *value_name = flags_value->value_name; if (value_nick) *value_nick = flags_value->value_nick; if (value_desc || value_help) { GimpFlagsDesc *flags_desc; flags_desc = gimp_flags_get_first_desc (flags_class, value); if (value_desc) *value_desc = ((flags_desc && flags_desc->value_desc) ? dgettext (gimp_type_get_translation_domain (flags_type), flags_desc->value_desc) : NULL); if (value_help) *value_help = ((flags_desc && flags_desc->value_desc) ? dgettext (gimp_type_get_translation_domain (flags_type), flags_desc->value_help) : NULL); } return TRUE; } return FALSE; } /** * gimp_flags_value_get_desc: * @flags_class: a #GFlagsClass * @flags_value: a #GFlagsValue from @flags_class * * Retrieves the translated description for a given @flags_value. * * Return value: the translated description of the flags value * * Since: 2.2 **/ const gchar * gimp_flags_value_get_desc (GFlagsClass *flags_class, GFlagsValue *flags_value) { GType type = G_TYPE_FROM_CLASS (flags_class); GimpFlagsDesc *flags_desc; flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value); if (flags_desc->value_desc) { const gchar *context; context = gimp_type_get_translation_context (type); if (context) /* the new way, using NC_() */ return g_dpgettext2 (gimp_type_get_translation_domain (type), context, flags_desc->value_desc); else /* for backward compatibility */ return g_strip_context (flags_desc->value_desc, dgettext (gimp_type_get_translation_domain (type), flags_desc->value_desc)); } return flags_value->value_name; } /** * gimp_flags_value_get_help: * @flags_class: a #GFlagsClass * @flags_value: a #GFlagsValue from @flags_class * * Retrieves the translated help for a given @flags_value. * * Return value: the translated help of the flags value * * Since: 2.2 **/ const gchar * gimp_flags_value_get_help (GFlagsClass *flags_class, GFlagsValue *flags_value) { GType type = G_TYPE_FROM_CLASS (flags_class); GimpFlagsDesc *flags_desc; flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value); if (flags_desc->value_help) return dgettext (gimp_type_get_translation_domain (type), flags_desc->value_help); return NULL; } /** * gimp_flags_value_get_abbrev: * @flags_class: a #GFlagsClass * @flags_value: a #GFlagsValue from @flags_class * * Retrieves the translated abbreviation for a given @flags_value. * * Return value: the translated abbreviation of the flags value * * Since: 2.10 **/ const gchar * gimp_flags_value_get_abbrev (GFlagsClass *flags_class, GFlagsValue *flags_value) { GType type = G_TYPE_FROM_CLASS (flags_class); GimpFlagsDesc *flags_desc; flags_desc = gimp_flags_get_first_desc (flags_class, flags_value->value); if (flags_desc && flags_desc[1].value == flags_desc->value && flags_desc[1].value_desc) { return g_dpgettext2 (gimp_type_get_translation_domain (type), gimp_type_get_translation_context (type), flags_desc[1].value_desc); } return NULL; } /** * gimp_stack_trace_available: * @optimal: whether we get optimal traces. * * Returns #TRUE if we have dependencies to generate backtraces. If * @optimal is #TRUE, the function will return #TRUE only when we * are able to generate optimal traces (i.e. with GDB or LLDB); * otherwise we return #TRUE even if only backtrace() API is available. * * On Win32, we return TRUE if Dr. Mingw is built-in, FALSE otherwise. * * Note: this function is not crash-safe, i.e. you should not try to use * it in a callback when the program is already crashing. In such a * case, call gimp_stack_trace_print() or gimp_stack_trace_query() * directly. * * Since: 2.10 **/ gboolean gimp_stack_trace_available (gboolean optimal) { #ifndef G_OS_WIN32 gchar *lld_path = NULL; gboolean has_lldb = FALSE; /* Similarly to gdb, we could check for lldb by calling: * gimp_utils_generic_available ("lldb", major, minor). * We don't do so on purpose because on macOS, when lldb is absent, it * triggers a popup asking to install Xcode. So instead, we just * search for the executable in path. * This is the reason why this function is not crash-safe, since * g_find_program_in_path() allocates memory. * See issue #1999. */ lld_path = g_find_program_in_path ("lldb"); if (lld_path) { has_lldb = TRUE; g_free (lld_path); } if (gimp_utils_gdb_available (7, 0) || has_lldb) return TRUE; #ifdef HAVE_EXECINFO_H if (! optimal) return TRUE; #endif #else /* G_OS_WIN32 */ #ifdef HAVE_EXCHNDL return TRUE; #endif #endif /* G_OS_WIN32 */ return FALSE; } /** * gimp_stack_trace_print: * @prog_name: the program to attach to. * @stream: a #FILE * stream. * @trace: location to store a newly allocated string of the trace. * * Attempts to generate a stack trace at current code position in * @prog_name. @prog_name is mostly a helper and can be set to NULL. * Nevertheless if set, it has to be the current program name (argv[0]). * This function is not meant to generate stack trace for third-party * programs, and will attach the current process id only. * Internally, this function uses `gdb` or `lldb` if they are available, * or the stacktrace() API on platforms where it is available. It always * fails on Win32. * * The stack trace, once generated, will either be printed to @stream or * returned as a newly allocated string in @trace, if not #NULL. * * In some error cases (e.g. segmentation fault), trying to allocate * more memory will trigger more segmentation faults and therefore loop * our error handling (which is just wrong). Therefore printing to a * file description is an implementation without any memory allocation. * Return value: #TRUE if a stack trace could be generated, #FALSE * otherwise. * * Since: 2.10 **/ gboolean gimp_stack_trace_print (const gchar *prog_name, gpointer stream, gchar **trace) { gboolean stack_printed = FALSE; /* This works only on UNIX systems. */ #ifndef G_OS_WIN32 GString *gtrace = NULL; gchar gimp_pid[16]; gchar buffer[256]; ssize_t read_n; int sync_fd[2]; int out_fd[2]; pid_t fork_pid; pid_t pid = getpid(); gint eintr_count = 0; #if defined(G_OS_WIN32) DWORD tid = GetCurrentThreadId (); #elif defined(PLATFORM_OSX) uint64 tid64; long tid; pthread_threadid_np (NULL, &tid64); tid = (long) tid64; #elif defined(SYS_gettid) long tid = syscall (SYS_gettid); #elif defined(HAVE_THR_SELF) long tid = 0; thr_self (&tid); #endif g_snprintf (gimp_pid, 16, "%u", (guint) pid); if (pipe (sync_fd) == -1) { return FALSE; } if (pipe (out_fd) == -1) { close (sync_fd[0]); close (sync_fd[1]); return FALSE; } fork_pid = fork (); if (fork_pid == 0) { /* Child process. */ gchar *args[9] = { "gdb", "-batch", "-ex", "info threads", "-ex", "thread apply all backtrace full", (gchar *) prog_name, NULL, NULL }; if (prog_name == NULL) args[6] = "-p"; args[7] = gimp_pid; /* Wait until the parent enabled us to ptrace it. */ { gchar dummy; close (sync_fd[1]); while (read (sync_fd[0], &dummy, 1) < 0 && errno == EINTR); close (sync_fd[0]); } /* Redirect the debugger output. */ dup2 (out_fd[1], STDOUT_FILENO); close (out_fd[0]); close (out_fd[1]); /* Run GDB if version 7.0 or over. Why I do such a check is that * it turns out older versions may not only fail, but also have * very undesirable side effects like terminating the debugged * program, at least on FreeBSD where GDB 6.1 is apparently * installed by default on the stable release at day of writing. * See bug 793514. */ if (! gimp_utils_gdb_available (7, 0) || execvp (args[0], args) == -1) { /* LLDB as alternative if the GDB call failed or if it was in * a too-old version. */ gchar *args_lldb[15] = { "lldb", "--attach-pid", NULL, "--batch", "--one-line", "thread list", "--one-line", "thread backtrace all", "--one-line", "bt all", "--one-line-on-crash", "bt", "--one-line-on-crash", "quit", NULL }; args_lldb[2] = gimp_pid; execvp (args_lldb[0], args_lldb); } _exit (0); } else if (fork_pid > 0) { /* Main process */ int status; /* Allow the child to ptrace us, and signal it to start. */ close (sync_fd[0]); #ifdef PR_SET_PTRACER prctl (PR_SET_PTRACER, fork_pid, 0, 0, 0); #endif close (sync_fd[1]); /* It is important to close the writing side of the pipe, otherwise * the read() will wait forever without getting the information that * writing is finished. */ close (out_fd[1]); while ((read_n = read (out_fd[0], buffer, 256)) != 0) { if (read_n < 0) { /* LLDB on macOS seems to trigger a few EINTR error (see * !13), though read() finally ends up working later. So * let's not make this error fatal, and instead try again. * Yet to avoid infinite loop (in case the error really * happens at every call), we abandon after a few * consecutive errors. */ if (errno == EINTR && eintr_count <= 5) { eintr_count++; continue; } break; } eintr_count = 0; if (! stack_printed) { #if defined(PLATFORM_OSX) if (stream) g_fprintf (stream, "\n# Stack traces obtained from PID %d - Thread 0x%lx #\n\n", pid, tid); #elif defined(G_OS_WIN32) || defined(SYS_gettid) || defined(HAVE_THR_SELF) if (stream) g_fprintf (stream, "\n# Stack traces obtained from PID %d - Thread %lu #\n\n", pid, tid); #endif if (trace) { gtrace = g_string_new (NULL); #if defined(PLATFORM_OSX) g_string_printf (gtrace, "\n# Stack traces obtained from PID %d - Thread 0x%lx #\n\n", pid, tid); #elif defined(G_OS_WIN32) || defined(SYS_gettid) || defined(HAVE_THR_SELF) g_string_printf (gtrace, "\n# Stack traces obtained from PID %d - Thread %lu #\n\n", pid, tid); #endif } } /* It's hard to know if the debugger was found since it * happened in the child. Let's just assume that any output * means it succeeded. */ stack_printed = TRUE; buffer[read_n] = '\0'; if (stream) g_fprintf (stream, "%s", buffer); if (trace) g_string_append (gtrace, (const gchar *) buffer); } close (out_fd[0]); #ifdef PR_SET_PTRACER /* Clear ptrace permission set above */ prctl (PR_SET_PTRACER, 0, 0, 0, 0); #endif waitpid (fork_pid, &status, 0); } /* else if (fork_pid == (pid_t) -1) * Fork failed! * Just continue, maybe the backtrace() API will succeed. */ #ifdef HAVE_EXECINFO_H if (! stack_printed) { /* As a last resort, try using the backtrace() Linux API. It is a bit * less fancy than gdb or lldb, which is why it is not given priority. */ void *bt_buf[100]; int n_symbols; n_symbols = backtrace (bt_buf, 100); if (trace && n_symbols) { char **symbols; int i; symbols = backtrace_symbols (bt_buf, n_symbols); if (symbols) { for (i = 0; i < n_symbols; i++) { if (stream) g_fprintf (stream, "%s\n", (const gchar *) symbols[i]); if (trace) { if (! gtrace) gtrace = g_string_new (NULL); g_string_append (gtrace, (const gchar *) symbols[i]); g_string_append_c (gtrace, '\n'); } } free (symbols); } } else if (n_symbols) { /* This allows to generate traces without memory allocation. * In some cases, this is necessary, especially during * segfault-type crashes. */ backtrace_symbols_fd (bt_buf, n_symbols, fileno ((FILE *) stream)); } stack_printed = (n_symbols > 0); } #endif /* HAVE_EXECINFO_H */ if (trace) { if (gtrace) *trace = g_string_free (gtrace, FALSE); else *trace = NULL; } #endif /* G_OS_WIN32 */ return stack_printed; } /** * gimp_stack_trace_query: * @prog_name: the program to attach to. * * This is mostly the same as g_on_error_query() except that we use our * own backtrace function, much more complete. * @prog_name must be the current program name (argv[0]). * It does nothing on Win32. * * Since: 2.10 **/ void gimp_stack_trace_query (const gchar *prog_name) { #ifndef G_OS_WIN32 gchar buf[16]; retry: g_fprintf (stdout, "%s (pid:%u): %s: ", prog_name, (guint) getpid (), "[E]xit, show [S]tack trace or [P]roceed"); fflush (stdout); if (isatty(0) && isatty(1)) fgets (buf, 8, stdin); else strcpy (buf, "E\n"); if ((buf[0] == 'E' || buf[0] == 'e') && buf[1] == '\n') _exit (0); else if ((buf[0] == 'P' || buf[0] == 'p') && buf[1] == '\n') return; else if ((buf[0] == 'S' || buf[0] == 's') && buf[1] == '\n') { if (! gimp_stack_trace_print (prog_name, stdout, NULL)) g_fprintf (stderr, "%s\n", "Stack trace not available on your system."); goto retry; } else goto retry; #endif } /* Private functions. */ static gboolean gimp_utils_generic_available (const gchar *program, gint major, gint minor) { #ifndef G_OS_WIN32 pid_t pid; int out_fd[2]; if (pipe (out_fd) == -1) { return FALSE; } /* XXX: I don't use g_spawn_sync() or similar glib functions because * to read the contents of the stdout, these functions would allocate * memory dynamically. As we know, when debugging crashes, this is a * definite blocker. So instead I simply use a buffer on the stack * with a lower level fork() call. */ pid = fork (); if (pid == 0) { /* Child process. */ gchar *args[3] = { (gchar *) program, "--version", NULL }; /* Redirect the debugger output. */ dup2 (out_fd[1], STDOUT_FILENO); close (out_fd[0]); close (out_fd[1]); /* Run version check. */ execvp (args[0], args); _exit (-1); } else if (pid > 0) { /* Main process */ gchar buffer[256]; ssize_t read_n; int status; gint installed_major = 0; gint installed_minor = 0; gboolean major_reading = FALSE; gboolean minor_reading = FALSE; gint i; gchar c; waitpid (pid, &status, 0); if (! WIFEXITED (status) || WEXITSTATUS (status) != 0) return FALSE; /* It is important to close the writing side of the pipe, otherwise * the read() will wait forever without getting the information that * writing is finished. */ close (out_fd[1]); /* I could loop forever until EOL, but I am pretty sure the * version information is stored on the first line and one call to * read() with 256 characters should be more than enough. */ read_n = read (out_fd[0], buffer, 256); /* This is quite a very stupid parser. I only look for the first * numbers and consider them as version information. This works * fine for both GDB and LLDB as far as I can see for the output * of `${program} --version` but this should obviously not be * considered as a *really* generic version test. */ for (i = 0; i < read_n; i++) { c = buffer[i]; if (c >= '0' && c <= '9') { if (minor_reading) { installed_minor = 10 * installed_minor + (c - '0'); } else { major_reading = TRUE; installed_major = 10 * installed_major + (c - '0'); } } else if (c == '.') { if (major_reading) { minor_reading = TRUE; major_reading = FALSE; } else if (minor_reading) { break; } } else if (c == '\n') { /* Version information should be in the first line. */ break; } } close (out_fd[0]); return (installed_major > 0 && (installed_major > major || (installed_major == major && installed_minor >= minor))); } #endif /* Fork failed, or Win32. */ return FALSE; } static gboolean gimp_utils_gdb_available (gint major, gint minor) { return gimp_utils_generic_available ("gdb", major, minor); }