htsn (empty) → 0.0.1
raw patch · 22 files changed
+2400/−0 lines, 22 filesdep +MissingHdep +ansi-terminaldep +basesetup-changed
Dependencies added: MissingH, ansi-terminal, base, cmdargs, configurator, directory, filepath, hdaemonize, hslogger, hxt, network, tasty, tasty-hunit, transformers, unix
Files
- Setup.hs +2/−0
- doc/LICENSE +619/−0
- doc/htsnrc.example +103/−0
- doc/init.openrc +15/−0
- doc/man1/htsn.1 +160/−0
- htsn.cabal +301/−0
- src/CommandLine.hs +117/−0
- src/Configuration.hs +82/−0
- src/ExitCodes.hs +26/−0
- src/Logging.hs +81/−0
- src/Main.hs +356/−0
- src/OptionalConfiguration.hs +182/−0
- src/TSN/FeedHosts.hs +57/−0
- src/TSN/Xml.hs +88/−0
- src/Terminal.hs +79/−0
- src/Unix.hs +118/−0
- test/TestSuite.hs +9/−0
- test/xml/19908216.xml +1/−0
- test/xml/19908217.xml +1/−0
- test/xml/19908245.xml +1/−0
- test/xml/19908246.xml +1/−0
- test/xml/19908247.xml +1/−0
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ doc/LICENSE view
@@ -0,0 +1,619 @@+ 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.
+ doc/htsnrc.example view
@@ -0,0 +1,103 @@+# Example configuration file for htsn. For this to take effect, you+# would need to place it in either /etc/htsnrc or $HOME/.htsnrc. On+# Windows, it probably needs to go in %APPDATA%, or+# C:\Users\<username>\Application Data.+++# Run in the background as a daemon?+#+# Default: false+#+# daemonize = true+++# A list of hostnames that supply the feed. You probably don't need to+# change this, but you can.+#+# Default: ["feed1.sportsnetwork.com",+# "feed2.sportsnetwork.com",+# "feed3.sportsnetwork.com"]+#+# feed_hosts = [ "hostname1", "hostname2", ... ]+++# If you specify a file path here, logs will be written to it+# (possibly in addition to syslog). Can be either a relative or+# absolute path. It will not be auto-rotated; use something like+# logrotate for that.+#+# Default: none+#+# log_file = "/var/log/htsn/htsn.log"+++# How verbose should the logs be? Valid levels are,+#+# "INFO", "WARNING", "ERROR"+#+# (there are others, but we don't emit them.)+#+# Default: "INFO"+#+# log_level = "WARNING"+++# By default, XML files will be written to the current working+# directory. Often this is not desirable, and you would rather save+# them to a specific location. Specify it here.+#+# Default: "."+#+# output_directory = "/var/lib/htsn"+++# The password associated with your TSN username.+#+# Default: none (required)+#+# password = "whatever"+++# (Daemon mode only) Create a PID file in the given location. This is+# used by the init system on Unix to keep track of the running+# daemon. Its parent directory must be writable by the user/group that+# we will run as!+#+# Default: /run/htsn/htsn.pid+#+# pidfile = /var/run/htsn/htsn.pid+++# (Daemon mode only) Run htsn as the specified system grup. The PID+# file is written before privileges are dropped, so the only+# privileges needed by htsn are those necessary to write the XML files+# and (optionally) the log file.+#+# Default: the current group+#+# run_as_group = "htsn"+++# (Daemon mode only) Run htsn as the specified system user. The PID+# file is written before privileges are dropped, so the only+# privileges needed by htsn are those necessary to write the XML files+# and (optionally) the log file.+#+# Default: the current user+#+# run_as_user = "htsn"++# Do you want to log to syslog? On Windows this will attempt to+# communicate (over UDP) with a syslog daemon on localhost, which will+# most likely not work.+#+# Default: false+#+# syslog = true+++# The username used to connect to the feed.+#+# Default: none (required)+#+# username = "whoever"
+ doc/init.openrc view
@@ -0,0 +1,15 @@+#!/sbin/runscript++#+# An example init file tailored for OpenRC on Gentoo.+#++description="Launch htsn to parse The Sports Network feed"+command="/usr/bin/htsn"+command_args="--daemonize"+pidfile="/run/htsn/htsn.pid"++depend() {+ use logger+ need dns net+}
+ doc/man1/htsn.1 view
@@ -0,0 +1,160 @@+.TH htsn 1++.SH NAME+htsn \- Parse XML files from The Sports Network feed.++.SH SYNOPSIS++\fBhtsn\fR [OPTIONS] [HOSTNAMES]++.SH DESCRIPTION+.P+The Sports Network <http://www.sportsnetwork.com/> offers an XML feed+containing various sports news and statistics. The goal of \fBhtsn\fR+is to watch the XML feed and parse the individual XML documents into+files.+.P+Once started, we will choose an XML feed host to connect to. The+choice is made from a list in a round-robin fashion, and by default,+the list contains all known TSN feed hosts. Once we have a connection,+your username and password are sent. If they are accepted, we begin to+parse the feed saving all XML files to the configured output directory+(see \fI\-\-output\-directory\fR).+.P+If we encounter an error (say, the connection is dropped), then we+will attempt to connect to the next host in the list after waiting+five seconds. This process continues indefinitely.+.P+The program can run either interactively (that is, outputting to the+console), or as a daemon with the \fI\-\-daemonize\fR flag.++.SH INPUT+.P+The program takes no input; a username and password must be supplied+on the command-line or in a configuration file.++.SH OUTPUT+.P+Output is not generated when running as a daemon; otherwise, standard+out and standard error are fairly noisy. All traffic between htsn and+the feed server is displayed on stdout. Status messages are+interspersed when they are generated with warnings and errors going to+stderr. The following can be expected:+.IP \[bu] 2+The only data we send to the feed are the username and password. These+will be highlighted in green on stdout.+.IP \[bu]+All data received from the feed will be echoed in the default color to+stdout.+.IP \[bu]+Informational messages will be highlighted in cyan and sent to stdout.+.IP \[bu]+Warnings will be highlighted in yellow and sent to stderr.+.IP \[bu]+Errors will be highlighted in red and sent to stderr.++.SH LOGGING+.P+Logging is done either to syslog or a file. The destination and+verbosity are controlled by the \fI\-\-log\-file\fR,+\fI\-\-log\-level\fR, and \fI\-\-syslog\fR parameters which may be+specified either on the command line or in the configuration file.++.SH OPTIONS++.IP \fB\-\-daemonize\fR,\ \fB\-d\fR+Run as a daemon, in the background. When running as a daemon the+\fI\-\-pidfile\fR, \fI\-\-run\-as\-group\fR, and+\fI\-\-run\-as\-user\fR flags become relevant.++Default: disabled++.IP \fB\-\-log-file\fR+If you specify a file here, logs will be written to it (possibly in+addition to syslog). Can be either a relative or absolute path. It+will not be auto-rotated; use something log logrotate for that.++Default: none++.IP \fB\-\-log-level\fR+How verbose should the logs be? We log notifications at three levels:+INFO, WARN, and ERROR. Specify the \(dqmost boring\(dq level of+notifications you would like to receive (in all-caps); more+interesting notifications will be logged as well.++Default: INFO++.IP \fB\-\-output\-directory\fR,\ \fB\-o\fR+To which directory should we write the XML files?++Default: .++.IP \fB\-\-password\fR+The password associated with your TSN username. A password is+required, so you must supply one either on the command line or in a+configuration file.++Default: none++.IP \fB\-\-pidfile\fR+(Daemon mode only) Create a PID file in the given location. This is+used by the init system on Unix to keep track of the running daemon.+Its parent directory must be writable by the user/group that we will+run as!++Default: /run/htsn/htsn.pid++.IP \fB\-\-run\-as\-group\fR+(Daemon mode only) Run as the given system group. The PID file is+written before privileges are dropped, so the only privileges needed+by htsn are those necessary to write the XML files and (optionally)+the log file.++Default: the current group++.IP \fB\-\-run\-as\-user\fR+(Daemon mode only) Run as the given system user. The PID file is+written before privileges are dropped, so the only privileges needed+by htsn are those necessary to write the XML files and (optionally)+the log file.++Default: the current user++.IP \fB\-\-syslog\fR,\ \fB\-s\fR+Enable logging to syslog. On Windows this will attempt to communicate+(over UDP) with a syslog daemon on localhost, which will most likely+not work.++Default: disabled++.IP \fB\-\-username\fR,\ \fB\-u\fR+Your TSN username. A username is required, so you must supply one+either on the command line or in a configuration file.++Default: none++.SH FEED HOSTS+.P+It is possible to pass a list of feed hostnames on the command-line+(see [HOSTNAMES] in the synopsis). By default \fBhtsn\fR will attempt+to connect to every known TSN XML feed host in a round-robin fashion,+so there is rarely a need to do this.++.SH CONFIGURATION FILE+.P+Any of the command-line options mentioned above can be specified in a+configuration file instead. We first look for \(dqhtsnrc\(dq in the+system configuration directory (/etc on Unix). We then look for a file+named \(dq.htsnrc\(dq in the user's home directory. The latter will+override the former.+.P+The file's syntax is given by examples in the htsnrc.example file+(included with \fBhtsn\fR).+.P+Options specified on the command-line override those in either+configuration file.++.SH BUGS++.P+Send bugs to michael@orlitzky.com.
+ htsn.cabal view
@@ -0,0 +1,301 @@+name: htsn+version: 0.0.1+cabal-version: >= 1.8+author: Michael Orlitzky+maintainer: Michael Orlitzky <michael@orlitzky.com>+category: Utils+license: GPL-3+license-file: doc/LICENSE+build-type: Simple+extra-source-files:+ doc/htsnrc.example+ doc/man1/htsn.1+ doc/init.openrc+ test/xml/*.xml+synopsis:+ Parse XML files from The Sports Network feed.+description:+ /Usage/:+ .+ @+ htsn [OPTIONS] [HOSTNAMES]+ @+ .+ The Sports Network <http://www.sportsnetwork.com/> offers an XML feed+ containing various sports news and statistics. The goal of /htsn/+ is to watch the XML feed and parse the individual XML documents into+ files.+ .+ Once started, we will choose an XML feed host to connect to. The+ choice is made from a list in a round-robin fashion, and by default,+ the list contains all known TSN feed hosts. Once we have a connection,+ your username and password are sent. If they are accepted, we begin to+ parse the feed saving all XML files to the configured output directory+ (see @--output-directory@).+ .+ If we encounter an error (say, the connection is dropped), then we+ will attempt to connect to the next host in the list after waiting+ five seconds. This process continues indefinitely.+ .+ The program can run either interactively (that is, outputting to the+ console), or as a daemon with the @--daemonize@ flag.+ .+ /Input/:+ .+ The program takes no input; a username and password must be supplied+ on the command-line or in a configuration file.+ .+ /Output/:+ .+ Output is not generated when running as a daemon; otherwise, standard+ out and standard error are fairly noisy. All traffic between /htsn/ and+ the feed server is displayed on stdout. Status messages are+ interspersed when they are generated with warnings and errors going to+ stderr. The following can be expected:+ .+ * The only data we send to the feed are the username and password.+ These will be highlighted in green on stdout.+ .+ * All data received from the feed will be echoed in the default color+ to stdout.+ .+ * Informational messages will be highlighted in cyan and sent to stdout.+ .+ * Warnings will be highlighted in yellow and sent to stderr.+ .+ * Errors will be highlighted in red and sent to stderr.+ .+ /Logging/:+ .+ Logging is done either to syslog or a file. The destination and+ verbosity are controlled by the @--log-file@, @--log-level@,+ and @--syslog@ parameters which may be specified either on the command+ line or in the configuration file.+ .+ /Options/:+ .+ @+ \--daemonize, -d+ @+ .+ Run as a daemon, in the background. When running as a daemon the+ \--pidfile, --run-as-group, and --run-as-user flags become relevant.+ .+ Default: disabled+ .+ @+ \--log-file+ @+ .+ If you specify a file here, logs will be written to it (possibly in+ addition to syslog). Can be either a relative or absolute path. It+ will not be auto-rotated; use something log logrotate for that.+ .+ Default: none+ .+ @+ \--log-level+ @+ .+ How verbose should the logs be? We log notifications at three levels:+ INFO, WARN, and ERROR. Specify the \"most boring\" level of+ notifications you would like to receive (in all-caps); more+ interesting notifications will be logged as well.+ .+ Default: INFO+ .+ @+ \--output-directory, -o+ @+ .+ To which directory should we write the XML files?+ .+ Default: .+ .+ @+ \--password+ @+ .+ The password associated with your TSN username. A password is+ required, so you must supply one either on the command line or in a+ configuration file.+ .+ Default: none+ .+ @+ \--pidfile+ @+ .+ (Daemon mode only) Create a PID file in the given location. This is+ used by the init system on Unix to keep track of the running daemon.+ Its parent directory must be writable by the user/group that we will+ run as!+ .+ Default: \/run\/htsn\/htsn.pid+ .+ @+ \--run-as-group+ @+ .+ (Daemon mode only) Run as the given system group. The PID file is+ written before privileges are dropped, so the only privileges needed+ by /htsn/ are those necessary to write the XML files and (optionally)+ the log file.+ .+ Default: the current group+ .+ @+ \--run-as-user+ @+ .+ (Daemon mode only) Run as the given system user. The PID file is+ written before privileges are dropped, so the only privileges needed+ by /htsn/ are those necessary to write the XML files and (optionally)+ the log file.+ .+ Default: the current user+ .+ @+ \--syslog, -s+ @+ .+ Enable logging to syslog. On Windows this will attempt to communicate+ (over UDP) with a syslog daemon on localhost, which will most likely+ not work.+ .+ Default: disabled+ .+ @+ \--username, -u+ @+ .+ Your TSN username. A username is required, so you must supply one+ either on the command line or in a configuration file.+ .+ Default: none+ .+ /Feed Hosts/:+ .+ It is possible to pass a list of feed hostnames on the command-line+ (see [HOSTNAMES] in the synopsis). By default /htsn/ will attempt+ to connect to every known TSN XML feed host in a round-robin fashion,+ so there is rarely a need to do this.+ .+ /Configuration File/:+ .+ Any of the command-line options mentioned above can be specified in a+ configuration file instead. We first look for \"htsnrc\" in the+ system configuration directory (/etc on Unix). We then look for a file+ named \".htsnrc\" in the user's home directory. The latter will override+ the former.+ .+ The file's syntax is given by examples in the htsnrc.example file+ (included with /htsn/).+ .+ Options specified on the command-line override those in either+ configuration file.+++executable htsn+ build-depends:+ ansi-terminal == 0.6.*,+ base == 4.*,+ cmdargs >= 0.10.6,+ configurator == 0.2.*,+ directory == 1.2.*,+ filepath == 1.3.*,+ hdaemonize == 0.4.*,+ hslogger == 1.2.*,+ hxt == 9.3.*,+ MissingH == 1.2.*,+ network == 2.4.*,+ tasty == 0.5.*,+ tasty-hunit == 0.4.*,+ transformers == 0.3.*,+ unix == 2.6.*++ main-is:+ Main.hs++ hs-source-dirs:+ src/++ other-modules:+ CommandLine+ Configuration+ ExitCodes+ Logging+ OptionalConfiguration+ Terminal+ TSN.FeedHosts+ TSN.Xml+ Unix++ ghc-options:+ -Wall+ -fwarn-hi-shadowing+ -fwarn-missing-signatures+ -fwarn-name-shadowing+ -fwarn-orphans+ -fwarn-type-defaults+ -fwarn-tabs+ -fwarn-incomplete-record-updates+ -fwarn-monomorphism-restriction+ -fwarn-unused-do-bind+ -rtsopts+ -threaded+ -optc-O3+ -optc-march=native+ -O2++ ghc-prof-options:+ -prof+ -auto-all+ -caf-all++++test-suite testsuite+ type: exitcode-stdio-1.0+ hs-source-dirs: src test+ main-is: TestSuite.hs+ build-depends:+ ansi-terminal == 0.6.*,+ base == 4.*,+ cmdargs >= 0.10.6,+ configurator == 0.2.*,+ directory == 1.2.*,+ filepath == 1.3.*,+ hdaemonize == 0.4.*,+ hslogger == 1.2.*,+ hxt == 9.3.*,+ MissingH == 1.2.*,+ network == 2.4.*,+ tasty == 0.5.*,+ tasty-hunit == 0.4.*,+ transformers == 0.3.*,+ unix == 2.6.*++ -- It's not entirely clear to me why I have to reproduce all of this.+ ghc-options:+ -Wall+ -fwarn-hi-shadowing+ -fwarn-missing-signatures+ -fwarn-name-shadowing+ -fwarn-orphans+ -fwarn-type-defaults+ -fwarn-tabs+ -fwarn-incomplete-record-updates+ -fwarn-monomorphism-restriction+ -fwarn-unused-do-bind+ -rtsopts+ -threaded+ -optc-O3+ -optc-march=native+ -O2+++source-repository head+ type: git+ location: http://michael.orlitzky.com/git/htsn.git+ branch: master
+ src/CommandLine.hs view
@@ -0,0 +1,117 @@+-- | Parse the command-line options, and display help text if+-- necessary.+module CommandLine (+ get_args )+where++import System.Console.CmdArgs (+ (&=),+ args,+ cmdArgs,+ def,+ details,+ help,+ program,+ summary,+ typ,+ typFile,+ typDir )++-- This let's us get the version from Cabal.+import Paths_htsn (version)+import Data.Version (showVersion)++import OptionalConfiguration ( OptionalConfiguration(..) )++-- | The description of the program, displayed as part of the help.+description :: String+description = "Parse XML files from The Sports Network feed."++-- | The name of this program.+program_name :: String+program_name = "htsn"++-- | A summary string output as part of the help.+my_summary :: String+my_summary = program_name ++ "-" ++ (showVersion version)+++-- | A description of the "daemonize" option.+daemonize_help :: String+daemonize_help =+ "Run as a daemon, in the background."++-- | A description of the "log_file" option.+log_file_help :: String+log_file_help =+ "Log to the given file."++-- | A description of the "log_level" option.+log_level_help :: String+log_level_help =+ "How verbose should the logs be? One of INFO, WARNING, ERROR."++-- | A description of the "output_directory" option.+output_directory_help :: String+output_directory_help =+ "Directory in which to output the XML files; must be writable."++-- | A description of the "password" option.+password_help :: String+password_help =+ "Password to use when connecting to the feed."++-- | A description of the "pidfile" option.+pidfile_help :: String+pidfile_help =+ "Location to create PID file (daemon only)."++-- | A description of the "run_as_group" option.+run_as_group_help :: String+run_as_group_help =+ "System group to run as (daemon only)."++-- | A description of the "run_as_user" option.+run_as_user_help :: String+run_as_user_help =+ "System user to run under (daemon only)."++-- | A description of the "syslog" option.+syslog_help :: String+syslog_help =+ "Enable logging to syslog."++-- | A description of the "username" option.+username_help :: String+username_help =+ "Username to use when connecting to the feed."++-- | A data structure representing the possible command-line+-- options. The CmdArgs library is doing heavy magic beneath the+-- hood here.+arg_spec :: OptionalConfiguration+arg_spec =+ OptionalConfiguration {+ -- Use an empty list for feed_hosts since cmdargs will appends to+ -- the default when the user supplies feed hosts. If he specifies+ -- any, those are all we should use.+ daemonize = def &= typ "BOOL" &= help daemonize_help,+ feed_hosts = def &= typ "HOSTNAMES" &= args,+ log_file = def &= typFile &= help log_file_help,+ log_level = def &= typ "LEVEL" &= help log_level_help,+ output_directory = def &= typDir &= help output_directory_help,+ password = def &= typ "PASSWORD" &= help password_help,+ pidfile = def &= typFile &= help pidfile_help,+ run_as_group = def &= typ "GROUP" &= help run_as_group_help,+ run_as_user = def &= typ "USER" &= help run_as_user_help,+ syslog = def &= typ "BOOL" &= help syslog_help,+ username = def &= typ "USERNAME" &= help username_help }+ &= program program_name+ &= summary my_summary+ &= details [description]+++-- | A convenience function; our only export. Meant to be used in+-- 'main' to retrieve the command-line arguments.+get_args :: IO OptionalConfiguration+get_args = cmdArgs arg_spec
+ src/Configuration.hs view
@@ -0,0 +1,82 @@+-- | This module defines the 'Configuration' type, which is just a+-- wrapper around all of the configuration options we accept on the+-- command line.+--+module Configuration (+ Configuration(..),+ merge_optional )+where++import System.Console.CmdArgs.Default ( Default(..) )+import System.Log ( Priority( INFO ) )++import qualified OptionalConfiguration as OC (+ OptionalConfiguration(..),+ merge_maybes )+import TSN.FeedHosts (FeedHosts(..))++-- | The main configuration data type. This will be passed to most of+-- the important functions once it has been created.+data Configuration =+ Configuration {+ daemonize :: Bool,+ feed_hosts :: FeedHosts,+ log_file :: Maybe FilePath,+ log_level :: Priority,+ output_directory :: FilePath,+ password :: String,+ pidfile :: FilePath,+ run_as_group :: Maybe String,+ run_as_user :: Maybe String,+ syslog :: Bool,+ username :: String }+ deriving (Show)++-- | A Configuration with all of its fields set to their default+-- values.+instance Default Configuration where+ def = Configuration {+ daemonize = def,+ feed_hosts = def,+ log_file = def,+ log_level = INFO,+ output_directory = ".",+ password = def,+ pidfile = "/run/htsn/htsn.pid",+ run_as_group = def,+ run_as_user = def,+ syslog = def,+ username = def }+++-- | Merge a Configuration with an OptionalConfiguration. This is more+-- or less the Monoid instance for OptionalConfiguration, but since+-- the two types are different, we have to repeat ourselves.+merge_optional :: Configuration+ -> OC.OptionalConfiguration+ -> Configuration+merge_optional cfg opt_cfg =+ Configuration+ (merge (daemonize cfg) (OC.daemonize opt_cfg))+ all_feed_hosts+ (OC.merge_maybes (log_file cfg) (OC.log_file opt_cfg))+ (merge (log_level cfg) (OC.log_level opt_cfg))+ (merge (output_directory cfg) (OC.output_directory opt_cfg))+ (merge (password cfg) (OC.password opt_cfg))+ (merge (pidfile cfg) (OC.pidfile opt_cfg))+ (OC.merge_maybes (run_as_group cfg) (OC.run_as_group opt_cfg))+ (OC.merge_maybes (run_as_user cfg) (OC.run_as_user opt_cfg))+ (merge (syslog cfg) (OC.syslog opt_cfg))+ (merge (username cfg) (OC.username opt_cfg))+ where+ -- | If the thing on the right is Just something, return that+ -- something, otherwise return the thing on the left.+ merge :: a -> Maybe a -> a+ merge x Nothing = x+ merge _ (Just y) = y++ -- If there are any optional usernames, use only those.+ all_feed_hosts = if (null (get_feed_hosts (OC.feed_hosts opt_cfg)))+ then (feed_hosts cfg)+ else (OC.feed_hosts opt_cfg)+
+ src/ExitCodes.hs view
@@ -0,0 +1,26 @@+-- | All exit codes that the program can return (excepting+-- ExitSuccess).+module ExitCodes (+ exit_no_feed_hosts,+ exit_no_password,+ exit_no_username,+ exit_pidfile_exists )+where++-- | No feed hosts were given on the command line or in the config file.+exit_no_feed_hosts :: Int+exit_no_feed_hosts = 1++-- | No password was given on the command line or in the config file.+exit_no_password :: Int+exit_no_password = 2++-- | No username was given on the command line or in the config file.+exit_no_username :: Int+exit_no_username = 3++-- | When running as a daemon, the existence of a fixed PID file is+-- used to determine whether or not the daemon is already+-- running. If the PID file already exists, we shouldn't start.+exit_pidfile_exists :: Int+exit_pidfile_exists = 4
+ src/Logging.hs view
@@ -0,0 +1,81 @@+module Logging (+ init_logging,+ log_debug,+ log_error,+ log_info,+ log_warning )+where++import Control.Monad ( when )+import System.Log.Formatter ( simpleLogFormatter )+import System.Log.Handler ( setFormatter )+import System.Log.Handler.Simple ( GenericHandler, fileHandler )+import System.Log.Handler.Syslog (+ Facility ( USER ),+ openlog )+import System.Log.Logger (+ Priority ( INFO ),+ addHandler,+ debugM,+ errorM,+ infoM,+ rootLoggerName,+ setHandlers,+ setLevel,+ updateGlobalLogger,+ warningM )+++-- | Log a message at the DEBUG level.+log_debug :: String -> IO ()+log_debug = debugM rootLoggerName++-- | Log a message at the ERROR level.+log_error :: String -> IO ()+log_error = errorM rootLoggerName++-- | Log a message at the INFO level.+log_info :: String -> IO ()+log_info = infoM rootLoggerName++-- | Log a message at the WARNING level.+log_warning :: String -> IO ()+log_warning = warningM rootLoggerName+++-- | Set up the logging. All logs are handled by the global "root"+-- logger provided by HSLogger. We remove all of its handlers so+-- that it does nothing; then we conditionally add back two handlers+-- -- one for syslog, and one for a normal file -- dependent upon+-- the 'syslog' and 'log_file' configuration items.+--+-- Why don't we take a Configuration as an argument? Because it+-- would create circular imports!+init_logging :: Maybe FilePath -> Priority -> Bool -> IO ()+init_logging log_file log_level syslog = do+ -- First set the global log level and clear the default handler.+ let no_handlers = [] :: [GenericHandler a]+ updateGlobalLogger rootLoggerName (setLevel log_level .+ setHandlers no_handlers)++ when syslog $ do+ let min_level = INFO+ let sl_level = if log_level < min_level then min_level else log_level++ -- The syslog handle gets its own level which will cowardly refuse+ -- to log all debug info (i.e. the entire feed) to syslog.+ sl_handler' <- openlog rootLoggerName [] USER sl_level++ -- Syslog should output the date by itself.+ let sl_formatter = simpleLogFormatter "htsn[$pid] $prio: $msg"+ let sl_handler = setFormatter sl_handler' sl_formatter++ updateGlobalLogger rootLoggerName (addHandler sl_handler)++ case log_file of+ Nothing -> return ()+ Just lf -> do+ lf_handler' <- fileHandler lf log_level+ let lf_formatter = simpleLogFormatter "$time: htsn[$pid] $prio: $msg"+ let lf_handler = setFormatter lf_handler' lf_formatter+ updateGlobalLogger rootLoggerName (addHandler lf_handler)
+ src/Main.hs view
@@ -0,0 +1,356 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DoAndIfThenElse #-}++module Main+where++import Control.Concurrent ( threadDelay )+import Control.Exception.Base ( bracket )+import Control.Monad ( when )+import Data.List ( isPrefixOf )+import Data.Maybe ( isNothing )+import Data.Monoid ( (<>) )+import Network (+ connectTo,+ PortID (PortNumber) )+import System.Console.CmdArgs ( def )+import System.Directory ( doesFileExist )+import System.Exit ( ExitCode(..), exitWith )+import System.FilePath ( (</>) )+import System.IO (+ BufferMode (NoBuffering),+ Handle,+ hClose,+ hGetChar,+ hGetLine,+ hPutStr,+ hSetBuffering,+ stderr,+ stdout )+import System.IO.Error ( catchIOError )+import System.Timeout ( timeout )++import CommandLine ( get_args )+import Configuration ( Configuration(..), merge_optional )+import ExitCodes (+ exit_no_feed_hosts,+ exit_no_password,+ exit_no_username,+ exit_pidfile_exists )+import Logging (+ init_logging,+ log_debug,+ log_error,+ log_info,+ log_warning )+import qualified OptionalConfiguration as OC (+ OptionalConfiguration(..),+ from_rc )+import Terminal (+ display_debug,+ display_error,+ display_info,+ display_sent,+ display_warning )+import TSN.FeedHosts ( FeedHosts(..) )+import TSN.Xml ( parse_xmlfid )+import Unix ( full_daemonize )++-- | Display and log debug information. WARNING! This does not+-- automatically append a newline. The output is displayed/logged+-- as-is, for, you know, debug purposes.+report_debug :: String -> IO ()+report_debug s = do+ display_debug s+ log_debug s+++-- | Display and log an error condition. This will prefix the error+-- with "ERROR: " when displaying (but not logging) it so that it+-- stands out.+--+report_error :: String -> IO ()+report_error s = do+ display_error $ "ERROR: " ++ s+ log_error s+++-- | Display and log an informational (status) message.+--+report_info :: String -> IO ()+report_info s = do+ display_info s+ log_info s+++-- | Display and log a warning. This will prefix the warning with+-- "WARNING: " when displaying (but not logging) it so that it+-- stands out.+--+report_warning :: String -> IO ()+report_warning s = do+ display_warning $ "WARNING: " ++ s+ log_warning s+++-- | Receive a single line of text from a Handle, and send it to the+-- debug log.+--+recv_line :: Handle -> IO String+recv_line h = do+ line <- hGetLine h+ report_debug (line ++ "\n")+ return line+++-- | Takes a Configuration, and an XML document (as a String). The XML+-- document is written to the output directory, as specified by the+-- Configuration.+--+-- This can fail, but we don't purposefully throw any exceptions. If+-- something goes wrong, we would rather log it and keep going.+--+save_document :: Configuration -> String -> IO ()+save_document cfg doc =+ case either_path of+ Left err -> report_error err+ Right path -> do+ already_exists <- doesFileExist path+ when already_exists $ do+ let msg = "File " ++ path ++ " already exists, overwriting."+ report_warning msg+ writeFile path doc+ report_info $ "Wrote file: " ++ path ++ "."+ where+ -- All the fmaps are because we're working inside a Maybe.+ xmlfid = fmap show (parse_xmlfid doc)+ filename = fmap (++ ".xml") xmlfid+ either_path = fmap ((output_directory cfg) </>) filename+++-- | Loop forever, writing the buffer to file whenever a </message>+-- tag is seen. This is the low-level "loop forever" function that+-- we stay in as long as we are connected to one feed.+--+-- The documentation at+-- <http://www.sportsnetworkdata.com/feeds/xml-levels.asp> states+-- that \<message\> will always be the root element of the XML+-- documents, and \</message\> will be the final line transmitted+-- for a given document. We therefore rely on this to simplify+-- processing.+--+loop :: Configuration -> Handle -> [String] -> IO ()+loop !cfg !h !buffer = do+ line <- recv_line h+ let new_buffer = line : buffer++ -- Use isPrefixOf to avoid line-ending issues. Hopefully they won't+ -- send invalid junk (on the same line) after closing the root+ -- element.+ if "</message>" `isPrefixOf` line+ then do+ -- The buffer is in reverse (newest first) order, though, so we+ -- have to reverse it first. We then concatenate all of its lines+ -- into one big string.+ let document = concat $ reverse new_buffer+ save_document cfg document+ loop cfg h [] -- Empty the buffer before looping again.+ else+ -- Append line to the head of the buffer and loop.+ loop cfg h new_buffer+++-- | Once we're connected to a feed, we need to log in. There's no+-- protocol for this (the docs don't mention one), but we have+-- (apparently) successfully guessed it.+--+-- The first thing TSN sends once we've connected is the string+-- "Username: ", containing 10 ASCII characters. We then send a+-- username, followed by a newline. If TSN likes the username, the+-- second they'll send is the string "Password: ", also containing+-- 10 ASCII characters, to which we reply in kind.+--+-- Assuming the above will always hold, it is implemented as follows:+--+-- 1. Receive 10 chars+--+-- 2. Send username if we got the username prompt+--+-- 3. Receive 10 chars+--+-- 4. Send password if we got the password prompt+--+-- If TSN likes the password as well, they send the string "The+-- Sports Network" before finally beginning to stream the feed.+--+log_in :: Configuration -> Handle -> IO ()+log_in cfg h = do+ prompt1 <- recv_prompt h++ if prompt1 /= username_prompt then+ report_error "Didn't receive username prompt."+ else do+ send_cred h (username cfg)+ prompt2 <- recv_prompt h++ if prompt2 /= password_prompt then+ report_error "Didn't receive password prompt."+ else do+ send_cred h (password cfg)+ _ <- recv_line h -- "The Sports Network"+ report_info $ "Logged in as " ++ (username cfg) ++ "."+ return ()+ where+ username_prompt = "Username: "+ password_prompt = "Password: "++ send_cred :: Handle -> String -> IO ()+ send_cred h' s = do+ -- The carriage return is super important!+ let line = s ++ "\r\n"+ hPutStr h' line+ display_sent line -- Don't log the username/password!++ recv_chars :: Int -> Handle -> IO String+ recv_chars n h' = do+ s <- sequence [ hGetChar h' | _ <- [1..n] ]+ report_debug s+ return s++ recv_prompt :: Handle -> IO String+ recv_prompt = recv_chars 10+++-- | Connect to @host@ and attempt to parse the feed. As long as we+-- stay connected and nothing bad happens, the program will remain in+-- this function. If anything goes wrong, then the current invocation+-- of connect_and_parse will return, and get called again later+-- (probably with a different @host@).+--+-- Steps:+--+-- 1. Connect to the host on the XML port+--+-- 2. Log in+--+-- 3. Go into the eternal read/save loop.+--+connect_and_parse :: Configuration -> String -> IO ()+connect_and_parse cfg host = do+ report_info $ "Connecting to " ++ host ++ "."+ bracket acquire_handle release_handle action+ return ()+ where+ five_seconds :: Int+ five_seconds = 5000000++ acquire_handle = connectTo host (PortNumber 4500)+ release_handle = hClose+ action h = do+ -- No buffering anywhere.+ hSetBuffering h NoBuffering++ -- The feed is often unresponsive after we send out username. It+ -- happens in a telnet session, too (albeit less frequently?),+ -- so there might be a bug on their end.+ --+ -- If we dump the packets with tcpdump, it looks like their+ -- software is getting confused: they send us some XML in+ -- the middle of the log-in procedure.+ --+ -- On the other hand, the documentation at+ -- <http://www.sportsnetworkdata.com/feeds/xml-levels.asp>+ -- states that you can only make one connection per username to+ -- a given host. So maybe they're simply rejecting the username+ -- in an unfriendly fashion. In any case, the easiest fix is to+ -- disconnect and try again.+ --+ login_worked <- timeout five_seconds $ log_in cfg h+ case login_worked of+ Nothing -> report_info $ "Login timed out (5 seconds). "+ ++ "Waiting 5 seconds to reconnect."+ Just _ -> loop cfg h []+++-- | A wrapper around threadDelay which takes seconds instead of+-- microseconds as its argument.+--+thread_sleep :: Int -> IO ()+thread_sleep seconds = do+ let microseconds = seconds * (10 ^ (6 :: Int))+ threadDelay microseconds+++-- | The entry point of the program.+--+main :: IO ()+main = do+ rc_cfg <- OC.from_rc+ cmd_cfg <- get_args++ -- Merge the config file options with the command-line ones,+ -- prefering the command-line ones.+ let opt_config = rc_cfg <> cmd_cfg++ -- Update a default config with any options that have been set in+ -- either the config file or on the command-line. We initialize+ -- logging before the missing parameter checks below so that we can+ -- log the errors.+ let cfg = (def :: Configuration) `merge_optional` opt_config+ init_logging (log_file cfg) (log_level cfg) (syslog cfg)++ -- Check the optional config for missing required options. This is+ -- necessary because if the user specifies an empty list of+ -- hostnames in e.g. the config file, we want to bail rather than+ -- fall back on the default list (which was merged from a default+ -- Configuration above).+ when (null $ get_feed_hosts (OC.feed_hosts opt_config)) $ do+ report_error "No feed hosts supplied."+ exitWith (ExitFailure exit_no_feed_hosts)++ when (isNothing (OC.password opt_config)) $ do+ report_error "No password supplied."+ exitWith (ExitFailure exit_no_password)++ when (isNothing (OC.username opt_config)) $ do+ report_error "No username supplied."+ exitWith (ExitFailure exit_no_username)++ when (daemonize cfg) $ do+ -- Old PID files can be left around after an unclean shutdown. We+ -- only care if we're running as a daemon.+ pidfile_exists <- doesFileExist (pidfile cfg)+ when pidfile_exists $ do+ report_error $ "PID file " ++ (pidfile cfg) ++ " already exists. "+ ++ "Refusing to start."+ exitWith (ExitFailure exit_pidfile_exists)++ -- This may be superstition (and I believe stderr is unbuffered),+ -- but it can't hurt.+ hSetBuffering stderr NoBuffering+ hSetBuffering stdout NoBuffering++ -- The rest of the program is kicked off by the following line which+ -- begins connecting to our feed hosts, starting with the first one,+ -- and proceeds in a round-robin fashion.+ let run_program = round_robin cfg 0++ -- If we were asked to daemonize, do that; otherwise just run the thing.+ if (daemonize cfg)+ then full_daemonize cfg run_program+ else run_program++ where+ -- | This is the top-level "loop forever" function. If an+ -- exception is thrown, it will propagate up to this point, where+ -- it will be logged and ignored in style.+ --+ -- Afterwards, we recurse (call ourself) again to loop more forevers.+ --+ round_robin :: Configuration -> Int -> IO ()+ round_robin cfg feed_host_idx = do+ let hosts = get_feed_hosts $ feed_hosts cfg+ let host = hosts !! feed_host_idx+ catchIOError (connect_and_parse cfg host) (report_error . show)+ thread_sleep 5 -- Wait 5s before attempting to reconnect.+ round_robin cfg $ (feed_host_idx + 1) `mod` (length hosts)
+ src/OptionalConfiguration.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}++-- | An OptionalConfiguration is just like a 'Configuration', except+-- all of its fields are optional. The user can set options in two+-- places: the command-line, and a configuration file. Obviously if+-- a parameter is set in one place, it doesn't need to be set in the+-- other. Thus, the latter needs to be optional.+--++module OptionalConfiguration (+ OptionalConfiguration(..),+ from_rc,+ merge_maybes )+where++import qualified Data.Configurator as DC (+ Worth(Optional),+ load,+ lookup )+import qualified Data.Configurator.Types as DCT (+ Configured,+ Value( String ),+ convert )+import Data.Data ( Data )+import Data.Maybe ( fromMaybe )+import Data.Monoid ( Monoid(..) )+import Data.Typeable ( Typeable )+import Paths_htsn ( getSysconfDir )+import System.Directory ( getHomeDirectory )+import System.FilePath ( (</>) )+import System.IO.Error ( catchIOError )+import System.Log ( Priority(..) )++import Logging ( log_error ) -- Can't import report_error from Main+import Terminal ( display_error ) -- 'cause of circular imports.+import TSN.FeedHosts ( FeedHosts(..) )+++-- Derive standalone instances of Data and Typeable for Priority. This+-- is necessary for OptionalConfiguration (which contains a Maybe+-- Priority) to derive Data and Typeable.+deriving instance Data Priority+deriving instance Typeable Priority++-- | The same as Configuration, except everything is optional. It's easy to+-- merge two of these by simply dropping the Nothings in favor of+-- the Justs. The 'feed_hosts' are left un-maybed so that cmdargs+-- can parse more than one of them.+--+data OptionalConfiguration =+ OptionalConfiguration {+ daemonize :: Maybe Bool,+ feed_hosts :: FeedHosts,+ log_file :: Maybe FilePath,+ log_level :: Maybe Priority,+ output_directory :: Maybe FilePath,+ password :: Maybe String,+ pidfile :: Maybe FilePath,+ run_as_group :: Maybe String,+ run_as_user :: Maybe String,+ syslog :: Maybe Bool,+ username :: Maybe String }+ deriving (Show, Data, Typeable)+++-- | Combine two Maybes into one, essentially mashing them+-- together. We give precedence to the second argument when both are+-- Justs.+merge_maybes :: (Maybe a) -> (Maybe a) -> (Maybe a)+merge_maybes Nothing Nothing = Nothing+merge_maybes (Just x) Nothing = Just x+merge_maybes Nothing (Just x) = Just x+merge_maybes (Just _) (Just y) = Just y+++-- | The Monoid instance for these lets us "combine" two+-- OptionalConfigurations. The "combine" operation that we'd like to+-- perform is, essentially, to mash them together. So if we have two+-- OptionalConfigurations, each half full, we could combine them+-- into one big one.+--+-- This is used to merge command-line and config-file settings.+--+instance Monoid OptionalConfiguration where+ -- | An empty OptionalConfiguration.+ mempty = OptionalConfiguration+ Nothing+ (FeedHosts [])+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+ Nothing+++ -- | Combine @cfg1@ and @cfg2@, giving precedence to @cfg2@.+ cfg1 `mappend` cfg2 =+ OptionalConfiguration+ (merge_maybes (daemonize cfg1) (daemonize cfg2))+ all_feed_hosts+ (merge_maybes (log_file cfg1) (log_file cfg2))+ (merge_maybes (log_level cfg1) (log_level cfg2))+ (merge_maybes (output_directory cfg1) (output_directory cfg2))+ (merge_maybes (password cfg1) (password cfg2))+ (merge_maybes (pidfile cfg1) (pidfile cfg2))+ (merge_maybes (run_as_group cfg1) (run_as_group cfg2))+ (merge_maybes (run_as_user cfg1) (run_as_user cfg2))+ (merge_maybes (syslog cfg1) (syslog cfg2))+ (merge_maybes (username cfg1) (username cfg2))+ where+ -- Use only the latter feed_hosts if there are any.+ all_feed_hosts =+ feed_hosts $ if (null (get_feed_hosts (feed_hosts cfg2)))+ then cfg1+ else cfg2+++instance DCT.Configured Priority where+ -- | This allows us to read a Priority level out of a Configurator+ -- config file. By default Configurator wouldn't know what to do,+ -- so we have to tell it that we expect one of the valid Priority+ -- constructors.+ convert (DCT.String "INFO") = Just INFO+ convert (DCT.String "WARNING") = Just WARNING+ convert (DCT.String "ERROR") = Just ERROR+ convert _ = Nothing+++-- | Obtain an OptionalConfiguration from htsnrc in either the global+-- configuration directory or the user's home directory. The one in+-- $HOME is prefixed by a dot so that it is hidden.+--+-- We make an attempt at cross-platform compatibility; we will try+-- to find the correct directory even on Windows. But if the calls+-- to getHomeDirectory/getSysconfDir fail for whatever reason, we+-- fall back to using the Unix-specific /etc and $HOME.+--+from_rc :: IO OptionalConfiguration+from_rc = do+ etc <- catchIOError getSysconfDir (\e -> do+ display_error (show e)+ log_error (show e)+ return "/etc")+ home <- catchIOError getHomeDirectory (\e -> do+ display_error (show e)+ log_error (show e)+ return "$(HOME)")+ let global_config_path = etc </> "htsnrc"+ let user_config_path = home </> ".htsnrc"+ cfg <- DC.load [ DC.Optional global_config_path,+ DC.Optional user_config_path ]+ cfg_daemonize <- DC.lookup cfg "daemonize"+ cfg_feed_hosts <- DC.lookup cfg "feed_hosts"+ cfg_log_file <- DC.lookup cfg "log_file"+ cfg_log_level <- DC.lookup cfg "log_level"+ cfg_output_directory <- DC.lookup cfg "output_directory"+ cfg_password <- DC.lookup cfg "password"+ cfg_pidfile <- DC.lookup cfg "pidfile"+ cfg_run_as_group <- DC.lookup cfg "run_as_group"+ cfg_run_as_user <- DC.lookup cfg "run_as_user"+ cfg_syslog <- DC.lookup cfg "syslog"+ cfg_username <- DC.lookup cfg "username"++ return $ OptionalConfiguration+ cfg_daemonize+ (fromMaybe (FeedHosts []) cfg_feed_hosts)+ cfg_log_file+ cfg_log_level+ cfg_output_directory+ cfg_password+ cfg_pidfile+ cfg_run_as_group+ cfg_run_as_user+ cfg_syslog+ cfg_username
+ src/TSN/FeedHosts.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | A newtype around a list of Strings which represent the feed+-- hosts. This is all to avoid an orphan instance of Configured for+-- [String] if we had defined one in e.g. OptionalConfiguration.+--+-- This was placed under the "TSN" namespace because its Default+-- instance is specific to TSN, even though otherwise it's just a+-- list of strings.+--+module TSN.FeedHosts+where++-- DC is needed only for the DCT.Configured instance of String.+import qualified Data.Configurator as DC()+import qualified Data.Configurator.Types as DCT (+ Configured,+ Value( List ),+ convert )+import Data.Data (Data)+import System.Console.CmdArgs.Default (Default(..))+import Data.Typeable (Typeable)+++-- | A (wrapper around a) list of hostnames that supply the XML feed.+--+newtype FeedHosts =+ FeedHosts { get_feed_hosts :: [String] }+ deriving (Data, Show, Typeable)+++instance Default FeedHosts where+ -- | The default list of feed hosts. These were found by checking+ -- PTR records in the neighborhood of the IP address in use. There+ -- is a feed4.sportsnetwork.com, but it was not operational when+ -- this was written.+ def = FeedHosts ["feed1.sportsnetwork.com",+ "feed2.sportsnetwork.com",+ "feed3.sportsnetwork.com"]+++instance DCT.Configured FeedHosts where+ -- | This allows us to read a FeedHosts object out of a Configurator+ -- config file. By default Configurator wouldn't know what to do,+ -- so we have to tell it that we expect a list, and if that list+ -- has strings in it, we can apply the FeedHosts constructor to+ -- it.+ convert (DCT.List xs) =+ -- mapM gives us a Maybe [String] here.+ fmap FeedHosts (mapM convert_string xs)+ where+ convert_string :: DCT.Value -> Maybe String+ convert_string = DCT.convert++ -- If we read anything other than a list of values out of the file,+ -- fail.+ convert _ = Nothing
+ src/TSN/Xml.hs view
@@ -0,0 +1,88 @@+-- | Minimal XML functionality needed to parse each document's+-- XML_File_ID.+--+module TSN.Xml (+ parse_xmlfid,+ xml_tests )+where++import Data.Either.Utils ( maybeToEither )+import Test.Tasty ( TestTree, testGroup )+import Test.Tasty.HUnit ( (@?=), Assertion, testCase )+import Text.Read ( readMaybe )+import Text.XML.HXT.Core (+ (>>>),+ (/>),+ getChildren,+ getText,+ hasName,+ runLA,+ xreadDoc )+++-- | A tiny parser written in HXT to extract the "XML_File_ID" element+-- from a document. If we fail to parse an XML_File_ID, we return+-- the reason wrapped in a 'Left' constructor. The reason should be+-- one of two things:+--+-- 1. No XML_File_ID elements were found.+--+-- 2. An XML_File_ID element was found, but it could not be read+-- into an Integer.+--+-- We use an Either rather than a Maybe because we do expect some+-- non-integer XML_File_IDs. In the examples, you will see+-- NHL_DepthChart_XML.XML with an XML_File_ID of "49618.61" and+-- CFL_Boxscore_XML1.xml with an XML_File_ID of "R28916". According+-- to Brijesh Patel of TSN, these are special category files and not+-- part of the usual feed.+--+-- We want to report them differently, "just in case."+--+parse_xmlfid :: String -- ^ The XML Document+ -> Either String Integer+parse_xmlfid doc =+ case parse_results of+ [] -> Left "No XML_File_ID elements found."+ (x:_) -> x+ where+ parse :: String -> [String]+ parse =+ runLA (xreadDoc+ >>> hasName "message"+ /> hasName "XML_File_ID"+ >>> getChildren+ >>> getText)++ read_either_integer :: String -> Either String Integer+ read_either_integer s =+ let msg = "Could not parse XML_File_ID " ++ s ++ " as an integer."+ in+ maybeToEither msg (readMaybe s)++ elements = parse doc+ parse_results = map read_either_integer elements+++-- * Tasty Tests+xml_tests :: TestTree+xml_tests =+ testGroup+ "XML tests"+ [ xml_file_id_tests ]+++xml_file_id_tests :: TestTree+xml_file_id_tests =+ testCase "XML_File_ID is parsed correctly" $ do+ let xmlfids = ["19908216", "19908216", "19908245", "19908246", "19908247"]+ mapM_ check xmlfids+ where+ check :: String -> Assertion+ check xmlfid = do+ xml <- readFile ("test/xml/" ++ xmlfid ++ ".xml")+ let actual = parse_xmlfid xml+ -- The maybeToEither should always succeed here, so the error+ -- message goes unused.+ let expected = maybeToEither "derp" (readMaybe xmlfid)+ actual @?= expected
+ src/Terminal.hs view
@@ -0,0 +1,79 @@+module Terminal (+ display_debug,+ display_error,+ display_info,+ display_sent,+ display_warning )+where++import Control.Monad.IO.Class (MonadIO(..))+import System.Console.ANSI (+ SGR( SetColor ),+ Color(..),+ ColorIntensity( Vivid ),+ ConsoleLayer( Foreground ),+ hSetSGR )+import System.IO ( Handle, hPutStr, stderr, stdout )++-- | Perform a computation (anything in MonadIO) with the given+-- graphics mode(s) enabled. Revert to the previous graphics mode+-- after the computation has finished.+with_sgr :: (MonadIO m) => Handle -> [SGR] -> m a -> m a+with_sgr h sgrs computation = do+ liftIO $ hSetSGR h sgrs+ x <- computation+ liftIO $ hSetSGR h []+ return x++-- | Perform a computation (anything in MonadIO) with the output set+-- to a certain color. Reset to the default color after the+-- computation has finished.+with_color :: (MonadIO m) => Handle -> Color -> m a -> m a+with_color h color =+ with_sgr h [SetColor Foreground Vivid color]+++-- | Write the given String to a handle in color. The funnyCaps are+-- for synergy with putstrLn and friends.+--+hPutStrColor :: Handle -> Color -> String -> IO ()+hPutStrColor h c = with_color h c . hPutStr h+++-- | Write the given line to a handle in color. The funnyCaps are for+-- synergy with putstrLn and friends.+--+hPutStrColorLn :: Handle -> Color -> String -> IO ()+hPutStrColorLn h c s = hPutStrColor h c (s ++ "\n")+++-- | Display text sent to the feed on the console. Don't automatically+-- append a newline.+--+display_sent :: String -> IO ()+display_sent = hPutStrColor stdout Green+++-- | Display debug text on the console. Don't automatically append a+-- newline in case the raw text is needed for, uh, debugging.+--+display_debug :: String -> IO ()+display_debug = putStr+++-- | Display an informational message on the console.+--+display_info :: String -> IO ()+display_info = hPutStrColorLn stdout Cyan+++-- | Display a warning on the console. Uses stderr instead of stdout.+--+display_warning :: String -> IO ()+display_warning = hPutStrColorLn stderr Yellow+++-- | Display an error on the console. Uses stderr instead of stdout.+--+display_error :: String -> IO ()+display_error = hPutStrColorLn stderr Red
+ src/Unix.hs view
@@ -0,0 +1,118 @@+-- | Non-portable code for daemonizing on unix.+--+module Unix+where++import Control.Concurrent ( ThreadId, myThreadId )+import Control.Exception ( throwTo )+import System.Exit ( ExitCode( ExitSuccess ) )+import System.IO.Error ( catchIOError )+import System.Posix (+ GroupEntry ( groupID ),+ GroupID,+ Handler ( Catch ),+ UserEntry ( userID ),+ UserID,+ exitImmediately,+ getGroupEntryForName,+ getProcessID,+ getRealGroupID,+ getRealUserID,+ getUserEntryForName,+ installHandler,+ removeLink,+ setFileCreationMask,+ setGroupID,+ setUserID,+ sigTERM )+import System.Posix.Daemonize ( daemonize )++import Configuration (+ Configuration( pidfile,+ run_as_group,+ run_as_user ))+import Logging ( log_info, log_error )+import Terminal ( display_error )++-- | Retrieve the uid associated with the given system user name. We+-- take a Maybe String as an argument so the user name can be passed+-- in directly from the config.+--+get_user_id :: Maybe String -> IO UserID+get_user_id Nothing = getRealUserID+get_user_id (Just s) = fmap userID (getUserEntryForName s)+++-- | Retrieve the gid associated with the given system group name. We+-- take a Maybe String as an argument so the group name can be+-- passed in directly from the config.+--+get_group_id :: Maybe String -> IO GroupID+get_group_id Nothing = getRealGroupID+get_group_id (Just s) = fmap groupID (getGroupEntryForName s)+++-- | This function will be called in response to a SIGTERM; i.e. when+-- someone tries to kill our process. We simply delete the PID file+-- and signal our parent thread to quit (successfully).+--+-- If that doesn't work, report the error and quit rudely.+--+graceful_shutdown :: Configuration -> ThreadId -> IO ()+graceful_shutdown cfg main_thread_id = do+ log_info "SIGTERM received, removing PID file and shutting down."+ catchIOError try_nicely (\e -> do+ display_error (show e)+ log_error (show e)+ exitImmediately ExitSuccess )+ where+ try_nicely = do+ removeLink (pidfile cfg)+ throwTo main_thread_id ExitSuccess+++-- | Write a PID file, install a SIGTERM handler, drop privileges, and+-- finally do the daemonization dance.+--+full_daemonize :: Configuration -> IO () -> IO ()+full_daemonize cfg program = do+ -- The call to 'daemonize' will set the umask to zero, but we want+ -- to retain it. So, we set the umask to zero before 'daemonize'+ -- can, so that we can record the previous umask value (returned by+ -- setFileCreationMask).+ orig_umask <- setFileCreationMask 0+ -- This is the 'daemonize' from System.Posix.Daemonize. If it+ -- doesn't work, we report the error and do not much else.+ catchIOError (daemonize (program' orig_umask))+ (\e -> do+ display_error (show e)+ log_error (show e))+ where+ -- We need to do all this stuff *after* we daemonize.+ program' orig_umask = do+ -- First we install a signal handler for sigTERM. We need to+ -- pass the thread ID to the signal handler so it knows which+ -- process to "exit."+ tid <- myThreadId+ _ <- installHandler sigTERM (Catch (graceful_shutdown cfg tid)) Nothing++ -- Next we drop privileges. Group ID has to go first, otherwise+ -- you ain't root to change groups.+ get_group_id (run_as_group cfg) >>= setGroupID+ get_user_id (run_as_user cfg) >>= setUserID++ -- Now we create the PID file.+ pid <- getProcessID++ -- The PID file needs to be read-only for anyone but its+ -- owner. Hopefully the umask accomplishes this!+ _ <- setFileCreationMask orig_umask++ -- When we later attempt to delete the PID file, it requires+ -- write permission to the parent directory and not to the PID+ -- file itself. Therefore, if that's going to work, this has to+ -- work, even as a limited user.+ writeFile (pidfile cfg) (show pid)++ -- Finally run the program we were asked to.+ program
+ test/TestSuite.hs view
@@ -0,0 +1,9 @@+import Test.Tasty ( TestTree, defaultMain )++import TSN.Xml ( xml_tests )++tests :: TestTree+tests = xml_tests++main :: IO ()+main = defaultMain tests
+ test/xml/19908216.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?> <!DOCTYPE message PUBLIC "-//TSN//DTD Scores 1.0/EN" "scoresxml.dtd"> <message> <XML_File_ID>19908216</XML_File_ID> <heading>BC-ABi+070:090* 0 0 0</heading> <game_id>4373</game_id> <schedule_id>4373</schedule_id> <category>Scores</category> <sport>NFL</sport> <location> <city>Tennessee</city> <state>TN</state> <country>USA</country> </location> <location> <city>Jacksonville</city> <state>FL</state> <country>USA</country> </location> <seasontype>Regular</seasontype> <game> <vteam id="070">Tennessee</vteam> <hteam id="090">Jacksonville</hteam> <vscore>0</vscore> <hscore>0</hscore> <status numeral="0" type="i">0 Qtr</status> <notes>Jacksonville - Wide receiver Geno Hayes (12/20, kneeLB) is questionable for Sunday's game against Tennessee.</notes> </game> <time_stamp> December 20, 2013, at 02:59 PM ET </time_stamp> </message>
+ test/xml/19908217.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?> <!DOCTYPE message PUBLIC "-//TSN//DTD Scores 1.0/EN" "scoresxml.dtd"> <message> <XML_File_ID>19908217</XML_File_ID> <heading>BC-ABi+070:090* 0 0 0</heading> <game_id>4373</game_id> <schedule_id>4373</schedule_id> <category>Scores</category> <sport>NFL</sport> <location> <city>Tennessee</city> <state>TN</state> <country>USA</country> </location> <location> <city>Jacksonville</city> <state>FL</state> <country>USA</country> </location> <seasontype>Regular</seasontype> <game> <vteam id="070">Tennessee</vteam> <hteam id="090">Jacksonville</hteam> <vscore>0</vscore> <hscore>0</hscore> <status numeral="0" type="i">0 Qtr</status> <notes>Jacksonville - Running back Maurice Jones-Drew (12/20, hamstring) is questionable for Sunday's game against Tennessee.</notes> </game> <time_stamp> December 20, 2013, at 02:59 PM ET </time_stamp> </message>
+ test/xml/19908245.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?> <!DOCTYPE message PUBLIC "-//TSN//DTD Heartbeat 1.0/EN" "Heartbeat.dtd"> <message> <XML_File_ID>19908245</XML_File_ID> <heading>BC-HBC</heading> <time_stamp> December 20, 2013, at 03:10 PM ET </time_stamp> </message>
+ test/xml/19908246.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?> <!DOCTYPE message PUBLIC "-//TSN//DTD Heartbeat 1.0/EN" "Heartbeat.dtd"> <message> <XML_File_ID>19908246</XML_File_ID> <heading>BC-HBC</heading> <time_stamp> December 20, 2013, at 03:10 PM ET </time_stamp> </message>
+ test/xml/19908247.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?> <!DOCTYPE message PUBLIC "-//TSN//DTD Scores 1.0/EN" "jfilexml.dtd"> <message> <XML_File_ID>19908247</XML_File_ID> <heading>BC-CZJ</heading> <category>JFILE</category> <sport>SOC-NPSL</sport> <gamelist> <game> <game_id>1287</game_id> <schedule_id>1287</schedule_id> <Odds_Info> <ListDate></ListDate> <HomeTeamID></HomeTeamID> <AwayTeamID></AwayTeamID> <HomeAbbr></HomeAbbr> <AwayAbbr></AwayAbbr> <HomeTeamName></HomeTeamName> <AwayTeamName></AwayTeamName> <HStarter></HStarter> <AStarter></AStarter> <GameDate></GameDate> <HGameKey></HGameKey> <AGameKey></AGameKey> <CurrentTimeStamp></CurrentTimeStamp> <Live></Live> <Notes1></Notes1> <Notes2></Notes2> <Notes3></Notes3> <Notes4></Notes4> <Notes5></Notes5> </Odds_Info> <seasontype>Regular</seasontype> <Game_Date>12/20/2013</Game_Date> <Game_Time>07:05 PM</Game_Time> <vteam teamid="226" abbr="BAL">Baltimore</vteam> <hteam teamid="ZX5" abbr="PEN">Pennsylvania</hteam> <vscore>0</vscore> <hscore>0</hscore> <status numeral="0">7:05 PM</status> </game> </gamelist> <time_stamp>December 20, 2013, at 03:10 PM ET </time_stamp> </message>