From: Julien Grall <julien.grall@arm.com>
To: Bhupinder Thakur <bhupinder.thakur@linaro.org>,
xen-devel@lists.xenproject.org
Cc: nd@arm.com, Stefano Stabellini <sstabellini@kernel.org>
Subject: Re: [PATCH 01/11] xen/arm: vpl011: Add pl011 uart emulation in Xen
Date: Sun, 5 Mar 2017 12:12:06 +0000 [thread overview]
Message-ID: <0f555eb4-c131-0469-44b6-ffa67e2d8537@arm.com> (raw)
In-Reply-To: <1487676368-22356-2-git-send-email-bhupinder.thakur@linaro.org>
Hi Bhupinder,
On 21/02/17 11:25, Bhupinder Thakur wrote:
> diff --git a/xen/arch/arm/vpl011.c b/xen/arch/arm/vpl011.c
> new file mode 100644
> index 0000000..88ba968
> --- /dev/null
> +++ b/xen/arch/arm/vpl011.c
[...]
> +static int vpl011_mmio_write(struct vcpu *v, mmio_info_t *info, register_t r, void *priv)
See my comment on the vpl011_mmio_read about the structure of the function.
> +{
> + unsigned char ch = r;
> +
> + switch (info->gpa - GUEST_PL011_BASE)
> + {
> + case VPL011_UARTCR_OFFSET:
This register is not required in the SBSA UART.
> + v->domain->arch.vpl011.control = r;
> + break;
> + case VPL011_UARTDR_OFFSET:
> + vpl011_write_data(v->domain, ch);
The implicit casting of register_t to ch is a bit confusing. Also I
would prefer to use uint8_t as it reflects the size of the field.
Lastly, what if vpl011_write_data is returning an error?
> + break;
> + case VPL011_UARTIMSC_OFFSET:
> + v->domain->arch.vpl011.intr_mask = r;
You need to sanitize the value written and make sure reserved field and
Write As Ones register and handle correctly (see the spec).
> + if ( (v->domain->arch.vpl011.raw_intr_status & v->domain->arch.vpl011.intr_mask) )
This line and the line below are over 80 columns. Also the code in
general would benefits if you define a local variable to point to
v->domain->arch.vpl011.
> + vgic_vcpu_inject_spi(v->domain, (int)v->domain->arch.hvm_domain.params[HVM_PARAM_VPL011_VIRQ]);
I don't think we need an HVM_PARAM for the pl011 SPI. We cannot hardcode
this in the guest layout (see include/public/arch-arm.h).
> + break;
> + case VPL011_UARTICR_OFFSET:
> + /*
> + * clear all bits which are set in the input
> + */
> + v->domain->arch.vpl011.raw_intr_status &= ~r;
> + if ( (v->domain->arch.vpl011.raw_intr_status & v->domain->arch.vpl011.intr_mask) )
> + {
{ } are not necessary.
> + vgic_vcpu_inject_spi(v->domain, (int)v->domain->arch.hvm_domain.params[HVM_PARAM_VPL011_VIRQ]);
> + }
The if is exactly the same as the on in IMSC. This is the call of an
helper to check the interrupt state and inject one if necessary.
> + break;
> + case VPL011_UARTRSR_OFFSET: // nothing to clear
The coding style for single line comment in Xen is /* ... */
Also, I think we may want to handle Overrun error as the ring may be full.
> + break;
> + case VPL011_UARTFR_OFFSET: // these are all RO registers
> + case VPL011_UARTRIS_OFFSET:
> + case VPL011_UARTMIS_OFFSET:
> + case VPL011_UARTDMACR_OFFSET:
Please have a look at the vGICv2/vGICv3 emulation see how we implemented
write ignore register.
> + break;
> + default:
> + printk ("vpl011_mmio_write: switch case not handled %d\n", (int)(info->gpa - GUEST_PL011_BASE));
See my comment about the prinkt in the read emulation.
> + break;
> + }
> +
> + return VPL011_EMUL_OK;
> +}
> +
> +static const struct mmio_handler_ops vpl011_mmio_handler = {
> + .read = vpl011_mmio_read,
> + .write = vpl011_mmio_write,
> +};
> +
> +
> +
Only newline is sufficient. This was mention by Konrad and I will try to
avoid repeating his comments.
> +int vpl011_map_guest_page(struct domain *d)
> +{
> + int rc=0;
> +
> + /*
> + * map the guest PFN to Xen address space
> + */
> + rc = prepare_ring_for_helper(d,
> + d->arch.hvm_domain.params[HVM_PARAM_VPL011_CONSOLE_PFN],
> + &d->arch.vpl011.ring_page,
> + (void **)&d->arch.vpl011.ring_buf);
Why do you need the cast?
Also I cannot find any code in this series which destroy the ring.
Please have a helper called vpl011_unmap_guest_page to do this job and
call when the domain is destroyed.
> + if ( rc < 0 )
> + {
> + printk("Failed to map vpl011 guest PFN\n");
> + }
> +
> + return rc;
> +}
> +
> +static int vpl011_data_avail(struct domain *d)
> +{
> + int rc=0;
> + unsigned long flags;
> +
> + struct console_interface *intf=(struct console_interface *)d->arch.vpl011.ring_buf;
> +
> + VPL011_LOCK(d, flags);
> +
> + /*`
> + * check IN ring buffer
> + */
> + if ( !VPL011_IN_RING_EMPTY(intf) )
> + {
> + /*
> + * clear the RX FIFO empty flag as the ring is not empty
> + */
> + d->arch.vpl011.flag &= ~(VPL011_UARTFR_RXFE);
> +
> + /*
> + * if the buffer is full then set the RX FIFO FULL flag
> + */
> + if ( VPL011_IN_RING_FULL(intf) )
> + d->arch.vpl011.flag |= (VPL011_UARTFR_RXFF);
> +
> + /*
> + * set the RX interrupt status
> + */
> + d->arch.vpl011.raw_intr_status |= (VPL011_UARTRIS_RXRIS);
> + }
> +
> + /*
> + * check OUT ring buffer
> + */
> + if ( !VPL011_OUT_RING_FULL(intf) )
> + {
> + /*
> + * if the buffer is not full then clear the TX FIFO full flag
> + */
> + d->arch.vpl011.flag &= ~(VPL011_UARTFR_TXFF);
> +
> + /*
> + * set the TX interrupt status
> + */
> + d->arch.vpl011.raw_intr_status |= (VPL011_UARTRIS_TXRIS);
> +
> + if ( VPL011_OUT_RING_EMPTY(intf) )
> + {
> + /*
> + * clear the uart busy flag and set the TX FIFO empty flag
> + */
> + d->arch.vpl011.flag &= ~(VPL011_UARTFR_BUSY);
> + d->arch.vpl011.flag |= (VPL011_UARTFR_TXFE);
> + }
> + }
> +
> + VPL011_UNLOCK(d, flags);
> +
> + /*
> + * send an interrupt if it is not masked
> + */
> + if ( (d->arch.vpl011.raw_intr_status & d->arch.vpl011.intr_mask) )
> + vgic_vcpu_inject_spi(d, (int)d->arch.hvm_domain.params[HVM_PARAM_VPL011_VIRQ]);
See my comment above about having an helper for this code.
> +
> + if ( !VPL011_OUT_RING_EMPTY(intf) )
> + {
> + /*
> + * raise an interrupt to dom0
The backend console does not necessarily live in DOM0. Anyway, Konrad
requested to drop the comment and I am happy with that.
> + */
> + rc = raw_evtchn_send(d,
> + d->arch.hvm_domain.params[HVM_PARAM_VPL011_CONSOLE_EVTCHN], NULL);
You are using a function that is been defined in a later patch (i.w #3).
All the patch should be able to compile without applying upcoming patch
to allow bisection.
Ideally, Xen should still be functional for every patch. But it is a
best effort.
> +
> + if ( rc < 0 )
> + printk("Failed to send vpl011 interrupt to dom0\n");
> + }
> +
> + return rc;
> +}
> +
> +int vpl011_read_data(struct domain *d, unsigned char *data)
This function does not seem to be used outside of this file. So why do
you export it?
> +{
> + int rc=0;
> + unsigned long flags;
> + struct console_interface *intf=(struct console_interface *)d->arch.vpl011.ring_buf;
> +
> + *data = 0;
> +
> + VPL011_LOCK(d, flags);
> +
> + /*
> + * if there is data in the ring buffer then copy it to the output buffer
> + */
> + if ( !VPL011_IN_RING_EMPTY(intf) )
> + {
> + *data = intf->in[MASK_VPL011CONS_IDX(intf->in_cons++, intf->in)];
> + }
> +
> + /*
> + * if the ring buffer is empty then set the RX FIFO empty flag
> + */
> + if ( VPL011_IN_RING_EMPTY(intf) )
> + {
> + d->arch.vpl011.flag |= (VPL011_UARTFR_RXFE);
> + d->arch.vpl011.raw_intr_status &= ~(VPL011_UARTRIS_RXRIS);
> + }
> +
> + /*
> + * clear the RX FIFO full flag
> + */
> + d->arch.vpl011.flag &= ~(VPL011_UARTFR_RXFF);
> +
> + VPL011_UNLOCK(d, flags);
> +
> + return rc;
> +}
> +
> +int vpl011_write_data(struct domain *d, unsigned char data)
Same has for vpl011_read_data.
> +{
> + int rc=0;
> + unsigned long flags;
> + struct console_interface *intf=(struct console_interface *)d->arch.vpl011.ring_buf;
> +
> + VPL011_LOCK(d, flags);
> +
> + /*
> + * if there is space in the ring buffer then write the data
> + */
> + if ( !VPL011_OUT_RING_FULL(intf) )
> + {
> + intf->out[MASK_VPL011CONS_IDX(intf->out_prod++, intf->out)] = data;
> + smp_wmb();
> + }
> +
> + /*
> + * if there is no space in the ring buffer then set the
> + * TX FIFO FULL flag
> + */
> + if ( VPL011_OUT_RING_FULL(intf) )
> + {
> + d->arch.vpl011.flag |= (VPL011_UARTFR_TXFF);
> + d->arch.vpl011.raw_intr_status &= ~(VPL011_UARTRIS_TXRIS);
> + }
> +
> + /*
> + * set the uart busy status
> + */
> + d->arch.vpl011.flag |= (VPL011_UARTFR_BUSY);
> +
> + /*
> + * clear the TX FIFO empty flag
> + */
> + d->arch.vpl011.flag &= ~(VPL011_UARTFR_TXFE);
> +
> + VPL011_UNLOCK(d, flags);
> +
> + /*
> + * raise an event to dom0 only if it is the first character in the buffer
> + */
> + if ( VPL011_RING_DEPTH(intf, out) == 1 )
> + {
> + rc = raw_evtchn_send(d,
> + d->arch.hvm_domain.params[HVM_PARAM_VPL011_CONSOLE_EVTCHN], NULL);
> +
> + if ( rc < 0 )
> + printk("Failed to send vpl011 interrupt to dom0\n");
> + }
> +
> + return rc;
> +}
> +
> +static void vpl011_notification(struct vcpu *v, unsigned int port)
> +{
> + vpl011_data_avail(v->domain);
vpl011_data_avail is returning an error but you don't check. What is the
point of it then?
> +}
> +
> +int domain_vpl011_init(struct domain *d)
I was expected a destroy function to clean-up the vpl011 emulation.
> +{
> + int rc=0;
> +
> + /*
> + * register xen event channel
> + */
> + rc = alloc_unbound_xen_event_channel(d, 0, current->domain->domain_id,
> + vpl011_notification);
This line looks wrong to me. The console backend may not live in the
same domain as the toolstack.
> + if (rc < 0)
> + {
> + printk ("Failed to allocate vpl011 event channel\n");
> + return rc;
> + }
> + d->arch.hvm_domain.params[HVM_PARAM_VPL011_CONSOLE_EVTCHN] = rc;
> +
> + /*
> + * allocate an SPI VIRQ for the guest
> + */
> + d->arch.hvm_domain.params[HVM_PARAM_VPL011_VIRQ] = vgic_allocate_spi(d);
It makes little sense to hardcode the MMIO region and not the SPI. Also,
I would rather avoid to introduce new HVM_PARAM when not necessary. So
hardcoding the value looks more sensible to me.
> +
> + /*
> + * register mmio handler
> + */
> + register_mmio_handler (d, &vpl011_mmio_handler, GUEST_PL011_BASE, GUEST_PL011_SIZE, NULL);
Coding style. No space between the function name and (.
> +
> + /*
> + * initialize the lock
> + */
> + spin_lock_init(&d->arch.vpl011.lock);
> +
> + /*
> + * clear the flag, control and interrupt status registers
> + */
> + d->arch.vpl011.control = 0;
> + d->arch.vpl011.flag = 0;
> + d->arch.vpl011.intr_mask = 0;
> + d->arch.vpl011.intr_clear = 0;
> + d->arch.vpl011.raw_intr_status = 0;
> + d->arch.vpl011.masked_intr_status = 0;
The domain structure is already zeroed. So no need to 0 it again.
> +
> + return 0;
> +}
Missing e-macs magic here.
> diff --git a/xen/arch/arm/vpl011.h b/xen/arch/arm/vpl011.h
> new file mode 100644
> index 0000000..f2c577f
> --- /dev/null
> +++ b/xen/arch/arm/vpl011.h
Header should live in include.
[...]
> +#ifndef _VPL011_H_
> +
> +#define _VPL011_H_
> +
> +/*
> + * register offsets
> + */
We already define the PL011 register in include/asm-arm/pl011-uart.h.
Please re-use them rather than re-inventing the wheel.
> +#define VPL011_UARTDR_OFFSET 0x0 // data register (RW)
> +#define VPL011_UARTRSR_OFFSET 0x4 // receive status and error clear register (RW)
> +#define VPL011_UARTFR_OFFSET 0x18 // flag register (RO)
> +#define VPL011_UARTRIS_OFFSET 0x3c // raw interrupt status register (RO)
> +#define VPL011_UARTMIS_OFFSET 0x40 // masked interrupt status register (RO)
> +#define VPL011_UARTIMSC_OFFSET 0x38 // interrupt mask set/clear register (RW)
> +#define VPL011_UARTICR_OFFSET 0x44 // interrupt clear register (WO)
> +#define VPL011_UARTCR_OFFSET 0x30 // uart control register
> +#define VPL011_UARTDMACR_OFFSET 0x48 // uart dma control register
[...]
> +#define MASK_VPL011CONS_IDX(idx, ring) ((idx) & (sizeof(ring)-1))
> +struct console_interface {
> + char in[1024];
> + char out[2048];
> + uint32_t in_cons, in_prod;
> + uint32_t out_cons, out_prod;
> +};
> +#endif
The communication between Xen and the console backend is based on the PV
console ring. It would be easier to re-use it rather than recreating one.
> diff --git a/xen/common/Kconfig b/xen/common/Kconfig
> index f2ecbc4..7e2feac 100644
> --- a/xen/common/Kconfig
> +++ b/xen/common/Kconfig
> @@ -237,4 +237,10 @@ config FAST_SYMBOL_LOOKUP
> The only user of this is Live patching.
>
> If unsure, say Y.
> +
> +config VPL011_CONSOLE
> + bool "Emulated pl011 console support"
> + default y
> + ---help---
> + Allows a guest to use pl011 UART as a console
> endmenu
This should go in arch/arm/Kconfig
> diff --git a/xen/include/asm-arm/domain.h b/xen/include/asm-arm/domain.h
> index 2d6fbb1..ff2403a 100644
> --- a/xen/include/asm-arm/domain.h
> +++ b/xen/include/asm-arm/domain.h
> @@ -40,6 +40,7 @@ struct vtimer {
> uint64_t cval;
> };
>
> +
Spurious line.
> struct arch_domain
> {
> #ifdef CONFIG_ARM_64
> @@ -131,6 +132,20 @@ struct arch_domain
> struct {
> uint8_t privileged_call_enabled : 1;
> } monitor;
> +
> +#ifdef CONFIG_VPL011_CONSOLE
> + struct vpl011 {
> + void *ring_buf;
> + struct page_info *ring_page;
> + uint32_t flag; /* flag register */
> + uint32_t control; /* control register */
> + uint32_t intr_mask; /* interrupt mask register*/
> + uint32_t intr_clear; /* interrupt clear register */
> + uint32_t raw_intr_status; /* raw interrupt status register */
> + uint32_t masked_intr_status; /* masked interrupt register */
> + spinlock_t lock;
> + } vpl011;
> +#endif
I think the structure should be defined in vpl011.h.
> } __cacheline_aligned;
>
> struct arch_vcpu
> diff --git a/xen/include/public/arch-arm.h b/xen/include/public/arch-arm.h
> index bd974fb..1d4548f 100644
> --- a/xen/include/public/arch-arm.h
> +++ b/xen/include/public/arch-arm.h
> @@ -410,6 +410,10 @@ typedef uint64_t xen_callback_t;
> #define GUEST_ACPI_BASE 0x20000000ULL
> #define GUEST_ACPI_SIZE 0x02000000ULL
>
> +/* PL011 mappings */
> +#define GUEST_PL011_BASE 0x22000000ULL
> +#define GUEST_PL011_SIZE 0x00001000ULL
> +
> /*
> * 16MB == 4096 pages reserved for guest to use as a region to map its
> * grant table in.
> @@ -420,6 +424,7 @@ typedef uint64_t xen_callback_t;
> #define GUEST_MAGIC_BASE xen_mk_ullong(0x39000000)
> #define GUEST_MAGIC_SIZE xen_mk_ullong(0x01000000)
>
> +
Spurious line.
> #define GUEST_RAM_BANKS 2
>
> #define GUEST_RAM0_BASE xen_mk_ullong(0x40000000) /* 3GB of low RAM @ 1GB */
>
Cheers,
--
Julien Grall
_______________________________________________
Xen-devel mailing list
Xen-devel@lists.xen.org
https://lists.xen.org/xen-devel
next prev parent reply other threads:[~2017-03-05 12:12 UTC|newest]
Thread overview: 82+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-02-21 11:25 [PATCH 00/11] pl011 emulation support in Xen Bhupinder Thakur
2017-02-21 11:25 ` [PATCH 01/11] xen/arm: vpl011: Add pl011 uart emulation " Bhupinder Thakur
2017-02-26 21:37 ` Julien Grall
2017-03-03 19:19 ` Julien Grall
2017-03-21 13:27 ` Bhupinder Thakur
2017-03-21 19:38 ` Julien Grall
2017-03-23 9:44 ` Bhupinder Thakur
2017-03-23 13:51 ` Julien Grall
2017-03-03 19:59 ` Konrad Rzeszutek Wilk
2017-03-05 1:04 ` Julien Grall
2017-03-06 14:22 ` Konrad Rzeszutek Wilk
2017-03-05 1:15 ` Julien Grall
2017-03-05 11:59 ` Julien Grall
2017-03-22 5:50 ` Bhupinder Thakur
2017-03-05 12:12 ` Julien Grall [this message]
2017-03-23 9:14 ` Bhupinder Thakur
2017-03-23 14:16 ` Julien Grall
2017-03-24 10:39 ` Bhupinder Thakur
2017-02-21 11:25 ` [PATCH 02/11] xen/arm: vpl011: Add new hvm params in Xen for ring buffer/event setup Bhupinder Thakur
2017-03-03 20:02 ` Konrad Rzeszutek Wilk
2017-03-24 6:58 ` Bhupinder Thakur
2017-03-05 12:35 ` Julien Grall
2017-03-06 8:06 ` Jan Beulich
2017-03-06 11:42 ` Julien Grall
2017-03-06 12:41 ` Jan Beulich
2017-03-06 13:21 ` Julien Grall
2017-03-06 13:48 ` Jan Beulich
2017-03-08 14:45 ` Julien Grall
2017-03-08 15:21 ` Jan Beulich
2017-03-08 18:22 ` Stefano Stabellini
2017-04-11 14:38 ` Bhupinder Thakur
2017-04-11 22:07 ` Stefano Stabellini
2017-04-14 7:12 ` Bhupinder Thakur
2017-04-19 18:43 ` Stefano Stabellini
2017-03-06 14:48 ` George Dunlap
2017-03-08 13:52 ` Julien Grall
2017-03-24 7:31 ` Bhupinder Thakur
2017-02-21 11:26 ` [PATCH 03/11] xen/arm: vpl011: Refactor evtchn_send in Xen to allow sending events from a xen bound channel Bhupinder Thakur
2017-03-03 21:13 ` Konrad Rzeszutek Wilk
2017-03-06 10:16 ` Bhupinder Thakur
2017-03-06 10:35 ` Jan Beulich
2017-03-05 12:39 ` Julien Grall
2017-03-06 8:15 ` Jan Beulich
2017-03-06 10:44 ` Bhupinder Thakur
2017-03-06 10:54 ` Jan Beulich
2017-03-06 11:12 ` Bhupinder Thakur
2017-03-28 9:43 ` Bhupinder Thakur
2017-02-21 11:26 ` [PATCH 04/11] xen/arm: vpl011: Enable vpl011 emulation for a domain in Xen Bhupinder Thakur
2017-03-03 21:47 ` Konrad Rzeszutek Wilk
2017-03-05 12:46 ` Julien Grall
2017-03-06 8:27 ` Jan Beulich
2017-02-21 11:26 ` [PATCH 05/11] xen/arm: vpl011: Initialize nr_spis in vgic_init in Xen to atleast 1 Bhupinder Thakur
2017-03-03 20:49 ` Konrad Rzeszutek Wilk
2017-03-05 12:51 ` Julien Grall
2017-03-16 6:50 ` Bhupinder Thakur
2017-03-16 8:24 ` Julien Grall
2017-03-16 10:31 ` Bhupinder Thakur
2017-03-16 13:24 ` Julien Grall
2017-03-20 16:29 ` Bhupinder Thakur
2017-02-21 11:26 ` [PATCH 06/11] xen/arm: vpl011: Add a new pl011 uart node in the guest DT in the toolstack Bhupinder Thakur
2017-03-03 20:15 ` Konrad Rzeszutek Wilk
2017-03-03 21:03 ` Konrad Rzeszutek Wilk
2017-03-05 12:59 ` Julien Grall
2017-03-05 13:04 ` Julien Grall
2017-03-14 13:00 ` Wei Liu
2017-02-21 11:26 ` [PATCH 07/11] xen/arm: vpl011: Add two new vpl011 parameters to xenstore Bhupinder Thakur
2017-03-03 20:58 ` Konrad Rzeszutek Wilk
2017-03-28 7:49 ` Bhupinder Thakur
2017-02-21 11:26 ` [PATCH 08/11] xen/arm: vpl011: Allocate a new PFN in the toolstack and pass to Xen using a hvm call Bhupinder Thakur
2017-03-03 20:51 ` Konrad Rzeszutek Wilk
2017-03-05 13:07 ` Julien Grall
2017-02-21 11:26 ` [PATCH 09/11] xen/arm: vpl011: Modify domain_create_ring in xenconsole to map the ring buffer and event channel Bhupinder Thakur
2017-03-03 21:46 ` Konrad Rzeszutek Wilk
2017-02-21 11:26 ` [PATCH 10/11] xen/arm: vpl011: Modify handle_ring_read and buffer_append to read/append vpl011 data Bhupinder Thakur
2017-03-03 21:06 ` Konrad Rzeszutek Wilk
2017-02-21 11:26 ` [PATCH 11/11] xen/arm: vpl011: Modify handle_tty_read in xenconsole to redirect user data to vpl011 IN ring buffer Bhupinder Thakur
2017-03-03 21:17 ` Konrad Rzeszutek Wilk
2017-03-03 20:23 ` [PATCH 00/11] pl011 emulation support in Xen Konrad Rzeszutek Wilk
2017-03-03 21:15 ` Konrad Rzeszutek Wilk
2017-03-14 7:44 ` Bhupinder Thakur
2017-03-05 11:46 ` Julien Grall
2017-03-14 7:47 ` Bhupinder Thakur
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=0f555eb4-c131-0469-44b6-ffa67e2d8537@arm.com \
--to=julien.grall@arm.com \
--cc=bhupinder.thakur@linaro.org \
--cc=nd@arm.com \
--cc=sstabellini@kernel.org \
--cc=xen-devel@lists.xenproject.org \
/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;
as well as URLs for NNTP newsgroup(s).