Commit 6b38193b authored by Kai Mäkisara's avatar Kai Mäkisara Committed by Linus Torvalds

[PATCH] SCSI tape EOT write fixes

This contains the following changes:
 - EOT detection fixed when writing in fixed block mode
 - asynchronous writes in fixed block mode and write threshold removed
   to enable the EOT fixes (the parameter accepted for compatibility)
parent 69ca9ecd
This file contains brief information about the SCSI tape driver. This file contains brief information about the SCSI tape driver.
The driver is currently maintained by Kai Mäkisara (email The driver is currently maintained by Kai Mäkisara (email
Kai.Makisara@metla.fi) Kai.Makisara@kolumbus.fi)
Last modified: Sat Dec 14 14:35:30 2002 by makisara Last modified: Sun Apr 6 22:44:13 2003 by makisara
BASICS BASICS
...@@ -132,7 +132,7 @@ minimum allowable buffer size. ...@@ -132,7 +132,7 @@ minimum allowable buffer size.
Asynchronous writing. Writing the buffer contents to the tape is Asynchronous writing. Writing the buffer contents to the tape is
started and the write call returns immediately. The status is checked started and the write call returns immediately. The status is checked
at the next tape operation. Asynchronous writes are not done with at the next tape operation. Asynchronous writes are not done with
direct i/o. direct i/o and not in fixed block mode.
Buffered writes and asynchronous writes may in some rare cases cause Buffered writes and asynchronous writes may in some rare cases cause
problems in multivolume operations if there is not enough space on the problems in multivolume operations if there is not enough space on the
...@@ -143,11 +143,6 @@ attempted even if the user does not want to get all of the data at ...@@ -143,11 +143,6 @@ attempted even if the user does not want to get all of the data at
this read command. Should be disabled for those drives that don't like this read command. Should be disabled for those drives that don't like
a filemark to truncate a read request or that don't like backspacing. a filemark to truncate a read request or that don't like backspacing.
The threshold for triggering asynchronous write in fixed block mode
is defined by ST_WRITE_THRESHOLD. This may be optimized for each
use pattern. The default triggers asynchronous write after three
default sized writes (10 kB) from tar.
Scatter/gather buffers (buffers that consist of chunks non-contiguous Scatter/gather buffers (buffers that consist of chunks non-contiguous
in the physical memory) are used if contiguous buffers can't be in the physical memory) are used if contiguous buffers can't be
allocated. To support all SCSI adapters (including those not allocated. To support all SCSI adapters (including those not
......
...@@ -9,16 +9,16 @@ ...@@ -9,16 +9,16 @@
Steve Hirsch, Andreas Koppenh"ofer, Michael Leodolter, Eyal Lebedinsky, Steve Hirsch, Andreas Koppenh"ofer, Michael Leodolter, Eyal Lebedinsky,
Michael Schaefer, J"org Weule, and Eric Youngdale. Michael Schaefer, J"org Weule, and Eric Youngdale.
Copyright 1992 - 2002 Kai Makisara Copyright 1992 - 2003 Kai Makisara
email Kai.Makisara@metla.fi email Kai.Makisara@kolumbus.fi
Last modified: Sat Dec 14 14:25:09 2002 by makisara Last modified: Sat Apr 12 20:20:39 2003 by makisara
Some small formal changes - aeb, 950809 Some small formal changes - aeb, 950809
Last modified: 18-JAN-1998 Richard Gooch <rgooch@atnf.csiro.au> Devfs support Last modified: 18-JAN-1998 Richard Gooch <rgooch@atnf.csiro.au> Devfs support
*/ */
static char *verstr = "20021214"; static char *verstr = "20030412";
#include <linux/module.h> #include <linux/module.h>
...@@ -65,7 +65,6 @@ static char *verstr = "20021214"; ...@@ -65,7 +65,6 @@ static char *verstr = "20021214";
#include "st.h" #include "st.h"
static int buffer_kbs; static int buffer_kbs;
static int write_threshold_kbs;
static int max_sg_segs; static int max_sg_segs;
static int try_direct_io = TRY_DIRECT_IO; static int try_direct_io = TRY_DIRECT_IO;
static int try_rdio = TRUE; static int try_rdio = TRUE;
...@@ -80,8 +79,6 @@ MODULE_LICENSE("GPL"); ...@@ -80,8 +79,6 @@ MODULE_LICENSE("GPL");
MODULE_PARM(buffer_kbs, "i"); MODULE_PARM(buffer_kbs, "i");
MODULE_PARM_DESC(buffer_kbs, "Default driver buffer size for fixed block mode (KB; 32)"); MODULE_PARM_DESC(buffer_kbs, "Default driver buffer size for fixed block mode (KB; 32)");
MODULE_PARM(write_threshold_kbs, "i");
MODULE_PARM_DESC(write_threshold_kbs, "Asynchronous write threshold (KB; 30)");
MODULE_PARM(max_sg_segs, "i"); MODULE_PARM(max_sg_segs, "i");
MODULE_PARM_DESC(max_sg_segs, "Maximum number of scatter/gather segments to use (256)"); MODULE_PARM_DESC(max_sg_segs, "Maximum number of scatter/gather segments to use (256)");
MODULE_PARM(try_direct_io, "i"); MODULE_PARM(try_direct_io, "i");
...@@ -94,6 +91,7 @@ MODULE_PARM(try_wdio, "i"); ...@@ -94,6 +91,7 @@ MODULE_PARM(try_wdio, "i");
MODULE_PARM_DESC(try_wdio, "Try direct write i/o when possible"); MODULE_PARM_DESC(try_wdio, "Try direct write i/o when possible");
#ifndef MODULE #ifndef MODULE
static int write_threshold_kbs; /* retained for compatibility */
static struct st_dev_parm { static struct st_dev_parm {
char *name; char *name;
int *val; int *val;
...@@ -101,11 +99,11 @@ static struct st_dev_parm { ...@@ -101,11 +99,11 @@ static struct st_dev_parm {
{ {
"buffer_kbs", &buffer_kbs "buffer_kbs", &buffer_kbs
}, },
{ { /* Retained for compatibility with 2.4 */
"write_threshold_kbs", &write_threshold_kbs "write_threshold_kbs", &write_threshold_kbs
}, },
{ {
"max_sg_segs", &max_sg_segs "max_sg_segs", NULL
}, },
{ {
"try_direct_io", &try_direct_io "try_direct_io", &try_direct_io
...@@ -117,7 +115,6 @@ static struct st_dev_parm { ...@@ -117,7 +115,6 @@ static struct st_dev_parm {
/* The default definitions have been moved to st_options.h */ /* The default definitions have been moved to st_options.h */
#define ST_FIXED_BUFFER_SIZE (ST_FIXED_BUFFER_BLOCKS * ST_KILOBYTE) #define ST_FIXED_BUFFER_SIZE (ST_FIXED_BUFFER_BLOCKS * ST_KILOBYTE)
#define ST_WRITE_THRESHOLD (ST_WRITE_THRESHOLD_BLOCKS * ST_KILOBYTE)
/* The buffer size should fit into the 24 bits for length in the /* The buffer size should fit into the 24 bits for length in the
6-byte SCSI read and write commands. */ 6-byte SCSI read and write commands. */
...@@ -146,7 +143,6 @@ DEB( static int debugging = DEBUG; ) ...@@ -146,7 +143,6 @@ DEB( static int debugging = DEBUG; )
static rwlock_t st_dev_arr_lock = RW_LOCK_UNLOCKED; static rwlock_t st_dev_arr_lock = RW_LOCK_UNLOCKED;
static int st_fixed_buffer_size = ST_FIXED_BUFFER_SIZE; static int st_fixed_buffer_size = ST_FIXED_BUFFER_SIZE;
static int st_write_threshold = ST_WRITE_THRESHOLD;
static int st_max_sg_segs = ST_MAX_SG; static int st_max_sg_segs = ST_MAX_SG;
static Scsi_Tape **scsi_tapes = NULL; static Scsi_Tape **scsi_tapes = NULL;
...@@ -158,6 +154,7 @@ static int enlarge_buffer(ST_buffer *, int, int); ...@@ -158,6 +154,7 @@ static int enlarge_buffer(ST_buffer *, int, int);
static void normalize_buffer(ST_buffer *); static void normalize_buffer(ST_buffer *);
static int append_to_buffer(const char *, ST_buffer *, int); static int append_to_buffer(const char *, ST_buffer *, int);
static int from_buffer(ST_buffer *, char *, int); static int from_buffer(ST_buffer *, char *, int);
static void move_buffer_data(ST_buffer *, int);
static void buf_to_sg(ST_buffer *, unsigned int); static void buf_to_sg(ST_buffer *, unsigned int);
static int st_map_user_pages(struct scatterlist *, const unsigned int, static int st_map_user_pages(struct scatterlist *, const unsigned int,
...@@ -1313,7 +1310,7 @@ static ssize_t ...@@ -1313,7 +1310,7 @@ static ssize_t
ssize_t total; ssize_t total;
ssize_t i, do_count, blks, transfer; ssize_t i, do_count, blks, transfer;
ssize_t retval; ssize_t retval;
int undone; int undone, retry_eot = 0, scode;
int async_write; int async_write;
unsigned char cmd[MAX_COMMAND_SIZE]; unsigned char cmd[MAX_COMMAND_SIZE];
const char *b_point; const char *b_point;
...@@ -1416,7 +1413,7 @@ static ssize_t ...@@ -1416,7 +1413,7 @@ static ssize_t
STps->rw = ST_WRITING; STps->rw = ST_WRITING;
b_point = buf; b_point = buf;
while (count > 0) { while (count > 0 && !retry_eot) {
if (STbp->do_dio) { if (STbp->do_dio) {
do_count = count; do_count = count;
...@@ -1441,12 +1438,8 @@ static ssize_t ...@@ -1441,12 +1438,8 @@ static ssize_t
filp->f_pos += do_count; filp->f_pos += do_count;
b_point += do_count; b_point += do_count;
async_write = !STbp->do_dio && async_write = STp->block_size == 0 && !STbp->do_dio &&
STm->do_async_writes && STps->eof < ST_EOM_OK; STm->do_async_writes && STps->eof < ST_EOM_OK;
if (STp->block_size != 0)
async_write &= count == 0 &&
(!STm->do_buffer_writes ||
STbp->buffer_bytes >= STp->write_threshold);
if (STp->block_size != 0 && STm->do_buffer_writes && if (STp->block_size != 0 && STm->do_buffer_writes &&
!(STp->try_dio && try_wdio) && STps->eof < ST_EOM_OK && !(STp->try_dio && try_wdio) && STps->eof < ST_EOM_OK &&
...@@ -1457,12 +1450,15 @@ static ssize_t ...@@ -1457,12 +1450,15 @@ static ssize_t
break; break;
} }
retry_write:
if (STp->block_size == 0) if (STp->block_size == 0)
blks = transfer = do_count; blks = transfer = do_count;
else { else {
if (!STbp->do_dio) if (!STbp->do_dio)
do_count = STbp->buffer_bytes; blks = STbp->buffer_bytes;
blks = do_count / STp->block_size; else
blks = do_count;
blks /= STp->block_size;
transfer = blks * STp->block_size; transfer = blks * STp->block_size;
} }
cmd[2] = blks >> 16; cmd[2] = blks >> 16;
...@@ -1488,15 +1484,15 @@ static ssize_t ...@@ -1488,15 +1484,15 @@ static ssize_t
DEBC(printk(ST_DEB_MSG "%s: Error on write:\n", name)); DEBC(printk(ST_DEB_MSG "%s: Error on write:\n", name));
if ((SRpnt->sr_sense_buffer[0] & 0x70) == 0x70 && if ((SRpnt->sr_sense_buffer[0] & 0x70) == 0x70 &&
(SRpnt->sr_sense_buffer[2] & 0x40)) { (SRpnt->sr_sense_buffer[2] & 0x40)) {
scode = SRpnt->sr_sense_buffer[2] & 0x0f;
if ((SRpnt->sr_sense_buffer[0] & 0x80) != 0) if ((SRpnt->sr_sense_buffer[0] & 0x80) != 0)
undone = (SRpnt->sr_sense_buffer[3] << 24) | undone = (SRpnt->sr_sense_buffer[3] << 24) |
(SRpnt->sr_sense_buffer[4] << 16) | (SRpnt->sr_sense_buffer[4] << 16) |
(SRpnt->sr_sense_buffer[5] << 8) | (SRpnt->sr_sense_buffer[5] << 8) |
SRpnt->sr_sense_buffer[6]; SRpnt->sr_sense_buffer[6];
else if (STp->block_size == 0 && else if (STp->block_size == 0 &&
(SRpnt->sr_sense_buffer[2] & 0x0f) == scode == VOLUME_OVERFLOW)
VOLUME_OVERFLOW) undone = transfer;
undone = do_count;
else else
undone = 0; undone = 0;
if (STp->block_size != 0) if (STp->block_size != 0)
...@@ -1504,7 +1500,7 @@ static ssize_t ...@@ -1504,7 +1500,7 @@ static ssize_t
filp->f_pos -= undone; filp->f_pos -= undone;
if (undone <= do_count) { if (undone <= do_count) {
/* Only data from this write is not written */ /* Only data from this write is not written */
count -= undone; count += undone;
do_count -= undone; do_count -= undone;
if (STp->block_size) if (STp->block_size)
blks = (transfer - undone) / STp->block_size; blks = (transfer - undone) / STp->block_size;
...@@ -1514,21 +1510,40 @@ static ssize_t ...@@ -1514,21 +1510,40 @@ static ssize_t
(retval left to zero) (retval left to zero)
*/ */
if (STp->block_size == 0 || if (STp->block_size == 0 ||
undone > 0 || count == 0) undone > 0 || count == 0)
retval = (-ENOSPC); /* EOM within current request */ retval = (-ENOSPC); /* EOM within current request */
DEBC(printk(ST_DEB_MSG DEBC(printk(ST_DEB_MSG
"%s: EOM with %d bytes unwritten.\n", "%s: EOM with %d bytes unwritten.\n",
name, transfer)); name, count));
} else { } else {
/* Previously buffered data not written */ /* EOT within data buffered earlier (possible only
count -= do_count; in fixed block mode without direct i/o) */
blks = do_count = 0; if (!retry_eot && (SRpnt->sr_sense_buffer[0] & 1) == 0 &&
STps->eof = ST_EOM_ERROR; (scode == NO_SENSE || scode == RECOVERED_ERROR)) {
STps->drv_block = (-1); /* Too cautious? */ move_buffer_data(STp->buffer, transfer - undone);
retval = (-EIO); /* EOM for old data */ retry_eot = TRUE;
DEBC(printk(ST_DEB_MSG if (STps->drv_block >= 0) {
"%s: EOM with lost data.\n", STps->drv_block += (transfer - undone) /
name)); STp->block_size;
}
STps->eof = ST_EOM_OK;
DEBC(printk(ST_DEB_MSG
"%s: Retry write of %d bytes at EOM.\n",
name, STp->buffer->buffer_bytes));
goto retry_write;
}
else {
/* Either error within data buffered by driver or
failed retry */
count -= do_count;
blks = do_count = 0;
STps->eof = ST_EOM_ERROR;
STps->drv_block = (-1); /* Too cautious? */
retval = (-EIO); /* EOM for old data */
DEBC(printk(ST_DEB_MSG
"%s: EOM with lost data.\n",
name));
}
} }
} else { } else {
filp->f_pos -= do_count; filp->f_pos -= do_count;
...@@ -1548,7 +1563,7 @@ static ssize_t ...@@ -1548,7 +1563,7 @@ static ssize_t
STbp->buffer_bytes = 0; STbp->buffer_bytes = 0;
STp->dirty = 0; STp->dirty = 0;
if (retval) { if (retval || retry_eot) {
if (count < total) if (count < total)
retval = total - count; retval = total - count;
goto out; goto out;
...@@ -1557,9 +1572,9 @@ static ssize_t ...@@ -1557,9 +1572,9 @@ static ssize_t
if (STps->eof == ST_EOD_1) if (STps->eof == ST_EOD_1)
STps->eof = ST_EOM_OK; STps->eof = ST_EOM_OK;
else else if (STps->eof != ST_EOM_OK)
STps->eof = ST_NOEOF; STps->eof = ST_NOEOF;
retval = total; retval = total - count;
out: out:
if (SRpnt != NULL) if (SRpnt != NULL)
...@@ -1994,16 +2009,7 @@ static int st_set_options(Scsi_Tape *STp, long options) ...@@ -1994,16 +2009,7 @@ static int st_set_options(Scsi_Tape *STp, long options)
debugging = value; ) debugging = value; )
st_log_options(STp, STm, name); st_log_options(STp, STm, name);
} else if (code == MT_ST_WRITE_THRESHOLD) { } else if (code == MT_ST_WRITE_THRESHOLD) {
value = (options & ~MT_ST_OPTIONS) * ST_KILOBYTE; /* Retained for compatibility */
if (value < 1 || value > st_fixed_buffer_size) {
printk(KERN_WARNING
"%s: Write threshold %d too small or too large.\n",
name, value);
return (-EIO);
}
STp->write_threshold = value;
printk(KERN_INFO "%s: Write threshold set to %d bytes.\n",
name, value);
} else if (code == MT_ST_DEF_BLKSIZE) { } else if (code == MT_ST_DEF_BLKSIZE) {
value = (options & ~MT_ST_OPTIONS); value = (options & ~MT_ST_OPTIONS);
if (value == ~MT_ST_OPTIONS) { if (value == ~MT_ST_OPTIONS) {
...@@ -3535,6 +3541,48 @@ static int from_buffer(ST_buffer * st_bp, char *ubp, int do_count) ...@@ -3535,6 +3541,48 @@ static int from_buffer(ST_buffer * st_bp, char *ubp, int do_count)
} }
/* Move data towards start of buffer */
static void move_buffer_data(ST_buffer * st_bp, int offset)
{
int src_seg, dst_seg, src_offset = 0, dst_offset;
int count, total;
if (offset == 0)
return;
total=st_bp->buffer_bytes - offset;
for (src_seg=0; src_seg < st_bp->frp_segs; src_seg++) {
src_offset = offset;
if (src_offset < st_bp->frp[src_seg].length)
break;
offset -= st_bp->frp[src_seg].length;
}
if (src_seg == st_bp->frp_segs) { /* Should never happen */
printk(KERN_WARNING "st: move_buffer offset overflow.\n");
return;
}
st_bp->buffer_bytes = st_bp->read_pointer = total;
for (dst_seg=dst_offset=0; total > 0; ) {
count = min(st_bp->frp[dst_seg].length - dst_offset,
st_bp->frp[src_seg].length - src_offset);
memmove(page_address(st_bp->frp[dst_seg].page) + dst_offset,
page_address(st_bp->frp[src_seg].page) + src_offset, count);
src_offset += count;
if (src_offset >= st_bp->frp[src_seg].length) {
src_seg++;
src_offset = 0;
}
dst_offset += count;
if (dst_offset >= st_bp->frp[dst_seg].length) {
dst_seg++;
dst_offset = 0;
}
total -= count;
}
}
/* Fill the s/g list up to the length required for this transfer */ /* Fill the s/g list up to the length required for this transfer */
static void buf_to_sg(ST_buffer *STbp, unsigned int length) static void buf_to_sg(ST_buffer *STbp, unsigned int length)
{ {
...@@ -3567,15 +3615,6 @@ static void validate_options(void) ...@@ -3567,15 +3615,6 @@ static void validate_options(void)
{ {
if (buffer_kbs > 0) if (buffer_kbs > 0)
st_fixed_buffer_size = buffer_kbs * ST_KILOBYTE; st_fixed_buffer_size = buffer_kbs * ST_KILOBYTE;
if (write_threshold_kbs > 0)
st_write_threshold = write_threshold_kbs * ST_KILOBYTE;
else if (buffer_kbs > 0)
st_write_threshold = st_fixed_buffer_size - 2048;
if (st_write_threshold > st_fixed_buffer_size) {
st_write_threshold = st_fixed_buffer_size;
printk(KERN_WARNING "st: write_threshold limited to %d bytes.\n",
st_write_threshold);
}
if (max_sg_segs >= ST_FIRST_SG) if (max_sg_segs >= ST_FIRST_SG)
st_max_sg_segs = max_sg_segs; st_max_sg_segs = max_sg_segs;
} }
...@@ -3592,15 +3631,20 @@ static int __init st_setup(char *str) ...@@ -3592,15 +3631,20 @@ static int __init st_setup(char *str)
if (ints[0] > 0) { if (ints[0] > 0) {
for (i = 0; i < ints[0] && i < ARRAY_SIZE(parms); i++) for (i = 0; i < ints[0] && i < ARRAY_SIZE(parms); i++)
*parms[i].val = ints[i + 1]; if (parms[i].val)
*parms[i].val = ints[i + 1];
} else { } else {
while (stp != NULL) { while (stp != NULL) {
for (i = 0; i < ARRAY_SIZE(parms); i++) { for (i = 0; i < ARRAY_SIZE(parms); i++) {
len = strlen(parms[i].name); len = strlen(parms[i].name);
if (!strncmp(stp, parms[i].name, len) && if (!strncmp(stp, parms[i].name, len) &&
(*(stp + len) == ':' || *(stp + len) == '=')) { (*(stp + len) == ':' || *(stp + len) == '=')) {
*parms[i].val = if (parms[i].val)
simple_strtoul(stp + len + 1, NULL, 0); *parms[i].val =
simple_strtoul(stp + len + 1, NULL, 0);
else
printk(KERN_WARNING "st: Obsolete parameter %s\n",
parms[i].name);
break; break;
} }
} }
...@@ -3763,7 +3807,6 @@ static int st_attach(Scsi_Device * SDp) ...@@ -3763,7 +3807,6 @@ static int st_attach(Scsi_Device * SDp)
tpnt->fast_mteom = ST_FAST_MTEOM; tpnt->fast_mteom = ST_FAST_MTEOM;
tpnt->scsi2_logical = ST_SCSI2LOGICAL; tpnt->scsi2_logical = ST_SCSI2LOGICAL;
tpnt->immediate = ST_NOWAIT; tpnt->immediate = ST_NOWAIT;
tpnt->write_threshold = st_write_threshold;
tpnt->default_drvbuffer = 0xff; /* No forced buffering */ tpnt->default_drvbuffer = 0xff; /* No forced buffering */
tpnt->partition = 0; tpnt->partition = 0;
tpnt->new_partition = 0; tpnt->new_partition = 0;
...@@ -3925,10 +3968,8 @@ static int __init init_st(void) ...@@ -3925,10 +3968,8 @@ static int __init init_st(void)
validate_options(); validate_options();
printk(KERN_INFO printk(KERN_INFO
"st: Version %s, fixed bufsize %d, wrt %d, " "st: Version %s, fixed bufsize %d, s/g segs %d\n",
"s/g segs %d\n", verstr, st_fixed_buffer_size, st_max_sg_segs);
verstr, st_fixed_buffer_size, st_write_threshold,
st_max_sg_segs);
if (register_chrdev(SCSI_TAPE_MAJOR, "st", &st_fops) >= 0) { if (register_chrdev(SCSI_TAPE_MAJOR, "st", &st_fops) >= 0) {
if (scsi_register_device(&st_template) == 0) if (scsi_register_device(&st_template) == 0)
......
...@@ -96,7 +96,6 @@ typedef struct { ...@@ -96,7 +96,6 @@ typedef struct {
unsigned char c_algo; /* compression algorithm */ unsigned char c_algo; /* compression algorithm */
unsigned char pos_unknown; /* after reset position unknown */ unsigned char pos_unknown; /* after reset position unknown */
int tape_type; int tape_type;
int write_threshold;
int timeout; /* timeout for normal commands */ int timeout; /* timeout for normal commands */
int long_timeout; /* timeout for commands known to take long time */ int long_timeout; /* timeout for commands known to take long time */
......
/* /*
The compile-time configurable defaults for the Linux SCSI tape driver. The compile-time configurable defaults for the Linux SCSI tape driver.
Copyright 1995-2002 Kai Makisara. Copyright 1995-2003 Kai Makisara.
Last modified: Fri Jul 26 15:54:31 2002 by makisara Last modified: Mon Apr 7 22:49:18 2003 by makisara
*/ */
#ifndef _ST_OPTIONS_H #ifndef _ST_OPTIONS_H
...@@ -39,11 +39,6 @@ ...@@ -39,11 +39,6 @@
Must be non-zero. */ Must be non-zero. */
#define ST_FIXED_BUFFER_BLOCKS 32 #define ST_FIXED_BUFFER_BLOCKS 32
/* The number of kilobytes of data in the buffer that triggers an
asynchronous write in fixed block mode. See also ST_ASYNC_WRITES
below. */
#define ST_WRITE_THRESHOLD_BLOCKS 30
/* Maximum number of scatter/gather segments */ /* Maximum number of scatter/gather segments */
#define ST_MAX_SG 256 #define ST_MAX_SG 256
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment