Merge lp:~paul-mcspadden/computer-janitor/bug-726616 into lp:computer-janitor

Proposed by Paul McSpadden
Status: Rejected
Rejected by: Barry Warsaw
Proposed branch: lp:~paul-mcspadden/computer-janitor/bug-726616
Merge into: lp:computer-janitor
Diff against target: 10244 lines (+9772/-0) (has conflicts)
83 files modified
COPYING (+676/-0)
CoverageTestRunner.py (+203/-0)
Makefile (+51/-0)
NEWS (+237/-0)
README (+31/-0)
TODO (+30/-0)
computer-janitor (+41/-0)
computer-janitor-gtk (+67/-0)
computer-janitor-gtk.8 (+36/-0)
computer-janitor.8 (+222/-0)
computerjanitorapp/__init__.py (+44/-0)
computerjanitorapp/cli/main.py (+312/-0)
computerjanitorapp/gtk/dialogs.py (+113/-0)
computerjanitorapp/gtk/main.py (+32/-0)
computerjanitorapp/gtk/store.py (+161/-0)
computerjanitorapp/gtk/ui.py (+537/-0)
computerjanitorapp/terminalsize.py (+64/-0)
computerjanitorapp/tests/test_all.py (+29/-0)
computerjanitorapp/tests/test_terminalsize.py (+54/-0)
computerjanitorapp/tests/test_utilities.py (+71/-0)
computerjanitorapp/utilities.py (+51/-0)
computerjanitord/application.py (+92/-0)
computerjanitord/authenticator.py (+85/-0)
computerjanitord/collector.py (+167/-0)
computerjanitord/data/com.ubuntu.ComputerJanitor.conf (+15/-0)
computerjanitord/data/com.ubuntu.ComputerJanitor.service (+4/-0)
computerjanitord/data/com.ubuntu.computerjanitor.policy (+19/-0)
computerjanitord/errors.py (+95/-0)
computerjanitord/main.py (+118/-0)
computerjanitord/service.py (+289/-0)
computerjanitord/state.py (+92/-0)
computerjanitord/tests/data/etc/apt/sources.list (+1/-0)
computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages (+54/-0)
computerjanitord/tests/data/var/lib/dpkg/status (+28/-0)
computerjanitord/tests/test_all.py (+41/-0)
computerjanitord/tests/test_application.py (+106/-0)
computerjanitord/tests/test_authenticator.py (+97/-0)
computerjanitord/tests/test_collector.py (+225/-0)
computerjanitord/tests/test_state.py (+141/-0)
computerjanitord/tests/test_whitelist.py (+119/-0)
computerjanitord/whitelist.py (+86/-0)
data/ComputerJanitor.ui (+406/-0)
data/computer-janitor-gtk.desktop.in (+11/-0)
debian/changelog (+488/-0)
debian/compat (+1/-0)
debian/computer-janitor-gtk.install (+6/-0)
debian/computer-janitor-gtk.lintian-overrides (+2/-0)
debian/computer-janitor.docs (+1/-0)
debian/computer-janitor.install (+8/-0)
debian/computer-janitor.postinst (+28/-0)
debian/computer-janitor.postrm (+18/-0)
debian/control (+50/-0)
debian/copyright (+35/-0)
debian/default.whitelist (+4/-0)
debian/pycompat (+1/-0)
debian/rules (+35/-0)
debian/watch (+3/-0)
janitord (+27/-0)
license-check (+72/-0)
license-exceptions (+24/-0)
plugins/add_nfs_common_plugin.py (+60/-0)
plugins/autoremoval_plugin.py (+41/-0)
plugins/autoremoval_plugin_tests.py (+53/-0)
plugins/check_admin_group_plugin.py (+83/-0)
plugins/dpkg_dotfile_plugin.py (+86/-0)
plugins/dpkg_dotfile_plugin_tests.py (+106/-0)
plugins/fstab_plugin.py (+123/-0)
plugins/fstab_plugin_tests.py (+123/-0)
plugins/landscape_stub_plugin.py (+57/-0)
plugins/landscape_stub_plugin_tests.py (+81/-0)
plugins/unsupported_plugin.py (+131/-0)
plugins/unsupported_plugin_tests.py (+107/-0)
po/POTFILES.in (+29/-0)
po/computerjanitor.pot (+289/-0)
po/es.po (+427/-0)
po/fi.po (+427/-0)
po/fr.po (+426/-0)
po/ja.po (+422/-0)
po/pl.po (+401/-0)
run_from_checkout.sh (+11/-0)
setup.cfg (+5/-0)
setup.py (+97/-0)
test-env (+31/-0)
Conflict adding file COPYING.  Moved existing file to COPYING.moved.
Conflict adding file CoverageTestRunner.py.  Moved existing file to CoverageTestRunner.py.moved.
Conflict adding file Makefile.  Moved existing file to Makefile.moved.
Conflict adding file NEWS.  Moved existing file to NEWS.moved.
Conflict adding file README.  Moved existing file to README.moved.
Conflict adding file TODO.  Moved existing file to TODO.moved.
Conflict adding file computer-janitor-gtk.8.  Moved existing file to computer-janitor-gtk.8.moved.
Conflict adding file computer-janitor-gtk.  Moved existing file to computer-janitor-gtk.moved.
Conflict adding file computer-janitor.8.  Moved existing file to computer-janitor.8.moved.
Conflict adding file computer-janitor.  Moved existing file to computer-janitor.moved.
Conflict adding file computerjanitorapp.  Moved existing file to computerjanitorapp.moved.
Conflict adding file computerjanitord.  Moved existing file to computerjanitord.moved.
Conflict adding file data.  Moved existing file to data.moved.
Conflict adding file debian.  Moved existing file to debian.moved.
Conflict adding file janitord.  Moved existing file to janitord.moved.
Conflict adding file license-check.  Moved existing file to license-check.moved.
Conflict adding file license-exceptions.  Moved existing file to license-exceptions.moved.
Conflict adding file plugins.  Moved existing file to plugins.moved.
Conflict adding file po.  Moved existing file to po.moved.
Conflict adding file run_from_checkout.sh.  Moved existing file to run_from_checkout.sh.moved.
Conflict adding file setup.cfg.  Moved existing file to setup.cfg.moved.
Conflict adding file setup.py.  Moved existing file to setup.py.moved.
Conflict adding file test-env.  Moved existing file to test-env.moved.
To merge this branch: bzr merge lp:~paul-mcspadden/computer-janitor/bug-726616
Reviewer Review Type Date Requested Status
Barry Warsaw Disapprove
Review via email: mp+51600@code.launchpad.net

Description of the change

NEWS: fixed a typo. (LP: #726616)

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

Thanks for following along in the UDD class on irc. It was my fault but your original branch should have been against ubuntu:computer-janitor instead of lp:computer-janitor.

review: Disapprove

Unmerged revisions

21. By Paul McSpadden

NEWS: fixed a typo. (LP: #726616)

20. By Colin Watson

merge lp:~barry/ubuntu/maverick/computer-janitor/bug-665740

19. By Barry Warsaw

Fix icon; given by Pavol Klačanský. (LP: #434431)

18. By Barry Warsaw

Remove dependency on python-fstab since the fstab_plugin is disabled
now and no package provides python-fstab any more.

17. By Barry Warsaw

* Fixes for translation support.
  - Turn {foo} string substitutions back into %(foo)s strings since
    gettext does not yet support validation of {foo} or $foo
    substitutions. (LP: #621723)
  - Ensure that POTFILES.in has the proper type information for the
    ComputerJanitor.ui file. (LP: #612493)

16. By Anders Kaseorg

Drop unused dependency on python-fstab, which was removed from squeeze
and maverick. (The fstab plugin that uses it is no longer installed.)
(LP: #610619)

15. By Michael Vogt

no change rebuild to generate proper orig.tar.gz

14. By Barry Warsaw

Bumping for Ubuntu.

13. By Michael Vogt

[ David Planella ]
* Removed obsolete template
* marked .ui file for string extraction, Thanks to Gabor Kelemen

[ Barry Warsaw ]
* remove recommended column (LP: #536908)

12. By Michael Vogt

* computerjanitorapp/ui_gtk.py:
  - fix find_and_bind_widgets (LP: #503727)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'COPYING'
2--- COPYING 1970-01-01 00:00:00 +0000
3+++ COPYING 2011-02-28 18:58:01 +0000
4@@ -0,0 +1,676 @@
5+
6+ GNU GENERAL PUBLIC LICENSE
7+ Version 3, 29 June 2007
8+
9+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
10+ Everyone is permitted to copy and distribute verbatim copies
11+ of this license document, but changing it is not allowed.
12+
13+ Preamble
14+
15+ The GNU General Public License is a free, copyleft license for
16+software and other kinds of works.
17+
18+ The licenses for most software and other practical works are designed
19+to take away your freedom to share and change the works. By contrast,
20+the GNU General Public License is intended to guarantee your freedom to
21+share and change all versions of a program--to make sure it remains free
22+software for all its users. We, the Free Software Foundation, use the
23+GNU General Public License for most of our software; it applies also to
24+any other work released this way by its authors. You can apply it to
25+your programs, too.
26+
27+ When we speak of free software, we are referring to freedom, not
28+price. Our General Public Licenses are designed to make sure that you
29+have the freedom to distribute copies of free software (and charge for
30+them if you wish), that you receive source code or can get it if you
31+want it, that you can change the software or use pieces of it in new
32+free programs, and that you know you can do these things.
33+
34+ To protect your rights, we need to prevent others from denying you
35+these rights or asking you to surrender the rights. Therefore, you have
36+certain responsibilities if you distribute copies of the software, or if
37+you modify it: responsibilities to respect the freedom of others.
38+
39+ For example, if you distribute copies of such a program, whether
40+gratis or for a fee, you must pass on to the recipients the same
41+freedoms that you received. You must make sure that they, too, receive
42+or can get the source code. And you must show them these terms so they
43+know their rights.
44+
45+ Developers that use the GNU GPL protect your rights with two steps:
46+(1) assert copyright on the software, and (2) offer you this License
47+giving you legal permission to copy, distribute and/or modify it.
48+
49+ For the developers' and authors' protection, the GPL clearly explains
50+that there is no warranty for this free software. For both users' and
51+authors' sake, the GPL requires that modified versions be marked as
52+changed, so that their problems will not be attributed erroneously to
53+authors of previous versions.
54+
55+ Some devices are designed to deny users access to install or run
56+modified versions of the software inside them, although the manufacturer
57+can do so. This is fundamentally incompatible with the aim of
58+protecting users' freedom to change the software. The systematic
59+pattern of such abuse occurs in the area of products for individuals to
60+use, which is precisely where it is most unacceptable. Therefore, we
61+have designed this version of the GPL to prohibit the practice for those
62+products. If such problems arise substantially in other domains, we
63+stand ready to extend this provision to those domains in future versions
64+of the GPL, as needed to protect the freedom of users.
65+
66+ Finally, every program is threatened constantly by software patents.
67+States should not allow patents to restrict development and use of
68+software on general-purpose computers, but in those that do, we wish to
69+avoid the special danger that patents applied to a free program could
70+make it effectively proprietary. To prevent this, the GPL assures that
71+patents cannot be used to render the program non-free.
72+
73+ The precise terms and conditions for copying, distribution and
74+modification follow.
75+
76+ TERMS AND CONDITIONS
77+
78+ 0. Definitions.
79+
80+ "This License" refers to version 3 of the GNU General Public License.
81+
82+ "Copyright" also means copyright-like laws that apply to other kinds of
83+works, such as semiconductor masks.
84+
85+ "The Program" refers to any copyrightable work licensed under this
86+License. Each licensee is addressed as "you". "Licensees" and
87+"recipients" may be individuals or organizations.
88+
89+ To "modify" a work means to copy from or adapt all or part of the work
90+in a fashion requiring copyright permission, other than the making of an
91+exact copy. The resulting work is called a "modified version" of the
92+earlier work or a work "based on" the earlier work.
93+
94+ A "covered work" means either the unmodified Program or a work based
95+on the Program.
96+
97+ To "propagate" a work means to do anything with it that, without
98+permission, would make you directly or secondarily liable for
99+infringement under applicable copyright law, except executing it on a
100+computer or modifying a private copy. Propagation includes copying,
101+distribution (with or without modification), making available to the
102+public, and in some countries other activities as well.
103+
104+ To "convey" a work means any kind of propagation that enables other
105+parties to make or receive copies. Mere interaction with a user through
106+a computer network, with no transfer of a copy, is not conveying.
107+
108+ An interactive user interface displays "Appropriate Legal Notices"
109+to the extent that it includes a convenient and prominently visible
110+feature that (1) displays an appropriate copyright notice, and (2)
111+tells the user that there is no warranty for the work (except to the
112+extent that warranties are provided), that licensees may convey the
113+work under this License, and how to view a copy of this License. If
114+the interface presents a list of user commands or options, such as a
115+menu, a prominent item in the list meets this criterion.
116+
117+ 1. Source Code.
118+
119+ The "source code" for a work means the preferred form of the work
120+for making modifications to it. "Object code" means any non-source
121+form of a work.
122+
123+ A "Standard Interface" means an interface that either is an official
124+standard defined by a recognized standards body, or, in the case of
125+interfaces specified for a particular programming language, one that
126+is widely used among developers working in that language.
127+
128+ The "System Libraries" of an executable work include anything, other
129+than the work as a whole, that (a) is included in the normal form of
130+packaging a Major Component, but which is not part of that Major
131+Component, and (b) serves only to enable use of the work with that
132+Major Component, or to implement a Standard Interface for which an
133+implementation is available to the public in source code form. A
134+"Major Component", in this context, means a major essential component
135+(kernel, window system, and so on) of the specific operating system
136+(if any) on which the executable work runs, or a compiler used to
137+produce the work, or an object code interpreter used to run it.
138+
139+ The "Corresponding Source" for a work in object code form means all
140+the source code needed to generate, install, and (for an executable
141+work) run the object code and to modify the work, including scripts to
142+control those activities. However, it does not include the work's
143+System Libraries, or general-purpose tools or generally available free
144+programs which are used unmodified in performing those activities but
145+which are not part of the work. For example, Corresponding Source
146+includes interface definition files associated with source files for
147+the work, and the source code for shared libraries and dynamically
148+linked subprograms that the work is specifically designed to require,
149+such as by intimate data communication or control flow between those
150+subprograms and other parts of the work.
151+
152+ The Corresponding Source need not include anything that users
153+can regenerate automatically from other parts of the Corresponding
154+Source.
155+
156+ The Corresponding Source for a work in source code form is that
157+same work.
158+
159+ 2. Basic Permissions.
160+
161+ All rights granted under this License are granted for the term of
162+copyright on the Program, and are irrevocable provided the stated
163+conditions are met. This License explicitly affirms your unlimited
164+permission to run the unmodified Program. The output from running a
165+covered work is covered by this License only if the output, given its
166+content, constitutes a covered work. This License acknowledges your
167+rights of fair use or other equivalent, as provided by copyright law.
168+
169+ You may make, run and propagate covered works that you do not
170+convey, without conditions so long as your license otherwise remains
171+in force. You may convey covered works to others for the sole purpose
172+of having them make modifications exclusively for you, or provide you
173+with facilities for running those works, provided that you comply with
174+the terms of this License in conveying all material for which you do
175+not control copyright. Those thus making or running the covered works
176+for you must do so exclusively on your behalf, under your direction
177+and control, on terms that prohibit them from making any copies of
178+your copyrighted material outside their relationship with you.
179+
180+ Conveying under any other circumstances is permitted solely under
181+the conditions stated below. Sublicensing is not allowed; section 10
182+makes it unnecessary.
183+
184+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
185+
186+ No covered work shall be deemed part of an effective technological
187+measure under any applicable law fulfilling obligations under article
188+11 of the WIPO copyright treaty adopted on 20 December 1996, or
189+similar laws prohibiting or restricting circumvention of such
190+measures.
191+
192+ When you convey a covered work, you waive any legal power to forbid
193+circumvention of technological measures to the extent such circumvention
194+is effected by exercising rights under this License with respect to
195+the covered work, and you disclaim any intention to limit operation or
196+modification of the work as a means of enforcing, against the work's
197+users, your or third parties' legal rights to forbid circumvention of
198+technological measures.
199+
200+ 4. Conveying Verbatim Copies.
201+
202+ You may convey verbatim copies of the Program's source code as you
203+receive it, in any medium, provided that you conspicuously and
204+appropriately publish on each copy an appropriate copyright notice;
205+keep intact all notices stating that this License and any
206+non-permissive terms added in accord with section 7 apply to the code;
207+keep intact all notices of the absence of any warranty; and give all
208+recipients a copy of this License along with the Program.
209+
210+ You may charge any price or no price for each copy that you convey,
211+and you may offer support or warranty protection for a fee.
212+
213+ 5. Conveying Modified Source Versions.
214+
215+ You may convey a work based on the Program, or the modifications to
216+produce it from the Program, in the form of source code under the
217+terms of section 4, provided that you also meet all of these conditions:
218+
219+ a) The work must carry prominent notices stating that you modified
220+ it, and giving a relevant date.
221+
222+ b) The work must carry prominent notices stating that it is
223+ released under this License and any conditions added under section
224+ 7. This requirement modifies the requirement in section 4 to
225+ "keep intact all notices".
226+
227+ c) You must license the entire work, as a whole, under this
228+ License to anyone who comes into possession of a copy. This
229+ License will therefore apply, along with any applicable section 7
230+ additional terms, to the whole of the work, and all its parts,
231+ regardless of how they are packaged. This License gives no
232+ permission to license the work in any other way, but it does not
233+ invalidate such permission if you have separately received it.
234+
235+ d) If the work has interactive user interfaces, each must display
236+ Appropriate Legal Notices; however, if the Program has interactive
237+ interfaces that do not display Appropriate Legal Notices, your
238+ work need not make them do so.
239+
240+ A compilation of a covered work with other separate and independent
241+works, which are not by their nature extensions of the covered work,
242+and which are not combined with it such as to form a larger program,
243+in or on a volume of a storage or distribution medium, is called an
244+"aggregate" if the compilation and its resulting copyright are not
245+used to limit the access or legal rights of the compilation's users
246+beyond what the individual works permit. Inclusion of a covered work
247+in an aggregate does not cause this License to apply to the other
248+parts of the aggregate.
249+
250+ 6. Conveying Non-Source Forms.
251+
252+ You may convey a covered work in object code form under the terms
253+of sections 4 and 5, provided that you also convey the
254+machine-readable Corresponding Source under the terms of this License,
255+in one of these ways:
256+
257+ a) Convey the object code in, or embodied in, a physical product
258+ (including a physical distribution medium), accompanied by the
259+ Corresponding Source fixed on a durable physical medium
260+ customarily used for software interchange.
261+
262+ b) Convey the object code in, or embodied in, a physical product
263+ (including a physical distribution medium), accompanied by a
264+ written offer, valid for at least three years and valid for as
265+ long as you offer spare parts or customer support for that product
266+ model, to give anyone who possesses the object code either (1) a
267+ copy of the Corresponding Source for all the software in the
268+ product that is covered by this License, on a durable physical
269+ medium customarily used for software interchange, for a price no
270+ more than your reasonable cost of physically performing this
271+ conveying of source, or (2) access to copy the
272+ Corresponding Source from a network server at no charge.
273+
274+ c) Convey individual copies of the object code with a copy of the
275+ written offer to provide the Corresponding Source. This
276+ alternative is allowed only occasionally and noncommercially, and
277+ only if you received the object code with such an offer, in accord
278+ with subsection 6b.
279+
280+ d) Convey the object code by offering access from a designated
281+ place (gratis or for a charge), and offer equivalent access to the
282+ Corresponding Source in the same way through the same place at no
283+ further charge. You need not require recipients to copy the
284+ Corresponding Source along with the object code. If the place to
285+ copy the object code is a network server, the Corresponding Source
286+ may be on a different server (operated by you or a third party)
287+ that supports equivalent copying facilities, provided you maintain
288+ clear directions next to the object code saying where to find the
289+ Corresponding Source. Regardless of what server hosts the
290+ Corresponding Source, you remain obligated to ensure that it is
291+ available for as long as needed to satisfy these requirements.
292+
293+ e) Convey the object code using peer-to-peer transmission, provided
294+ you inform other peers where the object code and Corresponding
295+ Source of the work are being offered to the general public at no
296+ charge under subsection 6d.
297+
298+ A separable portion of the object code, whose source code is excluded
299+from the Corresponding Source as a System Library, need not be
300+included in conveying the object code work.
301+
302+ A "User Product" is either (1) a "consumer product", which means any
303+tangible personal property which is normally used for personal, family,
304+or household purposes, or (2) anything designed or sold for incorporation
305+into a dwelling. In determining whether a product is a consumer product,
306+doubtful cases shall be resolved in favor of coverage. For a particular
307+product received by a particular user, "normally used" refers to a
308+typical or common use of that class of product, regardless of the status
309+of the particular user or of the way in which the particular user
310+actually uses, or expects or is expected to use, the product. A product
311+is a consumer product regardless of whether the product has substantial
312+commercial, industrial or non-consumer uses, unless such uses represent
313+the only significant mode of use of the product.
314+
315+ "Installation Information" for a User Product means any methods,
316+procedures, authorization keys, or other information required to install
317+and execute modified versions of a covered work in that User Product from
318+a modified version of its Corresponding Source. The information must
319+suffice to ensure that the continued functioning of the modified object
320+code is in no case prevented or interfered with solely because
321+modification has been made.
322+
323+ If you convey an object code work under this section in, or with, or
324+specifically for use in, a User Product, and the conveying occurs as
325+part of a transaction in which the right of possession and use of the
326+User Product is transferred to the recipient in perpetuity or for a
327+fixed term (regardless of how the transaction is characterized), the
328+Corresponding Source conveyed under this section must be accompanied
329+by the Installation Information. But this requirement does not apply
330+if neither you nor any third party retains the ability to install
331+modified object code on the User Product (for example, the work has
332+been installed in ROM).
333+
334+ The requirement to provide Installation Information does not include a
335+requirement to continue to provide support service, warranty, or updates
336+for a work that has been modified or installed by the recipient, or for
337+the User Product in which it has been modified or installed. Access to a
338+network may be denied when the modification itself materially and
339+adversely affects the operation of the network or violates the rules and
340+protocols for communication across the network.
341+
342+ Corresponding Source conveyed, and Installation Information provided,
343+in accord with this section must be in a format that is publicly
344+documented (and with an implementation available to the public in
345+source code form), and must require no special password or key for
346+unpacking, reading or copying.
347+
348+ 7. Additional Terms.
349+
350+ "Additional permissions" are terms that supplement the terms of this
351+License by making exceptions from one or more of its conditions.
352+Additional permissions that are applicable to the entire Program shall
353+be treated as though they were included in this License, to the extent
354+that they are valid under applicable law. If additional permissions
355+apply only to part of the Program, that part may be used separately
356+under those permissions, but the entire Program remains governed by
357+this License without regard to the additional permissions.
358+
359+ When you convey a copy of a covered work, you may at your option
360+remove any additional permissions from that copy, or from any part of
361+it. (Additional permissions may be written to require their own
362+removal in certain cases when you modify the work.) You may place
363+additional permissions on material, added by you to a covered work,
364+for which you have or can give appropriate copyright permission.
365+
366+ Notwithstanding any other provision of this License, for material you
367+add to a covered work, you may (if authorized by the copyright holders of
368+that material) supplement the terms of this License with terms:
369+
370+ a) Disclaiming warranty or limiting liability differently from the
371+ terms of sections 15 and 16 of this License; or
372+
373+ b) Requiring preservation of specified reasonable legal notices or
374+ author attributions in that material or in the Appropriate Legal
375+ Notices displayed by works containing it; or
376+
377+ c) Prohibiting misrepresentation of the origin of that material, or
378+ requiring that modified versions of such material be marked in
379+ reasonable ways as different from the original version; or
380+
381+ d) Limiting the use for publicity purposes of names of licensors or
382+ authors of the material; or
383+
384+ e) Declining to grant rights under trademark law for use of some
385+ trade names, trademarks, or service marks; or
386+
387+ f) Requiring indemnification of licensors and authors of that
388+ material by anyone who conveys the material (or modified versions of
389+ it) with contractual assumptions of liability to the recipient, for
390+ any liability that these contractual assumptions directly impose on
391+ those licensors and authors.
392+
393+ All other non-permissive additional terms are considered "further
394+restrictions" within the meaning of section 10. If the Program as you
395+received it, or any part of it, contains a notice stating that it is
396+governed by this License along with a term that is a further
397+restriction, you may remove that term. If a license document contains
398+a further restriction but permits relicensing or conveying under this
399+License, you may add to a covered work material governed by the terms
400+of that license document, provided that the further restriction does
401+not survive such relicensing or conveying.
402+
403+ If you add terms to a covered work in accord with this section, you
404+must place, in the relevant source files, a statement of the
405+additional terms that apply to those files, or a notice indicating
406+where to find the applicable terms.
407+
408+ Additional terms, permissive or non-permissive, may be stated in the
409+form of a separately written license, or stated as exceptions;
410+the above requirements apply either way.
411+
412+ 8. Termination.
413+
414+ You may not propagate or modify a covered work except as expressly
415+provided under this License. Any attempt otherwise to propagate or
416+modify it is void, and will automatically terminate your rights under
417+this License (including any patent licenses granted under the third
418+paragraph of section 11).
419+
420+ However, if you cease all violation of this License, then your
421+license from a particular copyright holder is reinstated (a)
422+provisionally, unless and until the copyright holder explicitly and
423+finally terminates your license, and (b) permanently, if the copyright
424+holder fails to notify you of the violation by some reasonable means
425+prior to 60 days after the cessation.
426+
427+ Moreover, your license from a particular copyright holder is
428+reinstated permanently if the copyright holder notifies you of the
429+violation by some reasonable means, this is the first time you have
430+received notice of violation of this License (for any work) from that
431+copyright holder, and you cure the violation prior to 30 days after
432+your receipt of the notice.
433+
434+ Termination of your rights under this section does not terminate the
435+licenses of parties who have received copies or rights from you under
436+this License. If your rights have been terminated and not permanently
437+reinstated, you do not qualify to receive new licenses for the same
438+material under section 10.
439+
440+ 9. Acceptance Not Required for Having Copies.
441+
442+ You are not required to accept this License in order to receive or
443+run a copy of the Program. Ancillary propagation of a covered work
444+occurring solely as a consequence of using peer-to-peer transmission
445+to receive a copy likewise does not require acceptance. However,
446+nothing other than this License grants you permission to propagate or
447+modify any covered work. These actions infringe copyright if you do
448+not accept this License. Therefore, by modifying or propagating a
449+covered work, you indicate your acceptance of this License to do so.
450+
451+ 10. Automatic Licensing of Downstream Recipients.
452+
453+ Each time you convey a covered work, the recipient automatically
454+receives a license from the original licensors, to run, modify and
455+propagate that work, subject to this License. You are not responsible
456+for enforcing compliance by third parties with this License.
457+
458+ An "entity transaction" is a transaction transferring control of an
459+organization, or substantially all assets of one, or subdividing an
460+organization, or merging organizations. If propagation of a covered
461+work results from an entity transaction, each party to that
462+transaction who receives a copy of the work also receives whatever
463+licenses to the work the party's predecessor in interest had or could
464+give under the previous paragraph, plus a right to possession of the
465+Corresponding Source of the work from the predecessor in interest, if
466+the predecessor has it or can get it with reasonable efforts.
467+
468+ You may not impose any further restrictions on the exercise of the
469+rights granted or affirmed under this License. For example, you may
470+not impose a license fee, royalty, or other charge for exercise of
471+rights granted under this License, and you may not initiate litigation
472+(including a cross-claim or counterclaim in a lawsuit) alleging that
473+any patent claim is infringed by making, using, selling, offering for
474+sale, or importing the Program or any portion of it.
475+
476+ 11. Patents.
477+
478+ A "contributor" is a copyright holder who authorizes use under this
479+License of the Program or a work on which the Program is based. The
480+work thus licensed is called the contributor's "contributor version".
481+
482+ A contributor's "essential patent claims" are all patent claims
483+owned or controlled by the contributor, whether already acquired or
484+hereafter acquired, that would be infringed by some manner, permitted
485+by this License, of making, using, or selling its contributor version,
486+but do not include claims that would be infringed only as a
487+consequence of further modification of the contributor version. For
488+purposes of this definition, "control" includes the right to grant
489+patent sublicenses in a manner consistent with the requirements of
490+this License.
491+
492+ Each contributor grants you a non-exclusive, worldwide, royalty-free
493+patent license under the contributor's essential patent claims, to
494+make, use, sell, offer for sale, import and otherwise run, modify and
495+propagate the contents of its contributor version.
496+
497+ In the following three paragraphs, a "patent license" is any express
498+agreement or commitment, however denominated, not to enforce a patent
499+(such as an express permission to practice a patent or covenant not to
500+sue for patent infringement). To "grant" such a patent license to a
501+party means to make such an agreement or commitment not to enforce a
502+patent against the party.
503+
504+ If you convey a covered work, knowingly relying on a patent license,
505+and the Corresponding Source of the work is not available for anyone
506+to copy, free of charge and under the terms of this License, through a
507+publicly available network server or other readily accessible means,
508+then you must either (1) cause the Corresponding Source to be so
509+available, or (2) arrange to deprive yourself of the benefit of the
510+patent license for this particular work, or (3) arrange, in a manner
511+consistent with the requirements of this License, to extend the patent
512+license to downstream recipients. "Knowingly relying" means you have
513+actual knowledge that, but for the patent license, your conveying the
514+covered work in a country, or your recipient's use of the covered work
515+in a country, would infringe one or more identifiable patents in that
516+country that you have reason to believe are valid.
517+
518+ If, pursuant to or in connection with a single transaction or
519+arrangement, you convey, or propagate by procuring conveyance of, a
520+covered work, and grant a patent license to some of the parties
521+receiving the covered work authorizing them to use, propagate, modify
522+or convey a specific copy of the covered work, then the patent license
523+you grant is automatically extended to all recipients of the covered
524+work and works based on it.
525+
526+ A patent license is "discriminatory" if it does not include within
527+the scope of its coverage, prohibits the exercise of, or is
528+conditioned on the non-exercise of one or more of the rights that are
529+specifically granted under this License. You may not convey a covered
530+work if you are a party to an arrangement with a third party that is
531+in the business of distributing software, under which you make payment
532+to the third party based on the extent of your activity of conveying
533+the work, and under which the third party grants, to any of the
534+parties who would receive the covered work from you, a discriminatory
535+patent license (a) in connection with copies of the covered work
536+conveyed by you (or copies made from those copies), or (b) primarily
537+for and in connection with specific products or compilations that
538+contain the covered work, unless you entered into that arrangement,
539+or that patent license was granted, prior to 28 March 2007.
540+
541+ Nothing in this License shall be construed as excluding or limiting
542+any implied license or other defenses to infringement that may
543+otherwise be available to you under applicable patent law.
544+
545+ 12. No Surrender of Others' Freedom.
546+
547+ If conditions are imposed on you (whether by court order, agreement or
548+otherwise) that contradict the conditions of this License, they do not
549+excuse you from the conditions of this License. If you cannot convey a
550+covered work so as to satisfy simultaneously your obligations under this
551+License and any other pertinent obligations, then as a consequence you may
552+not convey it at all. For example, if you agree to terms that obligate you
553+to collect a royalty for further conveying from those to whom you convey
554+the Program, the only way you could satisfy both those terms and this
555+License would be to refrain entirely from conveying the Program.
556+
557+ 13. Use with the GNU Affero General Public License.
558+
559+ Notwithstanding any other provision of this License, you have
560+permission to link or combine any covered work with a work licensed
561+under version 3 of the GNU Affero General Public License into a single
562+combined work, and to convey the resulting work. The terms of this
563+License will continue to apply to the part which is the covered work,
564+but the special requirements of the GNU Affero General Public License,
565+section 13, concerning interaction through a network will apply to the
566+combination as such.
567+
568+ 14. Revised Versions of this License.
569+
570+ The Free Software Foundation may publish revised and/or new versions of
571+the GNU General Public License from time to time. Such new versions will
572+be similar in spirit to the present version, but may differ in detail to
573+address new problems or concerns.
574+
575+ Each version is given a distinguishing version number. If the
576+Program specifies that a certain numbered version of the GNU General
577+Public License "or any later version" applies to it, you have the
578+option of following the terms and conditions either of that numbered
579+version or of any later version published by the Free Software
580+Foundation. If the Program does not specify a version number of the
581+GNU General Public License, you may choose any version ever published
582+by the Free Software Foundation.
583+
584+ If the Program specifies that a proxy can decide which future
585+versions of the GNU General Public License can be used, that proxy's
586+public statement of acceptance of a version permanently authorizes you
587+to choose that version for the Program.
588+
589+ Later license versions may give you additional or different
590+permissions. However, no additional obligations are imposed on any
591+author or copyright holder as a result of your choosing to follow a
592+later version.
593+
594+ 15. Disclaimer of Warranty.
595+
596+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
597+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
598+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
599+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
600+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
601+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
602+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
603+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
604+
605+ 16. Limitation of Liability.
606+
607+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
608+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
609+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
610+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
611+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
612+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
613+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
614+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
615+SUCH DAMAGES.
616+
617+ 17. Interpretation of Sections 15 and 16.
618+
619+ If the disclaimer of warranty and limitation of liability provided
620+above cannot be given local legal effect according to their terms,
621+reviewing courts shall apply local law that most closely approximates
622+an absolute waiver of all civil liability in connection with the
623+Program, unless a warranty or assumption of liability accompanies a
624+copy of the Program in return for a fee.
625+
626+ END OF TERMS AND CONDITIONS
627+
628+ How to Apply These Terms to Your New Programs
629+
630+ If you develop a new program, and you want it to be of the greatest
631+possible use to the public, the best way to achieve this is to make it
632+free software which everyone can redistribute and change under these terms.
633+
634+ To do so, attach the following notices to the program. It is safest
635+to attach them to the start of each source file to most effectively
636+state the exclusion of warranty; and each file should have at least
637+the "copyright" line and a pointer to where the full notice is found.
638+
639+ <one line to give the program's name and a brief idea of what it does.>
640+ Copyright (C) <year> <name of author>
641+
642+ This program is free software: you can redistribute it and/or modify
643+ it under the terms of the GNU General Public License as published by
644+ the Free Software Foundation, either version 3 of the License, or
645+ (at your option) any later version.
646+
647+ This program is distributed in the hope that it will be useful,
648+ but WITHOUT ANY WARRANTY; without even the implied warranty of
649+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
650+ GNU General Public License for more details.
651+
652+ You should have received a copy of the GNU General Public License
653+ along with this program. If not, see <http://www.gnu.org/licenses/>.
654+
655+Also add information on how to contact you by electronic and paper mail.
656+
657+ If the program does terminal interaction, make it output a short
658+notice like this when it starts in an interactive mode:
659+
660+ <program> Copyright (C) <year> <name of author>
661+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
662+ This is free software, and you are welcome to redistribute it
663+ under certain conditions; type `show c' for details.
664+
665+The hypothetical commands `show w' and `show c' should show the appropriate
666+parts of the General Public License. Of course, your program's commands
667+might be different; for a GUI interface, you would use an "about box".
668+
669+ You should also get your employer (if you work as a programmer) or school,
670+if any, to sign a "copyright disclaimer" for the program, if necessary.
671+For more information on this, and how to apply and follow the GNU GPL, see
672+<http://www.gnu.org/licenses/>.
673+
674+ The GNU General Public License does not permit incorporating your program
675+into proprietary programs. If your program is a subroutine library, you
676+may consider it more useful to permit linking proprietary applications with
677+the library. If this is what you want to do, use the GNU Lesser General
678+Public License instead of this License. But first, please read
679+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
680+
681
682=== renamed file 'COPYING' => 'COPYING.moved'
683=== added file 'CoverageTestRunner.py'
684--- CoverageTestRunner.py 1970-01-01 00:00:00 +0000
685+++ CoverageTestRunner.py 2011-02-28 18:58:01 +0000
686@@ -0,0 +1,203 @@
687+# Copyright (C) 2007 Lars Wirzenius <liw@iki.fi>
688+#
689+# This program is free software; you can redistribute it and/or modify
690+# it under the terms of the GNU General Public License as published by
691+# the Free Software Foundation; either version 2 of the License, or
692+# (at your option) any later version.
693+#
694+# This program is distributed in the hope that it will be useful,
695+# but WITHOUT ANY WARRANTY; without even the implied warranty of
696+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
697+# GNU General Public License for more details.
698+#
699+# You should have received a copy of the GNU General Public License along
700+# with this program; if not, write to the Free Software Foundation, Inc.,
701+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
702+
703+
704+import coverage
705+import unittest
706+import optparse
707+import os
708+import imp
709+import sys
710+import time
711+
712+
713+class CoverageTestResult(unittest.TestResult):
714+
715+ def __init__(self, output, total):
716+ unittest.TestResult.__init__(self)
717+ self.output = output
718+ self.total = total
719+ self.lastmsg = ""
720+ self.coverage_missed = []
721+ self.timings = []
722+
723+ def addCoverageMissed(self, filename, statements, missed_statements,
724+ missed_description):
725+ self.coverage_missed.append((filename, statements, missed_statements,
726+ missed_description))
727+
728+ def wasSuccessful(self, ignore_coverage=False):
729+ return (unittest.TestResult.wasSuccessful(self) and
730+ (ignore_coverage or not self.coverage_missed))
731+
732+ def clearmsg(self):
733+ self.output.write("\b \b" * len(self.lastmsg))
734+ self.lastmsg = ""
735+
736+ def write(self, test):
737+ self.clearmsg()
738+ self.lastmsg = "Running test %d/%d: %s" % (self.testsRun,
739+ self.total,
740+ str(test)[:50])
741+ self.output.write(self.lastmsg)
742+ self.output.flush()
743+
744+ def startTest(self, test):
745+ unittest.TestResult.startTest(self, test)
746+ self.write(test)
747+ self.start_time = time.time()
748+
749+ def stopTest(self, test):
750+ end_time = time.time()
751+ unittest.TestResult.stopTest(self, test)
752+ self.timings.append((end_time - self.start_time, test))
753+
754+
755+class CoverageTestRunner:
756+
757+ """A test runner class that insists modules' tests cover them fully."""
758+
759+ def __init__(self):
760+ self._module_pairs = []
761+
762+ def add_pair(self, module_pathname, test_module_pathname):
763+ """Add a module and its test module to list of tests."""
764+ self._module_pairs.append((module_pathname, test_module_pathname))
765+
766+ def find_pairs(self, dirname):
767+ """Find all module/test module pairs in directory tree.
768+
769+ This method relies on a naming convention: it scans a directory
770+ tree and assumes that for any file foo.py, if there exists a
771+ file foo_tests.py or fooTests.py, they form a pair.
772+
773+ """
774+
775+ suffixes = ["_tests.py", "Tests.py"]
776+
777+
778+ for dirname, dirnames, filenames in os.walk(dirname):
779+ tests = []
780+ for filename in filenames:
781+ for suffix in suffixes:
782+ if filename.endswith(suffix):
783+ module = filename[:-len(suffix)] + ".py"
784+ if module in filenames:
785+ module = os.path.join(dirname, module)
786+ filename = os.path.join(dirname, filename)
787+ self.add_pair(module, filename)
788+
789+ def _load_module_from_pathname(self, pathname):
790+ for tuple in imp.get_suffixes():
791+ suffix, mode, type = tuple
792+ if pathname.endswith(suffix):
793+ name = os.path.basename(pathname[:-len(suffix)])
794+ f = file(pathname, mode)
795+ return imp.load_module(name, f, pathname, tuple)
796+ raise Exception("Unknown module: %s" % pathname)
797+
798+ def _load_pairs(self):
799+ module_pairs = []
800+ loader = unittest.defaultTestLoader
801+ for pathname, test_pathname in self._module_pairs:
802+ module = self._load_module_from_pathname(pathname)
803+ test_module = self._load_module_from_pathname(test_pathname)
804+ suite = loader.loadTestsFromModule(test_module)
805+ module_pairs.append((module, test_module, suite))
806+ return module_pairs
807+
808+ def printErrorList(self, flavor, errors):
809+ for test, error in errors:
810+ print "%s: %s" % (flavor, str(test))
811+ print str(error)
812+
813+ def run(self):
814+ start_time = time.time()
815+
816+ module_pairs = self._load_pairs()
817+ total_tests = sum(suite.countTestCases()
818+ for x, y, suite in module_pairs)
819+ result = CoverageTestResult(sys.stdout, total_tests)
820+
821+ for module, test_module, suite in module_pairs:
822+ coverage.erase()
823+ coverage.exclude("#\s*pragma: no cover")
824+ coverage.start()
825+ sys.path.insert(0, os.path.dirname(module.__file__))
826+ reload(module)
827+ del sys.path[0]
828+ suite.run(result)
829+ coverage.stop()
830+ filename, stmts, missed, missed_desc = coverage.analysis(module)
831+ if missed:
832+ result.addCoverageMissed(filename, stmts, missed, missed_desc)
833+
834+ end_time = time.time()
835+
836+ sys.stdout.write("\n\n")
837+
838+ if result.wasSuccessful():
839+ print "OK"
840+ else:
841+ print "FAILED"
842+ print
843+ if result.errors:
844+ self.printErrorList("ERROR", result.errors)
845+ if result.failures:
846+ self.printErrorList("FAILURE", result.failures)
847+ if result.coverage_missed:
848+ print
849+ print "Statements missed by per-module tests:"
850+ width = max(len(x[0]) for x in result.coverage_missed)
851+ fmt = " %-*s %s"
852+ print fmt % (width, "Module", "Missed statements")
853+ for filename, _, _, desc in sorted(result.coverage_missed):
854+ print fmt % (width, filename, desc)
855+ print
856+
857+ print "%d failures, %d errors" % (len(result.failures),
858+ len(result.errors))
859+
860+ if end_time - start_time > 10:
861+ print
862+ print "Slowest tests:"
863+ for secs, test in sorted(result.timings)[-10:]:
864+ print " %5.1f s %s" % (secs, str(test)[:70])
865+
866+ print "Time: %.1f s" % (end_time - start_time)
867+
868+ return result
869+
870+
871+def run(dirname="."):
872+ """Use CoverageTestRunner on the desired directory."""
873+
874+ parser = optparse.OptionParser()
875+ parser.add_option("--ignore-coverage", action="store_true",
876+ help="Don't fail tests even if coverage is "
877+ "incomplete.")
878+
879+ opts, args = parser.parse_args()
880+
881+ runner = CoverageTestRunner()
882+ runner.find_pairs(dirname)
883+ result = runner.run()
884+ if not result.wasSuccessful(ignore_coverage=opts.ignore_coverage):
885+ sys.exit(1)
886+
887+
888+if __name__ == "__main__":
889+ run()
890
891=== renamed file 'CoverageTestRunner.py' => 'CoverageTestRunner.py.moved'
892=== added file 'Makefile'
893--- Makefile 1970-01-01 00:00:00 +0000
894+++ Makefile 2011-02-28 18:58:01 +0000
895@@ -0,0 +1,51 @@
896+# Makefile for Computer Janitor
897+# Copyright (C) 2008, 2009 Canonical, Ltd.
898+#
899+# This program is free software: you can redistribute it and/or modify
900+# it under the terms of the GNU General Public License as published by
901+# the Free Software Foundation, version 3 of the License.
902+#
903+# This program is distributed in the hope that it will be useful,
904+# but WITHOUT ANY WARRANTY; without even the implied warranty of
905+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
906+# GNU General Public License for more details.
907+#
908+# You should have received a copy of the GNU General Public License
909+# along with this program. If not, see <http://www.gnu.org/licenses/>.
910+
911+all: data/computer-janitor-gtk.desktop
912+
913+DESKTOP=data/computer-janitor-gtk.desktop
914+$(DESKTOP): $(DESKTOP).in
915+ intltool-merge -d po $(DESKTOP).in $(DESKTOP)
916+
917+check: check-unittests check-has-unit-tests check-licenses
918+
919+check-unittests:
920+ env COMPUTER_JANITOR_UNITTEST=yes LC_ALL=C python -m CoverageTestRunner
921+ rm -f .coverage
922+
923+check-has-unit-tests:
924+ find computerjanitorapp -name '*.py' ! -name '*_tests.py' | \
925+ grep -Exv '^computerjanitorapp/(__init__|ui_gtk).py' | \
926+ sed 's/\.py$$//' | \
927+ while read file; do \
928+ [ -e $${file}_tests.py ] || \
929+ (echo "Missing unit test: $$file.py"; exit 1); done
930+
931+check-licenses:
932+ ./license-check
933+
934+clean:
935+ rm -f computerjanitorapp/*.py[co] .coverage
936+ rm -f po/computerjanitor.pot
937+ rm -rf build $(DESKTOP)
938+
939+update-po:
940+ echo '[encoding: UTF-8]' > po/POTFILES.in; \
941+ find computerjanitorapp -name \*.py >> po/POTFILES.in; \
942+ for f in data/*.ui; do \
943+ echo '[type: gettext/glade]'$$f >> po/POTFILES.in; \
944+ done; \
945+ ls data/*.in plugins/*.py >> po/POTFILES.in; \
946+ python setup.py build_i18n --merge-po --po-dir po
947
948=== renamed file 'Makefile' => 'Makefile.moved'
949=== added file 'NEWS'
950--- NEWS 1970-01-01 00:00:00 +0000
951+++ NEWS 2011-02-28 18:58:01 +0000
952@@ -0,0 +1,237 @@
953+NEWS for Computer Janitor
954+=========================
955+
956+This file summarizes mainly user-visible changes. For detailed
957+changes, please see the bzr commit log messages.
958+
959+Version 2.0.4, released 2010-09-20
960+
961+ * Update translation strings (LP: #621723) (LP: #612493)
962+ * computer-janitor-gtk --help and --version switches added
963+ * Fixed SystemError crash in open() (LP: #545306)
964+ * Do not depend on python-fstab any more.
965+ * Fixed icon (LP: #434431) - Pavol Klačanský
966+
967+Version 2.0, released 2010-03-15
968+
969+ * Refactored the system modifying parts of Computer Janitor into a
970+ separate dbus daemon. This allows you to run the cli or gtk versions
971+ without being root. You will be prompted for the administrative
972+ password when necessary.
973+
974+ * The user interface has been cleaned up to remove previously unused gui
975+ elements.
976+
977+Version 1.13.3, released 2009-09-09
978+
979+ * Fixed URL in About dialog.
980+
981+ * The "Do cleanups" button is now set to sensitive when there is
982+ something to clean.
983+
984+ * The user is now told if nothing was found to clean up, so they
985+ don't wonder why the window is empty.
986+
987+ * The 'unsupported package' plugin is re-enabled.
988+
989+Version 1.13.2, released 2009-08-21
990+
991+ * Versions 1.13.0 and 1.13.1 were not released publically. This
992+ section documents all changes since 1.12.1.
993+
994+ * The GTK user interface has been completely rewritten from scratch.
995+ Thanks to Martin Albisetti for the general design. All bugs are belong
996+ to Lars Wirzenius anyway.
997+
998+ * Adapted to new python-apt API. This means the code only works on
999+ Ubuntu karmic, not earlier releases.
1000+
1001+ * The 'unsupported package' plugin is now not installed by default.
1002+ This should make it less likely people remove third-party packages
1003+ by default.
1004+
1005+ * The fstab plugin is not installed by default. It is no longer needed
1006+ in karmic, since the Linux kernel now uses relatime by default.
1007+
1008+ BUG FIXES:
1009+
1010+ * I18n changes to account for the fact that Computer Janitor's code
1011+ is in two places (itself, and Update Manager), and thus two po
1012+ files.
1013+
1014+ * I18n change to include plugin code as well. Thanks to Gabor Kelemen.
1015+
1016+ * Other i18n changes, in both the GTK and command line user interfaces.
1017+
1018+ * All instances of the word 'cruft' have been removed from the user
1019+ interface. It is not a proper English word.
1020+
1021+ * Typo fix in manual page. Thanks to Charlie_Smotherman.
1022+
1023+Version 1.12.1, released 2009-03-04
1024+
1025+ * Change the description text at the top of the main window to be
1026+ clearer.
1027+
1028+ * Sort cruft in the GTK user interface in groups. This should
1029+ some day get user-visible headings, so it's clearer to the user.
1030+
1031+ BUG FIXES:
1032+
1033+ * In the GTK user interface, the button "Cleanup" now uses "u" as the
1034+ accelerator key, rather than "c", which confliced with the
1035+ accelerator key for the "Close" button. Fixes LP: #311557, reported
1036+ by hackel.
1037+
1038+ * Changed the label for the yes/no column in the GTK UI to be
1039+ "Remove/fix?" rather than just "Remove?", since not all things
1040+ are removals. Fixes LP: #300354, reported by Hernando Torque.
1041+
1042+Version 1.12, released 2009-02-19
1043+
1044+ * New plugin: remove files left on the filesystem by the dpkg
1045+ conffile handling (*.dpkg-old and *.dpkg-new).
1046+
1047+ * Code base has been split so that the core parts (plugin management,
1048+ some of the plugins) now reside in the update-manager source tree.
1049+ This was done so that update-manager can use the computer-janitor
1050+ plugins, since there was no point in both programs having code to do
1051+ the same thing.
1052+
1053+ * When handling exceptions within the computer-janitor code, the
1054+ actual exception is now logged (at DEBUG level), to aid in
1055+ debugging the cause.
1056+
1057+ * Plugins can now declare that they should only be used under specific
1058+ conditions, rather than always. This is useful for plugins that are
1059+ used by update-manager.
1060+
1061+Version 1.11, released 2009-02-03
1062+
1063+ * Renamed to Computer Janitor. The old name, Cruft Remover, was using
1064+ computer jargon (cruft) and gave the wrong impression (the program
1065+ does not just remove stuff).
1066+
1067+ * New plugin to compact the dpkg status file, from Michael Vogt.
1068+
1069+Version 1.10.5, released 2009-01-16
1070+
1071+ * Upon startup, we now verify that the package lists look sane by
1072+ checking for the dash and gzip packages. If they're not available,
1073+ we assume something is badly wrong and we won't continue. This
1074+ makes is very unlikely that people will accidentally remove all
1075+ their packages because apt didn't have any package lists available.
1076+
1077+ * A further safeguard: the running kernel and related packages
1078+ won't ever be considered cruft anymore.
1079+
1080+ * It is now no longer enough to click on the "Cleanup" button to
1081+ initiate changes (pacxkage removals etc). Instead, a dialog window
1082+ will pop up and the user has to click on "OK" to continue.
1083+
1084+ * A .deb package's short description is now shown in the cruft
1085+ description. This will help users have an idea what it's all
1086+ about. (It's insufficient, but was all that there was time for
1087+ for the Ubuntu intrepid release schedule.)
1088+
1089+ * Columns in the cruft list now have titles, to make it easier
1090+ to understand what ticks mean (remove cruft? don't remove
1091+ cruft?).
1092+
1093+ * The program now reads whitelists in /etc/cruft-remover.d. Cruft
1094+ named in files in that directory will _not_ be removed.
1095+
1096+ * The plugin to remove packages apt considers autoremovable is now
1097+ enabled. It has been written ages ago, but not been enabled by
1098+ default until now.
1099+
1100+ * The application now uses the icon it has.
1101+
1102+ * Added Polish translation from Piotr Makowski.
1103+
1104+ * Added Finnish translation from Lars Wirzenius and Timo Jyrinki.
1105+
1106+Version 1.10.4, released 2008-10-27
1107+
1108+ * Quick bug fixes for the Ubuntu 8.10 release. A proper release
1109+ of Cruft Remover will be made later.
1110+
1111+Version 1.10.3, released 2008-10-14
1112+
1113+ * Bug fixes to how messages are logged, primarily to how character
1114+ sets are encoded. Found by James Westby.
1115+
1116+Version 1.10.2, released 2008-10-14
1117+
1118+ IMPORTANT CHANGES:
1119+
1120+ * If setting up logging to syslog fails, don't crash. Thanks,
1121+ Ricardo Wurmus. (LP:#274970)
1122+
1123+ * The program now supports translations via gettext. A Finnish
1124+ translation was added. (LP:#279580)
1125+
1126+ * Handles errors by giving nice(r) error messages also in the
1127+ command line version. The GTK+ version already did that.
1128+ Thanks, Marco Rodrigues. (LP:#281657)
1129+
1130+ MINOR CHANGES:
1131+
1132+ * Wording in manual page fixed. Thanks, James Westby.
1133+
1134+ * Non-functional help button hidden. Thanks, Loïc Minier. (LP:#279577)
1135+
1136+ * Cruft description in the GTK+ user interface is cleared after cruft has
1137+ been cleaned up.
1138+
1139+Version 1.10.1, released 2008-10-06
1140+
1141+ * Bugfix: Cruft in /etc/fstab now goes away from the GTK user interface
1142+ after a cleanup. Found by James Westby, thanks.
1143+
1144+ * cruft-remover-gtk now has a manual page.
1145+
1146+ * Bugfix: The GTK user interface now no longer imports the gtk and
1147+ gobject Python modules globally, but only when the code actually
1148+ runs, to prevent them from being loaded when running unit tests,
1149+ etc. (The GTK UI code is not unit tested.)
1150+
1151+Version 1.10, released 2008-09-30
1152+
1153+ * Important fixes to how GTK+ and threads are used: now all GTK+
1154+ stuff happens only in the main thread. This turns out to be
1155+ easier to get right than using gtk.gdk.threads_enter and _leave
1156+ correctly in all situations, and also allows things like
1157+ gtk.Dialog.run to be used.
1158+
1159+ * Missing test module for the deb_plugin module written.
1160+
1161+ * SystemError exceptions (which are raised by python-apt when
1162+ there is a dpkg error) are now handled, and an error displayed
1163+ to the user. This fixes LP: #275034.
1164+
1165+ * The GTK user interface now displays the version number at startup.
1166+ The command line interface shows it with the new --version option.
1167+
1168+Version 1.9, released 2008-09-26
1169+
1170+ * Bug fix: cruft-remover-gtk now correctly refreshes the list of
1171+ cruft after a bug has been fixed. (LP:274715)
1172+
1173+ * Bug fix: cruft-remover-gtk disables the "Cleanup" button if there
1174+ is nothing to clean up. (LP:274717)
1175+
1176+Version 1.8, released 2008-09-23
1177+
1178+ * The GTK user interface now lets you select a row without toggling
1179+ the status of the cruft. This is useful, because the description
1180+ of each cruft is shown for the selected row only.
1181+
1182+ * Both the GTK and command line user interfaces now attempt to
1183+ be more helpful in describing what each piece of cruft is.
1184+
1185+Version 1.7, released 2008-09-12
1186+
1187+ * First real public release, despite the version number. (If you have
1188+ seen any earlier versions, they were not real. Not real, I tell
1189+ you! Not real! They didn't exist! Really!)
1190
1191=== renamed file 'NEWS' => 'NEWS.moved'
1192=== added file 'README'
1193--- README 1970-01-01 00:00:00 +0000
1194+++ README 2011-02-28 18:58:01 +0000
1195@@ -0,0 +1,31 @@
1196+README for Computer Janitor
1197+===========================
1198+
1199+
1200+Computer Janitor is a tool to clean up a system so that it is more like
1201+a freshly installed one. It was written for Ubuntu, but should be
1202+possible to port to other Unix-like operating systems.
1203+
1204+
1205+Copyright license
1206+-----------------
1207+
1208+Computer Janitor - clean up a computer system
1209+Copyright (C) 2008, 2009 Canonical, Ltd.
1210+
1211+The icon (in data/cruftremover-256x256.png and data/cruftremover-24x25.png):
1212+Copyright (C) Marco Rodrigues
1213+
1214+The following license applies to all files (including the icons):
1215+
1216+This program is free software: you can redistribute it and/or modify
1217+it under the terms of the GNU General Public License as published by
1218+the Free Software Foundation, version 3 of the License.
1219+
1220+This program is distributed in the hope that it will be useful,
1221+but WITHOUT ANY WARRANTY; without even the implied warranty of
1222+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1223+GNU General Public License for more details.
1224+
1225+You should have received a copy of the GNU General Public License
1226+along with this program. If not, see <http://www.gnu.org/licenses/>.
1227
1228=== renamed file 'README' => 'README.moved'
1229=== added file 'TODO'
1230--- TODO 1970-01-01 00:00:00 +0000
1231+++ TODO 2011-02-28 18:58:01 +0000
1232@@ -0,0 +1,30 @@
1233+Computer Janitor ideas
1234+
1235+ - policykit
1236+ - aptdaemon backend
1237+ - nx is disabled?
1238+ - cronjob to run daily to report cruft
1239+ - vrms plugin
1240+ - plugin to suggest removing -generic kernel if more specific one
1241+ is installed and running (-server, e.g.)
1242+ - configuration mechanism for plugins
1243+ - plugin to check for sources.list inconsistencies: $released-updates
1244+ enabled for foo, if $released is enabled for foo (where foo is
1245+ main, universe, etc)
1246+ - UI improvement: group items so that previously ignored stuff is in a
1247+ separate section of the list, and new stuff is at the top.
1248+ - UI improvement: more information about a .deb: size, full
1249+ description, when was it installed, which release did it come
1250+ from
1251+ - UI improvement: show sections based on type of cruft. e.g., .deb, fstab
1252+ - add policykit support
1253+ - cj: setup.py shouldn't hardcode version string, but should not
1254+ import computerjanitorapp either, since that requires access
1255+ to the computerjanitor package.
1256+ - import computerjanitor.version?
1257+ - write plugin: unpurged packages
1258+ - kde UI
1259+ - write plugin (early karmic): apt-get autoclean
1260+ - relatime unnecessary in karmic? at least obey noatime, strictatime
1261+ http://mjg59.livejournal.com/109286.html
1262+
1263
1264=== renamed file 'TODO' => 'TODO.moved'
1265=== added file 'computer-janitor'
1266--- computer-janitor 1970-01-01 00:00:00 +0000
1267+++ computer-janitor 2011-02-28 18:58:01 +0000
1268@@ -0,0 +1,41 @@
1269+#!/usr/bin/python
1270+#
1271+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1272+#
1273+# This program is free software: you can redistribute it and/or modify
1274+# it under the terms of the GNU General Public License as published by
1275+# the Free Software Foundation, version 3 of the License.
1276+#
1277+# This program is distributed in the hope that it will be useful,
1278+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1279+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1280+# GNU General Public License for more details.
1281+#
1282+# You should have received a copy of the GNU General Public License
1283+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1284+
1285+"""Start the command-line computer janitor."""
1286+
1287+from __future__ import absolute_import, unicode_literals
1288+
1289+__metaclass__ = type
1290+__all__ = [
1291+ ]
1292+
1293+
1294+import sys
1295+
1296+from computerjanitorapp import setup_gettext
1297+from computerjanitorapp.cli.main import main
1298+from dbus.exceptions import DBusException
1299+_ = setup_gettext()
1300+
1301+
1302+try:
1303+ main()
1304+except KeyboardInterrupt:
1305+ pass
1306+except DBusException as error:
1307+ print >> sys.stderr, _(
1308+ 'Cannot contact Computer Janitor dbus service; try again.')
1309+ sys.exit(1)
1310
1311=== added file 'computer-janitor-gtk'
1312--- computer-janitor-gtk 1970-01-01 00:00:00 +0000
1313+++ computer-janitor-gtk 2011-02-28 18:58:01 +0000
1314@@ -0,0 +1,67 @@
1315+#!/usr/bin/python
1316+#
1317+# computer-janitor-gtk - clean up a Unix-like operating system (GTK version)
1318+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1319+#
1320+# This program is free software: you can redistribute it and/or modify
1321+# it under the terms of the GNU General Public License as published by
1322+# the Free Software Foundation, version 3 of the License.
1323+#
1324+# This program is distributed in the hope that it will be useful,
1325+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1326+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1327+# GNU General Public License for more details.
1328+#
1329+# You should have received a copy of the GNU General Public License
1330+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1331+
1332+"""Start the gtk computer janitor."""
1333+
1334+from __future__ import absolute_import, unicode_literals
1335+
1336+__metaclass__ = type
1337+__all__ = [
1338+ ]
1339+
1340+
1341+import os
1342+import sys
1343+import logging
1344+import argparse
1345+import traceback
1346+import computerjanitor
1347+
1348+from computerjanitorapp import __version__, setup_gettext
1349+from computerjanitorapp.gtk.main import main
1350+from dbus.exceptions import DBusException
1351+
1352+_ = setup_gettext()
1353+
1354+
1355+# Parse some simple command line arguments.
1356+parser = argparse.ArgumentParser(
1357+ description=_("""\
1358+ Find and remove cruft from your system.
1359+
1360+ Cruft is anything that shouldn't be on your system, but is.
1361+ Stretching the definition, cruft is also things that should be on
1362+ your system but aren't."""))
1363+parser.add_argument(
1364+ '--version', action='version',
1365+ version=_('Computer Janitor {version}').format(
1366+ version=__version__))
1367+parser.parse_args()
1368+# If --version or --help is given, .parse_args() will not return.
1369+
1370+try:
1371+ logging.basicConfig()
1372+ log = logging.getLogger('computerjanitor')
1373+ main()
1374+except KeyboardInterrupt:
1375+ pass
1376+except DBusException as error:
1377+ log.exception('Cannot contact Computer Janitor dbus service; try again.')
1378+ sys.exit(1)
1379+except:
1380+ log.exception('computer-janitor-gtk uncaught exception')
1381+ sys.exit(1)
1382
1383=== added file 'computer-janitor-gtk.8'
1384--- computer-janitor-gtk.8 1970-01-01 00:00:00 +0000
1385+++ computer-janitor-gtk.8 2011-02-28 18:58:01 +0000
1386@@ -0,0 +1,36 @@
1387+.\" Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1388+.\"
1389+.\" This program is free software: you can redistribute it and/or modify
1390+.\" it under the terms of the GNU General Public License as published by
1391+.\" the Free Software Foundation, version 3 of the License.
1392+.\"
1393+.\" This program is distributed in the hope that it will be useful,
1394+.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
1395+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1396+.\" GNU General Public License for more details.
1397+.\"
1398+.\" You should have received a copy of the GNU General Public License
1399+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
1400+.TH COMPUTER-JANITOR-GTK 8 2010-03-17 Ubuntu "Ubuntu system administration"
1401+.SH NAME
1402+computer-janitor-gtk \- remove cruft from system (GUI version)
1403+.SH SYNOPSIS
1404+.B computer-janitor-gtk
1405+.SH DESCRIPTION
1406+.B computer-janitor-gtk
1407+is a version of
1408+.BR computer-janitor (8)
1409+with a graphical user interface.
1410+See the latter's manual page for more information.
1411+.SH ENVIRONMENT
1412+In addition to the environment variables understood by
1413+.BR computer-janitor (8),
1414+the following one is understood by
1415+.B computer-janitor-gtk
1416+as well.
1417+.TP
1418+.B COMPUTER_JANITOR_GLADE
1419+The location of the Glade data file to specify the user interface.
1420+You should not need to set this, unless you really know what you are doing.
1421+.SH "SEE ALSO"
1422+.BR computer-janitor (8).
1423
1424=== renamed file 'computer-janitor-gtk.8' => 'computer-janitor-gtk.8.moved'
1425=== renamed file 'computer-janitor-gtk' => 'computer-janitor-gtk.moved'
1426=== added file 'computer-janitor.8'
1427--- computer-janitor.8 1970-01-01 00:00:00 +0000
1428+++ computer-janitor.8 2011-02-28 18:58:01 +0000
1429@@ -0,0 +1,222 @@
1430+.\" Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1431+.\"
1432+.\" This program is free software: you can redistribute it and/or modify
1433+.\" it under the terms of the GNU General Public License as published by
1434+.\" the Free Software Foundation, version 3 of the License.
1435+.\"
1436+.\" This program is distributed in the hope that it will be useful,
1437+.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
1438+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1439+.\" GNU General Public License for more details.
1440+.\"
1441+.\" You should have received a copy of the GNU General Public License
1442+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
1443+.TH COMPUTER-JANITOR 8 2010-03-17 Ubuntu "Ubuntu system administration"
1444+.SH NAME
1445+computer-janitor \- clean up a system installation
1446+.SH SYNOPSIS
1447+.B "computer-janitor"
1448+.RB [ --version|-h ]
1449+.br
1450+.B "computer-janitor find"
1451+.RB [ -i|-r|-s|-v|-h ]
1452+.RI
1453+.br
1454+.B "computer-janitor clean"
1455+.RB [ -a|-v|-h ]
1456+.RI [ CRUFT ]...
1457+.br
1458+.B "computer-janitor ignore"
1459+.RB [ -h ]
1460+.RI CRUFT
1461+.br
1462+.B "computer-janitor unignore"
1463+.RB [ -h ]
1464+.RI CRUFT
1465+.br
1466+.B computer-janitor-gtk
1467+.SH DESCRIPTION
1468+.B computer-janitor
1469+and
1470+.B computer-janitor-gtk
1471+find and remove
1472+.I cruft
1473+from your system.
1474+The former is a command line program,
1475+the latter has a graphical user interface.
1476+.PP
1477+Cruft is anything that shouldn't be on the system, but is.
1478+Stretching the definition, it is also things that should be on the system,
1479+but aren't.
1480+Examples:
1481+.IP \(bu
1482+Packages that were originally installed because something else depended on
1483+them, but the depending package has since been removed.
1484+Typically this includes large numbers of libraries.
1485+.IP \(bu
1486+Packages that are no longer supported by the current release of the
1487+operating system.
1488+For example, this can be applications whose development have stopped
1489+and that no longer have support, including for security issues.
1490+Keeping such applications installed can be dangerous.
1491+.IP \(bu
1492+Configuration tweaks that are missing from the system,
1493+but which would be there if the system was installed from scratch.
1494+For example, mount options for filesystems such as
1495+the
1496+.B relatime
1497+option.
1498+.PP
1499+.B computer-janitor
1500+has four modes of operation, invoked by the first non-option
1501+word in the argument list.
1502+.IP \(bu
1503+.B find
1504+searches for cruft and prints out a list of them.
1505+Each piece of cruft is also tagged with its state:
1506+.I ignored
1507+or
1508+.IR removable .
1509+.IP \(bu
1510+.B clean
1511+actually removes the cruft.
1512+It will remove those pieces of cruft you name on the command line.
1513+If you want to remove everything identified by
1514+.B find
1515+that is marked
1516+.IR removable ,
1517+use the
1518+.B --all
1519+option.
1520+.IP \(bu
1521+.B ignore
1522+and
1523+.B unignore
1524+mark cruft as
1525+.I ignored
1526+or
1527+.IR removable ,
1528+respectively.
1529+.SH OPTIONS
1530+Each subcommand listed above has its own set of options. If
1531+.B computer-janitor
1532+is invoked with no subcommand, the following options are available:
1533+.TP
1534+.B --version
1535+Print the version number and exit.
1536+.TP
1537+.BR --help | -h
1538+Print some global help and exit.
1539+.PP
1540+The
1541+.B find
1542+subcommand supports the following options:
1543+.TP
1544+.BR --ignored | -i
1545+Find and display only the system's ignored cruft.
1546+.TP
1547+.BR --removable | -r
1548+Find and display only the system's removable cruft.
1549+.TP
1550+.BR --short | -s
1551+Display only the cruft names; do not use with
1552+.BR --verbose .
1553+.TP
1554+.BR --verbose | -v
1555+Display a detailed explanation for each piece of cruft found.
1556+.TP
1557+.BR --help | -h
1558+Print detailed help for the
1559+.B find
1560+subcommand and exit.
1561+.PP
1562+The
1563+.B clean
1564+subcommand requires either a cruft name or the
1565+.B --all
1566+option to specify which cruft to remove. It supports the following options:
1567+.TP
1568+.B --all | -a
1569+Remove all system cruft that are not ignored.
1570+.TP
1571+.BR --verbose | -v
1572+Provide more details about the cruft being cleaned up.
1573+.TP
1574+.BR --help | -h
1575+Print detailed help for the
1576+.B clean
1577+subcommand and exit.
1578+.PP
1579+The
1580+.B ignore
1581+and
1582+.B unignore
1583+commands both take the name of a cruft to mark ignored or removable,
1584+respectively. They both also accept these options:
1585+.TP
1586+.BR --help | -h
1587+Print detailed help for the
1588+.B ignore
1589+or
1590+.B unignore
1591+subcommands and exit.
1592+.SH "EXIT STATUS"
1593+.B computer-janitor
1594+will return an exit code of 0 for successful operation (no errors).
1595+It will return a non-zero exit code if there are any errors.
1596+It is not an error to find cruft, or to not find cruft.
1597+.SH FILES
1598+.TP
1599+.B /var/lib/computer-janitor/state.dat
1600+This file stores the
1601+.I ignored
1602+or
1603+.I removable
1604+state of system cruft. Any cruft not listed in this file is by default
1605+.IR removable .
1606+.TP
1607+.B /etc/computer-janitor.d
1608+This directory contains whitelist files, which specify
1609+things that are never considered cruft.
1610+A whitelist file has a name that ends with
1611+.BR .whitelist ,
1612+and contains one (potential) cruft name per line.
1613+(Empty lines and lines beginning with # are ignored.)
1614+.SH EXAMPLE
1615+To find all cruft on the system:
1616+.sp 1
1617+.RS
1618+computer-janitor find
1619+.RE
1620+.PP
1621+To remove a specific piece of cruft:
1622+.sp 1
1623+.RS
1624+computer-janitor clean hello
1625+.RE
1626+.PP
1627+To mark a piece of cruft as
1628+.IR ignored ,
1629+so that it isn't removed by
1630+.B clean
1631+.BR --all :
1632+.sp 1
1633+.RS
1634+computer-janitor ignore hello
1635+.RE
1636+.PP
1637+To mark a piece of cruft as
1638+.I removable
1639+again:
1640+.sp 1
1641+.RS
1642+computer-janitor unignore hello
1643+.RE
1644+.PP
1645+To remove all cruft that isn't ignored:
1646+.sp 1
1647+.RS
1648+computer-janitor clean --all
1649+.RE
1650+.SH "SEE ALSO"
1651+.BR computer-janitor-gtk (8).
1652
1653=== renamed file 'computer-janitor.8' => 'computer-janitor.8.moved'
1654=== renamed file 'computer-janitor' => 'computer-janitor.moved'
1655=== added directory 'computerjanitorapp'
1656=== renamed directory 'computerjanitorapp' => 'computerjanitorapp.moved'
1657=== added file 'computerjanitorapp/__init__.py'
1658--- computerjanitorapp/__init__.py 1970-01-01 00:00:00 +0000
1659+++ computerjanitorapp/__init__.py 2011-02-28 18:58:01 +0000
1660@@ -0,0 +1,44 @@
1661+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1662+#
1663+# This program is free software: you can redistribute it and/or modify
1664+# it under the terms of the GNU General Public License as published by
1665+# the Free Software Foundation, version 3 of the License.
1666+#
1667+# This program is distributed in the hope that it will be useful,
1668+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1669+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1670+# GNU General Public License for more details.
1671+#
1672+# You should have received a copy of the GNU General Public License
1673+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1674+
1675+"""computerjanitorapp module."""
1676+
1677+from __future__ import absolute_import, unicode_literals
1678+
1679+__metaclass__ = type
1680+__all__ = [
1681+ '__version__',
1682+ 'setup_gettext',
1683+ ]
1684+
1685+
1686+__version__ = '2.0.4'
1687+
1688+
1689+# Set up gettext.
1690+def setup_gettext():
1691+ """Set up gettext for a module.
1692+
1693+ Return a method to be used for looking up translations. Usage:
1694+
1695+ >>> import computerjanitorapp
1696+ >>> _ = computerjanitorapp.setup_gettext()
1697+ """
1698+ import gettext
1699+ import os
1700+
1701+ domain = 'computerjanitor'
1702+ localedir = os.environ.get('LOCPATH', None)
1703+ t = gettext.translation(domain, localedir=localedir, fallback=True)
1704+ return t.ugettext
1705
1706=== added directory 'computerjanitorapp/cli'
1707=== added file 'computerjanitorapp/cli/__init__.py'
1708=== added file 'computerjanitorapp/cli/main.py'
1709--- computerjanitorapp/cli/main.py 1970-01-01 00:00:00 +0000
1710+++ computerjanitorapp/cli/main.py 2011-02-28 18:58:01 +0000
1711@@ -0,0 +1,312 @@
1712+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1713+#
1714+# This program is free software: you can redistribute it and/or modify
1715+# it under the terms of the GNU General Public License as published by
1716+# the Free Software Foundation, version 3 of the License.
1717+#
1718+# This program is distributed in the hope that it will be useful,
1719+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1720+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1721+# GNU General Public License for more details.
1722+#
1723+# You should have received a copy of the GNU General Public License
1724+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1725+
1726+"""Command line user interface."""
1727+
1728+from __future__ import absolute_import, unicode_literals
1729+
1730+__metaclass__ = type
1731+__all__ = [
1732+ 'main',
1733+ ]
1734+
1735+
1736+import sys
1737+import dbus
1738+import gobject
1739+import argparse
1740+import textwrap
1741+
1742+from dbus.mainloop.glib import DBusGMainLoop
1743+from operator import mod
1744+
1745+from computerjanitorapp import __version__, setup_gettext
1746+from computerjanitorapp.terminalsize import get_terminal_size
1747+from computerjanitorapp.utilities import format_size
1748+from computerjanitord.service import DBUS_INTERFACE_NAME
1749+_ = setup_gettext()
1750+
1751+
1752+class Options:
1753+ """Command line option parser."""
1754+ def __init__(self, runner):
1755+ """Parse the command line options.
1756+
1757+ :param runner: The class implementing the sub-commands.
1758+ :type runner: `Runner`
1759+ """
1760+ self.runner = runner
1761+ # Parse the arguments.
1762+ self.parser = argparse.ArgumentParser(
1763+ description=_("""\
1764+ Find and remove cruft from your system.
1765+
1766+ Cruft is anything that shouldn't be on your system, but is.
1767+ Stretching the definition, cruft is also things that should be on
1768+ your system but aren't."""))
1769+ self.parser.add_argument(
1770+ '--version', action='version',
1771+ # XXX barry 2010-08-23: LP: #622720
1772+ version=mod(_('Computer Janitor %(version)s'),
1773+ dict(version=__version__)))
1774+ subparser = self.parser.add_subparsers(title='Commands')
1775+ # The 'find' subcommand.
1776+ command = subparser.add_parser(
1777+ 'find', help=_('Find and display all cruft found on your system.'))
1778+ command.add_argument(
1779+ '-v', '--verbose', action='store_true',
1780+ help=_("""\
1781+ Display a detailed explanation for each piece of cruft found."""))
1782+ command.add_argument(
1783+ '-i', '--ignored', action='store_true',
1784+ help=_('Find and display only the ignored cruft.'))
1785+ command.add_argument(
1786+ '-r', '--removable', action='store_true',
1787+ help=_('Find and display only the removable cruft.'))
1788+ command.add_argument(
1789+ '-s', '--short', action='store_true',
1790+ help=_('Display only the package names. Do not use with -v.'))
1791+ command.set_defaults(func=self.runner.find)
1792+ # The 'ignore' subcommand.
1793+ command = subparser.add_parser(
1794+ 'ignore', help=_("""\
1795+ Ignore a piece of cruft so that it is not cleaned up."""))
1796+ command.add_argument(
1797+ 'cruft', nargs=1,
1798+ help=_('The name of the cruft to ignore.'))
1799+ command.set_defaults(func=self.runner.ignore)
1800+ # The 'unignore' subcommand.
1801+ command = subparser.add_parser(
1802+ 'unignore', help=_("""\
1803+ Unignore a piece of cruft so that it will be cleaned up."""))
1804+ command.add_argument(
1805+ 'cruft', nargs=1,
1806+ help=_('The name of the cruft to unignore.'))
1807+ command.set_defaults(func=self.runner.unignore)
1808+ # The 'clean' subcommand.
1809+ command = subparser.add_parser(
1810+ 'clean',
1811+ help=_('Remove the selected cruft from the system.'))
1812+ command.add_argument(
1813+ '-a', '--all', action='store_true',
1814+ help=_('Clean up all unignored cruft.'))
1815+ command.add_argument(
1816+ '-v', '--verbose', action='store_true',
1817+ help=_("Provide more details on what's being cleaned."))
1818+ command.add_argument(
1819+ 'cruft', nargs='?',
1820+ help=_("""\
1821+ The name of the cruft to clean up. Do not use if specifying
1822+ --all."""))
1823+ command.set_defaults(func=self.runner.clean)
1824+ # Parse the arguments and execute the subcommand.
1825+ self.arguments = self.parser.parse_args()
1826+
1827+
1828+class Runner:
1829+ """Implementations of subcommands."""
1830+
1831+ def __init__(self):
1832+ # Connect to the dbus service.
1833+ system_bus = dbus.SystemBus()
1834+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
1835+ self.janitord = dbus.Interface(
1836+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
1837+ # Connect to the signal the server will emit when cleaning up.
1838+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
1839+ # This will get backpatched by __main__. We need it to produce error
1840+ # messages from the argparser.
1841+ self.options = None
1842+ # The main loop for asynchronous calls and signal reception.
1843+ self.loop = gobject.MainLoop()
1844+
1845+ def _error(self, message):
1846+ """Generate a parser error and exit.
1847+
1848+ :param message: The error message.
1849+ :type message: string
1850+ """
1851+ self.options.parser.error(message)
1852+ # No return.
1853+
1854+ def find(self, arguments):
1855+ """Find and display all cruft.
1856+
1857+ :param arguments: Command line options.
1858+ """
1859+ # Cruft will be prefixed by 'removable' if it is not being ignored.
1860+ ignored = set(self.janitord.ignored())
1861+ cruft_names = set(self.janitord.find())
1862+ # Filter names based on option flags.
1863+ if arguments.ignored:
1864+ cruft = sorted(cruft_names & ignored)
1865+ elif arguments.removable:
1866+ cruft = sorted(cruft_names - ignored)
1867+ else:
1868+ cruft = sorted(cruft_names)
1869+ # The prefix will either be 'ignored' or 'removable' however this
1870+ # string will be translated, so calculate the prefix size in the
1871+ # native language, then add two columns of separator, followed by the
1872+ # cruft name.
1873+ prefixi = _('ignored')
1874+ prefixr = _('removable')
1875+ prefix_width = max(len(prefixi), len(prefixr))
1876+ # Long, short, shorter display.
1877+ if arguments.verbose and arguments.short:
1878+ self._error('Use either -s or -v but not both.')
1879+ if arguments.short:
1880+ # This is the shorter output.
1881+ for name in cruft:
1882+ print name
1883+ elif not arguments.verbose:
1884+ # This is the short output.
1885+ for name in cruft:
1886+ prefix = (prefixi if name in ignored else prefixr)
1887+ # 10 spaces for the prefix
1888+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
1889+ else:
1890+ # This is the verbose output. Start by getting the terminal's
1891+ # size, though all we care about is the width.
1892+ rows, columns = get_terminal_size()
1893+ width = ((80 if columns in (None, 0) else columns)
1894+ - prefix_width # space for prefix
1895+ - 2 # separator
1896+ - 1 # avoid last column, some terminals wrap
1897+ )
1898+ margin = ' ' * (prefix_width + 2)
1899+ for name in cruft:
1900+ prefix = (prefixi if name in ignored else prefixr)
1901+ # 10 spaces for the prefix
1902+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
1903+ # Print some details about the cruft.
1904+ cruft_type, disk_usage = self.janitord.get_details(name)
1905+ print '{0}{1} of type {2}'.format(
1906+ margin, format_size(disk_usage), cruft_type)
1907+ # Get the description, wrap it to the available columns, and
1908+ # display it line-by-line with the proper amount of leading
1909+ # spaces (prefix_width + 2).
1910+ description = self.janitord.get_description(name)
1911+ if len(description) == 0:
1912+ print
1913+ continue
1914+ paragraphs = description.split('\n\n')
1915+ for paragraph in paragraphs:
1916+ for line in textwrap.wrap(paragraph, width):
1917+ # 2010-02-09 barry: The original code forced the
1918+ # output to utf-8, claiming that was necessary to
1919+ # "write out stuff even when it's going to somewhere
1920+ # like a pipe [...] ignor[ing] the user's desired
1921+ # charset, which is bad, bad, bad...". This makes me
1922+ # pretty uncomfortable, so I'd like to see a bug
1923+ # report before I copy that from the previous
1924+ # implementation.
1925+ print '{0}{1}'.format(margin, line)
1926+ # Paragraph separator.
1927+ print
1928+
1929+ def ignore(self, arguments):
1930+ """Ignore some cruft.
1931+
1932+ :param arguments: Command line options.
1933+ """
1934+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1935+ self.janitord.ignore(arguments.cruft[0])
1936+ self.janitord.save()
1937+
1938+ def unignore(self, arguments):
1939+ """Unignore some cruft.
1940+
1941+ :param arguments: Command line options.
1942+ """
1943+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1944+ self.janitord.unignore(arguments.cruft[0])
1945+ self.janitord.save()
1946+
1947+ def _clean_reply(self):
1948+ """The 'clean' operation has completed successfully."""
1949+ self.loop.quit()
1950+ print 'done.'
1951+
1952+ def _clean_error(self, exception):
1953+ """The 'clean' operation has failed."""
1954+ self.loop.quit()
1955+ print 'dbus service error:', exception
1956+
1957+ def _clean_working(self, cruft):
1958+ """The 'clean' operation is in progress.
1959+
1960+ :param cruft: The cruft that is being cleaned up.
1961+ :type cruft: string
1962+ """
1963+ verbose = self.options.arguments.verbose
1964+ if cruft == '':
1965+ # We're done.
1966+ if not verbose:
1967+ sys.stdout.write(' ')
1968+ sys.stdout.flush()
1969+ else:
1970+ if verbose:
1971+ print 'Working on', cruft
1972+ else:
1973+ sys.stdout.write('.')
1974+ sys.stdout.flush()
1975+
1976+ def clean(self, arguments):
1977+ """Clean up the cruft.
1978+
1979+ :param arguments: Command line options.
1980+ """
1981+ if arguments.all:
1982+ # You can't specify both --all and a cruft name.
1983+ if arguments.cruft is not None:
1984+ self._error('Specify a cruft name or --all, but not both')
1985+ # No return.
1986+ all_cruft = set(self.janitord.find())
1987+ ignored = set(self.janitord.ignored())
1988+ cleanable_cruft = all_cruft - ignored
1989+ else:
1990+ if arguments.cruft is None:
1991+ self._error('You must specify a cruft name, or use --all')
1992+ # No return.
1993+ cleanable_cruft = (arguments.cruft,)
1994+ # Make the asynchronous call because this can take a long time. We'll
1995+ # get status updates periodically. Note however that even though this
1996+ # is asynchronous, dbus still expects a response within a certain
1997+ # amount of time. We have no idea how long it will take to clean up
1998+ # the cruft though, so just crank the timeout up to some insanely huge
1999+ # number (of seconds).
2000+ self.janitord.clean(cleanable_cruft,
2001+ reply_handler=self._clean_reply,
2002+ error_handler=self._clean_error,
2003+ # If it takes longer than an hour, we're screwed.
2004+ timeout=3600)
2005+ # Start the main loop. This will exit when the remote operation is
2006+ # complete.
2007+ if not arguments.verbose:
2008+ sys.stdout.write('processing')
2009+ sys.stdout.flush()
2010+ self.loop.run()
2011+
2012+
2013+def main():
2014+ # We'll need a main loop to receive status signals from the dbus service.
2015+ # Don't start the main loop yet though, since we only need it for 'clean'
2016+ # commands.
2017+ DBusGMainLoop(set_as_default=True)
2018+ runner = Runner()
2019+ options = Options(runner)
2020+ # Backpatch runner because of circular references.
2021+ runner.options = options
2022+ # Execute the subcommand.
2023+ options.arguments.func(options.arguments)
2024
2025=== added directory 'computerjanitorapp/gtk'
2026=== added file 'computerjanitorapp/gtk/__init__.py'
2027=== added file 'computerjanitorapp/gtk/dialogs.py'
2028--- computerjanitorapp/gtk/dialogs.py 1970-01-01 00:00:00 +0000
2029+++ computerjanitorapp/gtk/dialogs.py 2011-02-28 18:58:01 +0000
2030@@ -0,0 +1,113 @@
2031+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2032+#
2033+# This program is free software: you can redistribute it and/or modify
2034+# it under the terms of the GNU General Public License as published by
2035+# the Free Software Foundation, version 3 of the License.
2036+#
2037+# This program is distributed in the hope that it will be useful,
2038+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2039+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2040+# GNU General Public License for more details.
2041+#
2042+# You should have received a copy of the GNU General Public License
2043+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2044+
2045+"""Confirm some actions."""
2046+
2047+from __future__ import absolute_import, unicode_literals
2048+
2049+__metaclass__ = type
2050+__all__ = [
2051+ 'AreYouSure',
2052+ 'CleanupProblem',
2053+ ]
2054+
2055+
2056+import gtk
2057+
2058+from operator import mod
2059+
2060+from computerjanitorapp import setup_gettext
2061+from computerjanitorapp.gtk.store import ListStoreColumns
2062+
2063+_ = setup_gettext()
2064+NL = '\n'
2065+
2066+
2067+class DialogBase:
2068+ """Base class for dialogs."""
2069+
2070+ def __init__(self, ui):
2071+ self._ui = ui
2072+
2073+
2074+class AreYouSure(DialogBase):
2075+ """Confirmation of destructive actions."""
2076+
2077+ def verify(self):
2078+ """Confirm package removal."""
2079+ # Start by getting all the active, non-ignored cruft. These are the
2080+ # candidates for removal.
2081+ cleanable_cruft = self._ui.get_cleanable_cruft()
2082+ # It would be nice if we could produce better messages than these, but
2083+ # that would require a richer interface to the dbus service, and
2084+ # probably to the cruft plugin architecture underneath that.
2085+ #
2086+ message = _('Are you sure you want to clean your system?')
2087+ dialog = gtk.MessageDialog(
2088+ parent=self._ui.widgets['window'],
2089+ type=gtk.MESSAGE_WARNING,
2090+ buttons=gtk.BUTTONS_NONE,
2091+ message_format=message)
2092+ dialog.set_title(_('Clean up'))
2093+ package_cruft_count = sum(1 for cruft_name, is_pkg in cleanable_cruft
2094+ if is_pkg)
2095+ other_cruft_count = len(cleanable_cruft) - package_cruft_count
2096+ messages = []
2097+ ok_button = None
2098+ # XXX vv barry 2010-08-23: LP: #622720
2099+ if package_cruft_count > 0:
2100+ messages = [
2101+ _('<b>Software packages to remove: %(packages)s</b>.'),
2102+ _('\nRemoving packages that are still in use can '
2103+ 'cause errors.'),
2104+ ]
2105+ ok_button = _('Remove packages')
2106+ if other_cruft_count > 0:
2107+ messages.insert(1, _('Non-package items to remove: %(others)s.'))
2108+ ok_button = _('Clean up')
2109+ message = mod(NL.join(messages),
2110+ dict(packages=package_cruft_count,
2111+ others=other_cruft_count))
2112+ # XXX ^^ barry 2010-08-23: LP: #622720
2113+ dialog.format_secondary_markup(message)
2114+ dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE)
2115+ dialog.add_button(ok_button, gtk.RESPONSE_YES)
2116+ # Show the dialog and get the user's response.
2117+ dialog.show_all()
2118+ response = dialog.run()
2119+ dialog.hide()
2120+ return response == gtk.RESPONSE_YES
2121+
2122+
2123+class CleanupProblem(DialogBase):
2124+ """Some problem occurred during cleanup.
2125+
2126+ This is almost definitely caused by another package manager running at the
2127+ same time.
2128+ """
2129+
2130+ def run(self):
2131+ """Show the dialog and wait for confirmation."""
2132+ message = _('System clean up could not complete. '
2133+ 'Be sure no other package manager such as Synaptic or '
2134+ 'Update Manager is running.')
2135+ dialog = gtk.MessageDialog(
2136+ parent=self._ui.widgets['window'],
2137+ type=gtk.MESSAGE_ERROR,
2138+ buttons=gtk.BUTTONS_OK,
2139+ message_format=message)
2140+ dialog.set_title(_('System clean up failure'))
2141+ dialog.show_all()
2142+ dialog.run()
2143+ dialog.hide()
2144
2145=== added file 'computerjanitorapp/gtk/main.py'
2146--- computerjanitorapp/gtk/main.py 1970-01-01 00:00:00 +0000
2147+++ computerjanitorapp/gtk/main.py 2011-02-28 18:58:01 +0000
2148@@ -0,0 +1,32 @@
2149+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2150+#
2151+# This program is free software: you can redistribute it and/or modify
2152+# it under the terms of the GNU General Public License as published by
2153+# the Free Software Foundation, version 3 of the License.
2154+#
2155+# This program is distributed in the hope that it will be useful,
2156+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2157+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2158+# GNU General Public License for more details.
2159+#
2160+# You should have received a copy of the GNU General Public License
2161+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2162+
2163+"""Command line user interface."""
2164+
2165+from __future__ import absolute_import, unicode_literals
2166+
2167+__metaclass__ = type
2168+__all__ = [
2169+ 'main',
2170+ ]
2171+
2172+
2173+from computerjanitorapp.gtk.ui import UserInterface
2174+from dbus.mainloop.glib import DBusGMainLoop
2175+
2176+
2177+def main():
2178+ DBusGMainLoop(set_as_default=True)
2179+ ui = UserInterface()
2180+ ui.run()
2181
2182=== added file 'computerjanitorapp/gtk/store.py'
2183--- computerjanitorapp/gtk/store.py 1970-01-01 00:00:00 +0000
2184+++ computerjanitorapp/gtk/store.py 2011-02-28 18:58:01 +0000
2185@@ -0,0 +1,161 @@
2186+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2187+#
2188+# This program is free software: you can redistribute it and/or modify
2189+# it under the terms of the GNU General Public License as published by
2190+# the Free Software Foundation, version 3 of the License.
2191+#
2192+# This program is distributed in the hope that it will be useful,
2193+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2194+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2195+# GNU General Public License for more details.
2196+#
2197+# You should have received a copy of the GNU General Public License
2198+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2199+
2200+"""Gtk ListStore model for backing the TreeViews of cruft."""
2201+
2202+from __future__ import absolute_import, unicode_literals
2203+
2204+__metaclass__ = type
2205+__all__ = [
2206+ 'Store',
2207+ 'optimize',
2208+ 'unused',
2209+ ]
2210+
2211+
2212+import gtk
2213+import gobject
2214+
2215+
2216+# XXX 2010-03-04 barry: Use a munepy enum.
2217+class ListStoreColumns:
2218+ # The cruft name as known by the dbus service.
2219+ name = 0
2220+ # The short name of the cruft.
2221+ short_name = 1
2222+ # The text displayed in the cell.
2223+ text = 2
2224+ # Should the cruft be shown?
2225+ show = 3
2226+ # Is the cruft active locally (i.e. toggle button is set)?
2227+ active = 4
2228+ # Is the cruft being ignored in the dbus service?
2229+ server_ignored = 5
2230+ # Is the cruft's description expanded?
2231+ expanded = 6
2232+ # Is this cruft package cruft or some other kind of cruft?
2233+ is_package_cruft = 7
2234+
2235+
2236+class Store:
2237+ """The higher level wrapper around the gtk.ListStore."""
2238+
2239+ def __init__(self, janitord):
2240+ """Create the Store.
2241+
2242+ :param janitord: The `dbus.Interface` for talking to the Computer
2243+ Janitor dbus service.
2244+ """
2245+ self.janitord = janitord
2246+ # This ListStore is the backing data structure for the TreeViews in
2247+ # the main window. It holds information about the individual pieces
2248+ # of cruft. See ListStoreColumns for details.
2249+ #
2250+ # XXX 2010-03-04 barry: pygtk does not like 'unicode'.
2251+ self.store = gtk.ListStore(str, str, str, bool, bool, bool, bool, bool)
2252+
2253+ def find_cruft(self):
2254+ """Find cruft and populate the backing store.
2255+
2256+ This actually just asks the backend dbus service to find all the cruft
2257+ asynchronously. When the dbus service completes the task, it sends a
2258+ `find_finished` signal with the new list of cruft. Upon receipt of
2259+ that signal, the ListStore is updated.
2260+ """
2261+ self.janitord.connect_to_signal('find_finished', self.on_find_finished)
2262+ self.janitord.find_async()
2263+
2264+ def on_find_finished(self, all_cruft_names):
2265+ """dbus signal handler.
2266+
2267+ This actually updates the store with the new cruft results.
2268+ """
2269+ ignored_cruft = set(self.janitord.ignored())
2270+ all_cruft_names = set(all_cruft_names);
2271+ pkg_cruft_names = set(
2272+ cruft_name for cruft_name in all_cruft_names
2273+ if self.janitord.get_details(cruft_name)[0].lower()
2274+ == 'packagecruft')
2275+ self.store.clear()
2276+ # Cruft always starts out in the active, not-expanded state. Package
2277+ # cruft goes in the 'unused' column while non-package cruft goes in
2278+ # the 'optimize' column.
2279+ for cruft_name in all_cruft_names:
2280+ cruft_shortname = self.janitord.get_shortname(cruft_name)
2281+ self.store.append((
2282+ cruft_name, cruft_shortname,
2283+ # What is being displayed in the cell.
2284+ gobject.markup_escape_text(cruft_shortname),
2285+ # Show the cruft be shown?
2286+ cruft_name not in ignored_cruft,
2287+ # Is the cruft active locally?
2288+ cruft_name not in ignored_cruft,
2289+ # Is the cruft ignored in the dbus service?
2290+ cruft_name in ignored_cruft,
2291+ # Is the cruft's description expanded?
2292+ False,
2293+ # Is this package cruft?
2294+ cruft_name in pkg_cruft_names,
2295+ ))
2296+
2297+ # Forwards to the underlying store, for convenience. Really, these should
2298+ # use generic delegates, or clients should just use self.store.store.
2299+
2300+ def get_value(self, *args, **kws):
2301+ return self.store.get_value(*args, **kws)
2302+
2303+ def set_value(self, *args, **kws):
2304+ return self.store.set_value(*args, **kws)
2305+
2306+ def filter_new(self, *args, **kws):
2307+ return self.store.filter_new(*args, **kws)
2308+
2309+ def foreach(self, *args, **kws):
2310+ return self.store.foreach(*args, **kws)
2311+
2312+ def reorder(self, *args, **kws):
2313+ return self.store.reorder(*args, **kws)
2314+
2315+ def get_iter_first(self, *args, **kws):
2316+ return self.store.get_iter_first(*args, **kws)
2317+
2318+ def iter_next(self, *args, **kws):
2319+ return self.store.iter_next(*args, **kws)
2320+
2321+ def clear(self, *args, **kws):
2322+ return self.store.clear(*args, **kws)
2323+
2324+
2325+# Filter functions for use with TreeView column display.
2326+
2327+def unused(store, iter):
2328+ """True if the cruft is not being ignored and is package cruft.
2329+
2330+ :param store: The ListStore instance.
2331+ :param iter: The TreeIter instance.
2332+ """
2333+ show = store.get_value(iter, ListStoreColumns.show)
2334+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
2335+ return show and is_package_cruft
2336+
2337+
2338+def optimize(store, iter):
2339+ """True if the cruft is not package cruft.
2340+
2341+ :param store: The ListStore instance.
2342+ :param iter: The TreeIter instance.
2343+ """
2344+ show = store.get_value(iter, ListStoreColumns.show)
2345+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
2346+ return show and not is_package_cruft
2347
2348=== added file 'computerjanitorapp/gtk/ui.py'
2349--- computerjanitorapp/gtk/ui.py 1970-01-01 00:00:00 +0000
2350+++ computerjanitorapp/gtk/ui.py 2011-02-28 18:58:01 +0000
2351@@ -0,0 +1,537 @@
2352+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2353+#
2354+# This program is free software: you can redistribute it and/or modify
2355+# it under the terms of the GNU General Public License as published by
2356+# the Free Software Foundation, version 3 of the License.
2357+#
2358+# This program is distributed in the hope that it will be useful,
2359+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2360+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2361+# GNU General Public License for more details.
2362+#
2363+# You should have received a copy of the GNU General Public License
2364+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2365+
2366+"""Gtk user interface for computer janitor."""
2367+
2368+from __future__ import absolute_import, unicode_literals
2369+
2370+__metaclass__ = type
2371+__all__ = [
2372+ 'UserInterface',
2373+ ]
2374+
2375+
2376+import os
2377+import gtk
2378+import sys
2379+import dbus
2380+import glib
2381+import pango
2382+import gobject
2383+
2384+from operator import mod
2385+
2386+from computerjanitorapp import __version__, setup_gettext
2387+from computerjanitorapp.gtk.dialogs import AreYouSure, CleanupProblem
2388+from computerjanitorapp.gtk.store import (
2389+ ListStoreColumns, Store, optimize, unused)
2390+from computerjanitorapp.utilities import format_size
2391+from computerjanitord.service import DBUS_INTERFACE_NAME
2392+
2393+_ = setup_gettext()
2394+
2395+GLADE = '/usr/share/computer-janitor/ComputerJanitor.ui'
2396+ROOT_WIDTH = 900
2397+ROOT_HEIGHT = 500
2398+NL = '\n'
2399+
2400+# Keys are lower-cased cruft types, i.e. class name of cruft instances.
2401+ACTIONS = dict(
2402+ packagecruft=_('Package will be <b>removed</b>.'),
2403+ filecruft=_('Package will be <b>installed</b>.'),
2404+ missingpackagecruft=_('File will be <b>removed</b>.'),
2405+ )
2406+
2407+SENSITIVE_WIDGETS = (
2408+ 'do_button',
2409+ 'optimize_treeview',
2410+ 'quit_menuitem',
2411+ 'show_previously_ignored',
2412+ 'sort_by_name',
2413+ 'sort_by_size',
2414+ 'unused_treeview',
2415+ )
2416+
2417+
2418+class UserInterface:
2419+ """Implementation of the Gtk user interface."""
2420+
2421+ def __init__(self):
2422+ # Connect to the dbus service.
2423+ system_bus = dbus.SystemBus()
2424+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
2425+ self.janitord = dbus.Interface(
2426+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
2427+ # Create the model on which the TreeViews will be based.
2428+ self.store = Store(self.janitord)
2429+ self.popup_menus = {}
2430+ self.cruft_name_columns = set()
2431+ # Sort by name by default.
2432+ self._sort_key = self._by_name
2433+ # Work is happening asynchronously on the dbus service.
2434+ self.working = False
2435+ # Connect to the signal the server will emit when cleaning up.
2436+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
2437+
2438+ def run(self):
2439+ """Set up the widgets and run the main loop."""
2440+ builder = gtk.Builder()
2441+ builder.set_translation_domain('computerjanitor')
2442+ # Load the glade ui file, which can be overridden from the environment
2443+ # for testing purposes.
2444+ glade_file = os.environ.get('COMPUTER_JANITOR_GLADE', GLADE)
2445+ builder.add_from_file(glade_file)
2446+ # Bind widgets to callbacks. Initialize the Do... button to
2447+ # insensitive, but possibly toggle it back to sensitive if we find
2448+ # cruft.
2449+ self.find_and_bind_widgets(builder)
2450+ self.widgets['do_button'].set_sensitive(False)
2451+ self.janitord.connect_to_signal('find_finished', self._find_finished)
2452+ # Do the initial search for cruft and set up the TreeView model.
2453+ self.store.find_cruft()
2454+ self.sort_cruft()
2455+ # Now hook the TreeViews up to there view of the model.
2456+ self.create_column('unused_treeview', unused)
2457+ self.create_column('optimize_treeview', optimize)
2458+ # Set the dimensions of the root window.
2459+ root = self.widgets['window']
2460+ root.set_default_size(ROOT_WIDTH, ROOT_HEIGHT)
2461+ # Map the root window and go!
2462+ root.show()
2463+ gtk.main()
2464+
2465+ def find_and_bind_widgets(self, builder):
2466+ """Bind widgets and callbacks."""
2467+ # Start by extracting all the bindable widgets from the ui builder,
2468+ # keeping track of them as mapped to their name.
2469+ self.widgets = {}
2470+ for ui_object in builder.get_objects():
2471+ if issubclass(type(ui_object), gtk.Buildable):
2472+ widget_name = gtk.Buildable.get_name(ui_object)
2473+ self.widgets[widget_name] = ui_object
2474+ # Search through the attributes of this instance looking for
2475+ # callbacks for this widget. We use the naming convention
2476+ # 'on_<widget>_<signal>' for such methods. Both the widget
2477+ # name and signal (or event) can contain underscores. Connect
2478+ # the widget to the callback.
2479+ prefix = 'on_{0}_'.format(widget_name)
2480+ for method_name in dir(self):
2481+ if method_name.startswith(prefix):
2482+ signal_name = method_name[len(prefix):]
2483+ ui_object.connect(
2484+ signal_name, getattr(self, method_name))
2485+
2486+ def create_column(self, widget_name, filter_func):
2487+ """Set up a column in the named TreeView."""
2488+ treeview = self.widgets[widget_name]
2489+ # XXX 2010-03-04 barry: This is a bit of an abuse because it's
2490+ # supposed to specify whether users are to read across the rows. As a
2491+ # side effect it renders the columns with alternating row colors, but
2492+ # that's not it's primary function.
2493+ treeview.set_rules_hint(True)
2494+ # Each TreeView contains two columns. The leftmost one is a toggle
2495+ # that when select tells c-j to act on that cruft. Deselecting the
2496+ # toggle ignores the package for next time.
2497+ toggle_cr = gtk.CellRendererToggle()
2498+ toggle_cr.connect('toggled', self._toggled, treeview)
2499+ toggle_cr.set_property('yalign', 0)
2500+ toggle_col = gtk.TreeViewColumn()
2501+ toggle_col.pack_start(toggle_cr)
2502+ toggle_col.add_attribute(toggle_cr, 'active', ListStoreColumns.active)
2503+ treeview.append_column(toggle_col)
2504+ # The rightmost column contains the details of the cruft. It will
2505+ # always contain the cruft name and can be expanded to display cruft
2506+ # details. Tell the column to get its toggle's active state from the
2507+ # model.
2508+ name_cr = gtk.CellRendererText()
2509+ name_cr.set_property('yalign', 0)
2510+ name_cr.set_property('wrap-mode', pango.WRAP_WORD)
2511+ name_col = gtk.TreeViewColumn()
2512+ name_col.pack_start(name_cr)
2513+ name_col.add_attribute(name_cr, 'markup', ListStoreColumns.text)
2514+ treeview.append_column(name_col)
2515+ self.cruft_name_columns.add(name_col)
2516+ # The individual crufts may or may not be visible in this TreeView.
2517+ # It's the filter function that controls this, so set that now.
2518+ filter_store = self.store.filter_new()
2519+ filter_store.set_visible_func(filter_func)
2520+ treeview.set_model(filter_store)
2521+ # Each TreeView has a popup menu for select or deselecting all visible
2522+ # cruft.
2523+ self.create_popup_menu_for_treeview(treeview)
2524+
2525+ def create_popup_menu_for_treeview(self, treeview):
2526+ """The tree views have a popup menu to select/deselect everything.
2527+
2528+ :param treeview: The `TreeView` to attach the menu to.
2529+ """
2530+ select_all = gtk.MenuItem(label='Select all')
2531+ select_all.connect('activate', self.popup_menu_select_all, treeview)
2532+ unselect_all = gtk.MenuItem(label='Unselect all')
2533+ unselect_all.connect('activate',
2534+ self.popup_menu_unselect_all, treeview)
2535+ menu = gtk.Menu()
2536+ menu.append(select_all)
2537+ menu.append(unselect_all)
2538+ menu.show_all()
2539+ self.popup_menus[treeview] = menu
2540+
2541+ def _by_name(self, cruft_name):
2542+ """Sort by cruft name."""
2543+ return cruft_name
2544+
2545+ def _by_size(self, cruft_name):
2546+ """Sort by cruft size, from largest to smallest."""
2547+ # Return negative size to sort from largest to smallest.
2548+ return -self.janitord.get_details(cruft_name)[1]
2549+
2550+ def sort_cruft(self):
2551+ """Sort the cruft displays, either by name or size."""
2552+ # The way reordering (not technically 'sorting') works in gtk is that
2553+ # you give a list of integer indexes to the the ListStore. These
2554+ # indexes are in sorted order, and refer to the pre-sort indexes of
2555+ # the items in the store. IOW, the ListStore knows that if the first
2556+ # integer in the list is 7, it will move the 7th item to the top.
2557+ #
2558+ # Start by getting the indexes and names of the currenly sorted cruft.
2559+ # We'll fill this list with 2-tuples of the format:
2560+ # (sort-key, current-index).
2561+ cruft_data = []
2562+ def get(model, path, iter, crufts):
2563+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2564+ crufts.append((self._sort_key(cruft_name), len(crufts)))
2565+ # Continue iterating.
2566+ return False
2567+ self.store.foreach(get, cruft_data)
2568+ cruft_data.sort()
2569+ cruft_indexes = [index for key, index in cruft_data]
2570+ # No need to do anything if there is no cruft.
2571+ if len(cruft_indexes) > 0:
2572+ self.store.reorder(cruft_indexes)
2573+
2574+ def get_cleanable_cruft(self):
2575+ """Return the list of cleanable cruft candidates.
2576+
2577+ :return: List of cleanable cruft.
2578+ :rtype: list of 2-tuples of (cruft_name, is_package_cruft)
2579+ """
2580+ cleanable_cruft = []
2581+ def collect(model, path, iter, crufts):
2582+ # Only clean up active cruft, i.e. those that are specifically
2583+ # checked as ready for cleaning.
2584+ cruft_active = model.get_value(iter, ListStoreColumns.active)
2585+ if cruft_active:
2586+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2587+ cruft_is_package_cruft = model.get_value(
2588+ iter, ListStoreColumns.is_package_cruft)
2589+ crufts.append((cruft_name, cruft_is_package_cruft))
2590+ # Continue iterating.
2591+ return False
2592+ self.store.foreach(collect, cleanable_cruft)
2593+ return cleanable_cruft
2594+
2595+ def toggle_long_description(self, treeview):
2596+ """Toggle the currently selected cruft's long description.
2597+
2598+ :param treeview: The TreeView
2599+ """
2600+ selection = treeview.get_selection()
2601+ filter_model, selected = selection.get_selected()
2602+ if not selected:
2603+ return
2604+ model = filter_model.get_model()
2605+ iter = filter_model.convert_iter_to_child_iter(selected)
2606+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2607+ expanded = model.get_value(iter, ListStoreColumns.expanded)
2608+ shortname = model.get_value(iter, ListStoreColumns.short_name)
2609+ if expanded:
2610+ # Collapse it.
2611+ value = gobject.markup_escape_text(shortname)
2612+ else:
2613+ cruft_type, size = self.janitord.get_details(cruft_name)
2614+ lines = [gobject.markup_escape_text(shortname)]
2615+ action = ACTIONS.get(cruft_type.lower())
2616+ if action is not None:
2617+ lines.append(action)
2618+ # XXX barry 2010-08-23: LP: #622720
2619+ lines.append(mod(_('Size: %(bytes)s'),
2620+ dict(bytes=format_size(size))))
2621+ lines.append('')
2622+ description = self.janitord.get_description(cruft_name)
2623+ lines.append(gobject.markup_escape_text(description))
2624+ value = NL.join(lines)
2625+ model.set_value(iter, ListStoreColumns.text, value)
2626+ model.set_value(iter, ListStoreColumns.expanded, not expanded)
2627+
2628+ def desensitize(self):
2629+ """Make certain ui elements insensitive during work."""
2630+ for widget in SENSITIVE_WIDGETS:
2631+ self.widgets[widget].set_sensitive(False)
2632+
2633+ def sensitize(self):
2634+ """Make certain ui elements sensitive after work."""
2635+ for widget in SENSITIVE_WIDGETS:
2636+ self.widgets[widget].set_sensitive(True)
2637+
2638+ def _find_finished(self, all_cruft_names):
2639+ """dbus signal handler."""
2640+ self.widgets['do_button'].set_sensitive(len(all_cruft_names) > 0)
2641+
2642+ # Popup menu support.
2643+
2644+ def popup_menu_foreach_set_state(self, treeview, enabled):
2645+ """Set the state of the cruft 'active' flag for all cruft.
2646+
2647+ :param treeview: The `TreeView` to set cruft state on.
2648+ :param enabled: The new state flag for all cruft. True means enabled.
2649+ :type enabled: bool
2650+ """
2651+ def set_state(model, path, iter, user_data):
2652+ # Set the state on an individual piece of cruft. Start by
2653+ # changing the state of the cruft on the dbus service.
2654+ child_iter = model.convert_iter_to_child_iter(iter)
2655+ cruft_name = self.store.get_value(
2656+ child_iter, ListStoreColumns.name)
2657+ if enabled:
2658+ self.janitord.unignore(cruft_name)
2659+ else:
2660+ self.janitord.ignore(cruft_name)
2661+ # Now set the active state in the model.
2662+ self.store.set_value(child_iter, ListStoreColumns.active, enabled)
2663+ treeview.get_model().foreach(set_state, None)
2664+ # Save the updated state on the dbus service.
2665+ self.janitord.save()
2666+
2667+ def popup_menu_select_all(self, menuitem, treeview):
2668+ self.popup_menu_foreach_set_state(treeview, True)
2669+
2670+ def popup_menu_unselect_all(self, menuitem, treeview):
2671+ self.popup_menu_foreach_set_state(treeview, False)
2672+
2673+ # Progress bar
2674+
2675+ def pulse(self):
2676+ """Progress bar callback, showing that something is happening."""
2677+ progress = self.widgets['progressbar_status']
2678+ if self.working:
2679+ progress.show()
2680+ progress.pulse()
2681+ return True
2682+ else:
2683+ # All done. Hide the progress bar, make the ui elements sensitive
2684+ # again, update the store, and kill the timer.
2685+ progress.hide()
2686+ self.store.clear()
2687+ self.store.find_cruft()
2688+ self.sensitize()
2689+ return False
2690+
2691+ def _clean_working(self, cruft):
2692+ """dbus signal handler; the 'clean' operation is in progress.
2693+
2694+ :param done: The cruft that is being cleaned up.
2695+ :type done: string
2696+ """
2697+ # Just mark the status here. The progress bar pulsar will handle
2698+ # doing the actual work.
2699+ self.working = (cruft != '')
2700+ if self.working:
2701+ self.widgets['progressbar_status'].set_text(
2702+ # XXX barry 2010-08-23: LP: #622720
2703+ mod(_('Processing %(cruft)s'), dict(cruft=cruft)))
2704+
2705+ # Callbacks
2706+
2707+ def _toggled(self, widget, path, treeview):
2708+ """Handle the toggle button in a TreeView cell.
2709+
2710+ :param widget: The CellRendererToggle
2711+ :param path: The cell's path.
2712+ :param treeview: The TreeView
2713+ """
2714+ # Find out which cruft's toggle was clicked.
2715+ model = treeview.get_model()
2716+ filter_iter = model.get_iter(path)
2717+ child_iter = model.convert_iter_to_child_iter(filter_iter)
2718+ cruft_name = self.store.get_value(child_iter, ListStoreColumns.name)
2719+ state = self.store.get_value(child_iter, ListStoreColumns.active)
2720+ # Toggle the current state.
2721+ new_state = not state
2722+ if new_state:
2723+ self.janitord.unignore(cruft_name)
2724+ else:
2725+ self.janitord.ignore(cruft_name)
2726+ self.store.set_value(child_iter, ListStoreColumns.active, new_state)
2727+ self.store.set_value(
2728+ child_iter, ListStoreColumns.server_ignored, not new_state)
2729+ # Save the new ignored state on the dbus service.
2730+ self.janitord.save()
2731+
2732+ # Signal and event handlers.
2733+
2734+ def on_quit_menuitem_activate(self, *args):
2735+ """Signal and event handlers for quitting.
2736+
2737+ Since we just want things to go away, we don't really care about the
2738+ arguments. Just tell the main loop to exit.
2739+ """
2740+ # Don't quit while we're working.
2741+ if self.working:
2742+ return True
2743+ gtk.main_quit()
2744+
2745+ on_window_delete_event = on_quit_menuitem_activate
2746+
2747+ def treeview_button_press_event(self, treeview, event):
2748+ """Handle mouse button press events on the TreeView ourselves.
2749+
2750+ We handle mouse button presses ourselves so that we can either
2751+ toggle the long description (button 1, typically left) or
2752+ pop up a menu (button 3, typically right).
2753+ """
2754+ # Original comment: This is slightly tricky and probably a source of
2755+ # bugs. Oh well.
2756+ if event.button == 1:
2757+ # Left button event. Select the row being clicked on. If the
2758+ # click is on the cruft name, show or hide its long description.
2759+ # If the click the click is elsewhere do not handle it. This
2760+ # allows the toggle button event to be handled separately.
2761+ x = int(event.x)
2762+ y = int(event.y)
2763+ time = event.time
2764+ pathinfo = treeview.get_path_at_pos(x, y)
2765+ if pathinfo is None:
2766+ # The click was not in a cell, but we've handled it anyway.
2767+ return True
2768+ path, column, cell_x, cell_y = pathinfo
2769+ if column in self.cruft_name_columns:
2770+ treeview.set_cursor(path, column, False)
2771+ self.toggle_long_description(treeview)
2772+ return True
2773+ else:
2774+ # We are not handling this event so that the toggle button
2775+ # handling can occur.
2776+ return False
2777+ elif event.button == 3:
2778+ # Right button event. Pop up the select/deselect all menu.
2779+ treeview.grab_focus()
2780+ x = int(event.x)
2781+ y = int(event.y)
2782+ time = event.time
2783+ pathinfo = treeview.get_path_at_pos(x, y)
2784+ if pathinfo is not None:
2785+ path, column, cell_x, cell_y = pathinfo
2786+ treeview.set_cursor(path, column, False)
2787+ menu = self.popup_menus[treeview]
2788+ menu.popup(None, None, None, event.button, time)
2789+ return True
2790+ else:
2791+ # No other events are handled by us.
2792+ return False
2793+
2794+ # The actual event handler is totally generic. Alias it to names
2795+ # recognized by the automatic event binding scheme.
2796+ on_unused_treeview_button_press_event = treeview_button_press_event
2797+ on_optimize_treeview_button_press_event = treeview_button_press_event
2798+
2799+ def treeview_size_allocate(self, treeview, *args):
2800+ """Allocate space for the tree view and set wrap width.
2801+
2802+ :param treeview: The TreeView
2803+ :param args: Additional ignored positional arguments
2804+ """
2805+ # Get the rightmost of the two columns in the TreeView, i.e. the one
2806+ # containing the text.
2807+ column = treeview.get_column(1)
2808+ name_cr = column.get_cell_renderers()[0]
2809+ # Wrap to the entire width of the column.
2810+ width = column.get_width()
2811+ name_cr.set_property('wrap-width', width)
2812+
2813+ on_unused_treeview_size_allocate = treeview_size_allocate
2814+ on_optimize_treeview_size_allocate = treeview_size_allocate
2815+
2816+ def on_sort_by_name_toggled(self, menuitem):
2817+ """Reorder the crufts to be sorted by name or size."""
2818+ if menuitem.get_active():
2819+ self._sort_key = self._by_name
2820+ else:
2821+ self._sort_key = self._by_size
2822+ self.sort_cruft()
2823+
2824+ def on_about_menuitem_activate(self, *args):
2825+ dialog = self.widgets['about_dialog']
2826+ dialog.set_name(_('Computer Janitor'))
2827+ dialog.set_version(__version__)
2828+ dialog.show()
2829+ dialog.run()
2830+ dialog.hide()
2831+
2832+ def on_show_previously_ignored_toggled(self, menuitem):
2833+ """Show all cruft, even those being ignored.
2834+
2835+ Normally, we only show cruft that wasn't explicitly ignored. By
2836+ toggling this menu item, the janitor can also display cruft that is
2837+ marked as ignored on the dbus service.
2838+ """
2839+ show_ignored_cruft = menuitem.get_active()
2840+ iter = self.store.get_iter_first()
2841+ while iter:
2842+ cruft_name = self.store.get_value(iter, ListStoreColumns.name)
2843+ server_ignored = self.store.get_value(
2844+ iter, ListStoreColumns.server_ignored)
2845+ show = (show_ignored_cruft or not server_ignored)
2846+ self.store.set_value(iter, ListStoreColumns.show, show)
2847+ iter = self.store.iter_next(iter)
2848+
2849+ def on_do_button_clicked(self, *args):
2850+ """JFDI, well almost."""
2851+ self.count = 0
2852+ response = AreYouSure(self).verify()
2853+ if not response:
2854+ return
2855+ # This can take a long time. Make an asynchronous call to the dbus
2856+ # service and arrange for it to occasionally provide us with status.
2857+ # This isn't great ui, but OTOH, the package cruft cleaners themselves
2858+ # don't provide much granularity, so there's little we can do anyway
2859+ # without a major rewrite of the plugin architecture.
2860+ self.working = True
2861+ glib.timeout_add(150, self.pulse)
2862+ # Make various ui elements insensitive.
2863+ self.desensitize()
2864+ cleanable = [cruft for cruft, ispkg in self.get_cleanable_cruft()]
2865+ def error(exception):
2866+ # XXX 2010-05-19 barry: This will (almost?) always be caused by
2867+ # some other package manager already running, so if we get that
2868+ # exception, display a (hopefully) useful dialog. I'm not sure
2869+ # it's very helpful to display the low-level apt exception in a
2870+ # dialog. `exception` will always be a DBusException; we won't
2871+ # get the more useful PackageCleanupError.
2872+ print >> sys.stderr, exception
2873+ CleanupProblem(self).run()
2874+ self.working = False
2875+ def reply():
2876+ pass
2877+ # Make the asynchronous call because this can take a long time. We'll
2878+ # get status updates periodically. Note however that even though this
2879+ # is asynchronous, dbus still expects a response within a certain
2880+ # amount of time. We have no idea how long it will take to clean up
2881+ # the cruft though, so just crank the timeout up to some insanely huge
2882+ # number (of seconds).
2883+ self.widgets['progressbar_status'].set_text('Authenticating...')
2884+ self.janitord.clean(cleanable,
2885+ reply_handler=reply,
2886+ error_handler=error,
2887+ # If it takes longer than an hour, we're screwed.
2888+ timeout=3600)
2889
2890=== added file 'computerjanitorapp/terminalsize.py'
2891--- computerjanitorapp/terminalsize.py 1970-01-01 00:00:00 +0000
2892+++ computerjanitorapp/terminalsize.py 2011-02-28 18:58:01 +0000
2893@@ -0,0 +1,64 @@
2894+# terminalsize.py - find size of terminal
2895+# Copyright (C) 2008 Canonical, Ltd.
2896+#
2897+# This program is free software: you can redistribute it and/or modify
2898+# it under the terms of the GNU General Public License as published by
2899+# the Free Software Foundation, version 3 of the License.
2900+#
2901+# This program is distributed in the hope that it will be useful,
2902+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2903+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2904+# GNU General Public License for more details.
2905+#
2906+# You should have received a copy of the GNU General Public License
2907+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2908+
2909+# Inspired by code by Chuck Blake, at
2910+# http://pdos.csail.mit.edu/~cblake/cls/cls.py, but rewritten in an
2911+# attempt to clarify things. This code is slightly tricky, so I thought
2912+# the extra clarity would be worth it.
2913+
2914+import fcntl
2915+import os
2916+import struct
2917+import termios
2918+
2919+
2920+def get_terminal_size(fd=1):
2921+ """Return size of terminal attached to the standard output.
2922+
2923+ Use ioctl(2) to query a terminal for its size, given a file descriptor
2924+ attached to the terminal.
2925+
2926+ :param fd: Use the given file descriptor.
2927+ :type fd: int
2928+ :return: The columns and rows representing the size of the terminal. If
2929+ this cannot be determined, None is returned for both values.
2930+ :rtype: 2-tuple
2931+ """
2932+
2933+ try:
2934+ # Do the ioctl call. termios.TIOCGWINSZ is the code to query terminal
2935+ # size (see tty_ioctl(4), at least on Linux). We need to give it a
2936+ # string of suitable size to use as the input buffer for ioctl.
2937+ # ioctl() modifies the buffer and returns the modified buffer as its
2938+ # return value.
2939+ #
2940+ # The manual page specifies a struct winsize to be used, which
2941+ # consists of four unsigned shorts. We use struct.calcsize() to
2942+ # compute the size of that.
2943+ #
2944+ # Note that Blake's original code assumes only the first two shorts in
2945+ # the struct are used, and that two shorts fit into four bytes, which
2946+ # is probably true for all the relevant platforms, but is cramped
2947+ # enough that it makes me feel icky. Thus, I assume less. This will
2948+ # still break if the contents of the struct change, but since that
2949+ # would change the system call API, that's unlikely.
2950+ buflen = struct.calcsize('hhhh')
2951+ buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * buflen)
2952+ # ioctl returns a binary buffer that represents the struct
2953+ # at the C level. We unpack it with struct.unpack.
2954+ return tuple(struct.unpack('hhhh', buf)[:2])
2955+ except Exception:
2956+ # If anything went wrong, we give up and claim we don't know.
2957+ return None, None
2958
2959=== added directory 'computerjanitorapp/tests'
2960=== added file 'computerjanitorapp/tests/__init__.py'
2961=== added file 'computerjanitorapp/tests/test_all.py'
2962--- computerjanitorapp/tests/test_all.py 1970-01-01 00:00:00 +0000
2963+++ computerjanitorapp/tests/test_all.py 2011-02-28 18:58:01 +0000
2964@@ -0,0 +1,29 @@
2965+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2966+#
2967+# This program is free software: you can redistribute it and/or modify
2968+# it under the terms of the GNU General Public License as published by
2969+# the Free Software Foundation, version 3 of the License.
2970+#
2971+# This program is distributed in the hope that it will be useful,
2972+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2973+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2974+# GNU General Public License for more details.
2975+#
2976+# You should have received a copy of the GNU General Public License
2977+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2978+
2979+"""Test suite for Computer Janitor."""
2980+
2981+import unittest
2982+
2983+from computerjanitorapp.tests import test_terminalsize
2984+from computerjanitorapp.tests import test_utilities
2985+from computerjanitord.tests.test_all import test_suite as cjd_suite
2986+
2987+
2988+def test_suite():
2989+ suite = unittest.TestSuite()
2990+ suite.addTests(test_terminalsize.test_suite())
2991+ suite.addTests(test_utilities.test_suite())
2992+ suite.addTests(cjd_suite())
2993+ return suite
2994
2995=== added file 'computerjanitorapp/tests/test_terminalsize.py'
2996--- computerjanitorapp/tests/test_terminalsize.py 1970-01-01 00:00:00 +0000
2997+++ computerjanitorapp/tests/test_terminalsize.py 2011-02-28 18:58:01 +0000
2998@@ -0,0 +1,54 @@
2999+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3000+#
3001+# This program is free software: you can redistribute it and/or modify
3002+# it under the terms of the GNU General Public License as published by
3003+# the Free Software Foundation, version 3 of the License.
3004+#
3005+# This program is distributed in the hope that it will be useful,
3006+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3007+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3008+# GNU General Public License for more details.
3009+#
3010+# You should have received a copy of the GNU General Public License
3011+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3012+
3013+"""Test calculation of terminal sizes."""
3014+
3015+from __future__ import absolute_import, unicode_literals
3016+
3017+__metaclass__ = type
3018+__all__ = [
3019+ 'test_suite',
3020+ ]
3021+
3022+
3023+import os
3024+import unittest
3025+
3026+from computerjanitorapp import terminalsize
3027+
3028+
3029+class TestTerminalSize(unittest.TestCase):
3030+ """Test calculation of terminal sizes."""
3031+
3032+ def test_returns_unknown_when_querying_dev_null(self):
3033+ fd = os.open("/dev/null", os.O_RDONLY)
3034+ rows, cols = terminalsize.get_terminal_size(fd)
3035+ os.close(fd)
3036+ self.assertEqual(rows, None)
3037+ self.assertEqual(cols, None)
3038+
3039+ def test_returns_two_integers_when_stdout_is_a_terminal(self):
3040+ # We only run this check if stdout is a terminal.
3041+ # Unfortunately, there is no sensible way of checking the values.
3042+ # But that's OK, they're lumberjacks.
3043+ if os.isatty(1):
3044+ rows, cols = terminalsize.get_terminal_size(1)
3045+ self.assertEqual(type(rows), int)
3046+ self.assertEqual(type(cols), int)
3047+
3048+
3049+def test_suite():
3050+ suite = unittest.TestSuite()
3051+ suite.addTests(unittest.makeSuite(TestTerminalSize))
3052+ return suite
3053
3054=== added file 'computerjanitorapp/tests/test_utilities.py'
3055--- computerjanitorapp/tests/test_utilities.py 1970-01-01 00:00:00 +0000
3056+++ computerjanitorapp/tests/test_utilities.py 2011-02-28 18:58:01 +0000
3057@@ -0,0 +1,71 @@
3058+# Copyright (C) 2010 Canonical, Ltd.
3059+#
3060+# This program is free software: you can redistribute it and/or modify
3061+# it under the terms of the GNU General Public License as published by
3062+# the Free Software Foundation, version 3 of the License.
3063+#
3064+# This program is distributed in the hope that it will be useful,
3065+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3066+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3067+# GNU General Public License for more details.
3068+#
3069+# You should have received a copy of the GNU General Public License
3070+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3071+
3072+"""Test common utilities."""
3073+
3074+from __future__ import absolute_import, unicode_literals
3075+
3076+__metaclass__ = type
3077+__all__ = [
3078+ 'test_suite',
3079+ ]
3080+
3081+
3082+import os
3083+import unittest
3084+
3085+from computerjanitorapp.utilities import format_size
3086+
3087+
3088+class TestUtilities(unittest.TestCase):
3089+ """Test common utilities."""
3090+
3091+ def test_format_negative(self):
3092+ self.assertRaises(AssertionError, format_size, -1)
3093+
3094+ def test_format_zero(self):
3095+ self.assertEqual(format_size(0), '0B')
3096+
3097+ def test_format_small(self):
3098+ self.assertEqual(format_size(500), '500B')
3099+
3100+ def test_format_1k(self):
3101+ self.assertEqual(format_size(1000), '1kB')
3102+
3103+ def test_format_smallish(self):
3104+ self.assertEqual(format_size(500000), '500kB')
3105+
3106+ def test_format_1M(self):
3107+ self.assertEqual(format_size(1000000), '1MB')
3108+
3109+ def test_format_mediumish(self):
3110+ self.assertEqual(format_size(500000000), '500MB')
3111+
3112+ def test_format_1G(self):
3113+ self.assertEqual(format_size(1000000000), '1GB')
3114+
3115+ def test_format_bigish(self):
3116+ self.assertEqual(format_size(500000000000), '500GB')
3117+
3118+ def test_format_1T(self):
3119+ self.assertEqual(format_size(1000000000000), '1TB')
3120+
3121+ def test_format_hugish(self):
3122+ self.assertEqual(format_size(500000000000000), '>1TB')
3123+
3124+
3125+def test_suite():
3126+ suite = unittest.TestSuite()
3127+ suite.addTests(unittest.makeSuite(TestUtilities))
3128+ return suite
3129
3130=== added file 'computerjanitorapp/utilities.py'
3131--- computerjanitorapp/utilities.py 1970-01-01 00:00:00 +0000
3132+++ computerjanitorapp/utilities.py 2011-02-28 18:58:01 +0000
3133@@ -0,0 +1,51 @@
3134+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3135+#
3136+# This program is free software: you can redistribute it and/or modify
3137+# it under the terms of the GNU General Public License as published by
3138+# the Free Software Foundation, version 3 of the License.
3139+#
3140+# This program is distributed in the hope that it will be useful,
3141+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3142+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3143+# GNU General Public License for more details.
3144+#
3145+# You should have received a copy of the GNU General Public License
3146+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3147+
3148+"""Common utilities."""
3149+
3150+from __future__ import absolute_import, unicode_literals
3151+
3152+__metaclass__ = type
3153+__all__ = [
3154+ 'format_size',
3155+ ]
3156+
3157+
3158+from math import log10
3159+
3160+
3161+TABLE = (
3162+ 'B',
3163+ 'kB',
3164+ 'MB',
3165+ 'GB',
3166+ 'TB',
3167+ )
3168+
3169+
3170+def format_size(bytes):
3171+ """Format size in bytes.
3172+
3173+ :param bytes: Integer size in bytes.
3174+ :type bytes: integer
3175+ :return: Formatted size
3176+ :rtype: string
3177+ """
3178+ assert bytes >= 0, 'Cannot have negative sizes'
3179+ if bytes == 0:
3180+ return '0B'
3181+ if bytes > 10**12:
3182+ return '>1TB'
3183+ key = divmod(int(log10(bytes)), 3)[0]
3184+ return '{0}{1}'.format(bytes // 10**(key * 3), TABLE[key])
3185
3186=== added directory 'computerjanitord'
3187=== renamed directory 'computerjanitord' => 'computerjanitord.moved'
3188=== added file 'computerjanitord/__init__.py'
3189=== added file 'computerjanitord/application.py'
3190--- computerjanitord/application.py 1970-01-01 00:00:00 +0000
3191+++ computerjanitord/application.py 2011-02-28 18:58:01 +0000
3192@@ -0,0 +1,92 @@
3193+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3194+#
3195+# This program is free software: you can redistribute it and/or modify
3196+# it under the terms of the GNU General Public License as published by
3197+# the Free Software Foundation, version 3 of the License.
3198+#
3199+# This program is distributed in the hope that it will be useful,
3200+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3201+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3202+# GNU General Public License for more details.
3203+#
3204+# You should have received a copy of the GNU General Public License
3205+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3206+
3207+"""Application interface for use by plugins.
3208+
3209+This primarily makes certain apt functionality available to the plugin.
3210+"""
3211+
3212+from __future__ import absolute_import, unicode_literals
3213+
3214+__metaclass__ = type
3215+__all__ = [
3216+ 'Application',
3217+ 'SourcesListError',
3218+ ]
3219+
3220+
3221+import apt
3222+
3223+import computerjanitor
3224+import computerjanitorapp
3225+
3226+from computerjanitord.errors import (
3227+ MissingLandmarkError, NonDownloadableError)
3228+
3229+
3230+_ = computerjanitorapp.setup_gettext()
3231+
3232+SYNTAPTIC_PREFERENCES_FILE = '/var/lib/synaptic/preferences'
3233+# There really isn't anything special about these packages. These really just
3234+# represent landmarks in the package namespace that we look for to try to
3235+# judge the sanity of the apt cache. If these are missing, things are really
3236+# messed up and we can't actually figure out how to continue.
3237+LANDMARK_PACKAGES = [
3238+ 'dash',
3239+ 'gzip',
3240+ ]
3241+
3242+
3243+class Application:
3244+ """Interface for plugins requesting apt actions."""
3245+
3246+ def __init__(self, apt_cache=None):
3247+ """Create the application interface.
3248+
3249+ :param apt_cache: Alternative apt cache for testing purposes. When
3250+ `None` use the default apt cache.
3251+ """
3252+ if apt_cache is None:
3253+ # Use the real apt cache.
3254+ self.apt_cache = apt.Cache()
3255+ else:
3256+ self.apt_cache = apt_cache
3257+ self.refresh_apt_cache()
3258+
3259+ def refresh_apt_cache(self):
3260+ """Refresh the apt cache.
3261+
3262+ This API is used by plugins.
3263+ """
3264+ self.apt_cache.open()
3265+ # For historical purposes, Synaptic has a different way of pinning
3266+ # packages than apt, so we have to load its preferences file in order
3267+ # to know what it's pinning.
3268+ self.apt_cache._depcache.ReadPinFile(SYNTAPTIC_PREFERENCES_FILE)
3269+
3270+ def verify_apt_cache(self):
3271+ """Verify that essential packages are available in the apt cache.
3272+
3273+ This API is used by plugins.
3274+
3275+ :raises MissingLandmarkError: When an essential package is not
3276+ available.
3277+ :raises NonDownloadableError: When an essential package cannot be
3278+ downloaded.
3279+ """
3280+ for name in LANDMARK_PACKAGES:
3281+ if name not in self.apt_cache:
3282+ raise MissingLandmarkError(name)
3283+ if not any(v.downloadable for v in self.apt_cache[name].versions):
3284+ raise NonDownloadableError(name)
3285
3286=== added file 'computerjanitord/authenticator.py'
3287--- computerjanitord/authenticator.py 1970-01-01 00:00:00 +0000
3288+++ computerjanitord/authenticator.py 2011-02-28 18:58:01 +0000
3289@@ -0,0 +1,85 @@
3290+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3291+#
3292+# This program is free software: you can redistribute it and/or modify
3293+# it under the terms of the GNU General Public License as published by
3294+# the Free Software Foundation, version 3 of the License.
3295+#
3296+# This program is distributed in the hope that it will be useful,
3297+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3298+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3299+# GNU General Public License for more details.
3300+#
3301+# You should have received a copy of the GNU General Public License
3302+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3303+
3304+"""Authentication for Computer Janitor backend services."""
3305+
3306+from __future__ import absolute_import, unicode_literals
3307+
3308+__metaclass__ = type
3309+__all__ = [
3310+ 'Authenticator',
3311+ ]
3312+
3313+
3314+import dbus
3315+
3316+
3317+PK_AUTHORITY_BUS_NAME = 'org.freedesktop.PolicyKit1'
3318+PK_AUTHORITY_OBJECT_PATH = '/org/freedesktop/PolicyKit1/Authority'
3319+PK_AUTHORITY_INTERFACE = 'org.freedesktop.PolicyKit1.Authority'
3320+# From the PolicyKit API.
3321+# http://hal.freedesktop.org/docs/polkit/
3322+# eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html
3323+AllowUserInteraction = 0x00000001
3324+
3325+
3326+class Authenticator:
3327+ """PolicyKit authenticator."""
3328+
3329+ def authenticate(self, sender, connection, privilege):
3330+ """Authenticate with PolicyKit.
3331+
3332+ :param sender: The initiator of the action.
3333+ :param connection: The dbus connection that initiated the action.
3334+ :param privilege: The privilege being requested.
3335+ :return: Whether the subject is authorized or not.
3336+ :rtype: bool
3337+ """
3338+ policykit = self._get_policykit_proxy()
3339+ sender_pid = self._get_sender_pid(connection, sender)
3340+ # This is the CheckAuthorization() 'subject' structure.
3341+ subject = (
3342+ 'unix-process', {
3343+ 'pid': sender_pid,
3344+ 'start-time': 0,
3345+ })
3346+ # No details or cancellation_id needed.
3347+ details = {'': ''}
3348+ cancellation_id = ''
3349+ flags = AllowUserInteraction
3350+ # CheckAuthorization returns an AuthorizationResult structure, modeled
3351+ # as a 3-tuple. The only thing we care about though is the boolean
3352+ # describing whether we got authorized or not.
3353+ is_authorized, is_challenge, details = policykit.CheckAuthorization(
3354+ subject, privilege, details, flags, cancellation_id)
3355+ return is_authorized
3356+
3357+ def _get_policykit_proxy(self):
3358+ """Contact the system bus to get a PolicyKit proxy."""
3359+ system_bus = dbus.SystemBus()
3360+ pk_proxy = system_bus.get_object(
3361+ PK_AUTHORITY_BUS_NAME, PK_AUTHORITY_OBJECT_PATH)
3362+ return dbus.Interface(pk_proxy, PK_AUTHORITY_INTERFACE)
3363+
3364+ def _get_sender_pid(self, connection, sender):
3365+ """Contact the system bus to get the sender connection PID."""
3366+ # Since we're going to authorize a Unix process, we need to get the
3367+ # sender's process id. This is available on the dbus. The
3368+ # CheckAuthorization() method also requires us to have a start-time,
3369+ # but it's not clear what the semantics are for that, so we'll just
3370+ # put a zero there.
3371+ db_proxy = connection.get_object(
3372+ dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH, introspect=False)
3373+ info = dbus.Interface(db_proxy, dbus.BUS_DAEMON_IFACE)
3374+ return info.GetConnectionUnixProcessID(sender)
3375
3376=== added file 'computerjanitord/collector.py'
3377--- computerjanitord/collector.py 1970-01-01 00:00:00 +0000
3378+++ computerjanitord/collector.py 2011-02-28 18:58:01 +0000
3379@@ -0,0 +1,167 @@
3380+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3381+#
3382+# This program is free software: you can redistribute it and/or modify
3383+# it under the terms of the GNU General Public License as published by
3384+# the Free Software Foundation, version 3 of the License.
3385+#
3386+# This program is distributed in the hope that it will be useful,
3387+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3388+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3389+# GNU General Public License for more details.
3390+#
3391+# You should have received a copy of the GNU General Public License
3392+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3393+
3394+"""A cruft collector."""
3395+
3396+from __future__ import absolute_import, unicode_literals
3397+
3398+__metaclass__ = type
3399+__all__ = [
3400+ 'Collector',
3401+ ]
3402+
3403+
3404+import os
3405+import time
3406+import logging
3407+
3408+from computerjanitor import PluginManager
3409+from computerjanitord.errors import DuplicateCruftError, PackageCleanupError
3410+from computerjanitord.whitelist import Whitelist
3411+
3412+
3413+log = logging.getLogger('computerjanitor')
3414+MISSING = object()
3415+DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
3416+
3417+# For testing purposes.
3418+SLEEPY_TIME = float(os.environ.get('COMPUTER_JANITOR_SLEEPY_TIME', '1.0'))
3419+
3420+
3421+class Collector:
3422+ """A cruft collector."""
3423+
3424+ def __init__(self, application, plugin_manager_class=None,
3425+ whitelist_dirs=None, service=None):
3426+ """Create a cruft collector.
3427+
3428+ :param application: The `Application` class.
3429+ :type application: This object is used by plugins to access the apt
3430+ database. It must have an attribute named `apt_cache` and a
3431+ method named `refresh_apt_cache()`.
3432+ :param plugin_manager_class: The plugin manager class. If None (the
3433+ default), then `computerjanitor.PluginManager` is used.
3434+ :type plugin_manager_class: callable accepting a single argument,
3435+ which is a sequence of plugin directories.
3436+ :param whitelist_dirs: Sequence of directories to search for
3437+ '.whitelist' files. Passed directly to
3438+ `computerjanitord.whitelist.Whitelist`.
3439+ :param service: The dbus service; when doing plugin post-cleanup, this
3440+ will be used to emit a progress signal.
3441+ """
3442+ self.application = application
3443+ self.service = service
3444+ self.whitelist = Whitelist(whitelist_dirs)
3445+ # Keep track of cruft and map between the cruft's name and its Cruft
3446+ # instance. We'll use the latter when cruft cleanup is requested
3447+ # through the dbus API.
3448+ self.cruft = None
3449+ self.cruft_by_name = None
3450+ # Set up the plugin manager.
3451+ plugin_path = os.environ.get('COMPUTER_JANITOR_PLUGINS',
3452+ DEFAULT_PLUGINS_DIRS)
3453+ plugin_dirs = plugin_path.split(':')
3454+ if plugin_manager_class is None:
3455+ plugin_manager_class = PluginManager
3456+ self.plugin_manager = plugin_manager_class(application, plugin_dirs)
3457+ self.load()
3458+
3459+ def load(self):
3460+ """Reload all cruft."""
3461+ self.cruft = []
3462+ self.cruft_by_name = {}
3463+ # Ask all the plugins to find their cruft, filtering out whitelisted
3464+ # cruft.
3465+ for plugin in self.plugin_manager.get_plugins():
3466+ for cruft in plugin.get_cruft():
3467+ if not self.whitelist.is_whitelisted(cruft):
3468+ # Different plugins can give us duplicate cruft names,
3469+ # however the Cruft class better be the same, otherwise we
3470+ # won't actually know how to map the name back to a cruft
3471+ # instance for proper cleanup.
3472+ if cruft.get_name() in self.cruft_by_name:
3473+ my_cruft = self.cruft_by_name[cruft.get_name()]
3474+ if cruft.__class__ is my_cruft.__class__:
3475+ # We only need one instance of this cruft.
3476+ continue
3477+ else:
3478+ raise DuplicateCruftError(cruft.get_name())
3479+ #print ' ', cruft.get_name()
3480+ self.cruft.append(cruft)
3481+ self.cruft_by_name[cruft.get_name()] = cruft
3482+
3483+ def clean(self, names, dry_run=False):
3484+ """Clean up the named cruft.
3485+
3486+ :param names: The names of the cruft to clean up.
3487+ :type names: list of strings
3488+ :param dry_run: Flag indicating whether to do permanent changes.
3489+ :type dry_run: bool
3490+ """
3491+ for name in names:
3492+ # Ensure that all named cruft is known.
3493+ cruft = self.cruft_by_name.get(name, MISSING)
3494+ if cruft is MISSING:
3495+ log.error('No such cruft: {0}'.format(name))
3496+ raise NoSuchCruftError(name)
3497+ # Ensure that the cruft is not being ignored.
3498+ if cruft in self.service.state.ignore:
3499+ log.error('Skipping ignored cruft: {0}'.format(name))
3500+ continue
3501+ log.info('cleaning cruft{1}: {0}'.format(
3502+ cruft.get_name(), ('[X]' if dry_run else '[Y]')))
3503+ if not dry_run:
3504+ try:
3505+ cruft.cleanup()
3506+ except:
3507+ log.exception('cruft.cleanup(): {0}'.format(name))
3508+ raise
3509+ # Do plugin-specific post-cleanup.
3510+ for plugin in self.plugin_manager.get_plugins():
3511+ log.info('post-cleanup: {0}'.format(plugin))
3512+ if self.service is not None:
3513+ # Notify the client that we're not done yet.
3514+ #
3515+ # 2010-02-09 barry: this actually kind of sucks because the
3516+ # granularity is too coarse. Some plugins will post_cleanup()
3517+ # very quickly, others will take a long time. Unfortunately,
3518+ # the computerjanitor.Plugin API doesn't support a more
3519+ # granular feedback. Plugin.get_plugin(..., callback=foo)
3520+ # doesn't really cut it because that only gets called during
3521+ # get_plugins().
3522+ self.service.cleanup_status(plugin.__class__.__name__)
3523+ if dry_run:
3524+ # For testing purposes.
3525+ time.sleep(SLEEPY_TIME)
3526+ else:
3527+ try:
3528+ plugin.post_cleanup()
3529+ except SystemError as error:
3530+ # apt will raise a SystemError if some other package
3531+ # manager is already running. Turn this into a dbus
3532+ # derived exception so the client will be properly
3533+ # informed.
3534+ log.exception(
3535+ 'plugin.post_cleanup(): {0}'.format(plugin))
3536+ raise PackageCleanupError(str(error))
3537+ except:
3538+ log.exception(
3539+ 'plugin.post_cleanup(): {0}'.format(plugin))
3540+ raise
3541+ # Now we're done.
3542+ if self.service is not None:
3543+ self.service.cleanup_status('')
3544+ # Reload list of crufts.
3545+ self.application.refresh_apt_cache()
3546+ self.load()
3547
3548=== added directory 'computerjanitord/data'
3549=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.conf'
3550--- computerjanitord/data/com.ubuntu.ComputerJanitor.conf 1970-01-01 00:00:00 +0000
3551+++ computerjanitord/data/com.ubuntu.ComputerJanitor.conf 2011-02-28 18:58:01 +0000
3552@@ -0,0 +1,15 @@
3553+<!DOCTYPE busconfig PUBLIC
3554+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
3555+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
3556+<busconfig>
3557+ <policy user="root">
3558+ <allow own="com.ubuntu.ComputerJanitor"/>
3559+ </policy>
3560+
3561+ <policy context="default">
3562+ <allow send_interface="com.ubuntu.ComputerJanitor"/>
3563+ <allow receive_interface="com.ubuntu.ComputerJanitor"
3564+ receive_sender="com.ubuntu.ComputerJanitor"/>
3565+ </policy>
3566+
3567+</busconfig>
3568
3569=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.service'
3570--- computerjanitord/data/com.ubuntu.ComputerJanitor.service 1970-01-01 00:00:00 +0000
3571+++ computerjanitord/data/com.ubuntu.ComputerJanitor.service 2011-02-28 18:58:01 +0000
3572@@ -0,0 +1,4 @@
3573+[D-BUS Service]
3574+Name=com.ubuntu.ComputerJanitor
3575+Exec=/usr/share/computerjanitor/janitord
3576+User=root
3577
3578=== added file 'computerjanitord/data/com.ubuntu.computerjanitor.policy'
3579--- computerjanitord/data/com.ubuntu.computerjanitor.policy 1970-01-01 00:00:00 +0000
3580+++ computerjanitord/data/com.ubuntu.computerjanitor.policy 2011-02-28 18:58:01 +0000
3581@@ -0,0 +1,19 @@
3582+<?xml version="1.0" encoding="UTF-8"?>
3583+<!DOCTYPE policyconfig PUBLIC
3584+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
3585+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
3586+<policyconfig>
3587+
3588+ <vendor>ComputerJanitor</vendor>
3589+ <vendor_url>https://launchpad.net/computer-janitor</vendor_url>
3590+
3591+ <action id="com.ubuntu.computerjanitor.updatesystem">
3592+ <description>Clean up packages that are no longer necessary</description>
3593+ <message>Removing unused packages requires authentication</message>
3594+ <defaults>
3595+ <allow_inactive>no</allow_inactive>
3596+ <allow_active>auth_admin_keep</allow_active>
3597+ </defaults>
3598+ </action>
3599+
3600+</policyconfig>
3601
3602=== added file 'computerjanitord/errors.py'
3603--- computerjanitord/errors.py 1970-01-01 00:00:00 +0000
3604+++ computerjanitord/errors.py 2011-02-28 18:58:01 +0000
3605@@ -0,0 +1,95 @@
3606+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3607+#
3608+# This program is free software: you can redistribute it and/or modify
3609+# it under the terms of the GNU General Public License as published by
3610+# the Free Software Foundation, version 3 of the License.
3611+#
3612+# This program is distributed in the hope that it will be useful,
3613+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3614+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3615+# GNU General Public License for more details.
3616+#
3617+# You should have received a copy of the GNU General Public License
3618+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3619+
3620+"""Exceptions for the Computer Janitor daemon."""
3621+
3622+
3623+from __future__ import absolute_import, unicode_literals
3624+
3625+__metaclass__ = type
3626+__all__ = [
3627+ 'DuplicateCruftError',
3628+ 'LandmarkPackageError',
3629+ 'MissingLandmarkError',
3630+ 'NoSuchCruftError',
3631+ 'NonDownloadableError',
3632+ 'PermissionDeniedError',
3633+ ]
3634+
3635+
3636+import dbus
3637+import computerjanitor
3638+
3639+
3640+class DBusServiceBaseException(computerjanitor.Exception, dbus.DBusException):
3641+ """Base class exception for the Computer Janitor DBus service."""
3642+
3643+
3644+class PermissionDeniedError(DBusServiceBaseException):
3645+ """Permission denied by policy."""
3646+
3647+
3648+class CruftError(DBusServiceBaseException):
3649+ """Cruft exceptions passed back to dbus client."""
3650+
3651+ _errmsg = None
3652+
3653+ def __init__(self, cruft_name):
3654+ self.cruft_name = cruft_name
3655+
3656+ def __str__(self):
3657+ return self._errmsg.format(self)
3658+
3659+
3660+class DuplicateCruftError(CruftError):
3661+ """Duplicate cruft name with different cleanup."""
3662+
3663+ _errmsg = 'Duplicate cruft with different cleanup: {0.cruft_name}'
3664+
3665+
3666+class NoSuchCruftError(CruftError):
3667+ """There is no cruft by the given name."""
3668+
3669+ _errmsg = 'No such cruft: {0.cruft_name}'
3670+
3671+
3672+class LandmarkPackageError(DBusServiceBaseException):
3673+ """Base class for problems with the landmark packages."""
3674+
3675+ _errmsg = None
3676+
3677+ def __init__(self, package):
3678+ self.package = package
3679+
3680+ def __str__(self):
3681+ # gettext translation needs to be called at run time.
3682+ return self._errmsg.format(self)
3683+
3684+
3685+class MissingLandmarkError(LandmarkPackageError):
3686+ """A landmark package could not be found."""
3687+
3688+ _errmsg = 'Landmark package {0.package} is missing'
3689+
3690+
3691+class NonDownloadableError(LandmarkPackageError):
3692+ """A landmark package is not downloadable."""
3693+
3694+ _errmsg = 'Landmark package {0.package} is not downloadable'
3695+
3696+
3697+class PackageCleanupError(DBusServiceBaseException):
3698+ """Could not complete plugin post-cleanup."""
3699+
3700+ _errmsg = 'Post-cleanup exception'
3701
3702=== added file 'computerjanitord/main.py'
3703--- computerjanitord/main.py 1970-01-01 00:00:00 +0000
3704+++ computerjanitord/main.py 2011-02-28 18:58:01 +0000
3705@@ -0,0 +1,118 @@
3706+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3707+#
3708+# This program is free software: you can redistribute it and/or modify
3709+# it under the terms of the GNU General Public License as published by
3710+# the Free Software Foundation, version 3 of the License.
3711+#
3712+# This program is distributed in the hope that it will be useful,
3713+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3714+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3715+# GNU General Public License for more details.
3716+#
3717+# You should have received a copy of the GNU General Public License
3718+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3719+
3720+"""Main entry point for Computer Janitor dbus daemon."""
3721+
3722+from __future__ import absolute_import, unicode_literals
3723+
3724+__metaclass__ = type
3725+__all__ = [
3726+ 'main',
3727+ ]
3728+
3729+
3730+import os
3731+import gobject
3732+import logging
3733+import argparse
3734+import warnings
3735+import dbus.mainloop.glib
3736+import logging.handlers
3737+
3738+from computerjanitorapp import __version__, setup_gettext
3739+from computerjanitord.service import Service
3740+
3741+_ = setup_gettext()
3742+
3743+
3744+# 2010-02-09 barry: computerjanitor.package_cruft has a DeprecationWarning,
3745+# but that really needs to be fixed in that package, which in turn needs to be
3746+# ripped out of update-manager.
3747+warnings.filterwarnings('ignore', category=DeprecationWarning,
3748+ module='computerjanitor.package_cruft')
3749+
3750+
3751+# As per aptdaemon, when run as a dbus service, we must add the necessary
3752+# paths to $PATH.
3753+REQUIRED_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
3754+
3755+
3756+class Options:
3757+ """Command line options."""
3758+
3759+ def __init__(self):
3760+ self.parser = argparse.ArgumentParser(
3761+ description=_("""\
3762+ Computer janitor dbus daemon. You must run this daemon as root.
3763+ """))
3764+ self.parser.add_argument(
3765+ '--version', action='version',
3766+ version=_('Computer Janitor dbus daemon {version}'.format(
3767+ version=__version__)))
3768+ self.parser.add_argument(
3769+ '-n', '--dry-run', action='store_true',
3770+ help=_("""\
3771+ Only pretend to do anything permanent. This is useful for testing
3772+ and debugging."""))
3773+ self.parser.add_argument(
3774+ '-f', '--state-file', metavar='FILE',
3775+ help=_('Store ignored state in FILE instead of the default.'))
3776+ self.arguments = self.parser.parse_args()
3777+
3778+
3779+class ASCIIFormatter(logging.Formatter):
3780+ """Force the log messages to ASCII."""
3781+ def format(self, record):
3782+ message = logging.Formatter.format(self, record)
3783+ return message.encode('ascii', 'replace')
3784+
3785+
3786+def main():
3787+ """Main entry point."""
3788+ # Set up logging.
3789+ if os.environ.get('COMPUTER_JANITOR_DEBUG') is not None:
3790+ level = logging.DEBUG
3791+ else:
3792+ level = logging.INFO
3793+ # When running under dbus, $PATH will not be set. However it must be in
3794+ # order to find dpkg(1) and friends. Hard code it in the same way that
3795+ # aptdaemon does.
3796+ if os.environ.get('PATH') is None:
3797+ os.putenv('PATH', REQUIRED_PATH)
3798+ logging.basicConfig(level=level)
3799+ log = logging.getLogger('computerjanitor')
3800+ # SysLogHandler does not recognize unicode arguments.
3801+ syslog = logging.handlers.SysLogHandler(b'/dev/log')
3802+ # syslog will provide a timestamp.
3803+ formatter = ASCIIFormatter('computerjanitord:%(levelname)s: %(message)s')
3804+ syslog.setFormatter(formatter)
3805+ syslog.setLevel(level)
3806+ log.addHandler(syslog)
3807+ options = Options()
3808+ # Ensure that we're running as root. Do this here instead of ../janitord
3809+ # so that we can still run 'janitord --version' and 'janitord --help' as
3810+ # non-root.
3811+ if os.getuid() != 0:
3812+ options.parser.error('You must run this as root')
3813+ # No return.
3814+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
3815+ server = Service(options)
3816+ try:
3817+ gobject.MainLoop().run()
3818+ except KeyboardInterrupt:
3819+ pass
3820+
3821+
3822+if __name__ == '__main__':
3823+ main()
3824
3825=== added file 'computerjanitord/service.py'
3826--- computerjanitord/service.py 1970-01-01 00:00:00 +0000
3827+++ computerjanitord/service.py 2011-02-28 18:58:01 +0000
3828@@ -0,0 +1,289 @@
3829+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3830+#
3831+# This program is free software: you can redistribute it and/or modify
3832+# it under the terms of the GNU General Public License as published by
3833+# the Free Software Foundation, version 3 of the License.
3834+#
3835+# This program is distributed in the hope that it will be useful,
3836+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3837+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3838+# GNU General Public License for more details.
3839+#
3840+# You should have received a copy of the GNU General Public License
3841+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3842+
3843+"""dbus service for cleaning up crufty packages that are no longer needed."""
3844+
3845+
3846+from __future__ import absolute_import, unicode_literals
3847+
3848+__metaclass__ = type
3849+__all__ = [
3850+ 'Service',
3851+ ]
3852+
3853+
3854+import atexit
3855+import logging
3856+
3857+import dbus.service
3858+
3859+from computerjanitor import PackageCruft
3860+from computerjanitord.application import Application
3861+from computerjanitord.authenticator import Authenticator
3862+from computerjanitord.collector import Collector
3863+from computerjanitord.errors import NoSuchCruftError, PermissionDeniedError
3864+from computerjanitord.state import State, DEFAULT_STATE_FILE
3865+
3866+import glib
3867+
3868+log = logging.getLogger('computerjanitor')
3869+MISSING = object()
3870+
3871+DBUS_INTERFACE_NAME = 'com.ubuntu.ComputerJanitor'
3872+PRIVILEGE = 'com.ubuntu.computerjanitor.updatesystem'
3873+
3874+
3875+class Service(dbus.service.Object):
3876+ """Backend dbus service that handles removing crufty packages."""
3877+
3878+ def __init__(self, options):
3879+ """Create the dbus service.
3880+
3881+ :param options: The command line options class.
3882+ :type options: `Options`
3883+ """
3884+ self.dry_run = options.arguments.dry_run
3885+ self.state_file = (DEFAULT_STATE_FILE
3886+ if options.arguments.state_file is None
3887+ else options.arguments.state_file)
3888+ self.application = Application()
3889+ self.state = State()
3890+ self.state.load(self.state_file)
3891+ self.collector = Collector(self.application, service=self)
3892+ self.authenticator = Authenticator()
3893+ bus_name = dbus.service.BusName(
3894+ DBUS_INTERFACE_NAME, bus=dbus.SystemBus())
3895+ dbus.service.Object.__init__(self, bus_name, '/')
3896+ # We can't use the decorator because that doesn't work with methods;
3897+ # self doesn't get passed to the handler.
3898+ atexit.register(self._exit_handler)
3899+
3900+ def _exit_handler(self):
3901+ """Ensure that the state file is saved at exit."""
3902+ if not self.dry_run:
3903+ self.state.save(self.state_file)
3904+
3905+ def _authenticate(self, sender, connection):
3906+ """Authenticate via PolicyKit.
3907+
3908+ :param sender: The dbus client sender.
3909+ :param connection: The dbus client connection.
3910+ :raises PermissionDeniedError: when the authentication fails.
3911+ """
3912+ if not self.authenticator.authenticate(sender, connection, PRIVILEGE):
3913+ log.error('Permission denied: {0} for {1} on {2}'.format(
3914+ PRIVILEGE, sender, connection))
3915+ raise PermissionDeniedError(PRIVILEGE)
3916+ log.debug('Permission granted: {0} for {1} on {2}'.format(
3917+ PRIVILEGE, sender, connection))
3918+
3919+ @dbus.service.method(DBUS_INTERFACE_NAME,
3920+ out_signature='as')
3921+ def find(self):
3922+ """Find all the non-whitelisted cruft on the system.
3923+
3924+ Because this is a read-only interface it does not need authorization
3925+ to be called.
3926+
3927+ :return: A list of matching cruft names.
3928+ """
3929+ return list(cruft.get_name() for cruft in self.collector.cruft)
3930+
3931+ @dbus.service.method(DBUS_INTERFACE_NAME,
3932+ out_signature='b')
3933+ def find_async(self):
3934+ """Find all the non-whitelisted cruft on the system, asynchronously.
3935+
3936+ Because this is a read-only interface it does not need authorization
3937+ to be called. Use this method when you can't wait for the cruft
3938+ searching process to complete, since it might take some time. To
3939+ respond when the cruft has been found, set up a `find_finished` signal
3940+ handler.
3941+
3942+ :return: True
3943+ """
3944+ glib.timeout_add_seconds(1, self._find_async)
3945+ return True
3946+
3947+ def _find_async(self):
3948+ """Find all cruft asynchronously and call the signal handler."""
3949+ cruft = list(cruft.get_name() for cruft in self.collector.cruft)
3950+ self.find_finished(cruft)
3951+ # Only call the callback once.
3952+ return False
3953+
3954+ @dbus.service.signal(DBUS_INTERFACE_NAME, signature='as')
3955+ def find_finished(self, cruft):
3956+ """dbus signal called when `_find_async()` completes."""
3957+ log.debug('find_finished: {0}'.format(cruft))
3958+
3959+ @dbus.service.method(DBUS_INTERFACE_NAME,
3960+ out_signature='as',
3961+ # Must wrap these in str() because Python < 2.6.5
3962+ # does not like unicode keyword arguments.
3963+ sender_keyword=str('sender'),
3964+ connection_keyword=str('connection'))
3965+ def load(self, sender=None, connection=None):
3966+ """Load the state file."""
3967+ self._authenticate(sender, connection)
3968+ self.state.load(self.state_file)
3969+ return list(self.state.ignore)
3970+
3971+ @dbus.service.method(DBUS_INTERFACE_NAME,
3972+ # Must wrap these in str() because Python < 2.6.5
3973+ # does not like unicode keyword arguments.
3974+ sender_keyword=str('sender'),
3975+ connection_keyword=str('connection'))
3976+ def save(self, sender=None, connection=None):
3977+ """Save the state file."""
3978+ self._authenticate(sender, connection)
3979+ if not self.dry_run:
3980+ self.state.save(self.state_file)
3981+
3982+ @dbus.service.method(DBUS_INTERFACE_NAME,
3983+ in_signature='s',
3984+ # Must wrap these in str() because Python < 2.6.5
3985+ # does not like unicode keyword arguments.
3986+ sender_keyword=str('sender'),
3987+ connection_keyword=str('connection'))
3988+ def ignore(self, name, sender=None, connection=None):
3989+ """Ignore the named cruft.
3990+
3991+ :param name: The name of the cruft to ignore.
3992+ :type filename: string
3993+ """
3994+ # Make sure this is known cruft first.
3995+ cruft = self.collector.cruft_by_name.get(name, MISSING)
3996+ if cruft is MISSING:
3997+ log.error('ignore(): No such cruft: {0}'.format(name))
3998+ raise NoSuchCruftError(name)
3999+ self._authenticate(sender, connection)
4000+ if not self.dry_run:
4001+ self.state.ignore.add(name)
4002+
4003+ @dbus.service.method(DBUS_INTERFACE_NAME,
4004+ in_signature='s',
4005+ # Must wrap these in str() because Python < 2.6.5
4006+ # does not like unicode keyword arguments.
4007+ sender_keyword=str('sender'),
4008+ connection_keyword=str('connection'))
4009+ def unignore(self, name, sender=None, connection=None):
4010+ """Unignore the named cruft.
4011+
4012+ :param name: The name of the cruft to unignore.
4013+ :type filename: string
4014+ """
4015+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4016+ if cruft is MISSING:
4017+ log.error('ignore(): No such cruft: {0}'.format(name))
4018+ raise NoSuchCruftError(name)
4019+ self._authenticate(sender, connection)
4020+ if not self.dry_run:
4021+ # Don't worry if we're already not ignoring the cruft (i.e. don't
4022+ # raise a KeyError here if 'name' is not in the set).
4023+ self.state.ignore.discard(name)
4024+
4025+ @dbus.service.method(DBUS_INTERFACE_NAME,
4026+ out_signature='as')
4027+ def ignored(self):
4028+ """Return the list of ignored cruft.
4029+
4030+ :return: The names of the ignored cruft.
4031+ :rtype: list of strings
4032+ """
4033+ return list(self.state.ignore)
4034+
4035+ @dbus.service.method(DBUS_INTERFACE_NAME,
4036+ in_signature='s',
4037+ out_signature='s')
4038+ def get_description(self, name):
4039+ """Return the description of the named cruft.
4040+
4041+ :param name: The cruft name.
4042+ :type name: string
4043+ :return: The description of the cruft.
4044+ :rtype: string
4045+ """
4046+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4047+ if cruft is MISSING:
4048+ log.error('get_description(): No such cruft: {0}'.format(name))
4049+ raise NoSuchCruftError(name)
4050+ return cruft.get_description()
4051+
4052+ @dbus.service.method(DBUS_INTERFACE_NAME,
4053+ in_signature='s',
4054+ out_signature='s')
4055+ def get_shortname(self, name):
4056+ """Return the short name of the named cruft.
4057+
4058+ :param name: The cruft name.
4059+ :type name: string
4060+ :return: The short nameof the cruft.
4061+ :rtype: string
4062+ """
4063+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4064+ if cruft is MISSING:
4065+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4066+ raise NoSuchCruftError(name)
4067+ return cruft.get_shortname()
4068+
4069+ @dbus.service.method(DBUS_INTERFACE_NAME,
4070+ in_signature='s',
4071+ out_signature='st')
4072+ def get_details(self, name):
4073+ """Return some extra details about the named cruft.
4074+
4075+ :param name: The cruft name.
4076+ :type name: string
4077+ :return: Some extra details about the named cruft, specifically its
4078+ 'type' and the amount of disk space it consumes. The type is
4079+ simply the name of the cruft instance's class.
4080+ :rtype: string, uint64
4081+ """
4082+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4083+ if cruft is MISSING:
4084+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4085+ raise NoSuchCruftError(name)
4086+ return cruft.__class__.__name__, cruft.get_disk_usage()
4087+
4088+ @dbus.service.method(DBUS_INTERFACE_NAME,
4089+ in_signature='as', # array of strings
4090+ # Must wrap these in str() because Python < 2.6.5
4091+ # does not like unicode keyword arguments.
4092+ sender_keyword=str('sender'),
4093+ connection_keyword=str('connection'))
4094+ def clean(self, names, sender=None, connection=None):
4095+ """Clean the named crufts.
4096+
4097+ :param names: The names of the cruft to clean.
4098+ :type names: list of strings
4099+ """
4100+ self._authenticate(sender, connection)
4101+ self.collector.clean(names, self.dry_run)
4102+
4103+ @dbus.service.signal(DBUS_INTERFACE_NAME,
4104+ signature='s')
4105+ def cleanup_status(self, cruft):
4106+ """Signal cleanup status.
4107+
4108+ This signal is used to incrementally inform clients that some cleanup
4109+ work is being done. It is called at the beginning of the cleanup
4110+ process and after each plugin has completed its `post_cleanup()`
4111+ method.
4112+
4113+ :param done: The name of the next piece of cruft to be cleaned up, or
4114+ the empty string when there's nothing left to do.
4115+ :type done: string
4116+ """
4117+ log.debug('cleanup_status: {0}'.format(cruft))
4118
4119=== added file 'computerjanitord/state.py'
4120--- computerjanitord/state.py 1970-01-01 00:00:00 +0000
4121+++ computerjanitord/state.py 2011-02-28 18:58:01 +0000
4122@@ -0,0 +1,92 @@
4123+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4124+#
4125+# This program is free software: you can redistribute it and/or modify
4126+# it under the terms of the GNU General Public License as published by
4127+# the Free Software Foundation, version 3 of the License.
4128+#
4129+# This program is distributed in the hope that it will be useful,
4130+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4131+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4132+# GNU General Public License for more details.
4133+#
4134+# You should have received a copy of the GNU General Public License
4135+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4136+
4137+"""Maintaining package ignored state."""
4138+
4139+from __future__ import absolute_import, unicode_literals
4140+
4141+__metaclass__ = type
4142+__all__ = [
4143+ 'DEFAULT_STATE_FILE',
4144+ 'State',
4145+ ]
4146+
4147+
4148+import ConfigParser
4149+import textwrap
4150+
4151+
4152+DEFAULT_STATE_FILE = '/var/lib/computer-janitor/state.dat'
4153+
4154+
4155+class State:
4156+ """Maintain the state of cruft which should be ignored.
4157+
4158+ The file's format is a `ConfigParser` style .ini file. Each section is a
4159+ cruft's `.get_name()` value and contains a single boolean setting
4160+ `ignore`. If true, then the cruft is ignored.
4161+
4162+ For backward compatibility purposes, the setting `enabled` is also
4163+ recognized (on read only). If `enabled` is false then the cruft is
4164+ ignored.
4165+ """
4166+
4167+ def __init__(self):
4168+ self.ignore = set()
4169+
4170+ def load(self, filename):
4171+ """Load ignored state from a file.
4172+
4173+ This reset any previously determined state and re-initializes it with
4174+ the state stored in the file.
4175+
4176+ :param filename: The file to load.
4177+ :type filename: string
4178+ """
4179+ parser = ConfigParser.ConfigParser()
4180+ parser.read(filename)
4181+ # Reset the set of ignored packages.
4182+ self.ignore = set()
4183+ for cruft_name in parser.sections():
4184+ # For backwards compatibility, recognize both the 'ignore' setting
4185+ # and the 'enabled' setting. We only write the former.
4186+ try:
4187+ ignore = parser.getboolean(cruft_name, 'ignore')
4188+ except ConfigParser.NoOptionError:
4189+ try:
4190+ ignore = not parser.getboolean(cruft_name, 'enabled')
4191+ except ConfigParser.NoOptionError:
4192+ # No other settings are recognized.
4193+ ignore = False
4194+ if ignore:
4195+ self.ignore.add(cruft_name)
4196+
4197+ def save(self, filename):
4198+ """Save the ignored state to a file.
4199+
4200+ Only the packages being ignored are stored to the file, and writing
4201+ overwrites the previous contents of the file.
4202+
4203+ :param filename: The file to load.
4204+ :type filename: string
4205+ """
4206+ # It's easier just to write the .ini file directly instead of using
4207+ # the ConfigParser interface. This way we can guarantee sort order
4208+ # and can automatically cull unignored packages from the file.
4209+ with open(filename, 'w') as fp:
4210+ for cruft_name in self.ignore:
4211+ print >> fp, textwrap.dedent("""\
4212+ [{0}]
4213+ ignore: true
4214+ """.format(cruft_name))
4215
4216=== added directory 'computerjanitord/tests'
4217=== added file 'computerjanitord/tests/__init__.py'
4218=== added directory 'computerjanitord/tests/data'
4219=== added file 'computerjanitord/tests/data/empty'
4220=== added directory 'computerjanitord/tests/data/etc'
4221=== added directory 'computerjanitord/tests/data/etc/apt'
4222=== added file 'computerjanitord/tests/data/etc/apt/sources.list'
4223--- computerjanitord/tests/data/etc/apt/sources.list 1970-01-01 00:00:00 +0000
4224+++ computerjanitord/tests/data/etc/apt/sources.list 2011-02-28 18:58:01 +0000
4225@@ -0,0 +1,1 @@
4226+deb http://archive.ubuntu.com/ubuntu intrepid main restricted
4227
4228=== added directory 'computerjanitord/tests/data/var'
4229=== added directory 'computerjanitord/tests/data/var/cache'
4230=== added directory 'computerjanitord/tests/data/var/cache/apt'
4231=== added directory 'computerjanitord/tests/data/var/cache/apt/archives'
4232=== added directory 'computerjanitord/tests/data/var/cache/apt/archives/partial'
4233=== added directory 'computerjanitord/tests/data/var/lib'
4234=== added directory 'computerjanitord/tests/data/var/lib/apt'
4235=== added directory 'computerjanitord/tests/data/var/lib/apt/lists'
4236=== added file 'computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages'
4237--- computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 1970-01-01 00:00:00 +0000
4238+++ computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 2011-02-28 18:58:01 +0000
4239@@ -0,0 +1,54 @@
4240+Package: dash
4241+Priority: required
4242+Section: shells
4243+Installed-Size: 236
4244+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4245+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4246+Architecture: all
4247+Version: 0.5.5.1-3ubuntu1
4248+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4249+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4250+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4251+Size: 104190
4252+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4253+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4254+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4255+Description: POSIX-compliant shell
4256+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4257+ from ash.
4258+ .
4259+ Since it executes scripts faster than bash, and has fewer library
4260+ dependencies (making it more robust against software or hardware
4261+ failures), it is used as the default system shell on Debian systems.
4262+Homepage: http://gondor.apana.org.au/~herbert/dash/
4263+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4264+Origin: Ubuntu
4265+Supported: 5y
4266+Task: minimal
4267+
4268+Package: gzip
4269+Essential: yes
4270+Priority: required
4271+Section: utils
4272+Installed-Size: 284
4273+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4274+Original-Maintainer: Bdale Garbee <bdale@gag.com>
4275+Architecture: all
4276+Version: 1.3.12-9ubuntu1
4277+Pre-Depends: libc6 (>= 2.4)
4278+Suggests: less
4279+Filename: pool/main/g/gzip/gzip_1.3.12-9ubuntu1_all.deb
4280+Size: 107030
4281+MD5sum: f64beb93d2d1a3348cfc47f1fd176ee1
4282+SHA1: 3dd3e56f551fb85ba2ad385df463adeff1fff2d9
4283+SHA256: 2545f0a28514535006adf9ee8576ca3be2aa6da3d890047f92eeda18c0e3aa57
4284+Description: GNU compression utilities
4285+ This package provides the standard GNU file compression utilities, which
4286+ are also the default compression tools for Debian. They typically operate
4287+ on files with names ending in '.gz', but can also decompress files ending
4288+ in '.Z' created with 'compress'.
4289+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4290+Origin: Ubuntu
4291+Supported: 5y
4292+Task: minimal
4293+
4294
4295=== added directory 'computerjanitord/tests/data/var/lib/apt/lists/partial'
4296=== added directory 'computerjanitord/tests/data/var/lib/dpkg'
4297=== added file 'computerjanitord/tests/data/var/lib/dpkg/status'
4298--- computerjanitord/tests/data/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
4299+++ computerjanitord/tests/data/var/lib/dpkg/status 2011-02-28 18:58:01 +0000
4300@@ -0,0 +1,28 @@
4301+Package: dash-nodownload
4302+Priority: required
4303+Section: shells
4304+Installed-Size: 236
4305+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4306+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4307+Architecture: all
4308+Version: 0.5.5.1-3ubuntu1
4309+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4310+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4311+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4312+Size: 104190
4313+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4314+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4315+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4316+Description: POSIX-compliant shell
4317+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4318+ from ash.
4319+ .
4320+ Since it executes scripts faster than bash, and has fewer library
4321+ dependencies (making it more robust against software or hardware
4322+ failures), it is used as the default system shell on Debian systems.
4323+Homepage: http://gondor.apana.org.au/~herbert/dash/
4324+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4325+Origin: Ubuntu
4326+Supported: 5y
4327+Task: minimal
4328+
4329
4330=== added file 'computerjanitord/tests/test_all.py'
4331--- computerjanitord/tests/test_all.py 1970-01-01 00:00:00 +0000
4332+++ computerjanitord/tests/test_all.py 2011-02-28 18:58:01 +0000
4333@@ -0,0 +1,41 @@
4334+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4335+#
4336+# This program is free software: you can redistribute it and/or modify
4337+# it under the terms of the GNU General Public License as published by
4338+# the Free Software Foundation, version 3 of the License.
4339+#
4340+# This program is distributed in the hope that it will be useful,
4341+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4342+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4343+# GNU General Public License for more details.
4344+#
4345+# You should have received a copy of the GNU General Public License
4346+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4347+
4348+"""Test suite for Computer Janitor daemon (dbus backend)."""
4349+
4350+from __future__ import absolute_import, unicode_literals
4351+
4352+__metaclass__ = type
4353+__all__ = [
4354+ 'test_suite',
4355+ ]
4356+
4357+
4358+import unittest
4359+
4360+from computerjanitord.tests import test_application
4361+from computerjanitord.tests import test_authenticator
4362+from computerjanitord.tests import test_collector
4363+from computerjanitord.tests import test_state
4364+from computerjanitord.tests import test_whitelist
4365+
4366+
4367+def test_suite():
4368+ suite = unittest.TestSuite()
4369+ suite.addTests(test_application.test_suite())
4370+ suite.addTests(test_authenticator.test_suite())
4371+ suite.addTests(test_collector.test_suite())
4372+ suite.addTests(test_state.test_suite())
4373+ suite.addTests(test_whitelist.test_suite())
4374+ return suite
4375
4376=== added file 'computerjanitord/tests/test_application.py'
4377--- computerjanitord/tests/test_application.py 1970-01-01 00:00:00 +0000
4378+++ computerjanitord/tests/test_application.py 2011-02-28 18:58:01 +0000
4379@@ -0,0 +1,106 @@
4380+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4381+#
4382+# This program is free software: you can redistribute it and/or modify
4383+# it under the terms of the GNU General Public License as published by
4384+# the Free Software Foundation, version 3 of the License.
4385+#
4386+# This program is distributed in the hope that it will be useful,
4387+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4388+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4389+# GNU General Public License for more details.
4390+#
4391+# You should have received a copy of the GNU General Public License
4392+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4393+
4394+"""Test the plugin application interfaces."""
4395+
4396+from __future__ import absolute_import, unicode_literals
4397+
4398+__metaclass__ = type
4399+__all__ = [
4400+ 'ApplicationTestSetupMixin',
4401+ 'test_suite',
4402+ ]
4403+
4404+
4405+import os
4406+import apt
4407+import apt_pkg
4408+import unittest
4409+import warnings
4410+import pkg_resources
4411+
4412+from contextlib import contextmanager
4413+
4414+import computerjanitor
4415+import computerjanitord.application
4416+
4417+from computerjanitord.application import (
4418+ Application, MissingLandmarkError, NonDownloadableError)
4419+
4420+
4421+@contextmanager
4422+def landmarks(*packages):
4423+ # Hack the module global list of known landmark packages.
4424+ old_landmarks = computerjanitord.application.LANDMARK_PACKAGES[:]
4425+ computerjanitord.application.LANDMARK_PACKAGES[:] = packages
4426+ yield
4427+ computerjanitord.application.LANDMARK_PACKAGES[:] = old_landmarks
4428+
4429+
4430+class MockCruft:
4431+ def __init__(self, name):
4432+ self.name = name
4433+
4434+ def get_name(self):
4435+ warnings.warn('.get_name() is deprecated; use .name',
4436+ DeprecationWarning)
4437+ return self.name
4438+
4439+
4440+class ApplicationTestSetupMixin:
4441+ """Set up an `Application` instance with test data in its apt_cache."""
4442+
4443+ def setUp(self):
4444+ self.data_dir = os.path.abspath(
4445+ pkg_resources.resource_filename('computerjanitord.tests', 'data'))
4446+ # Make the test insensitive to the platform's architecture.
4447+ apt_pkg.Config.Set('APT::Architecture', 'i386')
4448+ self.cache = apt.Cache(rootdir=self.data_dir)
4449+ self.app = Application(self.cache)
4450+
4451+ def tearDown(self):
4452+ # Clear the cache.
4453+ cache_dir = os.path.join(self.data_dir, 'var', 'cache', 'apt')
4454+ for filename in os.listdir(cache_dir):
4455+ if filename.endswith('.bin'):
4456+ os.remove(os.path.join(cache_dir, filename))
4457+
4458+
4459+class TestApplication(unittest.TestCase, ApplicationTestSetupMixin):
4460+ """Test the `Application` interface."""
4461+
4462+ def setUp(self):
4463+ ApplicationTestSetupMixin.setUp(self)
4464+
4465+ def tearDown(self):
4466+ ApplicationTestSetupMixin.tearDown(self)
4467+
4468+ def test_verify_apt_cache_good_path(self):
4469+ # All essential packages are in the cache by default.
4470+ self.assertEqual(self.app.verify_apt_cache(), None)
4471+
4472+ def test_verify_apt_cache_with_nondownloadable_landmark(self):
4473+ # Test that a missing landmark file causes an exception.
4474+ with landmarks('gzip', 'dash-nodownload'):
4475+ self.assertRaises(NonDownloadableError, self.app.verify_apt_cache)
4476+
4477+ def test_verify_apt_cache_with_missing_landmark(self):
4478+ with landmarks('gzip', 'dash', 'i-am-not-here'):
4479+ self.assertRaises(MissingLandmarkError, self.app.verify_apt_cache)
4480+
4481+
4482+def test_suite():
4483+ suite = unittest.TestSuite()
4484+ suite.addTests(unittest.makeSuite(TestApplication))
4485+ return suite
4486
4487=== added file 'computerjanitord/tests/test_authenticator.py'
4488--- computerjanitord/tests/test_authenticator.py 1970-01-01 00:00:00 +0000
4489+++ computerjanitord/tests/test_authenticator.py 2011-02-28 18:58:01 +0000
4490@@ -0,0 +1,97 @@
4491+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4492+#
4493+# This program is free software: you can redistribute it and/or modify
4494+# it under the terms of the GNU General Public License as published by
4495+# the Free Software Foundation, version 3 of the License.
4496+#
4497+# This program is distributed in the hope that it will be useful,
4498+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4499+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4500+# GNU General Public License for more details.
4501+#
4502+# You should have received a copy of the GNU General Public License
4503+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4504+
4505+"""Test the authenticator."""
4506+
4507+import unittest
4508+
4509+from computerjanitord.authenticator import Authenticator
4510+
4511+SUCCESS_PID = 801
4512+FAILURE_PID = 999
4513+
4514+AUTHENTICATED_USER = 'desktop-user'
4515+IMPOSTER_USER = 'imposter'
4516+EXPECTED_PRIVILEGE = 'com.ubuntu.computerjanitor.cleanpackages'
4517+BOGUS_PRIVILEGE = 'com.example.evil-corp.killsystem'
4518+
4519+
4520+class MockPolicyKit(object):
4521+ """Mock the PolicyKit's CheckAuthorization() method."""
4522+
4523+ def CheckAuthorization(self, subject, privilege, details, flags,
4524+ cancellation_id):
4525+ """See `policykit.CheckAuthorization()`.
4526+
4527+ :return: (is_authorized, is_challenge, details)
4528+ """
4529+ if privilege == BOGUS_PRIVILEGE:
4530+ return False, False, ''
4531+ assert isinstance(subject, tuple) and len(subject) == 2, (
4532+ 'subject is not a 2-tuple')
4533+ assert subject[0] == 'unix-process', 'Badly formed subject'
4534+ assert isinstance(subject[1], dict), 'Badly formed subject details'
4535+ assert subject[1]['start-time'] == 0, 'subject missing start-time'
4536+ if subject[1]['pid'] == SUCCESS_PID:
4537+ return True, False, ''
4538+ else:
4539+ return False, False, ''
4540+
4541+
4542+class TestableAuthenticator(Authenticator):
4543+ """See `Authenticator`."""
4544+
4545+ def _get_policykit_proxy(self):
4546+ """See `Authenticator`."""
4547+ return MockPolicyKit()
4548+
4549+ def _get_sender_pid(self, connection, sender):
4550+ """See `Authenticator`."""
4551+ if sender == AUTHENTICATED_USER:
4552+ return SUCCESS_PID
4553+ else:
4554+ return FAILURE_PID
4555+
4556+
4557+class TestAuthenticator(unittest.TestCase):
4558+ """Tests of the PolicyKit authenticator."""
4559+
4560+ def setUp(self):
4561+ """See `unittest.TestCase`."""
4562+ self.authenticator = TestableAuthenticator()
4563+ self.connection = object()
4564+
4565+ def tearDown(self):
4566+ """See `unittest.TestCase`."""
4567+
4568+ def test_good_path(self):
4569+ # Test for successful authentication.
4570+ self.assertTrue(self.authenticator.authenticate(
4571+ AUTHENTICATED_USER, self.connection, EXPECTED_PRIVILEGE))
4572+
4573+ def test_bogus_privilege(self):
4574+ # Test for bogus privilege fails.
4575+ self.assertFalse(self.authenticator.authenticate(
4576+ AUTHENTICATED_USER, self.connection, BOGUS_PRIVILEGE))
4577+
4578+ def test_unauthorized(self):
4579+ # Test for some imposter not being able to authenticate.
4580+ self.assertFalse(self.authenticator.authenticate(
4581+ IMPOSTER_USER, self.connection, EXPECTED_PRIVILEGE))
4582+
4583+
4584+def test_suite():
4585+ suite = unittest.TestSuite()
4586+ suite.addTests(unittest.makeSuite(TestAuthenticator))
4587+ return suite
4588
4589=== added file 'computerjanitord/tests/test_collector.py'
4590--- computerjanitord/tests/test_collector.py 1970-01-01 00:00:00 +0000
4591+++ computerjanitord/tests/test_collector.py 2011-02-28 18:58:01 +0000
4592@@ -0,0 +1,225 @@
4593+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4594+#
4595+# This program is free software: you can redistribute it and/or modify
4596+# it under the terms of the GNU General Public License as published by
4597+# the Free Software Foundation, version 3 of the License.
4598+#
4599+# This program is distributed in the hope that it will be useful,
4600+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4601+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4602+# GNU General Public License for more details.
4603+#
4604+# You should have received a copy of the GNU General Public License
4605+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4606+
4607+"""Test the cruft collector."""
4608+
4609+from __future__ import absolute_import, unicode_literals
4610+
4611+__metaclass__ = type
4612+__all__ = [
4613+ 'test_suite',
4614+ ]
4615+
4616+
4617+import os
4618+import shutil
4619+import apt_pkg
4620+import tempfile
4621+import unittest
4622+import pkg_resources
4623+import multiprocessing
4624+
4625+from computerjanitor.plugin import Plugin
4626+from computerjanitord.collector import Collector
4627+from computerjanitord.errors import DuplicateCruftError, PackageCleanupError
4628+from computerjanitord.tests.test_application import ApplicationTestSetupMixin
4629+
4630+
4631+LOCK_FILE = pkg_resources.resource_filename(
4632+ 'computerjanitord.tests',
4633+ os.path.join('data', 'var', 'lib', 'dpkg', 'status'))
4634+
4635+
4636+class MockCruft:
4637+ """Mock cruft that supports the required `get_name()` interface."""
4638+
4639+ def __init__(self, name):
4640+ self.name = name
4641+
4642+ def get_name(self):
4643+ return self.name
4644+
4645+
4646+class MockPlugin(Plugin):
4647+ cruft_class = MockCruft
4648+
4649+ def __init__(self, prefix, shortnames):
4650+ super(MockPlugin, self).__init__()
4651+ self.prefix = prefix
4652+ self.shortnames = shortnames
4653+
4654+ def get_cruft(self):
4655+ for shortname in self.shortnames:
4656+ yield self.cruft_class('{0}:{1}'.format(self.prefix, shortname))
4657+
4658+
4659+class SawAppPlugin(Plugin):
4660+ """All this plugin does is set a marker attribute on the `Application`.
4661+
4662+ This proves that access to the application through the plugin works.
4663+ """
4664+ def get_cruft(self):
4665+ self.app.saw_app_plugin = True
4666+ return []
4667+
4668+
4669+class LockingPlugin(Plugin):
4670+ """All this plugin does is acquire a fake apt lock after cleanup."""
4671+
4672+ def get_cruft(self):
4673+ return []
4674+
4675+ def post_cleanup(self):
4676+ # If the lock cannot be acquired, an exception is raised.
4677+ with apt_pkg.FileLock(LOCK_FILE):
4678+ # Do something.
4679+ pass
4680+
4681+
4682+class MockPluginManager:
4683+ def __init__(self, app, plugin_dirs):
4684+ # Ignore plugin_dirs
4685+ self.app = app
4686+
4687+ def get_plugins(self):
4688+ shortnames = ('one', 'two', 'three')
4689+ for prefix in ('foo', 'bar', 'baz'):
4690+ plugin = MockPlugin(prefix, shortnames)
4691+ plugin.set_application(self.app)
4692+ yield plugin
4693+ for plugin_class in (SawAppPlugin, LockingPlugin):
4694+ plugin = plugin_class()
4695+ plugin.set_application(self.app)
4696+ yield plugin
4697+
4698+
4699+class MockCruftExtra(MockCruft):
4700+ """Cruft with a different class."""
4701+
4702+
4703+class MockPluginExtra(MockPlugin):
4704+ """A mock plugin that returns cruft with a different class."""
4705+
4706+ cruft_class = MockCruftExtra
4707+
4708+
4709+class IgnoredDuplicateCruftPluginManager(MockPluginManager):
4710+ """Add an additional piece of ignorable duplication cruft."""
4711+
4712+ def __init__(self, app, plugin_dirs):
4713+ self.app = app
4714+ # Ignore plugin_dirs
4715+
4716+ def get_plugins(self):
4717+ yield MockPlugin('one', ('foo', 'bar'))
4718+ yield MockPlugin('one', ('baz', 'foo'))
4719+
4720+
4721+class BadDuplicateCruftPluginManager(MockPluginManager):
4722+ """Add an additional piece of bad duplicate cruft."""
4723+ def __init__(self, app, plugin_dirs):
4724+ self.app = app
4725+ # Ignore plugin_dirs
4726+
4727+ def get_plugins(self):
4728+ yield MockPlugin('one', ('foo', 'bar'))
4729+ yield MockPluginExtra('one', ('baz', 'foo'))
4730+
4731+
4732+class TestCollector(unittest.TestCase, ApplicationTestSetupMixin):
4733+ """Test the cruft collector."""
4734+
4735+ def setUp(self):
4736+ # Set up the test data Application.
4737+ ApplicationTestSetupMixin.setUp(self)
4738+ self.tempdir = tempfile.mkdtemp()
4739+ whitelist_dirs = (self.tempdir,)
4740+ with open(os.path.join(self.tempdir, 'one.whitelist'), 'w') as fp:
4741+ print >> fp, 'foo:two'
4742+ print >> fp, 'bar:one'
4743+ print >> fp, 'baz:three'
4744+ self.collector = Collector(self.app, MockPluginManager, whitelist_dirs)
4745+
4746+ def tearDown(self):
4747+ shutil.rmtree(self.tempdir)
4748+ ApplicationTestSetupMixin.tearDown(self)
4749+
4750+ def test_cruft_collector(self):
4751+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4752+ self.assertEqual(cruft_names, set(('foo:one', 'foo:three',
4753+ 'bar:two', 'bar:three',
4754+ 'baz:one', 'baz:two')))
4755+
4756+ def test_plugin_needs_application(self):
4757+ # SawAppPlugin returned by the MockPluginManager sets this attribute
4758+ # on the Application.
4759+ self.assertTrue(self.app.saw_app_plugin)
4760+
4761+ def test_collector_name_mapping(self):
4762+ cruft_keys = set(self.collector.cruft_by_name)
4763+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4764+ self.assertEqual(cruft_keys, cruft_names)
4765+
4766+ def test_cleanup_lock(self):
4767+ # Pretend we're running synaptic at the same time. We have to acquire
4768+ # this fake-synaptic lock in a subprocess.
4769+ lock_event = multiprocessing.Event()
4770+ continue_event = multiprocessing.Event()
4771+ class AptLockThread(multiprocessing.Process):
4772+ def run(self):
4773+ with apt_pkg.FileLock(LOCK_FILE):
4774+ lock_event.set()
4775+ continue_event.wait(1.0)
4776+ apt_locker = AptLockThread()
4777+ apt_locker.start()
4778+ lock_event.wait(1.0)
4779+ self.assertRaises(PackageCleanupError, self.collector.clean, [])
4780+ continue_event.set()
4781+
4782+
4783+class TestDuplicateCruftCollector(
4784+ unittest.TestCase, ApplicationTestSetupMixin):
4785+
4786+ def setUp(self):
4787+ # Set up the test data Application.
4788+ ApplicationTestSetupMixin.setUp(self)
4789+
4790+ def tearDown(self):
4791+ ApplicationTestSetupMixin.tearDown(self)
4792+
4793+ def test_duplicate_cruft_error(self):
4794+ self.assertRaises(DuplicateCruftError, Collector,
4795+ self.app, BadDuplicateCruftPluginManager, [])
4796+
4797+ def test_duplicate_cruft_error_message(self):
4798+ try:
4799+ Collector(self.app, BadDuplicateCruftPluginManager, [])
4800+ except DuplicateCruftError as error:
4801+ self.assertEqual(
4802+ str(error),
4803+ 'Duplicate cruft with different cleanup: one:foo')
4804+ else:
4805+ raise AssertionError('DuplicateCruftError expected')
4806+
4807+ def test_ignored_duplicate_cruft(self):
4808+ collector = Collector(self.app, IgnoredDuplicateCruftPluginManager, [])
4809+ self.assertEqual(list(cruft.get_name() for cruft in collector.cruft),
4810+ ['one:foo', 'one:bar', 'one:baz'])
4811+
4812+
4813+def test_suite():
4814+ suite = unittest.TestSuite()
4815+ suite.addTests(unittest.makeSuite(TestCollector))
4816+ suite.addTests(unittest.makeSuite(TestDuplicateCruftCollector))
4817+ return suite
4818
4819=== added file 'computerjanitord/tests/test_state.py'
4820--- computerjanitord/tests/test_state.py 1970-01-01 00:00:00 +0000
4821+++ computerjanitord/tests/test_state.py 2011-02-28 18:58:01 +0000
4822@@ -0,0 +1,141 @@
4823+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4824+#
4825+# This program is free software: you can redistribute it and/or modify
4826+# it under the terms of the GNU General Public License as published by
4827+# the Free Software Foundation, version 3 of the License.
4828+#
4829+# This program is distributed in the hope that it will be useful,
4830+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4831+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4832+# GNU General Public License for more details.
4833+#
4834+# You should have received a copy of the GNU General Public License
4835+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4836+
4837+"""Test the package state."""
4838+
4839+from __future__ import absolute_import, unicode_literals
4840+
4841+__metaclass__ = type
4842+__all__ = [
4843+ 'test_suite',
4844+ ]
4845+
4846+
4847+import os
4848+import difflib
4849+import tempfile
4850+import textwrap
4851+import unittest
4852+
4853+from computerjanitord.state import State
4854+
4855+NL = '\n'
4856+
4857+
4858+class TestState(unittest.TestCase):
4859+ """Test the `State` class."""
4860+
4861+ def setUp(self):
4862+ self.state = State()
4863+ # Create a temporary file with some enabled and disabled packages.
4864+ fd, self._state_file = tempfile.mkstemp()
4865+ os.close(fd)
4866+ with open(self._state_file, 'w') as fp:
4867+ print >> fp, textwrap.dedent("""\
4868+ [deb:foo]
4869+ ignore: false
4870+ [deb:bar]
4871+ ignore: true
4872+ [deb:baz]
4873+ ignore: true
4874+ """)
4875+ fd, self._state_file_old = tempfile.mkstemp()
4876+ os.close(fd)
4877+ with open(self._state_file_old, 'w') as fp:
4878+ print >> fp, textwrap.dedent("""\
4879+ [deb:qux]
4880+ enabled: false
4881+ [deb:fno]
4882+ enabled: true
4883+ [deb:bla]
4884+ enabled: true
4885+ """)
4886+ fd, self._write_file = tempfile.mkstemp()
4887+ os.close(fd)
4888+
4889+ def tearDown(self):
4890+ os.remove(self._state_file)
4891+ os.remove(self._state_file_old)
4892+ os.remove(self._write_file)
4893+
4894+ def assertEqualNdiff(self, expected, got):
4895+ expected_lines = expected.splitlines()
4896+ got_lines = got.splitlines()
4897+ self.assertEqual(
4898+ expected, got,
4899+ '\n' + NL.join(difflib.ndiff(expected_lines, got_lines)))
4900+
4901+ def test_initially_no_previously_ignored(self):
4902+ self.assertEqual(self.state.ignore, set())
4903+
4904+ def test_load_state(self):
4905+ self.state.load(self._state_file)
4906+ self.assertEqual(self.state.ignore, set(('deb:bar', 'deb:baz')))
4907+
4908+ def test_backward_compatibility_file_format(self):
4909+ # Here, enabled:false means to ignore the package.
4910+ self.state.load(self._state_file_old)
4911+ self.assertEqual(self.state.ignore, set(('deb:qux',)))
4912+
4913+ def test_ignore(self):
4914+ self.state.ignore.add('deb:buz')
4915+ self.state.ignore.add('deb:baz')
4916+ self.assertEqual(self.state.ignore, set(('deb:buz', 'deb:baz')))
4917+
4918+ def test_more_ignores(self):
4919+ self.state.load(self._state_file)
4920+ self.state.ignore.add('deb:buz')
4921+ self.state.ignore.add('deb:baz')
4922+ self.assertEqual(self.state.ignore,
4923+ set(('deb:bar', 'deb:buz', 'deb:baz')))
4924+
4925+ def test_unignore(self):
4926+ self.state.load(self._state_file)
4927+ self.state.ignore.remove('deb:bar')
4928+ self.assertEqual(self.state.ignore, set(('deb:baz',)))
4929+
4930+ def test_write(self):
4931+ self.state.load(self._state_file)
4932+ self.state.ignore.add('deb:buz')
4933+ self.state.ignore.remove('deb:baz')
4934+ self.state.save(self._write_file)
4935+ with open(self._write_file) as fp:
4936+ got = fp.read()
4937+ expected = textwrap.dedent("""\
4938+ [deb:bar]
4939+ ignore: true
4940+
4941+ [deb:buz]
4942+ ignore: true
4943+
4944+ """)
4945+ self.assertEqualNdiff(expected, got)
4946+
4947+ def test_write_new_format(self):
4948+ self.state.load(self._state_file_old)
4949+ self.state.save(self._write_file)
4950+ with open(self._write_file) as fp:
4951+ got = fp.read()
4952+ expected = textwrap.dedent("""\
4953+ [deb:qux]
4954+ ignore: true
4955+
4956+ """)
4957+ self.assertEqualNdiff(expected, got)
4958+
4959+
4960+def test_suite():
4961+ suite = unittest.TestSuite()
4962+ suite.addTests(unittest.makeSuite(TestState))
4963+ return suite
4964
4965=== added file 'computerjanitord/tests/test_whitelist.py'
4966--- computerjanitord/tests/test_whitelist.py 1970-01-01 00:00:00 +0000
4967+++ computerjanitord/tests/test_whitelist.py 2011-02-28 18:58:01 +0000
4968@@ -0,0 +1,119 @@
4969+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4970+#
4971+# This program is free software: you can redistribute it and/or modify
4972+# it under the terms of the GNU General Public License as published by
4973+# the Free Software Foundation, version 3 of the License.
4974+#
4975+# This program is distributed in the hope that it will be useful,
4976+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4977+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4978+# GNU General Public License for more details.
4979+#
4980+# You should have received a copy of the GNU General Public License
4981+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4982+
4983+"""Test the whitelister."""
4984+
4985+from __future__ import absolute_import, unicode_literals
4986+
4987+__metaclass__ = type
4988+__all__ = [
4989+ 'test_suite',
4990+ ]
4991+
4992+
4993+import os
4994+import shutil
4995+import tempfile
4996+import unittest
4997+
4998+from computerjanitord.whitelist import Whitelist
4999+
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches