#pragma once #include #include #include #include #include #include #include #include #include #include "nlohmann/json.hpp" #include "Stopwatch.hh" /// \ingroup clientserver /// /// Object representing a Cadabra server, capable of receiving messages /// on a websocket, running Python code, and sending output back to the /// client. /// /// Contains the logic to intercept raw Python output but also provides /// functionality to the Python side which enables clients to send /// various objects in JSON encoded form. See Server::on_block_finished /// for the format of these messages. /// /// Every block is run on the same Python scope. They run sequentially, /// one at a time, on a thread separate from the websocket++ main loop. /// When the Python code finishes (or when it is interrupted), this thread /// locks the socket_mutex and calls on_block_finished(). class Server { public: Server(); Server(const Server&)=delete; Server(const std::string& socket); virtual ~Server(); /// The only user-visible part: just instantiate a server object and /// start it with run(). This will not return until the server has /// been shut down. Picks a random port when port==0. If /// `exit_on_disconnect==false`, keep the server alive on the /// same porta and with the same authentication token when /// the connection drops (so you can reconnect). void run(int port=0, bool exit_on_disconnect=true); /// Python output catching. We implement this in a C++ class /// because we want to be able to catch each line of output /// separately, and perhaps add additional functionality to send /// out-of-band messages to the client later. class CatchOutput { public: CatchOutput(); CatchOutput(const CatchOutput&); void write(const std::string& txt); void clear(); std::string str() const; private: std::string collect; }; CatchOutput catchOut, catchErr; Stopwatch server_stopwatch; Stopwatch sympy_stopwatch; /// Raw code to send a string (which must be JSON formatted) as /// a message to the client. Handles communication of the result back to the /// client in JSON format. This is always of the form /// /// { "header": { "parent_id": "...", /// "parent_origin": "client" | "server", /// "cell_id": "...", /// "cell_origin": "client" | "server" /// }, /// "content": { "output": "..." }, /// "msg_type": "..." /// } /// /// msg_type can be "output", "latex", "image_png" and so on, /// corresponding to the possible values of DataCell::CellType. /// /// Returns the serial number of the new cell sent. virtual uint64_t send(const std::string& output, const std::string& msg_type, uint64_t parent_id=0, bool last_in_sequence=false); // virtual void send_completion(uint64_t id, int pos, int alternative, std::string original, std::string completed); void send_progress_update(const std::string& msg, int n, int total); void send_json(const std::string&); bool handles(const std::string& otype) const; std::string architecture() const; /// Start a thread which waits for blocks to appear on the block queue, and /// executes them in turn. void wait_for_job(); protected: void init(); // WebSocket++ dependent parts below. typedef websocketpp::server WebsocketServer; void on_socket_init(websocketpp::connection_hdl hdl, boost::asio::ip::tcp::socket & s); void on_message(websocketpp::connection_hdl hdl, WebsocketServer::message_ptr msg); void on_open(websocketpp::connection_hdl hdl); void on_close(websocketpp::connection_hdl hdl); WebsocketServer wserver; std::string socket_name; // Connection tracking. There can be multiple connections to // the server, but they all have access to the same Python // scope. With multiple connections, one can inspect the Python // stack from a different client (e.g. for debugging purposes). // All connections share the same authentication token. class Connection { public: Connection(); websocketpp::connection_hdl hdl; boost::uuids::uuid uuid; }; typedef std::map> ConnectionMap; ConnectionMap connections; // Authentication token, needs to be sent along with any message. // Gets set when the server announces its port. std::string authentication_token; // Mutex to be able to use the websocket layer from both the // main loop and the python-running thread. std::mutex ws_mutex; // Basics for the working thread that processes blocks. std::thread runner; std::mutex block_available_mutex; std::condition_variable block_available; bool exit_on_disconnect; // Data and connection info for a single block of code. class Block { public: Block(websocketpp::connection_hdl, const std::string&, uint64_t id, const std::string& msg_type); websocketpp::connection_hdl hdl; // FIXME: decouple from websocket? std::string msg_type; std::string input; std::string output; std::string error; uint64_t cell_id; // Response message, partially filled in when the // request comes in. nlohmann::json response; }; std::queue block_queue; websocketpp::connection_hdl current_hdl; uint64_t current_id; // id of the block given to us by the client. // Run a piece of Python code. This is called from a separate // thread constructed by on_message(). std::string run_string(const std::string&, bool handle_output=true); /// Called by the run_block() thread upon completion of the /// task. This will send any output generated by printing directly /// to stdout or stderr from Python (so, output not generated by /// using the 'display' function). Indicates to the client that /// this block has finished executing. Will send an empty string /// if there has been no output 'print'ed. virtual void on_block_finished(Block); virtual void on_block_error(Block); virtual void on_kernel_fault(Block); // uint64_t return_cell_id; // serial number of cells generated by us. /// Halt the currently running block and prevent execution of any /// further blocks that may still be on the queue. void stop_block(); bool started; std::future job; /// Takes a JSON encoded message and performs the required action to process it. /// Where applicable these messages are compatible with IPython's message types, /// http://ipython.org/ipython-doc/dev/development/messaging.html void dispatch_message(websocketpp::connection_hdl, const std::string& json_string); // Python global info. pybind11::scoped_interpreter guard; pybind11::module main_module; pybind11::object main_namespace; // int cells_ran; };