Commit 4d07784b authored by Thomas Markwalder's avatar Thomas Markwalder
Browse files

[3087] Use IOServicePtr consistently in DHCP-DDNS

Some classes were using references to isc::asiolink::IOService, others
where using d2::IOServicePtr.  The latter is now used throughout for
consistency as well as support for future, possible, multi-threaded
implementation.
parent 2e425ff1
......@@ -50,6 +50,7 @@ BUILT_SOURCES = spec_config.h d2_messages.h d2_messages.cc
pkglibexec_PROGRAMS = b10-dhcp-ddns
b10_dhcp_ddns_SOURCES = main.cc
b10_dhcp_ddns_SOURCES += d2_asio.h
b10_dhcp_ddns_SOURCES += d2_log.cc d2_log.h
b10_dhcp_ddns_SOURCES += d2_process.cc d2_process.h
b10_dhcp_ddns_SOURCES += d_controller.cc d_controller.h
......
......@@ -12,40 +12,20 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#ifndef TEST_LIBRARIES_H
#define TEST_LIBRARIES_H
#ifndef D2_ASIO_H
#define D2_ASIO_H
#include <config.h>
#include <asiolink/asiolink.h>
namespace {
#include <boost/shared_ptr.hpp>
namespace isc {
namespace d2 {
// Take care of differences in DLL naming between operating systems.
/// @brief Defines a smart pointer to an IOService instance.
typedef boost::shared_ptr<isc::asiolink::IOService> IOServicePtr;
#ifdef OS_OSX
#define DLL_SUFFIX ".dylib"
#else
#define DLL_SUFFIX ".so"
}; // namespace isc::d2
}; // namespace isc
#endif
// Names of the libraries used in these tests. These libraries are built using
// libtool, so we need to look in the hidden ".libs" directory to locate the
// shared library.
// Library with load/unload functions creating marker files to check their
// operation.
static const char* CALLOUT_LIBRARY_1 = "/Users/tmark/ddns/build/trac3156/bind10/src/lib/dhcpsrv/tests/.libs/libco1"
DLL_SUFFIX;
static const char* CALLOUT_LIBRARY_2 = "/Users/tmark/ddns/build/trac3156/bind10/src/lib/dhcpsrv/tests/.libs/libco2"
DLL_SUFFIX;
// Name of a library which is not present.
static const char* NOT_PRESENT_LIBRARY = "/Users/tmark/ddns/build/trac3156/bind10/src/lib/dhcpsrv/tests/.libs/libnothere"
DLL_SUFFIX;
} // anonymous namespace
#endif // TEST_LIBRARIES_H
......@@ -15,9 +15,9 @@
#ifndef D2_CFG_MGR_H
#define D2_CFG_MGR_H
#include <asiolink/io_address.h>
#include <cc/data.h>
#include <exceptions/exceptions.h>
#include <d2/d2_asio.h>
#include <d2/d_cfg_mgr.h>
#include <d2/d2_config.h>
......
......@@ -15,8 +15,8 @@
#ifndef D2_CONFIG_H
#define D2_CONFIG_H
#include <asiolink/io_address.h>
#include <cc/data.h>
#include <d2/d2_asio.h>
#include <d2/d_cfg_mgr.h>
#include <dhcpsrv/dhcp_parsers.h>
#include <exceptions/exceptions.h>
......
......@@ -35,13 +35,13 @@ D2Process::D2Process(const char* name, IOServicePtr io_service)
// been received. This means that until we receive the configuration,
// D2 will neither receive nor process NameChangeRequests.
// Pass in IOService for NCR IO event processing.
queue_mgr_.reset(new D2QueueMgr(*getIoService()));
queue_mgr_.reset(new D2QueueMgr(getIoService()));
// Instantiate update manager.
// Pass in both queue manager and configuration manager.
// Pass in IOService for DNS update transaction IO event processing.
D2CfgMgrPtr tmp = getD2CfgMgr();
update_mgr_.reset(new D2UpdateMgr(queue_mgr_, tmp, *getIoService()));
update_mgr_.reset(new D2UpdateMgr(queue_mgr_, tmp, getIoService()));
};
void
......
......@@ -22,10 +22,13 @@ namespace d2 {
// Makes constant visible to Google test macros.
const size_t D2QueueMgr::MAX_QUEUE_DEFAULT;
D2QueueMgr::D2QueueMgr(isc::asiolink::IOService& io_service,
const size_t max_queue_size)
D2QueueMgr::D2QueueMgr(IOServicePtr& io_service, const size_t max_queue_size)
: io_service_(io_service), max_queue_size_(max_queue_size),
mgr_state_(NOT_INITTED), target_stop_state_(NOT_INITTED) {
if (!io_service_) {
isc_throw(D2QueueMgrError, "IOServicePtr cannot be null");
}
// Use setter to do validation.
setMaxQueueSize(max_queue_size);
}
......@@ -129,7 +132,7 @@ D2QueueMgr::startListening() {
// Instruct the listener to start listening and set state accordingly.
try {
listener_->startListening(io_service_);
listener_->startListening(*io_service_);
mgr_state_ = RUNNING;
} catch (const isc::Exception& ex) {
isc_throw(D2QueueMgrError, "D2QueueMgr listener start failed: "
......
......@@ -17,9 +17,8 @@
/// @file d2_queue_mgr.h This file defines the class D2QueueMgr.
#include <asiolink/io_address.h>
#include <asiolink/io_service.h>
#include <exceptions/exceptions.h>
#include <d2/d2_asio.h>
#include <dhcp_ddns/ncr_msg.h>
#include <dhcp_ddns/ncr_io.h>
......@@ -166,7 +165,7 @@ public:
/// This value must be greater than zero. It defaults to MAX_QUEUE_DEFAULT.
///
/// @throw D2QueueMgrError if max_queue_size is zero.
D2QueueMgr(isc::asiolink::IOService& io_service,
D2QueueMgr(IOServicePtr& io_service,
const size_t max_queue_size = MAX_QUEUE_DEFAULT);
/// @brief Destructor
......@@ -328,7 +327,7 @@ public:
void updateStopState();
/// @brief IOService that our listener should use for IO management.
isc::asiolink::IOService& io_service_;
IOServicePtr io_service_;
/// @brief Dictates the maximum number of entries allowed in the queue.
size_t max_queue_size_;
......
......@@ -24,7 +24,7 @@ namespace d2 {
const size_t D2UpdateMgr::MAX_TRANSACTIONS_DEFAULT;
D2UpdateMgr::D2UpdateMgr(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
isc::asiolink::IOService& io_service,
IOServicePtr& io_service,
const size_t max_transactions)
:queue_mgr_(queue_mgr), cfg_mgr_(cfg_mgr), io_service_(io_service) {
if (!queue_mgr_) {
......@@ -36,6 +36,10 @@ D2UpdateMgr::D2UpdateMgr(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
"D2UpdateMgr configuration manager cannot be null");
}
if (!io_service_) {
isc_throw(D2UpdateMgrError, "IOServicePtr cannot be null");
}
// Use setter to do validation.
setMaxTransactions(max_transactions);
}
......
......@@ -17,8 +17,8 @@
/// @file d2_update_mgr.h This file defines the class D2UpdateMgr.
#include <asiolink/io_service.h>
#include <exceptions/exceptions.h>
#include <d2/d2_asio.h>
#include <d2/d2_log.h>
#include <d2/d2_queue_mgr.h>
#include <d2/d2_cfg_mgr.h>
......@@ -100,7 +100,7 @@ public:
/// @throw D2UpdateMgrError if either the queue manager or configuration
/// managers are NULL, or max transactions is less than one.
D2UpdateMgr(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
isc::asiolink::IOService& io_service,
IOServicePtr& io_service,
const size_t max_transactions = MAX_TRANSACTIONS_DEFAULT);
/// @brief Destructor
......@@ -228,7 +228,7 @@ private:
/// passed into transactions to manager their IO events.
/// (For future reference, multi-threaded transactions would each use their
/// own IOService instance.)
isc::asiolink::IOService& io_service_;
IOServicePtr io_service_;
/// @brief Maximum number of concurrent transactions.
size_t max_transactions_;
......
......@@ -15,10 +15,10 @@
#ifndef D_CONTROLLER_H
#define D_CONTROLLER_H
#include <asiolink/asiolink.h>
#include <cc/data.h>
#include <cc/session.h>
#include <config/ccsession.h>
#include <d2/d2_asio.h>
#include <d2/d2_log.h>
#include <d2/d_process.h>
#include <exceptions/exceptions.h>
......
......@@ -15,16 +15,14 @@
#ifndef D_PROCESS_H
#define D_PROCESS_H
#include <asiolink/asiolink.h>
#include <cc/data.h>
#include <d2/d2_asio.h>
#include <d2/d_cfg_mgr.h>
#include <boost/shared_ptr.hpp>
#include <exceptions/exceptions.h>
typedef boost::shared_ptr<isc::asiolink::IOService> IOServicePtr;
namespace isc {
namespace d2 {
......
......@@ -65,7 +65,7 @@ public:
virtual void operator()(asiodns::IOFetch::Result result);
// Starts asynchronous DNS Update.
void doUpdate(asiolink::IOService& io_service,
void doUpdate(IOServicePtr& io_service,
const asiolink::IOAddress& ns_addr,
const uint16_t ns_port,
D2UpdateMessage& update,
......@@ -162,7 +162,7 @@ DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
}
void
DNSClientImpl::doUpdate(IOService& io_service,
DNSClientImpl::doUpdate(IOServicePtr& io_service,
const IOAddress& ns_addr,
const uint16_t ns_port,
D2UpdateMessage& update,
......@@ -189,11 +189,11 @@ DNSClientImpl::doUpdate(IOService& io_service,
// Timeout value is explicitly cast to the int type to avoid warnings about
// overflows when doing implicit cast. It should have been checked by the
// caller that the unsigned timeout value will fit into int.
IOFetch io_fetch(IOFetch::UDP, io_service, msg_buf, ns_addr, ns_port,
IOFetch io_fetch(IOFetch::UDP, *io_service, msg_buf, ns_addr, ns_port,
in_buf_, this, static_cast<int>(wait));
// Post the task to the task queue in the IO service. Caller will actually
// run these tasks by executing IOService::run.
io_service.post(io_fetch);
io_service->post(io_fetch);
}
......@@ -213,7 +213,7 @@ DNSClient::getMaxTimeout() {
}
void
DNSClient::doUpdate(IOService&,
DNSClient::doUpdate(IOServicePtr&,
const IOAddress&,
const uint16_t,
D2UpdateMessage&,
......@@ -224,11 +224,16 @@ DNSClient::doUpdate(IOService&,
}
void
DNSClient::doUpdate(IOService& io_service,
DNSClient::doUpdate(IOServicePtr& io_service,
const IOAddress& ns_addr,
const uint16_t ns_port,
D2UpdateMessage& update,
const unsigned int wait) {
if (!io_service) {
isc_throw(isc::BadValue,
"DNSClient::doUpdate: IOService cannot be null");
}
// The underlying implementation which we use to send DNS Updates uses
// signed integers for timeout. If we want to avoid overflows we need to
// respect this limitation here.
......
......@@ -16,8 +16,8 @@
#define DNS_CLIENT_H
#include <d2/d2_update_message.h>
#include <d2/d2_asio.h>
#include <asiolink/io_service.h>
#include <util/buffer.h>
#include <asiodns/io_fetch.h>
......@@ -151,7 +151,7 @@ public:
///
/// @todo Implement TSIG Support. Currently any attempt to call this
/// function will result in exception.
void doUpdate(asiolink::IOService& io_service,
void doUpdate(IOServicePtr& io_service,
const asiolink::IOAddress& ns_addr,
const uint16_t ns_port,
D2UpdateMessage& update,
......@@ -176,7 +176,7 @@ public:
/// @param wait A timeout (in seconds) for the response. If a response is
/// not received within the timeout, exchange is interrupted. This value
/// must not exceed maximal value for 'int' data type.
void doUpdate(asiolink::IOService& io_service,
void doUpdate(IOServicePtr& io_service,
const asiolink::IOAddress& ns_addr,
const uint16_t ns_port,
D2UpdateMessage& update,
......
......@@ -39,7 +39,7 @@ const int NameChangeTransaction::UPDATE_FAILED_EVT;
const int NameChangeTransaction::NCT_DERIVED_EVENT_MIN;
NameChangeTransaction::
NameChangeTransaction(isc::asiolink::IOService& io_service,
NameChangeTransaction(IOServicePtr& io_service,
dhcp_ddns::NameChangeRequestPtr& ncr,
DdnsDomainPtr& forward_domain,
DdnsDomainPtr& reverse_domain)
......@@ -48,8 +48,15 @@ NameChangeTransaction(isc::asiolink::IOService& io_service,
dns_update_status_(DNSClient::OTHER), dns_update_response_(),
forward_change_completed_(false), reverse_change_completed_(false),
current_server_list_(), current_server_(), next_server_pos_(0) {
// @todo if io_service is NULL we are multi-threading and should
// instantiate our own
if (!io_service_) {
isc_throw(NameChangeTransactionError, "IOServicePtr cannot be null");
}
if (!ncr_) {
isc_throw(NameChangeTransactionError, "NameChangeRequest cannot null");
isc_throw(NameChangeTransactionError,
"NameChangeRequest cannot be null");
}
if (ncr_->isForwardChange() && !(forward_domain_)) {
......
......@@ -17,8 +17,8 @@
/// @file nc_trans.h This file defines the class NameChangeTransaction.
#include <asiolink/io_service.h>
#include <exceptions/exceptions.h>
#include <d2/d2_asio.h>
#include <d2/d2_config.h>
#include <d2/dns_client.h>
#include <d2/state_model.h>
......@@ -163,7 +163,7 @@ public:
/// @throw NameChangeTransactionError if given an null request,
/// if forward change is enabled but forward domain is null, if
/// reverse change is enabled but reverse domain is null.
NameChangeTransaction(isc::asiolink::IOService& io_service,
NameChangeTransaction(IOServicePtr& io_service,
dhcp_ddns::NameChangeRequestPtr& ncr,
DdnsDomainPtr& forward_domain,
DdnsDomainPtr& reverse_domain);
......@@ -376,7 +376,7 @@ public:
private:
/// @brief The IOService which should be used to for IO processing.
isc::asiolink::IOService& io_service_;
IOServicePtr io_service_;
/// @brief The NameChangeRequest that the transaction is to fulfill.
dhcp_ddns::NameChangeRequestPtr ncr_;
......
......@@ -17,7 +17,6 @@
/// @file state_model.h This file defines the class StateModel.
#include <asiolink/io_service.h>
#include <exceptions/exceptions.h>
#include <d2/d2_config.h>
#include <d2/dns_client.h>
......
......@@ -51,7 +51,8 @@ if HAVE_GTEST
TESTS += d2_unittests
d2_unittests_SOURCES = ../d2_log.h ../d2_log.cc
d2_unittests_SOURCES = ../d2_asio.h
d2_unittests_SOURCES += ../d2_log.h ../d2_log.cc
d2_unittests_SOURCES += ../d_process.h
d2_unittests_SOURCES += ../d_controller.cc ../d2_controller.h
d2_unittests_SOURCES += ../d2_process.cc ../d2_process.h
......
......@@ -12,7 +12,7 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <asiolink/interval_timer.h>
#include <d2/d2_asio.h>
#include <d2/d2_queue_mgr.h>
#include <dhcp_ddns/ncr_udp.h>
#include <util/time_utilities.h>
......@@ -78,7 +78,7 @@ const long TEST_TIMEOUT = 5 * 1000;
/// @brief Tests that construction with max queue size of zero is not allowed.
TEST(D2QueueMgrBasicTest, construction1) {
isc::asiolink::IOService io_service;
IOServicePtr io_service(new isc::asiolink::IOService());
// Verify that constructing with max queue size of zero is not allowed.
EXPECT_THROW(D2QueueMgr(io_service, 0), D2QueueMgrError);
......@@ -86,7 +86,7 @@ TEST(D2QueueMgrBasicTest, construction1) {
/// @brief Tests default construction works.
TEST(D2QueueMgrBasicTest, construction2) {
isc::asiolink::IOService io_service;
IOServicePtr io_service(new isc::asiolink::IOService());
// Verify that valid constructor works.
D2QueueMgrPtr queue_mgr;
......@@ -97,7 +97,7 @@ TEST(D2QueueMgrBasicTest, construction2) {
/// @brief Tests construction with custom queue size works properly
TEST(D2QueueMgrBasicTest, construction3) {
isc::asiolink::IOService io_service;
IOServicePtr io_service(new isc::asiolink::IOService());
// Verify that custom queue size constructor works.
D2QueueMgrPtr queue_mgr;
......@@ -114,7 +114,7 @@ TEST(D2QueueMgrBasicTest, construction3) {
/// 4. Peek returns the first entry on the queue without altering queue content
/// 5. Dequeue removes the first entry on the queue
TEST(D2QueueMgrBasicTest, basicQueue) {
isc::asiolink::IOService io_service;
IOServicePtr io_service(new isc::asiolink::IOService());
// Construct the manager with max queue size set to number of messages
// we'll use.
......@@ -206,7 +206,7 @@ bool checkSendVsReceived(NameChangeRequestPtr sent_ncr,
class QueueMgrUDPTest : public virtual ::testing::Test,
NameChangeSender::RequestSendHandler {
public:
isc::asiolink::IOService io_service_;
IOServicePtr io_service_;
NameChangeSenderPtr sender_;
isc::asiolink::IntervalTimer test_timer_;
D2QueueMgrPtr queue_mgr_;
......@@ -215,7 +215,8 @@ public:
std::vector<NameChangeRequestPtr> sent_ncrs_;
std::vector<NameChangeRequestPtr> received_ncrs_;
QueueMgrUDPTest() : io_service_(), test_timer_(io_service_) {
QueueMgrUDPTest() : io_service_(new isc::asiolink::IOService()),
test_timer_(*io_service_) {
isc::asiolink::IOAddress addr(TEST_ADDRESS);
// Create our sender instance. Note that reuse_address is true.
sender_.reset(new NameChangeUDPSender(addr, SENDER_PORT,
......@@ -245,7 +246,7 @@ public:
///
/// This callback stops all running (hanging) tasks on IO service.
void testTimeoutHandler() {
io_service_.stop();
io_service_->stop();
FAIL() << "Test timeout hit.";
}
};
......@@ -296,7 +297,7 @@ TEST_F (QueueMgrUDPTest, stateModel) {
// Stopping requires IO cancel, which result in a callback.
// So process one event and verify we are STOPPED.
io_service_.run_one();
io_service_->run_one();
EXPECT_EQ(D2QueueMgr::STOPPED, queue_mgr_->getMgrState());
// Verify that we can re-enter the RUNNING from STOPPED by starting the
......@@ -313,7 +314,7 @@ TEST_F (QueueMgrUDPTest, stateModel) {
// Stopping requires IO cancel, which result in a callback.
// So process one event and verify we are STOPPED.
io_service_.run_one();
io_service_->run_one();
EXPECT_EQ(D2QueueMgr::STOPPED, queue_mgr_->getMgrState());
// Verify that we can remove the listener in the STOPPED state and
......@@ -355,7 +356,7 @@ TEST_F (QueueMgrUDPTest, liveFeed) {
ASSERT_EQ(D2QueueMgr::RUNNING, queue_mgr_->getMgrState());
// Place the sender into sending state.
ASSERT_NO_THROW(sender_->startSending(io_service_));
ASSERT_NO_THROW(sender_->startSending(*io_service_));
ASSERT_TRUE(sender_->amSending());
// Iterate over the list of requests sending and receiving
......@@ -366,8 +367,8 @@ TEST_F (QueueMgrUDPTest, liveFeed) {
ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
// running two should do the send then the receive
io_service_.run_one();
io_service_.run_one();
io_service_->run_one();
io_service_->run_one();
// Verify that the request can be added to the queue and queue
// size increments accordingly.
......@@ -390,8 +391,8 @@ TEST_F (QueueMgrUDPTest, liveFeed) {
ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
// running two should do the send then the receive
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_->run_one());
EXPECT_NO_THROW(io_service_->run_one());
EXPECT_EQ(i+1, queue_mgr_->getQueueSize());
}
......@@ -400,11 +401,11 @@ TEST_F (QueueMgrUDPTest, liveFeed) {
// Send another. The send should succeed.
ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_->run_one());
// Now execute the receive which should not throw but should move us
// to STOPPED_QUEUE_FULL state.
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_->run_one());
EXPECT_EQ(D2QueueMgr::STOPPED_QUEUE_FULL, queue_mgr_->getMgrState());
// Verify queue size did not increase beyond max.
......@@ -430,10 +431,10 @@ TEST_F (QueueMgrUDPTest, liveFeed) {
// Verify that we can again receive requests.
// Send should be fine.
ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_->run_one());
// Receive should succeed.
EXPECT_NO_THROW(io_service_.run_one());
EXPECT_NO_THROW(io_service_->run_one());
EXPECT_EQ(1, queue_mgr_->getQueueSize());
}
......
......@@ -12,7 +12,7 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <asiolink/interval_timer.h>
#include <d2/d2_asio.h>
#include <d2/d2_update_mgr.h>
#include <util/time_utilities.h>
#include <d_test_stubs.h>
......@@ -41,7 +41,7 @@ public:
///
/// Parameters match those needed by D2UpdateMgr.
D2UpdateMgrWrapper(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
isc::asiolink::IOService& io_service,
IOServicePtr& io_service,
const size_t max_transactions = MAX_TRANSACTIONS_DEFAULT)
: D2UpdateMgr(queue_mgr, cfg_mgr, io_service, max_transactions) {
}
......@@ -68,7 +68,7 @@ typedef boost::shared_ptr<D2UpdateMgrWrapper> D2UpdateMgrWrapperPtr;
/// functions.
class D2UpdateMgrTest : public ConfigParseTest {
public:
isc::asiolink::IOService io_service_;
IOServicePtr io_service_;
D2QueueMgrPtr queue_mgr_;
D2CfgMgrPtr cfg_mgr_;
//D2UpdateMgrPtr update_mgr_;
......@@ -77,6 +77,7 @@ public:
size_t canned_count_;
D2UpdateMgrTest() {
io_service_.reset(new isc::asiolink::IOService());
queue_mgr_.reset(new D2QueueMgr(io_service_));
cfg_mgr_.reset(new D2CfgMgr());
update_mgr_.reset(new D2UpdateMgrWrapper(queue_mgr_, cfg_mgr_,
......@@ -162,7 +163,7 @@ public:
/// 4. Default construction works and max transactions is defaulted properly
/// 5. Construction with custom max transactions works properly
TEST(D2UpdateMgr, construction) {
isc::asiolink::IOService io_service;
IOServicePtr io_service(new isc::asiolink::IOService());
D2QueueMgrPtr queue_mgr;
D2CfgMgrPtr cfg_mgr;
D2UpdateMgrPtr update_mgr;
......
......@@ -15,11 +15,11 @@
#ifndef D_TEST_STUBS_H
#define D_TEST_STUBS_H
#include <asiolink/asiolink.h>
#include <cc/data.h>
#include <cc/session.h>
#include <config/ccsession.h>
#include <d2/d2_asio.h>
#include <d2/d_controller.h>
#include <d2/d_cfg_mgr.h>
......
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