diff options
author | Guido Guenther <agx@sigxcpu.org> | 2007-08-23 15:47:20 +0200 |
---|---|---|
committer | Guido Guenther <agx@bogon.sigxcpu.org> | 2007-08-23 15:47:20 +0200 |
commit | e5f5a2ab23ace19c6de39512f76f8fed5f5ad912 (patch) | |
tree | a3a2a1ba762515d37acaf85d9e5c27f81a3ab6c1 |
Imported upstream version 0.6.21upstream/0.6.21debian/0.6.21-0.1upstream
-rw-r--r-- | AUTHORS | 2 | ||||
-rw-r--r-- | COPYING | 340 | ||||
-rw-r--r-- | ChangeLog | 87 | ||||
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | README | 52 | ||||
-rw-r--r-- | debian/changelog | 617 | ||||
-rw-r--r-- | debian/compat | 1 | ||||
-rw-r--r-- | debian/control | 26 | ||||
-rw-r--r-- | debian/copyright | 6 | ||||
-rw-r--r-- | debian/docs | 3 | ||||
-rw-r--r-- | debian/examples | 3 | ||||
-rw-r--r-- | debian/mini-dinstall.preinst | 16 | ||||
-rw-r--r-- | debian/pycompat | 1 | ||||
-rwxr-xr-x | debian/rules | 46 | ||||
-rw-r--r-- | doc/TODO | 2 | ||||
-rw-r--r-- | doc/mini-dinstall.1 | 300 | ||||
-rw-r--r-- | doc/mini-dinstall.conf | 132 | ||||
-rw-r--r-- | doc/mini-dinstall.conf.walters | 17 | ||||
-rwxr-xr-x | examples/sign-release.sh | 52 | ||||
-rw-r--r-- | lib/ChangeFile.py | 109 | ||||
-rw-r--r-- | lib/DebianSigVerifier.py | 35 | ||||
-rw-r--r-- | lib/Dnotify.py | 199 | ||||
-rwxr-xr-x | lib/DpkgControl.py | 156 | ||||
-rw-r--r-- | lib/DpkgDatalist.py | 81 | ||||
-rw-r--r-- | lib/GPGSigVerifier.py | 78 | ||||
-rw-r--r-- | lib/OrderedDict.py | 76 | ||||
-rwxr-xr-x | lib/SafeWriteFile.py | 81 | ||||
-rwxr-xr-x | lib/SignedFile.py | 107 | ||||
-rw-r--r-- | lib/__init__.py | 1 | ||||
-rw-r--r-- | lib/misc.py | 36 | ||||
-rw-r--r-- | lib/version.py | 1 | ||||
-rwxr-xr-x | mini-dinstall | 1483 |
32 files changed, 4165 insertions, 0 deletions
@@ -0,0 +1,2 @@ +Colin Walters <walters@debian.org> +Henning Glawe <glaweh@physik.fu-berlin.de> @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..945cbd3 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,87 @@ +2003-06-11 Colin Walters <walters@verbum.org> + + * configure.ac: Release 0.6.5. + + * mini-dinstall.in: Add - to package architecture regexps. + +2003-05-21 Colin Walters <walters@verbum.org> + + * configure.ac: Release 0.6.4. + + * mini-dinstall.in: Fix missing argument to _install_changefile in + non-daemon mode. + +2003-05-20 Colin Walters <walters@verbum.org> + + * configure.ac: Release 0.6.3. + + * mini-dinstall.in: Make --run option wait until the + Packages/Sources files are generated, too. + +2003-04-15 Colin Walters <walters@debian.org> + + * configure.ac: Release 0.6.2. + + * confiugre.ac: Make --disable-docs option work. + + * mini-dinstall.in: New option chown_changes_files. + + * doc/mini-dinstall.conf: Document incoming_permissions and + chown_changes_files. + +2003-04-15 Henning Glawe <glaweh@physik.fu-berlin.de> + Colin Walters <walters@verbum.org> + + * mini-dinstall.in: Only prepend dinstall_subdir to some paths + like logfile path if they're not already absolute. + +2003-04-02 Colin Walters <walters@verbum.org> + + * doc/mini-dinstall.xml: Add post_upload_command. + + * configure.ac: Release 0.6.1. + +2003-04-01 Tobias Burnus <tobias.burnus@physik.fu-berlin.de> + + * mini-dinstall.in: New option incoming_permissions. + +2003-04-01 Henning Glawe <glaweh@physik.fu-berlin.de> + + * mini-dinstall.in: New option mail_server. + +2003-03-26 Colin Walters <cwalters@gnome.org> + + * configure.ac: Bump version to 0.6.0. + + * mini-dinstall.in: Lots of stuff. We now support a -r + option for rerunning the queue immediately. The -k option now + shuts down the server cleanly instead of killing it outright. + Another nice thing is that if a crash happens in daemon mode, it + will exit cleanly instead of leaving some threads hanging. + + * lib/GPGSigVerifier.py (GPGSigVerifier.verify): Remove debugging + output to /tmp/foo (Fixes Debian bug #184490). + + * lib/Dnotify.py (DirectoryNotifier.__init__): Support a cancel event. + +2003-03-06 Colin Walters <walters@verbum.org> + + * mini-dinstall.in: Also append domain for success mails. + +2003-02-20 Colin Walters <walters@gnu.org> + + * mini-dinstall.in: Default to not generate release file, until + apt can handle it correctly. + +2003-02-16 Colin Walters <walters@gnu.org> + + * mini-dinstall.in: Mail to user@hostname instead of just user. + * Makefile.am: Fix installation libdir to just "minidinstall" instead + of "mini-dinstall", since Python doesn't grok - in module names. + +2003-01-10 Colin Walters <walters@debian.org> + + * Initial generally released version. I have discovered lots of + truly marvellous changes to make to the previous Debian-only build + system, which this ChangeLog is too small to contain. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6e3d233 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +# Makefile for mini-dinstall + +PYTHONVERSION=$(shell pyversions -dv) + +pkgdir = $(DESTDIR)/usr/lib/python$(PYTHONVERSION)/site-packages/minidinstall +bindir = $(DESTDIR)/usr/bin +mandir = $(DESTDIR)/usr/share/man/man1 + +install: + install -o root -g root -m 0755 -d $(pkgdir) $(bindir) $(mandir) + + # install the application + install -o root -g root -m 0644 lib/*.py $(pkgdir) + install -o root -g root -m 0755 mini-dinstall $(bindir) + + # install documentataion + install -o root -g root -m 0644 doc/mini-dinstall.1 $(mandir) + +.PHONY: install @@ -0,0 +1,52 @@ +** IMPORTANT ** +mini-dinstall has some error checking, but it is not perfect. +If it breaks, you get to keep both pieces. +** IMPORTANT ** + +First, you should probably set up a ~/.mini-dinstall.conf (or +/etc/mini-dinstall.conf), but it's not strictly necessary; the +defaults may suit you. Try copying the example file. If you do copy +the example file, be sure to change the "mail_to" variable. + +mini-dinstall can be run either as a daemon (the default), or via +cron. + +To run mini-dinstall as a daemon, just type: mini-dinstall +It will automatically create any directories necessary. You should +have set up a ~/.mini-dinstall.conf first. + +To run via cron: mini-dinstall --batch +Running from cron is less than ideal. Currently there is no support +for rejecting stale uploads; plus, it's less efficient. + +You may find it useful to run with --no-act at first, to see what is +going to happen without making any changes. + +The archive layout is not (yet) configurable. It consists of separate +distribution directories (the defaults are "sid" and "woody"), and +then architecture-specific subdirectories (the defaults are "all", +"i386", "sparc", and "powerpc"). + +Here's what an example apt line looks like: + +deb http://monk.debian.net/~walters/debian/ local/$(ARCH)/ +deb http://monk.debian.net/~walters/debian/ local/all/ +deb-src http://monk.debian.net/~walters/debian/ local/source/ + +In general, this should look like: + +deb <URI prefix>/<mini-dinstall directory>/ <respository>/$(ARCH)/ +deb <URI prefix>/<mini-dinstall directory>/ <respository>/all/ +deb-src <URI prefix>/<mini-dinstall directory>/ <respository>/source/ + +mini-dinstall will pick up on any upload (i.e. .changes) you stick in +an archive directory, and install the file. It will automatically +remove older versions of the package files, with one exception: if +there are both binaries and source for a particular version, and +you're installing a newer version .changes with source and binaries +for a different arch, the old source will not be removed. + +That's it for now...enjoy! + +-- Colin Walters <walters@debian.org> Tue, 13 Aug 2002 15:35:09 -0400 + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..0091f49 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,617 @@ +mini-dinstall (0.6.21-0.1) unstable; urgency=low + + * Non-maintainer upload (with maintainer permission). + * Update package to the new python policy (Closes: #380871). + * Bump Standards-Version to 3.7.2. + * Fix Makefile to use pyversions instead of custom hack to guess current + python version. + * Move debhelper to B-Depends (instead of B-D-I). + + -- Pierre Habouzit <madcoder@debian.org> Tue, 1 Aug 2006 21:13:37 +0200 + +mini-dinstall (0.6.21) unstable; urgency=medium + + * Improve daemonizing: Reopen file descriptors 0, 1, 2 to /dev/null. + This is important as it can corrupt the generated files, which is + why urgency medium is used. + Closes: #294353. + * Fixed a bunch of spelling mistakes in the man page. Closes: #295096. + + -- Thomas Viehmann <tv@beamnet.de> Tue, 15 Feb 2005 18:44:09 +0100 + +mini-dinstall (0.6.20) unstable; urgency=low + + * Autmatically accomodate for the current python version + during build. Closes: #293719. + Idea for patch by Tollef Fog Heen of Canonical/Ubuntu, thanks a lot. + * Fix man page typo. Closes: #271141. + * Rename example file sign-release.sh to match example config. + Closes: #259755. + + -- Thomas Viehmann <tv@beamnet.de> Mon, 7 Feb 2005 21:04:02 +0100 + +mini-dinstall (0.6.19) unstable; urgency=low + + * Important bug fix and very minor cosmetic update release only, maybe this + can make it for sarge. + * Retry a couple of times when dup2 returns EBUSY. (Closes: #265513) + * Change default distributions to unstable (only) and default + architectures to all, i386. (No offence to people using other arches, + but that's my setup.) Closes: #262700. + + -- Thomas Viehmann <tv@beamnet.de> Sun, 15 Aug 2004 11:22:38 +0200 + +mini-dinstall (0.6.18) unstable; urgency=low + + * I'm honored to be the new maintainer. (Thanks Graham.) + * Don't touch packages scheduled for reprocessing when rescanning, + also catch IOError when reprocessing changes to handle removed + changes files. + (Closes: #230325) + * Bumped Standards-Version: 3.6.1 (no changes necessary). + * Included Graham's fix for version.py generation. (Thanks.) + + -- Thomas Viehmann <tv@beamnet.de> Mon, 12 Apr 2004 21:17:20 +0200 + +mini-dinstall (0.6.17) unstable; urgency=low + + * Rebuild with a fixed build tool. (closes: #235411) + + -- Graham Wilson <graham@debian.org> Thu, 04 Mar 2004 00:32:12 +0000 + +mini-dinstall (0.6.16) unstable; urgency=low + + * Call ChangeFile.verify as it was intended. (closes: #228307) + * Add correct index to _archivemap[dist]. (closes: #195541) + * Handle ~ in filenames. (closes: #228745) + + -- Graham Wilson <graham@debian.org> Fri, 30 Jan 2004 07:11:41 +0000 + +mini-dinstall (0.6.15) unstable; urgency=low + + * Fix a typo in the man page. (closes: #230131) + + -- Graham Wilson <graham@debian.org> Wed, 28 Jan 2004 20:27:20 +0000 + +mini-dinstall (0.6.14) unstable; urgency=low + + * Only close fd's if we are daemonizing. (closes: #225439) + + -- Graham Wilson <graham@debian.org> Mon, 29 Dec 2003 22:23:43 +0000 + +mini-dinstall (0.6.13) unstable; urgency=low + + * Integrate the manual into the man page. (closes: #225363) + + -- Graham Wilson <graham@debian.org> Mon, 29 Dec 2003 06:13:47 +0000 + +mini-dinstall (0.6.12) unstable; urgency=low + + * Use consistent indentation. + * Man page corrections. + * Default architectures should be a list, not a tuple. + * Close all file descriptors after daemonizing. (closes: #222693) + + -- Graham Wilson <graham@debian.org> Wed, 17 Dec 2003 22:07:38 +0000 + +mini-dinstall (0.6.11) unstable; urgency=low + + * Add a complete man page. + * Skip over empty lines when reading changes file. (closes: #217548) + + -- Graham Wilson <graham@debian.org> Tue, 18 Nov 2003 16:04:56 +0000 + +mini-dinstall (0.6.10) unstable; urgency=low + + * Automatically generate lib/version.py from debian/rules. + * Call setsid(2) before the second fork, not after. (closes: #217794) + + -- Graham Wilson <graham@debian.org> Tue, 28 Oct 2003 01:18:23 +0000 + +mini-dinstall (0.6.9) unstable; urgency=medium + + * Change documentation from GNU FDL to GPL. (closes: #214488) + * Read dh_python(1): + - Build depend on python. (closes: #215044) + - Use ${python:Depends} in control. + + -- Graham Wilson <graham@debian.org> Fri, 10 Oct 2003 01:15:01 +0000 + +mini-dinstall (0.6.8) unstable; urgency=low + + * Fix minor whitespace problems in mini-dinstall. + * Correct type checking order in event queue. (closes: #212505) + * Explicity depend and build-depend on Python 2.3. + * Change DTD in manpage and manual to locally installed version. + * Use make instead of distutils. What was I thinking? + * Don't propagate exceptions that occur while logging. + - now we dont croak if we can't send email (closes: #213111) + + -- Graham Wilson <graham@debian.org> Mon, 29 Sep 2003 15:44:30 +0000 + +mini-dinstall (0.6.7) unstable; urgency=low + + * Use distutils instead of autotools; debhelper instead of cdbs. + (closes: #211462) + * Create a lib/version.py. Tell mini-dinstall and setup.py to use it. + * New maintainer. Thanks Colin. + * No need to build-dep on python-apt or apt-utils. + + -- Graham Wilson <graham@debian.org> Thu, 18 Sep 2003 02:25:45 +0000 + +mini-dinstall (0.6.6) unstable; urgency=low + + * Package is Debian-native again. + * Python 2.3 transition. + * Call reject_changefile with the correct number of arguments + (Closes: #195769) + + -- Colin Walters <walters@debian.org> Mon, 11 Aug 2003 01:23:25 -0400 + +mini-dinstall (0.6.5-2) unstable; urgency=low + + * debian/control: + - Bump Debhelper Build-Depends to ensure we have dh_python. + + -- Colin Walters <walters@debian.org> Mon, 23 Jun 2003 00:04:12 -0400 + +mini-dinstall (0.6.5-1) unstable; urgency=low + + * New upstream release. + - Adds - to architecture regexp (Closes: #189930). + * debian/rules: + - Use docbookxml.mk CDBS class. + - Don't ship python bytecode files (Closes: #195540). + * debian/mini-dinstall.preinst: + - Remove. + * debian/control: + - Bump Build-Depends on cdbs. + - Depend on python-logging instead of python2.2-logging. + + -- Colin Walters <walters@debian.org> Wed, 11 Jun 2003 04:02:57 -0400 + +mini-dinstall (0.6.4-1) unstable; urgency=low + + * New upstream release. + - Fixes crashing bug in batch mode, or in daemon startup when there + were already packages in the incoming dir (Closes: #194143). + + -- Colin Walters <walters@debian.org> Wed, 21 May 2003 12:05:53 -0400 + +mini-dinstall (0.6.3-1) unstable; urgency=low + + * The "Offin' Office Max" release. + * New upstream release. + * debian/control: + - Add Build-Depends on cdbs. + - Add Build-Depends on python-apt (Closes: #193092). + - Bump Standards-Version to 3.5.10, no changes required. + - Downgrade debian-keyring to a Suggests, for no particular reason. + * debian/rules, debian/rocks: + - Convert to cdbs. + + -- Colin Walters <walters@debian.org> Tue, 20 May 2003 04:07:25 -0400 + +mini-dinstall (0.6.2-2) unstable; urgency=low + + * The "Archaeological Dig Uncovers Ancient Race Of Skeleton People" + release. + * debian/control: + - Add Build-Depends on apt-utils (Closes: #192963). + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Sun, 11 May 2003 18:24:09 -0400 + +mini-dinstall (0.6.2-1) unstable; urgency=low + + * The "insane little dwarf Bush" release. (Courtesy of Mohammed + Saeed Al-Sahaf). + * New upstream release. + - Handles absolute path for logfile (Closes: #189007) + - New variable chown_changes_file (Closes: #188203) + * debian/control: + - Bump Standards-Version to 3.5.9, no changes required. + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Tue, 15 Apr 2003 02:11:22 -0400 + +mini-dinstall (0.6.1-1) unstable; urgency=low + + * New upstream release. + - New variable mail_server (Closes: #187176) + - New variable incoming_permissions (Closes: #187191) + + -- Colin Walters <walters@debian.org> Thu, 3 Apr 2003 00:22:36 -0500 + +mini-dinstall (0.6.0-1) unstable; urgency=low + + * The "Spring break, yay!" release. + * New upstream release. + - A number of goodies, see the upstream ChangeLog. + - Remove debugging output to /tmp/foo (Closes: #184490). + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Wed, 26 Mar 2003 18:02:19 -0500 + +mini-dinstall (0.5.3-1) unstable; urgency=low + + * The "Back that azz up" release. + * New upstream version. + * debian/control: + - Build-Depend on python2.2-logging (Closes: #182197). + - Depend on python2.2-logging instead of python-logging + (Closes: #183306). + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Thu, 6 Mar 2003 16:57:39 -0500 + +mini-dinstall (0.5.2-1) unstable; urgency=low + + * New upstream release. + - Disables Release file generation by default, until apt supports + the flat mode layout better (Closes: #176520). + + -- Colin Walters <walters@debian.org> Thu, 20 Feb 2003 23:38:23 -0500 + +mini-dinstall (0.5.1-1) unstable; urgency=low + + * New upstream release. + - Specify hostname when mailing stuff (Closes: #180271). + * debian/preinst: + - New file; remove old cruft from previous package version. + + -- Colin Walters <walters@debian.org> Sun, 16 Feb 2003 18:10:21 -0500 + +mini-dinstall (0.5.0-1) unstable; urgency=low + + * New upstream version. + - Note, this package is not Debian-native anymore. For + non-Debian-specific changes, please see the upstream changelog. + * debian/control: + - Add a strict dependency on Python 2.2, for both Build-Depends and + Depends. + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Fri, 10 Jan 2003 22:03:59 -0500 + +mini-dinstall (0.4.3) unstable; urgency=low + + * Be compatible with python-logging 0.4.7. + * debian/rules: + - Update to latest version of Colin's Build System. + * debian/control: + - Depend on the latest python-logging. + + -- Colin Walters <walters@debian.org> Fri, 3 Jan 2003 17:05:01 -0500 + +mini-dinstall (0.4.2) unstable; urgency=low + + * Generate Codename field in Release files. + * Don't consider source and other-arch binary packges as "old" when + presented with a binary-only upload (Closes: #173308). + * Change mode of mini-dinstall/incoming directory to 0750 on startup. + + -- Colin Walters <walters@debian.org> Thu, 19 Dec 2002 22:43:00 -0500 + +mini-dinstall (0.4.1) unstable; urgency=low + + * The "Hm...how did this bug sneak by when I tested it" release. + * Fix option parsing to correctly respect DEFAULT section. + * Restore compatibility with Python 2.1. + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Tue, 10 Dec 2002 15:21:06 -0500 + +mini-dinstall (0.4.0) unstable; urgency=low + + * Support for generating Release and Release.gpg. New related options: + generate_release, release_signscript, release_origin, release_label, + release_suite, and release_description. + * New sample signing script: sign-release-file.sh + * New options trigger_reindex and dynamic_reindex; see sample + mini-dinstall.conf for explanation. + * Exit with an error if no archive_style option is specified. You must + now give one of "flat" or "simple-subdir". In the future, the default + will be "flat". + * New command line option --version. Does the obvious thing. + * A fair amount of implementation cleanup (especially as related to + option handling). + * Fix version substitution. + * s/productname/application/ in the manual; this prevents all those + silly ™ characters from appearing. + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Sun, 8 Dec 2002 21:32:16 -0500 + +mini-dinstall (0.3.1) unstable; urgency=low + + * The "Someone should report this ext bug upstream too" release. + * Use work in 0.3.0 to force regeneration of the Packages/Sources files, + even if according to the filesystem they're not changed. This should + really work around the previously mentioned ext bug (Closes: #172275). + * Also force regeneration during the initial run. + * Fix the logic in the FlatArchiveDirIndexer to handle the case where + the Packages/Sources files are nonexistent, and clean up the logic in + the SimpleSubdirArchiveDirIndexer a bit. + * Add a quick Tips and Tricks chapter to the manual that talks about how + to set up dput using the local method. + * debian/rocks: + - Use docbookxml class to disable XML doctype validation. + + -- Colin Walters <walters@debian.org> Sun, 8 Dec 2002 16:34:16 -0500 + +mini-dinstall (0.3.0) unstable; urgency=low + + * The "Everyone should be using XFS" release. + .. or .. + * The "More threads == more fun" release. + * After installing a package, signal the indexing threads to re-index + immediately. This will mostly mitigate the effects of a bug that + strikes the poor users of ext2 and ext3, which doesn't update the + mtime on a directory when renaming a file into it, if there is already + an existing file with that name. Users of Real File Systems were not + affected :) + * debian/control: + - Fix typo in description (Closes: #169549). + - Bump Standards-Version to 3.5.8. + * debian/rules: + - Update to latest version of Colin's Build System. + + -- Colin Walters <walters@debian.org> Fri, 6 Dec 2002 17:44:03 -0500 + +mini-dinstall (0.2.18) unstable; urgency=low + + * debian/rules: + - Update to latest version of Colin's Build System. + * Apply patch from Masato Taruishi <taru@valinux.co.jp> to make the -c + option work (Closes: #170248). + + -- Colin Walters <walters@debian.org> Mon, 25 Nov 2002 11:40:22 -0500 + +mini-dinstall (0.2.17) unstable; urgency=low + + * debian/control: + - Remove Build-Depends on python and python-logging (Closes: #166660). + - Add Build-Depends on debhelper. + * debian/rules: + - Use Colin's Build System. + * Don't install CVS directories in doc dir (Closes: #166286). + * Add Makefile. + * Add the beginnings of a new spiffy XML manual. + * Convert SGML manpage into XML. Now just point users to the manual. + + -- Colin Walters <walters@debian.org> Thu, 14 Nov 2002 17:08:42 -0500 + +mini-dinstall (0.2.16) unstable; urgency=low + + * Pass keychain options down to ArchiveDirs. It would be nice if Python + had a real compiler which allowed one to check for these kinds of + errors. I mean, I like Python (especially the syntax) and all, but + why does "scripting language" imply "no static type analysis" and "no + lexical variable analysis"? Other languages like Dylan handle this + really elegantly, by allowing OPTIONAL type declarations. + Unfortunately the Gwydion Dylan implementation doesn't have stuff like + threading yet. But maybe someday I will rewrite mini-dinstall in it. + Or invent my own non-sucky programming language...but for now, this + (Closes: #165163). + + -- Colin Walters <walters@debian.org> Thu, 17 Oct 2002 16:28:31 -0400 + +mini-dinstall (0.2.15) unstable; urgency=low + + * Changes by Roland Mas <lolando@debian.org>: + - Fixed logic to determine whether to generate Packages and Sources + files. + - Removed a shebang line from a module (makes lintian happy). + * Change documentation to refer to 'verify_sigs' instead of + 'verify_signatures' (Closes: #164992). + + -- Colin Walters <walters@debian.org> Wed, 16 Oct 2002 11:05:09 -0400 + +mini-dinstall (0.2.14) unstable; urgency=low + + * Turn per-distribution options poll_time, max_retry_time, and + mail_on_success into integers. + * Print mode change in octal. + + -- Colin Walters <walters@debian.org> Tue, 15 Oct 2002 13:01:23 -0400 + +mini-dinstall (0.2.13) unstable; urgency=low + + * Fix algorithm for calculating old packages. It was totally broken. + This should really fix #163449. + * Fix typo in exception handler for _install_run_scripts. + * Don't use ACCEPT directory; just put .changes in the toplevel + directory, but chmod them 600 to prevent other people from uploading + the packages. + + -- Colin Walters <walters@debian.org> Tue, 15 Oct 2002 11:51:25 -0400 + +mini-dinstall (0.2.12) unstable; urgency=low + + * Remove all older files with the same names as files in an upload, not + just ones with the same name as the source package (Closes: #163449). + * Fix flat mode; Sorry, joeyh! I promise to test it in the future. + * Don't needlessly generate Packages/Sources files if the mtime on the + directory is older than the files. + * Use "foo in map.keys()" instead of just the much cooler "foo in map" + to be compatible with Python 2.1 (which is what's in woody). + + -- Colin Walters <walters@debian.org> Mon, 14 Oct 2002 01:48:04 -0400 + +mini-dinstall (0.2.11) unstable; urgency=low + + * Default to "simple-subdir" archive style again. We do plan to default + to "flat" in version 0.3.0, but the change shouldn't have been made + yet. + + -- Colin Walters <walters@debian.org> Sun, 13 Oct 2002 09:57:08 -0400 + +mini-dinstall (0.2.10) unstable; urgency=medium + + * The "hopefully Roland won't mailbomb me again :)" release. + * Don't install packages in a separate thread; instead, install them + from the incoming thread (Closes: #164323). It wasn't a useful + optimization, and caused bugs. We do keep around the indexing + threads, however. + * Default to not use dnotify; it is unreliable (Closes: #164387). + * Add better error checking when running md5sum; I think along with the + above changes this will avoid crashing when verifying md5sum output + (Closes: #164297). + * Don't try to call strerror attribute when handling an error + (Closes: #162923). + + -- Colin Walters <walters@debian.org> Sun, 13 Oct 2002 00:49:23 -0400 + +mini-dinstall (0.2.9) unstable; urgency=medium + + * Try not to delete the .orig.tar.gz if we're making Debian-revision + only update (Closes: #159500). + + -- Colin Walters <walters@debian.org> Sun, 15 Sep 2002 20:42:54 -0400 + +mini-dinstall (0.2.8) unstable; urgency=low + + * Just depend on python-logging, not python2.1-logging. + * Don't crash when appending .changes to screwed list. + + -- Colin Walters <walters@debian.org> Sun, 1 Sep 2002 23:06:55 -0400 + +mini-dinstall (0.2.7) unstable; urgency=low + + * Test whether the Distribution: field exists before assuming a .changes + is ready. Why it would fail to to exist is beyond me. Hopefully this + will work around the bug until we find the root cause. + * Ensure .changes with an unknown Distribution get added to the screwed + list. + + -- Colin Walters <walters@debian.org> Sun, 1 Sep 2002 14:53:51 -0400 + +mini-dinstall (0.2.6) unstable; urgency=low + + * Test whether a process in the lockfile pid exists before locking + (Thanks Ivo Timmermans <ivo@o2w.nl> for the hint). + * Don't delay in killing an existing process. + + -- Colin Walters <walters@debian.org> Mon, 26 Aug 2002 20:01:45 -0400 + +mini-dinstall (0.2.5) unstable; urgency=medium + + * mini-dinstall: + Bugs reported by Ivo Timmermans <ivo@o2w.nl>. + - Allow --config= option to actually work. + - Don't lose when trying to access the mail_log_level option. + Other bugs: + - Don't wait on pending installations if IncomingDir is in daemon + mode. + + -- Colin Walters <walters@debian.org> Sun, 25 Aug 2002 14:41:45 -0400 + +mini-dinstall (0.2.4) unstable; urgency=low + + * First upload to Debian proper (Closes: #156582). + * Handle unknown distributions better. + * More error cleanups. + + -- Colin Walters <walters@debian.org> Tue, 20 Aug 2002 00:43:33 -0400 + +mini-dinstall (0.2.3) staging; urgency=low + + * Just Recommend: debian-keyring. + + -- Colin Walters <walters@debian.org> Tue, 20 Aug 2002 00:39:20 -0400 + +mini-dinstall (0.2.2) staging; urgency=low + + * Really make sure IncomingDir handles incomplete uploads. + * Fix bug in cleaning up flat mode archives. + * Depend: on debian-keyring. + * Clean up error handling a bit. + * Add ability to override keyrings. + + -- Colin Walters <walters@debian.org> Tue, 20 Aug 2002 00:11:18 -0400 + +mini-dinstall (0.2.1) staging; urgency=low + + * Try not to spam joeyh. + * Hopefully make rejection work better. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 23:34:19 -0400 + +mini-dinstall (0.2.0) staging; urgency=medium + + * Support for verifying signatures on .changes; enabled by default! + * Fix race condition when installing multiple .changes at the same time. + * Don't try to parse archive_style as an int. + * Fix IncomingDir crashing bug on incomplete uploads. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 22:04:09 -0400 + +mini-dinstall (0.1.9) staging; urgency=low + + * Allow for multiple archive styles; currently "simple-subdir" and + "flat"; this is the config opt "archive_style". + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 16:38:33 -0400 + +mini-dinstall (0.1.1) staging; urgency=low + + * Use relative filenames in Packages file. + * Don't install extra cruft in the doc dir. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 15:24:53 -0400 + +mini-dinstall (0.1.0) staging; urgency=low + + * PRERELEASE + * Almost complete refactoring of the code. Now there is a real + incoming/ directory. This means that instead of uploading to one of + the distribution directories, you now just upload to + $ARCHIVEDIR/incoming, and put the distribution you want in the + debian/changelog. + * Support for mail_log_level = NONE + * Support for mailing you upon installation success. + * First pass at a man page. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 05:05:34 -0400 + +mini-dinstall (0.0.3.2) unstable; urgency=low + + * Hopefully support .udebs. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 00:40:11 -0400 + +mini-dinstall (0.0.3.1) unstable; urgency=low + + * Add / to section regexp + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 00:30:18 -0400 + +mini-dinstall (0.0.3.0) unstable; urgency=low + + * Fix a regexp bug in ChangeFile. + * Support mailing successful installs. + * Support limiting the rate of mailing log entries. + + -- Colin Walters <walters@debian.org> Mon, 19 Aug 2002 00:24:50 -0400 + +mini-dinstall (0.0.2.0) unstable; urgency=low + + * Support pre-installation scripts, mailing on errors, and other fun + stuff. + + -- Colin Walters <walters@debian.org> Sun, 18 Aug 2002 22:34:23 -0400 + +mini-dinstall (0.0.1.0) unstable; urgency=low + + * Initial release (Closes: #156582). + + -- Colin Walters <walters@debian.org> Tue, 13 Aug 2002 03:44:34 -0400 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +4 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..d17fe78 --- /dev/null +++ b/debian/control @@ -0,0 +1,26 @@ +Source: mini-dinstall +Priority: optional +Section: devel +Maintainer: Thomas Viehmann <tv@beamnet.de> +Build-Depends: debhelper (>= 4.1.25) +Build-Depends-Indep: python-dev, python-support (>= 0.3) +Standards-Version: 3.7.2 + +Package: mini-dinstall +Architecture: all +Depends: ${python:Depends}, python-apt, apt-utils +Suggests: debian-keyring +Description: daemon for updating Debian packages in a repository + This program implements a miniature version of the "dinstall" program + which installs packages in the Debian archive. It doesn't require a + PostgreSQL database, and is very easy to set up, maintain, and use. + mini-dinstall can be run via cron, or as a daemon. + . + This package is expressly designed for personal apt repositories, and + the like. In this vein, it contains fewer sanity checks; for + example, it will happily install a lower version of a package. You + can also generally just 'rm' files from the repository, and + mini-dinstall won't care. In fact, (when run as a daemon) it will + automatically detect that the directory changed, and update the + Packages file. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..1d778cf --- /dev/null +++ b/debian/copyright @@ -0,0 +1,6 @@ +Miniature implementation of dinstall in Python. +Copyright © 2002,2003 Colin Walters <walters@debian.org> + +Licensed under the Gnu GPL. + +See /usr/share/common-licenses/GPL on your Debian system. diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..b848402 --- /dev/null +++ b/debian/docs @@ -0,0 +1,3 @@ +doc/TODO +README +AUTHORS diff --git a/debian/examples b/debian/examples new file mode 100644 index 0000000..e53cf4e --- /dev/null +++ b/debian/examples @@ -0,0 +1,3 @@ +examples/sign-release.sh +doc/mini-dinstall.conf +doc/mini-dinstall.conf.walters diff --git a/debian/mini-dinstall.preinst b/debian/mini-dinstall.preinst new file mode 100644 index 0000000..f45a4e7 --- /dev/null +++ b/debian/mini-dinstall.preinst @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +OLDDIR='/usr/lib/site-python/minidinstall' + +if [ "$1" = upgrade ]; then + if [ -d "$OLDDIR" ]; then + printf "Removing old optimized Python files in %s\n" "$OLDDIR" + for i in pyc pyo; do + rm -f "$OLDDIR"/*.$i + done + fi +fi + +#DEBHELPER# diff --git a/debian/pycompat b/debian/pycompat new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/debian/pycompat @@ -0,0 +1 @@ +2 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..71a1e53 --- /dev/null +++ b/debian/rules @@ -0,0 +1,46 @@ +#!/usr/bin/make -f +# $Id: rules 51 2003-12-29 20:02:19Z bob $ +# Sample debian/rules that uses debhelper. +# This file is public domain software, originally written by Joey Hess. + +build: + +lib/version.py: debian/changelog + echo "pkg_version = \"`dpkg-parsechangelog | awk '/^Version:/ { print $$2 }'`\"" > lib/version.py + +clean: lib/version.py + dh_testdir + dh_testroot + rm -f build-stamp + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + $(MAKE) install DESTDIR=debian/mini-dinstall + +# Build architecture-independent files here. +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs + dh_installexamples + dh_link + dh_compress + dh_fixperms + dh_pysupport + dh_python + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb + +# Build architecture-dependent files here. +binary-arch: build install +# We have nothing to do by default. + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install diff --git a/doc/TODO b/doc/TODO new file mode 100644 index 0000000..8708cc9 --- /dev/null +++ b/doc/TODO @@ -0,0 +1,2 @@ +* Support pool structure (maybe)... +* Better docs diff --git a/doc/mini-dinstall.1 b/doc/mini-dinstall.1 new file mode 100644 index 0000000..bd2069f --- /dev/null +++ b/doc/mini-dinstall.1 @@ -0,0 +1,300 @@ +.\" $Id: mini-dinstall.1 59 2004-01-28 20:28:50Z bob $ +.\" +.\" Copyright (C) 2002 Colin Walters <walters@debian.org> +.\" Copyright (C) 2003 Graham Wilson <graham@debian.org> +.\" +.\" This program is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" This program is distributed in the hope that it will be useful, +.\" but WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +.\" GNU General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with this program; if not, write to the Free Software +.\" Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +.TH MINI\-DINSTALL 1 "December 29, 2003" "Debian Project" mini\-dinstall +.\" +.SH NAME +mini\-dinstall \- daemon for updating Debian packages in a repository +.\" +.SH SYNOPSIS +.B mini\-dinstall +[\fIoptions\fP] [\fIdirectory\fP] +.\" +.SH DESCRIPTION +\fBmini\-dinstall\fR is a tool for installing Debian packages into a personal +APT repository; it is very similar to the \fBdinstall\fR tool on auric: it takes +a changes file and installs it into the Debian archive. +.PP +The main focus of operation is a changes file. +This file specifies a set of Debian binary packages, and often contains +a source package too. Changes files are intended to group both Debian source and +binary packages together, so that there is a single file to manipulate when +uploading a package. +.PP +\fBmini-dinstall\fR takes a changes file in its \fIincoming\fR directory +(or on its command line in batch mode), and installs the files it references +into a directory, and sets up Packages and Sources files for use with APT. +.\" +.SH RUNNING +\fBmini\-dinstall\fR can run in one of two modes: batch mode or daemon mode. In +batch mode, the queue is process immediately, and the command exits when it is +done. In daemon mode, which is the default, \fBmini\-dinstall\fR runs in the +background and continually checks the queue, and will process it whenever it +has changed. +.PP +The optional \fIdirectory\fR argument specifies the root directory of the +queue. If no argument is specified, the value from the configuration file is +used. +.PP +The following options can be used: +.TP +\fB\-v\fR, \fB\-\-verbose\fR +display extra information while running +.TP +\fB\-q\fR, \fB\-\-quiet\fR +display as little information as possible +.TP +\fB\-c\fR, \fB\-\-config\fR=\fIFILE\fR +use FILE as the configuration file, instead of \fI~/.mini\-dinstall.conf\fR +.TP +\fB\-d\fR, \fB\-\-debug\fR +output debugging information to the terminal and to the log +.TP +\fB\-\-no\-log\fR +don't write any information to the logs +.TP +\fB\-n\fR, \fB\-\-no\-act\fR +don't perform any changes; useful in combination with the +.B \-v +flag +.TP +\fB\-b\fR, \fB\-\-batch\fR +run in batch mode +.TP +\fB\-r\fR, \fB\-\-run\fR +tell the currently running daemon to process the queue immediately +.TP +\fB\-k\fR, \fB\-\-kill\fR +kill the currently running daemon +.TP +\fB\-\-help\fR +display a short overview of available options +.TP +\fB\-\-version\fR +display the software version +.\" +.SH CONFIGURATION +\fBmini\-dinstall\fR's main configuration file is \fI~/.mini\-dinstall.conf\fP. +The file consists of a number of different sections, each one applying to a +different distribution (which corresponds to the Distribution field in a +changes file). There is also a default section (\fBDEFAULT\fP), which applies +to all distributions. +.PP +Each section can contain any number of +.PP +.RS +name = value +.RE +.PP +combinations, which set a configuration parameter for that distribution (or the +default one). Lists should be separated by commas, strings need only be +enclosed with quotes if they contain spaces or commas, and boolean values +should be 1 for true, and 0 for false. +.PP +The configuration parameters available in the \fBDEFAULT\fR section are as +follows: +.TP +.B archivedir +The root of the \fBmini\-dinstall\fR archive. Must be set, either here or on the +command line. +.TP +.B extra_keyrings +Additional GnuPG keyrings to use for signature verification. +.TP +.B incoming_permissions +The permissions for the \fIincoming\fR directory. \fBmini\-dinstall\fR will +attempt to set the directory's permissions at startup. Defaults to 0750. +.TP +.B keyrings +GnuPG keyrings to use for signature verification of changes files. Setting this +parameter will modify the default list; it is generally better to modify +\fBextra_keyrings\fR instead. Defaults to the keyrings from the debian\-keyring +package. +.TP +.B logfile +The filename (relative to \fBarchivedir\fR) where information will be logged. +Defaults to \*(lqmini-dinstall.log\*(rq. +.TP +.B mail_log_flush_count +Number of log messages after which queued messages will be sent to you. +Defaults to 10. +.TP +.B mail_log_flush_level +The log level upon which to immediately send all queued log messages. Valid +values are the same as for the \fBmail_log_level\fR option. Defaults to +\fBERROR\fR. +.TP +.B mail_log_level +The default log level which is sent to you by email. Valid values include +\fBDEBUG\fR, \fBINFO\fR, \fBWARN\fR, \fBERROR\fR, and \fBCRITICAL\fR. Defaults +to \fBERROR\fR. +.TP +.B mail_to +The user to whom logs should be mailed. Defaults to the current user. +.TP +.B trigger_reindex +In daemon mode, whether or not to recreate the Packages and Sources files after +every upload. If you disable this, you probably want to enable +\fBdynamic_reindex\fR. You may want to disable this if you install a \fIlot\fR +of packages. Defaults to enabled. +.TP +.B use_dnotify +If enabled, uses the \fBdnotify\fR(1) command to monitor directories for +changes. Only relevant if \fBdynamic_reindex\fR is enabled. Defaults to false. +.TP +.B verify_sigs +Whether or not to verify signatures on changes files. Defaults to enabled if +the debian\-keyring package is installed, disabled otherwise. +.\" +.PP +The configuration parameters that can be set in the \fBDEFAULT\fR section and +the distribution-specific sections are: +.TP +.B architectures +A list of architectures to create subdirectories for. Defaults to \*(lqall, i386, +powerpc, sparc\*(rq. +.TP +.B archive_style +Either \*(lqflat\*(rq or \*(lqsimple\-subdir\*(rq. A flat archive style puts all of +the binary packages into one subdirectory, while the simple archive style +splits up the binary packages by architecture. Must be set. +.RS +.PP +Sources for the \(lqflat\(rq style should look like: +.PP +.NF +.RS + deb file:///home/walters/debian/ unstable/ + deb-src file:///home/walters/debian/ unstable/ + deb file:///home/walters/debian/ experimental/ + deb-src file:///home/walters/debian/ experimental/ +.RE +.FI +.PP +Sources for the \(lqsubdir\(rq style should look like: +.PP +.NF +.RS + deb http://localhost/~walters/debian/ local/$(ARCH)/ + deb http://localhost/~walters/debian/ local/all/ + deb-src http://localhost/~walters/debian/ local/source/ +.RE +.FI +.RE +.TP +.B chown_changes_files +Determines if the changes files should be made unreadable by others. This is +enabled by default, and is a good thing, since somebody else could unexpectedly +upload your package. Think carefully before changing this. +.TP +.B dynamic_reindex +If enabled, directories are watched for changes and new Packages and Sources +files are created as needed. Only used in daemon mode. Defaults to true. +.TP +.B generate_release +Causes a Release file to be generated (see \fBrelease_*\fR below) if enabled. +Disabled by default. +.TP +.B keep_old +Whether or not old packages should be kept, instead of deleting them when newer +versions of the same packages are uploaded. Defaults to false. +.TP +.B mail_on_success +Whether to mail on successful installation. Defaults to true. +.TP +.B max_retry_time +The maximum amount of time to wait for an incomplete upload before rejecting +it. Specified in seconds. Defaults to two days. +.TP +.B poll_time +How often to poll directories (in seconds) for changes if \fBdynamic_reindex\fR +is enabled. Defaults to 30 seconds. +.TP +.B post_install_script +This script is run after the changes file is installed, with the full path of +the changes file as its argument. +.TP +.B pre_install_script +This script is run before the changes file is installed, with the full path of +the changes file as its argument. If it exits with an error, the changes file +is skipped. +.TP +.B release_codename +The Codename field in the Release file. Defaults to \*(lqNone\*(rq. +.TP +.B release_description +The Description field in the Release file. Defaults to \*(lqNone\*(rq. +.TP +.B release_label +The Label field in the Release file. Defaults to the current user's username. +.TP +.B release_origin +The Origin field in the Release file. Defaults to the current user's username. +.TP +.B release_suite +The Suite field in the Release file. Defaults to \*(lqNone\*(rq. +.TP +.B release_signscript +If specified, this script will be called to sign Release files. It will be +invoked in the directory containing the Release file, and should accept the +filename of the Release file to sign as the first argument (note that it is +passed a temporary filename, not \fIRelease\fR). It should generate a detached +signature in a file named \fIRelease.gpg\fR. +.\" +.SH "USING DPUT" +One convenient way to use \fBmini-dinstall\fR is in combination with +\fBdput\fR's \(lqlocal\(rq method. The author generally tests his Debian +packages by using \fBdput\fR to upload them to a local repository, and then +uses APT's \(lqfile\(rq method to retrieve them locally. Here's a sample +\fBdput\fR stanza: +.PP +.NF +.RS + [local] + fqdn = space\-ghost.verbum.private + incoming = /src/debian/mini\-dinstall/incoming + method = local + run_dinstall = 0 + post_upload_command = mini\-dinstall \-r +.RE +.FI +.PP +Obviously, you should replace the \(lqfqdn\(rq and \(lqincoming\(rq values with +whatever is appropriate for your machine. Some sample APT methods were listed +in the configuration section. +.PP +Now, all you have to do to test your Debian packages is: +.PP +.NF +.RS + $ dpkg-buildpackage + $ dput local ../program_1.2.3\-1_powerpc.changes + # wait a few seconds + $ apt\-get update + $ apt\-get install program +.RE +.FI +.\" +.SH AUTHOR +.B mini\-dinstall +was originally written by Colin Walters <walters@debian.org> and is now +maintained by Graham Wilson <graham@debian.org>. +.\" +.SH "SEE ALSO" +\fBapt\-get\fR(8), \fBdnotify\fR(1), \fBdput\fR(1), \fBgpg\fI(1) diff --git a/doc/mini-dinstall.conf b/doc/mini-dinstall.conf new file mode 100644 index 0000000..de323ea --- /dev/null +++ b/doc/mini-dinstall.conf @@ -0,0 +1,132 @@ +# Sample mini-dinstall.conf with all the options -*- coding: utf-8; mode: generic -*- + +# Options that apply to all distributions +[DEFAULT] +# The root of the archive. +archivedir = ~/debian/ + +# The default loglevel which is sent to you via email. Valid values +# are taken from the Python logging module: DEBUG, INFO, WARN, ERROR, +# and CRITICAL. You may also use NONE, to avoid email altogether. +mail_log_level = ERROR + +# The user to mail logs to +mail_to = username + +# The loglevel upon which to immediately send you queued log messages. +mail_log_flush_level = ERROR + +# The number of log messages upon which an email will be sent to you. +mail_log_flush_count = 10 + +# Whether or not to trigger a reindex of Packages/Sources files +# immediately after every installation (in daemon mode). If you +# disable this option, you should almost certainly have +# dynamic_reindex enabled. You may want to disable this if you +# install a *lot* of packages. +trigger_reindex = 1 + +# Whether or not to verify GPG signatures on .changes files +verify_sigs = 1 + +# GNUPG keyrings to use for signature verification, separated by +# commas. This will override the builtin keyrings. Generally you +# shouldn't specify this option; use extra_keyrings instead. +keyrings = /usr/share/keyrings/debian-keyring.gpg, /path/to/other/keyring.gpg + +# Additional GNUPG keyrings to use for signature verification, separated by commas +extra_keyrings = ~/.gnupg/pubring.gpg, ~/.gnupg/other.gpg + +# The permissions for the incoming directory. If you want to use +# mini-dinstall for a group of people, you might want to make this +# more permissive. +incoming_permissions = 0750 + +### The remaining options can also be specified in a per-distribution +### basis + +# What architecture subdirectories to create. +architectures = all, i386, sparc, powerpc + +# The style of archive. "flat" is the default; it puts all .debs in +# the archive directory. The other alternative is "simple-subdir", +# which puts .debs from each architecture in a separate subdirectory. +archive_style = flat + +# Whether or not to mail you about successful installations +mail_on_success = 1 + +# Whether or not to delete old packages +keep_old = 0 + +# A script to run before a .changes is installed. It is called with +# the full path to the .changes as an argument. If it exits with an +# error, then the .changes is skipped. +pre_install_script = ~/bin/pre-inst.sh + +# A script to run when a .changes is successfully installed. +# It is called with the full path to the .changes as an argument. +post_install_script = ~/bin/post-inst.sh + +# Whether or not to generate Release files +generate_release = 1 + +# The default Origin: field in the release file +release_origin = username + +# The default Label: field in the release file +release_label = username + +# The default Suite: field in the release file +release_suite = Penthouse + +# The default Description: field in the release file +release_description = My Happy Fun Packages + +# If specified, this script will be called to sign Release files. It +# will be invoked in the directory containing the Release file, and +# should accept the filename of the Release file to sign as the first +# argument (note it is passed a temporary filename, not "Release"). +# It should generate a detached signature in a file named Release.gpg. +release_signscript = ~/bin/sign-release.sh + +# Whether or not to watch directories for changes, and reindex +# Packages/Sources as needed. Only used in daemon mode. +dynamic_reindex = 1 + +# Whether or not to make .changes files unreadable to others by +# default. This will protect you from other people unexpectedly +# uploading your packages. Please think carefully about your security +# before you change this! +chown_changes_files = 1 + +# Whether or not to use /usr/bin/dnotify. This doesn't work on some +# systems, so you might want to disable it. Only used if +# dynamic_reindex is enabled. +use_dnotify = 0 + +# If you use the mtime-polling directory notifier, this is the number +# of seconds in between polls. Only used if dynamic_reindex is +# enabled. +poll_time = 30 + +# The maximum number of seconds to wait for an incomplete upload +# before rejecting it. The default is two days. +max_retry_time = 172800 + +# The following are just some sample distributions, with a few sample +# distribution-specific options. + +[local] +poll_time = 40 + +[woody] +max_retry_time = 30 +keep_old = 1 + +[staging] +post_install_script = ~/bin/staging-post-inst.sh + +[experimental] +architectures = all, i386, sparc, powerpc, ia64, sh4 +keep_old = 1 diff --git a/doc/mini-dinstall.conf.walters b/doc/mini-dinstall.conf.walters new file mode 100644 index 0000000..9f82297 --- /dev/null +++ b/doc/mini-dinstall.conf.walters @@ -0,0 +1,17 @@ +# Colin's mini-dinstall.conf + +[DEFAULT] +architectures = all, i386, sparc, powerpc +archivedir = /src/debian/ +use_dnotify = 0 +verify_sigs = 0 +extra_keyrings = ~/.gnupg/pubring.gpg +mail_on_success = 0 +archive_style = flat +poll_time = 10 + +mail_log_level = NONE + +[unstable] + +[experimental] diff --git a/examples/sign-release.sh b/examples/sign-release.sh new file mode 100755 index 0000000..a3a8331 --- /dev/null +++ b/examples/sign-release.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# -*- coding: utf-8 -*- +# Sample script to GPG sign Release files +# Copyright © 2002 Colin Walters <walters@debian.org> + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Usage: + +# You need to create a secret keyring (secring.gpg). You can use your +# existing one, or create a new one by doing something like the +# following: + +# $ GNUPGHOME=/src/debian/mini-dinstall/s3kr1t gnupg --gen-key + +set -e + +# User variables +# MAKE SURE TO MAKE THIS DIRECTORY 0700! +export GNUPGHOME=/src/debian/mini-dinstall/s3kr1t +if [ ! -d "$GNUPGHOME" ]; then + mkdir -p "$GNUPGHOME" +fi +if [ -z "$USER" ]; then + USER=$(id -n -u) +fi +# This is just a default value +KEYID=$(getent passwd $USER | cut -f 5 -d : | cut -f 1 -d ,) +PASSPHRASE=$(cat "$GNUPGHOME/passphrase") + +# These should fail if for some reason the directory isn't owned by us +chown "$USER" "$GNUPGHOME" +chmod 0700 "$GNUPGHOME" + +# Initialize GPG +gpg --help 1>/dev/null 2>&1 || true + +rm -f Release.gpg.tmp +echo "$PASSPHRASE" | gpg --no-tty --batch --passphrase-fd=0 --default-key "$KEYID" --detach-sign -o Release.gpg.tmp "$1" +mv Release.gpg.tmp Release.gpg diff --git a/lib/ChangeFile.py b/lib/ChangeFile.py new file mode 100644 index 0000000..b74e623 --- /dev/null +++ b/lib/ChangeFile.py @@ -0,0 +1,109 @@ +# ChangeFile + +# A class which represents a Debian change file. + +# Copyright 2002 Colin Walters <walters@gnu.org> + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, re, sys, string, stat, popen2 +import threading, Queue +import logging +from minidinstall import DpkgControl, SignedFile + +class ChangeFileException(Exception): + def __init__(self, value): + self._value = value + def __str__(self): + return `self._value` + +class ChangeFile(DpkgControl.DpkgParagraph): + def __init__(self): + DpkgControl.DpkgParagraph.__init__(self) + self._logger = logging.getLogger("mini-dinstall") + + def load_from_file(self, filename): + f = SignedFile.SignedFile(open(filename)) + self.load(f) + f.close() + + def getFiles(self): + out = [] + try: + files = self['files'] + except KeyError: + return [] + lineregexp = re.compile("^([0-9a-f]{32})[ \t]+(\d+)[ \t]+([-/a-zA-Z0-9]+)[ \t]+([-a-zA-Z0-9]+)[ \t]+([0-9a-zA-Z][-+:.,=~0-9a-zA-Z_]+)$") + for line in files: + if line == '': + continue + match = lineregexp.match(line) + if (match is None): + raise ChangeFileException("Couldn't parse file entry \"%s\" in Files field of .changes" % (line,)) + out.append((match.group(1), match.group(2), match.group(3), match.group(4), match.group(5))) + return out + + def verify(self, sourcedir): + for (md5sum, size, section, prioriy, filename) in self.getFiles(): + self._verify_file_integrity(os.path.join(sourcedir, filename), int(size), md5sum) + + def _verify_file_integrity(self, filename, expected_size, expected_md5sum): + self._logger.debug('Checking integrity of %s' % (filename,)) + try: + statbuf = os.stat(filename) + if not stat.S_ISREG(statbuf[stat.ST_MODE]): + raise ChangeFileException("%s is not a regular file" % (filename,)) + size = statbuf[stat.ST_SIZE] + except OSError, e: + raise ChangeFileException("Can't stat %s: %s" % (filename,e.strerror)) + if size != expected_size: + raise ChangeFileException("File size for %s does not match that specified in .dsc" % (filename,)) + if (self._get_file_md5sum(filename) != expected_md5sum): + raise ChangeFileException("md5sum for %s does not match that specified in .dsc" % (filename,)) + self._logger.debug('Verified md5sum %s and size %s for %s' % (expected_md5sum, expected_size, filename)) + + def _get_file_md5sum(self, filename): + if os.access('/usr/bin/md5sum', os.X_OK): + cmd = '/usr/bin/md5sum %s' % (filename,) + self._logger.debug("Running: %s" % (cmd,)) + child = popen2.Popen3(cmd, 1) + child.tochild.close() + erroutput = child.childerr.read() + child.childerr.close() + if erroutput != '': + child.fromchild.close() + raise ChangeFileException("md5sum returned error output \"%s\"" % (erroutput,)) + (md5sum, filename) = string.split(child.fromchild.read(), None, 1) + child.fromchild.close() + status = child.wait() + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + if os.WIFEXITED(status): + msg = "md5sum exited with error code %d" % (os.WEXITSTATUS(status),) + elif os.WIFSTOPPED(status): + msg = "md5sum stopped unexpectedly with signal %d" % (os.WSTOPSIG(status),) + elif os.WIFSIGNALED(status): + msg = "md5sum died with signal %d" % (os.WTERMSIG(status),) + raise ChangeFileException(msg) + return md5sum.strip() + import md5 + f = open(filename) + md5sum = md5.new() + buf = f.read(8192) + while buf != '': + md5sum.update(buf) + buf = f.read(8192) + return md5sum.hexdigest() + +# vim:ts=4:sw=4:et: diff --git a/lib/DebianSigVerifier.py b/lib/DebianSigVerifier.py new file mode 100644 index 0000000..d441a58 --- /dev/null +++ b/lib/DebianSigVerifier.py @@ -0,0 +1,35 @@ +# DebianSigVerifier -*- mode: python; coding: utf-8 -*- + +# A class for verifying signed files, using Debian keys + +# Copyright © 2002 Colin Walters <walters@gnu.org> + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, re, sys, string, stat, logging +from minidinstall.GPGSigVerifier import GPGSigVerifier + +class DebianSigVerifier(GPGSigVerifier): + _dpkg_ring = '/etc/dpkg/local-keyring.gpg' + def __init__(self, keyrings=None, extra_keyrings=None): + if keyrings is None: + keyrings = ['/usr/share/keyrings/debian-keyring.gpg', '/usr/share/keyrings/debian-keyring.pgp'] + if os.access(self._dpkg_ring, os.R_OK): + keyrings.append(self._dpkg_ring) + if not extra_keyrings is None: + keyrings += extra_keyrings + GPGSigVerifier.__init__(self, keyrings) + +# vim:ts=4:sw=4:et: diff --git a/lib/Dnotify.py b/lib/Dnotify.py new file mode 100644 index 0000000..122e03c --- /dev/null +++ b/lib/Dnotify.py @@ -0,0 +1,199 @@ +# Dnotify -*- mode: python; coding: utf-8 -*- + +# A simple FAM-like beast in Python + +# Copyright © 2002 Colin Walters <walters@gnu.org> + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, re, sys, string, stat, threading, Queue, time +import logging +from minidinstall import misc + +class DnotifyException(Exception): + def __init__(self, value): + self._value = value + def __str__(self): + return `self._value` + +class DirectoryNotifierFactory: + def create(self, dirs, use_dnotify=1, poll_time=30, logger=None, cancel_event=None): + if use_dnotify and os.access('/usr/bin/dnotify', os.X_OK): + if logger: + logger.debug("Using dnotify directory notifier") + return DnotifyDirectoryNotifier(dirs, logger) + else: + if logger: + logger.debug("Using mtime-polling directory notifier") + return MtimeDirectoryNotifier(dirs, poll_time, logger, cancel_event=cancel_event) + +class DnotifyNullLoggingFilter(logging.Filter): + def filter(self, record): + return 0 + +class DirectoryNotifier: + def __init__(self, dirs, logger, cancel_event=None): + self._cwd = os.getcwd() + self._dirs = dirs + if cancel_event is None: + self._cancel_event = threading.Event() + else: + self._cancel_event = cancel_event + if logger is None: + self._logger = logging.getLogger("Dnotify") + self._logger.addFilter(DnotifyNullLoggingFilter()) + else: + self._logger = logger + + def cancelled(self): + return self._cancel_event.isSet() + +class DirectoryNotifierAsyncWrapper(threading.Thread): + def __init__(self, dnotify, queue, logger=None, name=None): + if not name is None: + threading.Thread.__init__(self, name=name) + else: + threading.Thread.__init__(self) + self._eventqueue = queue + self._dnotify = dnotify + if logger is None: + self._logger = logging.getLogger("Dnotify") + self._logger.addFilter(DnotifyNullLoggingFilter()) + else: + self._logger = logger + + def cancel(self): + self._cancel_event.set() + + def run(self): + self._logger.info('Created new thread (%s) for async directory notification' % (self.getName())) + while not self._dnotify.cancelled(): + dir = self._dnotify.poll() + self._eventqueue.put(dir) + self._logger.info('Caught cancel event; async dnotify thread exiting') + +class MtimeDirectoryNotifier(DirectoryNotifier): + def __init__(self, dirs, poll_time, logger, cancel_event=None): + DirectoryNotifier.__init__(self, dirs, logger, cancel_event=cancel_event) + self._changed = [] + self._dirmap = {} + self._polltime = poll_time + for dir in dirs: + self._dirmap[dir] = os.stat(os.path.join(self._cwd, dir))[stat.ST_MTIME] + + def poll(self, timeout=None): + timeout_time = None + if timeout: + timeout_time = time.time() + timeout + while self._changed == []: + if timeout_time and time.time() > timeout_time: + return None + self._logger.debug('Polling...') + for dir in self._dirmap.keys(): + oldtime = self._dirmap[dir] + mtime = os.stat(os.path.join(self._cwd, dir))[stat.ST_MTIME] + if oldtime < mtime: + self._logger.debug('Directory "%s" has changed' % (dir,)) + self._changed.append(dir) + self._dirmap[dir] = mtime + if self._changed == []: + for x in range(self._polltime): + if self._cancel_event.isSet(): + return None + time.sleep(1) + ret = self._changed[0] + self._changed = self._changed[1:] + return ret + +class DnotifyDirectoryNotifier(DirectoryNotifier): + def __init__(self, dirs, logger): + DirectoryNotifier.__init__(self, dirs, logger) + self._queue = Queue.Queue() + dnotify = DnotifyThread(self._queue, self._dirs, self._logger) + dnotify.start() + + def poll(self, timeout=None): + # delete duplicates + i = self._queue.qsize() + self._logger.debug('Queue size: %d', (i,)) + set = {} + while i > 0: + dir = self._queue_get(timeout) + if dir is None: + # We shouldn't have to do this; no one else is reading + # from the queue. But we do it just to be safe. + for key in set.keys(): + self._queue.put(key) + return None + set[dir] = 1 + i -= 1 + for key in set.keys(): + self._queue.put(key) + i = self._queue.qsize() + self._logger.debug('Queue size (after duplicate filter): %d', (i,)) + return self._queue_get(timeout) + + def _queue_get(self, timeout): + if timeout is None: + return self._queue.get() + timeout_time = time.time() + timeout + while 1: + try: + self._queue.get(0) + except Queue.Empty: + if time.time() > timeout_time: + return None + else: + time.sleep(15) + +class DnotifyThread(threading.Thread): + def __init__(self, queue, dirs, logger): + threading.Thread.__init__(self) + self._queue = queue + self._dirs = dirs + self._logger = logger + + def run(self): + self._logger.debug('Starting dnotify reading thread') + (infd, outfd) = os.pipe() + pid = os.fork() + if pid == 0: + os.close(infd) + misc.dup2(outfd, 1) + args = ['dnotify', '-m', '-c', '-d', '-a', '-r'] + list(self._dirs) + ['-e', 'printf', '"{}\\0"'] + os.execv('/usr/bin/dnotify', args) + os.exit(1) + + os.close(outfd) + stdout = os.fdopen(infd) + c = 'x' + while c != '': + curline = '' + c = stdout.read(1) + while c != '' and c != '\0': + curline += c + c = stdout.read(1) + if c == '': + break + self._logger.debug('Directory "%s" changed' % (curline,)) + self._queue.put(curline) + (pid, status) = os.waitpid(pid, 0) + if status is None: + ecode = 0 + else: + ecode = os.WEXITSTATUS(status) + raise DnotifyException("dnotify exited with code %s" % (ecode,)) + +# vim:ts=4:sw=4:et: diff --git a/lib/DpkgControl.py b/lib/DpkgControl.py new file mode 100755 index 0000000..38147c7 --- /dev/null +++ b/lib/DpkgControl.py @@ -0,0 +1,156 @@ +# DpkgControl.py +# +# This module implements control file parsing. +# +# DpkgParagraph is a low-level class, that reads/parses a single paragraph +# from a file object. +# +# DpkgControl uses DpkgParagraph in a loop, pulling out the value of a +# defined key(package), and using that as a key in it's internal +# dictionary. +# +# DpkgSourceControl grabs the first paragraph from the file object, stores +# it in object.source, then passes control to DpkgControl.load, to parse +# the rest of the file. +# +# To test this, pass it a filetype char, a filename, then, optionally, +# the key to a paragraph to display, and if a fourth arg is given, only +# show that field. +# +# Copyright 2001 Adam Heath <doogie@debian.org> +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import re, string +from DpkgDatalist import * +from minidinstall.SignedFile import * +from types import ListType + +class DpkgParagraph(DpkgOrderedDatalist): + caseSensitive = 0 + trueFieldCasing = {} + + def setCaseSensitive( self, value ): self.caseSensitive = value + + def load( self, f ): + "Paragraph data from a file object." + key = None + value = None + while 1: + line = f.readline() + if not line: + return + # skip blank lines until we reach a paragraph + if line == '\n': + if not self: + continue + else: + return + line = line[ :-1 ] + if line[ 0 ] != ' ': + key, value = string.split( line, ":", 1 ) + if value: value = value[ 1: ] + if not self.caseSensitive: + newkey = string.lower( key ) + if not self.trueFieldCasing.has_key( key ): + self.trueFieldCasing[ newkey ] = key + key = newkey + else: + if isinstance( value, ListType ): + value.append( line[ 1: ] ) + else: + value = [ value, line[ 1: ] ] + self[ key ] = value + + def _storeField( self, f, value, lead = " " ): + if isinstance( value, ListType ): + value = string.join( map( lambda v, lead = lead: v and ( lead + v ) or v, value ), "\n" ) + else: + if value: value = lead + value + f.write( "%s\n" % ( value ) ) + + def _store( self, f ): + "Write our paragraph data to a file object" + for key in self.keys(): + value = self[ key ] + if self.trueFieldCasing.has_key( key ): + key = self.trueFieldCasing[ key ] + f.write( "%s:" % key ) + self._storeField( f, value ) + +class DpkgControl(DpkgOrderedDatalist): + + key = "package" + caseSensitive = 0 + + def setkey( self, key ): self.key = key + def setCaseSensitive( self, value ): self.caseSensitive = value + + def _load_one( self, f ): + p = DpkgParagraph( None ) + p.setCaseSensitive( self.caseSensitive ) + p.load( f ) + return p + + def load( self, f ): + while 1: + p = self._load_one( f ) + if not p: break + self[ p[ self.key ] ] = p + + def _store( self, f ): + "Write our control data to a file object" + + for key in self.keys(): + self[ key ]._store( f ) + f.write( "\n" ) + +class DpkgSourceControl( DpkgControl ): + source = None + + def load( self, f ): + f = SignedFile(f) + self.source = self._load_one( f ) + DpkgControl.load( self, f ) + + def __repr__( self ): + return self.source.__repr__() + "\n" + DpkgControl.__repr__( self ) + + def _store( self, f ): + "Write our control data to a file object" + self.source._store( f ) + f.write( "\n" ) + DpkgControl._store( self, f ) + +if __name__ == "__main__": + import sys + types = { 'p' : DpkgParagraph, 'c' : DpkgControl, 's' : DpkgSourceControl } + type = sys.argv[ 1 ] + if not types.has_key( type ): + print "Unknown type `%s'!" % type + sys.exit( 1 ) + file = open( sys.argv[ 2 ], "r" ) + data = types[ type ]() + data.load( file ) + if len( sys.argv ) > 3: + para = data[ sys.argv[ 3 ] ] + if len( sys.argv ) > 4: + para._storeField( sys.stdout, para[ sys.argv[ 4 ] ], "" ) + else: + para._store( sys.stdout ) + else: + data._store( sys.stdout ) + +# vim:ts=4:sw=4:et: diff --git a/lib/DpkgDatalist.py b/lib/DpkgDatalist.py new file mode 100644 index 0000000..0c11612 --- /dev/null +++ b/lib/DpkgDatalist.py @@ -0,0 +1,81 @@ +# DpkgDatalist.py +# +# This module implements DpkgDatalist, an abstract class for storing +# a list of objects in a file. Children of this class have to implement +# the load and _store methods. +# +# Copyright 2001 Wichert Akkerman <wichert@linux.com> +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, sys +from UserDict import UserDict +from OrderedDict import OrderedDict +from minidinstall.SafeWriteFile import SafeWriteFile +from types import StringType + +class DpkgDatalistException(Exception): + UNKNOWN = 0 + SYNTAXERROR = 1 + + def __init__(self, message="", reason=UNKNOWN, file=None, line=None): + self.message=message + self.reason=reason + self.filename=file + self.line=line + +class _DpkgDatalist: + def __init__(self, fn=""): + '''Initialize a DpkgDatalist object. An optional argument is a + file from which we load values.''' + + self.filename=fn + if self.filename: + self.load(self.filename) + + def store(self, fn=None): + "Store variable data in a file." + + if fn==None: + fn=self.filename + # Special case for writing to stdout + if not fn: + self._store(sys.stdout) + return + + # Write to a temporary file first + if type(fn) == StringType: + vf=SafeWriteFile(fn+".new", fn, "w") + else: + vf=fn + try: + self._store(vf) + finally: + if type(fn) == StringType: + vf.close() + + +class DpkgDatalist(UserDict, _DpkgDatalist): + def __init__(self, fn=""): + UserDict.__init__(self) + _DpkgDatalist.__init__(self, fn) + + +class DpkgOrderedDatalist(OrderedDict, _DpkgDatalist): + def __init__(self, fn=""): + OrderedDict.__init__(self) + _DpkgDatalist.__init__(self, fn) + +# vim:ts=4:sw=4:et: diff --git a/lib/GPGSigVerifier.py b/lib/GPGSigVerifier.py new file mode 100644 index 0000000..78aeebb --- /dev/null +++ b/lib/GPGSigVerifier.py @@ -0,0 +1,78 @@ +# GPGSigVerifier -*- mode: python; coding: utf-8 -*- + +# A class for verifying signed files + +# Copyright © 2002 Colin Walters <walters@gnu.org> + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, re, sys, string, stat +from minidinstall import misc + +class GPGSigVerifierException(Exception): + def __init__(self, value): + self._value = value + def __str__(self): + return `self._value` + +class GPGSigVerificationFailure(Exception): + def __init__(self, value, output): + self._value = value + self._output = output + def __str__(self): + return `self._value` + + def getOutput(self): + return self._output + +class GPGSigVerifier: + def __init__(self, keyrings, gpgv=None): + self._keyrings = keyrings + if gpgv is None: + gpgv = '/usr/bin/gpgv' + if not os.access(gpgv, os.X_OK): + raise GPGSigVerifierException("Couldn't execute \"%s\"" % (gpgv,)) + self._gpgv = gpgv + + def verify(self, filename, sigfilename=None): + (stdin, stdout) = os.pipe() + pid = os.fork() + if pid == 0: + os.close(stdin) + misc.dup2(stdout, 1) + misc.dup2(stdout, 2) + args = [] + for keyring in self._keyrings: + args.append('--keyring') + args.append(keyring) + if sigfilename: + args.append(sigfilename) + args = [self._gpgv] + args + [filename] + os.execv(self._gpgv, args) + os.exit(1) + os.close(stdout) + output = os.fdopen(stdin).readlines() + (pid, status) = os.waitpid(pid, 0) + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + if os.WIFEXITED(status): + msg = "gpgv exited with error code %d" % (os.WEXITSTATUS(status),) + elif os.WIFSTOPPED(status): + msg = "gpgv stopped unexpectedly with signal %d" % (os.WSTOPSIG(status),) + elif os.WIFSIGNALED(status): + msg = "gpgv died with signal %d" % (os.WTERMSIG(status),) + raise GPGSigVerificationFailure(msg, output) + return output + +# vim:ts=4:sw=4:et: diff --git a/lib/OrderedDict.py b/lib/OrderedDict.py new file mode 100644 index 0000000..fa3f276 --- /dev/null +++ b/lib/OrderedDict.py @@ -0,0 +1,76 @@ +# OrderedDict.py +# +# This class functions almost exactly like UserDict. However, when using +# the sequence methods, it returns items in the same order in which they +# were added, instead of some random order. +# +# Copyright 2001 Adam Heath <doogie@debian.org> +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +from UserDict import UserDict + +class OrderedDict(UserDict): + __order=[] + + def __init__(self, dict=None): + UserDict.__init__(self) + self.__order=[] + if dict is not None and dict.__class__ is not None: + self.update(dict) + + def __cmp__(self, dict): + if isinstance(dict, OrderedDict): + ret=cmp(self.__order, dict.__order) + if not ret: + ret=UserDict.__cmp__(self, dict) + return ret + else: + return UserDict.__cmp__(self, dict) + + def __setitem__(self, key, value): + if not self.has_key(key): + self.__order.append(key) + UserDict.__setitem__(self, key, value) + + def __delitem__(self, key): + if self.has_key(key): + del self.__order[self.__order.index(key)] + UserDict.__delitem__(self, key) + + def clear(self): + self.__order=[] + UserDict.clear(self) + + def copy(self): + if self.__class__ is OrderedDict: + return OrderedDict(self) + import copy + return copy.copy(self) + + def keys(self): + return self.__order + + def items(self): + return map(lambda x, self=self: (x, self.__getitem__(x)), self.__order) + + def values(self): + return map(lambda x, self=self: self.__getitem__(x), self.__order) + + def update(self, dict): + for k, v in dict.items(): + self.__setitem__(k, v) + +# vim:ts=4:sw=4:et: diff --git a/lib/SafeWriteFile.py b/lib/SafeWriteFile.py new file mode 100755 index 0000000..1777d36 --- /dev/null +++ b/lib/SafeWriteFile.py @@ -0,0 +1,81 @@ +# SafeWriteFile.py +# +# This file is a writable file object. It writes to a specified newname, +# and when closed, renames the file to the realname. If the object is +# deleted, without being closed, this rename isn't done. If abort() is +# called, it also disables the rename. +# +# Copyright 2001 Adam Heath <doogie@debian.org> +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +from types import StringType +from shutil import copy2 +from string import find +from os import rename + +class ObjectNotAllowed(Exception): + pass + + +class InvalidMode(Exception): + pass + + +class SafeWriteFile: + def __init__(self, newname, realname, mode="w", bufsize=-1): + + if type(newname)!=StringType: + raise ObjectNotAllowed(newname) + if type(realname)!=StringType: + raise ObjectNotAllowed(realname) + + if find(mode, "r")>=0: + raise InvalidMode(mode) + if find(mode, "a")>=0 or find(mode, "+") >= 0: + copy2(realname, newname) + self.fobj=open(newname, mode, bufsize) + self.newname=newname + self.realname=realname + self.__abort=0 + + def close(self): + self.fobj.close() + if not (self.closed and self.__abort): + rename(self.newname, self.realname) + + def abort(self): + self.__abort=1 + + def __del__(self): + self.abort() + del self.fobj + + def __getattr__(self, attr): + try: + return self.__dict__[attr] + except: + return eval("self.fobj." + attr) + + +if __name__ == "__main__": + import time + f=SafeWriteFile("sf.new", "sf.data") + f.write("test\n") + f.flush() + time.sleep(1) + f.close() + +# vim:ts=4:sw=4:et: diff --git a/lib/SignedFile.py b/lib/SignedFile.py new file mode 100755 index 0000000..648186e --- /dev/null +++ b/lib/SignedFile.py @@ -0,0 +1,107 @@ +# SignedFile -*- mode: python; coding: utf-8 -*- + +# SignedFile offers a subset of file object operations, and is +# designed to transparently handle files with PGP signatures. + +# Copyright © 2002 Colin Walters <walters@gnu.org> +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import re,string + +class SignedFile: + _stream = None + _eof = 0 + _signed = 0 + _signature = None + _signatureversion = None + _initline = None + def __init__(self, stream): + self._stream = stream + line = stream.readline() + if (line == "-----BEGIN PGP SIGNED MESSAGE-----\n"): + self._signed = 1 + while (1): + line = stream.readline() + if (len(line) == 0 or line == '\n'): + break + else: + self._initline = line + + def readline(self): + if self._eof: + return '' + if self._initline: + line = self._initline + self._initline = None + else: + line = self._stream.readline() + if not self._signed: + return line + elif line == "-----BEGIN PGP SIGNATURE-----\n": + self._eof = 1 + self._signature = [] + self._signatureversion = self._stream.readline() + self._stream.readline() # skip blank line + while 1: + line = self._stream.readline() + if len(line) == 0 or line == "-----END PGP SIGNATURE-----\n": + break + self._signature.append(line) + self._signature = string.join + return '' + return line + + def readlines(self): + ret = [] + while 1: + line = self.readline() + if (line != ''): + ret.append(line) + else: + break + return ret + + def close(self): + self._stream.close() + + def getSigned(self): + return self._signed + + def getSignature(self): + return self._signature + + def getSignatureVersion(self): + return self._signatureversion + +if __name__=="__main__": + import sys + if len(sys.argv) == 0: + print "Need one file as an argument" + sys.exit(1) + filename = sys.argv[1] + f=SignedFile(open(filename)) + if f.getSigned(): + print "**** SIGNED ****" + else: + print "**** NOT SIGNED ****" + lines=f.readlines() + print lines + if not f.getSigned(): + assert(len(lines) == len(actuallines)) + else: + print "Signature: %s" % (f.getSignature()) + +# vim:ts=4:sw=4:et: diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/__init__.py @@ -0,0 +1 @@ + diff --git a/lib/misc.py b/lib/misc.py new file mode 100644 index 0000000..eb52d00 --- /dev/null +++ b/lib/misc.py @@ -0,0 +1,36 @@ +# misc -*- mode: python; coding: utf-8 -*- + +# misc tools for mini-dinstall + +# Copyright © 2004 Thomas Viehmann <tv@beamnet.de> + +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os, errno, time + +def dup2(fd,fd2): + # dup2 with EBUSY retries (cf. dup2(2) and Debian bug #265513) + success = 0 + tries = 0 + while (not success): + try: + os.dup2(fd,fd2) + success = 1 + except OSError, e: + if (e.errno != errno.EBUSY) or (tries >= 3): + raise + # wait 0-2 seconds befor next try + time.sleep(tries) + tries += 1 diff --git a/lib/version.py b/lib/version.py new file mode 100644 index 0000000..316c536 --- /dev/null +++ b/lib/version.py @@ -0,0 +1 @@ +pkg_version = "0.6.21-0.1" diff --git a/mini-dinstall b/mini-dinstall new file mode 100755 index 0000000..e8386a2 --- /dev/null +++ b/mini-dinstall @@ -0,0 +1,1483 @@ +#!/usr/bin/python +# -*- mode: python; coding: utf-8 -*- +# Miniature version of "dinstall", for installing .changes into an +# archive +# Copyright © 2002,2003 Colin Walters <walters@gnu.org> + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os, sys, re, glob, getopt, time, traceback, gzip, getpass, socket +import signal, threading, select, Queue, SocketServer +import logging, logging.handlers +#logging.basicConfig() +import apt_pkg +apt_pkg.init() +from ConfigParser import * + +from minidinstall.ChangeFile import * +from minidinstall.Dnotify import * +from minidinstall.DebianSigVerifier import * +from minidinstall.GPGSigVerifier import * +from minidinstall.version import * +from minidinstall import misc + +debchanges_re = re.compile('([-a-z0-9+.]+)_(.+?)_([-a-zA-Z0-9]+)\.changes$') +debpackage_re = re.compile('([-a-z0-9+.]+)_(.+?)_([-a-zA-Z0-9]+)\.u?deb$') +debsrc_dsc_re = re.compile('([-a-z0-9+.]+)_(.+?)\.dsc$') +debsrc_diff_re = re.compile('([-a-z0-9+.]+)_(.+?)\.diff\.gz$') +debsrc_orig_re = re.compile('([-a-z0-9+.]+)_(.+?)\.orig\.tar\.gz$') +debsrc_native_re = re.compile('([-a-z0-9+.]+)_(.+?)\.tar\.gz$') + +native_version_re = re.compile('\s*.*-'); + +toplevel_directory = None +tmp_new_suffix = '.dinstall-new' +tmp_old_suffix = '.dinstall-old' +dinstall_subdir = 'mini-dinstall' +incoming_subdir = 'incoming' +socket_name = 'master' +logfile_name = 'mini-dinstall.log' +configfile_names = ['/etc/mini-dinstall.conf', '~/.mini-dinstall.conf'] +use_dnotify = 0 +mail_on_success = 1 +default_poll_time = 30 +default_max_retry_time = 2 * 24 * 60 * 60 +default_mail_log_level = logging.ERROR +trigger_reindex = 1 +mail_log_flush_level = logging.ERROR +mail_log_flush_count = 10 +mail_to = getpass.getuser() +mail_server = 'localhost' +incoming_permissions = 0750 + +default_architectures = ["all", "i386"] +default_distributions = ("unstable",) + +distributions = {} +scantime = 60 + +def usage(ecode, ver_only=None): + print "mini-dinstall", pkg_version + if ver_only: + sys.exit(ecode) + print "Copyright (C) 2002 Colin Walters <walters@gnu.org>" + print "Licensed under the GNU GPL." + print "Usage: mini-dinstall [OPTIONS...] [DIRECTORY]" + print "Options:" + print " -v, --verbose\t\tDisplay extra information" + print " -q, --quiet\t\tDisplay less information" + print " -c, --config=FILE\tParse configuration info from FILE" + print " -d, --debug\t\tOutput information to stdout as well as log" + print " --no-log\t\tDon't write information to log file" + print " -n, --no-act\t\tDon't actually perform changes" + print " -b, --batch\t\tDon't daemonize; run once, then exit" + print " -r, --run\t\tProcess queue immediately" + print " -k, --kill\t\tKill the running mini-dinstall" + print " --help\t\tWhat you're looking at" + print " --version\t\tPrint the software version and exit" + sys.exit(ecode) + +try: + opts, args = getopt.getopt(sys.argv[1:], 'vqc:dnbrk', + ['verbose', 'quiet', 'config=', 'debug', 'no-log', + 'no-act', 'batch', 'run', 'kill', 'help', 'version', ]) +except getopt.GetoptError, e: + sys.stderr.write("Error reading arguments: %s\n" % e) + usage(1) +for (key, val) in opts: + if key == '--help': + usage(0) + elif key == '--version': + usage(0, ver_only=1) +if len(args) > 1: + sys.stderr.write("Unknown arguments: %s\n" % args[1:]) + usage(1) + +# don't propagate exceptions that happen while logging +logging.raiseExceptions = 0 + +logger = logging.getLogger("mini-dinstall") + +loglevel = logging.WARN +no_act = 0 +debug_mode = 0 +run_mode = 0 +kill_mode = 0 +no_log = 0 +batch_mode = 0 +custom_config_files = 0 +for key, val in opts: + if key in ('-v', '--verbose'): + if loglevel == logging.INFO: + loglevel = logging.DEBUG + elif loglevel == logging.WARN: + loglevel = logging.INFO + elif key in ('-q', '--quiet'): + if loglevel == logging.WARN: + loglevel = logging.ERROR + elif loglevel == logging.WARN: + loglevel = logging.CRITICAL + elif key in ('-c', '--config'): + if not custom_config_files: + custom_config_files = 1 + configfile_names = [] + configfile_names.append(os.path.abspath(os.path.expanduser(val))) + elif key in ('-n', '--no-act'): + no_act = 1 + elif key in ('-d', '--debug'): + debug_mode = 1 + elif key in ('--no-log',): + no_log = 1 + elif key in ('-b', '--batch'): + batch_mode = 1 + elif key in ('-r', '--run'): + run_mode = 1 + elif key in ('-k', '--kill'): + kill_mode = 1 + +def do_mkdir(name): + if os.access(name, os.X_OK): + return + try: + logger.info('Creating directory "%s"' % (name)) + except: + pass + if not no_act: + os.mkdir(name) + +def do_rename(source, target): + try: + logger.debug('Renaming "%s" to "%s"' % (source, target)) + except: + pass + if not no_act: + os.rename(source, target) + +def do_chmod(name, mode): + try: + logger.info('Changing mode of "%s" to %o' % (name, mode)) + except: + pass + if not no_act: + os.chmod(name, mode) + +logger.setLevel(logging.DEBUG) +stderr_handler = logging.StreamHandler(strm=sys.stderr) +stderr_handler.setLevel(loglevel) +logger.addHandler(stderr_handler) +stderr_handler.setLevel(loglevel) +stderr_handler.setFormatter(logging.Formatter(fmt="%(name)s [%(thread)d] %(levelname)s: %(message)s")) + +configp = ConfigParser() +configfile_names = map(lambda x: os.path.abspath(os.path.expanduser(x)), configfile_names) +logger.debug("Reading config files: %s" % (configfile_names,)) +configp.read(configfile_names) + +class SubjectSpecifyingLoggingSMTPHandler(logging.handlers.SMTPHandler): + def __init__(self, subject, *args, **kwargs): + self._subject = subject + apply(logging.handlers.SMTPHandler.__init__, [self] + list(args) + ['dummy'], kwargs) + + def getSubject(self, record): + return re.sub('%l', record.levelname, self._subject) + +if not (configp.has_option('DEFAULT', 'mail_log_level') and configp.get('DEFAULT', 'mail_log_level') == 'NONE'): + if configp.has_option('DEFAULT', 'mail_log_level'): + mail_log_level = logging.__dict__[configp.get('DEFAULT', 'mail_log_level')] + else: + mail_log_level = default_mail_log_level + if configp.has_option('DEFAULT', 'mail_to'): + mail_to = configp.get('DEFAULT', 'mail_to') + if configp.has_option('DEFAULT', 'mail_server'): + mail_server = configp.get('DEFAULT', 'mail_server') + if configp.has_option('DEFAULT', 'mail_log_flush_count'): + mail_log_flush_count = configp.getint('DEFAULT', 'mail_log_flush_count') + if configp.has_option('DEFAULT', 'mail_log_flush_level'): + mail_log_flush_level = logging.__dict__[configp.get('DEFAULT', 'mail_log_flush_level')] + mail_smtp_handler = SubjectSpecifyingLoggingSMTPHandler('mini-dinstall log notice (%l)', mail_server, 'Mini-Dinstall <%s@%s>' % (getpass.getuser(),socket.gethostname()), [mail_to]) + mail_handler = logging.handlers.MemoryHandler(mail_log_flush_count, flushLevel=mail_log_flush_level, target=mail_smtp_handler) + + mail_handler.setLevel(mail_log_level) + logger.addHandler(mail_handler) + +if configp.has_option('DEFAULT', 'archivedir'): + toplevel_directory = os.path.expanduser(configp.get('DEFAULT', 'archivedir')) +elif len(args) > 0: + toplevel_directory = args[0] +else: + logger.error("No archivedir specified on command line or in config files.") + sys.exit(1) + +if configp.has_option('DEFAULT', 'incoming_permissions'): + incoming_permissions = int(configp.get('DEFAULT', 'incoming_permissions'), 8) + +do_mkdir(toplevel_directory) +dinstall_subdir = os.path.join(toplevel_directory, dinstall_subdir) +do_mkdir(dinstall_subdir) + +lockfilename = os.path.join(dinstall_subdir, 'mini-dinstall.lock') + +def process_exists(pid): + try: + os.kill(pid, 0) + except OSError, e: + return 0 + return 1 + +if os.access(lockfilename, os.R_OK): + pid = int(open(lockfilename).read()) + if not process_exists(pid): + if run_mode: + logger.error("No process running at %d; use mini-dinstall -k to remove lockfile") + sys.exit(1) + logger.warn("No process running at %d, removing lockfile" % (pid,)) + os.unlink(lockfilename) + if kill_mode: + sys.exit(0) + +if not os.path.isabs(socket_name): + socket_name = os.path.join(dinstall_subdir, socket_name) + +if run_mode or kill_mode: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + logger.debug('Connecting...') + sock.connect(socket_name) + if run_mode: + logger.debug('Sending RUN command') + sock.send('RUN\n') + else: + logger.debug('Sending DIE command') + sock.send('DIE\n') + logger.debug('Reading response') + response = sock.recv(8192) + print response + sys.exit(0) + +if configp.has_option('DEFAULT', 'logfile'): + logfile_name = configp.get('DEFAULT', 'logfile') + +if not no_log: + if not os.path.isabs(logfile_name): + logfile_name = os.path.join(dinstall_subdir, logfile_name) + logger.debug("Adding log file: %s" % (logfile_name,)) + filehandler = logging.FileHandler(logfile_name) + if loglevel == logging.WARN: + filehandler.setLevel(logging.INFO) + else: + filehandler.setLevel(logging.DEBUG) + logger.addHandler(filehandler) + filehandler.setFormatter(logging.Formatter(fmt="%(asctime)s %(name)s [%(thread)d] %(levelname)s: %(message)s", datefmt="%b %d %H:%M:%S")) + +logger.info('Booting mini-dinstall ' + pkg_version) + +class DinstallException(Exception): + def __init__(self, value): + self._value = value + def __str__(self): + return `self._value` + +if not configp.has_option('DEFAULT', 'archive_style'): + logger.critical("You must set the default archive_style option (since version 0.4.0)") + logging.shutdown() + sys.exit(1) + +default_verify_sigs = os.access('/usr/share/keyrings/debian-keyring.gpg', os.R_OK) +default_extra_keyrings = [] +default_keyrings = None + +if configp.has_option('DEFAULT', 'architectures'): + default_architectures = string.split(configp.get('DEFAULT', 'architectures'), ', ') +if configp.has_option('DEFAULT', 'verify_sigs'): + default_verify_sigs = configp.getboolean('DEFAULT', 'verify_sigs') +if configp.has_option('DEFAULT', 'trigger_reindex'): + default_trigger_reindex = configp.getboolean('DEFAULT', 'trigger_reindex') +if configp.has_option('DEFAULT', 'poll_time'): + default_poll_time = configp.getint('DEFAULT', 'poll_time') +if configp.has_option('DEFAULT', 'max_retry_time'): + default_max_retry_time = configp.getint('DEFAULT', 'max_retry_time') +if configp.has_option('DEFAULT', 'extra_keyrings'): + default_extra_keyrings = re.split(', ?', configp.get('DEFAULT', 'extra_keyrings')) +if configp.has_option('DEFAULT', 'keyrings'): + default_keyrings = re.split(', ?', configp.get('DEFAULT', 'keyrings')) +if configp.has_option('DEFAULT', 'use_dnotify'): + use_dnotify = configp.getboolean('DEFAULT', 'use_dnotify') + +sects = configp.sections() +if not len(sects) == 0: + for sect in sects: + distributions[sect] = {} + if configp.has_option(sect, "architectures"): + distributions[sect]["arches"] = string.split(configp.get(sect, "architectures"), ', ') + else: + distributions[sect]["arches"] = default_architectures +else: + for dist in default_distributions: + distributions[dist] = {"arches": default_architectures} + +class DistOptionHandler: + def __init__(self, distributions, configp): + self._configp = configp + self._distributions = distributions + self._optionmap = {} + self._optionmap['poll_time'] = ['int', default_poll_time] + # two days + self._optionmap['max_retry_time'] = ['int', default_max_retry_time] + self._optionmap['post_install_script'] = ['str', None] + self._optionmap['pre_install_script'] = ['str', None] + self._optionmap['dynamic_reindex'] = ['bool', 1] + self._optionmap['chown_changes_files'] = ['bool', 1] + self._optionmap['keep_old'] = ['bool', None] + self._optionmap['mail_on_success'] = ['bool', 1] + self._optionmap['archive_style'] = ['str', None] + # Release file stuff + self._optionmap['generate_release'] = ['bool', 0] + self._optionmap['release_origin'] = ['str', getpass.getuser()] + self._optionmap['release_label'] = ['str', self._optionmap['release_origin'][1]] + self._optionmap['release_suite'] = ['str', None] + self._optionmap['release_codename'] = ['str', None] + self._optionmap['release_description'] = ['str', None] + self._optionmap['release_signscript'] = ['str', None] + + def get_option_map(self, dist): + ret = self._distributions[dist] + for key in self._optionmap.keys(): + type = self._optionmap[key][0] + ret[key] = self._optionmap[key][1] + if self._configp.has_option ('DEFAULT', key): + ret[key] = self.get_option (type, 'DEFAULT', key) + if self._configp.has_option (dist, key): + ret[key] = self.get_option (type, dist, key) + return ret + + def get_option (self, type, dist, key): + if type == 'int': + return self._configp.getint(dist, key) + elif type == 'str': + return self._configp.get(dist, key) + elif type == 'bool': + return self._configp.getboolean(dist, key) + + assert(None) + + +distoptionhandler = DistOptionHandler(distributions, configp) + +for dist in distributions.keys(): + distributions[dist] = distoptionhandler.get_option_map(dist) + if not distributions[dist]['archive_style'] in ('simple-subdir', 'flat'): + raise DinstallException("Unknown archive style \"%s\"" % (distributions[dist]['archive_style'],)) + +logger.debug("Distributions: %s" % (distributions,)) + +# class DinstallTransaction: +# def __init__(self, dir): +# self._dir = dir + +# def start(self, pkgname): +# self._pkgname = pkgname +# self._transfilename = os.path.join(dir, pkgname + ".transaction") + +# def _log_op(self, type, state, str): +# tmpfile = self._transfilename + ".tmp" +# if (os.access(self._transfilename), os.R_OK): +# shutil.copyFile(self._transfilename, tmpfile) +# f = open(tmpfile, 'w') +# f.write('%s %s' % (type, str) ) +# f.close() + +# def _start_op(self, type, str): +# self._log_op(type, 'start', str) + +# def _stop_op(self, type, str): +# self._log_op(type, 'stop', str) + +# def renameFile(self, source, dst): +# self._start_op('rename', + + +# def _sync(): +# os.system("sync") +os.chdir(toplevel_directory) +do_mkdir(dinstall_subdir) +rejectdir = os.path.join(dinstall_subdir, 'REJECT') +incoming_subdir = os.path.join(dinstall_subdir, incoming_subdir) +do_mkdir(rejectdir) +do_mkdir(incoming_subdir) +do_chmod(incoming_subdir, incoming_permissions) + +## IPC stuff +# Used by all threads to determine whether or not they should exit +die_event = threading.Event() + +# These global variables are used in IncomingDir::daemonize +# I couldn't figure out any way to pass state to a BaseRequestHandler. +reprocess_needed = threading.Event() +reprocess_finished = threading.Event() + +reprocess_lock = threading.Lock() +class IncomingDirRequestHandler(SocketServer.StreamRequestHandler, SocketServer.BaseRequestHandler): + def handle(self): + logger.debug('Got request from %s' % (self.client_address,)) + req = self.rfile.readline() + if req == 'RUN\n': + logger.debug('Doing RUN command') + reprocess_lock.acquire() + reprocess_needed.set() + logger.debug('Waiting on reprocessing') + reprocess_finished.wait() + reprocess_finished.clear() + reprocess_lock.release() + self.wfile.write('200 Reprocessing complete\n') + elif req == 'DIE\n': + logger.debug('Doing DIE command') + self.wfile.write('200 Beginning shutdown\n') + die_event.set() + else: + logger.debug('Got unknown command %s' % (req,)) + self.wfile.write('500 Unknown request\n') + +class ExceptionThrowingThreadedUnixStreamServer(SocketServer.ThreadingUnixStreamServer): + def handle_error(self, request, client_address): + self._logger.exception("Unhandled exception during request processing; shutting down") + die_event.set() + +class IncomingDir(threading.Thread): + def __init__(self, dir, archivemap, logger, trigger_reindex=1, poll_time=30, max_retry_time=172800, batch_mode=0, verify_sigs=0): + threading.Thread.__init__(self, name="incoming") + self._dir = dir + self._archivemap = archivemap + self._logger = logger + self._trigger_reindex = trigger_reindex + self._poll_time = poll_time + self._batch_mode = batch_mode + self._verify_sigs = verify_sigs + self._max_retry_time = max_retry_time + self._last_failed_targets = {} + self._eventqueue = Queue.Queue() + self._done_event = threading.Event() + # ensure we always have some reprocess queue + self._reprocess_queue = {} + + def run(self): + self._logger.info('Created new installer thread (%s)' % (self.getName(),)) + self._logger.info('Entering batch mode...') + initial_reprocess_queue = [] + initial_fucked_list = [] + try: + for (changefilename, changefile) in self._get_changefiles(): + if self._changefile_ready(changefilename, changefile): + try: + self._install_changefile(changefilename, changefile, 0) + except Exception: + logger.exception("Unable to install \"%s\"; adding to screwed list" % (changefilename,)) + initial_fucked_list.append(changefilename) + else: + self._logger.warn('Skipping "%s"; upload incomplete' % (changefilename,)) + initial_reprocess_queue.append(changefilename) + if not self._batch_mode: + self._daemonize(initial_reprocess_queue, initial_fucked_list) + self._done_event.set() + self._logger.info('All packages in incoming dir installed; exiting') + except Exception, e: + self._logger.exception("Unhandled exception; shutting down") + die_event.set() + self._done_event.set() + return 0 + + def _abspath(self, *args): + return os.path.abspath(apply(os.path.join, [self._dir] + list(args))) + + def _get_changefiles(self): + ret = [] + globpath = self._abspath("*.changes") + self._logger.debug("glob: " + globpath) + changefilenames = glob.glob(globpath) + for changefilename in changefilenames: + if not self._reprocess_queue.has_key(changefilename): + self._logger.info('Examining "%s"' % (changefilename,)) + changefile = ChangeFile() + try: + changefile.load_from_file(changefilename) + except ChangeFileException: + self._logger.debug("Unable to parse \"%s\", skipping" % (changefilename,)) + continue + ret.append((changefilename, changefile)) + else: + self._logger.debug('Skipping "%s" during new scan because it is in the reprocess queue.' % (changefilename,)) + return ret + + def _changefile_ready(self, changefilename, changefile): + try: + dist = changefile['distribution'] + except KeyError, e: + self._logger.warn("Unable to read distribution field for \"%s\"; data: %s" % (changefilename, changefile,)) + return 0 + try: + changefile.verify(self._abspath('')) + except ChangeFileException: + return 0 + return 1 + + def _install_changefile(self, changefilename, changefile, doing_reprocess): + dist = changefile['distribution'] + if not dist in self._archivemap.keys(): + raise DinstallException('Unknown distribution "%s" in \"%s\"' % (dist, changefilename,)) + logger.debug('Installing %s in archive %s' % (changefilename, self._archivemap[dist][1].getName())) + self._archivemap[dist][0].install(changefilename, changefile, self._verify_sigs) + if self._trigger_reindex: + if doing_reprocess: + logger.debug('Waiting on archive %s to reprocess' % (self._archivemap[dist][1].getName())) + self._archivemap[dist][1].wait_reprocess() + else: + logger.debug('Notifying archive %s of change' % (self._archivemap[dist][1].getName())) + self._archivemap[dist][1].notify() + logger.debug('Finished processing %s' % (changefilename)) + + def _reject_changefile(self, changefilename, changefile, e): + dist = changefile['distribution'] + if not dist in self._archivemap: + raise DinstallException('Unknown distribution "%s" in \"%s\"' % (dist, changefilename,)) + self._archivemap[dist][0].reject(changefilename, changefile, e) + + def _daemon_server_isready(self): + (inready, outready, exready) = select.select([self._server.fileno()], [], [], 0) + return len(inready) > 0 + + def _daemon_event_ispending(self): + return die_event.isSet() or reprocess_needed.isSet() or self._daemon_server_isready() or (not self._eventqueue.empty()) + + def _daemon_reprocess_pending(self): + curtime = time.time() + for changefilename in self._reprocess_queue.keys(): + (starttime, nexttime, delay) = self._reprocess_queue[changefilename] + if curtime >= nexttime: + return 1 + return 0 + + def _daemonize(self, init_reprocess_queue, init_fucked_list): + self._logger.info('Entering daemon mode...') + self._dnotify = DirectoryNotifierFactory().create([self._dir], use_dnotify=use_dnotify, poll_time=self._poll_time, cancel_event=die_event) + self._async_dnotify = DirectoryNotifierAsyncWrapper(self._dnotify, self._eventqueue, logger=self._logger, name="Incoming watcher") + self._async_dnotify.start() + try: + os.unlink(socket_name) + except OSError, e: + pass + self._server = ExceptionThrowingThreadedUnixStreamServer(socket_name, IncomingDirRequestHandler) + self._server.allow_reuse_address = 1 + retry_time = 30 + self._reprocess_queue = {} + fucked = init_fucked_list + doing_reprocess = 0 + # Initialize the reprocessing queue + for changefilename in init_reprocess_queue: + curtime = time.time() + self._reprocess_queue[changefilename] = [curtime, curtime, retry_time] + + # The main daemon loop + while 1: + # Wait until we have something to do + while not (self._daemon_event_ispending() or self._daemon_reprocess_pending()): + time.sleep(0.5) + + self._logger.debug('Checking for pending server requests') + if self._daemon_server_isready(): + self._logger.debug('Handling one request') + self._server.handle_request() + + self._logger.debug('Checking for DIE event') + if die_event.isSet(): + self._logger.debug('DIE event caught') + break + + self._logger.debug('Scanning for changes') + # do we have anything to reprocess? + for changefilename in self._reprocess_queue.keys(): + (starttime, nexttime, delay) = self._reprocess_queue[changefilename] + curtime = time.time() + try: + changefile = ChangeFile() + changefile.load_from_file(changefilename) + except (ChangeFileException,IOError), e: + if not os.path.exists(changefilename): + self._logger.info('Changefile "%s" got removed' % (changefilename,)) + else: + self._logger.exception("Unable to load change file \"%s\"" % (changefilename,)) + self._logger.warn("Marking \"%s\" as screwed" % (changefilename,)) + fucked.append(changefilename) + del self._reprocess_queue[changefilename] + continue + if (curtime - starttime) > self._max_retry_time: + # We've tried too many times; reject it. + self._reject_changefile(changefilename, changefile, DinstallException("Couldn't install \"%s\" in %d seconds" % (changefilename, self._max_retry_time))) + elif curtime >= nexttime: + if self._changefile_ready(changefilename, changefile): + # Let's do it! + self._logger.debug('Preparing to install "%s"' % (changefilename,)) + try: + self._install_changefile(changefilename, changefile, doing_reprocess) + self._logger.debug('Removing "%s" from incoming queue after successful install.' % (changefilename,)) + del self._reprocess_queue[changefilename] + except Exception, e: + logger.exception("Unable to install \"%s\"; adding to screwed list" % (changefilename,)) + fucked.append(changefilename) + else: + delay *= 2 + if delay > 60 * 60: + delay = 60 * 60 + self._logger.info('Upload "%s" isn\'t complete; marking for retry in %d seconds' % (changefilename, delay)) + self._reprocess_queue[changefilename][1:3] = [time.time() + delay, delay] + # done reprocessing; now scan for changed dirs. + relname = None + self._logger.debug('Checking dnotify event queue') + if not self._eventqueue.empty(): + relname = os.path.basename(os.path.abspath(self._eventqueue.get())) + self._logger.debug('Got %s from dnotify' % (relname,)) + if relname is None: + if (not doing_reprocess) and reprocess_needed.isSet(): + self._logger.info('Got reprocessing event') + reprocess_needed.clear() + doing_reprocess = 1 + if relname is None and (not doing_reprocess): + self._logger.debug('No events to process') + continue + + for (changefilename, changefile) in self._get_changefiles(): + if changefilename in fucked: + self._logger.warn("Skipping screwed changefile \"%s\"" % (changefilename,)) + continue + # Have we tried this changefile before? + if not self._reprocess_queue.has_key(changefilename): + self._logger.debug('New change file "%s"' % (changefilename,)) + if self._changefile_ready(changefilename, changefile): + try: + self._install_changefile(changefilename, changefile, doing_reprocess) + except Exception, e: + logger.exception("Unable to install \"%s\"; adding to screwed list" % (changefilename,)) + fucked.append(changefilename) + else: + curtime = time.time() + self._logger.info('Upload "%s" isn\'t complete; marking for retry in %d seconds' % (changefilename, retry_time)) + self._reprocess_queue[changefilename] = [curtime, curtime + retry_time, retry_time] + if doing_reprocess: + doing_reprocess = 0 + self._logger.info('Reprocessing complete') + reprocess_finished.set() + + def wait(self): + self._done_event.wait() + +def parse_versions(fullversion): + debianversion = re.sub('^[0-9]+:', '', fullversion) + upstreamver = re.sub('-[^-]*$', '', debianversion) + + return (upstreamver, debianversion) + +class ArchiveDir: + def __init__(self, dir, logger, configdict, batch_mode=0, keyrings=None, extra_keyrings=None): + self._dir = dir + self._name = os.path.basename(os.path.abspath(dir)) + self._logger = logger + for key in configdict.keys(): + self._logger.debug("Setting \"%s\" => \"%s\" in archive \"%s\"" % ('_'+key, configdict[key], self._name)) + self.__dict__['_' + key] = configdict[key] + do_mkdir(dir) + self._batch_mode = batch_mode + self._keyrings = keyrings + if not extra_keyrings is None : + self._extra_keyrings = extra_keyrings + else: + self._extra_keyrings = [] + if self._mail_on_success: + self._success_logger = logging.Logger("mini-dinstall." + self._name) + self._success_logger.setLevel(logging.DEBUG) + handler = SubjectSpecifyingLoggingSMTPHandler('mini-dinstall success notice', mail_server, 'Mini-Dinstall <%s@%s>' % (getpass.getuser(),socket.gethostname()), [mail_to]) + handler.setLevel(logging.DEBUG) + self._success_logger.addHandler(handler) + self._clean_targets = [] + +# self._filerefmap = {} +# self._changefiles = [] + + def _abspath(self, *args): + return os.path.abspath(apply(os.path.join, [self._dir] + list(args))) + + def _relpath(self, *args): + return apply(os.path.join, [self._name] + list(args)) + + def install(self, changefilename, changefile, verify_sigs): + retval = 0 + try: + retval = self._install_run_scripts(changefilename, changefile, verify_sigs) + except Exception: + self._logger.exception("Unhandled exception during installation") + if not retval: + self._logger.info('Failed to install "%s"' % (changefilename,)) + + def reject(self, changefilename, changefile, reason): + self._reject_changefile(changefilename, changefile, reason) + + def _install_run_scripts(self, changefilename, changefile, verify_sigs): + self._logger.info('Preparing to install \"%s\" in archive %s' % (changefilename, self._name,)) + sourcename = changefile['source'] + version = changefile['version'] + if verify_sigs: + self._logger.info('Verifying signature on "%s"' % (changefilename,)) + try: + if self._keyrings: + verifier = DebianSigVerifier(keyrings=map(os.path.expanduser, self._keyrings), extra_keyrings=self._extra_keyrings) + else: + verifier = DebianSigVerifier(extra_keyrings=self._extra_keyrings) + output = verifier.verify(changefilename) + logger.debug(output) + logger.info('Good signature on "%s"' % (changefilename,)) + except GPGSigVerificationFailure, e: + msg = "Failed to verify signature on \"%s\": %s\n" % (changefilename, e) + msg += string.join(e.getOutput(), '') + logger.error(msg) + self._reject_changefile(changefilename, changefile, e) + return 0 + else: + self._logger.debug('Skipping signature verification on "%s"' % (changefilename,)) + if self._pre_install_script: + try: + self._logger.debug("Running pre-installation script: " + self._pre_install_script) + if self._run_script(os.path.abspath(changefilename), self._pre_install_script): + return 0 + except: + self._logger.exception("failure while running pre-installation script") + return 0 + try: + self._install_changefile_internal(changefilename, changefile) + except Exception, e: + self._logger.exception('Failed to process "%s"' % (changefilename,)) + self._reject_changefile(changefilename, changefile, e) + return 0 + if self._chown_changes_files: + do_chmod(changefilename, 0600) + target = os.path.join(self._dir, os.path.basename(changefilename)) + # the final step + do_rename(changefilename, target) + self._logger.info('Successfully installed %s %s to %s' % (sourcename, version, self._name)) + if self._mail_on_success: + self._success_logger.info('Successfully installed %s %s to %s' % (sourcename, version, self._name)) + + if self._post_install_script: + try: + self._logger.debug("Running post-installation script: " + self._post_install_script) + self._run_script(target, self._post_install_script) + except: + self._logger.exception("failure while running post-installation script") + return 0 + return 1 + + def _install_changefile_internal(self, changefilename, changefile): + sourcename = changefile['source'] + version = changefile['version'] + incomingdir = os.path.dirname(changefilename) + newfiles = [] + is_native = not native_version_re.match(version) + if is_native: + (ignored, newdebianver) = parse_versions(version) + else: + (newupstreamver, newdebianver) = parse_versions(version) + is_sourceful = 0 + for file in map(lambda x: x[4], changefile.getFiles()): + match = debpackage_re.search(file) + if match: + arch = match.group(3) + if not arch in self._arches: + raise DinstallException("Unknown architecture: %s" % (arch)) + target = self._arch_target(arch, file) + newfiles.append((os.path.join(incomingdir, file), target, match.group(1), arch)) + continue + match = debsrc_diff_re.search(file) + if match: + is_sourceful = 1 + target = self._source_target(file) + newfiles.append((os.path.join(incomingdir, file), target, match.group(1), 'source')) + continue + match = debsrc_orig_re.search(file) + if match: + is_sourceful = 1 + target = self._source_target(file) + newfiles.append((os.path.join(incomingdir, file), target, match.group(1), 'source')) + continue + match = debsrc_native_re.search(file) + if match: + is_sourceful = 1 + target = self._source_target(file) + newfiles.append((os.path.join(incomingdir, file), target, match.group(1), 'source')) + continue + match = debsrc_dsc_re.search(file) or debsrc_orig_re.search(file) + if match: + is_sourceful = 1 + target = self._source_target(file) + newfiles.append((os.path.join(incomingdir, file), target, match.group(1), 'source')) + continue + + all_arches = {} + for arch in map(lambda x: x[3], newfiles): + all_arches[arch] = 1 + completed = [] + oldfiles = [] + if not self._keep_old: + found_old_bins = 0 + for (oldversion, oldarch) in map(lambda x: x[1:], self._get_package_versions()): + if not all_arches.has_key(oldarch) and apt_pkg.VersionCompare(oldversion, version) < 0: + found_old_bins = 1 + for (pkgname, arch) in map(lambda x: x[2:], newfiles): + if arch == 'source' and found_old_bins: + continue + self._logger.debug('Scanning for old files') + for file in self._read_arch_dir(arch): + match = debpackage_re.search(file) + if not match: + continue + oldpkgname = match.group(1) + oldarch = match.group(3) + file = self._arch_target(arch, file) + if not file in map(lambda x: x[0], oldfiles): + target = file + tmp_old_suffix + if oldpkgname == pkgname and oldarch == arch: + oldfiles.append((file, target)) + self._logger.debug('Scanning "%s" for old files' % (self._abspath('source'))) + for file in self._read_source_dir(): + file = self._source_target(file) + if not file in map(lambda x: x[0], oldfiles): + target = file + tmp_old_suffix + match = debchanges_re.search(file) + if not match and is_sourceful: + match = debsrc_dsc_re.search(file) or debsrc_diff_re.search(file) + if match and match.group(1) == sourcename: + oldfiles.append((file, target)) + continue + # We skip the rest of this if it wasn't a + # sourceful upload; really all we do if it isn't + # is clean out old .changes files. + if not is_sourceful: + continue + match = debsrc_orig_re.search(file) + if match and match.group(1) == sourcename: + if not is_native: + (oldupstreamver, olddebianver) = parse_versions(match.group(2)) + if apt_pkg.VersionCompare(oldupstreamver, newupstreamver) < 0: + self._logger.debug('old upstream tarball "%s" version %s < %s, tagging for deletion' % (file, oldupstreamver, newupstreamver)) + oldfiles.append((file, target)) + continue + else: + self._logger.debug('keeping upstream tarball "%s" version %s' % (file, oldupstreamver)) + continue + else: + self._logger.debug('old native tarball "%s", tagging for deletion' % (file,)) + oldfiles.append((file, target)) + continue + match = debsrc_native_re.search(file) + if match and match.group(1) in map(lambda x: x[2], newfiles): + oldfiles.append((file, target)) + continue + + self._clean_targets = map(lambda x: x[1], oldfiles) + allrenames = oldfiles + map(lambda x: x[:2], newfiles) + try: + while not allrenames == []: + (oldname, newname) = allrenames[0] + do_rename(oldname, newname) + completed.append(allrenames[0]) + allrenames = allrenames[1:] + except OSError, e: + logger.exception("Failed to do rename (%s); attempting rollback" % (e.strerror,)) + try: + self._logger.error(traceback.format_tb(sys.exc_traceback)) + except: + pass + # Unwind to previous state + for (newname, oldname) in completed: + do_rename(oldname, newname) + raise + self._clean_targets = [] + # remove old files + self.clean() + + def _run_script(self, changefilename, script): + if script: + script = os.path.expanduser(script) + cmd = '%s %s' % (script, changefilename) + self._logger.info('Running \"%s\"' % (cmd,)) + if not no_act: + if not os.access(script, os.X_OK): + self._logger.error("Can't execute script \"%s\"" % (script,)) + return 1 + pid = os.fork() + if pid == 0: + os.execlp(script, script, changefilename) + sys.exit(1) + (pid, status) = os.waitpid(pid, 0) + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + self._logger.error("script \"%s\" exited with error code %d" % (cmd, os.WEXITSTATUS(status))) + return 1 + return 0 + + def _reject_changefile(self, changefilename, changefile, exception): + sourcename = changefile['source'] + version = changefile['version'] + incomingdir = os.path.dirname(changefilename) + try: + f = open(os.path.join(rejectdir, "%s_%s.reason" % (sourcename, version)), 'w') + if type(exception) == type('string'): + f.write(exception) + else: + traceback.print_exception(Exception, exception, None, None, f) + f.close() + for file in map(lambda x: x[4], changefile.getFiles()): + if os.access(os.path.join(incomingdir, file), os.R_OK): + file = os.path.join(incomingdir, file) + else: + file = self._abspath(file) + target = os.path.join(rejectdir, os.path.basename(file)) + do_rename(file, target) + self._logger.info('Rejecting "%s": %s' % (changefilename, `exception`)) + except Exception: + self._logger.error("Unhandled exception while rejecting %s; archive may be in inconsistent state" % (changefilename,)) + raise + + def clean(self): + self._logger.debug('Removing old files') + for file in self._clean_targets: + self._logger.debug('Deleting "%s"' % (file,)) + if not no_act: + os.unlink(file) + +class SimpleSubdirArchiveDir(ArchiveDir): + def __init__(self, *args, **kwargs): + apply(ArchiveDir.__init__, [self] + list(args), kwargs) + for arch in list(self._arches) + ['source']: + target = os.path.join(self._dir, arch) + do_mkdir(target) + + def _read_source_dir(self): + return os.listdir(self._abspath('source')) + + def _read_arch_dir(self, arch): + return os.listdir(self._abspath(arch)) + + def _arch_target(self, arch, file): + return self._abspath(arch, file) + + def _source_target(self, file): + return self._arch_target('source', file) + + def _get_package_versions(self): + ret = [] + for arch in self._arches: + for file in self._read_arch_dir(arch): + match = debpackage_re.search(file) + if match: + ret.append((match.group(1), match.group(2), match.group(3))) + return ret + + +class FlatArchiveDir(ArchiveDir): + def _read_source_dir(self): + return os.listdir(self._dir) + + def _read_arch_dir(self, arch): + return os.listdir(self._dir) + + def _arch_target(self, arch, file): + return self._abspath(file) + + def _source_target(self, file): + return self._arch_target('source', file) + + def _get_package_versions(self): + ret = [] + for file in self._abspath(''): + match = debpackage_re.search(file) + if match: + ret.append((match.group(1), match.group(2), match.group(3))) + return ret + +class ArchiveDirIndexer(threading.Thread): + def __init__(self, dir, logger, configdict, use_dnotify=0, batch_mode=1): + self._dir = dir + self._name = os.path.basename(os.path.abspath(dir)) + threading.Thread.__init__(self, name=self._name) + self._logger = logger + self._eventqueue = Queue.Queue() + for key in configdict.keys(): + self._logger.debug("Setting \"%s\" => \"%s\" in archive \"%s\"" % ('_'+key, configdict[key], self._name)) + self.__dict__['_' + key] = configdict[key] + do_mkdir(dir) + self._use_dnotify = use_dnotify + self._batch_mode = batch_mode + self._done_event = threading.Event() + + def _abspath(self, *args): + return os.path.abspath(apply(os.path.join, [self._dir] + list(args))) + + def _relpath(self, *args): + return apply(os.path.join, [self._name] + list(args)) + + def _make_indexfile(self, dir, type, name): + cmdline = ['apt-ftparchive', type, dir] + self._logger.debug("Running: " + string.join(cmdline, ' ')) + if no_act: + return + (infd, outfd) = os.pipe() + pid = os.fork() + if pid == 0: + os.chdir(self._dir) + os.chdir('..') + os.close(infd) + misc.dup2(outfd, 1) + os.execvp('apt-ftparchive', cmdline) + os.exit(1) + os.close(outfd) + stdout = os.fdopen(infd) + packagesfile = open(os.path.join(dir, name), 'w') + zpackagesfile = gzip.GzipFile(os.path.join(dir, name + '.gz'), 'w') + buf = stdout.read(8192) + while buf != '': + packagesfile.write(buf) + zpackagesfile.write(buf) + buf = stdout.read(8192) + stdout.close() + (pid, status) = os.waitpid(pid, 0) + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + raise DinstallException("apt-ftparchive exited with status code %d" % (status,)) + packagesfile.close() + zpackagesfile.close() + + def _make_packagesfile(self, dir): + self._make_indexfile(dir, 'packages', 'Packages') + + def _make_sourcesfile(self, dir): + self._make_indexfile(dir, 'sources', 'Sources') + + def _make_releasefile(self): + targetname = self._abspath('Release') + if not self._generate_release: + if os.access(targetname, os.R_OK): + self._logger.info("Release generation disabled, removing existing Release file") + try: + os.unlink(targetname) + except OSError, e: + pass + return + tmpname = targetname + tmp_new_suffix + release_needed = 0 + uncompr_indexfiles = self._get_all_indexfiles() + indexfiles = [] + comprexts = ['.gz'] + for index in uncompr_indexfiles: + indexfiles = indexfiles + [index] + for ext in comprexts: + indexfiles = indexfiles + [index + ext] + if os.access(targetname, os.R_OK): + release_mtime = os.stat(targetname)[stat.ST_MTIME] + for file in indexfiles: + if release_needed: + break + if os.stat(self._abspath(file))[stat.ST_MTIME] > release_mtime: + release_needed = 1 + else: + release_needed = 1 + + if not release_needed: + self._logger.info("Skipping Release generation") + return + self._logger.info("Generating Release...") + if no_act: + self._logger.info("Release generation complete") + return + f = open(tmpname, 'w') + f.write('Origin: ' + self._release_origin + '\n') + f.write('Label: ' + self._release_label + '\n') + suite = self._release_suite + if not suite: + suite = self._name + f.write('Suite: ' + suite + '\n') + codename = self._release_codename + if not codename: + codename = suite + f.write('Codename: ' + codename + '\n') + f.write('Date: ' + time.strftime("%a, %d %b %Y %H:%M:%S UTC", time.gmtime()) + '\n') + f.write('Architectures: ' + string.join(self._arches, ' ') + '\n') + if self._release_description: + f.write('Description: ' + self._release_description + '\n') + f.write('MD5Sum:\n') + for file in indexfiles: + absfile = self._abspath(file) + md5sum = self._get_file_sum('md5', absfile) + size = os.stat(absfile)[stat.ST_SIZE] + f.write(' %s% 16d %s\n' % (md5sum, size, file)) + f.write('SHA1:\n') + for file in indexfiles: + absfile = self._abspath(file) + shasum = self._get_file_sum('sha1', absfile) + size = os.stat(absfile)[stat.ST_SIZE] + f.write(' %s% 16d %s\n' % (shasum, size, file)) + f.close() + if self._sign_releasefile(tmpname): + os.rename(tmpname, targetname) + self._logger.info("Release generation complete") + + def _sign_releasefile(self, name): + if self._release_signscript: + try: + self._logger.debug("Running Release signing script: " + self._release_signscript) + if self._run_script(name, self._release_signscript, dir=self._abspath()): + return None + except: + self._logger.exception("failure while running Release signature script") + return None + return 1 + + # Copied from ArchiveDir + def _run_script(self, changefilename, script, dir=None): + if script: + script = os.path.expanduser(script) + cmd = '%s %s' % (script, changefilename) + self._logger.info('Running \"%s\"' % (cmd,)) + if not no_act: + if not os.access(script, os.X_OK): + self._logger.error("Can't execute script \"%s\"" % (script,)) + return 1 + pid = os.fork() + if pid == 0: + if dir: + os.chdir(dir) + os.execlp(script, script, changefilename) + sys.exit(1) + (pid, status) = os.waitpid(pid, 0) + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + self._logger.error("script \"%s\" exited with error code %d" % (cmd, os.WEXITSTATUS(status))) + return 1 + return 0 + + # Hacked up from ChangeFile.py; FIXME: merge the two + def _get_file_sum(self, type, filename): + if os.access('/usr/bin/%ssum' % (type,), os.X_OK): + cmd = '/usr/bin/%ssum %s' % (type, filename,) + self._logger.debug("Running: %s" % (cmd,)) + child = popen2.Popen3(cmd, 1) + child.tochild.close() + erroutput = child.childerr.read() + child.childerr.close() + if erroutput != '': + child.fromchild.close() + raise DinstallException("%ssum returned error output \"%s\"" % (type, erroutput,)) + (sum, filename) = string.split(child.fromchild.read(), None, 1) + child.fromchild.close() + status = child.wait() + if not (status is None or (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)): + if os.WIFEXITED(status): + msg = "%ssum exited with error code %d" % (type, os.WEXITSTATUS(status),) + elif os.WIFSTOPPED(status): + msg = "%ssum stopped unexpectedly with signal %d" % (type, os.WSTOPSIG(status),) + elif os.WIFSIGNALED(status): + msg = "%ssum died with signal %d" % (type, os.WTERMSIG(status),) + raise DinstallException(msg) + return sum.strip() + if type == 'md5': + import md5 + f = open(filename) + md5sum = md5.new() + buf = f.read(8192) + while buf != '': + md5sum.update(buf) + buf = f.read(8192) + return md5sum.hexdigest() + elif type == 'sha1': + import sha + f = open(filename) + shasum = sha.new() + buf = f.read(8192) + while buf != '': + shasum.update(buf) + buf = f.read(8192) + return shasum.hexdigest() + else: + raise DinstallException('cannot compute hash of type %s; no builtin method or /usr/bin/%ssum', type, type) + + def _index_all(self, force=None): + self._index(self._arches + ['source'], force) + + def run(self): + self._logger.info('Created new thread (%s) for archive indexer %s' % (self.getName(), self._name,)) + self._logger.info('Entering batch mode...') + try: + self._index_all(1) + self._make_releasefile() + if not self._batch_mode: + # never returns + self._daemonize() + self._done_event.set() + except Exception, e: + self._logger.exception("Unhandled exception; shutting down") + die_event.set() + self._done_event.set() + self._logger.info('Thread \"%s\" exiting' % (self.getName(),)) + + def _daemon_event_ispending(self): + return die_event.isSet() or (not self._eventqueue.empty()) + def _daemonize(self): + self._logger.info('Entering daemon mode...') + if self._dynamic_reindex: + self._dnotify = DirectoryNotifierFactory().create(self._get_dnotify_dirs(), use_dnotify=self._use_dnotify, poll_time=self._poll_time, cancel_event=die_event) + + self._async_dnotify = DirectoryNotifierAsyncWrapper(self._dnotify, self._eventqueue, logger=self._logger, name=self._name + " Indexer") + self._async_dnotify.start() + + # The main daemon loop + while 1: + + # Wait until we have a pending event + while not self._daemon_event_ispending(): + time.sleep(1) + + if die_event.isSet(): + break + + self._logger.debug('Reading from event queue') + setevent = None + dir = None + obj = self._eventqueue.get() + if type(obj) == type(''): + self._logger.debug('got dir change') + dir = obj + elif type(obj) == type(None): + self._logger.debug('got general event') + setevent = None + elif obj.__class__ == threading.Event().__class__: + self._logger.debug('got wait_reprocess event') + setevent = obj + else: + self._logger.error("unknown object %s in event queue" % (obj,)) + assert None + + # This is to protect against both lots of activity, and to + # prevent race conditions, so we can rely on timestamps. + time.sleep(1) + if not self._reindex_needed(): + if setevent: + self._logger.debug('setting wait_reprocess event') + setevent.set() + continue + if dir is None: + self._logger.debug('Got general change') + self._index_all(1) + else: + self._logger.debug('Got change in %s' % (dir,)) + self._index([os.path.basename(os.path.abspath(dir))]) + self._make_releasefile() + if setevent: + self._logger.debug('setting wait_reprocess event') + setevent.set() + + def _reindex_needed(self): + reindex_needed = 0 + if os.access(self._abspath('Release.gpg'), os.R_OK): + gpg_mtime = os.stat(self._abspath('Release.gpg'))[stat.ST_MTIME] + for dir in self._get_dnotify_dirs(): + dir_mtime = os.stat(self._abspath(dir))[stat.ST_MTIME] + if dir_mtime > gpg_mtime: + reindex_needed = 1 + else: + reindex_needed = 1 + return reindex_needed + + def _index(self, arches, force=None): + self._index_impl(arches, force=force) + + def wait_reprocess(self): + e = threading.Event() + self._eventqueue.put(e) + self._logger.debug('waiting on reprocess') + while not (e.isSet() or die_event.isSet()): + time.sleep(0.5) + self._logger.debug('done waiting on reprocess') + + def wait(self): + self._done_event.wait() + + def notify(self): + self._eventqueue.put(None) + +class SimpleSubdirArchiveDirIndexer(ArchiveDirIndexer): + def __init__(self, *args, **kwargs): + apply(ArchiveDirIndexer.__init__, [self] + list(args), kwargs) + for arch in list(self._arches) + ['source']: + target = os.path.join(self._dir, arch) + do_mkdir(target) + + def _index_impl(self, arches, force=None): + for arch in arches: + dirmtime = os.stat(self._relpath(arch))[stat.ST_MTIME] + if arch != 'source': + pkgsfile = self._relpath(arch, 'Packages') + if force or (not os.access(pkgsfile, os.R_OK)) or dirmtime > os.stat(pkgsfile)[stat.ST_MTIME]: + self._logger.info('Generating Packages file for %s...' % (arch,)) + self._make_packagesfile(self._relpath(arch)) + self._logger.info('Packages generation complete') + else: + self._logger.info('Skipping generation of Packages file for %s' % (arch,)) + + else: + pkgsfile = self._relpath(arch, 'Sources') + if force or (not os.access(pkgsfile, os.R_OK)) or dirmtime > os.stat(pkgsfile)[stat.ST_MTIME]: + self._logger.info('Generating Sources file for %s...' % (arch,)) + self._make_sourcesfile(self._relpath('source')) + self._logger.info('Sources generation complete') + else: + self._logger.info('Skipping generation of Sources file for %s' % (arch,)) + + def _in_archdir(self, *args): + return apply(lambda x,self=self: self._abspath(x), args) + + def _get_dnotify_dirs(self): + return map(lambda x, self=self: self._abspath(x), self._arches + ['source']) + + def _get_all_indexfiles(self): + return map(lambda arch: os.path.join(arch, 'Packages'), self._arches) + ['source/Sources'] + +class FlatArchiveDirIndexer(ArchiveDirIndexer): + def __init__(self, *args, **kwargs): + apply(ArchiveDirIndexer.__init__, [self] + list(args), kwargs) + + def _index_impl(self, arches, force=None): + pkgsfile = self._abspath('Packages') + dirmtime = os.stat(self._relpath())[stat.ST_MTIME] + if force or (not os.access(pkgsfile, os.R_OK)) or dirmtime > os.stat(pkgsfile)[stat.ST_MTIME]: + self._logger.info('Generating Packages file...') + self._make_packagesfile(self._relpath()) + self._logger.info('Packages generation complete') + else: + self._logger.info('Skipping generation of Packages file') + pkgsfile = self._abspath('Sources') + if force or (not os.access(pkgsfile, os.R_OK)) or dirmtime > os.stat(pkgsfile)[stat.ST_MTIME]: + self._logger.info('Generating Sources file...') + self._make_sourcesfile(self._relpath()) + self._logger.info('Sources generation complete') + else: + self._logger.info('Skipping generation of Sources file') + + def _in_archdir(self, *args): + return apply(lambda x,self=self: self._abspath(x), args[1:]) + + def _get_dnotify_dirs(self): + return [self._dir] + + def _get_all_indexfiles(self): + return ['Packages', 'Sources'] + +if os.access(lockfilename, os.R_OK): + logger.critical("lockfile \"%s\" exists (pid %s): is another mini-dinstall running?" % (lockfilename, open(lockfilename).read(10))) + logging.shutdown() + sys.exit(1) +logger.debug('Creating lock file: ' + lockfilename) +if not no_act: + lockfile = open(lockfilename, 'w') + lockfile.close() + +if not batch_mode: + # daemonize + logger.debug("Daemonizing...") + if os.fork() == 0: + os.setsid() + if os.fork() != 0: + sys.exit(0) + else: + sys.exit(0) + sys.stdin.close() + sys.stdout.close() + sys.stderr.close() + os.close(0) + os.close(1) + os.close(2) + # unix file descriptor allocation ensures that the followin are fd 0,1,2 + sys.stdin = open("/dev/null") + sys.stdout = open("/dev/null") + sys.stderr = open("/dev/null") + logger.debug("Finished daemonizing (pid %s)" % (os.getpid(),)) + +lockfile = open(lockfilename, 'w') +lockfile.write("%s" % (os.getpid(),)) +lockfile.close() + +if not (debug_mode or batch_mode): + # Don't log to stderr past this point + logger.removeHandler(stderr_handler) + +archivemap = {} +# Instantiaate archive classes for installing files +for dist in distributions.keys(): + if distributions[dist]['archive_style'] == 'simple-subdir': + newclass = SimpleSubdirArchiveDir + else: + newclass = FlatArchiveDir + archivemap[dist] = [newclass(dist, logger, distributions[dist], batch_mode=batch_mode, keyrings=default_keyrings, extra_keyrings=default_extra_keyrings), None] + +# Create archive indexing threads, but don't start them yet +for dist in distributions.keys(): + targetdir = os.path.join(toplevel_directory, dist) + logger.info('Initializing archive indexer %s' % (dist,)) + if distributions[dist]['archive_style'] == 'simple-subdir': + newclass = SimpleSubdirArchiveDirIndexer + else: + newclass = FlatArchiveDirIndexer + archive = newclass(targetdir, logger, distributions[dist], use_dnotify=use_dnotify, batch_mode=batch_mode) + archivemap[dist][1] = archive + +# Now: kick off the incoming processor +logger.info('Initializing incoming processor') +incoming = IncomingDir(incoming_subdir, archivemap, logger, trigger_reindex=trigger_reindex, poll_time=default_poll_time, max_retry_time=default_max_retry_time, batch_mode=batch_mode, verify_sigs=default_verify_sigs) +logger.debug('Starting incoming processor') +incoming.start() +if batch_mode: + logger.debug('Waiting for incoming processor to finish') + incoming.wait() + +# Once we've installed everything, start the indexing threads +for dist in distributions.keys(): + archive = archivemap[dist][1] + logger.debug('Starting archive %s' % (archive.getName(),)) + archive.start() + +# Wait for all the indexing threads to finish; none of these ever +# return if we're in daemon mode +if batch_mode: + for dist in distributions.keys(): + archive = archivemap[dist][1] + logger.debug('Waiting for archive %s to finish' % (archive.getName(),)) + archive.wait() +else: + logger.debug("Waiting for die event") + die_event.wait() + logger.info('Die event caught; waiting for incoming processor to finish') + incoming.wait() + for dist in distributions.keys(): + archive = archivemap[dist][1] + logger.info('Die event caught; waiting for archive %s to finish' % (archive.getName(),)) + archive.wait() + +#logging.shutdown() +logger.debug('Removing lock file: ' + lockfilename) +os.unlink(lockfilename) +logger.info("main thread exiting...") +sys.exit(0) + +# vim:ts=4:sw=4:et: |