diff --git a/CMakeLists.txt b/CMakeLists.txt index c34202f2c..4d8977bfd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,12 @@ ENDIF() # use shared libraries for protobuf ADD_DEFINITIONS(-DPROTOBUF_USE_DLLS) +if(UNIX) + add_definitions(-D_LINUX) +elseif(WIN32) + add_definitions(-DWIN32) +endif() + #### expose depends #### # find and make available libz diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 0521184ce..cef56d4da 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -10,6 +10,7 @@ IF(UNIX) ENDIF() include_directories (include) +include_directories (proto) SET(PERL_EXECUTABLE "perl" CACHE FILEPATH "This is the perl executable to run in the codegen step. Tweak it if you need to run a specific one.") @@ -41,6 +42,8 @@ include/Types.h include/VersionInfo.h include/VersionInfoFactory.h include/Virtual.h +include/RemoteClient.h +include/RemoteServer.h ) SET(MAIN_HEADERS_WINDOWS @@ -58,6 +61,8 @@ PluginManager.cpp TileTypes.cpp VersionInfoFactory.cpp Virtual.cpp +RemoteClient.cpp +RemoteServer.cpp ) SET(MAIN_SOURCES_WINDOWS @@ -158,7 +163,7 @@ STRING(REPLACE ".proto" ".pb.cc" PROJECT_PROTO_SRCS ${PROJECT_PROTOS}) STRING(REPLACE ".proto" ".pb.h" PROJECT_PROTO_HDRS ${PROJECT_PROTOS}) LIST(APPEND PROJECT_HEADERS ${PROJECT_PROTO_HDRS}) -LIST(APPEND PROJECT_HEADERS ${PROJECT_PROTO_SRCS}) +LIST(APPEND PROJECT_SOURCES ${PROJECT_PROTO_SRCS}) ADD_CUSTOM_COMMAND( OUTPUT ${PROJECT_PROTO_SRCS} ${PROJECT_PROTO_HDRS} @@ -211,6 +216,11 @@ ENDIF() ADD_LIBRARY(dfhack SHARED ${PROJECT_SOURCES}) ADD_DEPENDENCIES(dfhack generate_headers) +ADD_EXECUTABLE(dfhack-run dfhack-run.cpp + RemoteClient.cpp ColorText.cpp MiscUtils.cpp + ${PROJECT_PROTO_SRCS}) +ADD_DEPENDENCIES(dfhack-run dfhack) + IF(BUILD_EGGY) SET_TARGET_PROPERTIES(dfhack PROPERTIES OUTPUT_NAME "egg" ) else() @@ -221,8 +231,10 @@ endif() IF(WIN32) SET_TARGET_PROPERTIES(dfhack PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) + SET_TARGET_PROPERTIES(dfhack-run PROPERTIES COMPILE_FLAGS "/FI\"Export.h\"" ) ELSE() SET_TARGET_PROPERTIES(dfhack PROPERTIES COMPILE_FLAGS "-include Export.h" ) + SET_TARGET_PROPERTIES(dfhack-run PROPERTIES COMPILE_FLAGS "-include Export.h" ) ENDIF() #effectively disables debug builds... @@ -231,6 +243,8 @@ SET_TARGET_PROPERTIES(dfhack PROPERTIES DEBUG_POSTFIX "-debug" ) TARGET_LINK_LIBRARIES(dfhack protobuf-lite clsocket ${PROJECT_LIBS}) SET_TARGET_PROPERTIES(dfhack PROPERTIES LINK_INTERFACE_LIBRARIES "") +TARGET_LINK_LIBRARIES(dfhack-run protobuf-lite clsocket) + IF(UNIX) # On linux, copy our version of the df launch script which sets LD_PRELOAD install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack @@ -253,6 +267,7 @@ else() LIBRARY DESTINATION ${DFHACK_EGGY_DESTINATION} RUNTIME DESTINATION ${DFHACK_EGGY_DESTINATION}) endif() + #install the offset file install(FILES xml/symbols.xml DESTINATION ${DFHACK_DATA_DESTINATION}) #linux: share/dfhack @@ -260,6 +275,10 @@ install(FILES xml/symbols.xml install(FILES ../dfhack.init-example DESTINATION ${DFHACK_BINARY_DESTINATION}) +install(TARGETS dfhack-run + LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION} + RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION}) + # Unused for so long that it's not even relevant now... if(BUILD_DEVEL) if(WIN32) diff --git a/library/ColorText.cpp b/library/ColorText.cpp index fa5d57e0b..c399504be 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -143,6 +143,16 @@ void color_ostream::reset_color(void) color(COLOR_RESET); } +void color_ostream_wrapper::add_text(color_value, const std::string &text) +{ + out << text; +} + +void color_ostream_wrapper::flush_proxy() +{ + out << std::flush; +} + void buffered_color_ostream::add_text(color_value color, const std::string &text) { if (text.empty()) @@ -165,23 +175,20 @@ void buffered_color_ostream::add_text(color_value color, const std::string &text void color_ostream_proxy::flush_proxy() { - if (!buffer.empty()) + if (buffer.empty()) + return; + + if (target) { target->begin_batch(); for (auto it = buffer.begin(); it != buffer.end(); ++it) target->add_text(it->first, it->second); - buffer.clear(); - target->end_batch(); } -} -color_ostream_proxy::color_ostream_proxy(color_ostream &target) - : target(&target) -{ - // + buffer.clear(); } color_ostream_proxy::~color_ostream_proxy() diff --git a/library/Core.cpp b/library/Core.cpp index 9a005d332..0765f4cd1 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -48,6 +48,7 @@ using namespace std; #include "modules/World.h" #include "modules/Graphic.h" #include "modules/Windows.h" +#include "RemoteServer.h" using namespace DFHack; #include "df/ui.h" @@ -570,6 +571,7 @@ Core::Core() last_world_data_ptr = NULL; top_viewscreen = NULL; screen_window = NULL; + server = NULL; }; void Core::fatal (std::string output, bool deactivate) @@ -678,6 +680,12 @@ bool Core::Init() screen_window = new Windows::top_level_window(); screen_window->addChild(new Windows::dfhack_dummy(5,10)); started = true; + + cerr << "Starting the TCP listener.\n"; + server = new ServerMain(); + if (!server->listen(RemoteClient::GetDefaultPort())) + cerr << "TCP listen failed.\n"; + cerr << "DFHack is running.\n"; return true; } diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index aa141313b..dff8af0a0 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -24,7 +24,6 @@ distribution. #include "Internal.h" #include "Export.h" -#include "Core.h" #include "MiscUtils.h" #ifndef LINUX_BUILD diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp new file mode 100644 index 000000000..a8c804664 --- /dev/null +++ b/library/RemoteClient.cpp @@ -0,0 +1,359 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2011 Petr Mrázek + +A thread-safe logging console with a line editor for windows. + +Based on linenoise win32 port, +copyright 2010, Jon Griffiths . +All rights reserved. +Based on linenoise, copyright 2010, Salvatore Sanfilippo . +The original linenoise can be found at: http://github.com/antirez/linenoise + +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 Redis 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. +*/ + + +#include +#include +#include +#include +#include +#include +#include + +#include "RemoteClient.h" +#include "MiscUtils.h" + +#include +#include +#include + +#include + +using namespace DFHack; + +#include "tinythread.h" +using namespace tthread; + +using dfproto::CoreTextNotification; + +using google::protobuf::MessageLite; + +const char RPCHandshakeHeader::REQUEST_MAGIC[9] = "DFHack?\n"; +const char RPCHandshakeHeader::RESPONSE_MAGIC[9] = "DFHack!\n"; + +void color_ostream_proxy::decode(CoreTextNotification *data) +{ + flush_proxy(); + + int cnt = data->fragments_size(); + if (cnt > 0) { + target->begin_batch(); + + for (int i = 0; i < cnt; i++) + { + auto &frag = data->fragments(i); + + color_value color = frag.has_color() ? color_value(frag.color()) : COLOR_RESET; + target->add_text(color, frag.text()); + } + + target->end_batch(); + } +} + +RemoteClient::RemoteClient() +{ + active = false; +} + +RemoteClient::~RemoteClient() +{ + disconnect(); +} + +bool DFHack::readFullBuffer(CSimpleSocket &socket, void *buf, int size) +{ + if (!socket.IsSocketValid()) + return false; + + char *ptr = (char*)buf; + while (size > 0) { + int cnt = socket.Receive(size); + if (cnt <= 0) + return false; + memcpy(ptr, socket.GetData(), cnt); + ptr += cnt; + size -= cnt; + } + + return true; +} + +int RemoteClient::GetDefaultPort() +{ + const char *port = getenv("DFHACK_PORT"); + if (!port) port = "0"; + + int portval = atoi(port); + if (portval <= 0) + return 5000; + else + return portval; +} + +bool RemoteClient::connect(int port) +{ + assert(!active); + + if (port <= 0) + port = GetDefaultPort(); + + if (!socket.Initialize()) + { + std::cerr << "Socket init failed." << endl; + return false; + } + + if (!socket.Open((const uint8 *)"localhost", port)) + { + std::cerr << "Could not connect to localhost:" << port << endl; + return false; + } + + active = true; + + RPCHandshakeHeader header; + memcpy(header.magic, RPCHandshakeHeader::REQUEST_MAGIC, sizeof(header.magic)); + header.version = 1; + + if (socket.Send((uint8*)&header, sizeof(header)) != sizeof(header)) + { + std::cerr << "Could not send header." << endl; + socket.Close(); + return active = false; + } + + if (!readFullBuffer(socket, &header, sizeof(header))) + { + std::cerr << "Could not read header." << endl; + socket.Close(); + return active = false; + } + + if (memcmp(header.magic, RPCHandshakeHeader::RESPONSE_MAGIC, sizeof(header.magic)) || + header.version != 1) + { + std::cerr << "Invalid handshake response." << endl; + socket.Close(); + return active = false; + } + + bind_call.name = "BindMethod"; + bind_call.p_client = this; + bind_call.id = 0; + + return true; +} + +void RemoteClient::disconnect() +{ + if (active && socket.IsSocketValid()) + { + RPCMessageHeader header; + header.id = RPC_REQUEST_QUIT; + header.size = 0; + if (socket.Send((uint8_t*)&header, sizeof(header)) != sizeof(header)) + std::cerr << "Could not send the disconnect message." << endl; + } + + socket.Close(); +} + +bool RemoteClient::bind(color_ostream &out, RemoteFunctionBase *function, + const std::string &name, const std::string &proto) +{ + if (!active || !socket.IsSocketValid()) + return false; + + bind_call.reset(); + + { + auto in = bind_call.in(); + + in->set_method(name); + if (!proto.empty()) + in->set_plugin(proto); + in->set_input_msg(function->p_in_template->GetTypeName()); + in->set_output_msg(function->p_out_template->GetTypeName()); + } + + if (bind_call.execute(out) != CR_OK) + return false; + + function->p_client = this; + function->id = bind_call.out()->assigned_id(); + + return true; +} + +void RPCFunctionBase::reset(bool free) +{ + if (free) + { + delete p_in; + delete p_out; + p_in = p_out = NULL; + } + else + { + if (p_in) + p_in->Clear(); + if (p_out) + p_out->Clear(); + } +} + +bool RemoteFunctionBase::bind(color_ostream &out, RemoteClient *client, + const std::string &name, const std::string &proto) +{ + if (p_client == client) + return true; + + if (p_client) + { + out.printerr("Function already bound to %s::%s\n", + this->proto.c_str(), this->name.c_str()); + return false; + } + + this->name = name; + this->proto = proto; + + return client->bind(out, this, name, proto); +} + +bool DFHack::sendRemoteMessage(CSimpleSocket &socket, int16_t id, const MessageLite *msg) +{ + int size = msg->ByteSize(); + int fullsz = size + sizeof(RPCMessageHeader); + + std::auto_ptr data(new uint8_t[fullsz]); + RPCMessageHeader *hdr = (RPCMessageHeader*)data.get(); + + hdr->id = id; + hdr->size = size; + + if (!msg->SerializeToArray(data.get() + sizeof(RPCMessageHeader), size)) + return false; + + return (socket.Send(data.get(), fullsz) == fullsz); +} + +command_result RemoteFunctionBase::execute(color_ostream &out, + const message_type *input, message_type *output) +{ + if (!p_client) + { + out.printerr("Calling an unbound RPC function.\n"); + return CR_NOT_IMPLEMENTED; + } + + if (!p_client->socket.IsSocketValid()) + { + out.printerr("In call to %s::%s: invalid socket.\n", + this->proto.c_str(), this->name.c_str()); + return CR_FAILURE; + } + + if (!sendRemoteMessage(p_client->socket, id, input)) + { + out.printerr("In call to %s::%s: I/O error in send.\n", + this->proto.c_str(), this->name.c_str()); + return CR_FAILURE; + } + + color_ostream_proxy text_decoder(out); + CoreTextNotification text_data; + + output->Clear(); + + for (;;) { + RPCMessageHeader header; + + if (!readFullBuffer(p_client->socket, &header, sizeof(header))) + { + out.printerr("In call to %s::%s: I/O error in receive header.\n", + this->proto.c_str(), this->name.c_str()); + return CR_FAILURE; + } + + //out.print("Received %d:%d\n", header.id, header.size); + + if (header.id == RPC_REPLY_FAIL) + return header.size == CR_OK ? CR_FAILURE : command_result(header.size); + + if (header.size < 0 || header.size > 2*1048576) + { + out.printerr("In call to %s::%s: invalid received size %d.\n", + this->proto.c_str(), this->name.c_str(), header.size); + return CR_FAILURE; + } + + std::auto_ptr buf(new uint8_t[header.size]); + + if (!readFullBuffer(p_client->socket, buf.get(), header.size)) + { + out.printerr("In call to %s::%s: I/O error in receive %d bytes of data.\n", + this->proto.c_str(), this->name.c_str(), header.size); + return CR_FAILURE; + } + + switch (header.id) { + case RPC_REPLY_RESULT: + if (!output->ParseFromArray(buf.get(), header.size)) + { + out.printerr("In call to %s::%s: error parsing received result.\n", + this->proto.c_str(), this->name.c_str()); + return CR_FAILURE; + } + + return CR_OK; + + case RPC_REPLY_TEXT: + text_data.Clear(); + if (text_data.ParseFromArray(buf.get(), header.size)) + text_decoder.decode(&text_data); + else + out.printerr("In call to %s::%s: received invalid text data.\n", + this->proto.c_str(), this->name.c_str()); + break; + + default: + break; + } + } +} diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp new file mode 100644 index 000000000..5b0825700 --- /dev/null +++ b/library/RemoteServer.cpp @@ -0,0 +1,359 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2011 Petr Mrázek + +A thread-safe logging console with a line editor for windows. + +Based on linenoise win32 port, +copyright 2010, Jon Griffiths . +All rights reserved. +Based on linenoise, copyright 2010, Salvatore Sanfilippo . +The original linenoise can be found at: http://github.com/antirez/linenoise + +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 Redis 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. +*/ + + +#include +#include +#include +#include +#include +#include +#include + +#include "RemoteServer.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include +#include +#include + +#include + +using namespace DFHack; + +#include "tinythread.h" +using namespace tthread; + +using dfproto::CoreTextNotification; +using dfproto::CoreTextFragment; +using google::protobuf::MessageLite; + +CoreService::CoreService() { + // This must be the first method, so that it gets id 0 + addMethod("BindMethod", &CoreService::BindMethod); + + addMethod("RunCommand", &CoreService::RunCommand); +} + +command_result CoreService::BindMethod(color_ostream &stream, + const dfproto::CoreBindRequest *in, + dfproto::CoreBindReply *out) +{ + ServerFunctionBase *fn = connection()->findFunction(in->plugin(), in->method()); + + if (!fn) + { + stream.printerr("RPC method not found: %s::%s\n", + in->plugin().c_str(), in->method().c_str()); + return CR_FAILURE; + } + + if (fn->p_in_template->GetTypeName() != in->input_msg() || + fn->p_out_template->GetTypeName() != in->output_msg()) + { + stream.printerr("Requested wrong signature for RPC method: %s::%s\n", + in->plugin().c_str(), in->method().c_str()); + return CR_FAILURE; + } + + out->set_assigned_id(fn->getId()); + return CR_OK; +} + +command_result CoreService::RunCommand(color_ostream &stream, + const dfproto::CoreRunCommandRequest *in, + CoreVoidReply*) +{ + std::string cmd = in->command(); + std::vector args; + for (int i = 0; i < in->arguments_size(); i++) + args.push_back(in->arguments(i)); + + return Core::getInstance().plug_mgr->InvokeCommand(stream, cmd, args, false); +} + +RPCService::RPCService() +{ + owner = NULL; + holder = NULL; +} + +RPCService::~RPCService() +{ + for (size_t i = 0; i < functions.size(); i++) + delete functions[i]; +} + +ServerFunctionBase *RPCService::getFunction(const std::string &name) +{ + assert(owner); + return lookup[name]; +} + +void RPCService::finalize(ServerConnection *owner, std::vector *ftable) +{ + this->owner = owner; + + for (size_t i = 0; i < functions.size(); i++) + { + auto fn = functions[i]; + + fn->id = (int16_t)ftable->size(); + ftable->push_back(fn); + + lookup[fn->name] = fn; + } +} + +ServerConnection::ServerConnection(CActiveSocket *socket) + : socket(socket), stream(this) +{ + in_error = false; + + core_service = new CoreService(); + core_service->finalize(this, &functions); + + thread = new tthread::thread(threadFn, (void*)this); +} + +ServerConnection::~ServerConnection() +{ + in_error = true; + socket->Close(); + delete socket; + + for (auto it = plugin_services.begin(); it != plugin_services.end(); ++it) + delete it->second; + + delete core_service; +} + +ServerFunctionBase *ServerConnection::findFunction(const std::string &plugin, const std::string &name) +{ + if (plugin.empty()) + return core_service->getFunction(name); + else + return NULL; // todo: add plugin api support +} + +void ServerConnection::connection_ostream::flush_proxy() +{ + if (owner->in_error) + { + buffer.clear(); + return; + } + + if (buffer.empty()) + return; + + CoreTextNotification msg; + + for (auto it = buffer.begin(); it != buffer.end(); ++it) + { + auto frag = msg.add_fragments(); + frag->set_text(it->second); + if (it->first >= 0) + frag->set_color(CoreTextFragment::Color(it->first)); + } + + buffer.clear(); + + if (!sendRemoteMessage(*owner->socket, RPC_REPLY_TEXT, &msg)) + { + owner->in_error = true; + Core::printerr("Error writing text into client socket.\n"); + } +} + +void ServerConnection::threadFn(void *arg) +{ + ServerConnection *me = (ServerConnection*)arg; + color_ostream_proxy out(Core::getInstance().getConsole()); + + /* Handshake */ + + { + RPCHandshakeHeader header; + + if (!readFullBuffer(*me->socket, &header, sizeof(header))) + { + out << "In RPC server: could not read handshake header." << endl; + delete me; + return; + } + + if (memcmp(header.magic, RPCHandshakeHeader::REQUEST_MAGIC, sizeof(header.magic)) || + header.version != 1) + { + out << "In RPC server: invalid handshake header." << endl; + delete me; + return; + } + + memcpy(header.magic, RPCHandshakeHeader::RESPONSE_MAGIC, sizeof(header.magic)); + header.version = 1; + + if (me->socket->Send((uint8*)&header, sizeof(header)) != sizeof(header)) + { + out << "In RPC server: could not send handshake response." << endl; + delete me; + return; + } + } + + /* Processing */ + + std::cerr << "Client connection established." << endl; + + while (!me->in_error) { + RPCMessageHeader header; + + if (!readFullBuffer(*me->socket, &header, sizeof(header))) + { + out.printerr("In RPC server: I/O error in receive header.\n"); + break; + } + + if (header.id == RPC_REQUEST_QUIT) + break; + + if (header.size < 0 || header.size > 2*1048576) + { + out.printerr("In RPC server: invalid received size %d.\n", header.size); + break; + } + + std::auto_ptr buf(new uint8_t[header.size]); + + if (!readFullBuffer(*me->socket, buf.get(), header.size)) + { + out.printerr("In RPC server: I/O error in receive %d bytes of data.\n", header.size); + break; + } + + //out.print("Handling %d:%d\n", header.id, header.size); + + ServerFunctionBase *fn = vector_get(me->functions, header.id); + MessageLite *reply = NULL; + command_result res = CR_FAILURE; + + if (!fn) + { + me->stream.printerr("RPC call of invalid id %d\n", header.id); + } + else + { + if (!fn->in()->ParseFromArray(buf.get(), header.size)) + { + me->stream.printerr("In call to %s: could not decode input args.\n", fn->name); + } + else + { + reply = fn->out(); + res = fn->execute(me->stream); + } + } + + if (me->in_error) + break; + + me->stream.flush(); + + //out.print("Answer %d:%d\n", res, reply); + + if (res == CR_OK && reply) + { + if (!sendRemoteMessage(*me->socket, RPC_REPLY_RESULT, reply)) + { + out.printerr("In RPC server: I/O error in send result.\n"); + break; + } + } + else + { + header.id = RPC_REPLY_FAIL; + header.size = res; + + if (me->socket->Send((uint8_t*)&header, sizeof(header)) != sizeof(header)) + { + out.printerr("In RPC server: I/O error in send failure code.\n"); + break; + } + } + } + + std::cerr << "Shutting down client connection." << endl; + + delete me; +} + +ServerMain::ServerMain() +{ + thread = NULL; +} + +ServerMain::~ServerMain() +{ + socket.Close(); +} + +bool ServerMain::listen(int port) +{ + if (thread) + return true; + + socket.Initialize(); + + if (!socket.Listen((const uint8 *)"127.0.0.1", port)) + return false; + + thread = new tthread::thread(threadFn, this); + return true; +} + +void ServerMain::threadFn(void *arg) +{ + ServerMain *me = (ServerMain*)arg; + CActiveSocket *client; + + while ((client = me->socket.Accept()) != NULL) + { + new ServerConnection(client); + } +} diff --git a/library/dfhack-run.cpp b/library/dfhack-run.cpp new file mode 100644 index 000000000..941ce8d0e --- /dev/null +++ b/library/dfhack-run.cpp @@ -0,0 +1,90 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2011 Petr Mrázek + +A thread-safe logging console with a line editor for windows. + +Based on linenoise win32 port, +copyright 2010, Jon Griffiths . +All rights reserved. +Based on linenoise, copyright 2010, Salvatore Sanfilippo . +The original linenoise can be found at: http://github.com/antirez/linenoise + +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 Redis 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. +*/ + + +#include +#include +#include +#include +#include +#include +#include + +#include "RemoteClient.h" + +#include +#include +#include + +#include + +using namespace DFHack; +using namespace dfproto; +using std::cout; + +int main (int argc, char *argv[]) +{ + color_ostream_wrapper out(cout); + + if (argc <= 1) + { + fprintf(stderr, "Usage: dfhack-run [args...]\n"); + return 2; + } + + // Connect to DFHack + RemoteClient client; + if (!client.connect()) + return 2; + + // Bind to RunCommand + RemoteFunction command; + + if (!command.bind(out, &client, "RunCommand")) + return 2; + + // Execute it + command.in()->set_command(argv[1]); + for (int i = 2; i < argc; i++) + command.in()->add_arguments(argv[i]); + + if (command.execute(out) != CR_OK) + return 1; + + out.flush(); + return 0; +} diff --git a/library/include/ColorText.h b/library/include/ColorText.h index cdeaa3948..651f5831e 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -34,6 +34,11 @@ distribution. #include #include +namespace dfproto +{ + class CoreTextNotification; +} + namespace DFHack { class DFHACK_EXPORT color_ostream : public std::ostream @@ -112,6 +117,25 @@ namespace DFHack void reset_color(void); virtual bool is_console() { return false; } + virtual color_ostream *proxy_target() { return NULL; } + }; + + inline color_ostream &operator << (color_ostream &out, color_ostream::color_value clr) + { + out.color(clr); + return out; + } + + class DFHACK_EXPORT color_ostream_wrapper : public color_ostream + { + std::ostream &out; + + protected: + virtual void add_text(color_value color, const std::string &text); + virtual void flush_proxy(); + + public: + color_ostream_wrapper(std::ostream &os) : out(os) {} }; class DFHACK_EXPORT buffered_color_ostream : public color_ostream @@ -139,7 +163,11 @@ namespace DFHack virtual void flush_proxy(); public: - color_ostream_proxy(color_ostream &target); + color_ostream_proxy(color_ostream &target) : target(&target) {} ~color_ostream_proxy(); + + virtual color_ostream *proxy_target() { return target; } + + void decode(dfproto::CoreTextNotification *data); }; } diff --git a/library/include/Core.h b/library/include/Core.h index 321939c5d..1705bcc16 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -60,6 +60,7 @@ namespace DFHack class VersionInfoFactory; class PluginManager; class Core; + class ServerMain; namespace Windows { class df_window; @@ -195,6 +196,9 @@ namespace DFHack tthread::mutex * misc_data_mutex; std::map misc_data_map; + + friend class CoreService; + ServerMain *server; }; class CoreSuspender { diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h index 8e8f7760d..e617814cf 100644 --- a/library/include/PluginManager.h +++ b/library/include/PluginManager.h @@ -30,6 +30,9 @@ distribution. #include #include #include + +#include "RemoteClient.h" + struct DFLibrary; namespace tthread { @@ -46,14 +49,6 @@ namespace DFHack class PluginManager; class virtual_identity; - enum command_result - { - CR_WOULD_BREAK = -2, - CR_NOT_IMPLEMENTED = -1, - CR_FAILURE = 0, - CR_OK = 1, - CR_WRONG_USAGE = 2 - }; enum state_change_event { SC_GAME_LOADED, diff --git a/library/include/RemoteClient.h b/library/include/RemoteClient.h new file mode 100644 index 000000000..453f24794 --- /dev/null +++ b/library/include/RemoteClient.h @@ -0,0 +1,169 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#pragma once +#include "Pragma.h" +#include "Export.h" +#include "ColorText.h" + +#include "ActiveSocket.h" + +#include "CoreProtocol.pb.h" + +namespace DFHack +{ + using dfproto::CoreVoidReply; + + enum command_result + { + CR_WOULD_BREAK = -2, + CR_NOT_IMPLEMENTED = -1, + CR_OK = 0, + CR_FAILURE = 1, + CR_WRONG_USAGE = 2 + }; + + enum DFHackReplyCode : int16_t { + RPC_REPLY_RESULT = -1, + RPC_REPLY_FAIL = -2, + RPC_REPLY_TEXT = -3, + RPC_REQUEST_QUIT = -4 + }; + + struct RPCHandshakeHeader { + char magic[8]; + int version; + + static const char REQUEST_MAGIC[9]; + static const char RESPONSE_MAGIC[9]; + }; + + struct RPCMessageHeader { + int16_t id; + int32_t size; + }; + + class DFHACK_EXPORT RemoteClient; + + class DFHACK_EXPORT RPCFunctionBase { + public: + typedef ::google::protobuf::MessageLite message_type; + + const message_type *const p_in_template; + const message_type *const p_out_template; + + message_type *make_in() const { + return p_in_template->New(); + } + + message_type *in() { + if (!p_in) p_in = make_in(); + return p_in; + } + + message_type *make_out() const { + return p_out_template->New(); + } + + message_type *out() { + if (!p_out) p_out = make_out(); + return p_out; + } + + void reset(bool free = false); + + protected: + RPCFunctionBase(const message_type *in, const message_type *out) + : p_in_template(in), p_out_template(out), p_in(NULL), p_out(NULL) + {} + ~RPCFunctionBase() { delete p_in; delete p_out; } + + message_type *p_in, *p_out; + }; + + class DFHACK_EXPORT RemoteFunctionBase : public RPCFunctionBase { + public: + bool bind(color_ostream &out, + RemoteClient *client, const std::string &name, + const std::string &proto = std::string()); + bool isValid() { return (p_client != NULL); } + + protected: + friend class RemoteClient; + + RemoteFunctionBase(const message_type *in, const message_type *out) + : RPCFunctionBase(in, out), p_client(NULL), id(-1) + {} + + command_result execute(color_ostream &out, const message_type *input, message_type *output); + + std::string name, proto; + RemoteClient *p_client; + int16_t id; + }; + + template + class RemoteFunction : public RemoteFunctionBase { + public: + In *make_in() const { return static_cast(RemoteFunctionBase::make_in()); } + In *in() { return static_cast(RemoteFunctionBase::in()); } + Out *make_out() const { return static_cast(RemoteFunctionBase::make_out()); } + Out *out() { return static_cast(RemoteFunctionBase::out()); } + + RemoteFunction() : RemoteFunctionBase(&In::default_instance(), &Out::default_instance()) {} + + command_result execute(color_ostream &out) { + return RemoteFunctionBase::execute(out, this->in(), this->out()); + } + command_result operator() (color_ostream &out, const In *input, Out *output) { + return RemoteFunctionBase::execute(out, input, output); + } + }; + + bool readFullBuffer(CSimpleSocket &socket, void *buf, int size); + bool sendRemoteMessage(CSimpleSocket &socket, int16_t id, const ::google::protobuf::MessageLite *msg); + + class DFHACK_EXPORT RemoteClient + { + friend class RemoteFunctionBase; + + bool bind(color_ostream &out, RemoteFunctionBase *function, + const std::string &name, const std::string &proto); + + public: + RemoteClient(); + ~RemoteClient(); + + static int GetDefaultPort(); + + bool connect(int port = -1); + void disconnect(); + + private: + bool active; + CActiveSocket socket; + + RemoteFunction bind_call; + }; +} diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h new file mode 100644 index 000000000..a3cc6c044 --- /dev/null +++ b/library/include/RemoteServer.h @@ -0,0 +1,189 @@ +/* +https://github.com/peterix/dfhack +Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#pragma once +#include "Pragma.h" +#include "Export.h" +#include "RemoteClient.h" +#include "Core.h" + +#include "PassiveSocket.h" + +namespace DFHack +{ + class DFHACK_EXPORT ServerConnection; + class DFHACK_EXPORT RPCService; + + class DFHACK_EXPORT ServerFunctionBase : public RPCFunctionBase { + public: + const char *const name; + + virtual command_result execute(color_ostream &stream) = 0; + + int16_t getId() { return id; } + + protected: + friend class RPCService; + + ServerFunctionBase(const message_type *in, const message_type *out, RPCService *owner, const char *name) + : RPCFunctionBase(in, out), name(name), owner(owner), id(-1) + {} + + RPCService *owner; + int16_t id; + }; + + template + class ServerFunction : public ServerFunctionBase { + public: + typedef command_result (*function_type)(color_ostream &out, const In *input, Out *output); + + In *in() { return static_cast(RPCFunctionBase::in()); } + Out *out() { return static_cast(RPCFunctionBase::out()); } + + ServerFunction(RPCService *owner, const char *name, function_type fptr) + : ServerFunctionBase(&In::default_instance(), &Out::default_instance(), owner, name), + fptr(fptr) {} + + virtual command_result execute(color_ostream &stream) { return fptr(stream, in(), out()); } + + private: + function_type fptr; + }; + + template + class ServerMethod : public ServerFunctionBase { + public: + typedef command_result (Svc::*function_type)(color_ostream &out, const In *input, Out *output); + + In *in() { return static_cast(RPCFunctionBase::in()); } + Out *out() { return static_cast(RPCFunctionBase::out()); } + + ServerMethod(RPCService *owner, const char *name, function_type fptr) + : ServerFunctionBase(&In::default_instance(), &Out::default_instance(), owner, name), + fptr(fptr) {} + + virtual command_result execute(color_ostream &stream) { + return (static_cast(owner)->*fptr)(stream, in(), out()); + } + + private: + function_type fptr; + }; + + class Plugin; + + class DFHACK_EXPORT RPCService { + friend class ServerConnection; + + std::vector functions; + std::map lookup; + ServerConnection *owner; + + Plugin *holder; + + void finalize(ServerConnection *owner, std::vector *ftable); + + public: + RPCService(); + virtual ~RPCService(); + + ServerFunctionBase *getFunction(const std::string &name); + + template + void addFunction( + const char *name, + command_result (*fptr)(color_ostream &out, const In *input, Out *output) + ) { + assert(!owner); + functions.push_back(new ServerFunction(this, name, fptr)); + } + + protected: + ServerConnection *connection() { return owner; } + + template + void addMethod( + const char *name, + command_result (Svc::*fptr)(color_ostream &out, const In *input, Out *output) + ) { + assert(!owner); + functions.push_back(new ServerMethod(this, name, fptr)); + } + }; + + class CoreService : public RPCService { + public: + CoreService(); + + command_result BindMethod(color_ostream &stream, + const dfproto::CoreBindRequest *in, + dfproto::CoreBindReply *out); + command_result RunCommand(color_ostream &stream, + const dfproto::CoreRunCommandRequest *in, + CoreVoidReply*); + }; + + class DFHACK_EXPORT ServerConnection { + class connection_ostream : public buffered_color_ostream { + ServerConnection *owner; + + protected: + virtual void flush_proxy(); + + public: + connection_ostream(ServerConnection *owner) : owner(owner) {} + }; + + bool in_error; + CActiveSocket *socket; + connection_ostream stream; + + std::vector functions; + + CoreService *core_service; + std::map plugin_services; + + tthread::thread *thread; + static void threadFn(void *); + + public: + ServerConnection(CActiveSocket *socket); + ~ServerConnection(); + + ServerFunctionBase *findFunction(const std::string &plugin, const std::string &name); + }; + + class DFHACK_EXPORT ServerMain { + CPassiveSocket socket; + + tthread::thread *thread; + static void threadFn(void *); + public: + ServerMain(); + ~ServerMain(); + + bool listen(int port); + }; +} diff --git a/library/proto/CoreProtocol.proto b/library/proto/CoreProtocol.proto index 33ec0f4a0..eec5ef7f2 100644 --- a/library/proto/CoreProtocol.proto +++ b/library/proto/CoreProtocol.proto @@ -34,26 +34,28 @@ message CoreErrorNotification { enum ErrorCode { CR_WOULD_BREAK = -2; CR_NOT_IMPLEMENTED = -1; - CR_FAILURE = 0; - CR_OK = 1; + CR_OK = 0; + CR_FAILURE = 1; CR_WRONG_USAGE = 2; }; required ErrorCode code = 1; } +message CoreVoidReply {} + message CoreBindRequest { required string method = 1; - optional string plugin = 2; - optional int32 min_version = 3; + required string input_msg = 2; + required string output_msg = 3; + optional string plugin = 4; } message CoreBindReply { required int32 assigned_id = 1; - required int32 version = 2; } -message CoreRunStringRequest { +message CoreRunCommandRequest { required string command = 1; repeated string arguments = 2; } diff --git a/plugins/Plugins.cmake b/plugins/Plugins.cmake index 221e57ea9..8d68827a4 100644 --- a/plugins/Plugins.cmake +++ b/plugins/Plugins.cmake @@ -6,6 +6,7 @@ IF(UNIX) ENDIF() include_directories("${dfhack_SOURCE_DIR}/library/include") +include_directories("${dfhack_SOURCE_DIR}/library/proto") include_directories("${dfhack_SOURCE_DIR}/library/depends/xgetopt") MACRO(CAR var)