Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: "J. Johnston" <jjohnstn@redhat.com>
To: gdb-patches@sources.redhat.com
Subject: Re: RFA: gdb linux nptl patch 1
Date: Thu, 05 Jun 2003 18:35:00 -0000	[thread overview]
Message-ID: <3EDF8D55.5000902@redhat.com> (raw)
In-Reply-To: <3E84E672.3050506@redhat.com>

Has anybody had a chance to look at this?

-- Jeff J.

J. Johnston wrote:
> The following is the first of a set of proposed patches for gdb to support
> the new nptl threading model for linux.
> 
> In the old linuxthreads model, the lwp was equal to the pid.  In the new
> nptl model, the lwp is unique for each thread and the pid value is common
> among sibling threads.  A side-effect of this difference is that with 
> linuxthreads,
> you could see individual threads externally on the ps list.  With the new
> nptl kernel, you will not be able to see child thread lwps on the list.
> 
> Another key difference is how a tid is represented.  In the linuxthreads 
> model,
> a tid was an index into a table and was restricted to 16-bits.  In the 
> new nptl
> model, there is no such restriction on the number of threads and the tid 
> is in
> fact an address (i.e. not 16 bits).  In the current linux-proc.c logic, 
> when
> performing a gcore command, pr_status note information is created by 
> taking the pid and
> concatenating this to the tid to form a 32-bit construct.  Obviously, in 
> the new
> nptl model this won't work as the tid itself is either 32-bits or 64-bits.
> It should also be mentioned that this logic does not match what the kernel
> spits out in a corefile.  For linuxthreads, the kernel is just putting the
> lwp = pid in the pr_status notes.
> 
> Logic that works for either model and matches what the kernel is doing 
> is to
> use the lwp and only the lwp in the pr_status notes.  This creates a slight
> problem because linux-proc.c uses the thread_info list to run through 
> all the
> threads.  Unfortunately, the thread_info list contains ptid_t structs that
> contain the pid and the tid, but no lwp.  In the old linuxthreads model, 
> this
> isn't a problem because the pid is in fact equal to the lwp.  In the nptl
> model, we can't simply convert a pid/tid combination into its associated 
> lwp
> because this logic is hidden in libthread_db.  At present, the libthread_db
> library is only loaded by the thread-db layer.  While there are routines to
> convert a tid to lwp and vice-versa, these routines are not externally 
> exposed
> in the target vector.  The only externalized method that could do such a 
> job
> is to_pid_to_str() which converts to character.  Usage of this interface 
> would
> require parsing the output for the characters "LWP".
> 
> This patch proposes adding a secondary list of thread_info structs that 
> keeps
> track of the lwps.  This list is kept distinct from the regular thread 
> list.
> Since manipulating either list involves common operations, some base 
> operations
> are added for generic thread_info list manipulation.  The lin-lwp.c 
> layer is
> changed to add and delete lwp thread_info structs to the lwp list when lwps
> are added or deleted.  The linux-proc.c code is changed to iterate 
> through the
> lwp thread_info list rather than the thread list when creating the 
> pr_status
> notes.
> 
> I had brought up this idea on the gdb mailing list and there was some 
> discussion
> about how the thread_info struct should be renamed to something more 
> generic.
> That issue was not resolved, but I feel that it can be performed 
> distinct from this
> patch as it will require a global change to many pieces of code.  To 
> keep some
> new names at a reasonable length, I chose to use the name tinfo_list 
> rather than
> thread_info_list.  This would obviously also be renamed.
> 
> Ok to commit?  I have reindented and checked in the three C files 
> involved prior to
> building this patch.
> 
> -- Jeff J.
> 
> 2003-03-28  Jeff Johnston  <jjohnstn@redhat.com>
> 
>     * gdbthread.h (tinfo_list): New struct to represent list of
>     thread_info structs.
>     (get_thread_tinfo_list, get_lwp_tinfo_list): New prototypes.
>     (add_thread_info,  delete_thread_info): Ditto.
>     (thread_info_id_to_pid, pid_to_thread_info_id): Ditto.
>     (in_tinfo_list, valid_thread_info_id, find_thread_info_pid): Ditto.
>     (iterate_over_tinfo_list): Ditto.   
>     * thread.c (init_thread_list): Clear lwp list as well as thread list.
>     (get_thread_tinfo_list, get_lwp_tinfo_list): New functions
>     to get either the thread or lwp lists respectively.
>     (add_thread_info,  delete_thread_info): New generic thread_info 
> functions.
>     (thread_info_id_to_pid, pid_to_thread_info_id): Ditto.
>     (in_tinfo_list, valid_thread_info_id, find_thread_info_pid): Ditto.
>     (iterate_over_tinfo_list): Ditto.
>     * linux-proc.c (linux_do_thread_registers): Store lwp for prstatus note
>     rather than a merge of pid and tid.
>     (linux_make_note_section): Iterate over list of lwps rather than 
> threads
>     to create note data.
>     * lin-lwp.c (add_lwp): Call add_thread_info() to add new lwp ptid to 
> lwp list.
>     (delete_lwp): Call delete_thread_info() to delete lwp ptid from lwp 
> list.
> 
> 
> 
> ------------------------------------------------------------------------
> 
> Index: gdbthread.h
> ===================================================================
> RCS file: /cvs/src/src/gdb/gdbthread.h,v
> retrieving revision 1.6
> diff -u -r1.6 gdbthread.h
> --- gdbthread.h	6 Dec 2002 07:35:55 -0000	1.6
> +++ gdbthread.h	29 Mar 2003 00:02:45 -0000
> @@ -73,6 +73,58 @@
>    struct private_thread_info *private;
>  };
>  
> +struct tinfo_list
> +{
> +  struct thread_info *start;
> +  int highest_num;
> +};
> +
> +/* Thread info list operations.  */
> +
> +/* Create an empty tinfo list, or empty an existing one.  */
> +extern void init_tinfo_list (struct tinfo_list *lp);
> +
> +/* Get the thread info list for threads.  */
> +extern struct tinfo_list *get_thread_tinfo_list (void);
> +
> +/* Get the thread info list for lwps.  */
> +extern struct tinfo_list *get_lwp_tinfo_list (void);
> +
> +/* Add a thread_info to a thread_info list.  */
> +extern struct thread_info *add_thread_info (struct tinfo_list *lp,
> +					    ptid_t ptid);
> +
> +/* Delete an existing thread_info list entry.  */
> +extern void delete_thread_info (struct tinfo_list *lp, ptid_t);
> +
> +/* Translate the integer thread_info id (GDB's homegrown id, not the system's)
> +   into a "pid" (which may be overloaded with extra thread information).  */
> +extern ptid_t thread_info_id_to_pid (struct tinfo_list *lp, int);
> +
> +/* Translate a 'pid' (which may be overloaded with extra thread information) 
> +   into the integer thread id (GDB's homegrown id, not the system's).  */
> +extern int pid_to_thread_info_id (struct tinfo_list *lp, ptid_t ptid);
> +
> +/* Boolean test for an already-known pid (which may be overloaded with
> +   extra thread information).  */
> +extern int in_tinfo_list (struct tinfo_list *lp, ptid_t ptid);
> +
> +/* Boolean test for an already-known thread id (GDB's homegrown id, 
> +   not the system's).  */
> +extern int valid_thread_info_id (struct tinfo_list *lp, int thread);
> +
> +/* Search function to lookup a thread_info by 'pid'.  */
> +extern struct thread_info *find_thread_info_pid (struct tinfo_list *lp,
> +						 ptid_t ptid);
> +
> +/* Iterator function to call a user-provided callback function
> +   once for each known thread_info in a list.  */
> +typedef int (*thread_callback_func) (struct thread_info *, void *);
> +extern struct thread_info *iterate_over_tinfo_list (
> +  struct tinfo_list *lp, thread_callback_func, void *);
> +
> +/* Thread operations.  */
> +
>  /* Create an empty thread list, or empty the existing one.  */
>  extern void init_thread_list (void);
>  
> @@ -108,7 +160,6 @@
>  
>  /* Iterator function to call a user-provided callback function
>     once for each known thread.  */
> -typedef int (*thread_callback_func) (struct thread_info *, void *);
>  extern struct thread_info *iterate_over_threads (thread_callback_func, void *);
>  
>  /* infrun context switch: save the debugger state for the given thread.  */
> Index: thread.c
> ===================================================================
> RCS file: /cvs/src/src/gdb/thread.c,v
> retrieving revision 1.29
> diff -u -r1.29 thread.c
> --- thread.c	28 Mar 2003 21:42:41 -0000	1.29
> +++ thread.c	29 Mar 2003 00:02:45 -0000
> @@ -49,11 +49,15 @@
>  
>  void _initialize_thread (void);
>  
> -/* Prototypes for local functions. */
> +/* Thread info lists.  */
> +
> +static struct tinfo_list thread_tinfo_list = { NULL, 0 };
> +static struct tinfo_list lwp_tinfo_list = { NULL, 0 };
>  
> -static struct thread_info *thread_list = NULL;
> -static int highest_thread_num;
> +/* Prototypes for local functions. */
>  
> +static struct thread_info *find_thread_info_id (struct tinfo_list *lp,
> +						int num);
>  static struct thread_info *find_thread_id (int num);
>  
>  static void thread_command (char *tidstr, int from_tty);
> @@ -65,25 +69,22 @@
>  static void switch_to_thread (ptid_t ptid);
>  static void prune_threads (void);
>  
> -void
> -delete_step_resume_breakpoint (void *arg)
> -{
> -  struct breakpoint **breakpointp = (struct breakpoint **) arg;
> -  struct thread_info *tp;
> +/* Thread info list functions.  */
>  
> -  if (*breakpointp != NULL)
> -    {
> -      delete_breakpoint (*breakpointp);
> -      for (tp = thread_list; tp; tp = tp->next)
> -	if (tp->step_resume_breakpoint == *breakpointp)
> -	  tp->step_resume_breakpoint = NULL;
> +struct tinfo_list *
> +get_thread_tinfo_list (void)
> +{
> +  return &thread_tinfo_list;
> +}
>  
> -      *breakpointp = NULL;
> -    }
> +struct tinfo_list *
> +get_lwp_tinfo_list (void)
> +{
> +  return &lwp_tinfo_list;
>  }
>  
>  static void
> -free_thread (struct thread_info *tp)
> +free_thread_info (struct thread_info *tp)
>  {
>    /* NOTE: this will take care of any left-over step_resume breakpoints,
>       but not any user-specified thread-specific breakpoints. */
> @@ -99,48 +100,46 @@
>  }
>  
>  void
> -init_thread_list (void)
> +init_tinfo_list (struct tinfo_list *lp)
>  {
> +  int i;
>    struct thread_info *tp, *tpnext;
>  
> -  highest_thread_num = 0;
> -  if (!thread_list)
> +  lp->highest_num = 0;
> +  if (lp->start == NULL)
>      return;
>  
> -  for (tp = thread_list; tp; tp = tpnext)
> +  for (tp = lp->start; tp; tp = tpnext)
>      {
>        tpnext = tp->next;
> -      free_thread (tp);
> +      free_thread_info (tp);
>      }
>  
> -  thread_list = NULL;
> +  lp->start = NULL;
>  }
>  
> -/* add_thread now returns a pointer to the new thread_info, 
> -   so that back_ends can initialize their private data.  */
> -
>  struct thread_info *
> -add_thread (ptid_t ptid)
> +add_thread_info (struct tinfo_list *lp, ptid_t ptid)
>  {
>    struct thread_info *tp;
>  
>    tp = (struct thread_info *) xmalloc (sizeof (*tp));
>    memset (tp, 0, sizeof (*tp));
>    tp->ptid = ptid;
> -  tp->num = ++highest_thread_num;
> -  tp->next = thread_list;
> -  thread_list = tp;
> +  tp->num = ++lp->highest_num;
> +  tp->next = lp->start;
> +  lp->start = tp;
>    return tp;
>  }
>  
>  void
> -delete_thread (ptid_t ptid)
> +delete_thread_info (struct tinfo_list *lp, ptid_t ptid)
>  {
>    struct thread_info *tp, *tpprev;
>  
>    tpprev = NULL;
>  
> -  for (tp = thread_list; tp; tpprev = tp, tp = tp->next)
> +  for (tp = lp->start; tp; tpprev = tp, tp = tp->next)
>      if (ptid_equal (tp->ptid, ptid))
>        break;
>  
> @@ -150,17 +149,17 @@
>    if (tpprev)
>      tpprev->next = tp->next;
>    else
> -    thread_list = tp->next;
> +    lp->start = tp->next;
>  
> -  free_thread (tp);
> +  free_thread_info (tp);
>  }
>  
>  static struct thread_info *
> -find_thread_id (int num)
> +find_thread_info_id (struct tinfo_list *lp, int num)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if (tp->num == num)
>        return tp;
>  
> @@ -169,11 +168,11 @@
>  
>  /* Find a thread_info by matching PTID.  */
>  struct thread_info *
> -find_thread_pid (ptid_t ptid)
> +find_thread_info_pid (struct tinfo_list *lp, ptid_t ptid)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if (ptid_equal (tp->ptid, ptid))
>        return tp;
>  
> @@ -181,7 +180,7 @@
>  }
>  
>  /*
> - * Thread iterator function.
> + * Thread info list iterator function.
>   *
>   * Calls a callback function once for each thread, so long as
>   * the callback function returns false.  If the callback function
> @@ -190,17 +189,16 @@
>   * search for a thread with arbitrary attributes, or for applying
>   * some operation to every thread.
>   *
> - * FIXME: some of the existing functionality, such as 
> - * "Thread apply all", might be rewritten using this functionality.
>   */
>  
>  struct thread_info *
> -iterate_over_threads (int (*callback) (struct thread_info *, void *),
> -		      void *data)
> +iterate_over_tinfo_list (struct tinfo_list *lp,
> +			 int (*callback) (struct thread_info *, void *),
> +			 void *data)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if ((*callback) (tp, data))
>        return tp;
>  
> @@ -208,11 +206,11 @@
>  }
>  
>  int
> -valid_thread_id (int num)
> +valid_thread_info_id (struct tinfo_list *lp, int num)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if (tp->num == num)
>        return 1;
>  
> @@ -220,11 +218,11 @@
>  }
>  
>  int
> -pid_to_thread_id (ptid_t ptid)
> +pid_to_thread_info_id (struct tinfo_list *lp, ptid_t ptid)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if (ptid_equal (tp->ptid, ptid))
>        return tp->num;
>  
> @@ -232,9 +230,9 @@
>  }
>  
>  ptid_t
> -thread_id_to_pid (int num)
> +thread_info_id_to_pid (struct tinfo_list *lp, int num)
>  {
> -  struct thread_info *thread = find_thread_id (num);
> +  struct thread_info *thread = find_thread_info_id (lp, num);
>    if (thread)
>      return thread->ptid;
>    else
> @@ -242,17 +240,117 @@
>  }
>  
>  int
> -in_thread_list (ptid_t ptid)
> +in_tinfo_list (struct tinfo_list *lp, ptid_t ptid)
>  {
>    struct thread_info *tp;
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = lp->start; tp; tp = tp->next)
>      if (ptid_equal (tp->ptid, ptid))
>        return 1;
>  
>    return 0;			/* Never heard of 'im */
>  }
>  
> +/* Thread list specific functions.  */
> +
> +void
> +init_thread_list (void)
> +{
> +  init_tinfo_list (&thread_tinfo_list);
> +  init_tinfo_list (&lwp_tinfo_list);
> +}
> +
> +/* add_thread now returns a pointer to the new thread_info, 
> +   so that back_ends can initialize their private data.  */
> +
> +struct thread_info *
> +add_thread (ptid_t ptid)
> +{
> +  return add_thread_info (&thread_tinfo_list, ptid);
> +}
> +
> +void
> +delete_thread (ptid_t ptid)
> +{
> +  delete_thread_info (&thread_tinfo_list, ptid);
> +}
> +
> +static struct thread_info *
> +find_thread_id (int num)
> +{
> +  return find_thread_info_id (&thread_tinfo_list, num);
> +}
> +
> +/* Find a thread_info by matching PTID.  */
> +struct thread_info *
> +find_thread_pid (ptid_t ptid)
> +{
> +  return find_thread_info_pid (&thread_tinfo_list, ptid);
> +}
> +
> +/*
> + * Thread iterator function.
> + *
> + * Calls a callback function once for each thread, so long as
> + * the callback function returns false.  If the callback function
> + * returns true, the iteration will end and the current thread
> + * will be returned.  This can be useful for implementing a 
> + * search for a thread with arbitrary attributes, or for applying
> + * some operation to every thread.
> + *
> + * FIXME: some of the existing functionality, such as 
> + * "Thread apply all", might be rewritten using this functionality.
> + */
> +
> +struct thread_info *
> +iterate_over_threads (int (*callback) (struct thread_info *, void *),
> +		      void *data)
> +{
> +  return iterate_over_tinfo_list (&thread_tinfo_list, callback, data);
> +}
> +
> +int
> +valid_thread_id (int num)
> +{
> +  return valid_thread_info_id (&thread_tinfo_list, num);
> +}
> +
> +int
> +pid_to_thread_id (ptid_t ptid)
> +{
> +  return pid_to_thread_info_id (&thread_tinfo_list, ptid);
> +}
> +
> +ptid_t
> +thread_id_to_pid (int num)
> +{
> +  return thread_info_id_to_pid (&thread_tinfo_list, num);
> +}
> +
> +int
> +in_thread_list (ptid_t ptid)
> +{
> +  return in_tinfo_list (&thread_tinfo_list, ptid);
> +}
> +
> +
> +void
> +delete_step_resume_breakpoint (void *arg)
> +{
> +  struct breakpoint **breakpointp = (struct breakpoint **) arg;
> +  struct thread_info *tp;
> +
> +  if (*breakpointp != NULL)
> +    {
> +      delete_breakpoint (*breakpointp);
> +      for (tp = thread_tinfo_list.start; tp; tp = tp->next)
> +	if (tp->step_resume_breakpoint == *breakpointp)
> +	  tp->step_resume_breakpoint = NULL;
> +
> +      *breakpointp = NULL;
> +    }
> +}
> +
>  /* Print a list of thread ids currently known, and the total number of
>     threads. To be used from within catch_errors. */
>  static int
> @@ -267,7 +365,7 @@
>  
>    cleanup_chain = make_cleanup_ui_out_tuple_begin_end (uiout, "thread-ids");
>  
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = thread_tinfo_list.start; tp; tp = tp->next)
>      {
>        num++;
>        ui_out_field_int (uiout, "thread-id", tp->num);
> @@ -404,7 +502,7 @@
>  {
>    struct thread_info *tp, *next;
>  
> -  for (tp = thread_list; tp; tp = next)
> +  for (tp = thread_tinfo_list.start; tp; tp = next)
>      {
>        next = tp->next;
>        if (!thread_alive (tp))
> @@ -437,7 +535,7 @@
>    prune_threads ();
>    target_find_new_threads ();
>    current_ptid = inferior_ptid;
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = thread_tinfo_list.start; tp; tp = tp->next)
>      {
>        if (ptid_equal (tp->ptid, current_ptid))
>  	printf_filtered ("* ");
> @@ -564,7 +662,7 @@
>       execute_command */
>    saved_cmd = xstrdup (cmd);
>    saved_cmd_cleanup_chain = make_cleanup (xfree, (void *) saved_cmd);
> -  for (tp = thread_list; tp; tp = tp->next)
> +  for (tp = thread_tinfo_list.start; tp; tp = tp->next)
>      if (thread_alive (tp))
>        {
>  	switch_to_thread (tp->ptid);
> Index: linux-proc.c
> ===================================================================
> RCS file: /cvs/src/src/gdb/linux-proc.c,v
> retrieving revision 1.14
> diff -u -r1.14 linux-proc.c
> --- linux-proc.c	28 Mar 2003 21:42:41 -0000	1.14
> +++ linux-proc.c	29 Mar 2003 00:02:45 -0000
> @@ -171,14 +171,18 @@
>  #ifdef FILL_FPXREGSET
>    gdb_fpxregset_t fpxregs;
>  #endif
> -  unsigned long merged_pid = ptid_get_tid (ptid) << 16 | ptid_get_pid (ptid);
> +  unsigned long lwpid = 0;
> +  char *pid_str = NULL;
> +
> +  lwpid = ptid_get_lwp (ptid);
> +  if (lwpid == 0)
> +    lwpid = ptid_get_pid (ptid);
>  
>    fill_gregset (&gregs, -1);
>    note_data = (char *) elfcore_write_prstatus (obfd,
>  					       note_data,
>  					       note_size,
> -					       merged_pid,
> -					       stop_signal, &gregs);
> +					       lwpid, stop_signal, &gregs);
>  
>    fill_fpregset (&fpregs, -1);
>    note_data = (char *) elfcore_write_prfpreg (obfd,
> @@ -268,10 +272,11 @@
>    thread_args.note_data = note_data;
>    thread_args.note_size = note_size;
>    thread_args.num_notes = 0;
> -  iterate_over_threads (linux_corefile_thread_callback, &thread_args);
> +  iterate_over_tinfo_list (get_lwp_tinfo_list (),
> +			   linux_corefile_thread_callback, &thread_args);
>    if (thread_args.num_notes == 0)
>      {
> -      /* iterate_over_threads didn't come up with any threads;
> +      /* iterate_over_tinfo_list didn't come up with any lwps;
>           just use inferior_ptid.  */
>        note_data = linux_do_thread_registers (obfd, inferior_ptid,
>  					     note_data, note_size);
> Index: lin-lwp.c
> ===================================================================
> RCS file: /cvs/src/src/gdb/lin-lwp.c,v
> retrieving revision 1.43
> diff -u -r1.43 lin-lwp.c
> --- lin-lwp.c	28 Mar 2003 21:42:41 -0000	1.43
> +++ lin-lwp.c	29 Mar 2003 00:02:45 -0000
> @@ -220,6 +220,8 @@
>    if (++num_lwps > 1)
>      threaded = 1;
>  
> +  add_thread_info (get_lwp_tinfo_list (), ptid);
> +
>    return lp;
>  }
>  
> @@ -249,6 +251,8 @@
>      lwp_list = lp->next;
>  
>    xfree (lp);
> +
> +  delete_thread_info (get_lwp_tinfo_list (), ptid);
>  }
>  
>  /* Return a pointer to the structure describing the LWP corresponding



  reply	other threads:[~2003-06-05 18:35 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2003-03-29  0:19 J. Johnston
2003-06-05 18:35 ` J. Johnston [this message]
2003-06-17 15:28   ` Daniel Jacobowitz
2003-06-18 23:08 ` Michael Snyder
2003-06-18 23:11   ` Daniel Jacobowitz
2003-06-18 23:23     ` Michael Snyder
2003-06-18 23:27       ` Daniel Jacobowitz
2003-06-18 23:37         ` J. Johnston

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=3EDF8D55.5000902@redhat.com \
    --to=jjohnstn@redhat.com \
    --cc=gdb-patches@sources.redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox