Add a find_source hook for extension languages to locate missing source files.
Commit Message
This is implemented in Python as `gdb.find_source_hook`. If that property exists
and is callable it will be called with the path to the missing source file,
and if the callable returns a string that will be used as the new path
to the source file.
This can be used to fetch missing source files from a VCS or over HTTP,
for example.
---
Thanks to Tom Tromey for pointing me to where to start with this. I'm interested
in using this to provide on-demand source fetching from Mozilla's public
Mercurial server over HTTP for debugging Nightly or Release builds, as well
as post-mortem debugging of crashes from our user population. Microsoft's
debuggers have this functionality built in and it's really useful for those
scenarios.
gdb/doc/python.texi | 18 +++++
gdb/extension-priv.h | 12 +++
gdb/extension.c | 37 +++++++++
gdb/extension.h | 2 +
gdb/python/python.c | 79 +++++++++++++++++++
gdb/source.c | 20 +++++
gdb/testsuite/gdb.python/py-find-source-hook.c | 20 +++++
gdb/testsuite/gdb.python/py-find-source-hook.exp | 96 ++++++++++++++++++++++++
gdb/testsuite/gdb.python/py-find-source-hook.py | 42 +++++++++++
9 files changed, 326 insertions(+)
create mode 100644 gdb/testsuite/gdb.python/py-find-source-hook.c
create mode 100644 gdb/testsuite/gdb.python/py-find-source-hook.exp
create mode 100644 gdb/testsuite/gdb.python/py-find-source-hook.py
Comments
On Wed, May 6, 2015 at 8:58 PM, Ted Mielczarek <ted@mielczarek.org> wrote:
Hi Ted,
sorry for delay. In general, your patch doesn't have changelog entry. Take
a look at https://sourceware.org/gdb/wiki/ContributionChecklist Also, you
need copyright assignment. See "6. FSF copyright Assignment" in the
checklist above.
> diff --git a/gdb/extension.c b/gdb/extension.c
> index dac203b..e0c8b01 100644
> --- a/gdb/extension.c
> +++ b/gdb/extension.c
> @@ -1060,6 +1060,43 @@ ext_lang_before_prompt (const char *current_gdb_prompt)
> }
> }
>
> +/* Iterate over the extension languages giving them a chance to
> + locate the source file in FILENAME. The first one to return
> + a result wins, and no further languages are tried.
> + If there was an error, or if no extension succeeds, then NULL is returned.
> +*/
Nit, move "*/" to the previous line.
> +
> +char *
> +ext_lang_find_source (const char* filename)
> +{
> + int i;
> + const struct extension_language_defn *extlang;
> +
> + ALL_ENABLED_EXTENSION_LANGUAGES (i, extlang)
> + {
> + char *result = NULL;
> + enum ext_lang_rc rc;
> +
> + if (extlang->ops->find_source == NULL)
> + continue;
> + rc = extlang->ops->find_source (extlang, filename, &result);
> + switch (rc)
> + {
> + case EXT_LANG_RC_OK:
> + gdb_assert (result != NULL);
As documented in doc/python.texi, result should be an absolute path, and
its base name should be same as filename's. Let us add two gdb_assert
to check these properties.
> + return result;
> + case EXT_LANG_RC_ERROR:
> + return NULL;
> + case EXT_LANG_RC_NOP:
> + break;
> + default:
> + gdb_assert_not_reached ("bad return from find_source");
> + }
> + }
> +
> + return NULL;
> +}
> +
> extern initialize_file_ftype _initialize_extension;
>
> +/* This is the extension_language_ops.find_source "method". */
> +
> +static enum ext_lang_rc
> +gdbpy_find_source_hook (const struct extension_language_defn *extlang,
> + const char *filename,
> + char **found_filename)
The indentation looks odd.
>
>
> /* Printing. */
> diff --git a/gdb/source.c b/gdb/source.c
> index fbec0f1..8bee855 100644
> --- a/gdb/source.c
> +++ b/gdb/source.c
> @@ -20,6 +20,7 @@
> #include "arch-utils.h"
> #include "symtab.h"
> #include "expression.h"
> +#include "extension.h"
> #include "language.h"
> #include "command.h"
> #include "source.h"
> @@ -1100,6 +1101,25 @@ find_and_open_source (const char *filename,
> OPEN_MODE, fullname);
> }
>
> + if (result < 0)
> + {
> + /* Didn't work. Ask extension languages if they can find it. */
> + char *found_filename = ext_lang_find_source (filename);
> +
> + if (found_filename != NULL)
> + {
> + result = gdb_open_cloexec (found_filename, OPEN_MODE, 0);
> + if (result >= 0)
> + {
> + *fullname = found_filename;
> + }
> + else
> + {
> + make_cleanup (xfree, found_filename);
If there is something wrong, we can just xfree foud_filename immediately. Don't
have to append the xfree in to the cleanup chain.
> diff --git a/gdb/testsuite/gdb.python/py-find-source-hook.c b/gdb/testsuite/gdb.python/py-find-source-hook.c
> new file mode 100644
> index 0000000..50db568
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-find-source-hook.c
> @@ -0,0 +1,20 @@
> +/* This testcase is part of GDB, the GNU debugger.
> +
> + Copyright 2011-2015 Free Software Foundation, Inc.
2016
> +
> + 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/>. */
> +
> +
> +int main (int argc, char** argv) { return 0; }
> +
> diff --git a/gdb/testsuite/gdb.python/py-find-source-hook.exp b/gdb/testsuite/gdb.python/py-find-source-hook.exp
> new file mode 100644
> index 0000000..a63b0f8
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-find-source-hook.exp
> @@ -0,0 +1,96 @@
> +# Copyright (C) 2015 Free Software Foundation, Inc.
> +
2016
> +# 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/>.
> +
> +if [target_info exists use_gdb_stub] {
> + return 0
> +}
Why do you skip the test like this?
> +
> +load_lib gdb-python.exp
> +
> +standard_testfile
> +
> +set orig_srcfile $srcdir/$subdir/$testfile.c
> +# Copy the source file to the test directory so
> +# we can remove it to test the find_source hook.
> +set srcfile [standard_output_file $testfile.c]
> +set result [catch "exec cp $orig_srcfile $srcfile" output]
> +if {$result == 1} {
> + return -1
> +}
It doesn't work in remote-host testing, in which gdb is run on a different
machine other than the machine gdb is built.
The rest of test should be reviewed, but I can not finish the review now.
I am in a travel now, and hopefully I can finish the review next week.
On Tue, Sep 27, 2016, at 03:46 PM, Yao Qi wrote:
> On Wed, May 6, 2015 at 8:58 PM, Ted Mielczarek <ted@mielczarek.org>
> wrote:
>
> Hi Ted,
> sorry for delay. In general, your patch doesn't have changelog entry.
> Take
> a look at https://sourceware.org/gdb/wiki/ContributionChecklist Also,
> you
> need copyright assignment. See "6. FSF copyright Assignment" in the
> checklist above.
I filed a FSF copyright assignment years ago for contributing to wget.
Presumably that still applies?
-Ted
@@ -441,6 +441,24 @@ such as those used by readline for command input, and annotation
related prompts are prohibited from being changed.
@end defun
+@defun gdb.find_source_hook (source_file)
+@anchor{find_source_hook}
+
+If @var{find_source_hook} is callable, @value{GDBN} will call the method
+assigned to this operation when it cannot locate a requested source file.
+
+The parameter @code{source_file} contains the path to the source file
+that @value{GDBN} cannot locate, as provided by the debug information.
+This method must return a Python string or @code{None}. If a string is
+returned, it must be an absolute path to the source file. If @code{None}
+is returned, @value{GDBN} will proceed as usual without source. The path
+returned must have the same basename as @code{source_file} or GDB will not
+be able to match debug information to source.
+
+This hook may be used to fetch source on-demand from a version control system
+or other location.
+@end defun
+
@node Exception Handling
@subsubsection Exception Handling
@cindex python exceptions
@@ -262,6 +262,18 @@ struct extension_language_ops
enum ext_lang_rc (*before_prompt) (const struct extension_language_defn *,
const char *current_gdb_prompt);
+ /* Called when gdb cannot locate a source file, giving extension languages an
+ opportunity to locate the file and provide a path.
+ If successful the found path is stored in *FOUND_FILENAME, and the caller
+ must free it.
+ Returns EXT_LANG_RC_OK if a path was found, EXT_LANG_RC_NOP if no path
+ was found, and EXT_LANG_RC_ERROR if an error was encountered.
+ Extension languages are called in order, and once a path is returned
+ or an error occurs no further languages are called. */
+ enum ext_lang_rc (*find_source) (const struct extension_language_defn *,
+ const char *filename,
+ char **found_filename);
+
/* xmethod support:
clone_xmethod_worker_data, free_xmethod_worker_data,
get_matching_xmethod_workers, get_xmethod_arg_types,
@@ -1060,6 +1060,43 @@ ext_lang_before_prompt (const char *current_gdb_prompt)
}
}
+/* Iterate over the extension languages giving them a chance to
+ locate the source file in FILENAME. The first one to return
+ a result wins, and no further languages are tried.
+ If there was an error, or if no extension succeeds, then NULL is returned.
+*/
+
+char *
+ext_lang_find_source (const char* filename)
+{
+ int i;
+ const struct extension_language_defn *extlang;
+
+ ALL_ENABLED_EXTENSION_LANGUAGES (i, extlang)
+ {
+ char *result = NULL;
+ enum ext_lang_rc rc;
+
+ if (extlang->ops->find_source == NULL)
+ continue;
+ rc = extlang->ops->find_source (extlang, filename, &result);
+ switch (rc)
+ {
+ case EXT_LANG_RC_OK:
+ gdb_assert (result != NULL);
+ return result;
+ case EXT_LANG_RC_ERROR:
+ return NULL;
+ case EXT_LANG_RC_NOP:
+ break;
+ default:
+ gdb_assert_not_reached ("bad return from find_source");
+ }
+ }
+
+ return NULL;
+}
+
extern initialize_file_ftype _initialize_extension;
void
@@ -264,4 +264,6 @@ extern struct type *get_xmethod_result_type (struct xmethod_worker *,
struct value *object,
struct value **args, int nargs);
+extern char *ext_lang_find_source (const char* filename);
+
#endif /* EXTENSION_H */
@@ -149,6 +149,9 @@ static void gdbpy_set_quit_flag (const struct extension_language_defn *);
static int gdbpy_check_quit_flag (const struct extension_language_defn *);
static enum ext_lang_rc gdbpy_before_prompt_hook
(const struct extension_language_defn *, const char *current_gdb_prompt);
+static enum ext_lang_rc gdbpy_find_source_hook
+(const struct extension_language_defn *, const char *filename,
+ char **found_filename);
/* The interface between gdb proper and loading of python scripts. */
@@ -187,6 +190,7 @@ const struct extension_language_ops python_extension_ops =
gdbpy_check_quit_flag,
gdbpy_before_prompt_hook,
+ gdbpy_find_source_hook,
gdbpy_clone_xmethod_worker_data,
gdbpy_free_xmethod_worker_data,
@@ -1104,6 +1108,81 @@ gdbpy_before_prompt_hook (const struct extension_language_defn *extlang,
return EXT_LANG_RC_ERROR;
}
+/* This is the extension_language_ops.find_source "method". */
+
+static enum ext_lang_rc
+gdbpy_find_source_hook (const struct extension_language_defn *extlang,
+ const char *filename,
+ char **found_filename)
+{
+ struct cleanup *cleanup;
+ char *result = NULL;
+
+ if (!gdb_python_initialized)
+ return EXT_LANG_RC_NOP;
+
+ cleanup = ensure_python_env (get_current_arch (), current_language);
+
+ if (gdb_python_module
+ && PyObject_HasAttrString (gdb_python_module, "find_source_hook"))
+ {
+ PyObject *hook;
+
+ hook = PyObject_GetAttrString (gdb_python_module, "find_source_hook");
+ if (hook == NULL)
+ goto fail;
+
+ make_cleanup_py_decref (hook);
+
+ if (PyCallable_Check (hook))
+ {
+ PyObject *result_obj;
+ PyObject *in_filename;
+
+ in_filename = PyString_FromString (filename);
+ if (in_filename == NULL)
+ goto fail;
+
+ result_obj = PyObject_CallFunctionObjArgs (hook, in_filename, NULL);
+
+ Py_DECREF (in_filename);
+
+ if (result_obj == NULL)
+ goto fail;
+
+ make_cleanup_py_decref (result_obj);
+
+ /* Return type should be None, or a String. If it is None,
+ fall through. If it is a string, set FOUND_FILENAME.
+ Anything else, set an exception. */
+ if (result_obj != Py_None && ! PyString_Check (result_obj))
+ {
+ PyErr_Format (PyExc_RuntimeError,
+ _("Return from find_source_hook must " \
+ "be either a Python string, or None"));
+ goto fail;
+ }
+
+ if (result_obj != Py_None)
+ {
+ result = python_string_to_host_string (result_obj);
+ if (result == NULL)
+ goto fail;
+
+ *found_filename = result;
+ }
+ }
+ }
+
+ do_cleanups (cleanup);
+ return result != NULL ? EXT_LANG_RC_OK : EXT_LANG_RC_NOP;
+
+ fail:
+ gdbpy_print_stack ();
+ do_cleanups (cleanup);
+ return EXT_LANG_RC_ERROR;
+}
+
/* Printing. */
@@ -20,6 +20,7 @@
#include "arch-utils.h"
#include "symtab.h"
#include "expression.h"
+#include "extension.h"
#include "language.h"
#include "command.h"
#include "source.h"
@@ -1100,6 +1101,25 @@ find_and_open_source (const char *filename,
OPEN_MODE, fullname);
}
+ if (result < 0)
+ {
+ /* Didn't work. Ask extension languages if they can find it. */
+ char *found_filename = ext_lang_find_source (filename);
+
+ if (found_filename != NULL)
+ {
+ result = gdb_open_cloexec (found_filename, OPEN_MODE, 0);
+ if (result >= 0)
+ {
+ *fullname = found_filename;
+ }
+ else
+ {
+ make_cleanup (xfree, found_filename);
+ }
+ }
+ }
+
do_cleanups (cleanup);
return result;
}
new file mode 100644
@@ -0,0 +1,20 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2011-2015 Free Software Foundation, 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 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/>. */
+
+
+int main (int argc, char** argv) { return 0; }
+
new file mode 100644
@@ -0,0 +1,96 @@
+# Copyright (C) 2015 Free Software Foundation, 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 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/>.
+
+if [target_info exists use_gdb_stub] {
+ return 0
+}
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+set orig_srcfile $srcdir/$subdir/$testfile.c
+# Copy the source file to the test directory so
+# we can remove it to test the find_source hook.
+set srcfile [standard_output_file $testfile.c]
+set result [catch "exec cp $orig_srcfile $srcfile" output]
+if {$result == 1} {
+ return -1
+}
+
+if { [prepare_for_testing ${testfile}.exp ${binfile} ${srcfile}] } {
+ return -1
+}
+
+set pyfile ${srcdir}/${subdir}/${testfile}.py
+
+if { [skip_python_tests] } { continue }
+
+gdb_test_no_output "python exec (open ('${pyfile}').read ())" ""
+
+gdb_py_test_silent_cmd "python gdb.find_source_hook = find_source_do_nothing" "set find_source_hook" 0
+
+# Since we haven't moved the source file yet, the hook should not be
+# called.
+gdb_test "python print(do_nothing_calls)" "0"
+gdb_test "list 1" "10 This program is distributed in the hope that it will be useful," "find_source_do_nothing not called"
+gdb_test "python print(do_nothing_calls)" "0"
+
+# Now remove the source file so gdb can't find it.
+set result [catch "exec rm $srcfile" output]
+if {$result == 1} {
+ return -1
+}
+
+# Reset dir to reset file paths.
+# Note: do not use $subdir here because otherwise we'd find the file there!
+gdb_reinitialize_dir $srcdir
+
+# The hook should be invoked here.
+gdb_test "list 1" "1\[ \t\]+$srcfile: No such file or directory." "find_source_do_nothing gets called"
+# It actually gets invoked 3 times when running list, but I'd rather
+# not depend on that exact number.
+gdb_test "python print(do_nothing_calls > 0)" "True"
+
+# Check that a hook that raises prints an exception
+gdb_reinitialize_dir $srcdir
+
+gdb_py_test_silent_cmd "python gdb.find_source_hook = find_source_raise" "set find_source_hook" 0
+gdb_test "list 1" "Python Exception <type 'exceptions.Exception'> oops: +\r\n1\[ \t\]+$srcfile: No such file or directory." "find_source_raise output"
+
+# Check that a hook that returns a non-string prints an error.
+gdb_reinitialize_dir $srcdir
+
+gdb_py_test_silent_cmd "python gdb.find_source_hook = find_source_bad_return" "set find_source_hook" 0
+gdb_test "list 1" "Python Exception <type 'exceptions.RuntimeError'> Return from find_source_hook must be either a Python string, or None: +\r\n1\[ \t\]+$srcfile: No such file or directory." "find_source_bad_return output"
+
+# Finally check that a hook that returns a new, valid path works.
+gdb_reinitialize_dir $srcdir
+gdb_py_test_silent_cmd "python new_path = '$orig_srcfile'" "set find_source_hook" 0
+gdb_py_test_silent_cmd "python gdb.find_source_hook = find_source_new_path" "set find_source_hook" 0
+gdb_test "python print(new_path_calls)" "0"
+# Should get the source back now
+gdb_test "list 1" "10 This program is distributed in the hope that it will be useful," "find_source_new_path works"
+# This is 2 in my testing, but again I would rather not rely on that.
+gdb_test "python print(new_path_calls > 0)" "True"
+set new_path_calls [get_python_valueof "new_path_calls" "None"]
+if { ${new_path_calls} == "None" } {
+ fail "new_path_calls not found"
+}
+# Check that the returned path will be cached.
+gdb_test "list 1" "10 This program is distributed in the hope that it will be useful," "find_source_new_path return value gets cached"
+if { [get_python_valueof "new_path_calls" "None"] != ${new_path_calls} } {
+ fail "find_source hook shouldn't have been called again"
+}
new file mode 100644
@@ -0,0 +1,42 @@
+# Copyright (C) 2010-2015 Free Software Foundation, 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 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/>.
+
+# This file is part of the GDB testsuite. It tests python's find_source_hook.
+import gdb
+
+# This gets set by the test script.
+new_path = None
+
+# Number of calls to do_nothing
+do_nothing_calls = 0
+
+# Number of calls to new_path
+new_path_calls = 0
+
+def find_source_do_nothing (source):
+ global do_nothing_calls
+ do_nothing_calls += 1
+
+def find_source_raise (source):
+ raise Exception("oops")
+
+def find_source_bad_return (source):
+ return 1
+
+def find_source_new_path (source):
+ global new_path_calls
+ new_path_calls += 1
+ global new_path
+ return new_path