| // CODYlib -*- mode:c++ -*- |
| // Copyright (C) 2020 Nathan Sidwell, nathan@acm.org |
| // License: Apache v2.0 |
| |
| #ifndef CODY_HH |
| #define CODY_HH 1 |
| |
| // If the user specifies this as non-zero, it must be what we expect, |
| // generally only good for requesting no networking |
| #if !defined (CODY_NETWORKING) |
| // Have a known-good list of networking systems |
| #if defined (__unix__) || defined (__MACH__) |
| #define CODY_NETWORKING 1 |
| #else |
| #define CODY_NETWORKING 0 |
| #endif |
| #if 0 // For testing |
| #undef CODY_NETWORKING |
| #define CODY_NETWORKING 0 |
| #endif |
| #endif |
| |
| // C++ |
| #include <memory> |
| #include <string> |
| #include <vector> |
| // C |
| #include <cstddef> |
| // OS |
| #include <errno.h> |
| #include <sys/types.h> |
| #if CODY_NETWORKING |
| #include <sys/socket.h> |
| #endif |
| |
| namespace Cody { |
| |
| // Set version to 1, as this is completely incompatible with 0. |
| // Fortunately both versions 0 and 1 will recognize each other's HELLO |
| // messages sufficiently to error out |
| constexpr unsigned Version = 1; |
| |
| // FIXME: I guess we need a file-handle abstraction here |
| // Is windows DWORDPTR still?, or should it be FILE *? (ew). |
| |
| namespace Detail { |
| |
| // C++11 doesn't have utf8 character literals :( |
| |
| template<unsigned I> |
| constexpr char S2C (char const (&s)[I]) |
| { |
| static_assert (I == 2, "only single octet strings may be converted"); |
| return s[0]; |
| } |
| |
| /// Internal buffering class. Used to concatenate outgoing messages |
| /// and Lex incoming ones. |
| class MessageBuffer |
| { |
| std::vector<char> buffer; ///< buffer holding the message |
| size_t lastBol = 0; ///< location of the most recent Beginning Of |
| ///< Line, or position we've readed when writing |
| |
| public: |
| MessageBuffer () = default; |
| ~MessageBuffer () = default; |
| MessageBuffer (MessageBuffer &&) = default; |
| MessageBuffer &operator= (MessageBuffer &&) = default; |
| |
| public: |
| /// |
| /// Finalize a buffer to be written. No more lines can be added to |
| /// the buffer. Use before a sequence of Write calls. |
| void PrepareToWrite () |
| { |
| buffer.push_back (u8"\n"[0]); |
| lastBol = 0; |
| } |
| /// |
| /// Prepare a buffer for reading. Use before a sequence of Read calls. |
| void PrepareToRead () |
| { |
| buffer.clear (); |
| lastBol = 0; |
| } |
| |
| public: |
| /// Begin a message line. Use before a sequence of Append and |
| /// related calls. |
| void BeginLine (); |
| /// End a message line. Use after a sequence of Append and related calls. |
| void EndLine () {} |
| |
| public: |
| /// Append a string to the current line. No whitespace is prepended |
| /// or appended. |
| /// |
| /// @param str the string to be written |
| /// @param maybe_quote indicate if there's a possibility the string |
| /// contains characters that need quoting. Defaults to false. |
| /// It is always safe to set |
| /// this true, but that causes an additional scan of the string. |
| /// @param len The length of the string. If not specified, strlen |
| /// is used to find the length. |
| void Append (char const *str, bool maybe_quote = false, |
| size_t len = ~size_t (0)); |
| |
| /// |
| /// Add whitespace word separator. Multiple adjacent whitespace is fine. |
| void Space () |
| { |
| Append (Detail::S2C(u8" ")); |
| } |
| |
| public: |
| /// Add a word as with Append, but prefixing whitespace to make a |
| /// separate word |
| void AppendWord (char const *str, bool maybe_quote = false, |
| size_t len = ~size_t (0)) |
| { |
| if (buffer.size () != lastBol) |
| Space (); |
| Append (str, maybe_quote, len); |
| } |
| /// Add a word as with AppendWord |
| /// @param str the string to append |
| /// @param maybe_quote string might need quoting, as for Append |
| void AppendWord (std::string const &str, bool maybe_quote = false) |
| { |
| AppendWord (str.data (), maybe_quote, str.size ()); |
| } |
| /// |
| /// Add an integral value, prepending a space. |
| void AppendInteger (unsigned u); |
| |
| private: |
| /// Append a literal character. |
| /// @param c character to append |
| void Append (char c); |
| |
| public: |
| /// Lex the next input line into a vector of words. |
| /// @param words filled with a vector of lexed strings |
| /// @result 0 if no errors, an errno value on lexxing error such as |
| /// there being no next line (ENOENT), or malformed quoting (EINVAL) |
| int Lex (std::vector<std::string> &words); |
| |
| public: |
| /// Append the most-recently lexxed line to a string. May be useful |
| /// in error messages. The unparsed line is appended -- before any |
| /// unquoting. |
| /// If we had c++17 string_view, we'd simply return a view of the |
| /// line, and leave it to the caller to do any concatenation. |
| /// @param l string to-which the lexxed line is appended. |
| void LexedLine (std::string &l); |
| |
| public: |
| /// Detect if we have reached the end of the input buffer. |
| /// I.e. there are no more lines to Lex |
| /// @result True if at end |
| bool IsAtEnd () const |
| { |
| return lastBol == buffer.size (); |
| } |
| |
| public: |
| /// Read from end point into a read buffer, as with read(2). This will |
| /// not block , unless FD is blocking, and there is nothing |
| /// immediately available. |
| /// @param fd file descriptor to read from. This may be a regular |
| /// file, pipe or socket. |
| /// @result on error returns errno. If end of file occurs, returns |
| /// -1. At end of message returns 0. If there is more needed |
| /// returns EAGAIN (or possibly EINTR). If the message is |
| /// malformed, returns EINVAL. |
| int Read (int fd) noexcept; |
| |
| public: |
| /// Write to an end point from a write buffer, as with write(2). As |
| /// with Read, this will not usually block. |
| /// @param fd file descriptor to write to. This may be a regular |
| /// file, pipe or socket. |
| /// @result on error returns errno. |
| /// At end of message returns 0. If there is more to write |
| /// returns EAGAIN (or possibly EINTR). |
| int Write (int fd) noexcept; |
| }; |
| |
| /// |
| /// Request codes. Perhaps this should be exposed? These are likely |
| /// useful to servers that queue requests. |
| enum RequestCode |
| { |
| RC_CONNECT, |
| RC_MODULE_REPO, |
| RC_MODULE_EXPORT, |
| RC_MODULE_IMPORT, |
| RC_MODULE_COMPILED, |
| RC_INCLUDE_TRANSLATE, |
| RC_HWM |
| }; |
| |
| /// Internal file descriptor tuple. It's used as an anonymous union member. |
| struct FD |
| { |
| int from; ///< Read from this FD |
| int to; ///< Write to this FD |
| }; |
| |
| } |
| |
| // Flags for various requests |
| enum class Flags : unsigned |
| { |
| None, |
| NameOnly = 1<<0, // Only querying for CMI names, not contents |
| }; |
| |
| inline Flags operator& (Flags a, Flags b) |
| { |
| return Flags (unsigned (a) & unsigned (b)); |
| } |
| inline Flags operator| (Flags a, Flags b) |
| { |
| return Flags (unsigned (a) | unsigned (b)); |
| } |
| |
| /// |
| /// Response data for a request. Returned by Client's request calls, |
| /// which return a single Packet. When the connection is Corked, the |
| /// Uncork call will return a vector of Packets. |
| class Packet |
| { |
| public: |
| /// |
| /// Packet is a variant structure. These are the possible content types. |
| enum Category { INTEGER, STRING, VECTOR}; |
| |
| private: |
| // std:variant is a C++17 thing, so we're doing this ourselves. |
| union |
| { |
| size_t integer; ///< Integral value |
| std::string string; ///< String value |
| std::vector<std::string> vector; ///< Vector of string value |
| }; |
| Category cat : 2; ///< Discriminator |
| |
| private: |
| unsigned short code = 0; ///< Packet type |
| unsigned short request = 0; |
| |
| public: |
| Packet (unsigned c, size_t i = 0) |
| : integer (i), cat (INTEGER), code (c) |
| { |
| } |
| Packet (unsigned c, std::string &&s) |
| : string (std::move (s)), cat (STRING), code (c) |
| { |
| } |
| Packet (unsigned c, std::string const &s) |
| : string (s), cat (STRING), code (c) |
| { |
| } |
| Packet (unsigned c, std::vector<std::string> &&v) |
| : vector (std::move (v)), cat (VECTOR), code (c) |
| { |
| } |
| // No non-move constructor from a vector. You should not be doing |
| // that. |
| |
| // Only move constructor and move assignment |
| Packet (Packet &&t) |
| { |
| Create (std::move (t)); |
| } |
| Packet &operator= (Packet &&t) |
| { |
| Destroy (); |
| Create (std::move (t)); |
| |
| return *this; |
| } |
| ~Packet () |
| { |
| Destroy (); |
| } |
| |
| private: |
| /// |
| /// Variant move creation from another packet |
| void Create (Packet &&t); |
| /// |
| /// Variant destruction |
| void Destroy (); |
| |
| public: |
| /// |
| /// Return the packet type |
| unsigned GetCode () const |
| { |
| return code; |
| } |
| /// |
| /// Return the packet type |
| unsigned GetRequest () const |
| { |
| return request; |
| } |
| void SetRequest (unsigned r) |
| { |
| request = r; |
| } |
| /// |
| /// Return the category of the packet's payload |
| Category GetCategory () const |
| { |
| return cat; |
| } |
| |
| public: |
| /// |
| /// Return an integral payload. Undefined if the category is not INTEGER |
| size_t GetInteger () const |
| { |
| return integer; |
| } |
| /// |
| /// Return (a reference to) a string payload. Undefined if the |
| /// category is not STRING |
| std::string const &GetString () const |
| { |
| return string; |
| } |
| std::string &GetString () |
| { |
| return string; |
| } |
| /// |
| /// Return (a reference to) a constant vector of strings payload. |
| /// Undefined if the category is not VECTOR |
| std::vector<std::string> const &GetVector () const |
| { |
| return vector; |
| } |
| /// |
| /// Return (a reference to) a non-conatant vector of strings payload. |
| /// Undefined if the category is not VECTOR |
| std::vector<std::string> &GetVector () |
| { |
| return vector; |
| } |
| }; |
| |
| class Server; |
| |
| /// |
| /// Client-side (compiler) object. |
| class Client |
| { |
| public: |
| /// Response packet codes |
| enum PacketCode |
| { |
| PC_CORKED, ///< Messages are corked |
| PC_CONNECT, ///< Packet is integer version |
| PC_ERROR, ///< Packet is error string |
| PC_OK, |
| PC_BOOL, |
| PC_PATHNAME |
| }; |
| |
| private: |
| Detail::MessageBuffer write; ///< Outgoing write buffer |
| Detail::MessageBuffer read; ///< Incoming read buffer |
| std::string corked; ///< Queued request tags |
| union |
| { |
| Detail::FD fd; ///< FDs connecting to server |
| Server *server; ///< Directly connected server |
| }; |
| bool is_direct = false; ///< Discriminator |
| bool is_connected = false; /// Connection handshake succesful |
| |
| private: |
| Client (); |
| public: |
| /// Direct connection constructor. |
| /// @param s Server to directly connect |
| Client (Server *s) |
| : Client () |
| { |
| is_direct = true; |
| server = s; |
| } |
| /// Communication connection constructor |
| /// @param from file descriptor to read from |
| /// @param to file descriptor to write to, defaults to from |
| Client (int from, int to = -1) |
| : Client () |
| { |
| fd.from = from; |
| fd.to = to < 0 ? from : to; |
| } |
| ~Client (); |
| // We have to provide our own move variants, because of the variant member. |
| Client (Client &&); |
| Client &operator= (Client &&); |
| |
| public: |
| /// |
| /// Direct connection predicate |
| bool IsDirect () const |
| { |
| return is_direct; |
| } |
| /// |
| /// Successful handshake predicate |
| bool IsConnected () const |
| { |
| return is_connected; |
| } |
| |
| public: |
| /// |
| /// Get the read FD |
| /// @result the FD to read from, -1 if a direct connection |
| int GetFDRead () const |
| { |
| return is_direct ? -1 : fd.from; |
| } |
| /// |
| /// Get the write FD |
| /// @result the FD to write to, -1 if a direct connection |
| int GetFDWrite () const |
| { |
| return is_direct ? -1 : fd.to; |
| } |
| /// |
| /// Get the directly-connected server |
| /// @result the server, or nullptr if a communication connection |
| Server *GetServer () const |
| { |
| return is_direct ? server : nullptr; |
| } |
| |
| public: |
| /// |
| /// Perform connection handshake. All othe requests will result in |
| /// errors, until handshake is succesful. |
| /// @param agent compiler identification |
| /// @param ident compilation identifiation (maybe nullptr) |
| /// @param alen length of agent string, if known |
| /// @param ilen length of ident string, if known |
| /// @result packet indicating success (or deferrment) of the |
| /// connection, payload is optional flags |
| Packet Connect (char const *agent, char const *ident, |
| size_t alen = ~size_t (0), size_t ilen = ~size_t (0)); |
| /// std::string wrapper for connection |
| /// @param agent compiler identification |
| /// @param ident compilation identification |
| Packet Connect (std::string const &agent, std::string const &ident) |
| { |
| return Connect (agent.c_str (), ident.c_str (), |
| agent.size (), ident.size ()); |
| } |
| |
| public: |
| /// Request compiler module repository |
| /// @result packet indicating repo |
| Packet ModuleRepo (); |
| |
| public: |
| /// Inform of compilation of a named module interface or partition, |
| /// or a header unit |
| /// @param str module or header-unit |
| /// @param len name length, if known |
| /// @result CMI name (or deferrment/error) |
| Packet ModuleExport (char const *str, Flags flags, size_t len = ~size_t (0)); |
| |
| Packet ModuleExport (char const *str) |
| { |
| return ModuleExport (str, Flags::None, ~size_t (0)); |
| } |
| Packet ModuleExport (std::string const &s, Flags flags = Flags::None) |
| { |
| return ModuleExport (s.c_str (), flags, s.size ()); |
| } |
| |
| public: |
| /// Importation of a module, partition or header-unit |
| /// @param str module or header-unit |
| /// @param len name length, if known |
| /// @result CMI name (or deferrment/error) |
| Packet ModuleImport (char const *str, Flags flags, size_t len = ~size_t (0)); |
| |
| Packet ModuleImport (char const *str) |
| { |
| return ModuleImport (str, Flags::None, ~size_t (0)); |
| } |
| Packet ModuleImport (std::string const &s, Flags flags = Flags::None) |
| { |
| return ModuleImport (s.c_str (), flags, s.size ()); |
| } |
| |
| public: |
| /// Successful compilation of a module interface, partition or |
| /// header-unit. Must have been preceeded by a ModuleExport |
| /// request. |
| /// @param str module or header-unit |
| /// @param len name length, if known |
| /// @result OK (or deferment/error) |
| Packet ModuleCompiled (char const *str, Flags flags, size_t len = ~size_t (0)); |
| |
| Packet ModuleCompiled (char const *str) |
| { |
| return ModuleCompiled (str, Flags::None, ~size_t (0)); |
| } |
| Packet ModuleCompiled (std::string const &s, Flags flags = Flags::None) |
| { |
| return ModuleCompiled (s.c_str (), flags, s.size ()); |
| } |
| |
| /// Include translation query. |
| /// @param str header unit name |
| /// @param len name length, if known |
| /// @result Packet indicating include translation boolean, or CMI |
| /// name (or deferment/error) |
| Packet IncludeTranslate (char const *str, Flags flags, |
| size_t len = ~size_t (0)); |
| |
| Packet IncludeTranslate (char const *str) |
| { |
| return IncludeTranslate (str, Flags::None, ~size_t (0)); |
| } |
| Packet IncludeTranslate (std::string const &s, Flags flags = Flags::None) |
| { |
| return IncludeTranslate (s.c_str (), flags, s.size ()); |
| } |
| |
| public: |
| /// Cork the connection. All requests are queued up. Each request |
| /// call will return a PC_CORKED packet. |
| void Cork (); |
| |
| /// Uncork the connection. All queued requests are sent to the |
| /// server, and a block of responses waited for. |
| /// @result A vector of packets, containing the in-order responses to the |
| /// queued requests. |
| std::vector<Packet> Uncork (); |
| /// |
| /// Indicate corkedness of connection |
| bool IsCorked () const |
| { |
| return !corked.empty (); |
| } |
| |
| private: |
| Packet ProcessResponse (std::vector<std::string> &, unsigned code, |
| bool isLast); |
| Packet MaybeRequest (unsigned code); |
| int CommunicateWithServer (); |
| }; |
| |
| /// This server-side class is used to resolve requests from one or |
| /// more clients. You are expected to derive from it and override the |
| /// virtual functions it provides. The connection resolver may return |
| /// a different resolved object to service the remainder of the |
| /// connection -- for instance depending on the compiler that is |
| /// making the requests. |
| class Resolver |
| { |
| public: |
| Resolver () = default; |
| virtual ~Resolver (); |
| |
| protected: |
| /// Mapping from a module or header-unit name to a CMI file name. |
| /// @param module module name |
| /// @result CMI name |
| virtual std::string GetCMIName (std::string const &module); |
| |
| /// Return the CMI file suffix to use |
| /// @result CMI suffix, a statically allocated string |
| virtual char const *GetCMISuffix (); |
| |
| public: |
| /// When the requests of a directly-connected server are processed, |
| /// we may want to wait for the requests to complete (for instance a |
| /// set of subjobs). |
| /// @param s directly connected server. |
| virtual void WaitUntilReady (Server *s); |
| |
| public: |
| /// Provide an error response. |
| /// @param s the server to provide the response to. |
| /// @param msg the error message |
| virtual void ErrorResponse (Server *s, std::string &&msg); |
| |
| public: |
| /// Connection handshake. Provide response to server and return new |
| /// (or current) resolver, or nullptr. |
| /// @param s server to provide response to |
| /// @param version the client's version number |
| /// @param agent the client agent (compiler identification) |
| /// @param ident the compilation identification (may be empty) |
| /// @result nullptr in the case of an error. An error response will |
| /// be sent. If handing off to another resolver, return that, |
| /// otherwise this |
| virtual Resolver *ConnectRequest (Server *s, unsigned version, |
| std::string &agent, std::string &ident); |
| |
| public: |
| // return 0 on ok, ERRNO on failure, -1 on unspecific error |
| virtual int ModuleRepoRequest (Server *s); |
| |
| virtual int ModuleExportRequest (Server *s, Flags flags, |
| std::string &module); |
| virtual int ModuleImportRequest (Server *s, Flags flags, |
| std::string &module); |
| virtual int ModuleCompiledRequest (Server *s, Flags flags, |
| std::string &module); |
| virtual int IncludeTranslateRequest (Server *s, Flags flags, |
| std::string &include); |
| }; |
| |
| |
| /// This server-side (build system) class handles a single connection |
| /// to a client. It has 3 states, READING:accumulating a message |
| /// block froma client, WRITING:writing a message block to a client |
| /// and PROCESSING:resolving requests. If the server does not spawn |
| /// jobs to build needed artifacts, the PROCESSING state will be brief. |
| class Server |
| { |
| public: |
| enum Direction |
| { |
| READING, // Server is waiting for completion of a (set of) |
| // requests from client. The next state will be PROCESSING. |
| WRITING, // Server is writing a (set of) responses to client. |
| // The next state will be READING. |
| PROCESSING // Server is processing client request(s). The next |
| // state will be WRITING. |
| }; |
| |
| private: |
| Detail::MessageBuffer write; |
| Detail::MessageBuffer read; |
| Resolver *resolver; |
| Detail::FD fd; |
| bool is_connected = false; |
| Direction direction : 2; |
| |
| public: |
| Server (Resolver *r); |
| Server (Resolver *r, int from, int to = -1) |
| : Server (r) |
| { |
| fd.from = from; |
| fd.to = to >= 0 ? to : from; |
| } |
| ~Server (); |
| Server (Server &&); |
| Server &operator= (Server &&); |
| |
| public: |
| bool IsConnected () const |
| { |
| return is_connected; |
| } |
| |
| public: |
| void SetDirection (Direction d) |
| { |
| direction = d; |
| } |
| |
| public: |
| Direction GetDirection () const |
| { |
| return direction; |
| } |
| int GetFDRead () const |
| { |
| return fd.from; |
| } |
| int GetFDWrite () const |
| { |
| return fd.to; |
| } |
| Resolver *GetResolver () const |
| { |
| return resolver; |
| } |
| |
| public: |
| /// Process requests from a directly-connected client. This is a |
| /// small wrapper around ProcessRequests, with some buffer swapping |
| /// for communication. It is expected that such processessing is |
| /// immediate. |
| /// @param from message block from client |
| /// @param to message block to client |
| void DirectProcess (Detail::MessageBuffer &from, Detail::MessageBuffer &to); |
| |
| public: |
| /// Process the messages queued in the read buffer. We enter the |
| /// PROCESSING state, and each message line causes various resolver |
| /// methods to be called. Once processed, the server may need to |
| /// wait for all the requests to be ready, or it may be able to |
| /// immediately write responses back. |
| void ProcessRequests (); |
| |
| public: |
| /// Accumulate an error response. |
| /// @param error the error message to encode |
| /// @param elen length of error, if known |
| void ErrorResponse (char const *error, size_t elen = ~size_t (0)); |
| void ErrorResponse (std::string const &error) |
| { |
| ErrorResponse (error.data (), error.size ()); |
| } |
| |
| /// Accumulate an OK response |
| void OKResponse (); |
| |
| /// Accumulate a boolean response |
| void BoolResponse (bool); |
| |
| /// Accumulate a pathname response |
| /// @param path (may be nullptr, or empty) |
| /// @param rlen length, if known |
| void PathnameResponse (char const *path, size_t plen = ~size_t (0)); |
| void PathnameResponse (std::string const &path) |
| { |
| PathnameResponse (path.data (), path.size ()); |
| } |
| |
| public: |
| /// Accumulate a (successful) connection response |
| /// @param agent the server-side agent |
| /// @param alen agent length, if known |
| void ConnectResponse (char const *agent, size_t alen = ~size_t (0)); |
| void ConnectResponse (std::string const &agent) |
| { |
| ConnectResponse (agent.data (), agent.size ()); |
| } |
| |
| public: |
| /// Write message block to client. Semantics as for |
| /// MessageBuffer::Write. |
| /// @result errno or completion (0). |
| int Write () |
| { |
| return write.Write (fd.to); |
| } |
| /// Initialize for writing a message block. All responses to the |
| /// incomping message block must be complete Enters WRITING state. |
| void PrepareToWrite () |
| { |
| write.PrepareToWrite (); |
| direction = WRITING; |
| } |
| |
| public: |
| /// Read message block from client. Semantics as for |
| /// MessageBuffer::Read. |
| /// @result errno, eof (-1) or completion (0) |
| int Read () |
| { |
| return read.Read (fd.from); |
| } |
| /// Initialize for reading a message block. Enters READING state. |
| void PrepareToRead () |
| { |
| read.PrepareToRead (); |
| direction = READING; |
| } |
| }; |
| |
| // Helper network stuff |
| |
| #if CODY_NETWORKING |
| // Socket with specific address |
| int OpenSocket (char const **, sockaddr const *sock, socklen_t len); |
| int ListenSocket (char const **, sockaddr const *sock, socklen_t len, |
| unsigned backlog); |
| |
| // Local domain socket (eg AF_UNIX) |
| int OpenLocal (char const **, char const *name); |
| int ListenLocal (char const **, char const *name, unsigned backlog = 0); |
| |
| // ipv6 socket |
| int OpenInet6 (char const **e, char const *name, int port); |
| int ListenInet6 (char const **, char const *name, int port, |
| unsigned backlog = 0); |
| #endif |
| |
| // FIXME: Mapping file utilities? |
| |
| } |
| |
| #endif // CODY_HH |