From 9f9fbe48b66186939013584f675af127f30e1792 Mon Sep 17 00:00:00 2001 From: Jan Olszak Date: Wed, 12 Nov 2014 15:36:09 +0100 Subject: [PATCH] IPC: Pass error to return the value callback [Bug/Feature] Return value callback takes status enum Peer's socket is locket when processing the callbacks. [Cause] N/A [Solution] N/A [Verification] Build, install, run tests Change-Id: I74f30713e7c4fa6d8f35b79c2485137d1c9119c3 --- common/ipc/exception.hpp | 18 +++ common/ipc/internals/acceptor.cpp | 1 - common/ipc/internals/processor.cpp | 227 +++++++++++++++++++++++-------------- common/ipc/internals/processor.hpp | 38 +++++-- common/ipc/service.hpp | 4 +- common/ipc/types.cpp | 64 +++++++++++ common/ipc/types.hpp | 19 +++- tests/unit_tests/ipc/ut-ipc.cpp | 92 ++++++++++++++- 8 files changed, 359 insertions(+), 104 deletions(-) create mode 100644 common/ipc/types.cpp diff --git a/common/ipc/exception.hpp b/common/ipc/exception.hpp index 67d9c86..f9e8322 100644 --- a/common/ipc/exception.hpp +++ b/common/ipc/exception.hpp @@ -38,7 +38,25 @@ struct IPCException: public SecurityContainersException { IPCException(const std::string& error) : SecurityContainersException(error) {} }; +struct IPCParsingException: public IPCException { + IPCParsingException(const std::string& error) : IPCException(error) {} +}; + +struct IPCSerializationException: public IPCException { + IPCSerializationException(const std::string& error) : IPCException(error) {} +}; + +struct IPCPeerDisconnectedException: public IPCException { + IPCPeerDisconnectedException(const std::string& error) : IPCException(error) {} +}; + +struct IPCNaughtyPeerException: public IPCException { + IPCNaughtyPeerException(const std::string& error) : IPCException(error) {} +}; +struct IPCTimeoutException: public IPCException { + IPCTimeoutException(const std::string& error) : IPCException(error) {} +}; } diff --git a/common/ipc/internals/acceptor.cpp b/common/ipc/internals/acceptor.cpp index 9738546..193da61 100644 --- a/common/ipc/internals/acceptor.cpp +++ b/common/ipc/internals/acceptor.cpp @@ -103,7 +103,6 @@ void Acceptor::run() } LOGE("Error in poll: " << std::string(strerror(errno))); throw IPCException("Error in poll: " + std::string(strerror(errno))); - break; } // Check for incoming connections diff --git a/common/ipc/internals/processor.cpp b/common/ipc/internals/processor.cpp index 0465574..50327c0 100644 --- a/common/ipc/internals/processor.cpp +++ b/common/ipc/internals/processor.cpp @@ -36,10 +36,21 @@ #include #include - namespace security_containers { namespace ipc { +#define IGNORE_EXCEPTIONS(expr) \ + try \ + { \ + expr; \ + } \ + catch (const std::exception& e){ \ + LOGE("Callback threw an error: " << e.what()); \ + } + + + + const Processor::MethodID Processor::RETURN_METHOD_ID = std::numeric_limits::max(); Processor::Processor(const PeerCallback& newPeerCallback, @@ -98,10 +109,7 @@ Processor::PeerID Processor::addPeer(const std::shared_ptr& socketPtr) { Lock lock(mSocketsMutex); peerID = getNextPeerID(); - SocketInfo socketInfo; - socketInfo.peerID = peerID; - socketInfo.socketPtr = std::move(socketPtr); - mNewSockets.push(std::move(socketInfo)); + mNewSockets.emplace(peerID, std::move(socketPtr)); } LOGI("New peer added. Id: " << peerID); mEventQueue.send(Event::NEW_PEER); @@ -109,13 +117,29 @@ Processor::PeerID Processor::addPeer(const std::shared_ptr& socketPtr) return peerID; } -void Processor::removePeer(PeerID peerID) +void Processor::removePeer(const PeerID peerID, Status status) { LOGW("Removing naughty peer. ID: " << peerID); { Lock lock(mSocketsMutex); mSockets.erase(peerID); } + + { + // Erase associated return value callbacks + Lock lock(mReturnCallbacksMutex); + + std::shared_ptr data; + for (auto it = mReturnCallbacks.begin(); it != mReturnCallbacks.end();) { + if (it->second.peerID == peerID) { + IGNORE_EXCEPTIONS(it->second.process(status, data)); + it = mReturnCallbacks.erase(it); + } else { + ++it; + } + } + } + resetPolling(); } @@ -175,6 +199,7 @@ void Processor::run() } } + bool Processor::handleLostConnections() { std::list peersToRemove; @@ -190,14 +215,10 @@ bool Processor::handleLostConnections() } } - for (const auto peerID : peersToRemove) { - LOGT("Removing peer. ID: " << peerID); - mSockets.erase(peerID); - } } - if (!peersToRemove.empty()) { - resetPolling(); + for (const PeerID peerID : peersToRemove) { + removePeer(peerID, Status::PEER_DISCONNECTED); } return !peersToRemove.empty(); @@ -231,77 +252,102 @@ bool Processor::handleInput(const PeerID peerID, const Socket& socket) MethodID methodID; MessageID messageID; { - LOGI("Locking"); Socket::Guard guard = socket.getGuard(); socket.read(&methodID, sizeof(methodID)); socket.read(&messageID, sizeof(messageID)); - LOGI("Locked"); if (methodID == RETURN_METHOD_ID) { - LOGI("Return value for messageID: " << messageID); - ReturnCallbacks returnCallbacks; - try { - Lock lock(mReturnCallbacksMutex); - LOGT("Getting the return callback"); - returnCallbacks = std::move(mReturnCallbacks.at(messageID)); - mReturnCallbacks.erase(messageID); - } catch (const std::out_of_range&) { - LOGW("No return callback for messageID: " << messageID); - return false; - } + return onReturnValue(peerID, socket, messageID); + } else { + return onRemoteCall(peerID, socket, methodID, messageID); + } + } - std::shared_ptr data; - try { - LOGT("Parsing incoming return data"); - data = returnCallbacks.parse(socket.getFD()); - } catch (const IPCException&) { - removePeer(peerID); - return true; - } + return false; +} - guard.unlock(); +bool Processor::onReturnValue(const PeerID peerID, + const Socket& socket, + const MessageID messageID) +{ + LOGI("Return value for messageID: " << messageID); + ReturnCallbacks returnCallbacks; + try { + Lock lock(mReturnCallbacksMutex); + LOGT("Getting the return callback"); + returnCallbacks = std::move(mReturnCallbacks.at(messageID)); + mReturnCallbacks.erase(messageID); + } catch (const std::out_of_range&) { + LOGW("No return callback for messageID: " << messageID); + removePeer(peerID, Status::NAUGHTY_PEER); + return true; + } - LOGT("Process callback for methodID: " << methodID << "; messageID: " << messageID); - returnCallbacks.process(data); + std::shared_ptr data; + try { + LOGT("Parsing incoming return data"); + data = returnCallbacks.parse(socket.getFD()); + } catch (const std::exception& e) { + LOGE("Exception during parsing: " << e.what()); + IGNORE_EXCEPTIONS(returnCallbacks.process(Status::PARSING_ERROR, data)); + removePeer(peerID, Status::PARSING_ERROR); + return true; + } - } else { - LOGI("Remote call; methodID: " << methodID << " messageID: " << messageID); - std::shared_ptr methodCallbacks; - try { - Lock lock(mCallsMutex); - methodCallbacks = mMethodsCallbacks.at(methodID); - } catch (const std::out_of_range&) { - LOGW("No method callback for methodID: " << methodID); - removePeer(peerID); - return true; - } + LOGT("Process return value callback for messageID: " << messageID); + IGNORE_EXCEPTIONS(returnCallbacks.process(Status::OK, data)); - std::shared_ptr data; - try { - LOGT("Parsing incoming data"); - data = methodCallbacks->parse(socket.getFD()); - } catch (const IPCException&) { - removePeer(peerID); - return true; - } + return false; +} - guard.unlock(); - - LOGT("Process callback for methodID: " << methodID << "; messageID: " << messageID); - std::shared_ptr returnData = methodCallbacks->method(data); - - LOGT("Sending return data; methodID: " << methodID << "; messageID: " << messageID); - try { - // Send the call with the socket - Socket::Guard guard = socket.getGuard(); - socket.write(&RETURN_METHOD_ID, sizeof(RETURN_METHOD_ID)); - socket.write(&messageID, sizeof(messageID)); - methodCallbacks->serialize(socket.getFD(), returnData); - } catch (const IPCException&) { - removePeer(peerID); - return true; - } - } +bool Processor::onRemoteCall(const PeerID peerID, + const Socket& socket, + const MethodID methodID, + const MessageID messageID) +{ + LOGI("Remote call; methodID: " << methodID << " messageID: " << messageID); + + std::shared_ptr methodCallbacks; + try { + Lock lock(mCallsMutex); + methodCallbacks = mMethodsCallbacks.at(methodID); + } catch (const std::out_of_range&) { + LOGW("No method callback for methodID: " << methodID); + removePeer(peerID, Status::NAUGHTY_PEER); + return true; + } + + std::shared_ptr data; + try { + LOGT("Parsing incoming data"); + data = methodCallbacks->parse(socket.getFD()); + } catch (const std::exception& e) { + LOGE("Exception during parsing: " << e.what()); + removePeer(peerID, Status::PARSING_ERROR); + return true; + } + + LOGT("Process callback for methodID: " << methodID << "; messageID: " << messageID); + std::shared_ptr returnData; + try { + returnData = methodCallbacks->method(data); + } catch (const std::exception& e) { + LOGE("Exception in method handler: " << e.what()); + removePeer(peerID, Status::NAUGHTY_PEER); + return true; + } + + LOGT("Sending return data; methodID: " << methodID << "; messageID: " << messageID); + try { + // Send the call with the socket + Socket::Guard guard = socket.getGuard(); + socket.write(&RETURN_METHOD_ID, sizeof(RETURN_METHOD_ID)); + socket.write(&messageID, sizeof(messageID)); + methodCallbacks->serialize(socket.getFD(), returnData); + } catch (const std::exception& e) { + LOGE("Exception during serialization: " << e.what()); + removePeer(peerID, Status::SERIALIZATION_ERROR); + return true; } return false; @@ -325,8 +371,7 @@ bool Processor::handleEvent() case Event::CALL: { LOGD("Event CALL"); - handleCall(); - return false; + return handleCall(); } case Event::NEW_PEER: { @@ -340,15 +385,16 @@ bool Processor::handleEvent() if (mSockets.size() > mMaxNumberOfPeers) { LOGE("There are too many peers. I don't accept the connection with " << socketInfo.peerID); - + return false; } if (mSockets.count(socketInfo.peerID) != 0) { LOGE("There already was a socket for peerID: " << socketInfo.peerID); + return false; } - mSockets[socketInfo.peerID] = socketInfo.socketPtr; + + mSockets.emplace(socketInfo.peerID, std::move(socketInfo.socketPtr)); } resetPolling(); - if (mNewPeerCallback) { // Notify about the new user. mNewPeerCallback(socketInfo.peerID); @@ -384,23 +430,20 @@ Processor::Call Processor::getCall() return call; } -void Processor::handleCall() +bool Processor::handleCall() { - LOGT("Handle call from another thread"); + LOGT("Handle call (from another thread) to send a message."); Call call = getCall(); - ReturnCallbacks returnCallbacks; - returnCallbacks.parse = call.parse; - returnCallbacks.process = call.process; - std::shared_ptr socketPtr; try { - // Get the addressee's socket + // Get the peer's socket Lock lock(mSocketsMutex); socketPtr = mSockets.at(call.peerID); } catch (const std::out_of_range&) { LOGE("Peer disconnected. No socket with a peerID: " << call.peerID); - return; + IGNORE_EXCEPTIONS(call.process(Status::PEER_DISCONNECTED, call.data)); + return false; } MessageID messageID = getNextMessageID(); @@ -411,7 +454,9 @@ void Processor::handleCall() if (mReturnCallbacks.count(messageID) != 0) { LOGE("There already was a return callback for messageID: " << messageID); } - mReturnCallbacks[messageID] = std::move(returnCallbacks); + mReturnCallbacks.emplace(messageID, ReturnCallbacks(call.peerID, + std::move(call.parse), + std::move(call.process))); } try { @@ -422,12 +467,20 @@ void Processor::handleCall() call.serialize(socketPtr->getFD(), call.data); } catch (const std::exception& e) { LOGE("Error during sending a message: " << e.what()); + + // Inform about the error + IGNORE_EXCEPTIONS(mReturnCallbacks[messageID].process(Status::SERIALIZATION_ERROR, call.data)); + { Lock lock(mReturnCallbacksMutex); mReturnCallbacks.erase(messageID); } - // TODO: User should get the error code. + + removePeer(call.peerID, Status::SERIALIZATION_ERROR); + return true; } + + return false; } } // namespace ipc diff --git a/common/ipc/internals/processor.hpp b/common/ipc/internals/processor.hpp index cb14a67..3df3f9c 100644 --- a/common/ipc/internals/processor.hpp +++ b/common/ipc/internals/processor.hpp @@ -71,6 +71,8 @@ const unsigned int DEFAULT_MAX_NUMBER_OF_PEERS = 500; * - don't throw timeout if the message is already processed * - naming convention or methods that just commissions the PROCESS thread to do something * - removePeer API function +* - error handling - special message type +* - some mutexes may not be needed */ class Processor { public: @@ -214,6 +216,10 @@ private: ReturnCallbacks(ReturnCallbacks&&) = default; ReturnCallbacks& operator=(ReturnCallbacks &&) = default; + ReturnCallbacks(PeerID peerID, const ParseCallback& parse, const ResultHandler::type& process) + : peerID(peerID), parse(parse), process(process) {} + + PeerID peerID; ParseCallback parse; ResultHandler::type process; }; @@ -225,8 +231,11 @@ private: SocketInfo(SocketInfo&&) = default; SocketInfo& operator=(SocketInfo &&) = default; - std::shared_ptr socketPtr; + SocketInfo(const PeerID peerID, const std::shared_ptr& socketPtr) + : peerID(peerID), socketPtr(socketPtr) {} + PeerID peerID; + std::shared_ptr socketPtr; }; enum class Event : int { @@ -268,15 +277,22 @@ private: void run(); bool handleEvent(); - void handleCall(); + bool handleCall(); bool handleLostConnections(); bool handleInputs(); bool handleInput(const PeerID peerID, const Socket& socket); + bool onReturnValue(const PeerID peerID, + const Socket& socket, + const MessageID messageID); + bool onRemoteCall(const PeerID peerID, + const Socket& socket, + const MethodID methodID, + const MessageID messageID); void resetPolling(); MessageID getNextMessageID(); PeerID getNextPeerID(); Call getCall(); - void removePeer(PeerID peerID); + void removePeer(const PeerID peerID, Status status); }; @@ -352,9 +368,9 @@ void Processor::callAsync(const MethodID methodID, config::saveToFD(fd, *std::static_pointer_cast(data)); }; - call.process = [process](std::shared_ptr& data)->void { + call.process = [process](Status status, std::shared_ptr& data)->void { std::shared_ptr tmpData = std::static_pointer_cast(data); - return process(tmpData); + return process(status, tmpData); }; { @@ -387,8 +403,10 @@ std::shared_ptr Processor::callSync(const MethodID methodID, std::mutex mtx; std::unique_lock lck(mtx); std::condition_variable cv; + Status returnStatus = ipc::Status::UNDEFINED; - auto process = [&result, &cv](std::shared_ptr returnedData) { + auto process = [&result, &cv, &returnStatus](Status status, std::shared_ptr returnedData) { + returnStatus = status; result = returnedData; cv.notify_one(); }; @@ -399,15 +417,17 @@ std::shared_ptr Processor::callSync(const MethodID methodID, data, process); - auto isResultInitialized = [&result]() { - return static_cast(result); + auto isResultInitialized = [&returnStatus]() { + return returnStatus != ipc::Status::UNDEFINED; }; if (!cv.wait_for(lck, std::chrono::milliseconds(timeoutMS), isResultInitialized)) { LOGE("Function call timeout; methodID: " << methodID); - throw IPCException("Function call timeout; methodID: " + std::to_string(methodID)); + throw IPCTimeoutException("Function call timeout; methodID: " + std::to_string(methodID)); } + throwOnError(returnStatus); + return result; } diff --git a/common/ipc/service.hpp b/common/ipc/service.hpp index 08f7ec9..873673d 100644 --- a/common/ipc/service.hpp +++ b/common/ipc/service.hpp @@ -103,7 +103,7 @@ public: std::shared_ptr callSync(const MethodID methodID, const PeerID peerID, const std::shared_ptr& data, - unsigned int timeoutMS); + unsigned int timeoutMS = 500); /** * Asynchronous method call. The return callback will be called on @@ -140,7 +140,7 @@ template std::shared_ptr Service::callSync(const MethodID methodID, const PeerID peerID, const std::shared_ptr& data, - unsigned int timeoutMS = 500) + unsigned int timeoutMS) { LOGD("Sync calling method: " << methodID << " for user: " << peerID); return mProcessor.callSync(methodID, peerID, data, timeoutMS); diff --git a/common/ipc/types.cpp b/common/ipc/types.cpp new file mode 100644 index 0000000..e0ffc5b --- /dev/null +++ b/common/ipc/types.cpp @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2014 Samsung Electronics Co., Ltd All Rights Reserved +* +* Contact: Jan Olszak +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License +*/ + +/** + * @file + * @author Jan Olszak (j.olszak@samsung.com) + * @brief Types definitions and helper functions + */ + +#include "ipc/types.hpp" +#include "logger/logger.hpp" + + +namespace security_containers { +namespace ipc { + +std::string toString(const Status status) +{ + switch (status) { + case Status::OK: return "No error, everything is OK"; + case Status::PARSING_ERROR: return "Exception during reading/parsing data from the socket"; + case Status::SERIALIZATION_ERROR: return "Exception during writing/serializing data to the socket"; + case Status::PEER_DISCONNECTED: return "No such peer. Might got disconnected."; + case Status::NAUGHTY_PEER: return "Peer performed a forbidden action."; + case Status::UNDEFINED: return "Undefined state"; + default: return "Unknown status"; + } +} + +void throwOnError(const Status status) +{ + if (status == Status::OK) { + return; + } + + std::string message = toString(status); + LOGE(message); + + switch (status) { + case Status::PARSING_ERROR: throw IPCParsingException(message); + case Status::SERIALIZATION_ERROR: throw IPCSerializationException(message); + case Status::PEER_DISCONNECTED: throw IPCPeerDisconnectedException(message); + case Status::NAUGHTY_PEER: throw IPCNaughtyPeerException(message); + case Status::UNDEFINED: throw IPCException(message); + default: return throw IPCException(message); + } +} +} // namespace ipc +} // namespace security_containers diff --git a/common/ipc/types.hpp b/common/ipc/types.hpp index 4269664..c07e504 100644 --- a/common/ipc/types.hpp +++ b/common/ipc/types.hpp @@ -19,18 +19,33 @@ /** * @file * @author Jan Olszak (j.olszak@samsung.com) - * @brief Handler types definitions + * @brief Types definitions */ #ifndef COMMON_IPC_HANDLERS_HPP #define COMMON_IPC_HANDLERS_HPP +#include "ipc/exception.hpp" + #include #include +#include namespace security_containers { namespace ipc { +enum class Status : int { + OK = 0, + PARSING_ERROR, + SERIALIZATION_ERROR, + PEER_DISCONNECTED, + NAUGHTY_PEER, + UNDEFINED +}; + +std::string toString(const Status status); +void throwOnError(const Status status); + template struct MethodHandler { typedef std::function(std::shared_ptr&)> type; @@ -39,7 +54,7 @@ struct MethodHandler { template struct ResultHandler { - typedef std::function&)> type; + typedef std::function&)> type; }; } // namespace ipc diff --git a/tests/unit_tests/ipc/ut-ipc.cpp b/tests/unit_tests/ipc/ut-ipc.cpp index 48ad1e5..ea70b45 100644 --- a/tests/unit_tests/ipc/ut-ipc.cpp +++ b/tests/unit_tests/ipc/ut-ipc.cpp @@ -75,6 +75,21 @@ struct EmptyData { CONFIG_REGISTER_EMPTY }; +struct ThrowOnAcceptData { + template + void accept(Visitor) + { + LOGE("Serialization and parsing failed"); + throw std::exception(); + } + template + void accept(Visitor) const + { + LOGE("Const Serialization and parsing failed"); + throw std::exception(); + } +}; + std::shared_ptr returnEmptyCallback(std::shared_ptr&) { return std::shared_ptr(new EmptyData()); @@ -90,6 +105,12 @@ std::shared_ptr echoCallback(std::shared_ptr& data) return data; } +std::shared_ptr longEchoCallback(std::shared_ptr& data) +{ + std::this_thread::sleep_for(std::chrono::seconds(1)); + return data; +} + void testEcho(Client& c, const Client::MethodID methodID) { std::shared_ptr sentData(new SendData(34)); @@ -283,7 +304,8 @@ BOOST_AUTO_TEST_CASE(AsyncClientToServiceEchoTest) //Async call std::shared_ptr sentData(new SendData(34)); std::shared_ptr recvData; - auto dataBack = [&cv, &recvData](std::shared_ptr& data) { + auto dataBack = [&cv, &recvData](ipc::Status status, std::shared_ptr& data) { + BOOST_CHECK(status == ipc::Status::OK); recvData = data; cv.notify_one(); }; @@ -324,7 +346,8 @@ BOOST_AUTO_TEST_CASE(AsyncServiceToClientEchoTest) std::shared_ptr sentData(new SendData(56)); std::shared_ptr recvData; - auto dataBack = [&cv, &recvData](std::shared_ptr& data) { + auto dataBack = [&cv, &recvData](ipc::Status status, std::shared_ptr& data) { + BOOST_CHECK(status == ipc::Status::OK); recvData = data; cv.notify_one(); }; @@ -343,15 +366,78 @@ BOOST_AUTO_TEST_CASE(AsyncServiceToClientEchoTest) BOOST_AUTO_TEST_CASE(SyncTimeoutTest) { Service s(socketPath); + s.addMethodHandler(1, longEchoCallback); + + s.start(); + Client c(socketPath); + c.start(); + + std::shared_ptr sentData(new SendData(78)); + + BOOST_CHECK_THROW((c.callSync(1, sentData, 10)), IPCException); +} + +BOOST_AUTO_TEST_CASE(SerializationErrorTest) +{ + Service s(socketPath); + s.addMethodHandler(1, echoCallback); + s.start(); + + Client c(socketPath); + c.start(); + + std::shared_ptr throwingData(new ThrowOnAcceptData()); + + BOOST_CHECK_THROW((c.callSync(1, throwingData)), IPCSerializationException); + +} + +BOOST_AUTO_TEST_CASE(ParseErrorTest) +{ + Service s(socketPath); s.addMethodHandler(1, echoCallback); + s.start(); + + Client c(socketPath); + c.start(); + + std::shared_ptr sentData(new SendData(78)); + BOOST_CHECK_THROW((c.callSync(1, sentData, 10000)), IPCParsingException); +} + +BOOST_AUTO_TEST_CASE(DisconnectedPeerErrorTest) +{ + Service s(socketPath); + + auto method = [](std::shared_ptr&) { + return std::shared_ptr(new SendData(1)); + }; + // Method will throw during serialization and disconnect automatically + s.addMethodHandler(1, method); s.start(); + Client c(socketPath); c.start(); + std::mutex mtx; + std::unique_lock lck(mtx); + std::condition_variable cv; + ipc::Status retStatus = ipc::Status::UNDEFINED; + + auto dataBack = [&cv, &retStatus](ipc::Status status, std::shared_ptr&) { + retStatus = status; + cv.notify_one(); + }; + std::shared_ptr sentData(new SendData(78)); + c.callAsync(1, sentData, dataBack); - BOOST_CHECK_THROW((c.callSync(1, sentData, 1)), IPCException); + // Wait for the response + BOOST_CHECK(cv.wait_for(lck, std::chrono::seconds(10), [&retStatus]() { + return retStatus != ipc::Status::UNDEFINED; + })); + BOOST_CHECK(retStatus == ipc::Status::PEER_DISCONNECTED); } -- 2.7.4