Merge lp:~rigved/computer-janitor/bug-458872 into lp:computer-janitor

Proposed by Rigved Rakshit
Status: Needs review
Proposed branch: lp:~rigved/computer-janitor/bug-458872
Merge into: lp:computer-janitor
Diff against target: 10612 lines (+10140/-0) (has conflicts)
83 files modified
COPYING (+676/-0)
CoverageTestRunner.py (+203/-0)
Makefile (+52/-0)
NEWS (+262/-0)
README (+31/-0)
TODO (+30/-0)
computer-janitor (+41/-0)
computer-janitor-gtk (+68/-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 (+125/-0)
computerjanitorapp/gtk/main.py (+32/-0)
computerjanitorapp/gtk/store.py (+162/-0)
computerjanitorapp/gtk/ui.py (+596/-0)
computerjanitorapp/terminalsize.py (+63/-0)
computerjanitorapp/tests/test_all.py (+29/-0)
computerjanitorapp/tests/test_terminalsize.py (+54/-0)
computerjanitorapp/tests/test_utilities.py (+70/-0)
computerjanitorapp/utilities.py (+51/-0)
computerjanitord/application.py (+90/-0)
computerjanitord/authenticator.py (+85/-0)
computerjanitord/collector.py (+168/-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 (+120/-0)
computerjanitord/service.py (+288/-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 (+105/-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 (+106/-0)
data/ComputerJanitor.ui (+552/-0)
data/computer-janitor-gtk.desktop.in (+11/-0)
debian/changelog (+576/-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 (+56/-0)
debian/copyright (+35/-0)
debian/default.whitelist (+4/-0)
debian/pycompat (+1/-0)
debian/rules (+41/-0)
debian/watch (+3/-0)
janitord (+27/-0)
license-check (+72/-0)
license-exceptions (+24/-0)
plugins/add_nfs_common_plugin.py (+58/-0)
plugins/autoremoval_plugin.py (+40/-0)
plugins/autoremoval_plugin_tests.py (+54/-0)
plugins/check_admin_group_plugin.py (+85/-0)
plugins/dpkg_dotfile_plugin.py (+87/-0)
plugins/dpkg_dotfile_plugin_tests.py (+107/-0)
plugins/fstab_plugin.py (+124/-0)
plugins/fstab_plugin_tests.py (+123/-0)
plugins/landscape_stub_plugin.py (+59/-0)
plugins/landscape_stub_plugin_tests.py (+87/-0)
plugins/unsupported_plugin.py (+131/-0)
plugins/unsupported_plugin_tests.py (+103/-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 (+10/-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:~rigved/computer-janitor/bug-458872
Reviewer Review Type Date Requested Status
Barry Warsaw Needs Fixing
Review via email: mp+56079@code.launchpad.net

Description of the change

Description:
Compared the list of installed packages registered with dpkg (using a shell script because could not find python-dkpg bindings) and apt (using apt_pkg module).
The packages registered with dpkg, but not with apt, have definitely been manually installed.
So, those packages which have been manually installed are white-listed.

Fixes bug 458872

How to test:

1. Install a package manually via dpkg, like the getdeb-repository package available here: http://archive.getdeb.net/install_deb/getdeb-repository_0.1-1~getdeb1_all.deb

2. Ideally, a refresh of the apt cache or possibly a system restart is required, before the package will even register with apt-get autoremove.
Check the same with CJ.

3. apt-get autoremove should suggest that you remove getdeb-repository but CJ should not give this suggestion.

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

This branch has conflicts and the diff is way too big. Can you please resolve these problems and supersede this merge proposal with a fixed one? I'd like to try to fix this bug before Natty final which is coming up quickly, and if your solution pans out, it would be great to adopt.

Thanks.

review: Needs Fixing

Unmerged revisions

27. By Rigved Rakshit

computerjanitord/whitelist.py: Compared the list of installed
packages registered with dpkg and apt. This generates a list of
manually installed packages, which are whitelisted. (LP: #458872)

26. By Martin Pitt

[ Shane Harbour ]
* NEWS: fixed a typo. (LP: #726616)

[ Martin Pitt ]
* computerjanitorapp/gtk/ui.py: Update require_version() call to current
  pygobject API. Bump python-gobject dependency accordingly.
* computerjanitorapp/gtk/ui.py: Drop the "event.button.button" workaround.
  PyGI now properly handles unions, so it's just "event.button" now.

25. By Barry Warsaw

[ Loïc Minier ]
* debian/control: computer-janitor-gtk depends gir1.2-pango-1.0 not
  gir1.2-pango-2.0. (LP: #720529)

[ Barry Warsaw ]
* setup.py, computer-janitor-gtk.install: Install 24x24.png into
  /usr/share/computer-janitor which is where gtk expects it to be.
  (LP: #720743)
* Fix various apt_cache deprecation warnings.
* debian/rules:
  - Switch to dh_python2.
  - Run the unit tests at build time.
  - Add --keep to dh_installchangelogs.
  - Remove override_dh_pysupport.
* debian/control:
  - Add Build-Depends on python-dbus and update-manager-core so that the
    unit tests can be run at build time. Remove B-D on python-support.
    Change B-D to python-all for tests under all available Python versions.
  - Fix typo in descriptions.
  - Remove XB-Python-Version and add X-Python-Version for dh_python2.
* Note: 2.1.0-0ubuntu2 was a quick fix upload to the archive containing
  the above debian/control file fix.

24. By Loïc Minier

* control: computer-janitor-gtk depends gir1.2-pango-1.0 not
  gir1.2-pango-2.0; LP: #720529.
* Temporarily comment out Vcs-Bzr as I can't commit to it.

23. By Barry Warsaw

[ Martin Pitt ]
* data/ComputerJanitor.ui: Drop obsolete has_separator properties.
* run_from_checkout.sh: Drop setting of $PYTHONPATH. Current directory is
  there by default anyway, and this breaks setting it from the environment.
* computerjanitorapp/gtk/*: Port from pygtk2 to pygi. Works fully with GTK3
  now, with GTK2 we need to disable the right-click popup menu
  (popup_for_device() is introspection safe, but only exists in GTK3.
  popup() isn't introspectable).
* computerjanitorapp/gtk/ui.py: Force GTK2 for now, as we do not yet have a
  GTK3 theme in Natty, and don't carry the GTK3 stack in the default
  install.
* debian/control: Update dependencies for the pygtk → pygi switch.
* debian/control: Drop obsolete system-cleaner conflicts and gksu
  dependency.

[ Barry Warsaw ]
* Add Edit menu items for selecting and deselecting cruft.
* Bump version number.
* Update copyright years.
* Python style, whitespace, and import cleanups.
* Print error messages via logger instead of stderr.

22. By Barry Warsaw

* python-dbus cannot type-convert a set, so use a tuple for the package
  set when --all is given to the computer-janitor cli. (LP: #601585)
* Fix exceptoin when the user de-selects all cruft in the ui, then clicks
  on the 'Do' button. Instead, a dialog pops up that says there's nothing
  to do. (LP: #591433)
* Clean up some unused imports.

21. By Barry Warsaw

* debian/control: Add explicit dependency on dbus since depending on
  python-dbus does not yield an implicit dependency on dbus. (LP: #665740)
* computerjanitord/authenticator.py: Explicitly convert start-time to a
  UInt64(0). Under Natty, implicit conversion uses a UInt32 which does
  not match the CheckAuthorization signature. (LP: #676488)

20. By Matthias Klose

Rebuild with python 2.7 as the python default.

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.

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-04-03 15:43:25 +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-04-03 15:43:25 +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-04-03 15:43:25 +0000
895@@ -0,0 +1,52 @@
896+# Makefile for Computer Janitor
897+#
898+# Copyright (C) 2008-2011 Canonical, Ltd.
899+#
900+# This program is free software: you can redistribute it and/or modify
901+# it under the terms of the GNU General Public License as published by
902+# the Free Software Foundation, version 3 of the License.
903+#
904+# This program is distributed in the hope that it will be useful,
905+# but WITHOUT ANY WARRANTY; without even the implied warranty of
906+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
907+# GNU General Public License for more details.
908+#
909+# You should have received a copy of the GNU General Public License
910+# along with this program. If not, see <http://www.gnu.org/licenses/>.
911+
912+all: data/computer-janitor-gtk.desktop
913+
914+DESKTOP=data/computer-janitor-gtk.desktop
915+$(DESKTOP): $(DESKTOP).in
916+ intltool-merge -d po $(DESKTOP).in $(DESKTOP)
917+
918+check: check-unittests check-has-unit-tests check-licenses
919+
920+check-unittests:
921+ env COMPUTER_JANITOR_UNITTEST=yes LC_ALL=C python -m CoverageTestRunner
922+ rm -f .coverage
923+
924+check-has-unit-tests:
925+ find computerjanitorapp -name '*.py' ! -name '*_tests.py' | \
926+ grep -Exv '^computerjanitorapp/(__init__|ui_gtk).py' | \
927+ sed 's/\.py$$//' | \
928+ while read file; do \
929+ [ -e $${file}_tests.py ] || \
930+ (echo "Missing unit test: $$file.py"; exit 1); done
931+
932+check-licenses:
933+ ./license-check
934+
935+clean:
936+ rm -f computerjanitorapp/*.py[co] .coverage
937+ rm -f po/computerjanitor.pot
938+ rm -rf build $(DESKTOP)
939+
940+update-po:
941+ echo '[encoding: UTF-8]' > po/POTFILES.in; \
942+ find computerjanitorapp -name \*.py >> po/POTFILES.in; \
943+ for f in data/*.ui; do \
944+ echo '[type: gettext/glade]'$$f >> po/POTFILES.in; \
945+ done; \
946+ ls data/*.in plugins/*.py >> po/POTFILES.in; \
947+ python setup.py build_i18n --merge-po --po-dir po
948
949=== renamed file 'Makefile' => 'Makefile.moved'
950=== added file 'NEWS'
951--- NEWS 1970-01-01 00:00:00 +0000
952+++ NEWS 2011-04-03 15:43:25 +0000
953@@ -0,0 +1,262 @@
954+NEWS for Computer Janitor
955+=========================
956+
957+This file summarizes mainly user-visible changes. For detailed
958+changes, please see the bzr commit log messages.
959+
960+Version 2.1.1, released 20XX-XX-XX
961+
962+ * 24x24 icon location is fixed. (LP: #720743)
963+
964+Version 2.1.0, released 2011-02-16
965+
966+ Martin Pitt
967+ * Port from pygtk to pygi, giving identical functionality under GTK3,
968+ however main-panel popup menus are non-functional under GTK2.
969+ Barry Warsaw
970+ * Add Edit menu items for selecting and deselecting cruft.
971+ * Bump version number.
972+ * Update copyright years.
973+ * Python style, whitespace, and import cleanups.
974+ * Print error messages via logger instead of stderr.
975+
976+Version 2.0.5, released 2011-02-15
977+
978+ * python-dbus cannot type-convert a set, so use a tuple for the package
979+ set when --all is given to the computer-janitor cli. (LP: #601585)
980+ * Fix exception when the user de-selects all cruft in the ui, then clicks
981+ on the 'Do' button. Instead, a dialog pops up that says there's nothing
982+ to do. (LP: #591433)
983+ * Clean up some unused imports.
984+
985+Version 2.0.4, released 2010-09-20
986+
987+ * Update translation strings (LP: #621723) (LP: #612493)
988+ * computer-janitor-gtk --help and --version switches added
989+ * Fixed SystemError crash in open() (LP: #545306)
990+ * Do not depend on python-fstab any more.
991+ * Fixed icon (LP: #434431) - Pavol Klačanský
992+
993+Version 2.0, released 2010-03-15
994+
995+ * Refactored the system modifying parts of Computer Janitor into a
996+ separate dbus daemon. This allows you to run the cli or gtk versions
997+ without being root. You will be prompted for the administrative
998+ password when necessary.
999+
1000+ * The user interface has been cleaned up to remove previously unused gui
1001+ elements.
1002+
1003+Version 1.13.3, released 2009-09-09
1004+
1005+ * Fixed URL in About dialog.
1006+
1007+ * The "Do cleanups" button is now set to sensitive when there is
1008+ something to clean.
1009+
1010+ * The user is now told if nothing was found to clean up, so they
1011+ don't wonder why the window is empty.
1012+
1013+ * The 'unsupported package' plugin is re-enabled.
1014+
1015+Version 1.13.2, released 2009-08-21
1016+
1017+ * Versions 1.13.0 and 1.13.1 were not released publicly. This
1018+ section documents all changes since 1.12.1.
1019+
1020+ * The GTK user interface has been completely rewritten from scratch.
1021+ Thanks to Martin Albisetti for the general design. All bugs are belong
1022+ to Lars Wirzenius anyway.
1023+
1024+ * Adapted to new python-apt API. This means the code only works on
1025+ Ubuntu karmic, not earlier releases.
1026+
1027+ * The 'unsupported package' plugin is now not installed by default.
1028+ This should make it less likely people remove third-party packages
1029+ by default.
1030+
1031+ * The fstab plugin is not installed by default. It is no longer needed
1032+ in karmic, since the Linux kernel now uses relatime by default.
1033+
1034+ BUG FIXES:
1035+
1036+ * I18n changes to account for the fact that Computer Janitor's code
1037+ is in two places (itself, and Update Manager), and thus two po
1038+ files.
1039+
1040+ * I18n change to include plugin code as well. Thanks to Gabor Kelemen.
1041+
1042+ * Other i18n changes, in both the GTK and command line user interfaces.
1043+
1044+ * All instances of the word 'cruft' have been removed from the user
1045+ interface. It is not a proper English word.
1046+
1047+ * Typo fix in manual page. Thanks to Charlie_Smotherman.
1048+
1049+Version 1.12.1, released 2009-03-04
1050+
1051+ * Change the description text at the top of the main window to be
1052+ clearer.
1053+
1054+ * Sort cruft in the GTK user interface in groups. This should
1055+ some day get user-visible headings, so it's clearer to the user.
1056+
1057+ BUG FIXES:
1058+
1059+ * In the GTK user interface, the button "Cleanup" now uses "u" as the
1060+ accelerator key, rather than "c", which confliced with the
1061+ accelerator key for the "Close" button. Fixes LP: #311557, reported
1062+ by hackel.
1063+
1064+ * Changed the label for the yes/no column in the GTK UI to be
1065+ "Remove/fix?" rather than just "Remove?", since not all things
1066+ are removals. Fixes LP: #300354, reported by Hernando Torque.
1067+
1068+Version 1.12, released 2009-02-19
1069+
1070+ * New plugin: remove files left on the filesystem by the dpkg
1071+ conffile handling (*.dpkg-old and *.dpkg-new).
1072+
1073+ * Code base has been split so that the core parts (plugin management,
1074+ some of the plugins) now reside in the update-manager source tree.
1075+ This was done so that update-manager can use the computer-janitor
1076+ plugins, since there was no point in both programs having code to do
1077+ the same thing.
1078+
1079+ * When handling exceptions within the computer-janitor code, the
1080+ actual exception is now logged (at DEBUG level), to aid in
1081+ debugging the cause.
1082+
1083+ * Plugins can now declare that they should only be used under specific
1084+ conditions, rather than always. This is useful for plugins that are
1085+ used by update-manager.
1086+
1087+Version 1.11, released 2009-02-03
1088+
1089+ * Renamed to Computer Janitor. The old name, Cruft Remover, was using
1090+ computer jargon (cruft) and gave the wrong impression (the program
1091+ does not just remove stuff).
1092+
1093+ * New plugin to compact the dpkg status file, from Michael Vogt.
1094+
1095+Version 1.10.5, released 2009-01-16
1096+
1097+ * Upon startup, we now verify that the package lists look sane by
1098+ checking for the dash and gzip packages. If they're not available,
1099+ we assume something is badly wrong and we won't continue. This
1100+ makes is very unlikely that people will accidentally remove all
1101+ their packages because apt didn't have any package lists available.
1102+
1103+ * A further safeguard: the running kernel and related packages
1104+ won't ever be considered cruft anymore.
1105+
1106+ * It is now no longer enough to click on the "Cleanup" button to
1107+ initiate changes (pacxkage removals etc). Instead, a dialog window
1108+ will pop up and the user has to click on "OK" to continue.
1109+
1110+ * A .deb package's short description is now shown in the cruft
1111+ description. This will help users have an idea what it's all
1112+ about. (It's insufficient, but was all that there was time for
1113+ for the Ubuntu intrepid release schedule.)
1114+
1115+ * Columns in the cruft list now have titles, to make it easier
1116+ to understand what ticks mean (remove cruft? don't remove
1117+ cruft?).
1118+
1119+ * The program now reads whitelists in /etc/cruft-remover.d. Cruft
1120+ named in files in that directory will _not_ be removed.
1121+
1122+ * The plugin to remove packages apt considers autoremovable is now
1123+ enabled. It has been written ages ago, but not been enabled by
1124+ default until now.
1125+
1126+ * The application now uses the icon it has.
1127+
1128+ * Added Polish translation from Piotr Makowski.
1129+
1130+ * Added Finnish translation from Lars Wirzenius and Timo Jyrinki.
1131+
1132+Version 1.10.4, released 2008-10-27
1133+
1134+ * Quick bug fixes for the Ubuntu 8.10 release. A proper release
1135+ of Cruft Remover will be made later.
1136+
1137+Version 1.10.3, released 2008-10-14
1138+
1139+ * Bug fixes to how messages are logged, primarily to how character
1140+ sets are encoded. Found by James Westby.
1141+
1142+Version 1.10.2, released 2008-10-14
1143+
1144+ IMPORTANT CHANGES:
1145+
1146+ * If setting up logging to syslog fails, don't crash. Thanks,
1147+ Ricardo Wurmus. (LP:#274970)
1148+
1149+ * The program now supports translations via gettext. A Finnish
1150+ translation was added. (LP:#279580)
1151+
1152+ * Handles errors by giving nice(r) error messages also in the
1153+ command line version. The GTK+ version already did that.
1154+ Thanks, Marco Rodrigues. (LP:#281657)
1155+
1156+ MINOR CHANGES:
1157+
1158+ * Wording in manual page fixed. Thanks, James Westby.
1159+
1160+ * Non-functional help button hidden. Thanks, Loïc Minier. (LP:#279577)
1161+
1162+ * Cruft description in the GTK+ user interface is cleared after cruft has
1163+ been cleaned up.
1164+
1165+Version 1.10.1, released 2008-10-06
1166+
1167+ * Bugfix: Cruft in /etc/fstab now goes away from the GTK user interface
1168+ after a cleanup. Found by James Westby, thanks.
1169+
1170+ * cruft-remover-gtk now has a manual page.
1171+
1172+ * Bugfix: The GTK user interface now no longer imports the gtk and
1173+ gobject Python modules globally, but only when the code actually
1174+ runs, to prevent them from being loaded when running unit tests,
1175+ etc. (The GTK UI code is not unit tested.)
1176+
1177+Version 1.10, released 2008-09-30
1178+
1179+ * Important fixes to how GTK+ and threads are used: now all GTK+
1180+ stuff happens only in the main thread. This turns out to be
1181+ easier to get right than using gtk.gdk.threads_enter and _leave
1182+ correctly in all situations, and also allows things like
1183+ gtk.Dialog.run to be used.
1184+
1185+ * Missing test module for the deb_plugin module written.
1186+
1187+ * SystemError exceptions (which are raised by python-apt when
1188+ there is a dpkg error) are now handled, and an error displayed
1189+ to the user. This fixes LP: #275034.
1190+
1191+ * The GTK user interface now displays the version number at startup.
1192+ The command line interface shows it with the new --version option.
1193+
1194+Version 1.9, released 2008-09-26
1195+
1196+ * Bug fix: cruft-remover-gtk now correctly refreshes the list of
1197+ cruft after a bug has been fixed. (LP:274715)
1198+
1199+ * Bug fix: cruft-remover-gtk disables the "Cleanup" button if there
1200+ is nothing to clean up. (LP:274717)
1201+
1202+Version 1.8, released 2008-09-23
1203+
1204+ * The GTK user interface now lets you select a row without toggling
1205+ the status of the cruft. This is useful, because the description
1206+ of each cruft is shown for the selected row only.
1207+
1208+ * Both the GTK and command line user interfaces now attempt to
1209+ be more helpful in describing what each piece of cruft is.
1210+
1211+Version 1.7, released 2008-09-12
1212+
1213+ * First real public release, despite the version number. (If you have
1214+ seen any earlier versions, they were not real. Not real, I tell
1215+ you! Not real! They didn't exist! Really!)
1216
1217=== renamed file 'NEWS' => 'NEWS.moved'
1218=== added file 'README'
1219--- README 1970-01-01 00:00:00 +0000
1220+++ README 2011-04-03 15:43:25 +0000
1221@@ -0,0 +1,31 @@
1222+README for Computer Janitor
1223+===========================
1224+
1225+
1226+Computer Janitor is a tool to clean up a system so that it is more like
1227+a freshly installed one. It was written for Ubuntu, but should be
1228+possible to port to other Unix-like operating systems.
1229+
1230+
1231+Copyright license
1232+-----------------
1233+
1234+Computer Janitor - clean up a computer system
1235+Copyright (C) 2008-2011 Canonical, Ltd.
1236+
1237+The icon (in data/cruftremover-256x256.png and data/cruftremover-24x25.png):
1238+Copyright (C) Marco Rodrigues
1239+
1240+The following license applies to all files (including the icons):
1241+
1242+This program is free software: you can redistribute it and/or modify
1243+it under the terms of the GNU General Public License as published by
1244+the Free Software Foundation, version 3 of the License.
1245+
1246+This program is distributed in the hope that it will be useful,
1247+but WITHOUT ANY WARRANTY; without even the implied warranty of
1248+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1249+GNU General Public License for more details.
1250+
1251+You should have received a copy of the GNU General Public License
1252+along with this program. If not, see <http://www.gnu.org/licenses/>.
1253
1254=== renamed file 'README' => 'README.moved'
1255=== added file 'TODO'
1256--- TODO 1970-01-01 00:00:00 +0000
1257+++ TODO 2011-04-03 15:43:25 +0000
1258@@ -0,0 +1,30 @@
1259+Computer Janitor ideas
1260+
1261+ - policykit
1262+ - aptdaemon backend
1263+ - nx is disabled?
1264+ - cronjob to run daily to report cruft
1265+ - vrms plugin
1266+ - plugin to suggest removing -generic kernel if more specific one
1267+ is installed and running (-server, e.g.)
1268+ - configuration mechanism for plugins
1269+ - plugin to check for sources.list inconsistencies: $released-updates
1270+ enabled for foo, if $released is enabled for foo (where foo is
1271+ main, universe, etc)
1272+ - UI improvement: group items so that previously ignored stuff is in a
1273+ separate section of the list, and new stuff is at the top.
1274+ - UI improvement: more information about a .deb: size, full
1275+ description, when was it installed, which release did it come
1276+ from
1277+ - UI improvement: show sections based on type of cruft. e.g., .deb, fstab
1278+ - add policykit support
1279+ - cj: setup.py shouldn't hardcode version string, but should not
1280+ import computerjanitorapp either, since that requires access
1281+ to the computerjanitor package.
1282+ - import computerjanitor.version?
1283+ - write plugin: unpurged packages
1284+ - kde UI
1285+ - write plugin (early karmic): apt-get autoclean
1286+ - relatime unnecessary in karmic? at least obey noatime, strictatime
1287+ http://mjg59.livejournal.com/109286.html
1288+
1289
1290=== renamed file 'TODO' => 'TODO.moved'
1291=== added file 'computer-janitor'
1292--- computer-janitor 1970-01-01 00:00:00 +0000
1293+++ computer-janitor 2011-04-03 15:43:25 +0000
1294@@ -0,0 +1,41 @@
1295+#!/usr/bin/python
1296+#
1297+# Copyright (C) 2008-2011 Canonical, Ltd.
1298+#
1299+# This program is free software: you can redistribute it and/or modify
1300+# it under the terms of the GNU General Public License as published by
1301+# the Free Software Foundation, version 3 of the License.
1302+#
1303+# This program is distributed in the hope that it will be useful,
1304+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1305+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1306+# GNU General Public License for more details.
1307+#
1308+# You should have received a copy of the GNU General Public License
1309+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1310+
1311+"""Start the command-line computer janitor."""
1312+
1313+from __future__ import absolute_import, unicode_literals
1314+
1315+__metaclass__ = type
1316+__all__ = [
1317+ ]
1318+
1319+
1320+import sys
1321+
1322+from computerjanitorapp import setup_gettext
1323+from computerjanitorapp.cli.main import main
1324+from dbus.exceptions import DBusException
1325+_ = setup_gettext()
1326+
1327+
1328+try:
1329+ main()
1330+except KeyboardInterrupt:
1331+ pass
1332+except DBusException as error:
1333+ print >> sys.stderr, _(
1334+ 'Cannot contact Computer Janitor dbus service; try again.')
1335+ sys.exit(1)
1336
1337=== added file 'computer-janitor-gtk'
1338--- computer-janitor-gtk 1970-01-01 00:00:00 +0000
1339+++ computer-janitor-gtk 2011-04-03 15:43:25 +0000
1340@@ -0,0 +1,68 @@
1341+#!/usr/bin/python
1342+#
1343+# computer-janitor-gtk - clean up a Unix-like operating system (GTK version)
1344+#
1345+# Copyright (C) 2008-2011 Canonical, Ltd.
1346+#
1347+# This program is free software: you can redistribute it and/or modify
1348+# it under the terms of the GNU General Public License as published by
1349+# the Free Software Foundation, version 3 of the License.
1350+#
1351+# This program is distributed in the hope that it will be useful,
1352+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1353+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1354+# GNU General Public License for more details.
1355+#
1356+# You should have received a copy of the GNU General Public License
1357+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1358+
1359+"""Start the gtk computer janitor."""
1360+
1361+from __future__ import absolute_import, unicode_literals
1362+
1363+__metaclass__ = type
1364+__all__ = [
1365+ ]
1366+
1367+
1368+import os
1369+import sys
1370+import logging
1371+import argparse
1372+import traceback
1373+import computerjanitor
1374+
1375+from computerjanitorapp import __version__, setup_gettext
1376+from computerjanitorapp.gtk.main import main
1377+from dbus.exceptions import DBusException
1378+
1379+_ = setup_gettext()
1380+
1381+
1382+# Parse some simple command line arguments.
1383+parser = argparse.ArgumentParser(
1384+ description=_("""\
1385+ Find and remove cruft from your system.
1386+
1387+ Cruft is anything that shouldn't be on your system, but is.
1388+ Stretching the definition, cruft is also things that should be on
1389+ your system but aren't."""))
1390+parser.add_argument(
1391+ '--version', action='version',
1392+ version=_('Computer Janitor {version}').format(
1393+ version=__version__))
1394+parser.parse_args()
1395+# If --version or --help is given, .parse_args() will not return.
1396+
1397+try:
1398+ logging.basicConfig()
1399+ log = logging.getLogger('computerjanitor')
1400+ main()
1401+except KeyboardInterrupt:
1402+ pass
1403+except DBusException as error:
1404+ log.exception('Cannot contact Computer Janitor dbus service; try again.')
1405+ sys.exit(1)
1406+except:
1407+ log.exception('computer-janitor-gtk uncaught exception')
1408+ sys.exit(1)
1409
1410=== added file 'computer-janitor-gtk.8'
1411--- computer-janitor-gtk.8 1970-01-01 00:00:00 +0000
1412+++ computer-janitor-gtk.8 2011-04-03 15:43:25 +0000
1413@@ -0,0 +1,36 @@
1414+.\" Copyright (C) 2008-2011 Canonical, Ltd.
1415+.\"
1416+.\" This program is free software: you can redistribute it and/or modify
1417+.\" it under the terms of the GNU General Public License as published by
1418+.\" the Free Software Foundation, version 3 of the License.
1419+.\"
1420+.\" This program is distributed in the hope that it will be useful,
1421+.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
1422+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1423+.\" GNU General Public License for more details.
1424+.\"
1425+.\" You should have received a copy of the GNU General Public License
1426+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
1427+.TH COMPUTER-JANITOR-GTK 8 2011-02-16 Ubuntu "Ubuntu system administration"
1428+.SH NAME
1429+computer-janitor-gtk \- remove cruft from system (GUI version)
1430+.SH SYNOPSIS
1431+.B computer-janitor-gtk
1432+.SH DESCRIPTION
1433+.B computer-janitor-gtk
1434+is a version of
1435+.BR computer-janitor (8)
1436+with a graphical user interface.
1437+See the latter's manual page for more information.
1438+.SH ENVIRONMENT
1439+In addition to the environment variables understood by
1440+.BR computer-janitor (8),
1441+the following one is understood by
1442+.B computer-janitor-gtk
1443+as well.
1444+.TP
1445+.B COMPUTER_JANITOR_GLADE
1446+The location of the Glade data file to specify the user interface.
1447+You should not need to set this, unless you really know what you are doing.
1448+.SH "SEE ALSO"
1449+.BR computer-janitor (8).
1450
1451=== renamed file 'computer-janitor-gtk.8' => 'computer-janitor-gtk.8.moved'
1452=== renamed file 'computer-janitor-gtk' => 'computer-janitor-gtk.moved'
1453=== added file 'computer-janitor.8'
1454--- computer-janitor.8 1970-01-01 00:00:00 +0000
1455+++ computer-janitor.8 2011-04-03 15:43:25 +0000
1456@@ -0,0 +1,222 @@
1457+.\" Copyright (C) 2008-2011 Canonical, Ltd.
1458+.\"
1459+.\" This program is free software: you can redistribute it and/or modify
1460+.\" it under the terms of the GNU General Public License as published by
1461+.\" the Free Software Foundation, version 3 of the License.
1462+.\"
1463+.\" This program is distributed in the hope that it will be useful,
1464+.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
1465+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1466+.\" GNU General Public License for more details.
1467+.\"
1468+.\" You should have received a copy of the GNU General Public License
1469+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
1470+.TH COMPUTER-JANITOR 8 2011-02-16 Ubuntu "Ubuntu system administration"
1471+.SH NAME
1472+computer-janitor \- clean up a system installation
1473+.SH SYNOPSIS
1474+.B "computer-janitor"
1475+.RB [ --version|-h ]
1476+.br
1477+.B "computer-janitor find"
1478+.RB [ -i|-r|-s|-v|-h ]
1479+.RI
1480+.br
1481+.B "computer-janitor clean"
1482+.RB [ -a|-v|-h ]
1483+.RI [ CRUFT ]...
1484+.br
1485+.B "computer-janitor ignore"
1486+.RB [ -h ]
1487+.RI CRUFT
1488+.br
1489+.B "computer-janitor unignore"
1490+.RB [ -h ]
1491+.RI CRUFT
1492+.br
1493+.B computer-janitor-gtk
1494+.SH DESCRIPTION
1495+.B computer-janitor
1496+and
1497+.B computer-janitor-gtk
1498+find and remove
1499+.I cruft
1500+from your system.
1501+The former is a command line program,
1502+the latter has a graphical user interface.
1503+.PP
1504+Cruft is anything that shouldn't be on the system, but is.
1505+Stretching the definition, it is also things that should be on the system,
1506+but aren't.
1507+Examples:
1508+.IP \(bu
1509+Packages that were originally installed because something else depended on
1510+them, but the depending package has since been removed.
1511+Typically this includes large numbers of libraries.
1512+.IP \(bu
1513+Packages that are no longer supported by the current release of the
1514+operating system.
1515+For example, this can be applications whose development have stopped
1516+and that no longer have support, including for security issues.
1517+Keeping such applications installed can be dangerous.
1518+.IP \(bu
1519+Configuration tweaks that are missing from the system,
1520+but which would be there if the system was installed from scratch.
1521+For example, mount options for filesystems such as
1522+the
1523+.B relatime
1524+option.
1525+.PP
1526+.B computer-janitor
1527+has four modes of operation, invoked by the first non-option
1528+word in the argument list.
1529+.IP \(bu
1530+.B find
1531+searches for cruft and prints out a list of them.
1532+Each piece of cruft is also tagged with its state:
1533+.I ignored
1534+or
1535+.IR removable .
1536+.IP \(bu
1537+.B clean
1538+actually removes the cruft.
1539+It will remove those pieces of cruft you name on the command line.
1540+If you want to remove everything identified by
1541+.B find
1542+that is marked
1543+.IR removable ,
1544+use the
1545+.B --all
1546+option.
1547+.IP \(bu
1548+.B ignore
1549+and
1550+.B unignore
1551+mark cruft as
1552+.I ignored
1553+or
1554+.IR removable ,
1555+respectively.
1556+.SH OPTIONS
1557+Each subcommand listed above has its own set of options. If
1558+.B computer-janitor
1559+is invoked with no subcommand, the following options are available:
1560+.TP
1561+.B --version
1562+Print the version number and exit.
1563+.TP
1564+.BR --help | -h
1565+Print some global help and exit.
1566+.PP
1567+The
1568+.B find
1569+subcommand supports the following options:
1570+.TP
1571+.BR --ignored | -i
1572+Find and display only the system's ignored cruft.
1573+.TP
1574+.BR --removable | -r
1575+Find and display only the system's removable cruft.
1576+.TP
1577+.BR --short | -s
1578+Display only the cruft names; do not use with
1579+.BR --verbose .
1580+.TP
1581+.BR --verbose | -v
1582+Display a detailed explanation for each piece of cruft found.
1583+.TP
1584+.BR --help | -h
1585+Print detailed help for the
1586+.B find
1587+subcommand and exit.
1588+.PP
1589+The
1590+.B clean
1591+subcommand requires either a cruft name or the
1592+.B --all
1593+option to specify which cruft to remove. It supports the following options:
1594+.TP
1595+.B --all | -a
1596+Remove all system cruft that are not ignored.
1597+.TP
1598+.BR --verbose | -v
1599+Provide more details about the cruft being cleaned up.
1600+.TP
1601+.BR --help | -h
1602+Print detailed help for the
1603+.B clean
1604+subcommand and exit.
1605+.PP
1606+The
1607+.B ignore
1608+and
1609+.B unignore
1610+commands both take the name of a cruft to mark ignored or removable,
1611+respectively. They both also accept these options:
1612+.TP
1613+.BR --help | -h
1614+Print detailed help for the
1615+.B ignore
1616+or
1617+.B unignore
1618+subcommands and exit.
1619+.SH "EXIT STATUS"
1620+.B computer-janitor
1621+will return an exit code of 0 for successful operation (no errors).
1622+It will return a non-zero exit code if there are any errors.
1623+It is not an error to find cruft, or to not find cruft.
1624+.SH FILES
1625+.TP
1626+.B /var/lib/computer-janitor/state.dat
1627+This file stores the
1628+.I ignored
1629+or
1630+.I removable
1631+state of system cruft. Any cruft not listed in this file is by default
1632+.IR removable .
1633+.TP
1634+.B /etc/computer-janitor.d
1635+This directory contains whitelist files, which specify
1636+things that are never considered cruft.
1637+A whitelist file has a name that ends with
1638+.BR .whitelist ,
1639+and contains one (potential) cruft name per line.
1640+(Empty lines and lines beginning with # are ignored.)
1641+.SH EXAMPLE
1642+To find all cruft on the system:
1643+.sp 1
1644+.RS
1645+computer-janitor find
1646+.RE
1647+.PP
1648+To remove a specific piece of cruft:
1649+.sp 1
1650+.RS
1651+computer-janitor clean hello
1652+.RE
1653+.PP
1654+To mark a piece of cruft as
1655+.IR ignored ,
1656+so that it isn't removed by
1657+.B clean
1658+.BR --all :
1659+.sp 1
1660+.RS
1661+computer-janitor ignore hello
1662+.RE
1663+.PP
1664+To mark a piece of cruft as
1665+.I removable
1666+again:
1667+.sp 1
1668+.RS
1669+computer-janitor unignore hello
1670+.RE
1671+.PP
1672+To remove all cruft that isn't ignored:
1673+.sp 1
1674+.RS
1675+computer-janitor clean --all
1676+.RE
1677+.SH "SEE ALSO"
1678+.BR computer-janitor-gtk (8).
1679
1680=== renamed file 'computer-janitor.8' => 'computer-janitor.8.moved'
1681=== renamed file 'computer-janitor' => 'computer-janitor.moved'
1682=== added directory 'computerjanitorapp'
1683=== renamed directory 'computerjanitorapp' => 'computerjanitorapp.moved'
1684=== added file 'computerjanitorapp/__init__.py'
1685--- computerjanitorapp/__init__.py 1970-01-01 00:00:00 +0000
1686+++ computerjanitorapp/__init__.py 2011-04-03 15:43:25 +0000
1687@@ -0,0 +1,44 @@
1688+# Copyright (C) 2008-2011 Canonical, Ltd.
1689+#
1690+# This program is free software: you can redistribute it and/or modify
1691+# it under the terms of the GNU General Public License as published by
1692+# the Free Software Foundation, version 3 of the License.
1693+#
1694+# This program is distributed in the hope that it will be useful,
1695+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1696+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1697+# GNU General Public License for more details.
1698+#
1699+# You should have received a copy of the GNU General Public License
1700+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1701+
1702+"""computerjanitorapp module."""
1703+
1704+from __future__ import absolute_import, unicode_literals
1705+
1706+__metaclass__ = type
1707+__all__ = [
1708+ '__version__',
1709+ 'setup_gettext',
1710+ ]
1711+
1712+
1713+__version__ = '2.1.0'
1714+
1715+
1716+# Set up gettext.
1717+def setup_gettext():
1718+ """Set up gettext for a module.
1719+
1720+ Return a method to be used for looking up translations. Usage:
1721+
1722+ >>> import computerjanitorapp
1723+ >>> _ = computerjanitorapp.setup_gettext()
1724+ """
1725+ import gettext
1726+ import os
1727+
1728+ domain = 'computerjanitor'
1729+ localedir = os.environ.get('LOCPATH', None)
1730+ t = gettext.translation(domain, localedir=localedir, fallback=True)
1731+ return t.ugettext
1732
1733=== added directory 'computerjanitorapp/cli'
1734=== added file 'computerjanitorapp/cli/__init__.py'
1735=== added file 'computerjanitorapp/cli/main.py'
1736--- computerjanitorapp/cli/main.py 1970-01-01 00:00:00 +0000
1737+++ computerjanitorapp/cli/main.py 2011-04-03 15:43:25 +0000
1738@@ -0,0 +1,312 @@
1739+# Copyright (C) 2008-2011 Canonical, Ltd.
1740+#
1741+# This program is free software: you can redistribute it and/or modify
1742+# it under the terms of the GNU General Public License as published by
1743+# the Free Software Foundation, version 3 of the License.
1744+#
1745+# This program is distributed in the hope that it will be useful,
1746+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1747+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1748+# GNU General Public License for more details.
1749+#
1750+# You should have received a copy of the GNU General Public License
1751+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1752+
1753+"""Command line user interface."""
1754+
1755+from __future__ import absolute_import, unicode_literals
1756+
1757+__metaclass__ = type
1758+__all__ = [
1759+ 'main',
1760+ ]
1761+
1762+
1763+import sys
1764+import dbus
1765+import gobject
1766+import argparse
1767+import textwrap
1768+
1769+from dbus.mainloop.glib import DBusGMainLoop
1770+from operator import mod
1771+
1772+from computerjanitorapp import __version__, setup_gettext
1773+from computerjanitorapp.terminalsize import get_terminal_size
1774+from computerjanitorapp.utilities import format_size
1775+from computerjanitord.service import DBUS_INTERFACE_NAME
1776+_ = setup_gettext()
1777+
1778+
1779+class Options:
1780+ """Command line option parser."""
1781+ def __init__(self, runner):
1782+ """Parse the command line options.
1783+
1784+ :param runner: The class implementing the sub-commands.
1785+ :type runner: `Runner`
1786+ """
1787+ self.runner = runner
1788+ # Parse the arguments.
1789+ self.parser = argparse.ArgumentParser(
1790+ description=_("""\
1791+ Find and remove cruft from your system.
1792+
1793+ Cruft is anything that shouldn't be on your system, but is.
1794+ Stretching the definition, cruft is also things that should be on
1795+ your system but aren't."""))
1796+ self.parser.add_argument(
1797+ '--version', action='version',
1798+ # XXX barry 2010-08-23: LP: #622720
1799+ version=mod(_('Computer Janitor %(version)s'),
1800+ dict(version=__version__)))
1801+ subparser = self.parser.add_subparsers(title='Commands')
1802+ # The 'find' subcommand.
1803+ command = subparser.add_parser(
1804+ 'find', help=_('Find and display all cruft found on your system.'))
1805+ command.add_argument(
1806+ '-v', '--verbose', action='store_true',
1807+ help=_("""\
1808+ Display a detailed explanation for each piece of cruft found."""))
1809+ command.add_argument(
1810+ '-i', '--ignored', action='store_true',
1811+ help=_('Find and display only the ignored cruft.'))
1812+ command.add_argument(
1813+ '-r', '--removable', action='store_true',
1814+ help=_('Find and display only the removable cruft.'))
1815+ command.add_argument(
1816+ '-s', '--short', action='store_true',
1817+ help=_('Display only the package names. Do not use with -v.'))
1818+ command.set_defaults(func=self.runner.find)
1819+ # The 'ignore' subcommand.
1820+ command = subparser.add_parser(
1821+ 'ignore', help=_("""\
1822+ Ignore a piece of cruft so that it is not cleaned up."""))
1823+ command.add_argument(
1824+ 'cruft', nargs=1,
1825+ help=_('The name of the cruft to ignore.'))
1826+ command.set_defaults(func=self.runner.ignore)
1827+ # The 'unignore' subcommand.
1828+ command = subparser.add_parser(
1829+ 'unignore', help=_("""\
1830+ Unignore a piece of cruft so that it will be cleaned up."""))
1831+ command.add_argument(
1832+ 'cruft', nargs=1,
1833+ help=_('The name of the cruft to unignore.'))
1834+ command.set_defaults(func=self.runner.unignore)
1835+ # The 'clean' subcommand.
1836+ command = subparser.add_parser(
1837+ 'clean',
1838+ help=_('Remove the selected cruft from the system.'))
1839+ command.add_argument(
1840+ '-a', '--all', action='store_true',
1841+ help=_('Clean up all unignored cruft.'))
1842+ command.add_argument(
1843+ '-v', '--verbose', action='store_true',
1844+ help=_("Provide more details on what's being cleaned."))
1845+ command.add_argument(
1846+ 'cruft', nargs='?',
1847+ help=_("""\
1848+ The name of the cruft to clean up. Do not use if specifying
1849+ --all."""))
1850+ command.set_defaults(func=self.runner.clean)
1851+ # Parse the arguments and execute the subcommand.
1852+ self.arguments = self.parser.parse_args()
1853+
1854+
1855+class Runner:
1856+ """Implementations of subcommands."""
1857+
1858+ def __init__(self):
1859+ # Connect to the dbus service.
1860+ system_bus = dbus.SystemBus()
1861+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
1862+ self.janitord = dbus.Interface(
1863+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
1864+ # Connect to the signal the server will emit when cleaning up.
1865+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
1866+ # This will get backpatched by __main__. We need it to produce error
1867+ # messages from the argparser.
1868+ self.options = None
1869+ # The main loop for asynchronous calls and signal reception.
1870+ self.loop = gobject.MainLoop()
1871+
1872+ def _error(self, message):
1873+ """Generate a parser error and exit.
1874+
1875+ :param message: The error message.
1876+ :type message: string
1877+ """
1878+ self.options.parser.error(message)
1879+ # No return.
1880+
1881+ def find(self, arguments):
1882+ """Find and display all cruft.
1883+
1884+ :param arguments: Command line options.
1885+ """
1886+ # Cruft will be prefixed by 'removable' if it is not being ignored.
1887+ ignored = set(self.janitord.ignored())
1888+ cruft_names = set(self.janitord.find())
1889+ # Filter names based on option flags.
1890+ if arguments.ignored:
1891+ cruft = sorted(cruft_names & ignored)
1892+ elif arguments.removable:
1893+ cruft = sorted(cruft_names - ignored)
1894+ else:
1895+ cruft = sorted(cruft_names)
1896+ # The prefix will either be 'ignored' or 'removable' however this
1897+ # string will be translated, so calculate the prefix size in the
1898+ # native language, then add two columns of separator, followed by the
1899+ # cruft name.
1900+ prefixi = _('ignored')
1901+ prefixr = _('removable')
1902+ prefix_width = max(len(prefixi), len(prefixr))
1903+ # Long, short, shorter display.
1904+ if arguments.verbose and arguments.short:
1905+ self._error('Use either -s or -v but not both.')
1906+ if arguments.short:
1907+ # This is the shorter output.
1908+ for name in cruft:
1909+ print name
1910+ elif not arguments.verbose:
1911+ # This is the short output.
1912+ for name in cruft:
1913+ prefix = (prefixi if name in ignored else prefixr)
1914+ # 10 spaces for the prefix
1915+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
1916+ else:
1917+ # This is the verbose output. Start by getting the terminal's
1918+ # size, though all we care about is the width.
1919+ rows, columns = get_terminal_size()
1920+ width = ((80 if columns in (None, 0) else columns)
1921+ - prefix_width # space for prefix
1922+ - 2 # separator
1923+ - 1 # avoid last column, some terminals wrap
1924+ )
1925+ margin = ' ' * (prefix_width + 2)
1926+ for name in cruft:
1927+ prefix = (prefixi if name in ignored else prefixr)
1928+ # 10 spaces for the prefix
1929+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
1930+ # Print some details about the cruft.
1931+ cruft_type, disk_usage = self.janitord.get_details(name)
1932+ print '{0}{1} of type {2}'.format(
1933+ margin, format_size(disk_usage), cruft_type)
1934+ # Get the description, wrap it to the available columns, and
1935+ # display it line-by-line with the proper amount of leading
1936+ # spaces (prefix_width + 2).
1937+ description = self.janitord.get_description(name)
1938+ if len(description) == 0:
1939+ print
1940+ continue
1941+ paragraphs = description.split('\n\n')
1942+ for paragraph in paragraphs:
1943+ for line in textwrap.wrap(paragraph, width):
1944+ # 2010-02-09 barry: The original code forced the
1945+ # output to utf-8, claiming that was necessary to
1946+ # "write out stuff even when it's going to somewhere
1947+ # like a pipe [...] ignor[ing] the user's desired
1948+ # charset, which is bad, bad, bad...". This makes me
1949+ # pretty uncomfortable, so I'd like to see a bug
1950+ # report before I copy that from the previous
1951+ # implementation.
1952+ print '{0}{1}'.format(margin, line)
1953+ # Paragraph separator.
1954+ print
1955+
1956+ def ignore(self, arguments):
1957+ """Ignore some cruft.
1958+
1959+ :param arguments: Command line options.
1960+ """
1961+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1962+ self.janitord.ignore(arguments.cruft[0])
1963+ self.janitord.save()
1964+
1965+ def unignore(self, arguments):
1966+ """Unignore some cruft.
1967+
1968+ :param arguments: Command line options.
1969+ """
1970+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1971+ self.janitord.unignore(arguments.cruft[0])
1972+ self.janitord.save()
1973+
1974+ def _clean_reply(self):
1975+ """The 'clean' operation has completed successfully."""
1976+ self.loop.quit()
1977+ print 'done.'
1978+
1979+ def _clean_error(self, exception):
1980+ """The 'clean' operation has failed."""
1981+ self.loop.quit()
1982+ print 'dbus service error:', exception
1983+
1984+ def _clean_working(self, cruft):
1985+ """The 'clean' operation is in progress.
1986+
1987+ :param cruft: The cruft that is being cleaned up.
1988+ :type cruft: string
1989+ """
1990+ verbose = self.options.arguments.verbose
1991+ if cruft == '':
1992+ # We're done.
1993+ if not verbose:
1994+ sys.stdout.write(' ')
1995+ sys.stdout.flush()
1996+ else:
1997+ if verbose:
1998+ print 'Working on', cruft
1999+ else:
2000+ sys.stdout.write('.')
2001+ sys.stdout.flush()
2002+
2003+ def clean(self, arguments):
2004+ """Clean up the cruft.
2005+
2006+ :param arguments: Command line options.
2007+ """
2008+ if arguments.all:
2009+ # You can't specify both --all and a cruft name.
2010+ if arguments.cruft is not None:
2011+ self._error('Specify a cruft name or --all, but not both')
2012+ # No return.
2013+ all_cruft = set(self.janitord.find())
2014+ ignored = set(self.janitord.ignored())
2015+ cleanable_cruft = tuple(all_cruft - ignored)
2016+ else:
2017+ if arguments.cruft is None:
2018+ self._error('You must specify a cruft name, or use --all')
2019+ # No return.
2020+ cleanable_cruft = (arguments.cruft,)
2021+ # Make the asynchronous call because this can take a long time. We'll
2022+ # get status updates periodically. Note however that even though this
2023+ # is asynchronous, dbus still expects a response within a certain
2024+ # amount of time. We have no idea how long it will take to clean up
2025+ # the cruft though, so just crank the timeout up to some insanely huge
2026+ # number (of seconds).
2027+ self.janitord.clean(cleanable_cruft,
2028+ reply_handler=self._clean_reply,
2029+ error_handler=self._clean_error,
2030+ # If it takes longer than an hour, we're screwed.
2031+ timeout=3600)
2032+ # Start the main loop. This will exit when the remote operation is
2033+ # complete.
2034+ if not arguments.verbose:
2035+ sys.stdout.write('processing')
2036+ sys.stdout.flush()
2037+ self.loop.run()
2038+
2039+
2040+def main():
2041+ # We'll need a main loop to receive status signals from the dbus service.
2042+ # Don't start the main loop yet though, since we only need it for 'clean'
2043+ # commands.
2044+ DBusGMainLoop(set_as_default=True)
2045+ runner = Runner()
2046+ options = Options(runner)
2047+ # Backpatch runner because of circular references.
2048+ runner.options = options
2049+ # Execute the subcommand.
2050+ options.arguments.func(options.arguments)
2051
2052=== added directory 'computerjanitorapp/gtk'
2053=== added file 'computerjanitorapp/gtk/__init__.py'
2054=== added file 'computerjanitorapp/gtk/dialogs.py'
2055--- computerjanitorapp/gtk/dialogs.py 1970-01-01 00:00:00 +0000
2056+++ computerjanitorapp/gtk/dialogs.py 2011-04-03 15:43:25 +0000
2057@@ -0,0 +1,125 @@
2058+# Copyright (C) 2008-2011 Canonical, Ltd.
2059+#
2060+# This program is free software: you can redistribute it and/or modify
2061+# it under the terms of the GNU General Public License as published by
2062+# the Free Software Foundation, version 3 of the License.
2063+#
2064+# This program is distributed in the hope that it will be useful,
2065+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2066+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2067+# GNU General Public License for more details.
2068+#
2069+# You should have received a copy of the GNU General Public License
2070+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2071+
2072+"""Confirm some actions."""
2073+
2074+from __future__ import absolute_import, unicode_literals
2075+
2076+__metaclass__ = type
2077+__all__ = [
2078+ 'AreYouSure',
2079+ 'CleanupProblem',
2080+ ]
2081+
2082+
2083+from computerjanitorapp import setup_gettext
2084+from gi.repository import Gtk
2085+from operator import mod as interpolate
2086+
2087+
2088+_ = setup_gettext()
2089+NL = '\n'
2090+
2091+
2092+class DialogBase:
2093+ """Base class for dialogs."""
2094+
2095+ def __init__(self, ui):
2096+ self._ui = ui
2097+
2098+
2099+class AreYouSure(DialogBase):
2100+ """Confirmation of destructive actions."""
2101+
2102+ def verify(self):
2103+ """Confirm package removal."""
2104+ # Start by getting all the active, non-ignored cruft. These are the
2105+ # candidates for removal.
2106+ cleanable_cruft = self._ui.get_cleanable_cruft()
2107+ package_cruft_count = sum(1 for cruft_name, is_pkg in cleanable_cruft
2108+ if is_pkg)
2109+ other_cruft_count = len(cleanable_cruft) - package_cruft_count
2110+ messages = []
2111+ ok_button = None
2112+ # XXX vv barry 2010-08-23: LP: #622720
2113+ if package_cruft_count > 0:
2114+ messages = [
2115+ _('<b>Software packages to remove: %(packages)s</b>.'),
2116+ _('\nRemoving packages that are still in use can '
2117+ 'cause errors.'),
2118+ ]
2119+ ok_button = _('Remove packages')
2120+ if other_cruft_count > 0:
2121+ messages.insert(1, _('Non-package items to remove: %(others)s.'))
2122+ ok_button = _('Clean up')
2123+ secondary_message = interpolate(NL.join(messages),
2124+ dict(packages=package_cruft_count,
2125+ others=other_cruft_count))
2126+ # XXX ^^ barry 2010-08-23: LP: #622720
2127+ if ok_button is None:
2128+ # The user de-selected all cruft from the ui, so there's actually
2129+ # nothing to clean up.
2130+ dialog = Gtk.MessageDialog(
2131+ parent=self._ui.widgets['window'],
2132+ type=Gtk.MessageType.WARNING,
2133+ buttons=Gtk.ButtonsType.NONE,
2134+ message_format=_('There is nothing to clean up'),
2135+ )
2136+ dialog.set_title(_('Clean up'))
2137+ dialog.add_button(_('Ok'), Gtk.ResponseType.YES)
2138+ dialog.show_all()
2139+ dialog.run()
2140+ dialog.hide()
2141+ return False
2142+ # It would be nice if we could produce better messages than these,
2143+ # but that would require a richer interface to the dbus service,
2144+ # and probably to the cruft plugin architecture underneath that.
2145+ message = _('Are you sure you want to clean your system?')
2146+ dialog = Gtk.MessageDialog(
2147+ parent=self._ui.widgets['window'],
2148+ type=Gtk.MessageType.WARNING,
2149+ buttons=Gtk.ButtonsType.NONE,
2150+ message_format=message)
2151+ dialog.set_title(_('Clean up'))
2152+ dialog.format_secondary_markup(secondary_message)
2153+ dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CLOSE)
2154+ dialog.add_button(ok_button, Gtk.ResponseType.YES)
2155+ # Show the dialog and get the user's response.
2156+ dialog.show_all()
2157+ response = dialog.run()
2158+ dialog.hide()
2159+ return response == Gtk.ResponseType.YES
2160+
2161+
2162+class CleanupProblem(DialogBase):
2163+ """Some problem occurred during cleanup.
2164+
2165+ This is almost definitely caused by another package manager running at the
2166+ same time.
2167+ """
2168+
2169+ def run(self):
2170+ """Show the dialog and wait for confirmation."""
2171+ message = _('System clean up could not complete. '
2172+ 'Be sure no other package manager such as Synaptic or '
2173+ 'Update Manager is running.')
2174+ dialog = Gtk.MessageDialog(
2175+ parent=self._ui.widgets['window'],
2176+ type=Gtk.MessageType.ERROR,
2177+ buttons=Gtk.ButtonsType.OK,
2178+ message_format=message)
2179+ dialog.set_title(_('System clean up failure'))
2180+ dialog.show_all()
2181+ dialog.run()
2182+ dialog.hide()
2183
2184=== added file 'computerjanitorapp/gtk/main.py'
2185--- computerjanitorapp/gtk/main.py 1970-01-01 00:00:00 +0000
2186+++ computerjanitorapp/gtk/main.py 2011-04-03 15:43:25 +0000
2187@@ -0,0 +1,32 @@
2188+# Copyright (C) 2008-2011 Canonical, Ltd.
2189+#
2190+# This program is free software: you can redistribute it and/or modify
2191+# it under the terms of the GNU General Public License as published by
2192+# the Free Software Foundation, version 3 of the License.
2193+#
2194+# This program is distributed in the hope that it will be useful,
2195+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2196+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2197+# GNU General Public License for more details.
2198+#
2199+# You should have received a copy of the GNU General Public License
2200+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2201+
2202+"""Command line user interface."""
2203+
2204+from __future__ import absolute_import, unicode_literals
2205+
2206+__metaclass__ = type
2207+__all__ = [
2208+ 'main',
2209+ ]
2210+
2211+
2212+from computerjanitorapp.gtk.ui import UserInterface
2213+from dbus.mainloop.glib import DBusGMainLoop
2214+
2215+
2216+def main():
2217+ DBusGMainLoop(set_as_default=True)
2218+ ui = UserInterface()
2219+ ui.run()
2220
2221=== added file 'computerjanitorapp/gtk/store.py'
2222--- computerjanitorapp/gtk/store.py 1970-01-01 00:00:00 +0000
2223+++ computerjanitorapp/gtk/store.py 2011-04-03 15:43:25 +0000
2224@@ -0,0 +1,162 @@
2225+# Copyright (C) 2008-2011 Canonical, Ltd.
2226+#
2227+# This program is free software: you can redistribute it and/or modify
2228+# it under the terms of the GNU General Public License as published by
2229+# the Free Software Foundation, version 3 of the License.
2230+#
2231+# This program is distributed in the hope that it will be useful,
2232+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2233+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2234+# GNU General Public License for more details.
2235+#
2236+# You should have received a copy of the GNU General Public License
2237+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2238+
2239+"""Gtk ListStore model for backing the TreeViews of cruft."""
2240+
2241+from __future__ import absolute_import, unicode_literals
2242+
2243+__metaclass__ = type
2244+__all__ = [
2245+ 'Store',
2246+ 'optimize',
2247+ 'unused',
2248+ ]
2249+
2250+
2251+import gobject
2252+
2253+from gi.repository import Gtk
2254+
2255+
2256+# XXX 2010-03-04 barry: Use a munepy enum.
2257+class ListStoreColumns:
2258+ # The cruft name as known by the dbus service.
2259+ name = 0
2260+ # The short name of the cruft.
2261+ short_name = 1
2262+ # The text displayed in the cell.
2263+ text = 2
2264+ # Should the cruft be shown?
2265+ show = 3
2266+ # Is the cruft active locally (i.e. toggle button is set)?
2267+ active = 4
2268+ # Is the cruft being ignored in the dbus service?
2269+ server_ignored = 5
2270+ # Is the cruft's description expanded?
2271+ expanded = 6
2272+ # Is this cruft package cruft or some other kind of cruft?
2273+ is_package_cruft = 7
2274+
2275+
2276+class Store:
2277+ """The higher level wrapper around the Gtk.ListStore."""
2278+
2279+ def __init__(self, janitord):
2280+ """Create the Store.
2281+
2282+ :param janitord: The `dbus.Interface` for talking to the Computer
2283+ Janitor dbus service.
2284+ """
2285+ self.janitord = janitord
2286+ # This ListStore is the backing data structure for the TreeViews in
2287+ # the main window. It holds information about the individual pieces
2288+ # of cruft. See ListStoreColumns for details.
2289+ #
2290+ # XXX 2010-03-04 barry: pygtk does not like 'unicode'.
2291+ self.store = Gtk.ListStore(str, str, str, bool, bool, bool, bool, bool)
2292+
2293+ def find_cruft(self):
2294+ """Find cruft and populate the backing store.
2295+
2296+ This actually just asks the backend dbus service to find all the cruft
2297+ asynchronously. When the dbus service completes the task, it sends a
2298+ `find_finished` signal with the new list of cruft. Upon receipt of
2299+ that signal, the ListStore is updated.
2300+ """
2301+ self.janitord.connect_to_signal('find_finished', self.on_find_finished)
2302+ self.janitord.find_async()
2303+
2304+ def on_find_finished(self, all_cruft_names):
2305+ """dbus signal handler.
2306+
2307+ This actually updates the store with the new cruft results.
2308+ """
2309+ ignored_cruft = set(self.janitord.ignored())
2310+ all_cruft_names = set(all_cruft_names);
2311+ pkg_cruft_names = set(
2312+ cruft_name for cruft_name in all_cruft_names
2313+ if self.janitord.get_details(cruft_name)[0].lower()
2314+ == 'packagecruft')
2315+ self.store.clear()
2316+ # Cruft always starts out in the active, not-expanded state. Package
2317+ # cruft goes in the 'unused' column while non-package cruft goes in
2318+ # the 'optimize' column.
2319+ for cruft_name in all_cruft_names:
2320+ cruft_shortname = self.janitord.get_shortname(cruft_name)
2321+ self.store.append((
2322+ cruft_name, cruft_shortname,
2323+ # What is being displayed in the cell.
2324+ gobject.markup_escape_text(cruft_shortname),
2325+ # Show the cruft be shown?
2326+ cruft_name not in ignored_cruft,
2327+ # Is the cruft active locally?
2328+ cruft_name not in ignored_cruft,
2329+ # Is the cruft ignored in the dbus service?
2330+ cruft_name in ignored_cruft,
2331+ # Is the cruft's description expanded?
2332+ False,
2333+ # Is this package cruft?
2334+ cruft_name in pkg_cruft_names,
2335+ ))
2336+
2337+ # Forwards to the underlying store, for convenience. Really, these should
2338+ # use generic delegates, or clients should just use self.store.store.
2339+
2340+ def get_value(self, *args, **kws):
2341+ return self.store.get_value(*args, **kws)
2342+
2343+ def set_value(self, *args, **kws):
2344+ return self.store.set_value(*args, **kws)
2345+
2346+ def filter_new(self, *args, **kws):
2347+ return self.store.filter_new(*args, **kws)
2348+
2349+ def foreach(self, *args, **kws):
2350+ return self.store.foreach(*args, **kws)
2351+
2352+ def reorder(self, *args, **kws):
2353+ return self.store.reorder(*args, **kws)
2354+
2355+ def get_iter_first(self, *args, **kws):
2356+ return self.store.get_iter_first(*args, **kws)
2357+
2358+ def iter_next(self, *args, **kws):
2359+ return self.store.iter_next(*args, **kws)
2360+
2361+ def clear(self, *args, **kws):
2362+ return self.store.clear(*args, **kws)
2363+
2364+
2365+# Filter functions for use with TreeView column display.
2366+
2367+def unused(store, iter, data):
2368+ """True if the cruft is not being ignored and is package cruft.
2369+
2370+ :param store: The ListStore instance.
2371+ :param iter: The TreeIter instance.
2372+ """
2373+ show = store.get_value(iter, ListStoreColumns.show)
2374+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
2375+ return show and is_package_cruft
2376+
2377+
2378+def optimize(store, iter, data):
2379+ """True if the cruft is not package cruft.
2380+
2381+ :param store: The ListStore instance.
2382+ :param iter: The TreeIter instance.
2383+ """
2384+ show = store.get_value(iter, ListStoreColumns.show)
2385+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
2386+ return show and not is_package_cruft
2387
2388=== added file 'computerjanitorapp/gtk/ui.py'
2389--- computerjanitorapp/gtk/ui.py 1970-01-01 00:00:00 +0000
2390+++ computerjanitorapp/gtk/ui.py 2011-04-03 15:43:25 +0000
2391@@ -0,0 +1,596 @@
2392+# Copyright (C) 2008-2011 Canonical, Ltd.
2393+#
2394+# This program is free software: you can redistribute it and/or modify
2395+# it under the terms of the GNU General Public License as published by
2396+# the Free Software Foundation, version 3 of the License.
2397+#
2398+# This program is distributed in the hope that it will be useful,
2399+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2400+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2401+# GNU General Public License for more details.
2402+#
2403+# You should have received a copy of the GNU General Public License
2404+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2405+
2406+"""Gtk user interface for computer janitor."""
2407+
2408+from __future__ import absolute_import, unicode_literals
2409+
2410+__metaclass__ = type
2411+__all__ = [
2412+ 'UserInterface',
2413+ ]
2414+
2415+
2416+import os
2417+import dbus
2418+import glib
2419+import gobject
2420+import logging
2421+
2422+from operator import mod
2423+
2424+import gi
2425+gi.require_version('Gtk', '2.0')
2426+
2427+from gi.repository import Gtk, Pango
2428+
2429+from computerjanitorapp import __version__, setup_gettext
2430+from computerjanitorapp.gtk.dialogs import AreYouSure, CleanupProblem
2431+from computerjanitorapp.gtk.store import (
2432+ ListStoreColumns, Store, optimize, unused)
2433+from computerjanitorapp.utilities import format_size
2434+from computerjanitord.service import DBUS_INTERFACE_NAME
2435+
2436+
2437+log = logging.getLogger('computerjanitor')
2438+_ = setup_gettext()
2439+
2440+
2441+GLADE = '/usr/share/computer-janitor/ComputerJanitor.ui'
2442+ROOT_WIDTH = 900
2443+ROOT_HEIGHT = 500
2444+NL = '\n'
2445+
2446+# Keys are lower-cased cruft types, i.e. class name of cruft instances.
2447+ACTIONS = dict(
2448+ packagecruft=_('Package will be <b>removed</b>.'),
2449+ filecruft=_('Package will be <b>installed</b>.'),
2450+ missingpackagecruft=_('File will be <b>removed</b>.'),
2451+ )
2452+
2453+SENSITIVE_WIDGETS = (
2454+ 'do_button',
2455+ 'optimize_treeview',
2456+ 'quit_menuitem',
2457+ 'show_previously_ignored',
2458+ 'sort_by_name',
2459+ 'sort_by_size',
2460+ 'unused_treeview',
2461+ )
2462+
2463+
2464+class UserInterface:
2465+ """Implementation of the Gtk user interface."""
2466+
2467+ def __init__(self):
2468+ # Connect to the dbus service.
2469+ system_bus = dbus.SystemBus()
2470+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
2471+ self.janitord = dbus.Interface(
2472+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
2473+ # Create the model on which the TreeViews will be based.
2474+ self.store = Store(self.janitord)
2475+ self.popup_menus = {}
2476+ self.cruft_name_columns = set()
2477+ # Sort by name by default.
2478+ self._sort_key = self._by_name
2479+ # Work is happening asynchronously on the dbus service.
2480+ self.working = False
2481+ # Connect to the signal the server will emit when cleaning up.
2482+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
2483+
2484+ def run(self):
2485+ """Set up the widgets and run the main loop."""
2486+ builder = Gtk.Builder()
2487+ builder.set_translation_domain('computerjanitor')
2488+ # Load the glade ui file, which can be overridden from the environment
2489+ # for testing purposes.
2490+ glade_file = os.environ.get('COMPUTER_JANITOR_GLADE', GLADE)
2491+ builder.add_from_file(glade_file)
2492+ # Bind widgets to callbacks. Initialize the Do... button to
2493+ # insensitive, but possibly toggle it back to sensitive if we find
2494+ # cruft.
2495+ self.find_and_bind_widgets(builder)
2496+ self.widgets['do_button'].set_sensitive(False)
2497+ self.janitord.connect_to_signal('find_finished', self._find_finished)
2498+ # Do the initial search for cruft and set up the TreeView model.
2499+ self.store.find_cruft()
2500+ self.sort_cruft()
2501+ # Now hook the TreeViews up to there view of the model.
2502+ self.create_column('unused_treeview', unused)
2503+ self.create_column('optimize_treeview', optimize)
2504+ # Set the dimensions of the root window.
2505+ root = self.widgets['window']
2506+ root.set_default_size(ROOT_WIDTH, ROOT_HEIGHT)
2507+ # Map the root window and go!
2508+ root.show()
2509+ Gtk.main()
2510+
2511+ def find_and_bind_widgets(self, builder):
2512+ """Bind widgets and callbacks."""
2513+ # Start by extracting all the bindable widgets from the ui builder,
2514+ # keeping track of them as mapped to their name.
2515+ self.widgets = {}
2516+ for ui_object in builder.get_objects():
2517+ if issubclass(type(ui_object), Gtk.Buildable):
2518+ widget_name = Gtk.Buildable.get_name(ui_object)
2519+ self.widgets[widget_name] = ui_object
2520+ # Search through the attributes of this instance looking for
2521+ # callbacks for this widget. We use the naming convention
2522+ # 'on_<widget>_<signal>' for such methods. Both the widget
2523+ # name and signal (or event) can contain underscores. Connect
2524+ # the widget to the callback.
2525+ prefix = 'on_{0}_'.format(widget_name)
2526+ for method_name in dir(self):
2527+ if method_name.startswith(prefix):
2528+ signal_name = method_name[len(prefix):]
2529+ ui_object.connect(
2530+ signal_name, getattr(self, method_name))
2531+
2532+ def create_column(self, widget_name, filter_func):
2533+ """Set up a column in the named TreeView."""
2534+ treeview = self.widgets[widget_name]
2535+ # XXX 2010-03-04 barry: This is a bit of an abuse because it's
2536+ # supposed to specify whether users are to read across the rows. As a
2537+ # side effect it renders the columns with alternating row colors, but
2538+ # that's not it's primary function.
2539+ treeview.set_rules_hint(True)
2540+ # Each TreeView contains two columns. The leftmost one is a toggle
2541+ # that when select tells c-j to act on that cruft. Deselecting the
2542+ # toggle ignores the package for next time.
2543+ toggle_cr = Gtk.CellRendererToggle()
2544+ toggle_cr.connect('toggled', self._toggled, treeview)
2545+ toggle_cr.set_property('yalign', 0)
2546+ toggle_col = Gtk.TreeViewColumn()
2547+ toggle_col.pack_start(toggle_cr, True)
2548+ toggle_col.add_attribute(toggle_cr, 'active', ListStoreColumns.active)
2549+ treeview.append_column(toggle_col)
2550+ # The rightmost column contains the details of the cruft. It will
2551+ # always contain the cruft name and can be expanded to display cruft
2552+ # details. Tell the column to get its toggle's active state from the
2553+ # model.
2554+ name_cr = Gtk.CellRendererText()
2555+ name_cr.set_property('yalign', 0)
2556+ name_cr.set_property('wrap-mode', Pango.WrapMode.WORD)
2557+ name_col = Gtk.TreeViewColumn()
2558+ name_col.pack_start(name_cr, True)
2559+ name_col.add_attribute(name_cr, 'markup', ListStoreColumns.text)
2560+ treeview.append_column(name_col)
2561+ self.cruft_name_columns.add(name_col)
2562+ # The individual crufts may or may not be visible in this TreeView.
2563+ # It's the filter function that controls this, so set that now.
2564+ filter_store = self.store.filter_new(None)
2565+ filter_store.set_visible_func(filter_func, None)
2566+ treeview.set_model(filter_store)
2567+ # Each TreeView has a popup menu for select or deselecting all visible
2568+ # cruft.
2569+ self.create_popup_menu_for_treeview(treeview)
2570+
2571+ def create_popup_menu_for_treeview(self, treeview):
2572+ """The tree views have a popup menu to select/deselect everything.
2573+
2574+ :param treeview: The `TreeView` to attach the menu to.
2575+ """
2576+ select_all = Gtk.MenuItem(label='Select all')
2577+ select_all.connect('activate', self.popup_menu_select_all, treeview)
2578+ unselect_all = Gtk.MenuItem(label='Unselect all')
2579+ unselect_all.connect('activate',
2580+ self.popup_menu_unselect_all, treeview)
2581+ menu = Gtk.Menu()
2582+ menu.append(select_all)
2583+ menu.append(unselect_all)
2584+ menu.show_all()
2585+ self.popup_menus[treeview] = menu
2586+
2587+ def _by_name(self, cruft_name):
2588+ """Sort by cruft name."""
2589+ return cruft_name
2590+
2591+ def _by_size(self, cruft_name):
2592+ """Sort by cruft size, from largest to smallest."""
2593+ # Return negative size to sort from largest to smallest.
2594+ return -self.janitord.get_details(cruft_name)[1]
2595+
2596+ def sort_cruft(self):
2597+ """Sort the cruft displays, either by name or size."""
2598+ # The way reordering (not technically 'sorting') works in gtk is that
2599+ # you give a list of integer indexes to the the ListStore. These
2600+ # indexes are in sorted order, and refer to the pre-sort indexes of
2601+ # the items in the store. IOW, the ListStore knows that if the first
2602+ # integer in the list is 7, it will move the 7th item to the top.
2603+ #
2604+ # Start by getting the indexes and names of the currenly sorted cruft.
2605+ # We'll fill this list with 2-tuples of the format:
2606+ # (sort-key, current-index).
2607+ cruft_data = []
2608+ def get(model, path, iter, crufts):
2609+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2610+ crufts.append((self._sort_key(cruft_name), len(crufts)))
2611+ # Continue iterating.
2612+ return False
2613+ self.store.foreach(get, cruft_data)
2614+ cruft_data.sort()
2615+ cruft_indexes = [index for key, index in cruft_data]
2616+ # No need to do anything if there is no cruft.
2617+ if len(cruft_indexes) > 0:
2618+ self.store.reorder(cruft_indexes)
2619+
2620+ def get_cleanable_cruft(self):
2621+ """Return the list of cleanable cruft candidates.
2622+
2623+ :return: List of cleanable cruft.
2624+ :rtype: list of 2-tuples of (cruft_name, is_package_cruft)
2625+ """
2626+ cleanable_cruft = []
2627+ def collect(model, path, iter, crufts):
2628+ # Only clean up active cruft, i.e. those that are specifically
2629+ # checked as ready for cleaning.
2630+ cruft_active = model.get_value(iter, ListStoreColumns.active)
2631+ if cruft_active:
2632+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2633+ cruft_is_package_cruft = model.get_value(
2634+ iter, ListStoreColumns.is_package_cruft)
2635+ crufts.append((cruft_name, cruft_is_package_cruft))
2636+ # Continue iterating.
2637+ return False
2638+ self.store.foreach(collect, cleanable_cruft)
2639+ return cleanable_cruft
2640+
2641+ def toggle_long_description(self, treeview):
2642+ """Toggle the currently selected cruft's long description.
2643+
2644+ :param treeview: The TreeView
2645+ """
2646+ selection = treeview.get_selection()
2647+ filter_model, selected = selection.get_selected()
2648+ if not selected:
2649+ return
2650+ model = filter_model.get_model()
2651+ iter = filter_model.convert_iter_to_child_iter(selected)
2652+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2653+ expanded = model.get_value(iter, ListStoreColumns.expanded)
2654+ shortname = model.get_value(iter, ListStoreColumns.short_name)
2655+ if expanded:
2656+ # Collapse it.
2657+ value = gobject.markup_escape_text(shortname)
2658+ else:
2659+ cruft_type, size = self.janitord.get_details(cruft_name)
2660+ lines = [gobject.markup_escape_text(shortname)]
2661+ action = ACTIONS.get(cruft_type.lower())
2662+ if action is not None:
2663+ lines.append(action)
2664+ # XXX barry 2010-08-23: LP: #622720
2665+ lines.append(mod(_('Size: %(bytes)s'),
2666+ dict(bytes=format_size(size))))
2667+ lines.append('')
2668+ description = self.janitord.get_description(cruft_name)
2669+ lines.append(gobject.markup_escape_text(description))
2670+ value = NL.join(lines)
2671+ model.set_value(iter, ListStoreColumns.text, value)
2672+ model.set_value(iter, ListStoreColumns.expanded, not expanded)
2673+
2674+ def desensitize(self):
2675+ """Make certain ui elements insensitive during work."""
2676+ for widget in SENSITIVE_WIDGETS:
2677+ self.widgets[widget].set_sensitive(False)
2678+
2679+ def sensitize(self):
2680+ """Make certain ui elements sensitive after work."""
2681+ for widget in SENSITIVE_WIDGETS:
2682+ self.widgets[widget].set_sensitive(True)
2683+
2684+ def _find_finished(self, all_cruft_names):
2685+ """dbus signal handler."""
2686+ self.widgets['do_button'].set_sensitive(len(all_cruft_names) > 0)
2687+
2688+ # Popup menu support.
2689+
2690+ def _treeview_foreach_set_set_state(self, treeview, enabled):
2691+ """Set the state of the cruft 'active' flag for all cruft.
2692+
2693+ :param treeview: The `TreeView` to set cruft state on.
2694+ :param enabled: The new state flag for all cruft. True means enabled.
2695+ :type enabled: bool
2696+ """
2697+ def set_state(model, path, iter, user_data):
2698+ # Set the state on an individual piece of cruft. Start by
2699+ # changing the state of the cruft on the dbus service.
2700+ child_iter = model.convert_iter_to_child_iter(iter)
2701+ cruft_name = self.store.get_value(
2702+ child_iter, ListStoreColumns.name)
2703+ if enabled:
2704+ self.janitord.unignore(cruft_name)
2705+ else:
2706+ self.janitord.ignore(cruft_name)
2707+ # Now set the active state in the model.
2708+ self.store.set_value(child_iter, ListStoreColumns.active, enabled)
2709+ treeview.get_model().foreach(set_state, None)
2710+ # Save the updated state on the dbus service.
2711+ self.janitord.save()
2712+
2713+ def popup_menu_select_all(self, menuitem, treeview):
2714+ self._treeview_foreach_set_set_state(treeview, True)
2715+
2716+ def popup_menu_unselect_all(self, menuitem, treeview):
2717+ self._treeview_foreach_set_set_state(treeview, False)
2718+
2719+ # Progress bar
2720+
2721+ def pulse(self):
2722+ """Progress bar callback, showing that something is happening."""
2723+ progress = self.widgets['progressbar_status']
2724+ if self.working:
2725+ progress.show()
2726+ progress.pulse()
2727+ return True
2728+ else:
2729+ # All done. Hide the progress bar, make the ui elements sensitive
2730+ # again, update the store, and kill the timer.
2731+ progress.hide()
2732+ self.store.clear()
2733+ self.store.find_cruft()
2734+ self.sensitize()
2735+ return False
2736+
2737+ def _clean_working(self, cruft):
2738+ """dbus signal handler; the 'clean' operation is in progress.
2739+
2740+ :param done: The cruft that is being cleaned up.
2741+ :type done: string
2742+ """
2743+ # Just mark the status here. The progress bar pulsar will handle
2744+ # doing the actual work.
2745+ self.working = (cruft != '')
2746+ if self.working:
2747+ self.widgets['progressbar_status'].set_text(
2748+ # XXX barry 2010-08-23: LP: #622720
2749+ mod(_('Processing %(cruft)s'), dict(cruft=cruft)))
2750+
2751+ # Callbacks
2752+
2753+ def _toggled(self, widget, path, treeview):
2754+ """Handle the toggle button in a TreeView cell.
2755+
2756+ :param widget: The CellRendererToggle
2757+ :param path: The cell's path.
2758+ :param treeview: The TreeView
2759+ """
2760+ # Find out which cruft's toggle was clicked.
2761+ model = treeview.get_model()
2762+ filter_iter = model.get_iter(path)
2763+ child_iter = model.convert_iter_to_child_iter(filter_iter)
2764+ cruft_name = self.store.get_value(child_iter, ListStoreColumns.name)
2765+ state = self.store.get_value(child_iter, ListStoreColumns.active)
2766+ # Toggle the current state.
2767+ new_state = not state
2768+ if new_state:
2769+ self.janitord.unignore(cruft_name)
2770+ else:
2771+ self.janitord.ignore(cruft_name)
2772+ self.store.set_value(child_iter, ListStoreColumns.active, new_state)
2773+ self.store.set_value(
2774+ child_iter, ListStoreColumns.server_ignored, not new_state)
2775+ # Save the new ignored state on the dbus service.
2776+ self.janitord.save()
2777+
2778+ # Signal and event handlers.
2779+
2780+ def on_quit_menuitem_activate(self, *args):
2781+ """Signal and event handlers for quitting.
2782+
2783+ Since we just want things to go away, we don't really care about the
2784+ arguments. Just tell the main loop to exit.
2785+ """
2786+ # Don't quit while we're working.
2787+ if self.working:
2788+ return True
2789+ Gtk.main_quit()
2790+
2791+ on_window_delete_event = on_quit_menuitem_activate
2792+
2793+ def treeview_button_press_event(self, treeview, event):
2794+ """Handle mouse button press events on the TreeView ourselves.
2795+
2796+ We handle mouse button presses ourselves so that we can either
2797+ toggle the long description (button 1, typically left) or
2798+ pop up a menu (button 3, typically right).
2799+ """
2800+ # Original comment: This is slightly tricky and probably a source of
2801+ # bugs. Oh well.
2802+ if event.button == 1:
2803+ # Left button event. Select the row being clicked on. If the
2804+ # click is on the cruft name, show or hide its long description.
2805+ # If the click the click is elsewhere do not handle it. This
2806+ # allows the toggle button event to be handled separately.
2807+ x = int(event.x)
2808+ y = int(event.y)
2809+ time = event.time
2810+ pathinfo = treeview.get_path_at_pos(x, y)
2811+ if pathinfo is None:
2812+ # The click was not in a cell, but we've handled it anyway.
2813+ return True
2814+ path, column, cell_x, cell_y = pathinfo
2815+ if column in self.cruft_name_columns:
2816+ treeview.set_cursor(path, column, False)
2817+ self.toggle_long_description(treeview)
2818+ return True
2819+ else:
2820+ # We are not handling this event so that the toggle button
2821+ # handling can occur.
2822+ return False
2823+ elif event.button == 3:
2824+ # Right button event. Pop up the select/deselect all menu.
2825+ treeview.grab_focus()
2826+ x = int(event.x)
2827+ y = int(event.y)
2828+ time = event.time
2829+ pathinfo = treeview.get_path_at_pos(x, y)
2830+ if pathinfo is not None:
2831+ path, column, cell_x, cell_y = pathinfo
2832+ treeview.set_cursor(path, column, False)
2833+ menu = self.popup_menus[treeview]
2834+ try:
2835+ menu.popup_for_device(None, None, None, None, None,
2836+ event.button, time)
2837+ except AttributeError:
2838+ # popup_for_device() is introspection safe, but only exists in
2839+ # GTK3. popup() isn't introspectable, so in GTK 2 we need to
2840+ # disable the popup menu functionality
2841+ log.warning('popup menu not supported when using GTK2')
2842+ return True
2843+ else:
2844+ # No other events are handled by us.
2845+ return False
2846+
2847+ # The actual event handler is totally generic. Alias it to names
2848+ # recognized by the automatic event binding scheme.
2849+ on_unused_treeview_button_press_event = treeview_button_press_event
2850+ on_optimize_treeview_button_press_event = treeview_button_press_event
2851+
2852+ def treeview_size_allocate(self, treeview, *args):
2853+ """Allocate space for the tree view and set wrap width.
2854+
2855+ :param treeview: The TreeView
2856+ :param args: Additional ignored positional arguments
2857+ """
2858+ # Get the rightmost of the two columns in the TreeView, i.e. the one
2859+ # containing the text.
2860+ column = treeview.get_column(1)
2861+ name_cr = column.get_cells()[0]
2862+ # Wrap to the entire width of the column.
2863+ width = column.get_width()
2864+ name_cr.set_property('wrap-width', width)
2865+
2866+ on_unused_treeview_size_allocate = treeview_size_allocate
2867+ on_optimize_treeview_size_allocate = treeview_size_allocate
2868+
2869+ def on_sort_by_name_toggled(self, menuitem):
2870+ """Reorder the crufts to be sorted by name or size."""
2871+ if menuitem.get_active():
2872+ self._sort_key = self._by_name
2873+ else:
2874+ self._sort_key = self._by_size
2875+ self.sort_cruft()
2876+
2877+ def on_about_menuitem_activate(self, *args):
2878+ dialog = self.widgets['about_dialog']
2879+ dialog.set_name(_('Computer Janitor'))
2880+ dialog.set_version(__version__)
2881+ dialog.show()
2882+ dialog.run()
2883+ dialog.hide()
2884+
2885+ def on_show_previously_ignored_toggled(self, menuitem):
2886+ """Show all cruft, even those being ignored.
2887+
2888+ Normally, we only show cruft that wasn't explicitly ignored. By
2889+ toggling this menu item, the janitor can also display cruft that is
2890+ marked as ignored on the dbus service.
2891+ """
2892+ show_ignored_cruft = menuitem.get_active()
2893+ iter = self.store.get_iter_first()
2894+ while iter:
2895+ server_ignored = self.store.get_value(
2896+ iter, ListStoreColumns.server_ignored)
2897+ show = (show_ignored_cruft or not server_ignored)
2898+ self.store.set_value(iter, ListStoreColumns.show, show)
2899+ iter = self.store.iter_next(iter)
2900+
2901+ def _edit_menuitem_set_select_state(self, enabled, *treeviews):
2902+ """Set the specified state on the selected treeviews.
2903+
2904+ :param enabled: The new state flag for all cruft. True means enabled.
2905+ :type enabled: bool
2906+ :param treeviews: Sequence of `TreeView` object to set cruft state on.
2907+ """
2908+ for treeview in treeviews:
2909+ self._treeview_foreach_set_set_state(treeview, enabled)
2910+
2911+ def on_select_all_cruft_activate(self, menuitem):
2912+ """Select all cruft, both package and other."""
2913+ self._edit_menuitem_set_select_state(
2914+ True,
2915+ self.widgets['unused_treeview'],
2916+ self.widgets['optimize_treeview'])
2917+
2918+ def on_select_all_packages_activate(self, menuitem):
2919+ """Select all cruft, both package and other."""
2920+ self._edit_menuitem_set_select_state(
2921+ True, self.widgets['unused_treeview'])
2922+
2923+ def on_select_all_other_activate(self, menuitem):
2924+ """Select all cruft, both package and other."""
2925+ self._edit_menuitem_set_select_state(
2926+ True, self.widgets['optimize_treeview'])
2927+
2928+ def on_deselect_all_cruft_activate(self, menuitem):
2929+ """Select all cruft, both package and other."""
2930+ self._edit_menuitem_set_select_state(
2931+ False,
2932+ self.widgets['unused_treeview'],
2933+ self.widgets['optimize_treeview'])
2934+
2935+ def on_deselect_all_packages_activate(self, menuitem):
2936+ """Select all cruft, both package and other."""
2937+ self._edit_menuitem_set_select_state(
2938+ False, self.widgets['unused_treeview'])
2939+
2940+ def on_deselect_all_other_activate(self, menuitem):
2941+ """Select all cruft, both package and other."""
2942+ self._edit_menuitem_set_select_state(
2943+ False, self.widgets['optimize_treeview'])
2944+
2945+ def on_do_button_clicked(self, *args):
2946+ """JFDI, well almost."""
2947+ self.count = 0
2948+ response = AreYouSure(self).verify()
2949+ if not response:
2950+ return
2951+ # This can take a long time. Make an asynchronous call to the dbus
2952+ # service and arrange for it to occasionally provide us with status.
2953+ # This isn't great ui, but OTOH, the package cruft cleaners themselves
2954+ # don't provide much granularity, so there's little we can do anyway
2955+ # without a major rewrite of the plugin architecture.
2956+ self.working = True
2957+ glib.timeout_add(150, self.pulse)
2958+ # Make various ui elements insensitive.
2959+ self.desensitize()
2960+ cleanable = [cruft for cruft, ispkg in self.get_cleanable_cruft()]
2961+ def error(exception):
2962+ # XXX 2010-05-19 barry: This will (almost?) always be caused by
2963+ # some other package manager already running, so if we get that
2964+ # exception, display a (hopefully) useful dialog. I'm not sure
2965+ # it's very helpful to display the low-level apt exception in a
2966+ # dialog. `exception` will always be a DBusException; we won't
2967+ # get the more useful PackageCleanupError.
2968+ #
2969+ # We use log.error() instead of log.exception() because we're not
2970+ # in an exception handler.
2971+ log.error('%s', exception)
2972+ CleanupProblem(self).run()
2973+ self.working = False
2974+ def reply():
2975+ pass
2976+ # Make the asynchronous call because this can take a long time. We'll
2977+ # get status updates periodically. Note however that even though this
2978+ # is asynchronous, dbus still expects a response within a certain
2979+ # amount of time. We have no idea how long it will take to clean up
2980+ # the cruft though, so just crank the timeout up to some insanely huge
2981+ # number (of seconds).
2982+ self.widgets['progressbar_status'].set_text('Authenticating...')
2983+ self.janitord.clean(cleanable,
2984+ reply_handler=reply,
2985+ error_handler=error,
2986+ # If it takes longer than an hour, we're screwed.
2987+ timeout=3600)
2988
2989=== added file 'computerjanitorapp/terminalsize.py'
2990--- computerjanitorapp/terminalsize.py 1970-01-01 00:00:00 +0000
2991+++ computerjanitorapp/terminalsize.py 2011-04-03 15:43:25 +0000
2992@@ -0,0 +1,63 @@
2993+# terminalsize.py - find size of terminal
2994+#
2995+# Copyright (C) 2008-2011 Canonical, Ltd.
2996+#
2997+# This program is free software: you can redistribute it and/or modify
2998+# it under the terms of the GNU General Public License as published by
2999+# the Free Software Foundation, version 3 of the License.
3000+#
3001+# This program is distributed in the hope that it will be useful,
3002+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3003+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3004+# GNU General Public License for more details.
3005+#
3006+# You should have received a copy of the GNU General Public License
3007+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3008+
3009+# Inspired by code by Chuck Blake, at
3010+# http://pdos.csail.mit.edu/~cblake/cls/cls.py, but rewritten in an
3011+# attempt to clarify things. This code is slightly tricky, so I thought
3012+# the extra clarity would be worth it.
3013+
3014+import fcntl
3015+import struct
3016+import termios
3017+
3018+
3019+def get_terminal_size(fd=1):
3020+ """Return size of terminal attached to the standard output.
3021+
3022+ Use ioctl(2) to query a terminal for its size, given a file descriptor
3023+ attached to the terminal.
3024+
3025+ :param fd: Use the given file descriptor.
3026+ :type fd: int
3027+ :return: The columns and rows representing the size of the terminal. If
3028+ this cannot be determined, None is returned for both values.
3029+ :rtype: 2-tuple
3030+ """
3031+ try:
3032+ # Do the ioctl call. termios.TIOCGWINSZ is the code to query terminal
3033+ # size (see tty_ioctl(4), at least on Linux). We need to give it a
3034+ # string of suitable size to use as the input buffer for ioctl.
3035+ # ioctl() modifies the buffer and returns the modified buffer as its
3036+ # return value.
3037+ #
3038+ # The manual page specifies a struct winsize to be used, which
3039+ # consists of four unsigned shorts. We use struct.calcsize() to
3040+ # compute the size of that.
3041+ #
3042+ # Note that Blake's original code assumes only the first two shorts in
3043+ # the struct are used, and that two shorts fit into four bytes, which
3044+ # is probably true for all the relevant platforms, but is cramped
3045+ # enough that it makes me feel icky. Thus, I assume less. This will
3046+ # still break if the contents of the struct change, but since that
3047+ # would change the system call API, that's unlikely.
3048+ buflen = struct.calcsize('hhhh')
3049+ buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * buflen)
3050+ # ioctl returns a binary buffer that represents the struct
3051+ # at the C level. We unpack it with struct.unpack.
3052+ return tuple(struct.unpack('hhhh', buf)[:2])
3053+ except Exception:
3054+ # If anything went wrong, we give up and claim we don't know.
3055+ return None, None
3056
3057=== added directory 'computerjanitorapp/tests'
3058=== added file 'computerjanitorapp/tests/__init__.py'
3059=== added file 'computerjanitorapp/tests/test_all.py'
3060--- computerjanitorapp/tests/test_all.py 1970-01-01 00:00:00 +0000
3061+++ computerjanitorapp/tests/test_all.py 2011-04-03 15:43:25 +0000
3062@@ -0,0 +1,29 @@
3063+# Copyright (C) 2008-2011 Canonical, Ltd.
3064+#
3065+# This program is free software: you can redistribute it and/or modify
3066+# it under the terms of the GNU General Public License as published by
3067+# the Free Software Foundation, version 3 of the License.
3068+#
3069+# This program is distributed in the hope that it will be useful,
3070+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3071+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3072+# GNU General Public License for more details.
3073+#
3074+# You should have received a copy of the GNU General Public License
3075+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3076+
3077+"""Test suite for Computer Janitor."""
3078+
3079+import unittest
3080+
3081+from computerjanitorapp.tests import test_terminalsize
3082+from computerjanitorapp.tests import test_utilities
3083+from computerjanitord.tests.test_all import test_suite as cjd_suite
3084+
3085+
3086+def test_suite():
3087+ suite = unittest.TestSuite()
3088+ suite.addTests(test_terminalsize.test_suite())
3089+ suite.addTests(test_utilities.test_suite())
3090+ suite.addTests(cjd_suite())
3091+ return suite
3092
3093=== added file 'computerjanitorapp/tests/test_terminalsize.py'
3094--- computerjanitorapp/tests/test_terminalsize.py 1970-01-01 00:00:00 +0000
3095+++ computerjanitorapp/tests/test_terminalsize.py 2011-04-03 15:43:25 +0000
3096@@ -0,0 +1,54 @@
3097+# Copyright (C) 2008-2011 Canonical, Ltd.
3098+#
3099+# This program is free software: you can redistribute it and/or modify
3100+# it under the terms of the GNU General Public License as published by
3101+# the Free Software Foundation, version 3 of the License.
3102+#
3103+# This program is distributed in the hope that it will be useful,
3104+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3105+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3106+# GNU General Public License for more details.
3107+#
3108+# You should have received a copy of the GNU General Public License
3109+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3110+
3111+"""Test calculation of terminal sizes."""
3112+
3113+from __future__ import absolute_import, unicode_literals
3114+
3115+__metaclass__ = type
3116+__all__ = [
3117+ 'test_suite',
3118+ ]
3119+
3120+
3121+import os
3122+import unittest
3123+
3124+from computerjanitorapp import terminalsize
3125+
3126+
3127+class TestTerminalSize(unittest.TestCase):
3128+ """Test calculation of terminal sizes."""
3129+
3130+ def test_returns_unknown_when_querying_dev_null(self):
3131+ fd = os.open("/dev/null", os.O_RDONLY)
3132+ rows, cols = terminalsize.get_terminal_size(fd)
3133+ os.close(fd)
3134+ self.assertEqual(rows, None)
3135+ self.assertEqual(cols, None)
3136+
3137+ def test_returns_two_integers_when_stdout_is_a_terminal(self):
3138+ # We only run this check if stdout is a terminal.
3139+ # Unfortunately, there is no sensible way of checking the values.
3140+ # But that's OK, they're lumberjacks.
3141+ if os.isatty(1):
3142+ rows, cols = terminalsize.get_terminal_size(1)
3143+ self.assertEqual(type(rows), int)
3144+ self.assertEqual(type(cols), int)
3145+
3146+
3147+def test_suite():
3148+ suite = unittest.TestSuite()
3149+ suite.addTests(unittest.makeSuite(TestTerminalSize))
3150+ return suite
3151
3152=== added file 'computerjanitorapp/tests/test_utilities.py'
3153--- computerjanitorapp/tests/test_utilities.py 1970-01-01 00:00:00 +0000
3154+++ computerjanitorapp/tests/test_utilities.py 2011-04-03 15:43:25 +0000
3155@@ -0,0 +1,70 @@
3156+# Copyright (C) 2010-2011 Canonical, Ltd.
3157+#
3158+# This program is free software: you can redistribute it and/or modify
3159+# it under the terms of the GNU General Public License as published by
3160+# the Free Software Foundation, version 3 of the License.
3161+#
3162+# This program is distributed in the hope that it will be useful,
3163+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3164+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3165+# GNU General Public License for more details.
3166+#
3167+# You should have received a copy of the GNU General Public License
3168+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3169+
3170+"""Test common utilities."""
3171+
3172+from __future__ import absolute_import, unicode_literals
3173+
3174+__metaclass__ = type
3175+__all__ = [
3176+ 'test_suite',
3177+ ]
3178+
3179+
3180+import unittest
3181+
3182+from computerjanitorapp.utilities import format_size
3183+
3184+
3185+class TestUtilities(unittest.TestCase):
3186+ """Test common utilities."""
3187+
3188+ def test_format_negative(self):
3189+ self.assertRaises(AssertionError, format_size, -1)
3190+
3191+ def test_format_zero(self):
3192+ self.assertEqual(format_size(0), '0B')
3193+
3194+ def test_format_small(self):
3195+ self.assertEqual(format_size(500), '500B')
3196+
3197+ def test_format_1k(self):
3198+ self.assertEqual(format_size(1000), '1kB')
3199+
3200+ def test_format_smallish(self):
3201+ self.assertEqual(format_size(500000), '500kB')
3202+
3203+ def test_format_1M(self):
3204+ self.assertEqual(format_size(1000000), '1MB')
3205+
3206+ def test_format_mediumish(self):
3207+ self.assertEqual(format_size(500000000), '500MB')
3208+
3209+ def test_format_1G(self):
3210+ self.assertEqual(format_size(1000000000), '1GB')
3211+
3212+ def test_format_bigish(self):
3213+ self.assertEqual(format_size(500000000000), '500GB')
3214+
3215+ def test_format_1T(self):
3216+ self.assertEqual(format_size(1000000000000), '1TB')
3217+
3218+ def test_format_hugish(self):
3219+ self.assertEqual(format_size(500000000000000), '>1TB')
3220+
3221+
3222+def test_suite():
3223+ suite = unittest.TestSuite()
3224+ suite.addTests(unittest.makeSuite(TestUtilities))
3225+ return suite
3226
3227=== added file 'computerjanitorapp/utilities.py'
3228--- computerjanitorapp/utilities.py 1970-01-01 00:00:00 +0000
3229+++ computerjanitorapp/utilities.py 2011-04-03 15:43:25 +0000
3230@@ -0,0 +1,51 @@
3231+# Copyright (C) 2008-2011 Canonical, Ltd.
3232+#
3233+# This program is free software: you can redistribute it and/or modify
3234+# it under the terms of the GNU General Public License as published by
3235+# the Free Software Foundation, version 3 of the License.
3236+#
3237+# This program is distributed in the hope that it will be useful,
3238+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3239+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3240+# GNU General Public License for more details.
3241+#
3242+# You should have received a copy of the GNU General Public License
3243+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3244+
3245+"""Common utilities."""
3246+
3247+from __future__ import absolute_import, unicode_literals
3248+
3249+__metaclass__ = type
3250+__all__ = [
3251+ 'format_size',
3252+ ]
3253+
3254+
3255+from math import log10
3256+
3257+
3258+TABLE = (
3259+ 'B',
3260+ 'kB',
3261+ 'MB',
3262+ 'GB',
3263+ 'TB',
3264+ )
3265+
3266+
3267+def format_size(bytes):
3268+ """Format size in bytes.
3269+
3270+ :param bytes: Integer size in bytes.
3271+ :type bytes: integer
3272+ :return: Formatted size
3273+ :rtype: string
3274+ """
3275+ assert bytes >= 0, 'Cannot have negative sizes'
3276+ if bytes == 0:
3277+ return '0B'
3278+ if bytes > 10**12:
3279+ return '>1TB'
3280+ key = divmod(int(log10(bytes)), 3)[0]
3281+ return '{0}{1}'.format(bytes // 10**(key * 3), TABLE[key])
3282
3283=== added directory 'computerjanitord'
3284=== renamed directory 'computerjanitord' => 'computerjanitord.moved'
3285=== added file 'computerjanitord/__init__.py'
3286=== added file 'computerjanitord/application.py'
3287--- computerjanitord/application.py 1970-01-01 00:00:00 +0000
3288+++ computerjanitord/application.py 2011-04-03 15:43:25 +0000
3289@@ -0,0 +1,90 @@
3290+# Copyright (C) 2008-2011 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+"""Application interface for use by plugins.
3305+
3306+This primarily makes certain apt functionality available to the plugin.
3307+"""
3308+
3309+from __future__ import absolute_import, unicode_literals
3310+
3311+__metaclass__ = type
3312+__all__ = [
3313+ 'Application',
3314+ ]
3315+
3316+
3317+import apt
3318+
3319+import computerjanitorapp
3320+
3321+from computerjanitord.errors import (
3322+ MissingLandmarkError, NonDownloadableError)
3323+
3324+
3325+_ = computerjanitorapp.setup_gettext()
3326+
3327+SYNTAPTIC_PREFERENCES_FILE = '/var/lib/synaptic/preferences'
3328+# There really isn't anything special about these packages. These really just
3329+# represent landmarks in the package namespace that we look for to try to
3330+# judge the sanity of the apt cache. If these are missing, things are really
3331+# messed up and we can't actually figure out how to continue.
3332+LANDMARK_PACKAGES = [
3333+ 'dash',
3334+ 'gzip',
3335+ ]
3336+
3337+
3338+class Application:
3339+ """Interface for plugins requesting apt actions."""
3340+
3341+ def __init__(self, apt_cache=None):
3342+ """Create the application interface.
3343+
3344+ :param apt_cache: Alternative apt cache for testing purposes. When
3345+ `None` use the default apt cache.
3346+ """
3347+ if apt_cache is None:
3348+ # Use the real apt cache.
3349+ self.apt_cache = apt.Cache()
3350+ else:
3351+ self.apt_cache = apt_cache
3352+ self.refresh_apt_cache()
3353+
3354+ def refresh_apt_cache(self):
3355+ """Refresh the apt cache.
3356+
3357+ This API is used by plugins.
3358+ """
3359+ self.apt_cache.open()
3360+ # For historical purposes, Synaptic has a different way of pinning
3361+ # packages than apt, so we have to load its preferences file in order
3362+ # to know what it's pinning.
3363+ self.apt_cache._depcache.read_pinfile(SYNTAPTIC_PREFERENCES_FILE)
3364+
3365+ def verify_apt_cache(self):
3366+ """Verify that essential packages are available in the apt cache.
3367+
3368+ This API is used by plugins.
3369+
3370+ :raises MissingLandmarkError: When an essential package is not
3371+ available.
3372+ :raises NonDownloadableError: When an essential package cannot be
3373+ downloaded.
3374+ """
3375+ for name in LANDMARK_PACKAGES:
3376+ if name not in self.apt_cache:
3377+ raise MissingLandmarkError(name)
3378+ if not any(v.downloadable for v in self.apt_cache[name].versions):
3379+ raise NonDownloadableError(name)
3380
3381=== added file 'computerjanitord/authenticator.py'
3382--- computerjanitord/authenticator.py 1970-01-01 00:00:00 +0000
3383+++ computerjanitord/authenticator.py 2011-04-03 15:43:25 +0000
3384@@ -0,0 +1,85 @@
3385+# Copyright (C) 2008-2011 Canonical, Ltd.
3386+#
3387+# This program is free software: you can redistribute it and/or modify
3388+# it under the terms of the GNU General Public License as published by
3389+# the Free Software Foundation, version 3 of the License.
3390+#
3391+# This program is distributed in the hope that it will be useful,
3392+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3393+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3394+# GNU General Public License for more details.
3395+#
3396+# You should have received a copy of the GNU General Public License
3397+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3398+
3399+"""Authentication for Computer Janitor backend services."""
3400+
3401+from __future__ import absolute_import, unicode_literals
3402+
3403+__metaclass__ = type
3404+__all__ = [
3405+ 'Authenticator',
3406+ ]
3407+
3408+
3409+import dbus
3410+
3411+
3412+PK_AUTHORITY_BUS_NAME = 'org.freedesktop.PolicyKit1'
3413+PK_AUTHORITY_OBJECT_PATH = '/org/freedesktop/PolicyKit1/Authority'
3414+PK_AUTHORITY_INTERFACE = 'org.freedesktop.PolicyKit1.Authority'
3415+# From the PolicyKit API.
3416+# http://hal.freedesktop.org/docs/polkit/
3417+# eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html
3418+AllowUserInteraction = 0x00000001
3419+
3420+
3421+class Authenticator:
3422+ """PolicyKit authenticator."""
3423+
3424+ def authenticate(self, sender, connection, privilege):
3425+ """Authenticate with PolicyKit.
3426+
3427+ :param sender: The initiator of the action.
3428+ :param connection: The dbus connection that initiated the action.
3429+ :param privilege: The privilege being requested.
3430+ :return: Whether the subject is authorized or not.
3431+ :rtype: bool
3432+ """
3433+ policykit = self._get_policykit_proxy()
3434+ sender_pid = self._get_sender_pid(connection, sender)
3435+ # This is the CheckAuthorization() 'subject' structure.
3436+ subject = (
3437+ 'unix-process', {
3438+ 'pid': sender_pid,
3439+ 'start-time': dbus.UInt64(0),
3440+ })
3441+ # No details or cancellation_id needed.
3442+ details = {'': ''}
3443+ cancellation_id = ''
3444+ flags = AllowUserInteraction
3445+ # CheckAuthorization returns an AuthorizationResult structure, modeled
3446+ # as a 3-tuple. The only thing we care about though is the boolean
3447+ # describing whether we got authorized or not.
3448+ is_authorized, is_challenge, details = policykit.CheckAuthorization(
3449+ subject, privilege, details, flags, cancellation_id)
3450+ return is_authorized
3451+
3452+ def _get_policykit_proxy(self):
3453+ """Contact the system bus to get a PolicyKit proxy."""
3454+ system_bus = dbus.SystemBus()
3455+ pk_proxy = system_bus.get_object(
3456+ PK_AUTHORITY_BUS_NAME, PK_AUTHORITY_OBJECT_PATH)
3457+ return dbus.Interface(pk_proxy, PK_AUTHORITY_INTERFACE)
3458+
3459+ def _get_sender_pid(self, connection, sender):
3460+ """Contact the system bus to get the sender connection PID."""
3461+ # Since we're going to authorize a Unix process, we need to get the
3462+ # sender's process id. This is available on the dbus. The
3463+ # CheckAuthorization() method also requires us to have a start-time,
3464+ # but it's not clear what the semantics are for that, so we'll just
3465+ # put a zero there.
3466+ db_proxy = connection.get_object(
3467+ dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH, introspect=False)
3468+ info = dbus.Interface(db_proxy, dbus.BUS_DAEMON_IFACE)
3469+ return info.GetConnectionUnixProcessID(sender)
3470
3471=== added file 'computerjanitord/collector.py'
3472--- computerjanitord/collector.py 1970-01-01 00:00:00 +0000
3473+++ computerjanitord/collector.py 2011-04-03 15:43:25 +0000
3474@@ -0,0 +1,168 @@
3475+# Copyright (C) 2008-2011 Canonical, Ltd.
3476+#
3477+# This program is free software: you can redistribute it and/or modify
3478+# it under the terms of the GNU General Public License as published by
3479+# the Free Software Foundation, version 3 of the License.
3480+#
3481+# This program is distributed in the hope that it will be useful,
3482+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3483+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3484+# GNU General Public License for more details.
3485+#
3486+# You should have received a copy of the GNU General Public License
3487+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3488+
3489+"""A cruft collector."""
3490+
3491+from __future__ import absolute_import, unicode_literals
3492+
3493+__metaclass__ = type
3494+__all__ = [
3495+ 'Collector',
3496+ ]
3497+
3498+
3499+import os
3500+import time
3501+import logging
3502+
3503+from computerjanitor import PluginManager
3504+from computerjanitord.errors import (
3505+ DuplicateCruftError, NoSuchCruftError, PackageCleanupError)
3506+from computerjanitord.whitelist import Whitelist
3507+
3508+
3509+log = logging.getLogger('computerjanitor')
3510+MISSING = object()
3511+DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
3512+
3513+# For testing purposes.
3514+SLEEPY_TIME = float(os.environ.get('COMPUTER_JANITOR_SLEEPY_TIME', '1.0'))
3515+
3516+
3517+class Collector:
3518+ """A cruft collector."""
3519+
3520+ def __init__(self, application, plugin_manager_class=None,
3521+ whitelist_dirs=None, service=None):
3522+ """Create a cruft collector.
3523+
3524+ :param application: The `Application` class.
3525+ :type application: This object is used by plugins to access the apt
3526+ database. It must have an attribute named `apt_cache` and a
3527+ method named `refresh_apt_cache()`.
3528+ :param plugin_manager_class: The plugin manager class. If None (the
3529+ default), then `computerjanitor.PluginManager` is used.
3530+ :type plugin_manager_class: callable accepting a single argument,
3531+ which is a sequence of plugin directories.
3532+ :param whitelist_dirs: Sequence of directories to search for
3533+ '.whitelist' files. Passed directly to
3534+ `computerjanitord.whitelist.Whitelist`.
3535+ :param service: The dbus service; when doing plugin post-cleanup, this
3536+ will be used to emit a progress signal.
3537+ """
3538+ self.application = application
3539+ self.service = service
3540+ self.whitelist = Whitelist(whitelist_dirs)
3541+ # Keep track of cruft and map between the cruft's name and its Cruft
3542+ # instance. We'll use the latter when cruft cleanup is requested
3543+ # through the dbus API.
3544+ self.cruft = None
3545+ self.cruft_by_name = None
3546+ # Set up the plugin manager.
3547+ plugin_path = os.environ.get('COMPUTER_JANITOR_PLUGINS',
3548+ DEFAULT_PLUGINS_DIRS)
3549+ plugin_dirs = plugin_path.split(':')
3550+ if plugin_manager_class is None:
3551+ plugin_manager_class = PluginManager
3552+ self.plugin_manager = plugin_manager_class(application, plugin_dirs)
3553+ self.load()
3554+
3555+ def load(self):
3556+ """Reload all cruft."""
3557+ self.cruft = []
3558+ self.cruft_by_name = {}
3559+ # Ask all the plugins to find their cruft, filtering out whitelisted
3560+ # cruft.
3561+ for plugin in self.plugin_manager.get_plugins():
3562+ for cruft in plugin.get_cruft():
3563+ if not self.whitelist.is_whitelisted(cruft):
3564+ # Different plugins can give us duplicate cruft names,
3565+ # however the Cruft class better be the same, otherwise we
3566+ # won't actually know how to map the name back to a cruft
3567+ # instance for proper cleanup.
3568+ if cruft.get_name() in self.cruft_by_name:
3569+ my_cruft = self.cruft_by_name[cruft.get_name()]
3570+ if cruft.__class__ is my_cruft.__class__:
3571+ # We only need one instance of this cruft.
3572+ continue
3573+ else:
3574+ raise DuplicateCruftError(cruft.get_name())
3575+ #print ' ', cruft.get_name()
3576+ self.cruft.append(cruft)
3577+ self.cruft_by_name[cruft.get_name()] = cruft
3578+
3579+ def clean(self, names, dry_run=False):
3580+ """Clean up the named cruft.
3581+
3582+ :param names: The names of the cruft to clean up.
3583+ :type names: list of strings
3584+ :param dry_run: Flag indicating whether to do permanent changes.
3585+ :type dry_run: bool
3586+ """
3587+ for name in names:
3588+ # Ensure that all named cruft is known.
3589+ cruft = self.cruft_by_name.get(name, MISSING)
3590+ if cruft is MISSING:
3591+ log.error('No such cruft: {0}'.format(name))
3592+ raise NoSuchCruftError(name)
3593+ # Ensure that the cruft is not being ignored.
3594+ if cruft in self.service.state.ignore:
3595+ log.error('Skipping ignored cruft: {0}'.format(name))
3596+ continue
3597+ log.info('cleaning cruft{1}: {0}'.format(
3598+ cruft.get_name(), ('[X]' if dry_run else '[Y]')))
3599+ if not dry_run:
3600+ try:
3601+ cruft.cleanup()
3602+ except:
3603+ log.exception('cruft.cleanup(): {0}'.format(name))
3604+ raise
3605+ # Do plugin-specific post-cleanup.
3606+ for plugin in self.plugin_manager.get_plugins():
3607+ log.info('post-cleanup: {0}'.format(plugin))
3608+ if self.service is not None:
3609+ # Notify the client that we're not done yet.
3610+ #
3611+ # 2010-02-09 barry: this actually kind of sucks because the
3612+ # granularity is too coarse. Some plugins will post_cleanup()
3613+ # very quickly, others will take a long time. Unfortunately,
3614+ # the computerjanitor.Plugin API doesn't support a more
3615+ # granular feedback. Plugin.get_plugin(..., callback=foo)
3616+ # doesn't really cut it because that only gets called during
3617+ # get_plugins().
3618+ self.service.cleanup_status(plugin.__class__.__name__)
3619+ if dry_run:
3620+ # For testing purposes.
3621+ time.sleep(SLEEPY_TIME)
3622+ else:
3623+ try:
3624+ plugin.post_cleanup()
3625+ except SystemError as error:
3626+ # apt will raise a SystemError if some other package
3627+ # manager is already running. Turn this into a dbus
3628+ # derived exception so the client will be properly
3629+ # informed.
3630+ log.exception(
3631+ 'plugin.post_cleanup(): {0}'.format(plugin))
3632+ raise PackageCleanupError(str(error))
3633+ except:
3634+ log.exception(
3635+ 'plugin.post_cleanup(): {0}'.format(plugin))
3636+ raise
3637+ # Now we're done.
3638+ if self.service is not None:
3639+ self.service.cleanup_status('')
3640+ # Reload list of crufts.
3641+ self.application.refresh_apt_cache()
3642+ self.load()
3643
3644=== added directory 'computerjanitord/data'
3645=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.conf'
3646--- computerjanitord/data/com.ubuntu.ComputerJanitor.conf 1970-01-01 00:00:00 +0000
3647+++ computerjanitord/data/com.ubuntu.ComputerJanitor.conf 2011-04-03 15:43:25 +0000
3648@@ -0,0 +1,15 @@
3649+<!DOCTYPE busconfig PUBLIC
3650+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
3651+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
3652+<busconfig>
3653+ <policy user="root">
3654+ <allow own="com.ubuntu.ComputerJanitor"/>
3655+ </policy>
3656+
3657+ <policy context="default">
3658+ <allow send_interface="com.ubuntu.ComputerJanitor"/>
3659+ <allow receive_interface="com.ubuntu.ComputerJanitor"
3660+ receive_sender="com.ubuntu.ComputerJanitor"/>
3661+ </policy>
3662+
3663+</busconfig>
3664
3665=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.service'
3666--- computerjanitord/data/com.ubuntu.ComputerJanitor.service 1970-01-01 00:00:00 +0000
3667+++ computerjanitord/data/com.ubuntu.ComputerJanitor.service 2011-04-03 15:43:25 +0000
3668@@ -0,0 +1,4 @@
3669+[D-BUS Service]
3670+Name=com.ubuntu.ComputerJanitor
3671+Exec=/usr/share/computerjanitor/janitord
3672+User=root
3673
3674=== added file 'computerjanitord/data/com.ubuntu.computerjanitor.policy'
3675--- computerjanitord/data/com.ubuntu.computerjanitor.policy 1970-01-01 00:00:00 +0000
3676+++ computerjanitord/data/com.ubuntu.computerjanitor.policy 2011-04-03 15:43:25 +0000
3677@@ -0,0 +1,19 @@
3678+<?xml version="1.0" encoding="UTF-8"?>
3679+<!DOCTYPE policyconfig PUBLIC
3680+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
3681+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
3682+<policyconfig>
3683+
3684+ <vendor>ComputerJanitor</vendor>
3685+ <vendor_url>https://launchpad.net/computer-janitor</vendor_url>
3686+
3687+ <action id="com.ubuntu.computerjanitor.updatesystem">
3688+ <description>Clean up packages that are no longer necessary</description>
3689+ <message>Removing unused packages requires authentication</message>
3690+ <defaults>
3691+ <allow_inactive>no</allow_inactive>
3692+ <allow_active>auth_admin_keep</allow_active>
3693+ </defaults>
3694+ </action>
3695+
3696+</policyconfig>
3697
3698=== added file 'computerjanitord/errors.py'
3699--- computerjanitord/errors.py 1970-01-01 00:00:00 +0000
3700+++ computerjanitord/errors.py 2011-04-03 15:43:25 +0000
3701@@ -0,0 +1,95 @@
3702+# Copyright (C) 2008-2011 Canonical, Ltd.
3703+#
3704+# This program is free software: you can redistribute it and/or modify
3705+# it under the terms of the GNU General Public License as published by
3706+# the Free Software Foundation, version 3 of the License.
3707+#
3708+# This program is distributed in the hope that it will be useful,
3709+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3710+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3711+# GNU General Public License for more details.
3712+#
3713+# You should have received a copy of the GNU General Public License
3714+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3715+
3716+"""Exceptions for the Computer Janitor daemon."""
3717+
3718+
3719+from __future__ import absolute_import, unicode_literals
3720+
3721+__metaclass__ = type
3722+__all__ = [
3723+ 'DuplicateCruftError',
3724+ 'LandmarkPackageError',
3725+ 'MissingLandmarkError',
3726+ 'NoSuchCruftError',
3727+ 'NonDownloadableError',
3728+ 'PermissionDeniedError',
3729+ ]
3730+
3731+
3732+import dbus
3733+import computerjanitor
3734+
3735+
3736+class DBusServiceBaseException(computerjanitor.Exception, dbus.DBusException):
3737+ """Base class exception for the Computer Janitor DBus service."""
3738+
3739+
3740+class PermissionDeniedError(DBusServiceBaseException):
3741+ """Permission denied by policy."""
3742+
3743+
3744+class CruftError(DBusServiceBaseException):
3745+ """Cruft exceptions passed back to dbus client."""
3746+
3747+ _errmsg = None
3748+
3749+ def __init__(self, cruft_name):
3750+ self.cruft_name = cruft_name
3751+
3752+ def __str__(self):
3753+ return self._errmsg.format(self)
3754+
3755+
3756+class DuplicateCruftError(CruftError):
3757+ """Duplicate cruft name with different cleanup."""
3758+
3759+ _errmsg = 'Duplicate cruft with different cleanup: {0.cruft_name}'
3760+
3761+
3762+class NoSuchCruftError(CruftError):
3763+ """There is no cruft by the given name."""
3764+
3765+ _errmsg = 'No such cruft: {0.cruft_name}'
3766+
3767+
3768+class LandmarkPackageError(DBusServiceBaseException):
3769+ """Base class for problems with the landmark packages."""
3770+
3771+ _errmsg = None
3772+
3773+ def __init__(self, package):
3774+ self.package = package
3775+
3776+ def __str__(self):
3777+ # gettext translation needs to be called at run time.
3778+ return self._errmsg.format(self)
3779+
3780+
3781+class MissingLandmarkError(LandmarkPackageError):
3782+ """A landmark package could not be found."""
3783+
3784+ _errmsg = 'Landmark package {0.package} is missing'
3785+
3786+
3787+class NonDownloadableError(LandmarkPackageError):
3788+ """A landmark package is not downloadable."""
3789+
3790+ _errmsg = 'Landmark package {0.package} is not downloadable'
3791+
3792+
3793+class PackageCleanupError(DBusServiceBaseException):
3794+ """Could not complete plugin post-cleanup."""
3795+
3796+ _errmsg = 'Post-cleanup exception'
3797
3798=== added file 'computerjanitord/main.py'
3799--- computerjanitord/main.py 1970-01-01 00:00:00 +0000
3800+++ computerjanitord/main.py 2011-04-03 15:43:25 +0000
3801@@ -0,0 +1,120 @@
3802+# Copyright (C) 2008-2011 Canonical, Ltd.
3803+#
3804+# This program is free software: you can redistribute it and/or modify
3805+# it under the terms of the GNU General Public License as published by
3806+# the Free Software Foundation, version 3 of the License.
3807+#
3808+# This program is distributed in the hope that it will be useful,
3809+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3810+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3811+# GNU General Public License for more details.
3812+#
3813+# You should have received a copy of the GNU General Public License
3814+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3815+
3816+"""Main entry point for Computer Janitor dbus daemon."""
3817+
3818+from __future__ import absolute_import, unicode_literals
3819+
3820+__metaclass__ = type
3821+__all__ = [
3822+ 'main',
3823+ ]
3824+
3825+
3826+import os
3827+import gobject
3828+import logging
3829+import argparse
3830+import warnings
3831+import dbus.mainloop.glib
3832+import logging.handlers
3833+
3834+from computerjanitorapp import __version__, setup_gettext
3835+from computerjanitord.service import Service
3836+
3837+_ = setup_gettext()
3838+
3839+
3840+# 2010-02-09 barry: computerjanitor.package_cruft has a DeprecationWarning,
3841+# but that really needs to be fixed in that package, which in turn needs to be
3842+# ripped out of update-manager.
3843+warnings.filterwarnings('ignore', category=DeprecationWarning,
3844+ module='computerjanitor.package_cruft')
3845+
3846+
3847+# As per aptdaemon, when run as a dbus service, we must add the necessary
3848+# paths to $PATH.
3849+REQUIRED_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
3850+
3851+
3852+class Options:
3853+ """Command line options."""
3854+
3855+ def __init__(self):
3856+ self.parser = argparse.ArgumentParser(
3857+ description=_("""\
3858+ Computer janitor dbus daemon. You must run this daemon as root.
3859+ """))
3860+ self.parser.add_argument(
3861+ '--version', action='version',
3862+ version=_('Computer Janitor dbus daemon {version}'.format(
3863+ version=__version__)))
3864+ self.parser.add_argument(
3865+ '-n', '--dry-run', action='store_true',
3866+ help=_("""\
3867+ Only pretend to do anything permanent. This is useful for testing
3868+ and debugging."""))
3869+ self.parser.add_argument(
3870+ '-f', '--state-file', metavar='FILE',
3871+ help=_('Store ignored state in FILE instead of the default.'))
3872+ self.arguments = self.parser.parse_args()
3873+
3874+
3875+class ASCIIFormatter(logging.Formatter):
3876+ """Force the log messages to ASCII."""
3877+ def format(self, record):
3878+ message = logging.Formatter.format(self, record)
3879+ return message.encode('ascii', 'replace')
3880+
3881+
3882+def main():
3883+ """Main entry point."""
3884+ # Set up logging.
3885+ if os.environ.get('COMPUTER_JANITOR_DEBUG') is not None:
3886+ level = logging.DEBUG
3887+ else:
3888+ level = logging.INFO
3889+ # When running under dbus, $PATH will not be set. However it must be in
3890+ # order to find dpkg(1) and friends. Hard code it in the same way that
3891+ # aptdaemon does.
3892+ if os.environ.get('PATH') is None:
3893+ os.putenv('PATH', REQUIRED_PATH)
3894+ logging.basicConfig(level=level)
3895+ log = logging.getLogger('computerjanitor')
3896+ # SysLogHandler does not recognize unicode arguments.
3897+ syslog = logging.handlers.SysLogHandler(b'/dev/log')
3898+ # syslog will provide a timestamp.
3899+ formatter = ASCIIFormatter('computerjanitord:%(levelname)s: %(message)s')
3900+ syslog.setFormatter(formatter)
3901+ syslog.setLevel(level)
3902+ log.addHandler(syslog)
3903+ options = Options()
3904+ # Ensure that we're running as root. Do this here instead of ../janitord
3905+ # so that we can still run 'janitord --version' and 'janitord --help' as
3906+ # non-root.
3907+ if os.getuid() != 0:
3908+ options.parser.error('You must run this as root')
3909+ # No return.
3910+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
3911+ # pyflakes complains about this line, but we need it anyway to keep the
3912+ # dbus service object referenced during the running of the main loop.
3913+ server = Service(options)
3914+ try:
3915+ gobject.MainLoop().run()
3916+ except KeyboardInterrupt:
3917+ pass
3918+
3919+
3920+if __name__ == '__main__':
3921+ main()
3922
3923=== added file 'computerjanitord/service.py'
3924--- computerjanitord/service.py 1970-01-01 00:00:00 +0000
3925+++ computerjanitord/service.py 2011-04-03 15:43:25 +0000
3926@@ -0,0 +1,288 @@
3927+# Copyright (C) 2008-2011 Canonical, Ltd.
3928+#
3929+# This program is free software: you can redistribute it and/or modify
3930+# it under the terms of the GNU General Public License as published by
3931+# the Free Software Foundation, version 3 of the License.
3932+#
3933+# This program is distributed in the hope that it will be useful,
3934+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3935+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3936+# GNU General Public License for more details.
3937+#
3938+# You should have received a copy of the GNU General Public License
3939+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3940+
3941+"""dbus service for cleaning up crufty packages that are no longer needed."""
3942+
3943+
3944+from __future__ import absolute_import, unicode_literals
3945+
3946+__metaclass__ = type
3947+__all__ = [
3948+ 'Service',
3949+ ]
3950+
3951+
3952+import atexit
3953+import logging
3954+
3955+import glib
3956+import dbus.service
3957+
3958+from computerjanitord.application import Application
3959+from computerjanitord.authenticator import Authenticator
3960+from computerjanitord.collector import Collector
3961+from computerjanitord.errors import NoSuchCruftError, PermissionDeniedError
3962+from computerjanitord.state import State, DEFAULT_STATE_FILE
3963+
3964+
3965+log = logging.getLogger('computerjanitor')
3966+MISSING = object()
3967+
3968+DBUS_INTERFACE_NAME = 'com.ubuntu.ComputerJanitor'
3969+PRIVILEGE = 'com.ubuntu.computerjanitor.updatesystem'
3970+
3971+
3972+class Service(dbus.service.Object):
3973+ """Backend dbus service that handles removing crufty packages."""
3974+
3975+ def __init__(self, options):
3976+ """Create the dbus service.
3977+
3978+ :param options: The command line options class.
3979+ :type options: `Options`
3980+ """
3981+ self.dry_run = options.arguments.dry_run
3982+ self.state_file = (DEFAULT_STATE_FILE
3983+ if options.arguments.state_file is None
3984+ else options.arguments.state_file)
3985+ self.application = Application()
3986+ self.state = State()
3987+ self.state.load(self.state_file)
3988+ self.collector = Collector(self.application, service=self)
3989+ self.authenticator = Authenticator()
3990+ bus_name = dbus.service.BusName(
3991+ DBUS_INTERFACE_NAME, bus=dbus.SystemBus())
3992+ dbus.service.Object.__init__(self, bus_name, '/')
3993+ # We can't use the decorator because that doesn't work with methods;
3994+ # self doesn't get passed to the handler.
3995+ atexit.register(self._exit_handler)
3996+
3997+ def _exit_handler(self):
3998+ """Ensure that the state file is saved at exit."""
3999+ if not self.dry_run:
4000+ self.state.save(self.state_file)
4001+
4002+ def _authenticate(self, sender, connection):
4003+ """Authenticate via PolicyKit.
4004+
4005+ :param sender: The dbus client sender.
4006+ :param connection: The dbus client connection.
4007+ :raises PermissionDeniedError: when the authentication fails.
4008+ """
4009+ if not self.authenticator.authenticate(sender, connection, PRIVILEGE):
4010+ log.error('Permission denied: {0} for {1} on {2}'.format(
4011+ PRIVILEGE, sender, connection))
4012+ raise PermissionDeniedError(PRIVILEGE)
4013+ log.debug('Permission granted: {0} for {1} on {2}'.format(
4014+ PRIVILEGE, sender, connection))
4015+
4016+ @dbus.service.method(DBUS_INTERFACE_NAME,
4017+ out_signature='as')
4018+ def find(self):
4019+ """Find all the non-whitelisted cruft on the system.
4020+
4021+ Because this is a read-only interface it does not need authorization
4022+ to be called.
4023+
4024+ :return: A list of matching cruft names.
4025+ """
4026+ return list(cruft.get_name() for cruft in self.collector.cruft)
4027+
4028+ @dbus.service.method(DBUS_INTERFACE_NAME,
4029+ out_signature='b')
4030+ def find_async(self):
4031+ """Find all the non-whitelisted cruft on the system, asynchronously.
4032+
4033+ Because this is a read-only interface it does not need authorization
4034+ to be called. Use this method when you can't wait for the cruft
4035+ searching process to complete, since it might take some time. To
4036+ respond when the cruft has been found, set up a `find_finished` signal
4037+ handler.
4038+
4039+ :return: True
4040+ """
4041+ glib.timeout_add_seconds(1, self._find_async)
4042+ return True
4043+
4044+ def _find_async(self):
4045+ """Find all cruft asynchronously and call the signal handler."""
4046+ cruft = list(cruft.get_name() for cruft in self.collector.cruft)
4047+ self.find_finished(cruft)
4048+ # Only call the callback once.
4049+ return False
4050+
4051+ @dbus.service.signal(DBUS_INTERFACE_NAME, signature='as')
4052+ def find_finished(self, cruft):
4053+ """dbus signal called when `_find_async()` completes."""
4054+ log.debug('find_finished: {0}'.format(cruft))
4055+
4056+ @dbus.service.method(DBUS_INTERFACE_NAME,
4057+ out_signature='as',
4058+ # Must wrap these in str() because Python < 2.6.5
4059+ # does not like unicode keyword arguments.
4060+ sender_keyword=str('sender'),
4061+ connection_keyword=str('connection'))
4062+ def load(self, sender=None, connection=None):
4063+ """Load the state file."""
4064+ self._authenticate(sender, connection)
4065+ self.state.load(self.state_file)
4066+ return list(self.state.ignore)
4067+
4068+ @dbus.service.method(DBUS_INTERFACE_NAME,
4069+ # Must wrap these in str() because Python < 2.6.5
4070+ # does not like unicode keyword arguments.
4071+ sender_keyword=str('sender'),
4072+ connection_keyword=str('connection'))
4073+ def save(self, sender=None, connection=None):
4074+ """Save the state file."""
4075+ self._authenticate(sender, connection)
4076+ if not self.dry_run:
4077+ self.state.save(self.state_file)
4078+
4079+ @dbus.service.method(DBUS_INTERFACE_NAME,
4080+ in_signature='s',
4081+ # Must wrap these in str() because Python < 2.6.5
4082+ # does not like unicode keyword arguments.
4083+ sender_keyword=str('sender'),
4084+ connection_keyword=str('connection'))
4085+ def ignore(self, name, sender=None, connection=None):
4086+ """Ignore the named cruft.
4087+
4088+ :param name: The name of the cruft to ignore.
4089+ :type filename: string
4090+ """
4091+ # Make sure this is known cruft first.
4092+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4093+ if cruft is MISSING:
4094+ log.error('ignore(): No such cruft: {0}'.format(name))
4095+ raise NoSuchCruftError(name)
4096+ self._authenticate(sender, connection)
4097+ if not self.dry_run:
4098+ self.state.ignore.add(name)
4099+
4100+ @dbus.service.method(DBUS_INTERFACE_NAME,
4101+ in_signature='s',
4102+ # Must wrap these in str() because Python < 2.6.5
4103+ # does not like unicode keyword arguments.
4104+ sender_keyword=str('sender'),
4105+ connection_keyword=str('connection'))
4106+ def unignore(self, name, sender=None, connection=None):
4107+ """Unignore the named cruft.
4108+
4109+ :param name: The name of the cruft to unignore.
4110+ :type filename: string
4111+ """
4112+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4113+ if cruft is MISSING:
4114+ log.error('ignore(): No such cruft: {0}'.format(name))
4115+ raise NoSuchCruftError(name)
4116+ self._authenticate(sender, connection)
4117+ if not self.dry_run:
4118+ # Don't worry if we're already not ignoring the cruft (i.e. don't
4119+ # raise a KeyError here if 'name' is not in the set).
4120+ self.state.ignore.discard(name)
4121+
4122+ @dbus.service.method(DBUS_INTERFACE_NAME,
4123+ out_signature='as')
4124+ def ignored(self):
4125+ """Return the list of ignored cruft.
4126+
4127+ :return: The names of the ignored cruft.
4128+ :rtype: list of strings
4129+ """
4130+ return list(self.state.ignore)
4131+
4132+ @dbus.service.method(DBUS_INTERFACE_NAME,
4133+ in_signature='s',
4134+ out_signature='s')
4135+ def get_description(self, name):
4136+ """Return the description of the named cruft.
4137+
4138+ :param name: The cruft name.
4139+ :type name: string
4140+ :return: The description of the cruft.
4141+ :rtype: string
4142+ """
4143+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4144+ if cruft is MISSING:
4145+ log.error('get_description(): No such cruft: {0}'.format(name))
4146+ raise NoSuchCruftError(name)
4147+ return cruft.get_description()
4148+
4149+ @dbus.service.method(DBUS_INTERFACE_NAME,
4150+ in_signature='s',
4151+ out_signature='s')
4152+ def get_shortname(self, name):
4153+ """Return the short name of the named cruft.
4154+
4155+ :param name: The cruft name.
4156+ :type name: string
4157+ :return: The short nameof the cruft.
4158+ :rtype: string
4159+ """
4160+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4161+ if cruft is MISSING:
4162+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4163+ raise NoSuchCruftError(name)
4164+ return cruft.get_shortname()
4165+
4166+ @dbus.service.method(DBUS_INTERFACE_NAME,
4167+ in_signature='s',
4168+ out_signature='st')
4169+ def get_details(self, name):
4170+ """Return some extra details about the named cruft.
4171+
4172+ :param name: The cruft name.
4173+ :type name: string
4174+ :return: Some extra details about the named cruft, specifically its
4175+ 'type' and the amount of disk space it consumes. The type is
4176+ simply the name of the cruft instance's class.
4177+ :rtype: string, uint64
4178+ """
4179+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4180+ if cruft is MISSING:
4181+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4182+ raise NoSuchCruftError(name)
4183+ return cruft.__class__.__name__, cruft.get_disk_usage()
4184+
4185+ @dbus.service.method(DBUS_INTERFACE_NAME,
4186+ in_signature='as', # array of strings
4187+ # Must wrap these in str() because Python < 2.6.5
4188+ # does not like unicode keyword arguments.
4189+ sender_keyword=str('sender'),
4190+ connection_keyword=str('connection'))
4191+ def clean(self, names, sender=None, connection=None):
4192+ """Clean the named crufts.
4193+
4194+ :param names: The names of the cruft to clean.
4195+ :type names: list of strings
4196+ """
4197+ self._authenticate(sender, connection)
4198+ self.collector.clean(names, self.dry_run)
4199+
4200+ @dbus.service.signal(DBUS_INTERFACE_NAME,
4201+ signature='s')
4202+ def cleanup_status(self, cruft):
4203+ """Signal cleanup status.
4204+
4205+ This signal is used to incrementally inform clients that some cleanup
4206+ work is being done. It is called at the beginning of the cleanup
4207+ process and after each plugin has completed its `post_cleanup()`
4208+ method.
4209+
4210+ :param done: The name of the next piece of cruft to be cleaned up, or
4211+ the empty string when there's nothing left to do.
4212+ :type done: string
4213+ """
4214+ log.debug('cleanup_status: {0}'.format(cruft))
4215
4216=== added file 'computerjanitord/state.py'
4217--- computerjanitord/state.py 1970-01-01 00:00:00 +0000
4218+++ computerjanitord/state.py 2011-04-03 15:43:25 +0000
4219@@ -0,0 +1,92 @@
4220+# Copyright (C) 2008-2011 Canonical, Ltd.
4221+#
4222+# This program is free software: you can redistribute it and/or modify
4223+# it under the terms of the GNU General Public License as published by
4224+# the Free Software Foundation, version 3 of the License.
4225+#
4226+# This program is distributed in the hope that it will be useful,
4227+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4228+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4229+# GNU General Public License for more details.
4230+#
4231+# You should have received a copy of the GNU General Public License
4232+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4233+
4234+"""Maintaining package ignored state."""
4235+
4236+from __future__ import absolute_import, unicode_literals
4237+
4238+__metaclass__ = type
4239+__all__ = [
4240+ 'DEFAULT_STATE_FILE',
4241+ 'State',
4242+ ]
4243+
4244+
4245+import ConfigParser
4246+import textwrap
4247+
4248+
4249+DEFAULT_STATE_FILE = '/var/lib/computer-janitor/state.dat'
4250+
4251+
4252+class State:
4253+ """Maintain the state of cruft which should be ignored.
4254+
4255+ The file's format is a `ConfigParser` style .ini file. Each section is a
4256+ cruft's `.get_name()` value and contains a single boolean setting
4257+ `ignore`. If true, then the cruft is ignored.
4258+
4259+ For backward compatibility purposes, the setting `enabled` is also
4260+ recognized (on read only). If `enabled` is false then the cruft is
4261+ ignored.
4262+ """
4263+
4264+ def __init__(self):
4265+ self.ignore = set()
4266+
4267+ def load(self, filename):
4268+ """Load ignored state from a file.
4269+
4270+ This reset any previously determined state and re-initializes it with
4271+ the state stored in the file.
4272+
4273+ :param filename: The file to load.
4274+ :type filename: string
4275+ """
4276+ parser = ConfigParser.ConfigParser()
4277+ parser.read(filename)
4278+ # Reset the set of ignored packages.
4279+ self.ignore = set()
4280+ for cruft_name in parser.sections():
4281+ # For backwards compatibility, recognize both the 'ignore' setting
4282+ # and the 'enabled' setting. We only write the former.
4283+ try:
4284+ ignore = parser.getboolean(cruft_name, 'ignore')
4285+ except ConfigParser.NoOptionError:
4286+ try:
4287+ ignore = not parser.getboolean(cruft_name, 'enabled')
4288+ except ConfigParser.NoOptionError:
4289+ # No other settings are recognized.
4290+ ignore = False
4291+ if ignore:
4292+ self.ignore.add(cruft_name)
4293+
4294+ def save(self, filename):
4295+ """Save the ignored state to a file.
4296+
4297+ Only the packages being ignored are stored to the file, and writing
4298+ overwrites the previous contents of the file.
4299+
4300+ :param filename: The file to load.
4301+ :type filename: string
4302+ """
4303+ # It's easier just to write the .ini file directly instead of using
4304+ # the ConfigParser interface. This way we can guarantee sort order
4305+ # and can automatically cull unignored packages from the file.
4306+ with open(filename, 'w') as fp:
4307+ for cruft_name in self.ignore:
4308+ print >> fp, textwrap.dedent("""\
4309+ [{0}]
4310+ ignore: true
4311+ """.format(cruft_name))
4312
4313=== added directory 'computerjanitord/tests'
4314=== added file 'computerjanitord/tests/__init__.py'
4315=== added directory 'computerjanitord/tests/data'
4316=== added file 'computerjanitord/tests/data/empty'
4317=== added directory 'computerjanitord/tests/data/etc'
4318=== added directory 'computerjanitord/tests/data/etc/apt'
4319=== added file 'computerjanitord/tests/data/etc/apt/sources.list'
4320--- computerjanitord/tests/data/etc/apt/sources.list 1970-01-01 00:00:00 +0000
4321+++ computerjanitord/tests/data/etc/apt/sources.list 2011-04-03 15:43:25 +0000
4322@@ -0,0 +1,1 @@
4323+deb http://archive.ubuntu.com/ubuntu intrepid main restricted
4324
4325=== added directory 'computerjanitord/tests/data/var'
4326=== added directory 'computerjanitord/tests/data/var/cache'
4327=== added directory 'computerjanitord/tests/data/var/cache/apt'
4328=== added directory 'computerjanitord/tests/data/var/cache/apt/archives'
4329=== added directory 'computerjanitord/tests/data/var/cache/apt/archives/partial'
4330=== added directory 'computerjanitord/tests/data/var/lib'
4331=== added directory 'computerjanitord/tests/data/var/lib/apt'
4332=== added directory 'computerjanitord/tests/data/var/lib/apt/lists'
4333=== added file 'computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages'
4334--- computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 1970-01-01 00:00:00 +0000
4335+++ computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 2011-04-03 15:43:25 +0000
4336@@ -0,0 +1,54 @@
4337+Package: dash
4338+Priority: required
4339+Section: shells
4340+Installed-Size: 236
4341+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4342+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4343+Architecture: all
4344+Version: 0.5.5.1-3ubuntu1
4345+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4346+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4347+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4348+Size: 104190
4349+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4350+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4351+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4352+Description: POSIX-compliant shell
4353+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4354+ from ash.
4355+ .
4356+ Since it executes scripts faster than bash, and has fewer library
4357+ dependencies (making it more robust against software or hardware
4358+ failures), it is used as the default system shell on Debian systems.
4359+Homepage: http://gondor.apana.org.au/~herbert/dash/
4360+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4361+Origin: Ubuntu
4362+Supported: 5y
4363+Task: minimal
4364+
4365+Package: gzip
4366+Essential: yes
4367+Priority: required
4368+Section: utils
4369+Installed-Size: 284
4370+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4371+Original-Maintainer: Bdale Garbee <bdale@gag.com>
4372+Architecture: all
4373+Version: 1.3.12-9ubuntu1
4374+Pre-Depends: libc6 (>= 2.4)
4375+Suggests: less
4376+Filename: pool/main/g/gzip/gzip_1.3.12-9ubuntu1_all.deb
4377+Size: 107030
4378+MD5sum: f64beb93d2d1a3348cfc47f1fd176ee1
4379+SHA1: 3dd3e56f551fb85ba2ad385df463adeff1fff2d9
4380+SHA256: 2545f0a28514535006adf9ee8576ca3be2aa6da3d890047f92eeda18c0e3aa57
4381+Description: GNU compression utilities
4382+ This package provides the standard GNU file compression utilities, which
4383+ are also the default compression tools for Debian. They typically operate
4384+ on files with names ending in '.gz', but can also decompress files ending
4385+ in '.Z' created with 'compress'.
4386+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4387+Origin: Ubuntu
4388+Supported: 5y
4389+Task: minimal
4390+
4391
4392=== added directory 'computerjanitord/tests/data/var/lib/apt/lists/partial'
4393=== added directory 'computerjanitord/tests/data/var/lib/dpkg'
4394=== added file 'computerjanitord/tests/data/var/lib/dpkg/status'
4395--- computerjanitord/tests/data/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
4396+++ computerjanitord/tests/data/var/lib/dpkg/status 2011-04-03 15:43:25 +0000
4397@@ -0,0 +1,28 @@
4398+Package: dash-nodownload
4399+Priority: required
4400+Section: shells
4401+Installed-Size: 236
4402+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4403+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4404+Architecture: all
4405+Version: 0.5.5.1-3ubuntu1
4406+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4407+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4408+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4409+Size: 104190
4410+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4411+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4412+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4413+Description: POSIX-compliant shell
4414+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4415+ from ash.
4416+ .
4417+ Since it executes scripts faster than bash, and has fewer library
4418+ dependencies (making it more robust against software or hardware
4419+ failures), it is used as the default system shell on Debian systems.
4420+Homepage: http://gondor.apana.org.au/~herbert/dash/
4421+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4422+Origin: Ubuntu
4423+Supported: 5y
4424+Task: minimal
4425+
4426
4427=== added file 'computerjanitord/tests/test_all.py'
4428--- computerjanitord/tests/test_all.py 1970-01-01 00:00:00 +0000
4429+++ computerjanitord/tests/test_all.py 2011-04-03 15:43:25 +0000
4430@@ -0,0 +1,41 @@
4431+# Copyright (C) 2008-2011 Canonical, Ltd.
4432+#
4433+# This program is free software: you can redistribute it and/or modify
4434+# it under the terms of the GNU General Public License as published by
4435+# the Free Software Foundation, version 3 of the License.
4436+#
4437+# This program is distributed in the hope that it will be useful,
4438+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4439+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4440+# GNU General Public License for more details.
4441+#
4442+# You should have received a copy of the GNU General Public License
4443+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4444+
4445+"""Test suite for Computer Janitor daemon (dbus backend)."""
4446+
4447+from __future__ import absolute_import, unicode_literals
4448+
4449+__metaclass__ = type
4450+__all__ = [
4451+ 'test_suite',
4452+ ]
4453+
4454+
4455+import unittest
4456+
4457+from computerjanitord.tests import test_application
4458+from computerjanitord.tests import test_authenticator
4459+from computerjanitord.tests import test_collector
4460+from computerjanitord.tests import test_state
4461+from computerjanitord.tests import test_whitelist
4462+
4463+
4464+def test_suite():
4465+ suite = unittest.TestSuite()
4466+ suite.addTests(test_application.test_suite())
4467+ suite.addTests(test_authenticator.test_suite())
4468+ suite.addTests(test_collector.test_suite())
4469+ suite.addTests(test_state.test_suite())
4470+ suite.addTests(test_whitelist.test_suite())
4471+ return suite
4472
4473=== added file 'computerjanitord/tests/test_application.py'
4474--- computerjanitord/tests/test_application.py 1970-01-01 00:00:00 +0000
4475+++ computerjanitord/tests/test_application.py 2011-04-03 15:43:25 +0000
4476@@ -0,0 +1,105 @@
4477+# Copyright (C) 2008-2011 Canonical, Ltd.
4478+#
4479+# This program is free software: you can redistribute it and/or modify
4480+# it under the terms of the GNU General Public License as published by
4481+# the Free Software Foundation, version 3 of the License.
4482+#
4483+# This program is distributed in the hope that it will be useful,
4484+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4485+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4486+# GNU General Public License for more details.
4487+#
4488+# You should have received a copy of the GNU General Public License
4489+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4490+
4491+"""Test the plugin application interfaces."""
4492+
4493+from __future__ import absolute_import, unicode_literals
4494+
4495+__metaclass__ = type
4496+__all__ = [
4497+ 'ApplicationTestSetupMixin',
4498+ 'test_suite',
4499+ ]
4500+
4501+
4502+import os
4503+import apt
4504+import apt_pkg
4505+import unittest
4506+import warnings
4507+import pkg_resources
4508+
4509+from contextlib import contextmanager
4510+
4511+import computerjanitord.application
4512+
4513+from computerjanitord.application import (
4514+ Application, MissingLandmarkError, NonDownloadableError)
4515+
4516+
4517+@contextmanager
4518+def landmarks(*packages):
4519+ # Hack the module global list of known landmark packages.
4520+ old_landmarks = computerjanitord.application.LANDMARK_PACKAGES[:]
4521+ computerjanitord.application.LANDMARK_PACKAGES[:] = packages
4522+ yield
4523+ computerjanitord.application.LANDMARK_PACKAGES[:] = old_landmarks
4524+
4525+
4526+class MockCruft:
4527+ def __init__(self, name):
4528+ self.name = name
4529+
4530+ def get_name(self):
4531+ warnings.warn('.get_name() is deprecated; use .name',
4532+ DeprecationWarning)
4533+ return self.name
4534+
4535+
4536+class ApplicationTestSetupMixin:
4537+ """Set up an `Application` instance with test data in its apt_cache."""
4538+
4539+ def setUp(self):
4540+ self.data_dir = os.path.abspath(
4541+ pkg_resources.resource_filename('computerjanitord.tests', 'data'))
4542+ # Make the test insensitive to the platform's architecture.
4543+ apt_pkg.Config.set('APT::Architecture', 'i386')
4544+ self.cache = apt.Cache(rootdir=self.data_dir)
4545+ self.app = Application(self.cache)
4546+
4547+ def tearDown(self):
4548+ # Clear the cache.
4549+ cache_dir = os.path.join(self.data_dir, 'var', 'cache', 'apt')
4550+ for filename in os.listdir(cache_dir):
4551+ if filename.endswith('.bin'):
4552+ os.remove(os.path.join(cache_dir, filename))
4553+
4554+
4555+class TestApplication(unittest.TestCase, ApplicationTestSetupMixin):
4556+ """Test the `Application` interface."""
4557+
4558+ def setUp(self):
4559+ ApplicationTestSetupMixin.setUp(self)
4560+
4561+ def tearDown(self):
4562+ ApplicationTestSetupMixin.tearDown(self)
4563+
4564+ def test_verify_apt_cache_good_path(self):
4565+ # All essential packages are in the cache by default.
4566+ self.assertEqual(self.app.verify_apt_cache(), None)
4567+
4568+ def test_verify_apt_cache_with_nondownloadable_landmark(self):
4569+ # Test that a missing landmark file causes an exception.
4570+ with landmarks('gzip', 'dash-nodownload'):
4571+ self.assertRaises(NonDownloadableError, self.app.verify_apt_cache)
4572+
4573+ def test_verify_apt_cache_with_missing_landmark(self):
4574+ with landmarks('gzip', 'dash', 'i-am-not-here'):
4575+ self.assertRaises(MissingLandmarkError, self.app.verify_apt_cache)
4576+
4577+
4578+def test_suite():
4579+ suite = unittest.TestSuite()
4580+ suite.addTests(unittest.makeSuite(TestApplication))
4581+ return suite
4582
4583=== added file 'computerjanitord/tests/test_authenticator.py'
4584--- computerjanitord/tests/test_authenticator.py 1970-01-01 00:00:00 +0000
4585+++ computerjanitord/tests/test_authenticator.py 2011-04-03 15:43:25 +0000
4586@@ -0,0 +1,97 @@
4587+# Copyright (C) 2008-2011 Canonical, Ltd.
4588+#
4589+# This program is free software: you can redistribute it and/or modify
4590+# it under the terms of the GNU General Public License as published by
4591+# the Free Software Foundation, version 3 of the License.
4592+#
4593+# This program is distributed in the hope that it will be useful,
4594+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4595+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4596+# GNU General Public License for more details.
4597+#
4598+# You should have received a copy of the GNU General Public License
4599+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4600+
4601+"""Test the authenticator."""
4602+
4603+import unittest
4604+
4605+from computerjanitord.authenticator import Authenticator
4606+
4607+SUCCESS_PID = 801
4608+FAILURE_PID = 999
4609+
4610+AUTHENTICATED_USER = 'desktop-user'
4611+IMPOSTER_USER = 'imposter'
4612+EXPECTED_PRIVILEGE = 'com.ubuntu.computerjanitor.cleanpackages'
4613+BOGUS_PRIVILEGE = 'com.example.evil-corp.killsystem'
4614+
4615+
4616+class MockPolicyKit(object):
4617+ """Mock the PolicyKit's CheckAuthorization() method."""
4618+
4619+ def CheckAuthorization(self, subject, privilege, details, flags,
4620+ cancellation_id):
4621+ """See `policykit.CheckAuthorization()`.
4622+
4623+ :return: (is_authorized, is_challenge, details)
4624+ """
4625+ if privilege == BOGUS_PRIVILEGE:
4626+ return False, False, ''
4627+ assert isinstance(subject, tuple) and len(subject) == 2, (
4628+ 'subject is not a 2-tuple')
4629+ assert subject[0] == 'unix-process', 'Badly formed subject'
4630+ assert isinstance(subject[1], dict), 'Badly formed subject details'
4631+ assert subject[1]['start-time'] == 0, 'subject missing start-time'
4632+ if subject[1]['pid'] == SUCCESS_PID:
4633+ return True, False, ''
4634+ else:
4635+ return False, False, ''
4636+
4637+
4638+class TestableAuthenticator(Authenticator):
4639+ """See `Authenticator`."""
4640+
4641+ def _get_policykit_proxy(self):
4642+ """See `Authenticator`."""
4643+ return MockPolicyKit()
4644+
4645+ def _get_sender_pid(self, connection, sender):
4646+ """See `Authenticator`."""
4647+ if sender == AUTHENTICATED_USER:
4648+ return SUCCESS_PID
4649+ else:
4650+ return FAILURE_PID
4651+
4652+
4653+class TestAuthenticator(unittest.TestCase):
4654+ """Tests of the PolicyKit authenticator."""
4655+
4656+ def setUp(self):
4657+ """See `unittest.TestCase`."""
4658+ self.authenticator = TestableAuthenticator()
4659+ self.connection = object()
4660+
4661+ def tearDown(self):
4662+ """See `unittest.TestCase`."""
4663+
4664+ def test_good_path(self):
4665+ # Test for successful authentication.
4666+ self.assertTrue(self.authenticator.authenticate(
4667+ AUTHENTICATED_USER, self.connection, EXPECTED_PRIVILEGE))
4668+
4669+ def test_bogus_privilege(self):
4670+ # Test for bogus privilege fails.
4671+ self.assertFalse(self.authenticator.authenticate(
4672+ AUTHENTICATED_USER, self.connection, BOGUS_PRIVILEGE))
4673+
4674+ def test_unauthorized(self):
4675+ # Test for some imposter not being able to authenticate.
4676+ self.assertFalse(self.authenticator.authenticate(
4677+ IMPOSTER_USER, self.connection, EXPECTED_PRIVILEGE))
4678+
4679+
4680+def test_suite():
4681+ suite = unittest.TestSuite()
4682+ suite.addTests(unittest.makeSuite(TestAuthenticator))
4683+ return suite
4684
4685=== added file 'computerjanitord/tests/test_collector.py'
4686--- computerjanitord/tests/test_collector.py 1970-01-01 00:00:00 +0000
4687+++ computerjanitord/tests/test_collector.py 2011-04-03 15:43:25 +0000
4688@@ -0,0 +1,225 @@
4689+# Copyright (C) 2008-2011 Canonical, Ltd.
4690+#
4691+# This program is free software: you can redistribute it and/or modify
4692+# it under the terms of the GNU General Public License as published by
4693+# the Free Software Foundation, version 3 of the License.
4694+#
4695+# This program is distributed in the hope that it will be useful,
4696+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4697+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4698+# GNU General Public License for more details.
4699+#
4700+# You should have received a copy of the GNU General Public License
4701+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4702+
4703+"""Test the cruft collector."""
4704+
4705+from __future__ import absolute_import, unicode_literals
4706+
4707+__metaclass__ = type
4708+__all__ = [
4709+ 'test_suite',
4710+ ]
4711+
4712+
4713+import os
4714+import shutil
4715+import apt_pkg
4716+import tempfile
4717+import unittest
4718+import pkg_resources
4719+import multiprocessing
4720+
4721+from computerjanitor.plugin import Plugin
4722+from computerjanitord.collector import Collector
4723+from computerjanitord.errors import DuplicateCruftError, PackageCleanupError
4724+from computerjanitord.tests.test_application import ApplicationTestSetupMixin
4725+
4726+
4727+LOCK_FILE = pkg_resources.resource_filename(
4728+ 'computerjanitord.tests',
4729+ os.path.join('data', 'var', 'lib', 'dpkg', 'status'))
4730+
4731+
4732+class MockCruft:
4733+ """Mock cruft that supports the required `get_name()` interface."""
4734+
4735+ def __init__(self, name):
4736+ self.name = name
4737+
4738+ def get_name(self):
4739+ return self.name
4740+
4741+
4742+class MockPlugin(Plugin):
4743+ cruft_class = MockCruft
4744+
4745+ def __init__(self, prefix, shortnames):
4746+ super(MockPlugin, self).__init__()
4747+ self.prefix = prefix
4748+ self.shortnames = shortnames
4749+
4750+ def get_cruft(self):
4751+ for shortname in self.shortnames:
4752+ yield self.cruft_class('{0}:{1}'.format(self.prefix, shortname))
4753+
4754+
4755+class SawAppPlugin(Plugin):
4756+ """All this plugin does is set a marker attribute on the `Application`.
4757+
4758+ This proves that access to the application through the plugin works.
4759+ """
4760+ def get_cruft(self):
4761+ self.app.saw_app_plugin = True
4762+ return []
4763+
4764+
4765+class LockingPlugin(Plugin):
4766+ """All this plugin does is acquire a fake apt lock after cleanup."""
4767+
4768+ def get_cruft(self):
4769+ return []
4770+
4771+ def post_cleanup(self):
4772+ # If the lock cannot be acquired, an exception is raised.
4773+ with apt_pkg.FileLock(LOCK_FILE):
4774+ # Do something.
4775+ pass
4776+
4777+
4778+class MockPluginManager:
4779+ def __init__(self, app, plugin_dirs):
4780+ # Ignore plugin_dirs
4781+ self.app = app
4782+
4783+ def get_plugins(self):
4784+ shortnames = ('one', 'two', 'three')
4785+ for prefix in ('foo', 'bar', 'baz'):
4786+ plugin = MockPlugin(prefix, shortnames)
4787+ plugin.set_application(self.app)
4788+ yield plugin
4789+ for plugin_class in (SawAppPlugin, LockingPlugin):
4790+ plugin = plugin_class()
4791+ plugin.set_application(self.app)
4792+ yield plugin
4793+
4794+
4795+class MockCruftExtra(MockCruft):
4796+ """Cruft with a different class."""
4797+
4798+
4799+class MockPluginExtra(MockPlugin):
4800+ """A mock plugin that returns cruft with a different class."""
4801+
4802+ cruft_class = MockCruftExtra
4803+
4804+
4805+class IgnoredDuplicateCruftPluginManager(MockPluginManager):
4806+ """Add an additional piece of ignorable duplication cruft."""
4807+
4808+ def __init__(self, app, plugin_dirs):
4809+ self.app = app
4810+ # Ignore plugin_dirs
4811+
4812+ def get_plugins(self):
4813+ yield MockPlugin('one', ('foo', 'bar'))
4814+ yield MockPlugin('one', ('baz', 'foo'))
4815+
4816+
4817+class BadDuplicateCruftPluginManager(MockPluginManager):
4818+ """Add an additional piece of bad duplicate cruft."""
4819+ def __init__(self, app, plugin_dirs):
4820+ self.app = app
4821+ # Ignore plugin_dirs
4822+
4823+ def get_plugins(self):
4824+ yield MockPlugin('one', ('foo', 'bar'))
4825+ yield MockPluginExtra('one', ('baz', 'foo'))
4826+
4827+
4828+class TestCollector(unittest.TestCase, ApplicationTestSetupMixin):
4829+ """Test the cruft collector."""
4830+
4831+ def setUp(self):
4832+ # Set up the test data Application.
4833+ ApplicationTestSetupMixin.setUp(self)
4834+ self.tempdir = tempfile.mkdtemp()
4835+ whitelist_dirs = (self.tempdir,)
4836+ with open(os.path.join(self.tempdir, 'one.whitelist'), 'w') as fp:
4837+ print >> fp, 'foo:two'
4838+ print >> fp, 'bar:one'
4839+ print >> fp, 'baz:three'
4840+ self.collector = Collector(self.app, MockPluginManager, whitelist_dirs)
4841+
4842+ def tearDown(self):
4843+ shutil.rmtree(self.tempdir)
4844+ ApplicationTestSetupMixin.tearDown(self)
4845+
4846+ def test_cruft_collector(self):
4847+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4848+ self.assertEqual(cruft_names, set(('foo:one', 'foo:three',
4849+ 'bar:two', 'bar:three',
4850+ 'baz:one', 'baz:two')))
4851+
4852+ def test_plugin_needs_application(self):
4853+ # SawAppPlugin returned by the MockPluginManager sets this attribute
4854+ # on the Application.
4855+ self.assertTrue(self.app.saw_app_plugin)
4856+
4857+ def test_collector_name_mapping(self):
4858+ cruft_keys = set(self.collector.cruft_by_name)
4859+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4860+ self.assertEqual(cruft_keys, cruft_names)
4861+
4862+ def test_cleanup_lock(self):
4863+ # Pretend we're running synaptic at the same time. We have to acquire
4864+ # this fake-synaptic lock in a subprocess.
4865+ lock_event = multiprocessing.Event()
4866+ continue_event = multiprocessing.Event()
4867+ class AptLockThread(multiprocessing.Process):
4868+ def run(self):
4869+ with apt_pkg.FileLock(LOCK_FILE):
4870+ lock_event.set()
4871+ continue_event.wait(1.0)
4872+ apt_locker = AptLockThread()
4873+ apt_locker.start()
4874+ lock_event.wait(1.0)
4875+ self.assertRaises(PackageCleanupError, self.collector.clean, [])
4876+ continue_event.set()
4877+
4878+
4879+class TestDuplicateCruftCollector(
4880+ unittest.TestCase, ApplicationTestSetupMixin):
4881+
4882+ def setUp(self):
4883+ # Set up the test data Application.
4884+ ApplicationTestSetupMixin.setUp(self)
4885+
4886+ def tearDown(self):
4887+ ApplicationTestSetupMixin.tearDown(self)
4888+
4889+ def test_duplicate_cruft_error(self):
4890+ self.assertRaises(DuplicateCruftError, Collector,
4891+ self.app, BadDuplicateCruftPluginManager, [])
4892+
4893+ def test_duplicate_cruft_error_message(self):
4894+ try:
4895+ Collector(self.app, BadDuplicateCruftPluginManager, [])
4896+ except DuplicateCruftError as error:
4897+ self.assertEqual(
4898+ str(error),
4899+ 'Duplicate cruft with different cleanup: one:foo')
4900+ else:
4901+ raise AssertionError('DuplicateCruftError expected')
4902+
4903+ def test_ignored_duplicate_cruft(self):
4904+ collector = Collector(self.app, IgnoredDuplicateCruftPluginManager, [])
4905+ self.assertEqual(list(cruft.get_name() for cruft in collector.cruft),
4906+ ['one:foo', 'one:bar', 'one:baz'])
4907+
4908+
4909+def test_suite():
4910+ suite = unittest.TestSuite()
4911+ suite.addTests(unittest.makeSuite(TestCollector))
4912+ suite.addTests(unittest.makeSuite(TestDuplicateCruftCollector))
4913+ return suite
4914
4915=== added file 'computerjanitord/tests/test_state.py'
4916--- computerjanitord/tests/test_state.py 1970-01-01 00:00:00 +0000
4917+++ computerjanitord/tests/test_state.py 2011-04-03 15:43:25 +0000
4918@@ -0,0 +1,141 @@
4919+# Copyright (C) 2008-2011 Canonical, Ltd.
4920+#
4921+# This program is free software: you can redistribute it and/or modify
4922+# it under the terms of the GNU General Public License as published by
4923+# the Free Software Foundation, version 3 of the License.
4924+#
4925+# This program is distributed in the hope that it will be useful,
4926+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4927+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4928+# GNU General Public License for more details.
4929+#
4930+# You should have received a copy of the GNU General Public License
4931+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4932+
4933+"""Test the package state."""
4934+
4935+from __future__ import absolute_import, unicode_literals
4936+
4937+__metaclass__ = type
4938+__all__ = [
4939+ 'test_suite',
4940+ ]
4941+
4942+
4943+import os
4944+import difflib
4945+import tempfile
4946+import textwrap
4947+import unittest
4948+
4949+from computerjanitord.state import State
4950+
4951+NL = '\n'
4952+
4953+
4954+class TestState(unittest.TestCase):
4955+ """Test the `State` class."""
4956+
4957+ def setUp(self):
4958+ self.state = State()
4959+ # Create a temporary file with some enabled and disabled packages.
4960+ fd, self._state_file = tempfile.mkstemp()
4961+ os.close(fd)
4962+ with open(self._state_file, 'w') as fp:
4963+ print >> fp, textwrap.dedent("""\
4964+ [deb:foo]
4965+ ignore: false
4966+ [deb:bar]
4967+ ignore: true
4968+ [deb:baz]
4969+ ignore: true
4970+ """)
4971+ fd, self._state_file_old = tempfile.mkstemp()
4972+ os.close(fd)
4973+ with open(self._state_file_old, 'w') as fp:
4974+ print >> fp, textwrap.dedent("""\
4975+ [deb:qux]
4976+ enabled: false
4977+ [deb:fno]
4978+ enabled: true
4979+ [deb:bla]
4980+ enabled: true
4981+ """)
4982+ fd, self._write_file = tempfile.mkstemp()
4983+ os.close(fd)
4984+
4985+ def tearDown(self):
4986+ os.remove(self._state_file)
4987+ os.remove(self._state_file_old)
4988+ os.remove(self._write_file)
4989+
4990+ def assertEqualNdiff(self, expected, got):
4991+ expected_lines = expected.splitlines()
4992+ got_lines = got.splitlines()
4993+ self.assertEqual(
4994+ expected, got,
4995+ '\n' + NL.join(difflib.ndiff(expected_lines, got_lines)))
4996+
4997+ def test_initially_no_previously_ignored(self):
4998+ self.assertEqual(self.state.ignore, set())
4999+
5000+ def test_load_state(self):
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches