Unverified Commit ef309cf6 authored by Robert Kimball's avatar Robert Kimball Committed by GitHub

Start of windows build (#1306)

* compiles but does not link
parent 7d6a41f3
...@@ -159,6 +159,10 @@ if (NGRAPH_USE_GOLD) ...@@ -159,6 +159,10 @@ if (NGRAPH_USE_GOLD)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fuse-ld=gold") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fuse-ld=gold")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-ld=gold") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-ld=gold")
endif() endif()
if(WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOMINMAX")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_CRT_SECURE_NO_WARNINGS")
endif()
include(unit_test_control) include(unit_test_control)
set(UNIT_TEST_CONFIG_LIST "" CACHE INTERNAL "") set(UNIT_TEST_CONFIG_LIST "" CACHE INTERNAL "")
......
...@@ -217,7 +217,9 @@ endif() ...@@ -217,7 +217,9 @@ endif()
# Defines macro in C++ to load backend plugin # Defines macro in C++ to load backend plugin
target_include_directories(ngraph PUBLIC "${NGRAPH_INCLUDE_PATH}") target_include_directories(ngraph PUBLIC "${NGRAPH_INCLUDE_PATH}")
target_link_libraries(ngraph PUBLIC dl pthread) if (NOT WIN32)
target_link_libraries(ngraph PUBLIC dl pthread)
endif()
if (NGRAPH_ONNX_IMPORT_ENABLE) if (NGRAPH_ONNX_IMPORT_ENABLE)
target_sources(ngraph PRIVATE $<TARGET_OBJECTS:onnx_import_interface>) target_sources(ngraph PRIVATE $<TARGET_OBJECTS:onnx_import_interface>)
......
...@@ -15,24 +15,36 @@ ...@@ -15,24 +15,36 @@
*******************************************************************************/ *******************************************************************************/
#include <cassert> #include <cassert>
#ifdef WIN32
#include <windows.h>
#else
#include <dirent.h> #include <dirent.h>
#include <ftw.h>
#include <sys/file.h>
#include <sys/time.h>
#include <unistd.h>
#endif
#include <fcntl.h> #include <fcntl.h>
#include <fstream> #include <fstream>
#include <ftw.h>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string.h> #include <string.h>
#include <sys/file.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h>
#include <vector> #include <vector>
#include "ngraph/file_util.hpp" #include "ngraph/file_util.hpp"
#include "ngraph/log.hpp" #include "ngraph/log.hpp"
#ifdef WIN32
#define RMDIR(a) RemoveDirectoryA(a)
#define RMFILE(a) DeleteFileA(a)
#else
#define RMDIR(a) rmdir(a)
#define RMFILE(a) remove(a)
#endif
using namespace std; using namespace std;
using namespace ngraph; using namespace ngraph;
...@@ -135,15 +147,15 @@ void file_util::remove_directory(const string& dir) ...@@ -135,15 +147,15 @@ void file_util::remove_directory(const string& dir)
[](const string& file, bool is_dir) { [](const string& file, bool is_dir) {
if (is_dir) if (is_dir)
{ {
rmdir(file.c_str()); RMDIR(file.c_str());
} }
else else
{ {
remove(file.c_str()); RMFILE(file.c_str());
} }
}, },
true); true);
rmdir(dir.c_str()); RMDIR(dir.c_str());
} }
} }
...@@ -154,6 +166,9 @@ void file_util::remove_file(const string& file) ...@@ -154,6 +166,9 @@ void file_util::remove_file(const string& file)
bool file_util::make_directory(const string& dir) bool file_util::make_directory(const string& dir)
{ {
#ifdef WIN32
CreateDirectoryA(dir.c_str(), nullptr);
#else
if (mkdir(dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) if (mkdir(dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH))
{ {
if (errno == EEXIST) if (errno == EEXIST)
...@@ -163,6 +178,7 @@ bool file_util::make_directory(const string& dir) ...@@ -163,6 +178,7 @@ bool file_util::make_directory(const string& dir)
} }
throw runtime_error("error making directory " + dir + " " + strerror(errno)); throw runtime_error("error making directory " + dir + " " + strerror(errno));
} }
#endif
return true; return true;
} }
...@@ -221,6 +237,7 @@ string file_util::read_file_to_string(const string& path) ...@@ -221,6 +237,7 @@ string file_util::read_file_to_string(const string& path)
return ss.str(); return ss.str();
} }
#ifndef WIN32
static void iterate_files_worker(const string& path, static void iterate_files_worker(const string& path,
function<void(const string& file, bool is_dir)> func, function<void(const string& file, bool is_dir)> func,
bool recurse, bool recurse,
...@@ -272,6 +289,7 @@ static void iterate_files_worker(const string& path, ...@@ -272,6 +289,7 @@ static void iterate_files_worker(const string& path,
throw runtime_error("error enumerating file " + path); throw runtime_error("error enumerating file " + path);
} }
} }
#endif
void file_util::iterate_files(const string& path, void file_util::iterate_files(const string& path,
function<void(const string& file, bool is_dir)> func, function<void(const string& file, bool is_dir)> func,
...@@ -280,6 +298,19 @@ void file_util::iterate_files(const string& path, ...@@ -280,6 +298,19 @@ void file_util::iterate_files(const string& path,
{ {
vector<string> files; vector<string> files;
vector<string> dirs; vector<string> dirs;
#ifdef WIN32
string file_match = path_join(path, "*");
WIN32_FIND_DATA data;
HANDLE hFind = FindFirstFile(file_match.c_str(), &data);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
std::cout << data.cFileName << std::endl;
} while (FindNextFile(hFind, &data));
FindClose(hFind);
}
#else
iterate_files_worker(path, iterate_files_worker(path,
[&files, &dirs](const string& file, bool is_dir) { [&files, &dirs](const string& file, bool is_dir) {
if (is_dir) if (is_dir)
...@@ -293,6 +324,7 @@ void file_util::iterate_files(const string& path, ...@@ -293,6 +324,7 @@ void file_util::iterate_files(const string& path,
}, },
recurse, recurse,
include_links); include_links);
#endif
for (auto f : files) for (auto f : files)
{ {
...@@ -306,6 +338,10 @@ void file_util::iterate_files(const string& path, ...@@ -306,6 +338,10 @@ void file_util::iterate_files(const string& path,
string file_util::tmp_filename(const string& extension) string file_util::tmp_filename(const string& extension)
{ {
string rc;
#ifdef WIN32
rc = _tempnam(file_util::get_temp_directory_path().c_str(), "ngraph_");
#else
string tmp_template = string tmp_template =
file_util::path_join(file_util::get_temp_directory_path(), "ngraph_XXXXXX" + extension); file_util::path_join(file_util::get_temp_directory_path(), "ngraph_XXXXXX" + extension);
char* tmpname = strdup(tmp_template.c_str()); char* tmpname = strdup(tmp_template.c_str());
...@@ -313,8 +349,9 @@ string file_util::tmp_filename(const string& extension) ...@@ -313,8 +349,9 @@ string file_util::tmp_filename(const string& extension)
// mkstemp opens the file with open() so we need to close it // mkstemp opens the file with open() so we need to close it
close(mkstemps(tmpname, static_cast<int>(extension.size()))); close(mkstemps(tmpname, static_cast<int>(extension.size())));
string rc = tmpname; rc = tmpname;
free(tmpname); free(tmpname);
#endif
return rc; return rc;
} }
......
...@@ -27,10 +27,6 @@ ...@@ -27,10 +27,6 @@
#include "ngraph/op/result.hpp" #include "ngraph/op/result.hpp"
#include "ngraph/placement.hpp" #include "ngraph/placement.hpp"
#if not defined(EIGEN_MPL2_ONLY)
#error("The flag `EIGEN_MPL2_ONLY` must be defined");
#endif
using namespace std; using namespace std;
using namespace ngraph; using namespace ngraph;
......
...@@ -15,7 +15,10 @@ ...@@ -15,7 +15,10 @@
*******************************************************************************/ *******************************************************************************/
#include <algorithm> #include <algorithm>
#ifdef WIN32
#else
#include <cxxabi.h> #include <cxxabi.h>
#endif
#include <iomanip> #include <iomanip>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
...@@ -142,8 +145,10 @@ void ngraph::pass::Manager::run_passes(shared_ptr<Function> func, bool transitiv ...@@ -142,8 +145,10 @@ void ngraph::pass::Manager::run_passes(shared_ptr<Function> func, bool transitiv
{ {
PassBase* p = pass.get(); PassBase* p = pass.get();
string name = typeid(*p).name(); string name = typeid(*p).name();
#ifndef WIN32
int status; int status;
name = abi::__cxa_demangle(name.c_str(), 0, 0, &status); name = abi::__cxa_demangle(name.c_str(), 0, 0, &status);
#endif
cout << setw(7) << pass_timer.get_milliseconds() << "ms " << name << "\n"; cout << setw(7) << pass_timer.get_milliseconds() << "ms " << name << "\n";
} }
} }
......
...@@ -185,7 +185,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr< ...@@ -185,7 +185,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr<
size_t offset = 200; size_t offset = 200;
size_t width = 1000; size_t width = 1000;
size_t scale = width - offset; size_t scale = width - offset;
size_t line_spacing = stroke_width * 1.5; size_t line_spacing = static_cast<size_t>(stroke_width * 1.5);
size_t line_count = 0; size_t line_count = 0;
for (shared_ptr<Node> node : nodes) for (shared_ptr<Node> node : nodes)
{ {
...@@ -203,7 +203,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr< ...@@ -203,7 +203,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr<
float footprint = float(MemoryVisualize::memory_footprint(node)); float footprint = float(MemoryVisualize::memory_footprint(node));
y += line_spacing; y += line_spacing;
size_t x1 = offset; size_t x1 = offset;
size_t x2 = ((usage / memory_footprint) * scale) + offset; size_t x2 = static_cast<size_t>(((usage / memory_footprint) * scale) + offset);
file << "<text x=\"" << 0 << "\" y=\"" << y + text_offset << "\" fill=\"" file << "<text x=\"" << 0 << "\" y=\"" << y + text_offset << "\" fill=\""
<< "black" << "black"
<< "\">" << node->get_name() << "</text>\n"; << "\">" << node->get_name() << "</text>\n";
...@@ -211,7 +211,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr< ...@@ -211,7 +211,7 @@ void pass::MemoryVisualize::draw_histogram(ostream& file, const list<shared_ptr<
<< "\""; << "\"";
file << " style=\"stroke:forestgreen;stroke-width:" << stroke_width << "\" />\n"; file << " style=\"stroke:forestgreen;stroke-width:" << stroke_width << "\" />\n";
x1 = x2; x1 = x2;
x2 = ((footprint / memory_footprint) * scale) + offset; x2 = static_cast<size_t>(((footprint / memory_footprint) * scale) + offset);
file << "<line x1=\"" << x1 << "\" y1=\"" << y << "\" x2=\"" << x2 << "\" y2=\"" << y file << "<line x1=\"" << x1 << "\" y1=\"" << y << "\" x2=\"" << x2 << "\" y2=\"" << y
<< "\""; << "\"";
file << " style=\"stroke:firebrick;stroke-width:" << stroke_width << "\" />\n"; file << " style=\"stroke:firebrick;stroke-width:" << stroke_width << "\" />\n";
...@@ -241,11 +241,11 @@ int pass::MemoryVisualize::compute_op_weight(const shared_ptr<Node> exop) ...@@ -241,11 +241,11 @@ int pass::MemoryVisualize::compute_op_weight(const shared_ptr<Node> exop)
int mass = 0; int mass = 0;
for (const descriptor::Tensor* tensor : exop->liveness_new_list) for (const descriptor::Tensor* tensor : exop->liveness_new_list)
{ {
mass += tensor->size(); mass += static_cast<int>(tensor->size());
} }
for (const descriptor::Tensor* tensor : exop->liveness_free_list) for (const descriptor::Tensor* tensor : exop->liveness_free_list)
{ {
mass -= tensor->size(); mass -= static_cast<int>(tensor->size());
} }
return mass; return mass;
} }
......
...@@ -14,7 +14,11 @@ ...@@ -14,7 +14,11 @@
* limitations under the License. * limitations under the License.
*******************************************************************************/ *******************************************************************************/
#ifdef WIN32
#include <windows.h>
#else
#include <dlfcn.h> #include <dlfcn.h>
#endif
#include <sstream> #include <sstream>
#include "ngraph/file_util.hpp" #include "ngraph/file_util.hpp"
...@@ -25,6 +29,16 @@ ...@@ -25,6 +29,16 @@
using namespace std; using namespace std;
using namespace ngraph; using namespace ngraph;
#ifdef WIN32
#define OPEN_LIBRARY(a, b) LoadLibrary(a)
#define CLOSE_LIBRARY(a) FreeLibrary(a)
#define DLSYM(a, b) GetProcAddress(a, b)
#else
// #define OPEN_LIBRARY(a, b) dlopen(a, b)
#define CLOSE_LIBRARY(a) dlclose(a)
#define DLSYM(a, b) dlsym(a, b)
#endif
runtime::Backend::~Backend() runtime::Backend::~Backend()
{ {
} }
...@@ -32,16 +46,20 @@ runtime::Backend::~Backend() ...@@ -32,16 +46,20 @@ runtime::Backend::~Backend()
// This doodad finds the full path of the containing shared library // This doodad finds the full path of the containing shared library
static string find_my_file() static string find_my_file()
{ {
#ifdef WIN32
return ".";
#else
Dl_info dl_info; Dl_info dl_info;
dladdr(reinterpret_cast<void*>(find_my_file), &dl_info); dladdr(reinterpret_cast<void*>(find_my_file), &dl_info);
return dl_info.dli_fname; return dl_info.dli_fname;
#endif
} }
void* runtime::Backend::open_shared_library(string type) DL_HANDLE runtime::Backend::open_shared_library(string type)
{ {
string ext = SHARED_LIB_EXT; string ext = SHARED_LIB_EXT;
void* handle = nullptr; DL_HANDLE handle;
// strip off attributes, IE:CPU becomes IE // strip off attributes, IE:CPU becomes IE
auto colon = type.find(":"); auto colon = type.find(":");
...@@ -53,7 +71,11 @@ void* runtime::Backend::open_shared_library(string type) ...@@ -53,7 +71,11 @@ void* runtime::Backend::open_shared_library(string type)
string library_name = "lib" + to_lower(type) + "_backend" + string(SHARED_LIB_EXT); string library_name = "lib" + to_lower(type) + "_backend" + string(SHARED_LIB_EXT);
string my_directory = file_util::get_directory(find_my_file()); string my_directory = file_util::get_directory(find_my_file());
string library_path = file_util::path_join(my_directory, library_name); string library_path = file_util::path_join(my_directory, library_name);
#ifdef WIN32
handle = LoadLibrary(library_path.c_str());
#else
handle = dlopen(library_path.c_str(), RTLD_NOW | RTLD_GLOBAL); handle = dlopen(library_path.c_str(), RTLD_NOW | RTLD_GLOBAL);
#endif
return handle; return handle;
} }
...@@ -61,7 +83,7 @@ void* runtime::Backend::open_shared_library(string type) ...@@ -61,7 +83,7 @@ void* runtime::Backend::open_shared_library(string type)
shared_ptr<runtime::Backend> runtime::Backend::create(const string& type) shared_ptr<runtime::Backend> runtime::Backend::create(const string& type)
{ {
shared_ptr<runtime::Backend> rc; shared_ptr<runtime::Backend> rc;
void* handle = open_shared_library(type); DL_HANDLE handle = open_shared_library(type);
if (!handle) if (!handle)
{ {
throw runtime_error("Backend '" + type + "' not found"); throw runtime_error("Backend '" + type + "' not found");
...@@ -69,34 +91,34 @@ shared_ptr<runtime::Backend> runtime::Backend::create(const string& type) ...@@ -69,34 +91,34 @@ shared_ptr<runtime::Backend> runtime::Backend::create(const string& type)
else else
{ {
function<const char*()> get_ngraph_version_string = function<const char*()> get_ngraph_version_string =
reinterpret_cast<const char* (*)()>(dlsym(handle, "get_ngraph_version_string")); reinterpret_cast<const char* (*)()>(DLSYM(handle, "get_ngraph_version_string"));
if (!get_ngraph_version_string) if (!get_ngraph_version_string)
{ {
dlclose(handle); CLOSE_LIBRARY(handle);
throw runtime_error("Backend '" + type + throw runtime_error("Backend '" + type +
"' does not implement get_ngraph_version_string"); "' does not implement get_ngraph_version_string");
} }
function<runtime::Backend*(const char*)> new_backend = function<runtime::Backend*(const char*)> new_backend =
reinterpret_cast<runtime::Backend* (*)(const char*)>(dlsym(handle, "new_backend")); reinterpret_cast<runtime::Backend* (*)(const char*)>(DLSYM(handle, "new_backend"));
if (!new_backend) if (!new_backend)
{ {
dlclose(handle); CLOSE_LIBRARY(handle);
throw runtime_error("Backend '" + type + "' does not implement new_backend"); throw runtime_error("Backend '" + type + "' does not implement new_backend");
} }
function<void(runtime::Backend*)> delete_backend = function<void(runtime::Backend*)> delete_backend =
reinterpret_cast<void (*)(runtime::Backend*)>(dlsym(handle, "delete_backend")); reinterpret_cast<void (*)(runtime::Backend*)>(DLSYM(handle, "delete_backend"));
if (!delete_backend) if (!delete_backend)
{ {
dlclose(handle); CLOSE_LIBRARY(handle);
throw runtime_error("Backend '" + type + "' does not implement delete_backend"); throw runtime_error("Backend '" + type + "' does not implement delete_backend");
} }
runtime::Backend* backend = new_backend(type.c_str()); runtime::Backend* backend = new_backend(type.c_str());
rc = shared_ptr<runtime::Backend>(backend, [=](runtime::Backend* b) { rc = shared_ptr<runtime::Backend>(backend, [=](runtime::Backend* b) {
delete_backend(b); delete_backend(b);
// dlclose(handle); // CLOSE_LIBRARY(handle);
}); });
} }
return rc; return rc;
...@@ -113,14 +135,19 @@ map<string, string> runtime::Backend::get_registered_device_map() ...@@ -113,14 +135,19 @@ map<string, string> runtime::Backend::get_registered_device_map()
string backend_name; string backend_name;
if (is_backend_name(name, backend_name)) if (is_backend_name(name, backend_name))
{ {
auto handle = dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL); DL_HANDLE handle;
#ifdef WIN32
handle = LoadLibrary(file.c_str());
#else
handle = dlopen(file.c_str(), RTLD_LAZY | RTLD_LOCAL);
#endif
if (handle) if (handle)
{ {
if (dlsym(handle, "new_backend") && dlsym(handle, "delete_backend")) if (DLSYM(handle, "new_backend") && DLSYM(handle, "delete_backend"))
{ {
function<const char*()> get_ngraph_version_string = function<const char*()> get_ngraph_version_string =
reinterpret_cast<const char* (*)()>( reinterpret_cast<const char* (*)()>(
dlsym(handle, "get_ngraph_version_string")); DLSYM(handle, "get_ngraph_version_string"));
if (get_ngraph_version_string && if (get_ngraph_version_string &&
get_ngraph_version_string() == string(NGRAPH_VERSION)) get_ngraph_version_string() == string(NGRAPH_VERSION))
{ {
...@@ -128,7 +155,7 @@ map<string, string> runtime::Backend::get_registered_device_map() ...@@ -128,7 +155,7 @@ map<string, string> runtime::Backend::get_registered_device_map()
} }
} }
dlclose(handle); CLOSE_LIBRARY(handle);
} }
} }
}; };
......
...@@ -17,12 +17,20 @@ ...@@ -17,12 +17,20 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include "ngraph/function.hpp" #include "ngraph/function.hpp"
#include "ngraph/runtime/performance_counter.hpp" #include "ngraph/runtime/performance_counter.hpp"
#include "ngraph/shape.hpp" #include "ngraph/shape.hpp"
#include "ngraph/type/element_type.hpp" #include "ngraph/type/element_type.hpp"
#ifdef WIN32
#include <windows.h>
#define DL_HANDLE HMODULE
#else
#define DL_HANDLE void*
#endif
namespace ngraph namespace ngraph
{ {
namespace runtime namespace runtime
...@@ -83,7 +91,7 @@ namespace ngraph ...@@ -83,7 +91,7 @@ namespace ngraph
const std::vector<std::shared_ptr<runtime::TensorView>>& inputs); const std::vector<std::shared_ptr<runtime::TensorView>>& inputs);
private: private:
static void* open_shared_library(std::string type); static DL_HANDLE open_shared_library(std::string type);
static std::map<std::string, std::string> get_registered_device_map(); static std::map<std::string, std::string> get_registered_device_map();
static bool is_backend_name(const std::string& file, std::string& backend_name); static bool is_backend_name(const std::string& file, std::string& backend_name);
}; };
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
#include "ngraph/coordinate_transform.hpp" #include "ngraph/coordinate_transform.hpp"
#ifdef WIN32
#undef min
#endif
namespace ngraph namespace ngraph
{ {
namespace runtime namespace runtime
......
...@@ -130,6 +130,45 @@ void init_int_tv(shared_ptr<runtime::TensorView> tv, T min, T max) ...@@ -130,6 +130,45 @@ void init_int_tv(shared_ptr<runtime::TensorView> tv, T min, T max)
tv->write(vec.data(), 0, vec.size() * sizeof(T)); tv->write(vec.data(), 0, vec.size() * sizeof(T));
} }
template <>
void init_int_tv<char>(shared_ptr<runtime::TensorView> tv, char min, char max)
{
size_t size = tv->get_element_count();
uniform_int_distribution<int16_t> dist(static_cast<short>(min), static_cast<short>(max));
vector<char> vec(size);
for (char& element : vec)
{
element = static_cast<char>(dist(s_random_engine));
}
tv->write(vec.data(), 0, vec.size() * sizeof(char));
}
template <>
void init_int_tv<int8_t>(shared_ptr<runtime::TensorView> tv, int8_t min, int8_t max)
{
size_t size = tv->get_element_count();
uniform_int_distribution<int16_t> dist(static_cast<short>(min), static_cast<short>(max));
vector<int8_t> vec(size);
for (int8_t& element : vec)
{
element = static_cast<int8_t>(dist(s_random_engine));
}
tv->write(vec.data(), 0, vec.size() * sizeof(int8_t));
}
template <>
void init_int_tv<uint8_t>(shared_ptr<runtime::TensorView> tv, uint8_t min, uint8_t max)
{
size_t size = tv->get_element_count();
uniform_int_distribution<int16_t> dist(static_cast<short>(min), static_cast<short>(max));
vector<uint8_t> vec(size);
for (uint8_t& element : vec)
{
element = static_cast<uint8_t>(dist(s_random_engine));
}
tv->write(vec.data(), 0, vec.size() * sizeof(uint8_t));
}
template <typename T> template <typename T>
void init_real_tv(shared_ptr<runtime::TensorView> tv, T min, T max) void init_real_tv(shared_ptr<runtime::TensorView> tv, T min, T max)
{ {
......
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