Merge topic 'server-refactor'

0e4d5033 server: Added thread check to protect writedata
124424e9 server: Protect several fields from potentially pointing to bad memory
693fa0a9 server: Added assert to monitor uv_run status
882dcef8 server: Made connections in a server have a mutex to avoid use after frees
7ef28843 server: Moved buffer formatting into bufferstrategy
dc7a18d8 server: test buffer parsing
f8fd5a97 server: Made stdio connection accept different types of streams
6afc7f88 server: Remove unused fields / functions
...

Acked-by: Kitware Robot <kwrobot@kitware.com>
Merge-request: !1111
This commit is contained in:
Brad King
2017-09-01 14:34:07 +00:00
committed by Kitware Robot
11 changed files with 341 additions and 120 deletions

View File

@@ -753,6 +753,9 @@ add_subdirectory(Tests)
if(NOT CMake_TEST_EXTERNAL_CMAKE)
if(BUILD_TESTING)
CMAKE_SET_TARGET_FOLDER(CMakeLibTests "Tests")
IF(TARGET CMakeServerLibTests)
CMAKE_SET_TARGET_FOLDER(CMakeServerLibTests "Tests")
ENDIF()
endif()
if(TARGET documentation)
CMAKE_SET_TARGET_FOLDER(documentation "Documentation")

View File

@@ -38,11 +38,6 @@ void cmEventBasedConnection::on_read(uv_stream_t* stream, ssize_t nread,
delete[](buf->base);
}
void cmEventBasedConnection::on_close_delete(uv_handle_t* handle)
{
delete handle;
}
void cmEventBasedConnection::on_close(uv_handle_t* /*handle*/)
{
}
@@ -72,9 +67,19 @@ bool cmEventBasedConnection::IsOpen() const
return this->WriteStream != nullptr;
}
void cmEventBasedConnection::WriteData(const std::string& data)
void cmEventBasedConnection::WriteData(const std::string& _data)
{
#ifndef NDEBUG
auto curr_thread_id = uv_thread_self();
assert(this->Server);
assert(uv_thread_equal(&curr_thread_id, &this->Server->ServeThreadId));
#endif
auto data = _data;
assert(this->WriteStream);
if (BufferStrategy) {
data = BufferStrategy->BufferOutMessage(data);
}
auto ds = data.size();
@@ -119,7 +124,9 @@ void cmEventBasedConnection::OnDisconnect(int onerror)
{
(void)onerror;
this->OnConnectionShuttingDown();
this->Server->OnDisconnect(this);
if (this->Server) {
this->Server->OnDisconnect(this);
}
}
cmConnection::~cmConnection()
@@ -128,6 +135,7 @@ cmConnection::~cmConnection()
bool cmConnection::OnConnectionShuttingDown()
{
this->Server = nullptr;
return true;
}
@@ -149,9 +157,12 @@ bool cmConnection::OnServeStart(std::string* errString)
bool cmEventBasedConnection::OnConnectionShuttingDown()
{
this->WriteStream->data = nullptr;
this->ReadStream->data = nullptr;
if (this->WriteStream) {
this->WriteStream->data = nullptr;
}
if (this->ReadStream) {
this->ReadStream->data = nullptr;
}
this->ReadStream = nullptr;
this->WriteStream = nullptr;
return true;

View File

@@ -38,6 +38,17 @@ public:
*/
virtual std::string BufferMessage(std::string& rawBuffer) = 0;
/***
* Called to properly buffer an outgoing message.
*
* @param rawBuffer Message to format in the correct way
*
* @return Formatted message
*/
virtual std::string BufferOutMessage(const std::string& rawBuffer) const
{
return rawBuffer;
};
/***
* Resets the internal state of the buffering
*/
@@ -100,7 +111,12 @@ public:
uv_stream_t* WriteStream = nullptr;
static void on_close(uv_handle_t* handle);
static void on_close_delete(uv_handle_t* handle);
template <typename T>
static void on_close_delete(uv_handle_t* handle)
{
delete reinterpret_cast<T*>(handle);
}
protected:
std::string RawReadBuffer;

View File

@@ -19,7 +19,8 @@ void cmPipeConnection::Connect(uv_stream_t* server)
uv_pipe_init(this->Server->GetLoop(), rejectPipe, 0);
uv_accept(server, reinterpret_cast<uv_stream_t*>(rejectPipe));
uv_close(reinterpret_cast<uv_handle_t*>(rejectPipe), &on_close_delete);
uv_close(reinterpret_cast<uv_handle_t*>(rejectPipe),
&on_close_delete<uv_pipe_t>);
return;
}
@@ -28,7 +29,8 @@ void cmPipeConnection::Connect(uv_stream_t* server)
this->ClientPipe->data = static_cast<cmEventBasedConnection*>(this);
auto client = reinterpret_cast<uv_stream_t*>(this->ClientPipe);
if (uv_accept(server, client) != 0) {
uv_close(reinterpret_cast<uv_handle_t*>(client), &on_close_delete);
uv_close(reinterpret_cast<uv_handle_t*>(client),
&on_close_delete<uv_pipe_t>);
this->ClientPipe = nullptr;
return;
}
@@ -65,15 +67,16 @@ bool cmPipeConnection::OnConnectionShuttingDown()
{
if (this->ClientPipe) {
uv_close(reinterpret_cast<uv_handle_t*>(this->ClientPipe),
&on_close_delete);
&on_close_delete<uv_pipe_t>);
this->WriteStream->data = nullptr;
}
uv_close(reinterpret_cast<uv_handle_t*>(this->ServerPipe), &on_close_delete);
uv_close(reinterpret_cast<uv_handle_t*>(this->ServerPipe),
&on_close_delete<uv_pipe_t>);
this->ClientPipe = nullptr;
this->ServerPipe = nullptr;
this->WriteStream = nullptr;
this->ReadStream = nullptr;
return cmConnection::OnConnectionShuttingDown();
return cmEventBasedConnection::OnConnectionShuttingDown();
}

View File

@@ -16,6 +16,7 @@
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <memory>
#include <utility>
@@ -57,10 +58,6 @@ cmServer::cmServer(cmConnection* conn, bool supportExperimental)
cmServer::~cmServer()
{
if (!this->Protocol) { // Server was never fully started!
return;
}
for (cmServerProtocol* p : this->SupportedProtocols) {
delete p;
}
@@ -110,6 +107,7 @@ void cmServer::ProcessRequest(cmConnection* connection,
void cmServer::RegisterProtocol(cmServerProtocol* protocol)
{
if (protocol->IsExperimental() && !this->SupportExperimental) {
delete protocol;
return;
}
auto version = protocol->ProtocolVersion();
@@ -247,9 +245,11 @@ cmFileMonitor* cmServer::FileMonitor() const
void cmServer::WriteJsonObject(const Json::Value& jsonValue,
const DebugInfo* debug) const
{
uv_rwlock_rdlock(&ConnectionsMutex);
for (auto& connection : this->Connections) {
WriteJsonObject(connection.get(), jsonValue, debug);
}
uv_rwlock_rdunlock(&ConnectionsMutex);
}
void cmServer::WriteJsonObject(cmConnection* connection,
@@ -285,8 +285,7 @@ void cmServer::WriteJsonObject(cmConnection* connection,
}
}
connection->WriteData(std::string("\n") + kSTART_MAGIC + std::string("\n") +
result + kEND_MAGIC + std::string("\n"));
connection->WriteData(result);
}
cmServerProtocol* cmServer::FindMatchingProtocol(
@@ -413,18 +412,36 @@ static void __start_thread(void* arg)
{
auto server = reinterpret_cast<cmServerBase*>(arg);
std::string error;
server->Serve(&error);
bool success = server->Serve(&error);
if (!success || error.empty() == false) {
std::cerr << "Error during serve: " << error << std::endl;
}
}
static void __shutdownThread(uv_async_t* arg)
{
auto server = reinterpret_cast<cmServerBase*>(arg->data);
on_walk_to_shutdown(reinterpret_cast<uv_handle_t*>(arg), nullptr);
server->StartShutDown();
}
bool cmServerBase::StartServeThread()
{
ServeThreadRunning = true;
uv_async_init(&Loop, &this->ShutdownSignal, __shutdownThread);
this->ShutdownSignal.data = this;
uv_thread_create(&ServeThread, __start_thread, this);
return true;
}
bool cmServerBase::Serve(std::string* errorMessage)
{
#ifndef NDEBUG
uv_thread_t blank_thread_t = {};
assert(uv_thread_equal(&blank_thread_t, &ServeThreadId));
ServeThreadId = uv_thread_self();
#endif
errorMessage->clear();
uv_signal_init(&Loop, &this->SIGINTHandler);
@@ -438,15 +455,24 @@ bool cmServerBase::Serve(std::string* errorMessage)
OnServeStart();
for (auto& connection : Connections) {
if (!connection->OnServeStart(errorMessage)) {
return false;
{
uv_rwlock_rdlock(&ConnectionsMutex);
for (auto& connection : Connections) {
if (!connection->OnServeStart(errorMessage)) {
uv_rwlock_rdunlock(&ConnectionsMutex);
return false;
}
}
uv_rwlock_rdunlock(&ConnectionsMutex);
}
if (uv_run(&Loop, UV_RUN_DEFAULT) != 0) {
// It is important we don't ever let the event loop exit with open handles
// at best this is a memory leak, but it can also introduce race conditions
// which can hang the program.
assert(false && "Event loop stopped in unclean state.");
*errorMessage = "Internal Error: Event loop stopped in unclean state.";
StartShutDown();
return false;
}
@@ -458,14 +484,8 @@ void cmServerBase::OnConnected(cmConnection*)
{
}
void cmServerBase::OnDisconnect()
{
}
void cmServerBase::OnServeStart()
{
uv_signal_start(&this->SIGINTHandler, &on_signal, SIGINT);
uv_signal_start(&this->SIGHUPHandler, &on_signal, SIGHUP);
}
void cmServerBase::StartShutDown()
@@ -480,16 +500,16 @@ void cmServerBase::StartShutDown()
uv_signal_stop(&this->SIGHUPHandler);
}
for (auto& connection : Connections) {
connection->OnConnectionShuttingDown();
{
uv_rwlock_wrlock(&ConnectionsMutex);
for (auto& connection : Connections) {
connection->OnConnectionShuttingDown();
}
Connections.clear();
uv_rwlock_wrunlock(&ConnectionsMutex);
}
Connections.clear();
uv_stop(&Loop);
uv_walk(&Loop, on_walk_to_shutdown, nullptr);
uv_run(&Loop, UV_RUN_DEFAULT);
}
bool cmServerBase::OnSignal(int signum)
@@ -501,13 +521,12 @@ bool cmServerBase::OnSignal(int signum)
cmServerBase::cmServerBase(cmConnection* connection)
{
uv_loop_init(&Loop);
auto err = uv_loop_init(&Loop);
(void)err;
assert(err == 0);
uv_signal_init(&Loop, &this->SIGINTHandler);
uv_signal_init(&Loop, &this->SIGHUPHandler);
this->SIGINTHandler.data = this;
this->SIGHUPHandler.data = this;
err = uv_rwlock_init(&ConnectionsMutex);
assert(err == 0);
AddNewConnection(connection);
}
@@ -516,16 +535,19 @@ cmServerBase::~cmServerBase()
{
if (ServeThreadRunning) {
StartShutDown();
uv_async_send(&this->ShutdownSignal);
uv_thread_join(&ServeThread);
}
uv_loop_close(&Loop);
uv_rwlock_destroy(&ConnectionsMutex);
}
void cmServerBase::AddNewConnection(cmConnection* ownedConnection)
{
uv_rwlock_wrlock(&ConnectionsMutex);
Connections.emplace_back(ownedConnection);
uv_rwlock_wrunlock(&ConnectionsMutex);
ownedConnection->SetServer(this);
}
@@ -539,9 +561,11 @@ void cmServerBase::OnDisconnect(cmConnection* pConnection)
auto pred = [pConnection](const std::unique_ptr<cmConnection>& m) {
return m.get() == pConnection;
};
uv_rwlock_wrlock(&ConnectionsMutex);
Connections.erase(
std::remove_if(Connections.begin(), Connections.end(), pred),
Connections.end());
uv_rwlock_wrunlock(&ConnectionsMutex);
if (Connections.empty()) {
StartShutDown();
}

View File

@@ -37,13 +37,12 @@ public:
* This should almost always be called by the given connections
* directly.
*
* @param connection The connectiont the request was received on
* @param connection The connection the request was received on
* @param request The actual request
*/
virtual void ProcessRequest(cmConnection* connection,
const std::string& request) = 0;
virtual void OnConnected(cmConnection* connection);
virtual void OnDisconnect();
/***
* Start a dedicated thread. If this is used to start the server, it will
@@ -62,10 +61,21 @@ public:
void OnDisconnect(cmConnection* pConnection);
protected:
mutable uv_rwlock_t ConnectionsMutex;
std::vector<std::unique_ptr<cmConnection>> Connections;
bool ServeThreadRunning = false;
uv_thread_t ServeThread;
uv_async_t ShutdownSignal;
#ifndef NDEBUG
public:
// When the server starts it will mark down it's current thread ID,
// which is useful in other contexts to just assert that operations
// are performed on that same thread.
uv_thread_t ServeThreadId = {};
protected:
#endif
uv_loop_t Loop;
@@ -140,22 +150,6 @@ private:
cmServerProtocol* Protocol = nullptr;
std::vector<cmServerProtocol*> SupportedProtocols;
std::string DataBuffer;
std::string JsonData;
typedef union
{
uv_tty_t tty;
uv_pipe_t pipe;
} InOutUnion;
InOutUnion Input;
InOutUnion Output;
uv_stream_t* InputStream = nullptr;
uv_stream_t* OutputStream = nullptr;
mutable bool Writing = false;
friend class cmServerProtocol;
friend class cmServerRequest;
};

View File

@@ -2,76 +2,139 @@
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmServerConnection.h"
#include "cmConfigure.h"
#include "cmServer.h"
#include "cmServerDictionary.h"
#ifdef _WIN32
#include "io.h"
#else
#include <unistd.h>
#endif
#include <cassert>
cmStdIoConnection::cmStdIoConnection(
cmConnectionBufferStrategy* bufferStrategy)
: cmEventBasedConnection(bufferStrategy)
, Input()
, Output()
{
}
void cmStdIoConnection::SetupStream(uv_stream_t*& stream, int file_id)
{
assert(stream == nullptr);
switch (uv_guess_handle(file_id)) {
case UV_TTY: {
auto tty = new uv_tty_t();
uv_tty_init(this->Server->GetLoop(), tty, file_id, file_id == 0);
uv_tty_set_mode(tty, UV_TTY_MODE_NORMAL);
stream = reinterpret_cast<uv_stream_t*>(tty);
break;
}
case UV_FILE:
if (file_id == 0) {
return;
}
// Intentional fallthrough; stdin can _not_ be treated as a named
// pipe, however stdout can be.
CM_FALLTHROUGH;
case UV_NAMED_PIPE: {
auto pipe = new uv_pipe_t();
uv_pipe_init(this->Server->GetLoop(), pipe, 0);
uv_pipe_open(pipe, file_id);
stream = reinterpret_cast<uv_stream_t*>(pipe);
break;
}
default:
assert(false && "Unable to determine stream type");
return;
}
stream->data = static_cast<cmEventBasedConnection*>(this);
}
void cmStdIoConnection::SetServer(cmServerBase* s)
{
cmConnection::SetServer(s);
if (uv_guess_handle(1) == UV_TTY) {
usesTty = true;
this->Input.tty = new uv_tty_t();
uv_tty_init(this->Server->GetLoop(), this->Input.tty, 0, 1);
uv_tty_set_mode(this->Input.tty, UV_TTY_MODE_NORMAL);
this->Input.tty->data = static_cast<cmEventBasedConnection*>(this);
this->ReadStream = reinterpret_cast<uv_stream_t*>(this->Input.tty);
this->Output.tty = new uv_tty_t();
uv_tty_init(this->Server->GetLoop(), this->Output.tty, 1, 0);
uv_tty_set_mode(this->Output.tty, UV_TTY_MODE_NORMAL);
this->Output.tty->data = static_cast<cmEventBasedConnection*>(this);
this->WriteStream = reinterpret_cast<uv_stream_t*>(this->Output.tty);
} else {
usesTty = false;
this->Input.pipe = new uv_pipe_t();
uv_pipe_init(this->Server->GetLoop(), this->Input.pipe, 0);
uv_pipe_open(this->Input.pipe, 0);
this->Input.pipe->data = static_cast<cmEventBasedConnection*>(this);
this->ReadStream = reinterpret_cast<uv_stream_t*>(this->Input.pipe);
this->Output.pipe = new uv_pipe_t();
uv_pipe_init(this->Server->GetLoop(), this->Output.pipe, 0);
uv_pipe_open(this->Output.pipe, 1);
this->Output.pipe->data = static_cast<cmEventBasedConnection*>(this);
this->WriteStream = reinterpret_cast<uv_stream_t*>(this->Output.pipe);
if (!s) {
return;
}
SetupStream(this->ReadStream, 0);
SetupStream(this->WriteStream, 1);
}
void shutdown_connection(uv_prepare_t* prepare)
{
cmStdIoConnection* connection =
reinterpret_cast<cmStdIoConnection*>(prepare->data);
if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(prepare))) {
uv_close(reinterpret_cast<uv_handle_t*>(prepare),
&cmEventBasedConnection::on_close_delete<uv_prepare_t>);
}
connection->OnDisconnect(0);
}
bool cmStdIoConnection::OnServeStart(std::string* pString)
{
uv_read_start(this->ReadStream, on_alloc_buffer, on_read);
Server->OnConnected(this);
if (this->ReadStream) {
uv_read_start(this->ReadStream, on_alloc_buffer, on_read);
} else if (uv_guess_handle(0) == UV_FILE) {
char buffer[1024];
while (auto len = read(0, buffer, sizeof(buffer))) {
ReadData(std::string(buffer, buffer + len));
}
// We can't start the disconnect from here, add a prepare hook to do that
// for us
auto prepare = new uv_prepare_t();
prepare->data = this;
uv_prepare_init(Server->GetLoop(), prepare);
uv_prepare_start(prepare, shutdown_connection);
}
return cmConnection::OnServeStart(pString);
}
void cmStdIoConnection::ShutdownStream(uv_stream_t*& stream)
{
if (!stream) {
return;
}
switch (stream->type) {
case UV_TTY: {
assert(!uv_is_closing(reinterpret_cast<uv_handle_t*>(stream)));
if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(stream))) {
uv_close(reinterpret_cast<uv_handle_t*>(stream),
&on_close_delete<uv_tty_t>);
}
break;
}
case UV_FILE:
case UV_NAMED_PIPE: {
assert(!uv_is_closing(reinterpret_cast<uv_handle_t*>(stream)));
if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(stream))) {
uv_close(reinterpret_cast<uv_handle_t*>(stream),
&on_close_delete<uv_pipe_t>);
}
break;
}
default:
assert(false && "Unable to determine stream type");
}
stream = nullptr;
}
bool cmStdIoConnection::OnConnectionShuttingDown()
{
cmEventBasedConnection::OnConnectionShuttingDown();
if (usesTty) {
uv_read_stop(reinterpret_cast<uv_stream_t*>(this->Input.tty));
uv_close(reinterpret_cast<uv_handle_t*>(this->Input.tty),
&on_close_delete);
uv_close(reinterpret_cast<uv_handle_t*>(this->Output.tty),
&on_close_delete);
} else {
uv_close(reinterpret_cast<uv_handle_t*>(this->Input.pipe),
&on_close_delete);
uv_close(reinterpret_cast<uv_handle_t*>(this->Output.pipe),
&on_close_delete);
if (ReadStream) {
uv_read_stop(ReadStream);
}
ShutdownStream(ReadStream);
ShutdownStream(WriteStream);
cmEventBasedConnection::OnConnectionShuttingDown();
return true;
}
@@ -93,6 +156,13 @@ void cmConnectionBufferStrategy::clear()
{
}
std::string cmServerBufferStrategy::BufferOutMessage(
const std::string& rawBuffer) const
{
return std::string("\n") + kSTART_MAGIC + std::string("\n") + rawBuffer +
kEND_MAGIC + std::string("\n");
}
std::string cmServerBufferStrategy::BufferMessage(std::string& RawReadBuffer)
{
for (;;) {

View File

@@ -25,6 +25,7 @@ class cmServerBufferStrategy : public cmConnectionBufferStrategy
{
public:
std::string BufferMessage(std::string& rawBuffer) override;
std::string BufferOutMessage(const std::string& rawBuffer) const override;
private:
std::string RequestBuffer;
@@ -45,16 +46,8 @@ public:
bool OnServeStart(std::string* pString) override;
private:
typedef union
{
uv_tty_t* tty;
uv_pipe_t* pipe;
} InOutUnion;
bool usesTty = false;
InOutUnion Input;
InOutUnion Output;
void SetupStream(uv_stream_t*& stream, int file_id);
void ShutdownStream(uv_stream_t*& stream);
};
/***

View File

@@ -146,6 +146,10 @@ if(BUILD_TESTING)
if(NOT CMake_TEST_EXTERNAL_CMAKE)
add_subdirectory(CMakeLib)
if(CMake_TEST_SERVER_MODE)
add_subdirectory(CMakeServerLib)
endif()
endif()
add_subdirectory(CMakeOnly)
add_subdirectory(RunCMake)

View File

@@ -0,0 +1,17 @@
include_directories(
${CMAKE_CURRENT_BINARY_DIR}
${CMake_BINARY_DIR}/Source
${CMake_SOURCE_DIR}/Source
)
set(CMakeServerLib_TESTS
testServerBuffering
)
create_test_sourcelist(CMakeLib_TEST_SRCS CMakeServerLibTests.cxx ${CMakeServerLib_TESTS})
add_executable(CMakeServerLibTests ${CMakeLib_TEST_SRCS})
target_link_libraries(CMakeServerLibTests CMakeLib CMakeServerLib)
foreach(test ${CMakeServerLib_TESTS})
add_test(CMakeServerLib.${test} CMakeServerLibTests ${test} ${${test}_ARGS})
endforeach()

View File

@@ -0,0 +1,86 @@
#include "cmConnection.h"
#include "cmServerConnection.h"
#include <iostream>
#include <stddef.h>
#include <string>
#include <vector>
void print_error(const std::vector<std::string>& input,
const std::vector<std::string>& output)
{
std::cerr << "Responses don't equal input messages input." << std::endl;
std::cerr << "Responses: " << std::endl;
for (auto& msg : output) {
std::cerr << "'" << msg << "'" << std::endl;
}
std::cerr << "Input messages" << std::endl;
for (auto& msg : input) {
std::cerr << "'" << msg << "'" << std::endl;
}
}
std::string trim_newline(const std::string& _buffer)
{
auto buffer = _buffer;
while (!buffer.empty() && (buffer.back() == '\n' || buffer.back() == '\r')) {
buffer.pop_back();
}
return buffer;
}
int testServerBuffering(int, char** const)
{
std::vector<std::string> messages = {
"{ \"test\": 10}", "{ \"test\": { \"test2\": false} }",
"{ \"test\": [1, 2, 3] }",
"{ \"a\": { \"1\": {}, \n\n\n \"2\":[] \t\t\t\t}}"
};
std::string fullMessage;
for (auto& msg : messages) {
fullMessage += "[== \"CMake Server\" ==[\n";
fullMessage += msg;
fullMessage += "\n]== \"CMake Server\" ==]\n";
}
// The buffering strategy should cope with any fragmentation, including
// just getting the characters one at a time.
auto bufferingStrategy =
std::unique_ptr<cmConnectionBufferStrategy>(new cmServerBufferStrategy);
std::vector<std::string> response;
std::string rawBuffer;
for (size_t i = 0; i < fullMessage.size(); i++) {
rawBuffer += fullMessage[i];
std::string packet = bufferingStrategy->BufferMessage(rawBuffer);
do {
if (!packet.empty() && packet != "\r\n") {
response.push_back(trim_newline(packet));
}
packet = bufferingStrategy->BufferMessage(rawBuffer);
} while (!packet.empty());
}
if (response != messages) {
print_error(messages, response);
return 1;
}
// We should also be able to deal with getting a bunch at once
response.clear();
std::string packet = bufferingStrategy->BufferMessage(fullMessage);
do {
if (!packet.empty() && packet != "\r\n") {
response.push_back(trim_newline(packet));
}
packet = bufferingStrategy->BufferMessage(fullMessage);
} while (!packet.empty());
if (response != messages) {
print_error(messages, response);
return 1;
}
return 0;
}