// CODYlib -*- mode:c++ -*- // Copyright (C) 2020 Nathan Sidwell, nathan@acm.org // License: Apache v2.0 #ifndef CODY_HH #define CODY_HH 1 // 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 // C++ #include #include #include // C #include // OS #include #include #if CODY_NETWORKING #include #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 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 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 &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 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 &&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 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 &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 Uncork (); /// /// Indicate corkedness of connection bool IsCorked () const { return !corked.empty (); } private: Packet ProcessResponse (std::vector &, 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