Commit e718576d authored by iposva@chromium.org's avatar iposva@chromium.org

Split handle scopes into an internal version and a version accessible

through the API. This allows us to verify state on entry through the API.
In this change verification in the API entry is checking that the current
thread holds the V8 lock when a HandleScope is instantiated if a v8::Locker
has ever been used by the V8 instance.
Review URL: http://codereview.chromium.org/18707

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@1140 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent 108fe9db
......@@ -396,19 +396,11 @@ template <class T> class EXPORT_INLINE Persistent : public Handle<T> {
*/
class EXPORT HandleScope {
public:
HandleScope() : previous_(current_), is_closed_(false) {
current_.extensions = 0;
}
HandleScope();
~HandleScope() {
// TODO(1245391): In a perfect world, there would be a way of not
// having to check for explicitly closed scopes maybe through
// subclassing HandleScope?
if (!is_closed_) RestorePreviousState();
}
~HandleScope();
/**
* TODO(1245391): Consider introducing a subclass for this.
* Closes the handle scope and returns the value as a handle in the
* previous scope, which is the new current scope after the call.
*/
......@@ -432,6 +424,8 @@ class EXPORT HandleScope {
void* operator new(size_t size);
void operator delete(void*, size_t);
// This Data class is accessible internally through a typedef in the
// ImplementationUtilities class.
class EXPORT Data {
public:
int extensions;
......@@ -443,31 +437,13 @@ class EXPORT HandleScope {
}
};
static Data current_;
const Data previous_;
/**
* Re-establishes the previous scope state. Should be called only
* once, and only for the current scope.
*/
void RestorePreviousState() {
if (current_.extensions > 0) DeleteExtensions();
current_ = previous_;
#ifdef DEBUG
ZapRange(current_.next, current_.limit);
#endif
}
Data previous_;
// TODO(1245391): Consider creating a subclass for this.
// Allow for the active closing of HandleScopes which allows to pass a handle
// from the HandleScope being closed to the next top most HandleScope.
bool is_closed_;
void** RawClose(void** value);
/** Deallocates any extensions used by the current scope.*/
static void DeleteExtensions();
// Zaps the handles in the half-open interval [start, end).
static void ZapRange(void** start, void** end);
friend class ImplementationUtilities;
};
......@@ -2284,10 +2260,17 @@ class EXPORT Locker {
*/
static bool IsLocked();
/**
* Returns whether v8::Locker is being used by this V8 instance.
*/
static bool IsActive() { return active_; }
private:
bool has_lock_;
bool top_level_;
static bool active_;
// Disallow copying and assigning.
Locker(const Locker&);
void operator=(const Locker&);
......
......@@ -36,6 +36,7 @@
#include "platform.h"
#include "serialize.h"
#include "snapshot.h"
#include "v8threads.h"
namespace i = v8::internal;
......@@ -73,6 +74,15 @@ namespace v8 {
} while (false)
#define API_ENTRY_CHECK(msg) \
do { \
if (v8::Locker::IsActive()) { \
ApiCheck(i::ThreadManager::IsLockedByCurrentThread(), \
msg, \
"Entering the V8 API without proper locking in place"); \
} \
} while (false)
// --- D a t a t h a t i s s p e c i f i c t o a t h r e a d ---
......@@ -190,6 +200,19 @@ static void EnsureInitialized(const char* location) {
}
ImplementationUtilities::HandleScopeData*
ImplementationUtilities::CurrentHandleScope() {
return &i::HandleScope::current_;
}
#ifdef DEBUG
void ImplementationUtilities::ZapHandleRange(void** begin, void** end) {
i::HandleScope::ZapRange(begin, end);
}
#endif
v8::Handle<v8::Primitive> ImplementationUtilities::Undefined() {
if (IsDeadCheck("v8::Undefined()")) return v8::Handle<v8::Primitive>();
EnsureInitialized("v8::Undefined()");
......@@ -359,55 +382,26 @@ void V8::DisposeGlobal(void** obj) {
// --- H a n d l e s ---
HandleScope::Data HandleScope::current_ = { -1, NULL, NULL };
HandleScope::HandleScope() : is_closed_(false) {
API_ENTRY_CHECK("HandleScope::HandleScope");
i::HandleScope::Enter(&previous_);
}
int HandleScope::NumberOfHandles() {
int n = thread_local.Blocks()->length();
if (n == 0) return 0;
return ((n - 1) * i::kHandleBlockSize) +
(current_.next - thread_local.Blocks()->last());
HandleScope::~HandleScope() {
if (!is_closed_) {
i::HandleScope::Leave(&previous_);
}
}
void** v8::HandleScope::CreateHandle(void* value) {
void** result = current_.next;
if (result == current_.limit) {
// Make sure there's at least one scope on the stack and that the
// top of the scope stack isn't a barrier.
if (!ApiCheck(current_.extensions >= 0,
"v8::HandleScope::CreateHandle()",
"Cannot create a handle without a HandleScope")) {
return NULL;
}
// If there's more room in the last block, we use that. This is used
// for fast creation of scopes after scope barriers.
if (!thread_local.Blocks()->is_empty()) {
void** limit = &thread_local.Blocks()->last()[i::kHandleBlockSize];
if (current_.limit != limit) {
current_.limit = limit;
}
}
int HandleScope::NumberOfHandles() {
return i::HandleScope::NumberOfHandles();
}
// If we still haven't found a slot for the handle, we extend the
// current handle scope by allocating a new handle block.
if (result == current_.limit) {
// If there's a spare block, use it for growing the current scope.
result = thread_local.GetSpareOrNewBlock();
// Add the extension to the global list of blocks, but count the
// extension as part of the current scope.
thread_local.Blocks()->Add(result);
current_.extensions++;
current_.limit = &result[i::kHandleBlockSize];
}
}
// Update the current next field, set the value in the created
// handle, and return the result.
ASSERT(result < current_.limit);
current_.next = result + 1;
*result = value;
return result;
void** v8::HandleScope::CreateHandle(void* value) {
return i::HandleScope::CreateHandle(value);
}
......@@ -436,20 +430,6 @@ void Context::Exit() {
}
void v8::HandleScope::DeleteExtensions() {
ASSERT(current_.extensions != 0);
thread_local.DeleteExtensions(current_.extensions);
}
void HandleScope::ZapRange(void** start, void** end) {
if (start == NULL) return;
for (void** p = start; p < end; p++) {
*p = reinterpret_cast<void*>(v8::internal::kHandleZapValue);
}
}
void** v8::HandleScope::RawClose(void** value) {
if (!ApiCheck(!is_closed_,
"v8::HandleScope::Close()",
......@@ -461,7 +441,7 @@ void** v8::HandleScope::RawClose(void** value) {
// Read the result before popping the handle block.
i::Object* result = reinterpret_cast<i::Object*>(*value);
is_closed_ = true;
RestorePreviousState();
i::HandleScope::Leave(&previous_);
// Allocate a new handle on the previous handle block.
i::Handle<i::Object> handle(result);
......@@ -2951,8 +2931,8 @@ char* HandleScopeImplementer::ArchiveThread(char* storage) {
char* HandleScopeImplementer::ArchiveThreadHelper(char* storage) {
ImplementationUtilities::HandleScopeData* current =
ImplementationUtilities::CurrentHandleScope();
v8::ImplementationUtilities::HandleScopeData* current =
v8::ImplementationUtilities::CurrentHandleScope();
handle_scope_data_ = *current;
memcpy(storage, this, sizeof(*this));
......@@ -2975,7 +2955,7 @@ char* HandleScopeImplementer::RestoreThread(char* storage) {
char* HandleScopeImplementer::RestoreThreadHelper(char* storage) {
memcpy(this, storage, sizeof(*this));
*ImplementationUtilities::CurrentHandleScope() = handle_scope_data_;
*v8::ImplementationUtilities::CurrentHandleScope() = handle_scope_data_;
return storage + ArchiveSpacePerThread();
}
......@@ -2983,7 +2963,7 @@ char* HandleScopeImplementer::RestoreThreadHelper(char* storage) {
void HandleScopeImplementer::Iterate(
ObjectVisitor* v,
List<void**>* blocks,
ImplementationUtilities::HandleScopeData* handle_data) {
v8::ImplementationUtilities::HandleScopeData* handle_data) {
// Iterate over all handles in the blocks except for the last.
for (int i = blocks->length() - 2; i >= 0; --i) {
Object** block =
......@@ -3000,8 +2980,8 @@ void HandleScopeImplementer::Iterate(
void HandleScopeImplementer::Iterate(ObjectVisitor* v) {
ImplementationUtilities::HandleScopeData* current =
ImplementationUtilities::CurrentHandleScope();
v8::ImplementationUtilities::HandleScopeData* current =
v8::ImplementationUtilities::CurrentHandleScope();
Iterate(v, thread_local.Blocks(), current);
}
......@@ -3010,7 +2990,7 @@ char* HandleScopeImplementer::Iterate(ObjectVisitor* v, char* storage) {
HandleScopeImplementer* thread_local =
reinterpret_cast<HandleScopeImplementer*>(storage);
List<void**>* blocks_of_archived_thread = thread_local->Blocks();
ImplementationUtilities::HandleScopeData* handle_data_of_archived_thread =
v8::ImplementationUtilities::HandleScopeData* handle_data_of_archived_thread =
&thread_local->handle_scope_data_;
Iterate(v, blocks_of_archived_thread, handle_data_of_archived_thread);
......
......@@ -28,6 +28,7 @@
#ifndef V8_API_H_
#define V8_API_H_
#include "apiutils.h"
#include "factory.h"
namespace v8 {
......@@ -159,45 +160,6 @@ class RegisteredExtension {
};
class ImplementationUtilities {
public:
static v8::Handle<v8::Primitive> Undefined();
static v8::Handle<v8::Primitive> Null();
static v8::Handle<v8::Boolean> True();
static v8::Handle<v8::Boolean> False();
static int GetNameCount(ExtensionConfiguration* that) {
return that->name_count_;
}
static const char** GetNames(ExtensionConfiguration* that) {
return that->names_;
}
static v8::Arguments NewArguments(Local<Value> data,
Local<Object> holder,
Local<Function> callee,
bool is_construct_call,
void** argv, int argc) {
return v8::Arguments(data, holder, callee, is_construct_call, argv, argc);
}
// Introduce an alias for the handle scope data to allow non-friends
// to access the HandleScope data.
typedef v8::HandleScope::Data HandleScopeData;
static HandleScopeData* CurrentHandleScope() {
return &v8::HandleScope::current_;
}
#ifdef DEBUG
static void ZapHandleRange(void** begin, void** end) {
v8::HandleScope::ZapRange(begin, end);
}
#endif
};
class Utils {
public:
static bool ReportApiFailure(const char* location, const char* message);
......@@ -275,7 +237,7 @@ static inline T* ToApi(v8::internal::Handle<v8::internal::Object> obj) {
template <class T>
v8::internal::Handle<T> v8::internal::Handle<T>::EscapeFrom(
HandleScope* scope) {
v8::HandleScope* scope) {
return Utils::OpenHandle(*scope->Close(Utils::ToLocal(*this)));
}
......@@ -408,11 +370,11 @@ class HandleScopeImplementer {
List<Handle<Object> > saved_contexts_;
bool ignore_out_of_memory;
// This is only used for threading support.
ImplementationUtilities::HandleScopeData handle_scope_data_;
v8::ImplementationUtilities::HandleScopeData handle_scope_data_;
static void Iterate(ObjectVisitor* v,
List<void**>* blocks,
ImplementationUtilities::HandleScopeData* handle_data);
List<void**>* blocks,
v8::ImplementationUtilities::HandleScopeData* handle_data);
char* RestoreThreadHelper(char* from);
char* ArchiveThreadHelper(char* to);
......@@ -474,13 +436,14 @@ void HandleScopeImplementer::DeleteExtensions(int extensions) {
for (int i = extensions; i > 1; --i) {
void** block = blocks.RemoveLast();
#ifdef DEBUG
ImplementationUtilities::ZapHandleRange(block, &block[kHandleBlockSize]);
v8::ImplementationUtilities::ZapHandleRange(block,
&block[kHandleBlockSize]);
#endif
DeleteArray(block);
}
spare = reinterpret_cast<Object**>(blocks.RemoveLast());
#ifdef DEBUG
ImplementationUtilities::ZapHandleRange(
v8::ImplementationUtilities::ZapHandleRange(
reinterpret_cast<void**>(spare),
reinterpret_cast<void**>(&spare[kHandleBlockSize]));
#endif
......
// Copyright 2009 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#ifndef V8_APIUTILS_H_
#define V8_APIUTILS_H_
namespace v8 {
class ImplementationUtilities {
public:
static v8::Handle<v8::Primitive> Undefined();
static v8::Handle<v8::Primitive> Null();
static v8::Handle<v8::Boolean> True();
static v8::Handle<v8::Boolean> False();
static int GetNameCount(ExtensionConfiguration* that) {
return that->name_count_;
}
static const char** GetNames(ExtensionConfiguration* that) {
return that->names_;
}
static v8::Arguments NewArguments(Local<Value> data,
Local<Object> holder,
Local<Function> callee,
bool is_construct_call,
void** argv, int argc) {
return v8::Arguments(data, holder, callee, is_construct_call, argv, argc);
}
// Introduce an alias for the handle scope data to allow non-friends
// to access the HandleScope data.
typedef v8::HandleScope::Data HandleScopeData;
static HandleScopeData* CurrentHandleScope();
#ifdef DEBUG
static void ZapHandleRange(void** begin, void** end);
#endif
};
} // namespace v8
#endif // V8_APIUTILS_H_
......@@ -327,7 +327,7 @@ Handle<Object> Factory::NewReferenceError(Handle<String> message) {
Handle<Object> Factory::NewError(const char* maker, const char* type,
Vector< Handle<Object> > args) {
HandleScope scope;
v8::HandleScope scope; // Instantiate a closeable HandleScope for EscapeFrom.
Handle<FixedArray> array = Factory::NewFixedArray(args.length());
for (int i = 0; i < args.length(); i++) {
array->set(i, *args[i]);
......
......@@ -29,6 +29,7 @@
#ifndef V8_HANDLES_INL_H_
#define V8_HANDLES_INL_H_
#include "apiutils.h"
#include "handles.h"
#include "api.h"
......@@ -51,8 +52,8 @@ inline T* Handle<T>::operator*() const {
#ifdef DEBUG
inline NoHandleAllocation::NoHandleAllocation() {
ImplementationUtilities::HandleScopeData* current =
ImplementationUtilities::CurrentHandleScope();
v8::ImplementationUtilities::HandleScopeData* current =
v8::ImplementationUtilities::CurrentHandleScope();
extensions_ = current->extensions;
// Shrink the current handle scope to make it impossible to do
// handle allocations without an explicit handle scope.
......@@ -64,7 +65,7 @@ inline NoHandleAllocation::NoHandleAllocation() {
inline NoHandleAllocation::~NoHandleAllocation() {
// Restore state in current handle scope to re-enable handle
// allocations.
ImplementationUtilities::CurrentHandleScope()->extensions = extensions_;
v8::ImplementationUtilities::CurrentHandleScope()->extensions = extensions_;
}
#endif
......
......@@ -40,6 +40,74 @@
namespace v8 { namespace internal {
v8::ImplementationUtilities::HandleScopeData HandleScope::current_ =
{ -1, NULL, NULL };
int HandleScope::NumberOfHandles() {
int n = HandleScopeImplementer::instance()->Blocks()->length();
if (n == 0) return 0;
return ((n - 1) * kHandleBlockSize) +
(current_.next - HandleScopeImplementer::instance()->Blocks()->last());
}
void** HandleScope::CreateHandle(void* value) {
void** result = current_.next;
if (result == current_.limit) {
// Make sure there's at least one scope on the stack and that the
// top of the scope stack isn't a barrier.
if (current_.extensions < 0) {
Utils::ReportApiFailure("v8::HandleScope::CreateHandle()",
"Cannot create a handle without a HandleScope");
return NULL;
}
HandleScopeImplementer* impl = HandleScopeImplementer::instance();
// If there's more room in the last block, we use that. This is used
// for fast creation of scopes after scope barriers.
if (!impl->Blocks()->is_empty()) {
void** limit = &impl->Blocks()->last()[kHandleBlockSize];
if (current_.limit != limit) {
current_.limit = limit;
}
}
// If we still haven't found a slot for the handle, we extend the
// current handle scope by allocating a new handle block.
if (result == current_.limit) {
// If there's a spare block, use it for growing the current scope.
result = impl->GetSpareOrNewBlock();
// Add the extension to the global list of blocks, but count the
// extension as part of the current scope.
impl->Blocks()->Add(result);
current_.extensions++;
current_.limit = &result[kHandleBlockSize];
}
}
// Update the current next field, set the value in the created
// handle, and return the result.
ASSERT(result < current_.limit);
current_.next = result + 1;
*result = value;
return result;
}
void HandleScope::DeleteExtensions() {
ASSERT(current_.extensions != 0);
HandleScopeImplementer::instance()->DeleteExtensions(current_.extensions);
}
void HandleScope::ZapRange(void** start, void** end) {
if (start == NULL) return;
for (void** p = start; p < end; p++) {
*p = reinterpret_cast<void*>(v8::internal::kHandleZapValue);
}
}
Handle<FixedArray> AddKeysFromJSArray(Handle<FixedArray> content,
Handle<JSArray> array) {
CALL_HEAP_FUNCTION(content->AddKeysFromJSArray(*array), FixedArray);
......
......@@ -28,12 +28,14 @@
#ifndef V8_HANDLES_H_
#define V8_HANDLES_H_
#include "apiutils.h"
namespace v8 { namespace internal {
// ----------------------------------------------------------------------------
// A Handle provides a reference to an object that survives relocation by
// the garbage collector.
// Handles are only valid withing a HandleScope.
// Handles are only valid within a HandleScope.
// When a handle is created for an object a cell is allocated in the heap.
template<class T>
......@@ -90,6 +92,75 @@ class Handle {
};
// A stack-allocated class that governs a number of local handles.
// After a handle scope has been created, all local handles will be
// allocated within that handle scope until either the handle scope is
// deleted or another handle scope is created. If there is already a
// handle scope and a new one is created, all allocations will take
// place in the new handle scope until it is deleted. After that,
// new handles will again be allocated in the original handle scope.
//
// After the handle scope of a local handle has been deleted the
// garbage collector will no longer track the object stored in the
// handle and may deallocate it. The behavior of accessing a handle
// for which the handle scope has been deleted is undefined.
class HandleScope {
public:
HandleScope() : previous_(current_) {
current_.extensions = 0;
}
~HandleScope() {
Leave(&previous_);
}
// Counts the number of allocated handles.
static int NumberOfHandles();
// Creates a new handle with the given value.
static void** CreateHandle(void* value);
private:
// Prevent heap allocation or illegal handle scopes.
HandleScope(const HandleScope&);
void operator=(const HandleScope&);
void* operator new(size_t size);
void operator delete(void* size_t);
static v8::ImplementationUtilities::HandleScopeData current_;
const v8::ImplementationUtilities::HandleScopeData previous_;
// Pushes a fresh handle scope to be used when allocating new handles.
static void Enter(
v8::ImplementationUtilities::HandleScopeData* previous) {
*previous = current_;
current_.extensions = 0;
}
// Re-establishes the previous scope state. Should be called only
// once, and only for the current scope.
static void Leave(
const v8::ImplementationUtilities::HandleScopeData* previous) {
if (current_.extensions > 0) {
DeleteExtensions();
}
current_ = *previous;
#ifdef DEBUG
ZapRange(current_.next, current_.limit);
#endif
}
// Deallocates any extensions used by the current scope.
static void DeleteExtensions();
// Zaps the handles in the half-open interval [start, end).
static void ZapRange(void** start, void** end);
friend class v8::HandleScope;
friend class v8::ImplementationUtilities;
};
// ----------------------------------------------------------------------------
// Handle operations.
// They might invoke garbage collection. The result is an handle to
......
......@@ -66,7 +66,7 @@ Handle<Object> MessageHandler::MakeMessageObject(
Vector< Handle<Object> > args,
Handle<String> stack_trace) {
// Build error message object
HandleScope scope;
v8::HandleScope scope; // Instantiate a closeable HandleScope for EscapeFrom.
Handle<Object> type_str = Factory::LookupAsciiSymbol(type);
Handle<Object> array = Factory::NewJSArray(args.length());
for (int i = 0; i < args.length(); i++)
......
......@@ -38,9 +38,17 @@ namespace v8 {
static internal::Thread::LocalStorageKey thread_state_key =
internal::Thread::CreateThreadLocalKey();
// Track whether this V8 instance has ever called v8::Locker. This allows the
// API code to verify that the lock is always held when V8 is being entered.
bool Locker::active_ = false;
// Constructor for the Locker object. Once the Locker is constructed the
// current thread will be guaranteed to have the big V8 lock.
Locker::Locker() : has_lock_(false), top_level_(true) {
// Record that the Locker has been used at least once.
active_ = true;
// Get the big lock if necessary.
if (!internal::ThreadManager::IsLockedByCurrentThread()) {
internal::ThreadManager::Lock();
......
......@@ -259,6 +259,7 @@
/* Begin PBXFileReference section */
8900116B0E71CA2300F91F35 /* libraries.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = libraries.cc; sourceTree = "<group>"; };
893986D40F29020C007D5254 /* apiutils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = apiutils.h; sourceTree = "<group>"; };
8944AD0E0F1D4D3A0028D560 /* regexp-stack.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "regexp-stack.cc"; sourceTree = "<group>"; };
8944AD0F0F1D4D3A0028D560 /* regexp-stack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "regexp-stack.h"; sourceTree = "<group>"; };
89471C7F0EB23EE400B6874B /* flag-definitions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "flag-definitions.h"; sourceTree = "<group>"; };
......@@ -595,6 +596,7 @@
897FF0F90E719B8F00D62E90 /* allocation.h */,
897FF0FA0E719B8F00D62E90 /* api.cc */,
897FF0FB0E719B8F00D62E90 /* api.h */,
893986D40F29020C007D5254 /* apiutils.h */,
897FF0FC0E719B8F00D62E90 /* arguments.h */,
897FF0FD0E719B8F00D62E90 /* assembler-arm-inl.h */,
897FF0FE0E719B8F00D62E90 /* assembler-arm.cc */,
......
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