| /* debuginfod utilities for GDB. | |
| Copyright (C) 2020-2025 Free Software Foundation, Inc. | |
| This file is part of GDB. | |
| 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 3 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, see <http://www.gnu.org/licenses/>. */ | |
| #include "diagnostics.h" | |
| #include <errno.h> | |
| #include "gdbsupport/cleanups.h" | |
| #include "gdbsupport/scoped_fd.h" | |
| #include "debuginfod-support.h" | |
| #include <optional> | |
| #include "cli/cli-cmds.h" | |
| #include "cli/cli-style.h" | |
| #include "cli-out.h" | |
| #include "target.h" | |
| /* Set/show debuginfod commands. */ | |
| static cmd_list_element *set_debuginfod_prefix_list; | |
| static cmd_list_element *show_debuginfod_prefix_list; | |
| /* maint set/show debuginfod commands. */ | |
| static cmd_list_element *maint_set_debuginfod_cmdlist; | |
| static cmd_list_element *maint_show_debuginfod_cmdlist; | |
| static const char debuginfod_on[] = "on"; | |
| static const char debuginfod_off[] = "off"; | |
| static const char debuginfod_ask[] = "ask"; | |
| static const char *debuginfod_enabled_enum[] = | |
| { | |
| debuginfod_on, | |
| debuginfod_off, | |
| debuginfod_ask, | |
| nullptr | |
| }; | |
| static const char *debuginfod_enabled = | |
| #if defined(HAVE_LIBDEBUGINFOD) | |
| debuginfod_ask; | |
| #else | |
| debuginfod_off; | |
| #endif | |
| /* Controls whether ELF/DWARF section downloading is enabled. */ | |
| static bool debuginfod_download_sections = | |
| #if defined(HAVE_LIBDEBUGINFOD_FIND_SECTION) | |
| true; | |
| #else | |
| false; | |
| #endif | |
| static unsigned int debuginfod_verbose = 1; | |
| #ifndef HAVE_LIBDEBUGINFOD | |
| scoped_fd | |
| debuginfod_source_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *srcpath, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| return scoped_fd (-ENOSYS); | |
| } | |
| scoped_fd | |
| debuginfod_debuginfo_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| return scoped_fd (-ENOSYS); | |
| } | |
| scoped_fd | |
| debuginfod_exec_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| return scoped_fd (-ENOSYS); | |
| } | |
| scoped_fd | |
| debuginfod_section_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| const char *section_name, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| return scoped_fd (-ENOSYS); | |
| } | |
| #define NO_IMPL _("Support for debuginfod is not compiled into GDB.") | |
| #else | |
| #include <elfutils/debuginfod.h> | |
| struct user_data | |
| { | |
| user_data (const char *desc, const char *fname) | |
| : desc (desc), fname (fname) | |
| { } | |
| const char * const desc; | |
| const char * const fname; | |
| ui_out::progress_update progress; | |
| }; | |
| /* Convert SIZE into a unit suitable for use with progress updates. | |
| SIZE should in given in bytes and will be converted into KB, MB, GB | |
| or remain unchanged. UNIT will be set to "B", "KB", "MB" or "GB" | |
| accordingly. */ | |
| static const char * | |
| get_size_and_unit (double &size) | |
| { | |
| if (size < 1024) | |
| /* If size is less than 1 KB then set unit to B. */ | |
| return "B"; | |
| size /= 1024; | |
| if (size < 1024) | |
| /* If size is less than 1 MB then set unit to KB. */ | |
| return "K"; | |
| size /= 1024; | |
| if (size < 1024) | |
| /* If size is less than 1 GB then set unit to MB. */ | |
| return "M"; | |
| size /= 1024; | |
| return "G"; | |
| } | |
| static int | |
| progressfn (debuginfod_client *c, long cur, long total) | |
| { | |
| user_data *data = static_cast<user_data *> (debuginfod_get_user_data (c)); | |
| gdb_assert (data != nullptr); | |
| string_file styled_fname (current_uiout->can_emit_style_escape ()); | |
| fprintf_styled (&styled_fname, file_name_style.style (), "%s", | |
| data->fname); | |
| if (check_quit_flag ()) | |
| { | |
| ui_file *outstream = get_unbuffered (gdb_stdout); | |
| gdb_printf (outstream, _("Cancelling download of %s %s...\n"), | |
| data->desc, styled_fname.c_str ()); | |
| return 1; | |
| } | |
| if (debuginfod_verbose == 0) | |
| return 0; | |
| /* Print progress update. Include the transfer size if available. */ | |
| if (total > 0) | |
| { | |
| /* Transfer size is known. */ | |
| double howmuch = (double) cur / (double) total; | |
| if (howmuch >= 0.0 && howmuch <= 1.0) | |
| { | |
| double d_total = (double) total; | |
| const char *unit = get_size_and_unit (d_total); | |
| std::string msg = string_printf ("Downloading %0.2f %s %s %s", | |
| d_total, unit, data->desc, | |
| styled_fname.c_str ()); | |
| data->progress.update_progress (msg, unit, howmuch, d_total); | |
| return 0; | |
| } | |
| } | |
| std::string msg = string_printf ("Downloading %s %s", | |
| data->desc, styled_fname.c_str ()); | |
| data->progress.update_progress (msg); | |
| return 0; | |
| } | |
| /* Return a pointer to the single global debuginfod_client, initialising it | |
| first if needed. */ | |
| static debuginfod_client * | |
| get_debuginfod_client () | |
| { | |
| static debuginfod_client *global_client = nullptr; | |
| if (global_client == nullptr) | |
| { | |
| global_client = debuginfod_begin (); | |
| if (global_client != nullptr) | |
| { | |
| /* It is important that we cleanup the debuginfod_client object | |
| before calling exit. Some of the libraries used by debuginfod | |
| make use of at_exit handlers to perform cleanup. | |
| If we wrapped the debuginfod_client in a unique_ptr and relied | |
| on its destructor to cleanup then this would be run as part of | |
| the global C++ object destructors, which is after the at_exit | |
| handlers, which is too late. | |
| So instead, we make use of GDB's final cleanup mechanism. */ | |
| add_final_cleanup ([] () | |
| { | |
| debuginfod_end (global_client); | |
| }); | |
| debuginfod_set_progressfn (global_client, progressfn); | |
| } | |
| } | |
| return global_client; | |
| } | |
| /* Check if debuginfod is enabled. If configured to do so, ask the user | |
| whether to enable debuginfod. */ | |
| static bool | |
| debuginfod_is_enabled () | |
| { | |
| const char *urls = skip_spaces (getenv (DEBUGINFOD_URLS_ENV_VAR)); | |
| if (debuginfod_enabled == debuginfod_off | |
| || urls == nullptr | |
| || *urls == '\0') | |
| return false; | |
| if (debuginfod_enabled == debuginfod_ask) | |
| { | |
| gdb_printf (_("\nThis GDB supports auto-downloading debuginfo " \ | |
| "from the following URLs:\n")); | |
| std::string_view url_view (urls); | |
| while (true) | |
| { | |
| size_t off = url_view.find_first_not_of (' '); | |
| if (off == std::string_view::npos) | |
| break; | |
| url_view = url_view.substr (off); | |
| /* g++ 11.2.1 on s390x, g++ 11.3.1 on ppc64le and g++ 11 on | |
| hppa seem convinced url_view might be of SIZE_MAX length. | |
| And so complains because the length of an array can only | |
| be PTRDIFF_MAX. */ | |
| DIAGNOSTIC_PUSH | |
| DIAGNOSTIC_IGNORE_STRINGOP_OVERREAD | |
| off = url_view.find_first_of (' '); | |
| DIAGNOSTIC_POP | |
| gdb_printf | |
| (_(" <%ps>\n"), | |
| styled_string (file_name_style.style (), | |
| std::string (url_view.substr (0, off)).c_str ())); | |
| if (off == std::string_view::npos) | |
| break; | |
| url_view = url_view.substr (off); | |
| } | |
| int resp = nquery (_("Enable debuginfod for this session? ")); | |
| if (!resp) | |
| { | |
| gdb_printf (_("Debuginfod has been disabled.\nTo make this " \ | |
| "setting permanent, add \'set debuginfod " \ | |
| "enabled off\' to .gdbinit.\n")); | |
| debuginfod_enabled = debuginfod_off; | |
| return false; | |
| } | |
| gdb_printf (_("Debuginfod has been enabled.\nTo make this " \ | |
| "setting permanent, add \'set debuginfod enabled " \ | |
| "on\' to .gdbinit.\n")); | |
| debuginfod_enabled = debuginfod_on; | |
| } | |
| return true; | |
| } | |
| /* Print the result of the most recent attempted download. */ | |
| static void | |
| print_outcome (int fd, const char *desc, const char *fname) | |
| { | |
| if (fd < 0 && fd != -ENOENT) | |
| { | |
| ui_file *outstream = get_unbuffered (gdb_stdout); | |
| gdb_printf (outstream, | |
| _("Download failed: %s. Continuing without %s %ps.\n"), | |
| safe_strerror (-fd), | |
| desc, | |
| styled_string (file_name_style.style (), fname)); | |
| } | |
| } | |
| /* See debuginfod-support.h */ | |
| scoped_fd | |
| debuginfod_source_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *srcpath, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| if (!debuginfod_is_enabled ()) | |
| return scoped_fd (-ENOSYS); | |
| debuginfod_client *c = get_debuginfod_client (); | |
| if (c == nullptr) | |
| return scoped_fd (-ENOMEM); | |
| char *dname = nullptr; | |
| scoped_fd fd; | |
| std::optional<target_terminal::scoped_restore_terminal_state> term_state; | |
| { | |
| user_data data ("source file", srcpath); | |
| debuginfod_set_user_data (c, &data); | |
| if (target_supports_terminal_ours ()) | |
| { | |
| term_state.emplace (); | |
| target_terminal::ours (); | |
| } | |
| fd = scoped_fd (debuginfod_find_source (c, | |
| build_id, | |
| build_id_len, | |
| srcpath, | |
| &dname)); | |
| debuginfod_set_user_data (c, nullptr); | |
| } | |
| print_outcome (fd.get (), "source file", srcpath); | |
| if (fd.get () >= 0) | |
| destname->reset (dname); | |
| return fd; | |
| } | |
| /* See debuginfod-support.h */ | |
| scoped_fd | |
| debuginfod_debuginfo_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| if (!debuginfod_is_enabled ()) | |
| return scoped_fd (-ENOSYS); | |
| debuginfod_client *c = get_debuginfod_client (); | |
| if (c == nullptr) | |
| return scoped_fd (-ENOMEM); | |
| char *dname = nullptr; | |
| scoped_fd fd; | |
| std::optional<target_terminal::scoped_restore_terminal_state> term_state; | |
| { | |
| user_data data ("separate debug info for", filename); | |
| debuginfod_set_user_data (c, &data); | |
| if (target_supports_terminal_ours ()) | |
| { | |
| term_state.emplace (); | |
| target_terminal::ours (); | |
| } | |
| fd = scoped_fd (debuginfod_find_debuginfo (c, build_id, build_id_len, | |
| &dname)); | |
| debuginfod_set_user_data (c, nullptr); | |
| } | |
| print_outcome (fd.get (), "separate debug info for", filename); | |
| if (fd.get () >= 0) | |
| destname->reset (dname); | |
| return fd; | |
| } | |
| /* See debuginfod-support.h */ | |
| scoped_fd | |
| debuginfod_exec_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| if (!debuginfod_is_enabled ()) | |
| return scoped_fd (-ENOSYS); | |
| debuginfod_client *c = get_debuginfod_client (); | |
| if (c == nullptr) | |
| return scoped_fd (-ENOMEM); | |
| char *dname = nullptr; | |
| scoped_fd fd; | |
| std::optional<target_terminal::scoped_restore_terminal_state> term_state; | |
| { | |
| user_data data ("file", filename); | |
| debuginfod_set_user_data (c, &data); | |
| if (target_supports_terminal_ours ()) | |
| { | |
| term_state.emplace (); | |
| target_terminal::ours (); | |
| } | |
| fd = scoped_fd (debuginfod_find_executable (c, build_id, build_id_len, | |
| &dname)); | |
| debuginfod_set_user_data (c, nullptr); | |
| } | |
| print_outcome (fd.get (), "file", filename); | |
| if (fd.get () >= 0) | |
| destname->reset (dname); | |
| return fd; | |
| } | |
| /* See debuginfod-support.h */ | |
| scoped_fd | |
| debuginfod_section_query (const unsigned char *build_id, | |
| int build_id_len, | |
| const char *filename, | |
| const char *section_name, | |
| gdb::unique_xmalloc_ptr<char> *destname) | |
| { | |
| #if !defined (HAVE_LIBDEBUGINFOD_FIND_SECTION) | |
| return scoped_fd (-ENOSYS); | |
| #else | |
| if (!debuginfod_download_sections || !debuginfod_is_enabled ()) | |
| return scoped_fd (-ENOSYS); | |
| debuginfod_client *c = get_debuginfod_client (); | |
| if (c == nullptr) | |
| return scoped_fd (-ENOMEM); | |
| char *dname = nullptr; | |
| std::string desc = std::string ("section ") + section_name + " for"; | |
| scoped_fd fd; | |
| std::optional<target_terminal::scoped_restore_terminal_state> term_state; | |
| { | |
| user_data data (desc.c_str (), filename); | |
| debuginfod_set_user_data (c, &data); | |
| if (target_supports_terminal_ours ()) | |
| { | |
| term_state.emplace (); | |
| target_terminal::ours (); | |
| } | |
| fd = scoped_fd (debuginfod_find_section (c, build_id, build_id_len, | |
| section_name, &dname)); | |
| debuginfod_set_user_data (c, nullptr); | |
| } | |
| print_outcome (fd.get (), desc.c_str (), filename); | |
| gdb_assert (destname != nullptr); | |
| if (fd.get () >= 0) | |
| destname->reset (dname); | |
| return fd; | |
| #endif /* HAVE_LIBDEBUGINFOD_FIND_SECTION */ | |
| } | |
| #endif | |
| /* Set callback for "set debuginfod enabled". */ | |
| static void | |
| set_debuginfod_enabled (const char *value) | |
| { | |
| #if defined(HAVE_LIBDEBUGINFOD) | |
| debuginfod_enabled = value; | |
| #else | |
| /* Disabling debuginfod when gdb is not built with it is a no-op. */ | |
| if (value != debuginfod_off) | |
| error (NO_IMPL); | |
| #endif | |
| } | |
| /* Get callback for "set debuginfod enabled". */ | |
| static const char * | |
| get_debuginfod_enabled () | |
| { | |
| return debuginfod_enabled; | |
| } | |
| /* Show callback for "set debuginfod enabled". */ | |
| static void | |
| show_debuginfod_enabled (ui_file *file, int from_tty, cmd_list_element *cmd, | |
| const char *value) | |
| { | |
| gdb_printf (file, | |
| _("Debuginfod functionality is currently set to " | |
| "\"%s\".\n"), debuginfod_enabled); | |
| } | |
| /* Set callback for "set debuginfod urls". */ | |
| static void | |
| set_debuginfod_urls (const std::string &urls) | |
| { | |
| #if defined(HAVE_LIBDEBUGINFOD) | |
| if (setenv (DEBUGINFOD_URLS_ENV_VAR, urls.c_str (), 1) != 0) | |
| warning (_("Unable to set debuginfod URLs: %s"), safe_strerror (errno)); | |
| #else | |
| error (NO_IMPL); | |
| #endif | |
| } | |
| /* Get callback for "set debuginfod urls". */ | |
| static const std::string& | |
| get_debuginfod_urls () | |
| { | |
| static std::string urls; | |
| #if defined(HAVE_LIBDEBUGINFOD) | |
| const char *envvar = getenv (DEBUGINFOD_URLS_ENV_VAR); | |
| if (envvar != nullptr) | |
| urls = envvar; | |
| else | |
| urls.clear (); | |
| #endif | |
| return urls; | |
| } | |
| /* Show callback for "set debuginfod urls". */ | |
| static void | |
| show_debuginfod_urls (ui_file *file, int from_tty, cmd_list_element *cmd, | |
| const char *value) | |
| { | |
| if (value[0] == '\0') | |
| gdb_printf (file, _("Debuginfod URLs have not been set.\n")); | |
| else | |
| gdb_printf (file, _("Debuginfod URLs are currently set to:\n%s\n"), | |
| value); | |
| } | |
| /* Show callback for "set debuginfod verbose". */ | |
| static void | |
| show_debuginfod_verbose_command (ui_file *file, int from_tty, | |
| cmd_list_element *cmd, const char *value) | |
| { | |
| gdb_printf (file, _("Debuginfod verbose output is set to %s.\n"), | |
| value); | |
| } | |
| /* Set callback for "maint set debuginfod download-sections". */ | |
| static void | |
| maint_set_debuginfod_download_sections (bool value) | |
| { | |
| #if !defined(HAVE_LIBDEBUGINFOD_FIND_SECTION) | |
| if (value) | |
| error (_("Support for section downloading is not compiled into GDB. " \ | |
| "Defaulting to \"off\".")); | |
| #endif | |
| debuginfod_download_sections = value; | |
| } | |
| /* Get callback for "maint set debuginfod download-sections". */ | |
| static bool | |
| maint_get_debuginfod_download_sections () | |
| { | |
| return debuginfod_download_sections; | |
| } | |
| /* Register debuginfod commands. */ | |
| INIT_GDB_FILE (debuginfod) | |
| { | |
| /* set/show debuginfod */ | |
| add_setshow_prefix_cmd ("debuginfod", class_run, | |
| _("Set debuginfod options."), | |
| _("Show debuginfod options."), | |
| &set_debuginfod_prefix_list, | |
| &show_debuginfod_prefix_list, | |
| &setlist, &showlist); | |
| add_setshow_enum_cmd ("enabled", class_run, debuginfod_enabled_enum, | |
| _("Set whether to use debuginfod."), | |
| _("Show whether to use debuginfod."), | |
| _("\ | |
| When set to \"on\", enable the use of debuginfod to download missing\n\ | |
| debug info and source files. GDB may also download components of debug\n\ | |
| info instead of entire files. \"off\" disables the use of debuginfod.\n\ | |
| When set to \"ask\", prompt whether to enable or disable debuginfod." ), | |
| set_debuginfod_enabled, | |
| get_debuginfod_enabled, | |
| show_debuginfod_enabled, | |
| &set_debuginfod_prefix_list, | |
| &show_debuginfod_prefix_list); | |
| /* set/show debuginfod urls */ | |
| add_setshow_string_noescape_cmd ("urls", class_run, _("\ | |
| Set the list of debuginfod server URLs."), _("\ | |
| Show the list of debuginfod server URLs."), _("\ | |
| Manage the space-separated list of debuginfod server URLs that GDB will\n\ | |
| query when missing debuginfo, executables or source files.\n\ | |
| The default value is copied from the DEBUGINFOD_URLS environment variable."), | |
| set_debuginfod_urls, | |
| get_debuginfod_urls, | |
| show_debuginfod_urls, | |
| &set_debuginfod_prefix_list, | |
| &show_debuginfod_prefix_list); | |
| /* set/show debuginfod verbose */ | |
| add_setshow_zuinteger_cmd ("verbose", class_support, | |
| &debuginfod_verbose, _("\ | |
| Set verbosity of debuginfod output."), _("\ | |
| Show debuginfod debugging."), _("\ | |
| When set to a non-zero value, display verbose output for each debuginfod \ | |
| query.\nTo disable, set to zero. Verbose output is displayed by default."), | |
| nullptr, | |
| show_debuginfod_verbose_command, | |
| &set_debuginfod_prefix_list, | |
| &show_debuginfod_prefix_list); | |
| /* maint set/show debuginfod. */ | |
| add_setshow_prefix_cmd ("debuginfod", class_maintenance, | |
| _("Set debuginfod specific variables."), | |
| _("Show debuginfod specific variables."), | |
| &maint_set_debuginfod_cmdlist, | |
| &maint_show_debuginfod_cmdlist, | |
| &maintenance_set_cmdlist, &maintenance_show_cmdlist); | |
| /* maint set/show debuginfod download-sections. */ | |
| add_setshow_boolean_cmd ("download-sections", class_maintenance, _("\ | |
| Set whether debuginfod may download individual ELF/DWARF sections."), _("\ | |
| Show whether debuginfod may download individual ELF/DWARF sections."), _("\ | |
| When enabled, debuginfod may attempt to download individual ELF/DWARF\n\ | |
| sections from debug info files.\n\ | |
| If disabled, only whole debug info files may be downloaded."), | |
| maint_set_debuginfod_download_sections, | |
| maint_get_debuginfod_download_sections, | |
| nullptr, | |
| &maint_set_debuginfod_cmdlist, | |
| &maint_show_debuginfod_cmdlist); | |
| } |