Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
Actor Framework
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
cpp-libs
Actor Framework
Commits
ecee4ead
Commit
ecee4ead
authored
Nov 17, 2018
by
Dominik Charousset
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix logger output for class names
parent
bfce1cee
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
285 additions
and
97 deletions
+285
-97
libcaf_core/caf/logger.hpp
libcaf_core/caf/logger.hpp
+34
-27
libcaf_core/src/logger.cpp
libcaf_core/src/logger.cpp
+161
-62
libcaf_core/test/logger.cpp
libcaf_core/test/logger.cpp
+90
-8
No files found.
libcaf_core/caf/logger.hpp
View file @
ecee4ead
...
...
@@ -27,14 +27,15 @@
#include <type_traits>
#include <unordered_map>
#include "caf/fwd.hpp"
#include "caf/config.hpp"
#include "caf/unifyn.hpp"
#include "caf/type_nr.hpp"
#include "caf/timestamp.hpp"
#include "caf/ref_counted.hpp"
#include "caf/abstract_actor.hpp"
#include "caf/config.hpp"
#include "caf/deep_to_string.hpp"
#include "caf/fwd.hpp"
#include "caf/ref_counted.hpp"
#include "caf/string_view.hpp"
#include "caf/timestamp.hpp"
#include "caf/type_nr.hpp"
#include "caf/unifyn.hpp"
#include "caf/intrusive/drr_queue.hpp"
#include "caf/intrusive/fifo_inbox.hpp"
...
...
@@ -71,20 +72,24 @@ public:
struct
event
:
intrusive
::
singly_linked
<
event
>
{
event
()
=
default
;
event
(
int
lvl
,
const
char
*
cat
,
const
char
*
fun
,
const
char
*
fn
,
int
line
,
std
::
string
msg
,
std
::
thread
::
id
t
,
actor_id
a
,
timestamp
ts
);
event
(
int
lvl
,
string_view
cat
,
string_view
full_fun
,
string_view
fun
,
string_view
fn
,
int
line
,
std
::
string
msg
,
std
::
thread
::
id
t
,
actor_id
a
,
timestamp
ts
);
/// Level/priority of the event.
int
level
;
/// Name of the category (component) logging the event.
const
char
*
category_name
;
string_view
category_name
;
/// Name of the current context as reported by `__PRETTY_FUNCTION__`.
const
char
*
pretty_fun
;
/// Name of the current function as reported by `__PRETTY_FUNCTION__`.
string_view
pretty_fun
;
/// Name of the current function as reported by `__func__`.
string_view
simple_fun
;
/// Name of the current file.
const
char
*
file_name
;
string_view
file_name
;
/// Current line in the file.
int
line_number
;
...
...
@@ -179,6 +184,8 @@ public:
line_builder
&
operator
<<
(
const
std
::
string
&
str
);
line_builder
&
operator
<<
(
string_view
str
);
line_builder
&
operator
<<
(
const
char
*
str
);
line_builder
&
operator
<<
(
char
x
);
...
...
@@ -206,12 +213,7 @@ public:
static
logger
*
current_logger
();
bool
accepts
(
int
level
,
const
char
*
cname_begin
,
const
char
*
cname_end
);
template
<
size_t
N
>
bool
accepts
(
int
level
,
const
char
(
&
component
)[
N
])
{
return
accepts
(
level
,
component
,
component
+
N
-
1
);
}
bool
accepts
(
int
level
,
string_view
cname
);
/** @endcond */
...
...
@@ -226,10 +228,10 @@ public:
}
/// Renders the prefix (namespace and class) of a fully qualified function.
static
void
render_fun_prefix
(
std
::
ostream
&
out
,
const
char
*
pretty_fun
);
static
void
render_fun_prefix
(
std
::
ostream
&
out
,
const
event
&
x
);
/// Renders the name of a fully qualified function.
static
void
render_fun_name
(
std
::
ostream
&
out
,
const
char
*
pretty_fun
);
static
void
render_fun_name
(
std
::
ostream
&
out
,
const
event
&
x
);
/// Renders the difference between `t0` and `tn` in milliseconds.
static
void
render_time_diff
(
std
::
ostream
&
out
,
timestamp
t0
,
timestamp
tn
);
...
...
@@ -245,7 +247,7 @@ public:
static
line_format
parse_format
(
const
std
::
string
&
format_str
);
/// Skips path in `filename`.
static
const
char
*
skip_path
(
const
char
*
filename
);
static
string_view
skip_path
(
string_view
filename
);
/// Returns a string representation of the joined groups of `x` if `x` is an
/// actor with the `subscriber` mixin.
...
...
@@ -383,18 +385,23 @@ inline caf::actor_id caf_set_aid_dummy() { return 0; }
#else // CAF_LOG_LEVEL
#define CAF_LOG_MAKE_EVENT(aid, component, loglvl, message) \
::caf::logger::event { \
loglvl, component, CAF_PRETTY_FUN, __func__, \
caf::logger::skip_path(__FILE__), __LINE__, \
(::caf::logger::line_builder{} << message).get(), \
::std::this_thread::get_id(), aid, ::caf::make_timestamp() \
}
#define CAF_LOG_IMPL(component, loglvl, message) \
do { \
auto CAF_UNIFYN(caf_logger) = caf::logger::current_logger(); \
if (CAF_UNIFYN(caf_logger) != nullptr \
&& CAF_UNIFYN(caf_logger)->accepts(loglvl, component)) \
CAF_UNIFYN(caf_logger) \
->log(new ::caf::logger::event{ \
loglvl, component, CAF_PRETTY_FUN, caf::logger::skip_path(__FILE__), \
__LINE__, (::caf::logger::line_builder{} << message).get(), \
::std::this_thread::get_id(), \
CAF_UNIFYN(caf_logger)->thread_local_aid(), \
::caf::make_timestamp()}); \
->log( \
new CAF_LOG_MAKE_EVENT(CAF_UNIFYN(caf_logger)->thread_local_aid(), \
component, loglvl, message)); \
} while (false)
#define CAF_PUSH_AID(aarg) \
...
...
libcaf_core/src/logger.cpp
View file @
ecee4ead
...
...
@@ -47,7 +47,7 @@ namespace caf {
namespace
{
constexpr
const
char
*
log_level_name
[]
=
{
constexpr
string_view
log_level_name
[]
=
{
"ERROR"
,
"WARN"
,
"INFO"
,
...
...
@@ -55,6 +55,90 @@ constexpr const char* log_level_name[] = {
"TRACE"
};
constexpr
string_view
fun_prefixes
[]
=
{
"virtual "
,
"static "
,
"const "
,
"signed "
,
"unsigned "
,
};
constexpr
string_view
anon_ns
=
"(anonymous namespace)"
;
/// Reduces symbol by printing all prefixes to `out` and returning the
/// remainder. For example, "ns::foo::bar" prints "ns.foo" to `out` and returns
/// "bar".
string_view
reduce_symbol
(
std
::
ostream
&
out
,
string_view
symbol
)
{
string_view
last
=
""
;
bool
printed
=
false
;
// Prints the content of `last` and then replaces it with `x`.
auto
set_last
=
[
&
](
string_view
y
)
{
if
(
!
last
.
empty
())
{
if
(
printed
)
out
<<
"."
;
else
printed
=
true
;
for
(
auto
ch
:
last
)
if
(
ch
==
' '
)
out
<<
"%20"
;
else
out
<<
ch
;
}
last
=
y
;
};
size_t
pos
=
0
;
auto
advance
=
[
&
](
size_t
n
)
{
set_last
(
symbol
.
substr
(
0
,
pos
));
symbol
.
remove_prefix
(
pos
+
n
);
pos
=
0
;
};
auto
flush
=
[
&
]
{
advance
(
1
);
// Some compilers put a whitespace after nested templates that we wish to
// ignore here, e.g.,
// foo::tpl<foo::tpl<int> >::fun(int)
// ^
if
(
last
!=
" "
)
set_last
(
""
);
};
while
(
pos
<
symbol
.
size
())
{
switch
(
symbol
[
pos
])
{
// A colon can only appear as scope separator, i.e., "::".
case
':'
:
advance
(
2
);
break
;
// This either marks the beginning of the argument list *or* on GCC
// is part of "(anonymous namespace)".
case
'('
:
if
(
starts_with
(
symbol
,
anon_ns
))
{
set_last
(
"$"
);
// The anonymous namespace is always followed by "::".
symbol
.
remove_prefix
(
anon_ns
.
size
()
+
2
);
pos
=
0
;
break
;
}
// We reached the end of the function name. Print "GLOBAL" if we didn't
// print anything yet as "global namespace".
set_last
(
""
);
if
(
!
printed
)
out
<<
"GLOBAL"
;
return
symbol
;
case
'<'
:
flush
();
out
<<
'<'
;
symbol
=
reduce_symbol
(
out
,
symbol
);
break
;
case
'>'
:
flush
();
out
<<
'>'
;
return
symbol
;
default:
++
pos
;
}
}
return
symbol
;
}
#if CAF_LOG_LEVEL >= 0
#if defined(CAF_NO_THREAD_LOCAL)
...
...
@@ -112,12 +196,13 @@ inline logger* get_current_logger() {
}
// namespace <anonymous>
logger
::
event
::
event
(
int
lvl
,
const
char
*
cat
,
const
char
*
fun
,
const
char
*
f
n
,
int
line
,
std
::
string
msg
,
std
::
thread
::
id
t
,
actor_id
a
,
timestamp
ts
)
logger
::
event
::
event
(
int
lvl
,
string_view
cat
,
string_view
full_fu
n
,
string_view
fun
,
string_view
fn
,
int
line
,
std
::
string
msg
,
std
::
thread
::
id
t
,
actor_id
a
,
timestamp
ts
)
:
level
(
lvl
),
category_name
(
cat
),
pretty_fun
(
fun
),
pretty_fun
(
full_fun
),
simple_fun
(
fun
),
file_name
(
fn
),
line_number
(
line
),
message
(
std
::
move
(
msg
)),
...
...
@@ -146,6 +231,13 @@ logger::line_builder& logger::line_builder::operator<<(const std::string& str) {
return
*
this
;
}
logger
::
line_builder
&
logger
::
line_builder
::
operator
<<
(
string_view
str
)
{
if
(
!
str_
.
empty
()
&&
str_
.
back
()
!=
' '
)
str_
+=
" "
;
str_
.
insert
(
str_
.
end
(),
str
.
begin
(),
str
.
end
());
return
*
this
;
}
logger
::
line_builder
&
logger
::
line_builder
::
operator
<<
(
const
char
*
str
)
{
if
(
!
str_
.
empty
())
str_
+=
" "
;
...
...
@@ -211,15 +303,13 @@ logger* logger::current_logger() {
return
get_current_logger
();
}
bool
logger
::
accepts
(
int
level
,
const
char
*
cname_begin
,
const
char
*
cname_end
)
{
CAF_ASSERT
(
cname_begin
!=
nullptr
&&
cname_end
!=
nullptr
);
bool
logger
::
accepts
(
int
level
,
string_view
cname
)
{
CAF_ASSERT
(
level
>=
0
&&
level
<=
4
);
if
(
level
>
max_level_
)
return
false
;
if
(
!
component_filter
.
empty
())
{
auto
it
=
std
::
search
(
component_filter
.
begin
(),
component_filter
.
end
(),
cname
_begin
,
cname_end
);
cname
.
begin
(),
cname
.
end
()
);
return
it
!=
component_filter
.
end
();
}
return
true
;
...
...
@@ -302,55 +392,60 @@ void logger::init(actor_system_config& cfg) {
#endif
}
void
logger
::
render_fun_prefix
(
std
::
ostream
&
out
,
const
char
*
pretty_fun
)
{
auto
first
=
pretty_fun
;
//
set end to beginning of arguments
const
char
*
last
=
strchr
(
pretty_fun
,
'('
);
if
(
last
==
nullptr
)
retur
n
;
auto
strsize
=
static_cast
<
size_t
>
(
last
-
first
);
auto
jump_to_next_whitespace
=
[
&
]
{
// leave `first` unchanged if no whitespaces is present,
// e.g., in constructor signatures
auto
tmp
=
std
::
find
(
first
,
last
,
' '
)
;
if
(
tmp
!=
last
)
first
=
tmp
+
1
;
void
logger
::
render_fun_prefix
(
std
::
ostream
&
out
,
const
event
&
x
)
{
// Extract the prefix of a function name. For example:
//
virtual std::vector<int> my::namespace::foo(int);
// ^~~~~~~~~~~~~
// Here, we output Java-style "my.namespace" to `out`.
auto
reduced
=
x
.
pretty_fu
n
;
// Skip all prefixes that can preceed the return type.
auto
skip
=
[
&
](
string_view
str
)
{
if
(
starts_with
(
reduced
,
str
))
{
reduced
.
remove_prefix
(
str
.
size
());
return
true
;
}
return
false
;
};
// skip "virtual" prefix if present
if
(
strncmp
(
pretty_fun
,
"virtual "
,
std
::
min
<
size_t
>
(
strsize
,
8
))
==
0
)
jump_to_next_whitespace
();
// skip "static" prefix if present
if
(
strncmp
(
pretty_fun
,
"static "
,
std
::
min
<
size_t
>
(
strsize
,
7
))
==
0
)
jump_to_next_whitespace
();
// skip return type
jump_to_next_whitespace
();
if
(
first
==
last
)
return
;
const
char
sep
[]
=
"::"
;
// separator for namespaces and classes
auto
sep_first
=
std
::
begin
(
sep
);
auto
sep_last
=
sep_first
+
2
;
// using end() includes \0
auto
colons
=
first
;
decltype
(
colons
)
nextcs
;
while
((
nextcs
=
std
::
search
(
colons
+
1
,
last
,
sep_first
,
sep_last
))
!=
last
)
colons
=
nextcs
;
std
::
string
result
;
result
.
assign
(
first
,
colons
);
detail
::
prettify_type_name
(
result
);
out
<<
result
;
}
void
logger
::
render_fun_name
(
std
::
ostream
&
out
,
const
char
*
pretty_fun
)
{
// Find the end of the function name by looking for the opening parenthesis
// trailing it.
CAF_ASSERT
(
pretty_fun
!=
nullptr
);
const
char
*
e
=
strchr
(
pretty_fun
,
'('
);
if
(
e
==
nullptr
)
// Remove any type of the return type.
while
(
std
::
any_of
(
std
::
begin
(
fun_prefixes
),
std
::
end
(
fun_prefixes
),
skip
))
;
// Repeat.
// Skip the return type.
auto
skip_return_type
=
[
&
]
{
size_t
template_nesting
=
0
;
size_t
pos
=
0
;
for
(
size_t
pos
=
0
;
pos
<
reduced
.
size
();
++
pos
)
{
switch
(
reduced
[
pos
])
{
case
' '
:
if
(
template_nesting
==
0
)
{
// Skip any pointers and references. We need to loop, because each
// pointer/reference can be const-qualified.
do
{
pos
=
reduced
.
find_first_not_of
(
" *&"
,
pos
);
reduced
.
remove_prefix
(
pos
);
pos
=
0
;
}
while
(
skip
(
"const"
));
return
;
/// Now look for the beginning of the function name.
using
rev_iter
=
std
::
reverse_iterator
<
const
char
*>
;
auto
b
=
std
::
find_if
(
rev_iter
(
e
),
rev_iter
(
pretty_fun
),
[](
char
x
)
{
return
x
==
':'
||
x
==
' '
;
});
out
.
write
(
b
.
base
(),
e
-
b
.
base
());
}
break
;
case
'<'
:
++
template_nesting
;
break
;
case
'>'
:
--
template_nesting
;
break
;
default:
break
;
}
}
reduced
.
remove_prefix
(
pos
);
};
skip_return_type
();
// We reached the function name itself and can recursively print the prefix.
reduce_symbol
(
out
,
reduced
);
}
void
logger
::
render_fun_name
(
std
::
ostream
&
out
,
const
event
&
e
)
{
out
<<
e
.
simple_fun
;
}
void
logger
::
render_time_diff
(
std
::
ostream
&
out
,
timestamp
t0
,
timestamp
tn
)
{
...
...
@@ -366,12 +461,12 @@ void logger::render(std::ostream& out, const line_format& lf,
for
(
auto
&
f
:
lf
)
switch
(
f
.
kind
)
{
case
category_field
:
out
<<
x
.
category_name
;
break
;
case
class_name_field
:
render_fun_prefix
(
out
,
x
.
pretty_fun
);
break
;
case
class_name_field
:
render_fun_prefix
(
out
,
x
);
break
;
case
date_field
:
render_date
(
out
,
x
.
tstamp
);
break
;
case
file_field
:
out
<<
x
.
file_name
;
break
;
case
line_field
:
out
<<
x
.
line_number
;
break
;
case
message_field
:
out
<<
x
.
message
;
break
;
case
method_field
:
render_fun_name
(
out
,
x
.
pretty_fun
);
break
;
case
method_field
:
render_fun_name
(
out
,
x
);
break
;
case
newline_field
:
out
<<
std
::
endl
;
break
;
case
priority_field
:
out
<<
log_level_name
[
x
.
level
];
break
;
case
runtime_field
:
render_time_diff
(
out
,
t0_
,
x
.
tstamp
);
break
;
...
...
@@ -428,9 +523,11 @@ logger::line_format logger::parse_format(const std::string& format_str) {
return
res
;
}
const
char
*
logger
::
skip_path
(
const
char
*
path
)
{
auto
ptr
=
strrchr
(
path
,
'/'
);
return
ptr
==
nullptr
?
path
:
(
ptr
+
1
);
string_view
logger
::
skip_path
(
string_view
path
)
{
auto
find_slash
=
[
&
]
{
return
path
.
find
(
'/'
);
};
for
(
auto
p
=
find_slash
();
p
!=
string_view
::
npos
;
p
=
find_slash
())
path
.
remove_prefix
(
p
+
1
);
return
path
;
}
void
logger
::
run
()
{
...
...
@@ -509,6 +606,7 @@ void logger::log_first_line() {
return
{
CAF_LOG_LEVEL_INFO
,
CAF_LOG_COMPONENT
,
CAF_PRETTY_FUN
,
__func__
,
__FILE__
,
__LINE__
,
std
::
move
(
msg
),
...
...
@@ -526,6 +624,7 @@ void logger::log_last_line() {
event
tmp
{
CAF_LOG_LEVEL_INFO
,
CAF_LOG_COMPONENT
,
CAF_PRETTY_FUN
,
__func__
,
__FILE__
,
__LINE__
,
"EOF"
,
...
...
libcaf_core/test/logger.cpp
View file @
ecee4ead
...
...
@@ -27,9 +27,28 @@
#include "caf/all.hpp"
using
namespace
caf
;
using
namespace
std
;
using
namespace
std
::
chrono
;
using
std
::
string
;
#define CHECK_FUN_PREFIX(PrefixName) \
do { \
auto e = CAF_LOG_MAKE_EVENT(0, "caf", CAF_LOG_LEVEL_DEBUG, ""); \
std::ostringstream oss; \
::caf::logger::render_fun_prefix(oss, e); \
auto prefix = oss.str(); \
if (prefix != PrefixName) \
CAF_ERROR("rendering the prefix of " << e.pretty_fun << " produced " \
<< prefix << " instead of " \
<< PrefixName); \
else \
CAF_CHECK_EQUAL(prefix, PrefixName); \
} while (false)
void
global_fun
()
{
CHECK_FUN_PREFIX
(
"GLOBAL"
);
}
namespace
{
struct
fixture
{
...
...
@@ -48,8 +67,8 @@ struct fixture {
template
<
class
F
,
class
...
Ts
>
string
render
(
F
f
,
Ts
&&
...
xs
)
{
ostringstream
oss
;
f
(
oss
,
forward
<
Ts
>
(
xs
)...);
std
::
ostringstream
oss
;
f
(
oss
,
std
::
forward
<
Ts
>
(
xs
)...);
return
oss
.
str
();
}
...
...
@@ -57,6 +76,52 @@ struct fixture {
logger
::
line_format
lf
;
};
void
fun
()
{
CHECK_FUN_PREFIX
(
"$"
);
}
const
char
*
ptr_fun
(
const
char
*
x
)
{
CHECK_FUN_PREFIX
(
"$"
);
return
x
;
}
const
int
&
ref_fun
(
const
int
&
x
)
{
CHECK_FUN_PREFIX
(
"$"
);
return
x
;
}
template
<
class
T
>
struct
tpl
{
static
void
run
(
std
::
string
t_name
)
{
CHECK_FUN_PREFIX
(
"$.tpl<"
+
t_name
+
">"
);
}
};
namespace
foo
{
void
fun
()
{
CHECK_FUN_PREFIX
(
"$.foo"
);
}
const
char
*
ptr_fun
(
const
char
*
x
)
{
CHECK_FUN_PREFIX
(
"$.foo"
);
return
x
;
}
const
int
&
ref_fun
(
const
int
&
x
)
{
CHECK_FUN_PREFIX
(
"$.foo"
);
return
x
;
}
template
<
class
T
>
struct
tpl
{
static
void
run
(
std
::
string
t_name
)
{
CHECK_FUN_PREFIX
(
"$.foo.tpl<"
+
t_name
+
">"
);
}
};
}
// namespace foo
constexpr
const
char
*
file_format
=
"%r %c %p %a %t %C %M %F:%L %m%n"
;
}
// namespace <anonymous>
...
...
@@ -95,10 +160,6 @@ CAF_TEST(parse_default_format_strings) {
}
CAF_TEST
(
rendering
)
{
// Rendering of type names and function names.
const
char
*
foobar
=
"void ns::foo::bar()"
;
CAF_CHECK_EQUAL
(
render
(
logger
::
render_fun_name
,
foobar
),
"bar"
);
CAF_CHECK_EQUAL
(
render
(
logger
::
render_fun_prefix
,
foobar
),
"ns.foo"
);
// Rendering of time points.
timestamp
t0
;
timestamp
t1
{
timestamp
::
duration
{
5000000
}};
// epoch + 5000000ns (5ms)
...
...
@@ -113,13 +174,17 @@ CAF_TEST(rendering) {
CAF_LOG_LEVEL_WARNING
,
"unit.test"
,
"void ns::foo::bar()"
,
"bar"
,
"foo.cpp"
,
42
,
"hello world"
,
this_thread
::
get_id
(),
std
::
this_thread
::
get_id
(),
0
,
t0
};
CAF_CHECK_EQUAL
(
render
(
logger
::
render_fun_name
,
e
),
string_view
{
"bar"
});
CAF_CHECK_EQUAL
(
render
(
logger
::
render_fun_prefix
,
e
),
string_view
{
"ns.foo"
});
// Exclude %r and %t from rendering test because they are nondeterministic.
actor_system
sys
{
cfg
};
auto
lf
=
logger
::
parse_format
(
"%c %p %a %C %M %F:%L %m"
);
...
...
@@ -130,4 +195,21 @@ CAF_TEST(rendering) {
"unit.test WARN actor0 ns.foo bar foo.cpp:42 hello world"
);
}
CAF_TEST
(
render_fun_prefix
)
{
int
i
=
42
;
global_fun
();
fun
();
ptr_fun
(
nullptr
);
ref_fun
(
i
);
tpl
<
int
>::
run
(
"int"
);
tpl
<
unsigned
int
>::
run
(
"unsigned%20int"
);
tpl
<
tpl
<
signed
char
>>::
run
(
"$.tpl<signed%20char>"
);
foo
::
fun
();
foo
::
ptr_fun
(
nullptr
);
foo
::
ref_fun
(
i
);
foo
::
tpl
<
int
>::
run
(
"int"
);
foo
::
tpl
<
unsigned
int
>::
run
(
"unsigned%20int"
);
foo
::
tpl
<
foo
::
tpl
<
signed
char
>>::
run
(
"$.foo.tpl<signed%20char>"
);
}
CAF_TEST_FIXTURE_SCOPE_END
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment