summaryrefslogtreecommitdiffstats
path: root/contrib/imhiredis
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--contrib/imhiredis/COPYING674
-rw-r--r--contrib/imhiredis/Makefile.am7
-rw-r--r--contrib/imhiredis/Makefile.in799
-rw-r--r--contrib/imhiredis/README81
-rw-r--r--contrib/imhiredis/imhiredis.c2298
5 files changed, 3859 insertions, 0 deletions
diff --git a/contrib/imhiredis/COPYING b/contrib/imhiredis/COPYING
new file mode 100644
index 0000000..f44bd49
--- /dev/null
+++ b/contrib/imhiredis/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/contrib/imhiredis/Makefile.am b/contrib/imhiredis/Makefile.am
new file mode 100644
index 0000000..6528a19
--- /dev/null
+++ b/contrib/imhiredis/Makefile.am
@@ -0,0 +1,7 @@
+pkglib_LTLIBRARIES = imhiredis.la
+imhiredis_la_SOURCES = imhiredis.c
+imhiredis_la_CPPFLAGS = $(RSRT_CFLAGS) $(PTHREADS_CFLAGS) $(HIREDIS_CFLAGS)
+imhiredis_la_LDFLAGS = -module -avoid-version
+imhiredis_la_LIBADD = $(HIREDIS_LIBS)
+
+EXTRA_DIST =
diff --git a/contrib/imhiredis/Makefile.in b/contrib/imhiredis/Makefile.in
new file mode 100644
index 0000000..fb3a3e2
--- /dev/null
+++ b/contrib/imhiredis/Makefile.in
@@ -0,0 +1,799 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = contrib/imhiredis
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_check_define.m4 \
+ $(top_srcdir)/m4/atomic_operations.m4 \
+ $(top_srcdir)/m4/atomic_operations_64bit.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)"
+LTLIBRARIES = $(pkglib_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+imhiredis_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_imhiredis_la_OBJECTS = imhiredis_la-imhiredis.lo
+imhiredis_la_OBJECTS = $(am_imhiredis_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+imhiredis_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(imhiredis_la_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/imhiredis_la-imhiredis.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(imhiredis_la_SOURCES)
+DIST_SOURCES = $(imhiredis_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp COPYING \
+ README
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APU_CFLAGS = @APU_CFLAGS@
+APU_LIBS = @APU_LIBS@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CIVETWEB_LIBS = @CIVETWEB_LIBS@
+CONF_FILE_PATH = @CONF_FILE_PATH@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CURL_CFLAGS = @CURL_CFLAGS@
+CURL_LIBS = @CURL_LIBS@
+CYGPATH_W = @CYGPATH_W@
+CZMQ_CFLAGS = @CZMQ_CFLAGS@
+CZMQ_LIBS = @CZMQ_LIBS@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DL_LIBS = @DL_LIBS@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FAUP_LIBS = @FAUP_LIBS@
+FGREP = @FGREP@
+GLIB_CFLAGS = @GLIB_CFLAGS@
+GLIB_LIBS = @GLIB_LIBS@
+GNUTLS_CFLAGS = @GNUTLS_CFLAGS@
+GNUTLS_LIBS = @GNUTLS_LIBS@
+GREP = @GREP@
+GSS_LIBS = @GSS_LIBS@
+GT_KSI_LS12_CFLAGS = @GT_KSI_LS12_CFLAGS@
+GT_KSI_LS12_LIBS = @GT_KSI_LS12_LIBS@
+HASH_XXHASH_LIBS = @HASH_XXHASH_LIBS@
+HIREDIS_CFLAGS = @HIREDIS_CFLAGS@
+HIREDIS_LIBS = @HIREDIS_LIBS@
+IMUDP_LIBS = @IMUDP_LIBS@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+IP = @IP@
+JAVA = @JAVA@
+JAVAC = @JAVAC@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LEX = @LEX@
+LEXLIB = @LEXLIB@
+LEX_OUTPUT_ROOT = @LEX_OUTPUT_ROOT@
+LIBCAPNG_CFLAGS = @LIBCAPNG_CFLAGS@
+LIBCAPNG_LIBS = @LIBCAPNG_LIBS@
+LIBCAPNG_PRESENT_CFLAGS = @LIBCAPNG_PRESENT_CFLAGS@
+LIBCAPNG_PRESENT_LIBS = @LIBCAPNG_PRESENT_LIBS@
+LIBDBI_CFLAGS = @LIBDBI_CFLAGS@
+LIBDBI_LIBS = @LIBDBI_LIBS@
+LIBESTR_CFLAGS = @LIBESTR_CFLAGS@
+LIBESTR_LIBS = @LIBESTR_LIBS@
+LIBEVENT_CFLAGS = @LIBEVENT_CFLAGS@
+LIBEVENT_LIBS = @LIBEVENT_LIBS@
+LIBFASTJSON_CFLAGS = @LIBFASTJSON_CFLAGS@
+LIBFASTJSON_LIBS = @LIBFASTJSON_LIBS@
+LIBGCRYPT_CFLAGS = @LIBGCRYPT_CFLAGS@
+LIBGCRYPT_CONFIG = @LIBGCRYPT_CONFIG@
+LIBGCRYPT_LIBS = @LIBGCRYPT_LIBS@
+LIBLOGGING_CFLAGS = @LIBLOGGING_CFLAGS@
+LIBLOGGING_LIBS = @LIBLOGGING_LIBS@
+LIBLOGGING_STDLOG_CFLAGS = @LIBLOGGING_STDLOG_CFLAGS@
+LIBLOGGING_STDLOG_LIBS = @LIBLOGGING_STDLOG_LIBS@
+LIBLOGNORM_CFLAGS = @LIBLOGNORM_CFLAGS@
+LIBLOGNORM_LIBS = @LIBLOGNORM_LIBS@
+LIBLZ4_CFLAGS = @LIBLZ4_CFLAGS@
+LIBLZ4_LIBS = @LIBLZ4_LIBS@
+LIBM = @LIBM@
+LIBMONGOC_CFLAGS = @LIBMONGOC_CFLAGS@
+LIBMONGOC_LIBS = @LIBMONGOC_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBRDKAFKA_CFLAGS = @LIBRDKAFKA_CFLAGS@
+LIBRDKAFKA_LIBS = @LIBRDKAFKA_LIBS@
+LIBS = @LIBS@
+LIBSYSTEMD_CFLAGS = @LIBSYSTEMD_CFLAGS@
+LIBSYSTEMD_JOURNAL_CFLAGS = @LIBSYSTEMD_JOURNAL_CFLAGS@
+LIBSYSTEMD_JOURNAL_LIBS = @LIBSYSTEMD_JOURNAL_LIBS@
+LIBSYSTEMD_LIBS = @LIBSYSTEMD_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUUID_CFLAGS = @LIBUUID_CFLAGS@
+LIBUUID_LIBS = @LIBUUID_LIBS@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OPENSSL_CFLAGS = @OPENSSL_CFLAGS@
+OPENSSL_LIBS = @OPENSSL_LIBS@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PID_FILE_PATH = @PID_FILE_PATH@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+PROTON_CFLAGS = @PROTON_CFLAGS@
+PROTON_LIBS = @PROTON_LIBS@
+PROTON_PROACTOR_CFLAGS = @PROTON_PROACTOR_CFLAGS@
+PROTON_PROACTOR_LIBS = @PROTON_PROACTOR_LIBS@
+PTHREADS_CFLAGS = @PTHREADS_CFLAGS@
+PTHREADS_LIBS = @PTHREADS_LIBS@
+PYTHON = @PYTHON@
+PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
+PYTHON_PLATFORM = @PYTHON_PLATFORM@
+PYTHON_PREFIX = @PYTHON_PREFIX@
+PYTHON_VERSION = @PYTHON_VERSION@
+RABBITMQ_CFLAGS = @RABBITMQ_CFLAGS@
+RABBITMQ_LIBS = @RABBITMQ_LIBS@
+RANLIB = @RANLIB@
+READLINK = @READLINK@
+REDIS = @REDIS@
+RELP_CFLAGS = @RELP_CFLAGS@
+RELP_LIBS = @RELP_LIBS@
+RSRT_CFLAGS = @RSRT_CFLAGS@
+RSRT_CFLAGS1 = @RSRT_CFLAGS1@
+RSRT_LIBS = @RSRT_LIBS@
+RSRT_LIBS1 = @RSRT_LIBS1@
+RST2MAN = @RST2MAN@
+RT_LIBS = @RT_LIBS@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SNMP_CFLAGS = @SNMP_CFLAGS@
+SNMP_LIBS = @SNMP_LIBS@
+SOL_LIBS = @SOL_LIBS@
+STRIP = @STRIP@
+TCL_BIN_DIR = @TCL_BIN_DIR@
+TCL_INCLUDE_SPEC = @TCL_INCLUDE_SPEC@
+TCL_LIB_FILE = @TCL_LIB_FILE@
+TCL_LIB_FLAG = @TCL_LIB_FLAG@
+TCL_LIB_SPEC = @TCL_LIB_SPEC@
+TCL_PATCH_LEVEL = @TCL_PATCH_LEVEL@
+TCL_SRC_DIR = @TCL_SRC_DIR@
+TCL_STUB_LIB_FILE = @TCL_STUB_LIB_FILE@
+TCL_STUB_LIB_FLAG = @TCL_STUB_LIB_FLAG@
+TCL_STUB_LIB_SPEC = @TCL_STUB_LIB_SPEC@
+TCL_VERSION = @TCL_VERSION@
+UDPSPOOF_CFLAGS = @UDPSPOOF_CFLAGS@
+UDPSPOOF_LIBS = @UDPSPOOF_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+WARN_CFLAGS = @WARN_CFLAGS@
+WARN_LDFLAGS = @WARN_LDFLAGS@
+WARN_SCANNERFLAGS = @WARN_SCANNERFLAGS@
+WGET = @WGET@
+YACC = @YACC@
+YACC_FOUND = @YACC_FOUND@
+YFLAGS = @YFLAGS@
+ZLIB_CFLAGS = @ZLIB_CFLAGS@
+ZLIB_LIBS = @ZLIB_LIBS@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moddirs = @moddirs@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+pkgpyexecdir = @pkgpyexecdir@
+pkgpythondir = @pkgpythondir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+pyexecdir = @pyexecdir@
+pythondir = @pythondir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+pkglib_LTLIBRARIES = imhiredis.la
+imhiredis_la_SOURCES = imhiredis.c
+imhiredis_la_CPPFLAGS = $(RSRT_CFLAGS) $(PTHREADS_CFLAGS) $(HIREDIS_CFLAGS)
+imhiredis_la_LDFLAGS = -module -avoid-version
+imhiredis_la_LIBADD = $(HIREDIS_LIBS)
+EXTRA_DIST =
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu contrib/imhiredis/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --gnu contrib/imhiredis/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+imhiredis.la: $(imhiredis_la_OBJECTS) $(imhiredis_la_DEPENDENCIES) $(EXTRA_imhiredis_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(imhiredis_la_LINK) -rpath $(pkglibdir) $(imhiredis_la_OBJECTS) $(imhiredis_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imhiredis_la-imhiredis.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)depbase=`echo $@ | sed 's|[^/]*$$|$(DEPDIR)/&|;s|\.o$$||'`;\
+@am__fastdepCC_TRUE@ $(COMPILE) -MT $@ -MD -MP -MF $$depbase.Tpo -c -o $@ $< &&\
+@am__fastdepCC_TRUE@ $(am__mv) $$depbase.Tpo $$depbase.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)depbase=`echo $@ | sed 's|[^/]*$$|$(DEPDIR)/&|;s|\.obj$$||'`;\
+@am__fastdepCC_TRUE@ $(COMPILE) -MT $@ -MD -MP -MF $$depbase.Tpo -c -o $@ `$(CYGPATH_W) '$<'` &&\
+@am__fastdepCC_TRUE@ $(am__mv) $$depbase.Tpo $$depbase.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)depbase=`echo $@ | sed 's|[^/]*$$|$(DEPDIR)/&|;s|\.lo$$||'`;\
+@am__fastdepCC_TRUE@ $(LTCOMPILE) -MT $@ -MD -MP -MF $$depbase.Tpo -c -o $@ $< &&\
+@am__fastdepCC_TRUE@ $(am__mv) $$depbase.Tpo $$depbase.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+imhiredis_la-imhiredis.lo: imhiredis.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imhiredis_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT imhiredis_la-imhiredis.lo -MD -MP -MF $(DEPDIR)/imhiredis_la-imhiredis.Tpo -c -o imhiredis_la-imhiredis.lo `test -f 'imhiredis.c' || echo '$(srcdir)/'`imhiredis.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/imhiredis_la-imhiredis.Tpo $(DEPDIR)/imhiredis_la-imhiredis.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='imhiredis.c' object='imhiredis_la-imhiredis.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(imhiredis_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o imhiredis_la-imhiredis.lo `test -f 'imhiredis.c' || echo '$(srcdir)/'`imhiredis.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-pkglibLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imhiredis_la-imhiredis.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/imhiredis_la-imhiredis.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkglibLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-pkglibLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkglibLTLIBRARIES install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/contrib/imhiredis/README b/contrib/imhiredis/README
new file mode 100644
index 0000000..70a9b31
--- /dev/null
+++ b/contrib/imhiredis/README
@@ -0,0 +1,81 @@
+Redis Input Plugin using hiredis library
+
+REQUIREMENTS:
+
+* hiredis ( https://github.com/redis/hiredis.git )
+
+USAGE:
+
+This plugin has two current "modes" that it supports:
+
+1. "queue"
+The queue mode will LPOP or RPOP your message from a redis list.
+Following parameters are required:
+ - mode: Set mode to "queue" to enable the queue mode
+ - key: The key to xPOP on
+ - server: The name or IP address of the redis server
+ - port: The redis listening port
+
+Following parameters are optional:
+ - password: If set, the plugin will issue an "AUTH" command before calling xPOP
+ - uselpop: If set to "1", LPOP will be used instead of default RPOP
+
+Redis pipelining is used inside the worker thread. The dequeue batch size is configured with the "batchsize" parameter (default is 10).
+
+Imhiredis will query Redis every second to see if entries are in the list, if that's the case they will be dequeued
+continuously by batches of "batchsize elements" until none remains.
+
+Due to its balance between polling interval and pipelining and its use of lists, this mode is quite performant and reliable.
+However, due to the 1 second polling frequency, one may consider using the `subscribe` mode instead if very low latency is required.
+
+```
+module(load="imhiredis")
+
+input(
+ type="imhiredis"
+ mode="queue"
+ key="vulture"
+ server="127.0.0.1"
+ port="6379"
+ uselpop="1"
+ password="foobar"
+ batchsize="10"
+)
+```
+
+
+
+2. "subscribe"
+The subscribe mode will SUBSCRIBE to a redis channel. The "key"
+parameter is required and will be used for the subscribe channel.
+
+Following parameters are required:
+ - mode: Set mode to "subscribe" to enable the subscribe mode
+ - key: The key to subscribe to (aka the "channel")
+ - server: The name or IP address of the redis server
+ - port: The redis listening port
+
+Following parameters are optional:
+ - password: If set, the plugin will issue an "AUTH" command before listening to a channel
+ - uselpop: If set to "1", LPOP will be used instead of default RPOP
+
+
+```
+module(load="imhiredis")
+
+input(
+ type="imhiredis"
+ mode="subscribe"
+ key="vulture"
+ server="127.0.0.1"
+ port="6379"
+ password="foobar"
+ batchsize="10"
+)
+```
+
+
+TODO
+* TLS support
+
+
diff --git a/contrib/imhiredis/imhiredis.c b/contrib/imhiredis/imhiredis.c
new file mode 100644
index 0000000..e1a8e1c
--- /dev/null
+++ b/contrib/imhiredis/imhiredis.c
@@ -0,0 +1,2298 @@
+/* imhiredis.c
+* Copyright 2021 aDvens
+*
+* This file is contrib for rsyslog.
+* This input plugin is a log consumer from REDIS
+* See README for doc
+*
+*
+* This program is free software: you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public License
+* as published by the Free Software Foundation, either version 3 of
+* the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful, but
+* WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public
+* License along with this program. If not, see
+* <http://www.gnu.org/licenses/>.
+*
+* Author: Jérémie Jourdin
+* <jeremie.jourdin@advens.fr>
+*/
+
+#include "config.h"
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sys/uio.h>
+#include <hiredis/hiredis.h>
+#include <hiredis/async.h>
+#include <hiredis/adapters/libevent.h>
+#include <event2/thread.h>
+
+#include "rsyslog.h"
+#include "conf.h"
+#include "syslogd-types.h"
+#include "srUtils.h"
+#include "template.h"
+#include "module-template.h"
+#include "errmsg.h"
+#include "atomic.h"
+#include "statsobj.h"
+#include "unicode-helper.h"
+#include "prop.h"
+#include "ruleset.h"
+#include "glbl.h"
+#include "cfsysline.h"
+#include "msg.h"
+#include "dirty.h"
+
+MODULE_TYPE_INPUT
+MODULE_TYPE_NOKEEP
+MODULE_CNFNAME("imhiredis")
+
+/* static data */
+DEF_IMOD_STATIC_DATA
+#define BATCH_SIZE 10
+#define WAIT_TIME_MS 500
+#define STREAM_INDEX_STR_MAXLEN 44 // "18446744073709551615-18446744073709551615"
+#define IMHIREDIS_MODE_QUEUE 1
+#define IMHIREDIS_MODE_SUBSCRIBE 2
+#define IMHIREDIS_MODE_STREAM 3
+DEFobjCurrIf(prop)
+DEFobjCurrIf(ruleset)
+DEFobjCurrIf(glbl)
+DEFobjCurrIf(statsobj)
+
+
+typedef struct redisNode_s {
+ sbool isMaster;
+ sbool usesSocket;
+ uchar *socketPath;
+ uchar *server;
+ int port;
+ struct redisNode_s *next;
+} redisNode;
+
+
+struct instanceConf_s {
+ uchar *password;
+ uchar *key;
+ uchar *modeDescription;
+ uchar *streamConsumerGroup;
+ uchar *streamConsumerName;
+ uchar *streamReadFrom;
+ int streamAutoclaimIdleTime;
+ sbool streamConsumerACK;
+ int mode;
+ uint batchsize;
+ sbool useLPop;
+
+ struct {
+ int nmemb;
+ char **name;
+ char **varname;
+ } fieldList;
+
+ ruleset_t *pBindRuleset; /* ruleset to bind listener to (use system default if unspecified) */
+ uchar *pszBindRuleset; /* default name of Ruleset to bind to */
+
+ redisContext *conn;
+ redisAsyncContext *aconn;
+ struct event_base *evtBase;
+
+ redisNode *currentNode; /* currently used redis node, can be any of the nodes in the redisNodesList list */
+ /* the list of seen nodes
+ * the preferred node (the one from configuration) will always be the first element
+ * second one is a master (if preferred one is unavailable/replica) or a replica, others are replicas
+ * the preferred node may appear twice, but it is accepted
+ */
+ redisNode *redisNodesList;
+
+ struct instanceConf_s *next;
+};
+
+
+struct modConfData_s {
+ rsconf_t *pConf; /* our overall config object */
+ instanceConf_t *root, *tail;
+};
+
+/* The following structure controls the worker threads. Global data is
+ * needed for their access.
+ */
+static struct imhiredisWrkrInfo_s {
+ pthread_t tid; /* the worker's thread ID */
+ instanceConf_t *inst; /* Pointer to imhiredis instance */
+ rsRetVal (*fnConnectMaster)(instanceConf_t *inst);
+ sbool (*fnIsConnected)(instanceConf_t *inst);
+ rsRetVal (*fnRun)(instanceConf_t *inst);
+} *imhiredisWrkrInfo;
+
+
+/* GLOBAL DATA */
+pthread_attr_t wrkrThrdAttr; /* Attribute for worker threads ; read only after startup */
+
+static int activeHiredisworkers = 0;
+static char *REDIS_REPLIES[] = {
+ "unknown", // 0
+ "string", // 1
+ "array", // 2
+ "integer", // 3
+ "nil", // 4
+ "status", // 5
+ "error", // 6
+ "double", // 7
+ "bool", // 8
+ "map", // 9
+ "set", // 10
+ "attr", // 11
+ "push", // 12
+ "bignum", // 13
+ "verb", // 14
+ };
+
+static modConfData_t *loadModConf = NULL;/* modConf ptr to use for the current load process */
+static modConfData_t *runModConf = NULL;/* modConf ptr to use for the current load process */
+
+static prop_t *pInputName = NULL;
+/* there is only one global inputName for all messages generated by this input */
+
+
+/* module-global parameters */
+static struct cnfparamdescr modpdescr[] = {};
+static struct cnfparamblk modpblk =
+ { CNFPARAMBLK_VERSION,
+ sizeof(modpdescr)/sizeof(struct cnfparamdescr),
+ modpdescr
+ };
+
+/* input instance parameters */
+static struct cnfparamdescr inppdescr[] = {
+ { "socketPath", eCmdHdlrGetWord, 0 },
+ { "server", eCmdHdlrGetWord, 0 },
+ { "port", eCmdHdlrInt, 0 },
+ { "password", eCmdHdlrGetWord, 0 },
+ { "mode", eCmdHdlrGetWord, 0 },
+ { "batchsize", eCmdHdlrInt, 0 },
+ { "key", eCmdHdlrGetWord, CNFPARAM_REQUIRED },
+ { "uselpop", eCmdHdlrBinary, 0 },
+ { "ruleset", eCmdHdlrString, 0 },
+ { "stream.consumerGroup", eCmdHdlrGetWord, 0 },
+ { "stream.consumerName", eCmdHdlrGetWord, 0 },
+ { "stream.readFrom", eCmdHdlrGetWord, 0 },
+ { "stream.consumerACK", eCmdHdlrBinary, 0 },
+ { "stream.autoclaimIdleTime", eCmdHdlrNonNegInt, 0 },
+ { "fields", eCmdHdlrArray, 0 },
+};
+static struct cnfparamblk inppblk =
+ { CNFPARAMBLK_VERSION,
+ sizeof(inppdescr)/sizeof(struct cnfparamdescr),
+ inppdescr
+ };
+
+struct timeval glblRedisConnectTimeout = { 3, 0 }; /* 3 seconds */
+
+
+#include "im-helper.h" /* must be included AFTER the type definitions! */
+
+
+/* forward references */
+static void redisAsyncRecvCallback (redisAsyncContext __attribute__((unused)) *c, void *reply, void *inst_obj);
+static void redisAsyncConnectCallback (const redisAsyncContext *c, int status);
+static void redisAsyncDisconnectCallback (const redisAsyncContext *c, int status);
+static struct json_object* _redisParseIntegerReply(const redisReply *reply);
+static struct json_object* _redisParseStringReply(const redisReply *reply);
+static struct json_object* _redisParseArrayReply(const redisReply *reply);
+#ifdef REDIS_REPLY_DOUBLE
+static struct json_object* _redisParseDoubleReply(const redisReply *reply);
+#endif
+static rsRetVal enqMsg(instanceConf_t *const inst, const char *message, size_t msgLen);
+static rsRetVal enqMsgJson(instanceConf_t *const inst, struct json_object *json, struct json_object *metadata);
+rsRetVal redisAuthentSynchronous(redisContext *conn, uchar *password);
+rsRetVal redisAuthentAsynchronous(redisAsyncContext *aconn, uchar *password);
+rsRetVal redisActualizeCurrentNode(instanceConf_t *inst);
+rsRetVal redisGetServersList(redisNode *node, uchar *password, redisNode **result);
+rsRetVal redisAuthenticate(instanceConf_t *inst);
+rsRetVal redisConnectSync(redisContext **conn, redisNode *node);
+rsRetVal connectMasterSync(instanceConf_t *inst);
+static sbool isConnectedSync(instanceConf_t *inst);
+rsRetVal redisConnectAsync(redisAsyncContext **aconn, redisNode *node);
+rsRetVal connectMasterAsync(instanceConf_t *inst);
+static sbool isConnectedAsync(instanceConf_t *inst);
+rsRetVal redisDequeue(instanceConf_t *inst);
+rsRetVal ensureConsumerGroupCreated(instanceConf_t *inst);
+rsRetVal ackStreamIndex(instanceConf_t *inst, uchar *stream, uchar *group, uchar *index);
+static rsRetVal enqueueRedisStreamReply(instanceConf_t *const inst, redisReply *reply);
+static rsRetVal handleRedisXREADReply(instanceConf_t *const inst, const redisReply *reply);
+static rsRetVal handleRedisXAUTOCLAIMReply(
+ instanceConf_t *const inst, const redisReply *reply, char **autoclaimIndex);
+rsRetVal redisStreamRead(instanceConf_t *inst);
+rsRetVal redisSubscribe(instanceConf_t *inst);
+void workerLoop(struct imhiredisWrkrInfo_s *me);
+static void *imhirediswrkr(void *myself);
+static rsRetVal createRedisNode(redisNode **root);
+rsRetVal copyNode(redisNode *src, redisNode **dst);
+redisNode *freeNode(redisNode *node);
+void insertNodeAfter(redisNode *root, redisNode *elem);
+void dbgPrintNode(redisNode *node);
+
+
+/* create input instance, set default parameters, and
+ * add it to the list of instances.
+ */
+static rsRetVal
+createInstance(instanceConf_t **pinst)
+{
+ DEFiRet;
+ instanceConf_t *inst;
+ CHKmalloc(inst = calloc(1, sizeof(instanceConf_t)));
+
+ inst->next = NULL;
+ inst->password = NULL;
+ inst->key = NULL;
+ inst->mode = 0;
+ inst->batchsize = 0;
+ inst->useLPop = 0;
+ inst->streamConsumerGroup = NULL;
+ inst->streamConsumerName = NULL;
+ CHKmalloc(inst->streamReadFrom = calloc(1, STREAM_INDEX_STR_MAXLEN));
+ inst->streamAutoclaimIdleTime = 0;
+ inst->streamConsumerACK = 1;
+ inst->pszBindRuleset = NULL;
+ inst->pBindRuleset = NULL;
+ inst->fieldList.nmemb = 0;
+
+ /* Redis objects */
+ inst->conn = NULL;
+ inst->aconn = NULL;
+
+ /* redis nodes list */
+ CHKiRet(createRedisNode(&(inst->redisNodesList)));
+ inst->currentNode = inst->redisNodesList;
+
+ /* libevent base for async connection */
+ inst->evtBase = NULL;
+
+ /* node created, let's add to config */
+ if(loadModConf->tail == NULL) {
+ loadModConf->tail = loadModConf->root = inst;
+ } else {
+ loadModConf->tail->next = inst;
+ loadModConf->tail = inst;
+ }
+
+ *pinst = inst;
+finalize_it:
+ RETiRet;
+}
+
+/* this function checks instance parameters and does some required pre-processing
+ */
+static rsRetVal ATTR_NONNULL()
+checkInstance(instanceConf_t *const inst)
+{
+ DEFiRet;
+ /* first node should be created from configuration */
+ assert(inst->redisNodesList != NULL);
+
+ /* check and print redis connection settings */
+ if (inst->redisNodesList->server != NULL && inst->redisNodesList->socketPath != NULL) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: both 'server' and 'socketPath' are given, "
+ "ignoring 'socketPath'.");
+ free(inst->redisNodesList->socketPath);
+ inst->redisNodesList->socketPath = NULL;
+ }
+
+ if(inst->redisNodesList->server != NULL && inst->redisNodesList->server[0] != '\0') {
+ if (inst->redisNodesList->port == 0) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "imhiredis: port not set, setting default 6379");
+ inst->redisNodesList->port = 6379;
+ }
+ DBGPRINTF("imhiredis: preferred server is %s (%d)\n",
+ inst->redisNodesList->server,
+ inst->redisNodesList->port);
+ inst->redisNodesList->usesSocket = 0;
+ }
+ else if(inst->redisNodesList->socketPath != NULL && inst->redisNodesList->socketPath[0] != '\0') {
+ DBGPRINTF("imhiredis: preferred server is %s\n",
+ inst->redisNodesList->socketPath);
+ inst->redisNodesList->usesSocket = 1;
+ } else {
+ LogError(0, RS_RET_CONFIG_ERROR, "imhiredis: neither 'server' nor 'socketPath' are defined!");
+ ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
+ }
+
+
+ if (inst->mode < IMHIREDIS_MODE_QUEUE || inst->mode > IMHIREDIS_MODE_STREAM) {
+ LogError(0, RS_RET_CONFIG_ERROR, "imhiredis: invalid mode, please choose 'subscribe', "
+ "'queue' or 'stream' mode.");
+ ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
+ }
+
+ if (inst->mode != IMHIREDIS_MODE_QUEUE && inst->useLPop) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'uselpop' set with mode != queue : ignored.");
+ }
+
+ if (inst->mode == IMHIREDIS_MODE_STREAM) {
+ if(inst->streamConsumerGroup != NULL && inst->streamConsumerName == NULL) {
+ LogError(0, RS_RET_CONFIG_ERROR, "imhiredis: invalid configuration, "
+ "please set a consumer name when mode is 'stream' and a consumer group is set");
+ ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
+ }
+ if(inst->streamAutoclaimIdleTime != 0 && inst->streamConsumerGroup == NULL) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'stream.autoclaimIdleTime' "
+ "set with no consumer group set : ignored.");
+ }
+ if(inst->streamReadFrom[0] == '\0') {
+ inst->streamReadFrom[0] = '$';
+ }
+ } else {
+ if (inst->streamConsumerGroup != NULL) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'stream.consumerGroup' "
+ "set with mode != stream : ignored.");
+ }
+ if (inst->streamConsumerName != NULL) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'stream.consumerName' "
+ "set with mode != stream : ignored.");
+ }
+ if (inst->streamAutoclaimIdleTime != 0) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'stream.autoclaimIdleTime' "
+ "set with mode != stream : ignored.");
+ }
+ if (inst->streamConsumerACK == 0) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'stream.consumerACK' "
+ "disabled with mode != stream : ignored.");
+ }
+ if (inst->fieldList.nmemb > 0) {
+ LogMsg(0, RS_RET_CONFIG_ERROR, LOG_WARNING,"imhiredis: 'fields' "
+ "unused for mode != stream : ignored.");
+ }
+ }
+
+ if (inst->batchsize !=0 ) {
+ DBGPRINTF("imhiredis: batchsize is '%d'\n", inst->batchsize);
+ }
+ else {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING,
+ "imhiredis: batchsize not set, setting to default (%d)",BATCH_SIZE);
+ inst->batchsize=BATCH_SIZE;
+ }
+
+ if (inst->password != NULL) {
+ DBGPRINTF("imhiredis: password is '%s'\n", inst->password);
+ }
+
+ // set default current node as first node in list (preferred node)
+ inst->currentNode = inst->redisNodesList;
+ // search master node (should be either preferred node or its master)
+ if (RS_RET_OK != redisActualizeCurrentNode(inst) || inst->currentNode == NULL) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_WARNING, "imhiredis: could not connect to a valid master!");
+ }
+
+finalize_it:
+ RETiRet;
+}
+
+/* function to generate an error message if the ruleset cannot be found */
+static inline void
+std_checkRuleset_genErrMsg(__attribute__((unused)) modConfData_t *modConf, instanceConf_t *inst)
+{
+ LogError(0, NO_ERRCODE, "imhiredis: ruleset '%s' not found - "
+ "using default ruleset instead",
+ inst->pszBindRuleset);
+}
+
+
+BEGINnewInpInst
+ struct cnfparamvals *pvals;
+ instanceConf_t *inst;
+ int i;
+CODESTARTnewInpInst
+ DBGPRINTF("newInpInst (imhiredis)\n");
+
+ if((pvals = nvlstGetParams(lst, &inppblk, NULL)) == NULL) {
+ ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
+ }
+
+ if(Debug) {
+ dbgprintf("input param blk in imhiredis:\n");
+ cnfparamsPrint(&inppblk, pvals);
+ }
+
+ CHKiRet(createInstance(&inst));
+ for(i = 0 ; i < inppblk.nParams ; ++i) {
+ if(!pvals[i].bUsed)
+ continue;
+
+ if(!strcmp(inppblk.descr[i].name, "server")) {
+ inst->redisNodesList->server = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "socketPath")) {
+ inst->redisNodesList->socketPath = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "ruleset")) {
+ inst->pszBindRuleset = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "port")) {
+ inst->redisNodesList->port = (int) pvals[i].val.d.n;
+ } else if(!strcmp(inppblk.descr[i].name, "password")) {
+ inst->password = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "stream.consumerGroup")) {
+ inst->streamConsumerGroup = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "stream.consumerName")) {
+ inst->streamConsumerName = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else if(!strcmp(inppblk.descr[i].name, "stream.consumerACK")) {
+ inst->streamConsumerACK = pvals[i].val.d.n;
+ } else if(!strcmp(inppblk.descr[i].name, "stream.readFrom")) {
+ // inst->streamReadFrom is already allocated, only copy contents
+ memcpy(inst->streamReadFrom,
+ es_getBufAddr(pvals[i].val.d.estr),
+ es_strlen(pvals[i].val.d.estr));
+ inst->streamReadFrom[es_strlen(pvals[i].val.d.estr)] = '\0';
+ } else if(!strcmp(inppblk.descr[i].name, "stream.autoclaimIdleTime")) {
+ inst->streamAutoclaimIdleTime = pvals[i].val.d.n;
+ } else if(!strcmp(inppblk.descr[i].name, "uselpop")) {
+ inst->useLPop = pvals[i].val.d.n;
+ } else if(!strcmp(inppblk.descr[i].name, "mode")) {
+ inst->modeDescription = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ if (!strcmp((const char*)inst->modeDescription, "queue")) {
+ inst->mode = IMHIREDIS_MODE_QUEUE;
+ } else if (!strcmp((const char*)inst->modeDescription, "subscribe")) {
+ inst->mode = IMHIREDIS_MODE_SUBSCRIBE;
+ } else if (!strcmp((const char*)inst->modeDescription, "stream")) {
+ inst->mode = IMHIREDIS_MODE_STREAM;
+ } else {
+ LogMsg(0, RS_RET_PARAM_ERROR, LOG_ERR, "imhiredis: unsupported mode "
+ "'%s'", inst->modeDescription);
+ ABORT_FINALIZE(RS_RET_PARAM_ERROR);
+ }
+ } else if (!strcmp(inppblk.descr[i].name, "fields")) {
+ inst->fieldList.nmemb = pvals[i].val.d.ar->nmemb;
+ CHKmalloc(inst->fieldList.name = calloc(inst->fieldList.nmemb, sizeof(char *)));
+ CHKmalloc(inst->fieldList.varname = calloc(inst->fieldList.nmemb, sizeof(char *)));
+ for (int j = 0; j < pvals[i].val.d.ar->nmemb; ++j) {
+ char *const param = es_str2cstr(pvals[i].val.d.ar->arr[j], NULL);
+ char *varname = NULL;
+ char *name;
+ if(*param == ':') {
+ char *b = strchr(param+1, ':');
+ if(b == NULL) {
+ LogMsg(0, RS_RET_PARAM_ERROR, LOG_ERR,
+ "imhiredis: missing closing colon: '%s'", param);
+ ABORT_FINALIZE(RS_RET_ERR);
+ }
+ *b = '\0'; /* split name & varname */
+ varname = param+1;
+ name = b+1;
+ } else {
+ name = param;
+ }
+ CHKmalloc(inst->fieldList.name[j] = strdup(name));
+ char vnamebuf[1024];
+ snprintf(vnamebuf, sizeof(vnamebuf),
+ "!%s", (varname == NULL) ? name : varname);
+ CHKmalloc(inst->fieldList.varname[j] = strdup(vnamebuf));
+ DBGPRINTF("will map '%s' to '%s'\n",
+ inst->fieldList.name[j],
+ inst->fieldList.varname[j]);
+ free(param);
+ }
+ } else if(!strcmp(inppblk.descr[i].name, "batchsize")) {
+ inst->batchsize = (int) pvals[i].val.d.n;
+ } else if(!strcmp(inppblk.descr[i].name, "key")) {
+ inst->key = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL);
+ } else {
+ dbgprintf("imhiredis: program error, non-handled "
+ "param '%s'\n", inppblk.descr[i].name);
+ }
+ }
+
+ DBGPRINTF("imhiredis: checking config sanity\n");
+ if (inst->modeDescription == NULL) {
+ CHKmalloc(inst->modeDescription = (uchar*)strdup("subscribe"));
+ inst->mode = IMHIREDIS_MODE_SUBSCRIBE;
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "imhiredis: \"mode\" parameter not specified "
+ "using default redis 'subscribe' mode -- this may not be what you want!");
+ }
+ if (inst->key == NULL) {
+ LogMsg(0, RS_RET_PARAM_ERROR, LOG_ERR, "imhiredis: \"key\" required parameter not specified!");
+ ABORT_FINALIZE(RS_RET_PARAM_ERROR);
+ }
+ if(inst->redisNodesList->server == NULL && inst->redisNodesList->socketPath == NULL) {
+ CHKmalloc(inst->redisNodesList->server = (uchar *)strdup("127.0.0.1"));
+ inst->redisNodesList->port = 6379;
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "imhiredis: no server parameter specified "
+ "using default 127.0.0.1:6379 -- this may not be what you want!");
+ }
+ if (inst->password == NULL) {
+ LogMsg(0, RS_RET_OK, LOG_INFO, "imhiredis: no password specified");
+ }
+
+ DBGPRINTF("imhiredis: newInpInst key=%s, mode=%s, uselpop=%d\n",
+ inst->key, inst->modeDescription, inst->useLPop);
+
+finalize_it:
+CODE_STD_FINALIZERnewInpInst
+ cnfparamvalsDestruct(pvals, &inppblk);
+ENDnewInpInst
+
+
+BEGINbeginCnfLoad
+CODESTARTbeginCnfLoad
+ loadModConf = pModConf;
+ pModConf->pConf = pConf;
+ENDbeginCnfLoad
+
+
+BEGINsetModCnf
+ struct cnfparamvals *pvals = NULL;
+ int i;
+CODESTARTsetModCnf
+ pvals = nvlstGetParams(lst, &modpblk, NULL);
+ if(pvals == NULL) {
+ LogError(0, RS_RET_MISSING_CNFPARAMS, "imhiredis: error processing module "
+ "config parameters [module(...)]");
+ ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
+ }
+
+ if(Debug) {
+ dbgprintf("module (global) param blk for imhiredis:\n");
+ cnfparamsPrint(&modpblk, pvals);
+ }
+
+ for(i = 0 ; i < modpblk.nParams ; ++i) {
+ if(!pvals[i].bUsed) {
+ continue;
+ } else {
+ dbgprintf("imhiredis: program error, non-handled "
+ "param '%s' in beginCnfLoad\n", modpblk.descr[i].name);
+ }
+ }
+finalize_it:
+ if(pvals != NULL)
+ cnfparamvalsDestruct(pvals, &modpblk);
+ENDsetModCnf
+
+BEGINendCnfLoad
+CODESTARTendCnfLoad
+ENDendCnfLoad
+
+BEGINcheckCnf
+ instanceConf_t *inst;
+CODESTARTcheckCnf
+ for(inst = pModConf->root ; inst != NULL ; inst = inst->next) {
+ std_checkRuleset(pModConf, inst);
+ }
+ENDcheckCnf
+
+
+BEGINactivateCnfPrePrivDrop
+CODESTARTactivateCnfPrePrivDrop
+ runModConf = pModConf;
+ENDactivateCnfPrePrivDrop
+
+BEGINactivateCnf
+CODESTARTactivateCnf
+ for(instanceConf_t *inst = pModConf->root ; inst != NULL ; inst = inst->next) {
+ iRet = checkInstance(inst);
+ if (inst->mode == IMHIREDIS_MODE_SUBSCRIBE)
+ inst->evtBase = event_base_new();
+ }
+ENDactivateCnf
+
+
+BEGINfreeCnf
+ instanceConf_t *inst, *del;
+ redisNode *node;
+CODESTARTfreeCnf
+ for(inst = pModConf->root ; inst != NULL ; ) {
+ if (inst->evtBase)
+ event_base_free(inst->evtBase);
+ if (inst->password != NULL)
+ free(inst->password);
+ free(inst->modeDescription);
+ free(inst->key);
+ if(inst->streamConsumerGroup != NULL)
+ free(inst->streamConsumerGroup);
+ if(inst->streamConsumerName != NULL)
+ free(inst->streamConsumerName);
+ free(inst->streamReadFrom);
+ free(inst->pszBindRuleset);
+ if(inst->fieldList.name != NULL) {
+ for(int i = 0 ; i < inst->fieldList.nmemb ; ++i) {
+ free(inst->fieldList.name[i]);
+ free(inst->fieldList.varname[i]);
+ }
+ free(inst->fieldList.name);
+ free(inst->fieldList.varname);
+ }
+ if(inst->conn != NULL) {
+ redisFree(inst->conn);
+ inst->conn = NULL;
+ }
+ if(inst->aconn != NULL) {
+ redisAsyncFree(inst->aconn);
+ inst->aconn = NULL;
+ }
+
+ for (node = inst->redisNodesList; node != NULL; node = freeNode(node)) {;}
+
+ del = inst;
+ inst = inst->next;
+ free(del);
+ }
+ENDfreeCnf
+
+
+/* Cleanup imhiredis worker threads */
+static void
+shutdownImhiredisWorkers(void)
+{
+ int i;
+ instanceConf_t *inst;
+
+ assert(imhiredisWrkrInfo != NULL);
+
+ for(inst = runModConf->root ; inst != NULL ; inst = inst->next) {
+ if (inst->mode == IMHIREDIS_MODE_SUBSCRIBE && inst->aconn) {
+ DBGPRINTF("imhiredis: disconnecting async worker\n");
+ redisAsyncDisconnect(inst->aconn);
+ }
+ }
+
+ // event_base_loopbreak(runModConf->evtBase);
+
+ DBGPRINTF("imhiredis: waiting on imhiredis workerthread termination\n");
+ for(i = 0 ; i < activeHiredisworkers ; ++i) {
+ pthread_join(imhiredisWrkrInfo[i].tid, NULL);
+ DBGPRINTF("imhiredis: Stopped worker %d\n", i);
+ }
+ free(imhiredisWrkrInfo);
+ imhiredisWrkrInfo = NULL;
+
+ return;
+}
+
+
+/* This function is called to gather input. */
+BEGINrunInput
+ int i;
+ instanceConf_t *inst;
+CODESTARTrunInput
+ DBGPRINTF("imhiredis: runInput loop started ...\n");
+ activeHiredisworkers = 0;
+ for(inst = runModConf->root ; inst != NULL ; inst = inst->next) {
+ ++activeHiredisworkers;
+ }
+
+ if(activeHiredisworkers == 0) {
+ LogError(0, RS_RET_ERR, "imhiredis: no active inputs, input does "
+ "not run - there should have been additional error "
+ "messages given previously");
+ ABORT_FINALIZE(RS_RET_ERR);
+ }
+
+
+ DBGPRINTF("imhiredis: Starting %d imhiredis workerthreads\n", activeHiredisworkers);
+ imhiredisWrkrInfo = calloc(activeHiredisworkers, sizeof(struct imhiredisWrkrInfo_s));
+ if (imhiredisWrkrInfo == NULL) {
+ LogError(errno, RS_RET_OUT_OF_MEMORY, "imhiredis: worker-info array allocation failed.");
+ ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
+ }
+
+ /* Start worker threads for each imhiredis input source
+ */
+ i = 0;
+ for(inst = runModConf->root ; inst != NULL ; inst = inst->next) {
+ /* init worker info structure! */
+ imhiredisWrkrInfo[i].inst = inst; /* Set reference pointer */
+ pthread_create(&imhiredisWrkrInfo[i].tid, &wrkrThrdAttr, imhirediswrkr, &(imhiredisWrkrInfo[i]));
+ i++;
+ }
+
+ // This thread simply runs the various threads, then waits for Rsyslog to stop
+ while(glbl.GetGlobalInputTermState() == 0) {
+ if(glbl.GetGlobalInputTermState() == 0)
+ /* Check termination state every 0.5s
+ * should be sufficient to grant fast response to shutdown while not hogging CPU
+ */
+ srSleep(0, 500000);
+ }
+ DBGPRINTF("imhiredis: terminating upon request of rsyslog core\n");
+
+ shutdownImhiredisWorkers();
+finalize_it:
+ENDrunInput
+
+
+BEGINwillRun
+CODESTARTwillRun
+ /* we need to create the inputName property (only once during our lifetime) */
+ CHKiRet(prop.Construct(&pInputName));
+ CHKiRet(prop.SetString(pInputName, UCHAR_CONSTANT("imhiredis"), sizeof("imhiredis") - 1));
+ CHKiRet(prop.ConstructFinalize(pInputName));
+finalize_it:
+ENDwillRun
+
+
+BEGINafterRun
+CODESTARTafterRun
+ if(pInputName != NULL)
+ prop.Destruct(&pInputName);
+
+ENDafterRun
+
+
+BEGINmodExit
+CODESTARTmodExit
+ pthread_attr_destroy(&wrkrThrdAttr);
+
+ /* force cleaning of all libevent-related structures
+ * (clean shutdowns are not always guaranteed without it)
+ */
+ libevent_global_shutdown();
+
+ /* release objects we used */
+ objRelease(statsobj, CORE_COMPONENT);
+ objRelease(ruleset, CORE_COMPONENT);
+ objRelease(glbl, CORE_COMPONENT);
+ objRelease(prop, CORE_COMPONENT);
+ENDmodExit
+
+
+BEGINisCompatibleWithFeature
+CODESTARTisCompatibleWithFeature
+ if(eFeat == sFEATURENonCancelInputTermination)
+ iRet = RS_RET_OK;
+ENDisCompatibleWithFeature
+
+
+BEGINqueryEtryPt
+CODESTARTqueryEtryPt
+CODEqueryEtryPt_STD_IMOD_QUERIES
+CODEqueryEtryPt_STD_CONF2_QUERIES
+CODEqueryEtryPt_STD_CONF2_PREPRIVDROP_QUERIES
+CODEqueryEtryPt_STD_CONF2_IMOD_QUERIES
+CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES
+CODEqueryEtryPt_IsCompatibleWithFeature_IF_OMOD_QUERIES
+ENDqueryEtryPt
+
+
+BEGINmodInit()
+CODESTARTmodInit
+ *ipIFVersProvided = CURR_MOD_IF_VERSION;
+CODEmodInit_QueryRegCFSLineHdlr
+ /* request objects we use */
+ CHKiRet(objUse(glbl, CORE_COMPONENT));
+ CHKiRet(objUse(prop, CORE_COMPONENT));
+ CHKiRet(objUse(ruleset, CORE_COMPONENT));
+ CHKiRet(objUse(statsobj, CORE_COMPONENT));
+
+ /* initialize "read-only" thread attributes */
+ pthread_attr_init(&wrkrThrdAttr);
+ pthread_attr_setstacksize(&wrkrThrdAttr, 4096*1024);
+
+ /* activate libevent for (p)threads support */
+ evthread_use_pthreads();
+
+ENDmodInit
+
+
+/* ------------------------------ callbacks ------------------------------ */
+
+
+/**
+ * Asynchronous subscribe callback handler
+ */
+static void redisAsyncRecvCallback (redisAsyncContext *aconn, void *reply, void __attribute__((unused)) *unused) {
+ /*
+ redisReply is supposed to be an array of three elements: [''message', <channel>, <message>]
+
+
+ JJO: For future reference (https://github.com/redis/hiredis/blob/master/README.md)
+
+ Important: the current version of hiredis (1.0.0) frees replies when the asynchronous API is used.
+ This means you should not call freeReplyObject when you use this API.
+ The reply is cleaned up by hiredis after the callback returns.
+ TODO We may have to change this function in the future to free replies.
+ */
+ instanceConf_t *const inst = (instanceConf_t *) aconn->data;
+ redisReply * r = (redisReply *) reply;
+ if (r == NULL) return;
+
+ if (r->elements < 3 || r->element[2]->str == NULL) {
+ return;
+ }
+ enqMsg(inst, r->element[2]->str, r->element[2]->len);
+
+ return;
+}
+
+
+/**
+ * Asynchronous connection callback handler
+ */
+static void redisAsyncConnectCallback (const redisAsyncContext *c, int status) {
+ if (status != REDIS_OK) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "imhiredis (async): could not connect to redis: "
+ "%s", c->errstr);
+ // remove async context from instance config object, still contained in context's 'data' field
+ instanceConf_t *inst = (instanceConf_t *) c->data;
+ assert(inst != NULL);
+ inst->aconn = NULL;
+ return;
+ }
+ DBGPRINTF("imhiredis (async): successfully connected!\n");
+
+ return;
+}
+
+
+/**
+ * Asynchronous disconnection callback handler
+ */
+static void redisAsyncDisconnectCallback (const redisAsyncContext *c, int status) {
+
+ // remove context from instance config object (which is stored in the context 'data' field by us)
+ // context will be freed by the library, so it's only set to NULL here
+ instanceConf_t *inst = (instanceConf_t *) c->data;
+ assert(inst != NULL);
+ inst->aconn = NULL;
+ inst->currentNode = NULL;
+
+ if (status != REDIS_OK) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "imhiredis (async): got disconnected from redis: "
+ "%s", c->errstr);
+ return;
+ }
+ DBGPRINTF("imhiredis (async): successfully disconnected!\n");
+
+ return;
+}
+
+
+/* ------------------------------ end callbacks ------------------------------ */
+
+/*
+ * sends a ROLE command to the redis pointed by context
+ * context should be a valid redis context
+ * returns a valid redisReply pointer if an array reply was received, NULL otherwise
+ */
+redisReply *getRole(redisContext *c) {
+ redisReply *reply;
+
+ assert(c != NULL);
+
+ reply = redisCommand(c, "ROLE");
+ if (reply == NULL) {
+ DBGPRINTF("imhiredis: could not get reply from ROLE command\n");
+ }
+ else if (reply->type == REDIS_REPLY_ERROR) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_WARNING, "imhiredis got an error while querying role -> "
+ "%s\n", reply->str);
+ freeReplyObject(reply);
+ reply = NULL;
+ }
+ else if (reply->type != REDIS_REPLY_ARRAY) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "imhiredis: did not get an array from ROLE command");
+ freeReplyObject(reply);
+ reply = NULL;
+ }
+
+ return reply;
+}
+
+
+static struct json_object* _redisParseIntegerReply(const redisReply *reply) {
+ return json_object_new_int64(reply->integer);
+}
+
+#ifdef REDIS_REPLY_DOUBLE
+static struct json_object* _redisParseDoubleReply(const redisReply *reply) {
+ return json_object_new_double_s(reply->dval, reply->str);
+}
+#endif
+
+static struct json_object* _redisParseStringReply(const redisReply *reply) {
+ return json_object_new_string_len(reply->str, reply->len);
+}
+
+static struct json_object* _redisParseArrayReply(const redisReply *reply) {
+ struct json_object *result = NULL;
+ struct json_object *res = NULL;
+ char *key = NULL;
+
+ result = json_object_new_object(); // the redis type name is ARRAY, but represents a dict
+
+ if (result != NULL) {
+ for(size_t i = 0; i < reply->elements; i++) {
+ if (reply->element[i]->type == REDIS_REPLY_STRING && i % 2 == 0) {
+ key = reply->element[i]->str;
+ } else {
+ switch(reply->element[i]->type) {
+ case REDIS_REPLY_INTEGER:
+ res = _redisParseIntegerReply(reply->element[i]);
+ json_object_object_add(result, key, res);
+ break;
+#ifdef REDIS_REPLY_DOUBLE
+ case REDIS_REPLY_DOUBLE:
+ res = _redisParseDoubleReply(reply->element[i]);
+ json_object_object_add(result, key, res);
+ break;
+#endif
+ case REDIS_REPLY_STRING:
+ res = _redisParseStringReply(reply->element[i]);
+ json_object_object_add(result, key, res);
+ break;
+ case REDIS_REPLY_ARRAY:
+ res = _redisParseArrayReply(reply->element[i]);
+ json_object_object_add(result, key, res);
+ break;
+ default:
+ DBGPRINTF("Unhandled case!\n");
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING,
+ "Redis reply object contains an unhandled type!");
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+
+/*
+ * enqueue the hiredis message. The provided string is
+ * not freed - this must be done by the caller.
+ */
+static rsRetVal enqMsg(instanceConf_t *const inst, const char *message, size_t msgLen) {
+ DEFiRet;
+ smsg_t *pMsg;
+
+ if (message == NULL || message[0] == '\0') {
+ /* we do not process empty lines */
+ FINALIZE;
+ }
+
+ DBGPRINTF("imhiredis: enqMsg: Msg -> '%s'\n", message);
+
+ CHKiRet(msgConstruct(&pMsg));
+ MsgSetInputName(pMsg, pInputName);
+ MsgSetRawMsg(pMsg, message, msgLen);
+ MsgSetFlowControlType(pMsg, eFLOWCTL_LIGHT_DELAY);
+ MsgSetRuleset(pMsg, inst->pBindRuleset);
+ MsgSetMSGoffs(pMsg, 0); /* we do not have a header... */
+ CHKiRet(submitMsg2(pMsg));
+
+finalize_it:
+ RETiRet;
+}
+
+
+static rsRetVal enqMsgJson(instanceConf_t *const inst, struct json_object *json, struct json_object *metadata) {
+ DEFiRet;
+ smsg_t *pMsg;
+ struct json_object *tempJson = NULL;
+
+ assert(json != NULL);
+
+ CHKiRet(msgConstruct(&pMsg)); // In case of allocation error -> needs to break
+ MsgSetInputName(pMsg, pInputName);
+ MsgSetRuleset(pMsg, inst->pBindRuleset);
+ MsgSetMSGoffs(pMsg, 0); /* we do not have a header... */
+ if(RS_RET_OK != MsgSetFlowControlType(pMsg, eFLOWCTL_LIGHT_DELAY))
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING,
+ "Could not set Flow Control on message.");
+ if(inst->fieldList.nmemb != 0) {
+ for (int i = 0; i < inst->fieldList.nmemb; i++)
+ {
+ DBGPRINTF("processing field '%s'\n", inst->fieldList.name[i]);
+
+ /* case 1: static field. We simply forward it */
+ if (inst->fieldList.name[i][0] != '!' && inst->fieldList.name[i][0] != '.')
+ {
+ DBGPRINTF("field is static, taking it as it is...\n");
+ tempJson = json_object_new_string(inst->fieldList.name[i]);
+ }
+ /* case 2: dynamic field. We retrieve its value from the JSON logline and add it */
+ else
+ {
+ DBGPRINTF("field is dynamic, searching in root object...\n");
+ if (!json_object_object_get_ex(json, inst->fieldList.name[i]+1, &tempJson)) {
+
+ DBGPRINTF("Did not find value %s in message\n", inst->fieldList.name[i]);
+ continue;
+ }
+ // Getting object as it will not keep the same lifetime as its origin object
+ tempJson = json_object_get(tempJson);
+ // original object is put: no need for it anymore
+ json_object_put(json);
+ }
+
+ DBGPRINTF("got value of field '%s'\n", inst->fieldList.name[i]);
+ DBGPRINTF("will insert to '%s'\n", inst->fieldList.varname[i]);
+
+ if(RS_RET_OK != msgAddJSON(pMsg, (uchar *)inst->fieldList.varname[i], tempJson, 0, 0)) {
+ LogMsg(0, RS_RET_OBJ_CREATION_FAILED, LOG_ERR,
+ "Failed to add value to '%s'", inst->fieldList.varname[i]);
+ }
+
+ tempJson = NULL;
+ }
+ } else {
+ if(RS_RET_OK != msgAddJSON(pMsg, (uchar*)"!", json, 0, 0)) {
+ LogMsg(0, RS_RET_OBJ_CREATION_FAILED, LOG_ERR,
+ "Failed to add json info to message!");
+ ABORT_FINALIZE(RS_RET_OBJ_CREATION_FAILED);
+ }
+ }
+ if (metadata != NULL && RS_RET_OK != msgAddJSON(pMsg, (uchar*)".", metadata, 0, 0)) {
+ LogMsg(0, RS_RET_OBJ_CREATION_FAILED, LOG_ERR,
+ "Failed to add metadata to message!");
+ ABORT_FINALIZE(RS_RET_OBJ_CREATION_FAILED);
+ }
+ if(RS_RET_OK != submitMsg2(pMsg)) {
+ LogMsg(0, RS_RET_OBJ_CREATION_FAILED, LOG_ERR,
+ "Failed to submit message to main queue!");
+ ABORT_FINALIZE(RS_RET_OBJ_CREATION_FAILED);
+ }
+ DBGPRINTF("enqMsgJson: message enqueued!\n");
+
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * execute a synchronous authentication using the context conn
+ * conn and password should be non-NULL
+ * conn should be a valid context
+ */
+rsRetVal redisAuthentSynchronous(redisContext *conn, uchar *password) {
+ DEFiRet;
+ redisReply *reply = NULL;
+
+ assert(conn != NULL);
+ assert(password != NULL);
+ assert(password[0] != '\0');
+
+ reply = (redisReply *) redisCommand(conn, "AUTH %s", password);
+ if (reply == NULL) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: Could not authenticate!\n");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ } else if (strncmp(reply->str, "OK", 2)) {
+ LogError(0, RS_RET_REDIS_AUTH_FAILED, "imhiredis: Authentication failure -> %s\n", reply->str);
+ ABORT_FINALIZE(RS_RET_REDIS_AUTH_FAILED);
+ }
+
+finalize_it:
+ if(reply)
+ freeReplyObject(reply);
+ RETiRet;
+}
+
+
+/*
+ * execute an asynchronous authentication using the context aconn
+ * aconn and password should be non-NULL
+ * aconn should be a valid (async) context
+ */
+rsRetVal redisAuthentAsynchronous(redisAsyncContext *aconn, uchar *password) {
+ DEFiRet;
+
+ assert(aconn != NULL);
+ assert(password != NULL);
+ assert(password[0] != '\0');
+
+ if (REDIS_OK != redisAsyncCommand(aconn, NULL, NULL, "AUTH %s", password)) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: error while authenticating asynchronously -> %s\n",
+ aconn->errstr);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * connect to node, authenticate (if necessary), get role, then get all node information provided by ROLE
+ * node should be a non-NULL valid redisNode pointer
+ * password can be NULL, meaning no authentication will be done
+ * result will hold the result of the ROLE command executed on the node:
+ * - NULL if the node was a single master instance
+ * - a single (master) node if the provided node was a replica
+ * - a list of (replica) nodes if the provided node was a master
+ */
+rsRetVal redisGetServersList(redisNode *node, uchar *password, redisNode **result) {
+ DEFiRet;
+ redisContext *context;
+ redisReply *reply = NULL, *replica;
+ unsigned int i;
+
+ assert(node != NULL);
+
+ CHKiRet(redisConnectSync(&context, node));
+
+ if(password != NULL && password[0] != '\0') {
+ CHKiRet(redisAuthentSynchronous(context, password));
+ }
+
+ reply = getRole(context);
+
+ if(reply == NULL) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_WARNING, "imhiredis: did not get the role of the server");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+ /*
+ * string comparisons for ROLE could be skipped
+ * as each role returns a different number of elements,
+ * but lets keep it as a security...
+ */
+ if ( reply->elements == 5 &&
+ strncmp(reply->element[0]->str, "slave", 5) == 0) {
+
+ CHKiRet(createRedisNode(result));
+ (*result)->server = (uchar *) strdup((const char *)reply->element[1]->str);
+ (*result)->port = reply->element[2]->integer;
+ (*result)->isMaster = 1;
+ }
+ else if ( reply->elements == 3 &&
+ reply->element[2]->type == REDIS_REPLY_ARRAY &&
+ strncmp(reply->element[0]->str, "master", 6) == 0) {
+
+ // iterate on all replicas given in the reply (if any)
+ for (i = 0; i < reply->element[2]->elements; i++) {
+ replica = reply->element[2]->element[i];
+
+ if (replica->type == REDIS_REPLY_ARRAY && replica->elements == 3) {
+ /* node will be a new node every time
+ * with old ones shifted in the list
+ */
+ CHKiRet(createRedisNode(result));
+ (*result)->server = (uchar *) strdup((const char *)replica->element[0]->str);
+ // yes, the value in that case is a string and NOT an integer!
+ (*result)->port = atoi(replica->element[1]->str);
+ }
+ }
+ } else {
+ // we have a sentinel, or a problem
+ ABORT_FINALIZE(RS_RET_NOT_FOUND);
+ }
+
+finalize_it:
+ if (reply != NULL)
+ freeReplyObject(reply);
+ if (context != NULL)
+ redisFree(context);
+ RETiRet;
+}
+
+
+
+/*
+ * actualize the current master node to use during connection for instance inst
+ * inst should be a valid, non-NULL instanceConf object
+ * inst should also possess at least a single node in inst->redisNodeList
+ * if the function returns RS_RET_OK, inst->currentNode and inst->redisNodeList have been both updated
+ * to reflect new master and potential replicas
+ * the first configured node (called preferred node) is always kept as the first entry in redisNodeList
+ */
+rsRetVal redisActualizeCurrentNode(instanceConf_t *inst) {
+ DEFiRet;
+ redisReply *reply = NULL;
+ redisNode *node, *tmp, *newList = NULL;
+
+ assert(inst != NULL);
+ assert(inst->redisNodesList != NULL);
+
+ inst->currentNode = NULL;
+ // keep first node in list = preferred node (comes from configuration)
+ copyNode(inst->redisNodesList, &newList);
+ newList->next = NULL;
+
+ for (node = inst->redisNodesList; node != NULL; node = node->next) {
+ tmp = NULL;
+
+ DBGPRINTF("imhiredis: trying to connect to node to get info...\n");
+ dbgPrintNode(node);
+
+ if (RS_RET_OK == redisGetServersList(node, inst->password, &tmp)) {
+ // server replied
+
+ if (tmp && tmp->isMaster) {
+ DBGPRINTF("imhiredis: node replied with a master node, is a replica\n");
+ // master node, keep it as potential new active node
+ inst->currentNode = tmp;
+ tmp = NULL;
+
+ // try to connect to the master and get replicas
+ if(RS_RET_OK != redisGetServersList(inst->currentNode, inst->password, &tmp)) {
+
+ /* had a master, but cannot connect
+ * save suspected master in new list but keep searching with other nodes
+ */
+ DBGPRINTF("imhiredis: had a master but cannot connect, keeping in list\n");
+ dbgPrintNode(inst->currentNode);
+ insertNodeAfter(newList, inst->currentNode);
+ inst->currentNode = NULL;
+ continue;
+ }
+ } else {
+ DBGPRINTF("imhiredis: node replied with a list of replicas, is a master\n");
+ // copy the node to the new currentNode, list owning node will be freed
+ node->isMaster = 1;
+ copyNode(node, &(inst->currentNode));
+ inst->currentNode->next = NULL;
+ }
+
+ /*
+ * here, tmp is a list of replicas or NULL (single node)
+ * inst->currentNode is the new active master
+ */
+
+ // add the replicas to the list
+ if (tmp) {
+ insertNodeAfter(newList, tmp);
+ DBGPRINTF("imhiredis: inserting replicas to list\n");
+ for (tmp = newList->next; tmp != NULL; tmp = tmp->next) {
+ dbgPrintNode(tmp);
+ }
+ }
+ // insert the master after the preferred node (configuration)
+ DBGPRINTF("imhiredis: inserting new master node in list\n");
+ dbgPrintNode(inst->currentNode);
+ insertNodeAfter(newList, inst->currentNode);
+
+ // swap newList and redisNodesList to free old list at the end of the function
+ tmp = newList;
+ newList = inst->redisNodesList;
+ inst->redisNodesList = tmp;
+ FINALIZE;
+ }
+ }
+
+ DBGPRINTF("imhiredis: did not find a valid master");
+ iRet = RS_RET_NOT_FOUND;
+ inst->currentNode = NULL;
+
+finalize_it:
+ if (reply != NULL)
+ freeReplyObject(reply);
+ // newList is always completely freed
+ for (node = newList; node != NULL; ) {
+ node = freeNode(node);
+ }
+
+ RETiRet;
+}
+
+
+/*
+ * authentication function, for both synchronous and asynchronous modes (queue or subscribe)
+ * inst, inst->curentMode and inst->password should not be NULL
+ */
+rsRetVal redisAuthenticate(instanceConf_t *inst) {
+ DEFiRet;
+ redisContext *usedContext = NULL;
+ redisReply *reply = NULL;
+
+ assert(inst != NULL);
+ assert(inst->currentNode != NULL);
+ assert(inst->password != NULL);
+ assert(inst->password[0] != '\0');
+
+ DBGPRINTF("imhiredis: authenticating...\n");
+
+ // Create a temporary context for synchronous connection, used to validate AUTH command in asynchronous contexts
+ if (inst->mode == IMHIREDIS_MODE_SUBSCRIBE) {
+ if (RS_RET_OK != redisConnectSync(&usedContext, inst->currentNode)) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_WARNING, "imhiredis: could not connect to current "
+ "active node synchronously to validate authentication");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+ } else {
+ usedContext = inst->conn;
+ }
+
+ /*
+ * Try synchronous connection, whatever the method for the instance
+ * This is also done for the asynchronous mode, to validate the successful authentication
+ */
+ CHKiRet(redisAuthentSynchronous(usedContext, inst->password));
+
+ if (inst->mode == IMHIREDIS_MODE_SUBSCRIBE) {
+ CHKiRet(redisAuthentAsynchronous(inst->aconn, inst->password));
+ }
+
+ DBGPRINTF("imhiredis: authentication successful\n");
+
+finalize_it:
+ if(inst->mode == IMHIREDIS_MODE_SUBSCRIBE && usedContext)
+ redisFree(usedContext);
+ if(reply)
+ freeReplyObject(reply);
+ RETiRet;
+}
+
+
+/*
+ * connection function for synchronous (queue) mode
+ * node should not be NULL
+ */
+rsRetVal redisConnectSync(redisContext **conn, redisNode *node) {
+ DEFiRet;
+
+ assert(node != NULL);
+
+ if (node->usesSocket)
+ *conn = redisConnectUnixWithTimeout((const char *)node->socketPath, glblRedisConnectTimeout);
+ else
+ *conn = redisConnectWithTimeout((const char *)node->server, node->port, glblRedisConnectTimeout);
+
+ if (*conn == NULL) {
+ if (node->usesSocket) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: can not connect to redis server '%s' "
+ "-> could not allocate context!\n", node->socketPath);
+ } else {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: can not connect to redis server '%s', "
+ "port %d -> could not allocate context!\n", node->server, node->port);
+ }
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+ else if ((*conn)->err) {
+ if (node->usesSocket) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: can not connect to redis server '%s' "
+ "-> %s\n", node->socketPath, (*conn)->errstr);
+ } else {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis: can not connect to redis server '%s', "
+ "port %d -> %s\n", node->server, node->port, (*conn)->errstr);
+ }
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+finalize_it:
+ if (iRet != RS_RET_OK) {
+ if (*conn)
+ redisFree(*conn);
+ *conn = NULL;
+ }
+ RETiRet;
+}
+
+
+/*
+ * connection function for asynchronous (subscribe) mode
+ * node should not be NULL
+ */
+rsRetVal redisConnectAsync(redisAsyncContext **aconn, redisNode *node) {
+ DEFiRet;
+
+ assert(node != NULL);
+
+ if (node->usesSocket)
+ *aconn = redisAsyncConnectUnix((const char*)node->socketPath);
+ else
+ *aconn = redisAsyncConnect((const char *)node->server, node->port);
+
+ if(*aconn == NULL) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis (async): could not allocate context!\n");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ } else if ((*aconn)->err) {
+ if (node->usesSocket) {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis (async): cannot connect to server '%s' "
+ "-> %s\n", node->socketPath, (*aconn)->errstr);
+ } else {
+ LogError(0, RS_RET_REDIS_ERROR, "imhiredis (async): cannot connect to server '%s', port '%d' "
+ "-> %s\n", node->server, node->port, (*aconn)->errstr);
+ }
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+finalize_it:
+ if (iRet != RS_RET_OK) {
+ if(*aconn)
+ redisAsyncFree(*aconn);
+ *aconn = NULL;
+ }
+ RETiRet;
+}
+
+/*
+ * Helper method to connect to the current master asynchronously
+ * 'inst' parameter should be non-NULL and have a valid currentNode object
+ */
+rsRetVal connectMasterAsync(instanceConf_t *inst) {
+ DEFiRet;
+
+ if(RS_RET_OK != redisConnectAsync(&(inst->aconn), inst->currentNode)) {
+ inst->currentNode = NULL;
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+ if( inst->password != NULL &&
+ inst->password[0] != '\0' &&
+ RS_RET_OK != redisAuthenticate(inst)) {
+
+ redisAsyncFree(inst->aconn);
+ inst->aconn = NULL;
+ inst->currentNode = NULL;
+ ABORT_FINALIZE(RS_RET_REDIS_AUTH_FAILED);
+ }
+
+ // finalize context creation
+ inst->aconn->data = (void *)inst;
+ redisAsyncSetConnectCallback(inst->aconn, redisAsyncConnectCallback);
+ redisAsyncSetDisconnectCallback(inst->aconn, redisAsyncDisconnectCallback);
+ redisLibeventAttach(inst->aconn, inst->evtBase);
+
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * Helper method to check if (async) instance is connected
+ */
+static sbool isConnectedAsync(instanceConf_t *inst) {
+ return inst->aconn != NULL;
+}
+
+
+/*
+ * Helper method to connect to the current master synchronously
+ * 'inst' parameter should be non-NULL and have a valid currentNode object
+ */
+rsRetVal connectMasterSync(instanceConf_t *inst) {
+ DEFiRet;
+
+ if(RS_RET_OK != redisConnectSync(&(inst->conn), inst->currentNode)) {
+ inst->currentNode = NULL;
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+ if( inst->password != NULL &&
+ inst->password[0] != '\0' &&
+ RS_RET_OK != redisAuthenticate(inst)) {
+
+ redisFree(inst->conn);
+ inst->conn = NULL;
+ inst->currentNode = NULL;
+ ABORT_FINALIZE(RS_RET_REDIS_AUTH_FAILED);
+ }
+
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * Helper method to check if instance is connected
+ */
+static sbool isConnectedSync(instanceConf_t *inst) {
+ return inst->conn != NULL;
+}
+
+/*
+ * dequeue all entries in the redis list, using batches of 10 commands
+ */
+rsRetVal redisDequeue(instanceConf_t *inst) {
+ DEFiRet;
+ redisReply *reply = NULL;
+ uint replyType = 0, i;
+
+ assert(inst != NULL);
+
+ DBGPRINTF("redisDequeue: beginning to dequeue key '%s'\n", inst->key);
+
+ do {
+ // append a batch of inst->batchsize POP commands (either LPOP or RPOP depending on conf)
+ if (inst->useLPop == 1) {
+ DBGPRINTF("redisDequeue: Queuing #%d LPOP commands on key '%s' \n",
+ inst->batchsize,
+ inst->key);
+ for (i=0; i<inst->batchsize; ++i ) {
+ if (REDIS_OK != redisAppendCommand(inst->conn, "LPOP %s", inst->key))
+ break;
+ }
+ } else {
+ DBGPRINTF("redisDequeue: Queuing #%d RPOP commands on key '%s' \n",
+ inst->batchsize,
+ inst->key);
+ for (i=0; i<inst->batchsize; i++) {
+ if (REDIS_OK != redisAppendCommand(inst->conn, "RPOP %s", inst->key))
+ break;
+ }
+ }
+
+ DBGPRINTF("redisDequeue: Dequeuing...\n")
+ // parse responses from appended commands
+ do {
+ if (REDIS_OK != redisGetReply(inst->conn, (void **) &reply)) {
+ // error getting reply, must stop
+ LogError(0, RS_RET_REDIS_ERROR, "redisDequeue: Error reading reply after POP #%d "
+ "on key '%s'", (inst->batchsize - i), inst->key);
+ // close connection
+ redisFree(inst->conn);
+ inst->currentNode = NULL;
+ inst->conn = NULL;
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ } else {
+ if (reply != NULL) {
+ replyType = reply->type;
+ switch(replyType) {
+ case REDIS_REPLY_STRING:
+ enqMsg(inst, reply->str, reply->len);
+ break;
+ case REDIS_REPLY_NIL:
+ // replies are dequeued but are empty = end of list
+ break;
+ case REDIS_REPLY_ERROR:
+ // There is a problem with the key or the Redis instance
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "redisDequeue: error "
+ "while POP'ing key '%s' -> %s", inst->key, reply->str);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ default:
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "redisDequeue: "
+ "unexpected reply type: %s", REDIS_REPLIES[replyType%15]);
+ }
+ freeReplyObject(reply);
+ reply = NULL;
+ } else { /* reply == NULL */
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "redisDequeue: unexpected empty reply "
+ "for successful return");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+ }
+
+ // while there are replies to unpack, continue
+ } while (--i > 0);
+
+ if(replyType == REDIS_REPLY_NIL) {
+ /* sleep 1s between 2 POP tries, when no new entries are available (list is empty)
+ * this does NOT limit dequeing rate, but prevents the input from polling Redis too often
+ */
+ for(i = 0; i < 10; i++) {
+ // Time to stop the thread
+ if (glbl.GetGlobalInputTermState() != 0)
+ FINALIZE;
+ // 100ms sleeps
+ srSleep(0, 100000);
+ }
+ }
+
+ // while input can run, continue with a new batch
+ } while (glbl.GetGlobalInputTermState() == 0);
+
+ DBGPRINTF("redisDequeue: finished to dequeue key '%s'\n", inst->key);
+
+finalize_it:
+ if (reply)
+ freeReplyObject(reply);
+ RETiRet;
+}
+
+
+rsRetVal ensureConsumerGroupCreated(instanceConf_t *inst) {
+ DEFiRet;
+ redisReply *reply = NULL;
+
+ DBGPRINTF("ensureConsumerGroupCreated: Creating group %s on stream %s\n", inst->streamConsumerGroup, inst->key);
+
+ reply = (redisReply *)redisCommand(inst->conn, "XGROUP CREATE %s %s %s MKSTREAM",
+ inst->key,
+ inst->streamConsumerGroup,
+ inst->streamReadFrom);
+ if(reply != NULL) {
+ switch(reply->type) {
+ case REDIS_REPLY_STATUS:
+ case REDIS_REPLY_STRING:
+ if(0 == strncmp("OK", reply->str, reply->len))
+ DBGPRINTF("ensureConsumerGroupCreated: Consumer group %s created successfully "
+ "for stream %s\n",
+ inst->streamConsumerGroup,
+ inst->key);
+ break;
+ case REDIS_REPLY_ERROR:
+ if(strcasestr(reply->str, "BUSYGROUP") != NULL) {
+ DBGPRINTF("ensureConsumerGroupCreated: Consumer group %s already exists for "
+ "stream %s, ignoring\n",
+ inst->streamConsumerGroup,
+ inst->key);
+ } else {
+ LogError(0, RS_RET_ERR, "ensureConsumerGroupCreated: An unknown error "
+ "occurred while creating a Consumer group %s on stream %s -> %s",
+ inst->streamConsumerGroup,
+ inst->key,
+ reply->str);
+ ABORT_FINALIZE(RS_RET_ERR);
+ }
+ break;
+ default:
+ LogError(0, RS_RET_ERR, "ensureConsumerGroupCreated: An unknown reply was received "
+ "-> %s", REDIS_REPLIES[(reply->type)%15]);
+ ABORT_FINALIZE(RS_RET_ERR);
+ }
+ } else {
+ LogError(0, RS_RET_REDIS_ERROR, "ensureConsumerGroupCreated: Could not create group %s on stream %s!",
+ inst->streamConsumerGroup,
+ inst->key);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+finalize_it:
+ if(reply != NULL)
+ freeReplyObject(reply);
+ RETiRet;
+}
+
+
+rsRetVal ackStreamIndex(instanceConf_t *inst, uchar *stream, uchar *group, uchar *index) {
+ DEFiRet;
+ redisReply *reply = NULL;
+
+ DBGPRINTF("ackStream: Acknowledging index '%s' in stream %s\n", index, stream);
+
+ reply = (redisReply *)redisCommand(inst->conn, "XACK %s %s %s",
+ stream,
+ group,
+ index);
+ if(reply != NULL) {
+ switch(reply->type) {
+ case REDIS_REPLY_INTEGER:
+ if(reply->integer == 1) {
+ DBGPRINTF("ackStreamIndex: index successfully acknowledged "
+ "for stream %s\n",
+ inst->key);
+ } else {
+ DBGPRINTF("ackStreamIndex: message was not acknowledged "
+ "-> already done?");
+ }
+ break;
+ case REDIS_REPLY_ERROR:
+ LogError(0, RS_RET_ERR, "ackStreamIndex: An error occurred "
+ "while trying to ACK message %s on %s[%s] -> %s",
+ index,
+ stream,
+ group,
+ reply->str);
+ ABORT_FINALIZE(RS_RET_ERR);
+ default:
+ LogError(0, RS_RET_ERR, "ackStreamIndex: unexpected reply type: %s",
+ REDIS_REPLIES[(reply->type)%15]);
+ ABORT_FINALIZE(RS_RET_ERR);
+ }
+
+ } else {
+ LogError(0, RS_RET_REDIS_ERROR, "ackStreamIndex: Could not ACK message with index %s for %s[%s]!",
+ index,
+ stream,
+ group);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+finalize_it:
+ if(reply != NULL) {
+ freeReplyObject(reply);
+ }
+ RETiRet;
+}
+
+
+static rsRetVal enqueueRedisStreamReply(instanceConf_t *const inst, redisReply *reply) {
+ DEFiRet;
+ struct json_object *json = NULL, *metadata = NULL, *redis = NULL;
+
+ json = _redisParseArrayReply(reply->element[1]);
+
+ CHKmalloc(metadata = json_object_new_object());
+ CHKmalloc(redis = json_object_new_object());
+ json_object_object_add(redis, "stream", json_object_new_string((char *)inst->key));
+ json_object_object_add(redis, "index", _redisParseStringReply(reply->element[0]));
+ if(inst->streamConsumerGroup != NULL) {
+ json_object_object_add(redis, "group", json_object_new_string((char *)inst->streamConsumerGroup));
+ }
+ if(inst->streamConsumerName != NULL) {
+ json_object_object_add(redis, "consumer", json_object_new_string((char *)inst->streamConsumerName));
+ }
+
+ // ownership of redis object allocated by json_object_new_object() is taken by json
+ // no need to free/destroy/put redis object
+ json_object_object_add(metadata, "redis", redis);
+
+ CHKiRet(enqMsgJson(inst, json, metadata));
+ // enqueued message successfully, json and metadata objects are now owned by enqueued message
+ // no need to free/destroy/put json objects
+ json = NULL;
+ metadata = NULL;
+
+ if(inst->streamConsumerGroup != NULL && inst->streamConsumerACK) {
+ CHKiRet(ackStreamIndex(
+ inst,
+ (uchar *)inst->key,
+ inst->streamConsumerGroup,
+ (uchar *)reply->element[0]->str
+ ));
+ }
+
+finalize_it:
+ // If that happens, there was an error during one of the steps and the json object is not enqueued
+ if(json != NULL) json_object_put(json);
+ if(metadata != NULL) json_object_put(metadata);
+ RETiRet;
+}
+
+
+/*
+ * handle the hiredis Stream XREAD/XREADGROUP return objects. The provided reply is
+ * not freed - this must be done by the caller.
+ * example of stream to parse:
+ * 1) 1) "mystream" <- name of the stream indexes are from (list of streams requested)
+ * 2) 1) 1) "1681749395006-0" <- list of indexes returned for stream
+ * 2) 1) "key1"
+ * 2) "value1"
+ * 2) 1) "1681749409349-0"
+ * 2) 1) "key2"
+ * 2) "value2"
+ * 3) "key2.2"
+ * 4) "value2.2"
+ * json equivalent:
+ * [
+ * "mystream": [
+ * {
+ * "1681749395006-0": {
+ * "key1": "value1"
+ * }
+ * },
+ * {
+ * "1681749409349-0": {
+ * "key2": "value2",
+ * "key2.2": "value2.2"
+ * }
+ * }
+ * ]
+ * ]
+ */
+static rsRetVal handleRedisXREADReply(instanceConf_t *const inst, const redisReply *reply) {
+ DEFiRet;
+ redisReply *streamObj = NULL, *msgList = NULL, *msgObj = NULL;
+
+ if(reply == NULL || reply->type != REDIS_REPLY_ARRAY) {
+ /* we do not process empty or non-ARRAY lines */
+ DBGPRINTF("handleRedisXREADReply: object is not an array, ignoring\n");
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: object is not an array, ignoring");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ } else {
+ // iterating on streams
+ for(size_t i = 0; i < reply->elements; i++) {
+ streamObj = reply->element[i];
+ // object should contain the name of the stream, and an array containing the messages
+ if(streamObj->type != REDIS_REPLY_ARRAY || streamObj->elements != 2) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong object format, "
+ "object should contain the name of the stream and an array of messages");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+ if(streamObj->element[0]->type != REDIS_REPLY_STRING) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong field format, "
+ "first entry is not a string (supposed to be stream name)");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ msgList = streamObj->element[1];
+
+ if(msgList->type != REDIS_REPLY_ARRAY) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong field format, "
+ "second entry is not an array (supposed to be list of messages for stream)");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ DBGPRINTF("handleRedisXREADReply: enqueuing messages for stream '%s'\n",
+ streamObj->element[0]->str);
+
+ for(size_t j = 0; j < msgList->elements; j++) {
+ msgObj = msgList->element[j];
+ // Object should contain the name of the index, and its content(s)
+ if(msgObj->type != REDIS_REPLY_ARRAY || msgObj->elements != 2) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong object "
+ "format, object should contain the index and its content(s)");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+ if(msgObj->element[0]->type != REDIS_REPLY_STRING) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong field "
+ "format, first entry should be a string (index name)");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ if(msgObj->type != REDIS_REPLY_ARRAY) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXREADReply: wrong field "
+ "format, second entry should be an array (index content(s))");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ CHKiRet(enqueueRedisStreamReply(inst, msgObj));
+
+ // Update current stream index
+ memcpy(inst->streamReadFrom, msgObj->element[0]->str, msgObj->element[0]->len);
+ inst->streamReadFrom[msgObj->element[0]->len] = '\0';
+ DBGPRINTF("handleRedisXREADReply: current stream index is %s\n", inst->streamReadFrom);
+ }
+ }
+ }
+
+ DBGPRINTF("handleRedisXREADReply: finished enqueuing!\n");
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * handle the hiredis Stream XAUTOCLAIM return object. The provided reply is
+ * not freed - this must be done by the caller.
+ * example of stream to parse:
+ * 1) "1681904437564-0" <- next index to use for XAUTOCLAIM
+ * 2) 1) 1) "1681904437525-0" <- list of indexes reclaimed
+ * 2) 1) "toto"
+ * 2) "tata"
+ * 2) 1) "1681904437532-0"
+ * 2) 1) "titi"
+ * 2) "tutu"
+ * 3) (empty) <- indexes that no longer exist, were deleted from the PEL
+ * json equivalent:
+ * "1681904437564-0": [
+ * {
+ * "1681904437525-0": {
+ * "toto": "tata"
+ * }
+ * },
+ * {
+ * "1681904437532-0": {
+ * "titi": "tutu"
+ * }
+ * }
+ * ]
+ */
+static rsRetVal handleRedisXAUTOCLAIMReply(
+ instanceConf_t *const inst,
+ const redisReply *reply,
+ char **autoclaimIndex) {
+ DEFiRet;
+ redisReply *msgList = NULL, *msgObj = NULL;
+
+ if(reply == NULL || reply->type != REDIS_REPLY_ARRAY) {
+ /* we do not process empty or non-ARRAY lines */
+ DBGPRINTF("handleRedisXAUTOCLAIMReply: object is not an array, ignoring\n");
+ FINALIZE;
+ } else {
+ // Object should contain between 2 and 3 elements (depends on Redis server version)
+ if(reply->elements < 2 || reply->elements > 3) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: wrong number of fields, "
+ "cannot process entry");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+ if(reply->element[0]->type != REDIS_REPLY_STRING) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: the first element "
+ "is not a string, cannot process entry");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ msgList = reply->element[1];
+
+ if(msgList->type != REDIS_REPLY_ARRAY) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: the second element "
+ "is not an array, cannot process entry");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ DBGPRINTF("handleRedisXAUTOCLAIMReply: re-claiming messages for stream '%s'\n", inst->key);
+
+ for(size_t j = 0; j < msgList->elements; j++) {
+ msgObj = msgList->element[j];
+ // Object should contain the name of the index, and its content(s)
+ if(msgObj->type != REDIS_REPLY_ARRAY || msgObj->elements != 2) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: wrong message "
+ "format, cannot process");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+ if(msgObj->element[0]->type != REDIS_REPLY_STRING) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: first message "
+ "element not a string, cannot process");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ if(msgObj->type != REDIS_REPLY_ARRAY) {
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "handleRedisXAUTOCLAIMReply: second message "
+ "element not an array, cannot process");
+ ABORT_FINALIZE(RS_RET_OK_WARN);
+ }
+
+ CHKiRet(enqueueRedisStreamReply(inst, msgObj));
+ }
+
+ // Update current stream index with next index from XAUTOCLAIM
+ // No message has to be claimed after that if value is "0-0"
+ memcpy(*autoclaimIndex, reply->element[0]->str, reply->element[0]->len);
+ (*autoclaimIndex)[reply->element[0]->len] = '\0';
+ DBGPRINTF("handleRedisXAUTOCLAIMReply: next stream index is %s\n", (*autoclaimIndex));
+ }
+
+ DBGPRINTF("handleRedisXAUTOCLAIMReply: finished re-claiming!\n");
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * Read Redis stream
+ */
+rsRetVal redisStreamRead(instanceConf_t *inst) {
+ DEFiRet;
+ redisReply *reply = NULL;
+ uint replyType = 0;
+ sbool mustClaimIdle = 0;
+ char *autoclaimIndex = NULL;
+
+ assert(inst != NULL);
+
+ // Ensure stream group is created before reading from it
+ if(inst->streamConsumerGroup != NULL) {
+ CHKiRet(ensureConsumerGroupCreated(inst));
+ }
+
+
+ if(inst->streamAutoclaimIdleTime != 0) {
+ DBGPRINTF("redisStreamRead: getting pending entries for stream '%s' from '%s', with idle time %d\n",
+ inst->key, inst->streamReadFrom, inst->streamAutoclaimIdleTime);
+ CHKmalloc(autoclaimIndex = calloc(1, STREAM_INDEX_STR_MAXLEN));
+ // Cannot claim from '$', will have to claim from the beginning of the stream
+ if(inst->streamReadFrom[0] == '$') {
+ LogMsg(0, RS_RET_OK, LOG_WARNING, "Cannot claim pending entries from '$', "
+ "will have to claim from the beginning of the stream");
+ memcpy(autoclaimIndex, "0-0", 4);
+ } else {
+ memcpy(autoclaimIndex, inst->streamReadFrom, STREAM_INDEX_STR_MAXLEN);
+ }
+ mustClaimIdle = 1;
+ } else {
+ DBGPRINTF("redisStreamRead: beginning to read stream '%s' from '%s'\n",
+ inst->key, inst->streamReadFrom);
+ }
+
+ do {
+ if(inst->streamConsumerGroup == NULL) {
+ reply = (redisReply *)redisCommand(inst->conn, "XREAD COUNT %d BLOCK %d STREAMS %s %s",
+ BATCH_SIZE,
+ WAIT_TIME_MS,
+ inst->key,
+ inst->streamReadFrom);
+ } else {
+ if(mustClaimIdle) {
+ reply = (redisReply *)redisCommand(inst->conn,
+ "XAUTOCLAIM %s %s %s %d %s COUNT %d",
+ inst->key,
+ inst->streamConsumerGroup,
+ inst->streamConsumerName,
+ inst->streamAutoclaimIdleTime,
+ autoclaimIndex,
+ BATCH_SIZE);
+ } else {
+
+ reply = (redisReply *)redisCommand(inst->conn,
+ "XREADGROUP GROUP %s %s COUNT %d BLOCK %d STREAMS %s >",
+ inst->streamConsumerGroup,
+ inst->streamConsumerName,
+ BATCH_SIZE,
+ WAIT_TIME_MS,
+ inst->key);
+ }
+ }
+ if(reply == NULL) {
+ LogError(0, RS_RET_REDIS_ERROR, "redisStreamRead: Error while trying to read stream '%s'",
+ inst->key);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+ replyType = reply->type;
+ switch(replyType) {
+ case REDIS_REPLY_ARRAY:
+ DBGPRINTF("reply is an array, proceeding...\n");
+ if(mustClaimIdle) {
+ CHKiRet(handleRedisXAUTOCLAIMReply(inst, reply, &autoclaimIndex));
+ if(!strncmp(autoclaimIndex, "0-0", 4)) {
+ DBGPRINTF("redisStreamRead: Caught up with pending messages, "
+ "getting back to regular reads\n");
+ mustClaimIdle = 0;
+ }
+ } else {
+ CHKiRet(handleRedisXREADReply(inst, reply));
+ }
+ break;
+ case REDIS_REPLY_NIL:
+ // replies are dequeued but are empty = end of list
+ if(mustClaimIdle) mustClaimIdle = 0;
+ break;
+ case REDIS_REPLY_ERROR:
+ // There is a problem with the key or the Redis instance
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "redisStreamRead: error "
+ "while reading stream(s) -> %s", reply->str);
+ srSleep(1, 0);
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ default:
+ LogMsg(0, RS_RET_OK_WARN, LOG_WARNING, "redisStreamRead: unexpected "
+ "reply type: %s", REDIS_REPLIES[replyType%15]);
+ }
+ freeReplyObject(reply);
+ reply = NULL;
+
+ // while input can run, continue with a new batch
+ } while (glbl.GetGlobalInputTermState() == 0);
+
+ DBGPRINTF("redisStreamRead: finished to dequeue key '%s'\n", inst->key);
+
+finalize_it:
+ if(reply != NULL)
+ freeReplyObject(reply);
+ if(inst->conn != NULL) {
+ redisFree(inst->conn);
+ inst->conn = NULL;
+ inst->currentNode = NULL;
+ }
+ if(autoclaimIndex != NULL)
+ free(autoclaimIndex);
+ RETiRet;
+}
+
+
+/*
+ * Subscribe to Redis channel
+ */
+rsRetVal redisSubscribe(instanceConf_t *inst) {
+ DEFiRet;
+
+ DBGPRINTF("redisSubscribe: subscribing to channel '%s'\n", inst->key);
+ int ret = redisAsyncCommand(
+ inst->aconn,
+ redisAsyncRecvCallback,
+ NULL,
+ "SUBSCRIBE %s",
+ inst->key);
+
+ if (ret != REDIS_OK) {
+ LogMsg(0, RS_RET_REDIS_ERROR, LOG_ERR, "redisSubscribe: Could not subscribe");
+ ABORT_FINALIZE(RS_RET_REDIS_ERROR);
+ }
+
+ // Will block on this function as long as connection is open and event loop is not stopped
+ event_base_dispatch(inst->evtBase);
+ DBGPRINTF("redisSubscribe: finished.\n");
+
+finalize_it:
+ RETiRet;
+}
+
+
+/*
+ * generic worker function
+ */
+void workerLoop(struct imhiredisWrkrInfo_s *me) {
+ uint i;
+ DBGPRINTF("workerLoop: beginning of worker loop...\n");
+
+ // Connect first time without delay
+ if (me->inst->currentNode != NULL) {
+ rsRetVal ret = me->fnConnectMaster(me->inst);
+ if(ret != RS_RET_OK) {
+ LogMsg(0, ret, LOG_WARNING, "workerLoop: Could not connect successfully to master");
+ }
+ }
+
+ while(glbl.GetGlobalInputTermState() == 0) {
+ if (!me->fnIsConnected(me->inst)) {
+ /*
+ * Sleep 10 seconds before attempting to resume a broken connexion
+ * (sleep small amounts to avoid missing termination status)
+ */
+ LogMsg(0, RS_RET_OK, LOG_INFO, "workerLoop: "
+ "no valid connection, sleeping 10 seconds before retrying...");
+ for(i = 0; i < 100; i++) {
+ // Rsyslog asked for shutdown, thread should be stopped
+ if (glbl.GetGlobalInputTermState() != 0)
+ goto end_loop;
+ // 100ms sleeps
+ srSleep(0, 100000);
+ }
+
+ // search the current master node
+ if (me->inst->currentNode == NULL) {
+ if(RS_RET_OK != redisActualizeCurrentNode(me->inst))
+ continue;
+ }
+
+ // connect to current master
+ if (me->inst->currentNode != NULL) {
+ rsRetVal ret = me->fnConnectMaster(me->inst);
+ if(ret != RS_RET_OK) {
+ LogMsg(0, ret, LOG_WARNING, "workerLoop: "
+ "Could not connect successfully to master");
+ }
+ }
+ }
+ if (me->fnIsConnected(me->inst)) {
+ me->fnRun(me->inst);
+ }
+ }
+
+end_loop:
+ return;
+}
+
+
+/*
+ * Workerthread function for a single hiredis consumer
+ */
+static void *
+imhirediswrkr(void *myself)
+{
+ struct imhiredisWrkrInfo_s *me = (struct imhiredisWrkrInfo_s*) myself;
+ DBGPRINTF("imhiredis: started hiredis consumer workerthread\n");
+ dbgPrintNode(me->inst->currentNode);
+
+ if(me->inst->mode == IMHIREDIS_MODE_QUEUE) {
+ me->fnConnectMaster = connectMasterSync;
+ me->fnIsConnected = isConnectedSync;
+ me->fnRun = redisDequeue;
+ }
+ else if (me->inst->mode == IMHIREDIS_MODE_STREAM) {
+ me->fnConnectMaster = connectMasterSync;
+ me->fnIsConnected = isConnectedSync;
+ me->fnRun = redisStreamRead;
+ }
+ else if (me->inst->mode == IMHIREDIS_MODE_SUBSCRIBE) {
+ me->fnConnectMaster = connectMasterAsync;
+ me->fnIsConnected = isConnectedAsync;
+ me->fnRun = redisSubscribe;
+ }
+
+ workerLoop(me);
+
+ DBGPRINTF("imhiredis: stopped hiredis consumer workerthread\n");
+ return NULL;
+}
+
+
+
+// -------------------------- redisNode functions -----------------------------------
+
+/*
+ * create a redisNode and set default values
+ * if a valid node is given as parameter, the new node is inserted as the new head of the linked list
+ */
+static rsRetVal
+createRedisNode(redisNode **root) {
+ redisNode *node;
+ DEFiRet;
+ CHKmalloc(node = malloc(sizeof(redisNode)));
+ node->port = 0;
+ node->server = NULL;
+ node->socketPath = NULL;
+ node->usesSocket = 0;
+ node->isMaster = 0;
+ node->next = NULL;
+
+ if ((*root) == NULL) {
+ *root = node;
+ } else {
+ node->next = (*root);
+ *root = node;
+ }
+
+finalize_it:
+ RETiRet;
+}
+
+/*
+ * make a complete copy of the src node into the newly-created node in dst
+ * if dst already contains a node, the new node will be added as the new head of the provided list
+ * src should not be NULL
+ */
+rsRetVal copyNode(redisNode *src, redisNode **dst) {
+ DEFiRet;
+
+ assert(src != NULL);
+
+ CHKiRet(createRedisNode(dst));
+
+ (*dst)->isMaster = src->isMaster;
+ (*dst)->next = src->next;
+ (*dst)->port = src->port;
+ (*dst)->usesSocket = src->usesSocket;
+
+ if (src->server)
+ (*dst)->server = (uchar *) strdup((const char *)src->server);
+ if (src->socketPath)
+ (*dst)->socketPath = (uchar *) strdup((const char *)src->socketPath);
+
+finalize_it:
+ RETiRet;
+}
+
+/*
+ * free all ressources of the node
+ * will return next node if one is present, NULL otherwise
+ */
+redisNode *freeNode(redisNode *node) {
+ redisNode *ret = NULL;
+ if (node != NULL) {
+ if (node->next != NULL)
+ ret = node->next;
+
+ if(node->server != NULL)
+ free(node->server);
+ if(node->socketPath != NULL)
+ free(node->socketPath);
+ free(node);
+ }
+
+ return ret;
+}
+
+/*
+ * insert node 'elem' after node 'root' in the linked list
+ * both root and elem should not be NULL
+ */
+void insertNodeAfter(redisNode *root, redisNode *elem) {
+ assert(root != NULL);
+ assert(elem != NULL);
+
+ if(root->next != NULL) {
+ elem->next = root->next;
+ }
+ root->next = elem;
+
+ return;
+}
+
+void dbgPrintNode(redisNode *node) {
+ if (node != NULL) {
+ if (node->usesSocket) {
+ if (node->isMaster) {
+ DBGPRINTF("imhiredis: node is %s (master)\n", node->socketPath);
+ } else {
+ DBGPRINTF("imhiredis: node is %s (replica)\n", node->socketPath);
+ }
+ } else {
+ if (node->isMaster) {
+ DBGPRINTF("imhiredis: node is %s:%d (master)\n", node->server, node->port);
+ } else {
+ DBGPRINTF("imhiredis: node is %s:%d (replica)\n", node->server, node->port);
+ }
+ }
+ }
+ return;
+}