diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d8977bfd..d39d940e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ set(DF_VERSION_MINOR "34") set(DF_VERSION_PATCH "05") set(DF_VERSION "${DF_VERSION_MAJOR}.${DF_VERSION_MINOR}.${DF_VERSION_PATCH}") -set(DFHACK_RELEASE "1d") +set(DFHACK_RELEASE "1e") set(DFHACK_VERSION "${DF_VERSION_MAJOR}.${DF_VERSION_MINOR}.${DF_VERSION_PATCH}-r${DFHACK_RELEASE}") add_definitions(-DDFHACK_VERSION="${DFHACK_VERSION}") diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index cef56d4da..0bda587a7 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -159,8 +159,8 @@ ENDIF() # Protobuf FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto) -STRING(REPLACE ".proto" ".pb.cc" PROJECT_PROTO_SRCS ${PROJECT_PROTOS}) -STRING(REPLACE ".proto" ".pb.h" PROJECT_PROTO_HDRS ${PROJECT_PROTOS}) +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_SOURCES ${PROJECT_PROTO_SRCS}) @@ -249,6 +249,8 @@ IF(UNIX) # On linux, copy our version of the df launch script which sets LD_PRELOAD install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack DESTINATION .) + install(PROGRAMS ${dfhack_SOURCE_DIR}/package/linux/dfhack-run + DESTINATION .) ELSE() if(NOT BUILD_EGGY) # On windows, copy the renamed SDL so DF can still run. diff --git a/library/ColorText.cpp b/library/ColorText.cpp index c399504be..5d6e1d82a 100644 --- a/library/ColorText.cpp +++ b/library/ColorText.cpp @@ -56,6 +56,8 @@ using namespace DFHack; #include "tinythread.h" using namespace tthread; +bool color_ostream::log_errors_to_stderr = false; + void color_ostream::flush_buffer(bool flush) { auto buffer = buf(); @@ -122,7 +124,8 @@ void color_ostream::vprinterr(const char *format, va_list args) { color_value save = cur_color; - fprintf(stderr, format, args); + if (log_errors_to_stderr) + fprintf(stderr, format, args); color(COLOR_LIGHTRED); vprint(format, args); diff --git a/library/Core.cpp b/library/Core.cpp index 0765f4cd1..6f04f5edb 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -572,6 +572,8 @@ Core::Core() top_viewscreen = NULL; screen_window = NULL; server = NULL; + + color_ostream::log_errors_to_stderr = true; }; void Core::fatal (std::string output, bool deactivate) diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp index a8c804664..6f284dd15 100644 --- a/library/RemoteClient.cpp +++ b/library/RemoteClient.cpp @@ -176,6 +176,10 @@ bool RemoteClient::connect(int port) bind_call.p_client = this; bind_call.id = 0; + runcmd_call.name = "RunCommand"; + runcmd_call.p_client = this; + runcmd_call.id = 1; + return true; } @@ -220,6 +224,24 @@ bool RemoteClient::bind(color_ostream &out, RemoteFunctionBase *function, return true; } +command_result RemoteClient::run_command(color_ostream &out, const std::string &cmd, + const std::vector &args) +{ + if (!active || !socket.IsSocketValid()) + { + out.printerr("In RunCommand: client connection not valid.\n"); + return CR_FAILURE; + } + + runcmd_call.reset(); + + runcmd_call.in()->set_command(cmd); + for (size_t i = 0; i < args.size(); i++) + runcmd_call.in()->add_arguments(args[i]); + + return runcmd_call.execute(out); +} + void RPCFunctionBase::reset(bool free) { if (free) @@ -256,11 +278,14 @@ bool RemoteFunctionBase::bind(color_ostream &out, RemoteClient *client, return client->bind(out, this, name, proto); } -bool DFHack::sendRemoteMessage(CSimpleSocket &socket, int16_t id, const MessageLite *msg) +bool DFHack::sendRemoteMessage(CSimpleSocket &socket, int16_t id, const MessageLite *msg, int *psz) { int size = msg->ByteSize(); int fullsz = size + sizeof(RPCMessageHeader); + if (psz) + *psz = size; + std::auto_ptr data(new uint8_t[fullsz]); RPCMessageHeader *hdr = (RPCMessageHeader*)data.get(); diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index 5b0825700..302080780 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -64,10 +64,11 @@ using dfproto::CoreTextFragment; using google::protobuf::MessageLite; CoreService::CoreService() { - // This must be the first method, so that it gets id 0 + // These 2 methods must be first, so that they get id 0 and 1 addMethod("BindMethod", &CoreService::BindMethod); - addMethod("RunCommand", &CoreService::RunCommand); + + // Add others here: } command_result CoreService::BindMethod(color_ostream &stream, @@ -96,8 +97,7 @@ command_result CoreService::BindMethod(color_ostream &stream, } command_result CoreService::RunCommand(color_ostream &stream, - const dfproto::CoreRunCommandRequest *in, - CoreVoidReply*) + const dfproto::CoreRunCommandRequest *in) { std::string cmd = in->command(); std::vector args; @@ -242,6 +242,7 @@ void ServerConnection::threadFn(void *arg) std::cerr << "Client connection established." << endl; while (!me->in_error) { + // Read the message RPCMessageHeader header; if (!readFullBuffer(*me->socket, &header, sizeof(header))) @@ -269,6 +270,9 @@ void ServerConnection::threadFn(void *arg) //out.print("Handling %d:%d\n", header.id, header.size); + // Find and call the function + int in_size = header.size; + ServerFunctionBase *fn = vector_get(me->functions, header.id); MessageLite *reply = NULL; command_result res = CR_FAILURE; @@ -290,6 +294,7 @@ void ServerConnection::threadFn(void *arg) } } + // Flush all text output if (me->in_error) break; @@ -297,9 +302,12 @@ void ServerConnection::threadFn(void *arg) //out.print("Answer %d:%d\n", res, reply); + // Send reply + int out_size = 0; + if (res == CR_OK && reply) { - if (!sendRemoteMessage(*me->socket, RPC_REPLY_RESULT, reply)) + if (!sendRemoteMessage(*me->socket, RPC_REPLY_RESULT, reply, &out_size)) { out.printerr("In RPC server: I/O error in send result.\n"); break; @@ -307,6 +315,9 @@ void ServerConnection::threadFn(void *arg) } else { + if (reply) + out_size = reply->ByteSize(); + header.id = RPC_REPLY_FAIL; header.size = res; @@ -316,6 +327,12 @@ void ServerConnection::threadFn(void *arg) break; } } + + // Cleanup + if (fn) + { + fn->reset(out_size > 32768 || in_size > 32768); + } } std::cerr << "Shutting down client connection." << endl; diff --git a/library/dfhack-run.cpp b/library/dfhack-run.cpp index 941ce8d0e..eab039013 100644 --- a/library/dfhack-run.cpp +++ b/library/dfhack-run.cpp @@ -71,19 +71,19 @@ int main (int argc, char *argv[]) if (!client.connect()) return 2; - // Bind to RunCommand - RemoteFunction command; + // Call the command + std::vector args; + for (int i = 2; i < argc; i++) + args.push_back(argv[i]); - if (!command.bind(out, &client, "RunCommand")) - return 2; + command_result rv = client.run_command(out, argv[1], args); - // Execute it - command.in()->set_command(argv[1]); - for (int i = 2; i < argc; i++) - command.in()->add_arguments(argv[i]); + if (rv != CR_OK) { + if (rv == CR_NOT_IMPLEMENTED) + out.printerr("%s is not a recognized command.\n", argv[1]); - 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 651f5831e..105832efd 100644 --- a/library/include/ColorText.h +++ b/library/include/ColorText.h @@ -118,6 +118,8 @@ namespace DFHack virtual bool is_console() { return false; } virtual color_ostream *proxy_target() { return NULL; } + + static bool log_errors_to_stderr; }; inline color_ostream &operator << (color_ostream &out, color_ostream::color_value clr) diff --git a/library/include/RemoteClient.h b/library/include/RemoteClient.h index 453f24794..6fd762080 100644 --- a/library/include/RemoteClient.h +++ b/library/include/RemoteClient.h @@ -33,7 +33,7 @@ distribution. namespace DFHack { - using dfproto::CoreVoidReply; + using dfproto::EmptyMessage; enum command_result { @@ -64,6 +64,43 @@ namespace DFHack int32_t size; }; + /* Protocol description: + * + * 1. Handshake + * + * Client initiates connection by sending the handshake + * request header. The server responds with the response + * magic. Currently both versions must be 1. + * + * 2. Interaction + * + * Requests are done by exchanging messages between the + * client and the server. Messages consist of a serialized + * protobuf message preceeded by RPCMessageHeader. The size + * field specifies the length of the protobuf part. + * + * NOTE: As a special exception, RPC_REPLY_FAIL uses the size + * field to hold the error code directly. + * + * Every callable function is assigned a non-negative id by + * the server. Id 0 is reserved for BindMethod, which can be + * used to request any other id by function name. Id 1 is + * RunCommand, used to call console commands remotely. + * + * The client initiates every call by sending a message with + * appropriate function id and input arguments. The server + * responds with zero or more RPC_REPLY_TEXT:CoreTextNotification + * messages, followed by RPC_REPLY_RESULT containing the output + * of the function if it succeeded, or RPC_REPLY_FAIL with the + * error code if it did not. + * + * 3. Disconnect + * + * The client terminates the connection by sending an + * RPC_REQUEST_QUIT header with zero size and immediately + * closing the socket. + */ + class DFHACK_EXPORT RemoteClient; class DFHACK_EXPORT RPCFunctionBase { @@ -123,7 +160,7 @@ namespace DFHack int16_t id; }; - template + template class RemoteFunction : public RemoteFunctionBase { public: In *make_in() const { return static_cast(RemoteFunctionBase::make_in()); } @@ -133,16 +170,33 @@ namespace DFHack RemoteFunction() : RemoteFunctionBase(&In::default_instance(), &Out::default_instance()) {} - command_result execute(color_ostream &out) { - return RemoteFunctionBase::execute(out, this->in(), this->out()); + command_result execute(color_ostream &stream) { + return RemoteFunctionBase::execute(stream, in(), out()); } - command_result operator() (color_ostream &out, const In *input, Out *output) { - return RemoteFunctionBase::execute(out, input, output); + command_result operator() (color_ostream &stream, const In *input, Out *output) { + return RemoteFunctionBase::execute(stream, input, output); + } + }; + + template + class RemoteFunction : public RemoteFunctionBase { + public: + In *make_in() const { return static_cast(RemoteFunctionBase::make_in()); } + In *in() { return static_cast(RemoteFunctionBase::in()); } + + RemoteFunction() : RemoteFunctionBase(&In::default_instance(), &EmptyMessage::default_instance()) {} + + command_result execute(color_ostream &stream) { + return RemoteFunctionBase::execute(stream, in(), out()); + } + command_result operator() (color_ostream &stream, const In *input) { + return RemoteFunctionBase::execute(stream, input, out()); } }; bool readFullBuffer(CSimpleSocket &socket, void *buf, int size); - bool sendRemoteMessage(CSimpleSocket &socket, int16_t id, const ::google::protobuf::MessageLite *msg); + bool sendRemoteMessage(CSimpleSocket &socket, int16_t id, + const ::google::protobuf::MessageLite *msg, int *psz = NULL); class DFHACK_EXPORT RemoteClient { @@ -160,10 +214,14 @@ namespace DFHack bool connect(int port = -1); void disconnect(); + command_result run_command(color_ostream &out, const std::string &cmd, + const std::vector &args); + private: bool active; CActiveSocket socket; RemoteFunction bind_call; + RemoteFunction runcmd_call; }; } diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index a3cc6c044..76abbe726 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -72,6 +72,23 @@ namespace DFHack function_type fptr; }; + template + class VoidServerFunction : public ServerFunctionBase { + public: + typedef command_result (*function_type)(color_ostream &out, const In *input); + + In *in() { return static_cast(RPCFunctionBase::in()); } + + VoidServerFunction(RPCService *owner, const char *name, function_type fptr) + : ServerFunctionBase(&In::default_instance(), &EmptyMessage::default_instance(), owner, name), + fptr(fptr) {} + + virtual command_result execute(color_ostream &stream) { return fptr(stream, in()); } + + private: + function_type fptr; + }; + template class ServerMethod : public ServerFunctionBase { public: @@ -92,6 +109,25 @@ namespace DFHack function_type fptr; }; + template + class VoidServerMethod : public ServerFunctionBase { + public: + typedef command_result (Svc::*function_type)(color_ostream &out, const In *input); + + In *in() { return static_cast(RPCFunctionBase::in()); } + + VoidServerMethod(RPCService *owner, const char *name, function_type fptr) + : ServerFunctionBase(&In::default_instance(), &EmptyMessage::default_instance(), owner, name), + fptr(fptr) {} + + virtual command_result execute(color_ostream &stream) { + return (static_cast(owner)->*fptr)(stream, in()); + } + + private: + function_type fptr; + }; + class Plugin; class DFHACK_EXPORT RPCService { @@ -120,6 +156,15 @@ namespace DFHack functions.push_back(new ServerFunction(this, name, fptr)); } + template + void addFunction( + const char *name, + command_result (*fptr)(color_ostream &out, const In *input) + ) { + assert(!owner); + functions.push_back(new VoidServerFunction(this, name, fptr)); + } + protected: ServerConnection *connection() { return owner; } @@ -131,6 +176,15 @@ namespace DFHack assert(!owner); functions.push_back(new ServerMethod(this, name, fptr)); } + + template + void addMethod( + const char *name, + command_result (Svc::*fptr)(color_ostream &out, const In *input) + ) { + assert(!owner); + functions.push_back(new VoidServerMethod(this, name, fptr)); + } }; class CoreService : public RPCService { @@ -141,8 +195,7 @@ namespace DFHack const dfproto::CoreBindRequest *in, dfproto::CoreBindReply *out); command_result RunCommand(color_ostream &stream, - const dfproto::CoreRunCommandRequest *in, - CoreVoidReply*); + const dfproto::CoreRunCommandRequest *in); }; class DFHACK_EXPORT ServerConnection { diff --git a/library/proto/CoreProtocol.proto b/library/proto/CoreProtocol.proto index eec5ef7f2..2e2fe2c24 100644 --- a/library/proto/CoreProtocol.proto +++ b/library/proto/CoreProtocol.proto @@ -42,7 +42,7 @@ message CoreErrorNotification { required ErrorCode code = 1; } -message CoreVoidReply {} +message EmptyMessage {} message CoreBindRequest { required string method = 1; diff --git a/package/linux/dfhack b/package/linux/dfhack index d539237ff..3e5a0d803 100755 --- a/package/linux/dfhack +++ b/package/linux/dfhack @@ -35,7 +35,7 @@ fi # Now run -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack/deplibs" +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack" case "$1" in -g | --gdb) diff --git a/package/linux/dfhack-run b/package/linux/dfhack-run new file mode 100755 index 000000000..cc69db964 --- /dev/null +++ b/package/linux/dfhack-run @@ -0,0 +1,8 @@ +#!/bin/sh + +DF_DIR=$(dirname "$0") +cd "${DF_DIR}" + +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack" + +exec hack/dfhack-run "$@" diff --git a/package/linux/egghack b/package/linux/egghack index 1ce583ceb..2265ab29e 100755 --- a/package/linux/egghack +++ b/package/linux/egghack @@ -1,7 +1,7 @@ #!/bin/sh DF_DIR=$(dirname "$0") cd "${DF_DIR}" -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack/deplibs" +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack" export SDL_DISABLE_LOCK_KEYS=1 # Work around for bug in Debian/Ubuntu SDL patch. #export SDL_VIDEO_CENTERED=1 # Centre the screen. Messes up resizing. ./libs/Dwarf_Fortress $* # Go, go, go! :) diff --git a/plugins/server/CMakeLists.txt b/plugins/server/CMakeLists.txt index 5af8e58fd..6fd7c44d0 100644 --- a/plugins/server/CMakeLists.txt +++ b/plugins/server/CMakeLists.txt @@ -15,7 +15,7 @@ IF(UNIX) ${include_directories} ${server_SOURCE_DIR}/zeromq ) - install(PROGRAMS ${server_SOURCE_DIR}/zeromq/libzmq.so.1 DESTINATION "hack/deplibs") + install(PROGRAMS ${server_SOURCE_DIR}/zeromq/libzmq.so.1 DESTINATION ${DFHACK_LIBRARY_DESTINATION}) ELSE() SET(PROJECT_LIBS zmq @@ -31,7 +31,7 @@ ELSE() ${include_directories} ${server_SOURCE_DIR}/zeromq ) - install(PROGRAMS ${server_SOURCE_DIR}/zeromq/libzmq.dll DESTINATION ".") + install(PROGRAMS ${server_SOURCE_DIR}/zeromq/libzmq.dll DESTINATION ${DFHACK_LIBRARY_DESTINATION}) ENDIF()