Commit 91ab7076 authored by unknown's avatar unknown

Background:

Since long, the compiled code of stored routines has been printed in the trace file
when starting mysqld with the "--debug" flag. (At creation time only, and only in
debug builds of course.) This has been helpful when debugging stored procedure
execution, but it's a bit awkward to use. Also, the printing of some of the
instructions is a bit terse, in particular for sp_instr_stmt where only the command
code was printed.

This improves the printout of several of the instructions, and adds the debugging-
only commands "show procedure code <name>" and "show function code <name>".
(In non-debug builds they are not available.)


sql/lex.h:
  New symbol for debug-only command (e.g. show procedure code).
sql/sp_head.cc:
  Fixed some minor debug-mode bugs in show_create_*().
  New method for debugging: sp_head::show_routine_code() - returns the "assembly code"
  for a stored routine as a result set.
  Improved the print() methods for many sp_instr* classes, particularly for
  sp_instr_stmt where the query string is printed as well (up to a max length, just
  to give a hint of which statement it is). Also print the names of variables and
  cursors in some instruction.
sql/sp_head.h:
  New debugging-only method in sp_head: show_routine_code().
  Added offset member to sp_instr_cpush for improved debug printing.
sql/sp_pcontext.cc:
  Moved find_pvar(uint i) method from sp_pcontext.h, and made it work for all
  frames, not just the first one. (For debugging purposes)
  Added a similar find_cursor(uint i, ...) method, for debugging.
sql/sp_pcontext.h:
  Moved find_pvar(uint i) method to sp_pcontext.cc.
  Added a similar find_cursor(uint i, ...) method, for debugging.
sql/sql_lex.h:
  Added new sql_command codes for debugging.
sql/sql_parse.cc:
  Added new commands for debugging, e.g. "show procedure code".
sql/sql_yacc.yy:
  Added new commands for debugging purposes:
  "show procedure code ..." and "show function code ...".
  These are only enabled in debug builds, otherwise they result in a syntax error.
  (I.e. they don't exist)
parent 14637f97
...@@ -110,6 +110,7 @@ static SYMBOL symbols[] = { ...@@ -110,6 +110,7 @@ static SYMBOL symbols[] = {
{ "CIPHER", SYM(CIPHER_SYM)}, { "CIPHER", SYM(CIPHER_SYM)},
{ "CLIENT", SYM(CLIENT_SYM)}, { "CLIENT", SYM(CLIENT_SYM)},
{ "CLOSE", SYM(CLOSE_SYM)}, { "CLOSE", SYM(CLOSE_SYM)},
{ "CODE", SYM(CODE_SYM)},
{ "COLLATE", SYM(COLLATE_SYM)}, { "COLLATE", SYM(COLLATE_SYM)},
{ "COLLATION", SYM(COLLATION_SYM)}, { "COLLATION", SYM(COLLATION_SYM)},
{ "COLUMN", SYM(COLUMN_SYM)}, { "COLUMN", SYM(COLUMN_SYM)},
......
...@@ -105,6 +105,8 @@ sp_get_flags_for_command(LEX *lex) ...@@ -105,6 +105,8 @@ sp_get_flags_for_command(LEX *lex)
case SQLCOM_SHOW_TABLES: case SQLCOM_SHOW_TABLES:
case SQLCOM_SHOW_VARIABLES: case SQLCOM_SHOW_VARIABLES:
case SQLCOM_SHOW_WARNS: case SQLCOM_SHOW_WARNS:
case SQLCOM_SHOW_PROC_CODE:
case SQLCOM_SHOW_FUNC_CODE:
flags= sp_head::MULTI_RESULTS; flags= sp_head::MULTI_RESULTS;
break; break;
/* /*
...@@ -1698,7 +1700,7 @@ sp_head::show_create_procedure(THD *thd) ...@@ -1698,7 +1700,7 @@ sp_head::show_create_procedure(THD *thd)
LINT_INIT(sql_mode_len); LINT_INIT(sql_mode_len);
if (check_show_routine_access(thd, this, &full_access)) if (check_show_routine_access(thd, this, &full_access))
return 1; DBUG_RETURN(1);
sql_mode_str= sql_mode_str=
sys_var_thd_sql_mode::symbolic_mode_representation(thd, sys_var_thd_sql_mode::symbolic_mode_representation(thd,
...@@ -1711,10 +1713,7 @@ sp_head::show_create_procedure(THD *thd) ...@@ -1711,10 +1713,7 @@ sp_head::show_create_procedure(THD *thd)
max(buffer.length(), 1024))); max(buffer.length(), 1024)));
if (protocol->send_fields(&field_list, Protocol::SEND_NUM_ROWS | if (protocol->send_fields(&field_list, Protocol::SEND_NUM_ROWS |
Protocol::SEND_EOF)) Protocol::SEND_EOF))
{ DBUG_RETURN(1);
res= 1;
goto done;
}
protocol->prepare_for_resend(); protocol->prepare_for_resend();
protocol->store(m_name.str, m_name.length, system_charset_info); protocol->store(m_name.str, m_name.length, system_charset_info);
protocol->store((char*) sql_mode_str, sql_mode_len, system_charset_info); protocol->store((char*) sql_mode_str, sql_mode_len, system_charset_info);
...@@ -1723,7 +1722,6 @@ sp_head::show_create_procedure(THD *thd) ...@@ -1723,7 +1722,6 @@ sp_head::show_create_procedure(THD *thd)
res= protocol->write(); res= protocol->write();
send_eof(thd); send_eof(thd);
done:
DBUG_RETURN(res); DBUG_RETURN(res);
} }
...@@ -1768,7 +1766,7 @@ sp_head::show_create_function(THD *thd) ...@@ -1768,7 +1766,7 @@ sp_head::show_create_function(THD *thd)
LINT_INIT(sql_mode_len); LINT_INIT(sql_mode_len);
if (check_show_routine_access(thd, this, &full_access)) if (check_show_routine_access(thd, this, &full_access))
return 1; DBUG_RETURN(1);
sql_mode_str= sql_mode_str=
sys_var_thd_sql_mode::symbolic_mode_representation(thd, sys_var_thd_sql_mode::symbolic_mode_representation(thd,
...@@ -1780,10 +1778,7 @@ sp_head::show_create_function(THD *thd) ...@@ -1780,10 +1778,7 @@ sp_head::show_create_function(THD *thd)
max(buffer.length(),1024))); max(buffer.length(),1024)));
if (protocol->send_fields(&field_list, if (protocol->send_fields(&field_list,
Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF))
{ DBUG_RETURN(1);
res= 1;
goto done;
}
protocol->prepare_for_resend(); protocol->prepare_for_resend();
protocol->store(m_name.str, m_name.length, system_charset_info); protocol->store(m_name.str, m_name.length, system_charset_info);
protocol->store((char*) sql_mode_str, sql_mode_len, system_charset_info); protocol->store((char*) sql_mode_str, sql_mode_len, system_charset_info);
...@@ -1792,7 +1787,6 @@ sp_head::show_create_function(THD *thd) ...@@ -1792,7 +1787,6 @@ sp_head::show_create_function(THD *thd)
res= protocol->write(); res= protocol->write();
send_eof(thd); send_eof(thd);
done:
DBUG_RETURN(res); DBUG_RETURN(res);
} }
...@@ -1852,6 +1846,51 @@ sp_head::opt_mark(uint ip) ...@@ -1852,6 +1846,51 @@ sp_head::opt_mark(uint ip)
} }
#ifndef DBUG_OFF
int
sp_head::show_routine_code(THD *thd)
{
Protocol *protocol= thd->protocol;
char buff[2048];
String buffer(buff, sizeof(buff), system_charset_info);
int res;
List<Item> field_list;
bool full_access;
uint ip;
sp_instr *i;
DBUG_ENTER("sp_head::show_routine_code");
DBUG_PRINT("info", ("procedure %s", m_name.str));
if (check_show_routine_access(thd, this, &full_access) || !full_access)
DBUG_RETURN(1);
field_list.push_back(new Item_uint("Pos", 9));
// 1024 is for not to confuse old clients
field_list.push_back(new Item_empty_string("Instruction",
max(buffer.length(), 1024)));
if (protocol->send_fields(&field_list, Protocol::SEND_NUM_ROWS |
Protocol::SEND_EOF))
DBUG_RETURN(1);
for (ip= 0; (i = get_instr(ip)) ; ip++)
{
protocol->prepare_for_resend();
protocol->store((longlong)ip);
buffer.set("", 0, system_charset_info);
i->print(&buffer);
protocol->store(buffer.c_ptr_quick(), buffer.length(), system_charset_info);
if ((res= protocol->write()))
break;
}
send_eof(thd);
DBUG_RETURN(res);
}
#endif // ifndef DBUG_OFF
/* /*
Prepare LEX and thread for execution of instruction, if requested open Prepare LEX and thread for execution of instruction, if requested open
and lock LEX's tables, execute instruction's core function, perform and lock LEX's tables, execute instruction's core function, perform
...@@ -2010,14 +2049,34 @@ sp_instr_stmt::execute(THD *thd, uint *nextp) ...@@ -2010,14 +2049,34 @@ sp_instr_stmt::execute(THD *thd, uint *nextp)
DBUG_RETURN(res); DBUG_RETURN(res);
} }
#define STMT_PRINT_MAXLEN 40
void void
sp_instr_stmt::print(String *str) sp_instr_stmt::print(String *str)
{ {
str->reserve(12); uint i, len;
str->reserve(STMT_PRINT_MAXLEN+20);
str->append("stmt "); str->append("stmt ");
str->qs_append((uint)m_lex_keeper.sql_command()); str->qs_append((uint)m_lex_keeper.sql_command());
str->append(" \"");
len= m_query.length;
/*
Print the query string (but not too much of it), just to indicate which
statement it is.
*/
if (len > STMT_PRINT_MAXLEN)
len= STMT_PRINT_MAXLEN-3;
/* Copy the query string and replace '\n' with ' ' in the process */
for (i= 0 ; i < len ; i++)
if (m_query.str[i] == '\n')
str->append(' ');
else
str->append(m_query.str[i]);
if (m_query.length > STMT_PRINT_MAXLEN)
str->append("..."); /* Indicate truncated string */
str->append('"');
} }
#undef STMT_PRINT_MAXLEN
int int
sp_instr_stmt::exec_core(THD *thd, uint *nextp) sp_instr_stmt::exec_core(THD *thd, uint *nextp)
...@@ -2054,8 +2113,19 @@ sp_instr_set::exec_core(THD *thd, uint *nextp) ...@@ -2054,8 +2113,19 @@ sp_instr_set::exec_core(THD *thd, uint *nextp)
void void
sp_instr_set::print(String *str) sp_instr_set::print(String *str)
{ {
str->reserve(12); int rsrv = 16;
sp_pvar_t *var = m_ctx->find_pvar(m_offset);
/* 'var' should always be non-null, but just in case... */
if (var)
rsrv+= var->name.length;
str->reserve(rsrv);
str->append("set "); str->append("set ");
if (var)
{
str->append(var->name.str, var->name.length);
str->append('@');
}
str->qs_append(m_offset); str->qs_append(m_offset);
str->append(' '); str->append(' ');
m_value->print(str); m_value->print(str);
...@@ -2346,12 +2416,26 @@ sp_instr_hpush_jump::print(String *str) ...@@ -2346,12 +2416,26 @@ sp_instr_hpush_jump::print(String *str)
str->reserve(32); str->reserve(32);
str->append("hpush_jump "); str->append("hpush_jump ");
str->qs_append(m_dest); str->qs_append(m_dest);
str->append(" t="); str->append(' ');
str->qs_append(m_type);
str->append(" f=");
str->qs_append(m_frame); str->qs_append(m_frame);
str->append(" h="); switch (m_type)
str->qs_append(m_ip+1); {
case SP_HANDLER_NONE:
str->append(" NONE"); // This would be a bug
break;
case SP_HANDLER_EXIT:
str->append(" EXIT");
break;
case SP_HANDLER_CONTINUE:
str->append(" CONTINUE");
break;
case SP_HANDLER_UNDO:
str->append(" UNDO");
break;
default:
str->append(" UNKNOWN:"); // This would be a bug as well
str->qs_append(m_type);
}
} }
uint uint
...@@ -2474,7 +2558,17 @@ sp_instr_cpush::execute(THD *thd, uint *nextp) ...@@ -2474,7 +2558,17 @@ sp_instr_cpush::execute(THD *thd, uint *nextp)
void void
sp_instr_cpush::print(String *str) sp_instr_cpush::print(String *str)
{ {
str->append("cpush"); LEX_STRING n;
my_bool found= m_ctx->find_cursor(m_cursor, &n);
str->reserve(32);
str->append("cpush ");
if (found)
{
str->append(n.str, n.length);
str->append('@');
}
str->qs_append(m_cursor);
} }
...@@ -2570,8 +2664,16 @@ sp_instr_copen::exec_core(THD *thd, uint *nextp) ...@@ -2570,8 +2664,16 @@ sp_instr_copen::exec_core(THD *thd, uint *nextp)
void void
sp_instr_copen::print(String *str) sp_instr_copen::print(String *str)
{ {
str->reserve(12); LEX_STRING n;
my_bool found= m_ctx->find_cursor(m_cursor, &n);
str->reserve(32);
str->append("copen "); str->append("copen ");
if (found)
{
str->append(n.str, n.length);
str->append('@');
}
str->qs_append(m_cursor); str->qs_append(m_cursor);
} }
...@@ -2599,8 +2701,16 @@ sp_instr_cclose::execute(THD *thd, uint *nextp) ...@@ -2599,8 +2701,16 @@ sp_instr_cclose::execute(THD *thd, uint *nextp)
void void
sp_instr_cclose::print(String *str) sp_instr_cclose::print(String *str)
{ {
str->reserve(12); LEX_STRING n;
my_bool found= m_ctx->find_cursor(m_cursor, &n);
str->reserve(32);
str->append("cclose "); str->append("cclose ");
if (found)
{
str->append(n.str, n.length);
str->append('@');
}
str->qs_append(m_cursor); str->qs_append(m_cursor);
} }
...@@ -2629,14 +2739,23 @@ sp_instr_cfetch::print(String *str) ...@@ -2629,14 +2739,23 @@ sp_instr_cfetch::print(String *str)
{ {
List_iterator_fast<struct sp_pvar> li(m_varlist); List_iterator_fast<struct sp_pvar> li(m_varlist);
sp_pvar_t *pv; sp_pvar_t *pv;
LEX_STRING n;
my_bool found= m_ctx->find_cursor(m_cursor, &n);
str->reserve(12); str->reserve(32);
str->append("cfetch "); str->append("cfetch ");
if (found)
{
str->append(n.str, n.length);
str->append('@');
}
str->qs_append(m_cursor); str->qs_append(m_cursor);
while ((pv= li++)) while ((pv= li++))
{ {
str->reserve(8); str->reserve(16);
str->append(' '); str->append(' ');
str->append(pv->name.str, pv->name.length);
str->append('@');
str->qs_append(pv->offset); str->qs_append(pv->offset);
} }
} }
......
...@@ -295,6 +295,12 @@ class sp_head :private Query_arena ...@@ -295,6 +295,12 @@ class sp_head :private Query_arena
return test(m_flags & return test(m_flags &
(CONTAINS_DYNAMIC_SQL|MULTI_RESULTS|HAS_SET_AUTOCOMMIT_STMT)); (CONTAINS_DYNAMIC_SQL|MULTI_RESULTS|HAS_SET_AUTOCOMMIT_STMT));
} }
#ifndef DBUG_OFF
int show_routine_code(THD *thd);
#endif
private: private:
MEM_ROOT *m_thd_root; // Temp. store for thd's mem_root MEM_ROOT *m_thd_root; // Temp. store for thd's mem_root
...@@ -856,8 +862,8 @@ class sp_instr_cpush : public sp_instr ...@@ -856,8 +862,8 @@ class sp_instr_cpush : public sp_instr
public: public:
sp_instr_cpush(uint ip, sp_pcontext *ctx, LEX *lex) sp_instr_cpush(uint ip, sp_pcontext *ctx, LEX *lex, uint offset)
: sp_instr(ip, ctx), m_lex_keeper(lex, TRUE) : sp_instr(ip, ctx), m_lex_keeper(lex, TRUE), m_cursor(offset)
{} {}
virtual ~sp_instr_cpush() virtual ~sp_instr_cpush()
...@@ -876,6 +882,7 @@ class sp_instr_cpush : public sp_instr ...@@ -876,6 +882,7 @@ class sp_instr_cpush : public sp_instr
private: private:
sp_lex_keeper m_lex_keeper; sp_lex_keeper m_lex_keeper;
uint m_cursor; /* Frame offset (for debugging) */
}; // class sp_instr_cpush : public sp_instr }; // class sp_instr_cpush : public sp_instr
......
...@@ -169,6 +169,29 @@ sp_pcontext::find_pvar(LEX_STRING *name, my_bool scoped) ...@@ -169,6 +169,29 @@ sp_pcontext::find_pvar(LEX_STRING *name, my_bool scoped)
return NULL; return NULL;
} }
/*
Find a variable by offset from the top.
This used for two things:
- When evaluating parameters at the beginning, and setting out parameters
at the end, of invokation. (Top frame only, so no recursion then.)
- For printing of sp_instr_set. (Debug mode only.)
*/
sp_pvar_t *
sp_pcontext::find_pvar(uint i)
{
if (m_poffset <= i && i < m_poffset + m_pvar.elements)
{ // This frame
sp_pvar_t *p;
get_dynamic(&m_pvar, (gptr)&p, i - m_poffset);
return p;
}
else if (m_parent)
return m_parent->find_pvar(i); // Some previous frame
else
return NULL; // index out of bounds
}
void void
sp_pcontext::push_pvar(LEX_STRING *name, enum enum_field_types type, sp_pcontext::push_pvar(LEX_STRING *name, enum enum_field_types type,
sp_param_mode_t mode) sp_param_mode_t mode)
...@@ -331,3 +354,22 @@ sp_pcontext::find_cursor(LEX_STRING *name, uint *poff, my_bool scoped) ...@@ -331,3 +354,22 @@ sp_pcontext::find_cursor(LEX_STRING *name, uint *poff, my_bool scoped)
return m_parent->find_cursor(name, poff, scoped); return m_parent->find_cursor(name, poff, scoped);
return FALSE; return FALSE;
} }
/*
Find a cursor by offset from the top.
This is only used for debugging.
*/
my_bool
sp_pcontext::find_cursor(uint i, LEX_STRING *n)
{
if (m_coffset <= i && i < m_coffset + m_cursor.elements)
{ // This frame
get_dynamic(&m_cursor, (gptr)n, i - m_poffset);
return TRUE;
}
else if (m_parent)
return m_parent->find_cursor(i, n); // Some previous frame
else
return FALSE; // index out of bounds
}
...@@ -172,16 +172,7 @@ class sp_pcontext : public Sql_alloc ...@@ -172,16 +172,7 @@ class sp_pcontext : public Sql_alloc
// Find by index // Find by index
sp_pvar_t * sp_pvar_t *
find_pvar(uint i) find_pvar(uint i);
{
sp_pvar_t *p;
if (i < m_pvar.elements)
get_dynamic(&m_pvar, (gptr)&p, i);
else
p= NULL;
return p;
}
// //
// Labels // Labels
...@@ -261,6 +252,10 @@ class sp_pcontext : public Sql_alloc ...@@ -261,6 +252,10 @@ class sp_pcontext : public Sql_alloc
my_bool my_bool
find_cursor(LEX_STRING *name, uint *poff, my_bool scoped=0); find_cursor(LEX_STRING *name, uint *poff, my_bool scoped=0);
/* Find by index (for debugging only) */
my_bool
find_cursor(uint i, LEX_STRING *n);
inline uint inline uint
max_cursors() max_cursors()
{ {
......
...@@ -90,6 +90,7 @@ enum enum_sql_command { ...@@ -90,6 +90,7 @@ enum enum_sql_command {
SQLCOM_CREATE_TRIGGER, SQLCOM_DROP_TRIGGER, SQLCOM_CREATE_TRIGGER, SQLCOM_DROP_TRIGGER,
SQLCOM_XA_START, SQLCOM_XA_END, SQLCOM_XA_PREPARE, SQLCOM_XA_START, SQLCOM_XA_END, SQLCOM_XA_PREPARE,
SQLCOM_XA_COMMIT, SQLCOM_XA_ROLLBACK, SQLCOM_XA_RECOVER, SQLCOM_XA_COMMIT, SQLCOM_XA_ROLLBACK, SQLCOM_XA_RECOVER,
SQLCOM_SHOW_PROC_CODE, SQLCOM_SHOW_FUNC_CODE,
/* This should be the last !!! */ /* This should be the last !!! */
SQLCOM_END SQLCOM_END
......
...@@ -4560,6 +4560,30 @@ mysql_execute_command(THD *thd) ...@@ -4560,6 +4560,30 @@ mysql_execute_command(THD *thd)
lex->wild->ptr() : NullS)); lex->wild->ptr() : NullS));
break; break;
} }
#ifndef DBUG_OFF
case SQLCOM_SHOW_PROC_CODE:
case SQLCOM_SHOW_FUNC_CODE:
{
sp_head *sp;
if (lex->spname->m_name.length > NAME_LEN)
{
my_error(ER_TOO_LONG_IDENT, MYF(0), lex->spname->m_name.str);
goto error;
}
if (lex->sql_command == SQLCOM_SHOW_PROC_CODE)
sp= sp_find_procedure(thd, lex->spname);
else
sp= sp_find_function(thd, lex->spname);
if (!sp || !sp->show_routine_code(thd))
{ /* We don't distinguish between errors for now */
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
SP_COM_STRING(lex), lex->spname->m_name.str);
goto error;
}
break;
}
#endif // ifndef DBUG_OFF
case SQLCOM_CREATE_VIEW: case SQLCOM_CREATE_VIEW:
{ {
if (end_active_trans(thd)) if (end_active_trans(thd))
......
...@@ -175,6 +175,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, ulong *yystacksize); ...@@ -175,6 +175,7 @@ bool my_yyoverflow(short **a, YYSTYPE **b, ulong *yystacksize);
%token CLIENT_SYM %token CLIENT_SYM
%token CLOSE_SYM %token CLOSE_SYM
%token COALESCE %token COALESCE
%token CODE_SYM
%token COLLATE_SYM %token COLLATE_SYM
%token COLLATION_SYM %token COLLATION_SYM
%token COLUMNS %token COLUMNS
...@@ -1698,7 +1699,8 @@ sp_decl: ...@@ -1698,7 +1699,8 @@ sp_decl:
delete $5; delete $5;
YYABORT; YYABORT;
} }
i= new sp_instr_cpush(sp->instructions(), ctx, $5); i= new sp_instr_cpush(sp->instructions(), ctx, $5,
ctx->current_cursors());
sp->add_instr(i); sp->add_instr(i);
ctx->push_cursor(&$2); ctx->push_cursor(&$2);
$$.vars= $$.conds= $$.hndlrs= 0; $$.vars= $$.conds= $$.hndlrs= 0;
...@@ -6625,7 +6627,28 @@ show_param: ...@@ -6625,7 +6627,28 @@ show_param:
YYABORT; YYABORT;
if (prepare_schema_table(YYTHD, lex, 0, SCH_PROCEDURES)) if (prepare_schema_table(YYTHD, lex, 0, SCH_PROCEDURES))
YYABORT; YYABORT;
}; }
| PROCEDURE CODE_SYM sp_name
{
#ifdef DBUG_OFF
yyerror(ER(ER_SYNTAX_ERROR));
YYABORT;
#else
Lex->sql_command= SQLCOM_SHOW_PROC_CODE;
Lex->spname= $3;
#endif
}
| FUNCTION_SYM CODE_SYM sp_name
{
#ifdef DBUG_OFF
yyerror(ER(ER_SYNTAX_ERROR));
YYABORT;
#else
Lex->sql_command= SQLCOM_SHOW_FUNC_CODE;
Lex->spname= $3;
#endif
}
;
show_engine_param: show_engine_param:
STATUS_SYM STATUS_SYM
...@@ -7534,6 +7557,7 @@ keyword_sp: ...@@ -7534,6 +7557,7 @@ keyword_sp:
| CHANGED {} | CHANGED {}
| CIPHER_SYM {} | CIPHER_SYM {}
| CLIENT_SYM {} | CLIENT_SYM {}
| CODE_SYM {}
| COLLATION_SYM {} | COLLATION_SYM {}
| COLUMNS {} | COLUMNS {}
| COMMITTED_SYM {} | COMMITTED_SYM {}
......
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