packages feed

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 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>