Merge lp:~gary-lasker/software-center/fix-shutdown-crash-lp996333 into lp:software-center

Proposed by Gary Lasker
Status: Merged
Merged at revision: 3006
Proposed branch: lp:~gary-lasker/software-center/fix-shutdown-crash-lp996333
Merge into: lp:software-center
Diff against target: 189 lines (+77/-34)
4 files modified
softwarecenter/config.py (+20/-5)
softwarecenter/log.py (+2/-6)
softwarecenter/utils.py (+37/-22)
test/test_utils.py (+18/-1)
To merge this branch: bzr merge lp:~gary-lasker/software-center/fix-shutdown-crash-lp996333
Reviewer Review Type Date Requested Status
Michael Vogt Approve
Review via email: mp+105027@code.launchpad.net

Description of the change

This branch fixes bug 996333, which is a crash that can occur if an error is encountered when writing out the software center config file on shutdown. Note that this crash has the nasty side-effect of blocking shutdown, so software-center never exits.

I've also added logging for errors that occur when both writing and reading the config file.

Many thanks for your review!

To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote :

Great that you found the root cause of the issue! The fix is fine, just some thoughts below:

I currently see two conditions for the failure:
a) a file that can not be written exists, so we could use os.access(path, os.W_OK) and try unlink the file
b) disk full, nothing we can do

I think it would be good to add a bit of code that tries to auto-correct (a). Code in softwarecenter/log.py
is a good example for this:

# according to bug 688682 many people have a non-writeable logfile
if os.path.exists(logfile_path) and not os.access(logfile_path, os.W_OK):
    try:
        logging.warn("trying to fix non-writeable logfile")
        os.remove(logfile_path)
    except:
        logging.exception("failed to fix non-writeable logfile")

Do you think you could add this ? Maybe adding a "ensure_writable()" helper to utils.py to reuse the
code from log.py ?

Revision history for this message
Gary Lasker (gary-lasker) wrote :

Hi mvo! This sound great, I'll work on this and push the changes for you to look at.

3002. By Gary Lasker

add a new ensure_file_writeable utility for utils and use it for both the config file and the log file

3003. By Gary Lasker

pep8, oops

Revision history for this message
Gary Lasker (gary-lasker) wrote :

Hi Michael, I've added a new ensure_file_writeable method to utils.py and wired it into the config and log code. Seems to work well!

Please take a look when you have the time. Thanks again!

Revision history for this message
Michael Vogt (mvo) wrote :

This looks good, having a unittest for the ensure_file_writeable() would be ideal, its hopefully
as simple as creating a 0600 file and ensuring that its still there after ensure_writalble and
a 0400 file and ensure if not.

Thinking about it a better name is probably "ensure_file_writable_and_delete_if_not" or "try_remove_if_non_writable()" to ensure that the dangerous nature of it is clearer.

3004. By Gary Lasker

change name of method to ensure_file_writable_and_delete_if_not to make it's dangerousness more clear, and also improve for log message for the same reason

3005. By Gary Lasker

add unit test for new utils function ensure_file_writable_and_delete_if_not

Revision history for this message
Gary Lasker (gary-lasker) wrote :

Hi again mvo, just a note that the unit test is added now and I've renamed the method in utils.

THanks again!

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks for the test you added and the rename of the function.

Unfortunately it has some issues, see my comments below:

[..]
167 + # first test that a non-writable file is deleted
168 + test_file_not_writeable = NamedTemporaryFile()
169 + os.chmod(test_file_not_writeable.name, 600)
[..]

To use mode decimal 600 here is very confusing. Normally you would use mode octal 0600 (as I outlined
in my previous comment) which means "user-read", "user-write" and test if the file is not deleted.
Mode numeric 600 is octal 01130 which does not make much sense in the unix permission model.
You probably want to use 0400 here instead or stat.S_IRUSR.

173 + # then test that a writable file is not deleted
174 + test_file_writeable = NamedTemporaryFile()
175 + os.chmod(test_file_writeable.name, 400)
[..]

Same problem as above, using the decimal value here is confusing, its 0420 which is user-read,user-write
and group-write. A uncommon combination. Please use 0600 here instead or stat.S_IRUSR|stat.S_IWUSR

review: Needs Fixing
3006. By Gary Lasker

use stat values for setting the chmod values in the unit test

Revision history for this message
Gary Lasker (gary-lasker) wrote :

Hi Michael, yes, thanks, decimal values are unuseful indeed, my apologies for the mistake. I updated the test using stat.S_IRUSR and stat.S_IWUSR as these definitely seem more proper (although, hmm, not exactly "readable" with those names, oh well).

Hopefully this will take care of it. Thanks again!

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks, that fixes the issue nicely. I agree that the stat values are not that great either, I added a small
comment there with the octal mode (0600, 0400) too (I find the octal stuff easier than the stat stuff, but
that is just personal taste).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'softwarecenter/config.py'
--- softwarecenter/config.py 2012-04-02 18:23:20 +0000
+++ softwarecenter/config.py 2012-05-11 22:58:19 +0000
@@ -23,9 +23,12 @@
23except ImportError:23except ImportError:
24 from ConfigParser import SafeConfigParser24 from ConfigParser import SafeConfigParser
25import os25import os
26import logging
2627
27from paths import SOFTWARE_CENTER_CONFIG_FILE28from paths import SOFTWARE_CENTER_CONFIG_FILE
2829
30LOG = logging.getLogger(__name__)
31
2932
30class SoftwareCenterConfig(SafeConfigParser):33class SoftwareCenterConfig(SafeConfigParser):
3134
@@ -39,16 +42,28 @@
39 self.configfile = config42 self.configfile = config
40 try:43 try:
41 self.read(self.configfile)44 self.read(self.configfile)
42 except:45 except Exception as e:
43 # don't crash on a corrupted config file46 # don't crash on a corrupted config file
47 LOG.warn("Could not read the config file '%s': %s",
48 self.configfile, e)
44 pass49 pass
4550
46 def write(self):51 def write(self):
47 tmpname = self.configfile + ".new"52 tmpname = self.configfile + ".new"
48 f = open(tmpname, "w")53 from utils import ensure_file_writable_and_delete_if_not
49 SafeConfigParser.write(self, f)54 ensure_file_writable_and_delete_if_not(tmpname)
50 f.close()55 ensure_file_writable_and_delete_if_not(self.configfile)
51 os.rename(tmpname, self.configfile)56 try:
57 f = open(tmpname, "w")
58 SafeConfigParser.write(self, f)
59 f.close()
60 os.rename(tmpname, self.configfile)
61 except Exception as e:
62 # don't crash if there's an error when writing to the config file
63 # (LP: #996333)
64 LOG.warn("Could not write the config file '%s': %s",
65 self.configfile, e)
66 pass
5267
5368
54_software_center_config = None69_software_center_config = None
5570
=== modified file 'softwarecenter/log.py'
--- softwarecenter/log.py 2012-03-15 10:43:13 +0000
+++ softwarecenter/log.py 2012-05-11 22:58:19 +0000
@@ -23,6 +23,7 @@
23import os.path23import os.path
2424
25from paths import SOFTWARE_CENTER_CACHE_DIR25from paths import SOFTWARE_CENTER_CACHE_DIR
26from utils import ensure_file_writable_and_delete_if_not
2627
27""" setup global logging for software-center """28""" setup global logging for software-center """
2829
@@ -110,12 +111,7 @@
110 os.makedirs(SOFTWARE_CENTER_CACHE_DIR)111 os.makedirs(SOFTWARE_CENTER_CACHE_DIR)
111112
112# according to bug 688682 many people have a non-writeable logfile113# according to bug 688682 many people have a non-writeable logfile
113if os.path.exists(logfile_path) and not os.access(logfile_path, os.W_OK):114ensure_file_writable_and_delete_if_not(logfile_path)
114 try:
115 logging.warn("trying to fix non-writeable logfile")
116 os.remove(logfile_path)
117 except:
118 logging.exception("failed to fix non-writeable logfile")
119115
120logfile_handler = logging.handlers.RotatingFileHandler(116logfile_handler = logging.handlers.RotatingFileHandler(
121 logfile_path, maxBytes=100 * 1000, backupCount=5)117 logfile_path, maxBytes=100 * 1000, backupCount=5)
122118
=== modified file 'softwarecenter/utils.py'
--- softwarecenter/utils.py 2012-04-25 08:19:42 +0000
+++ softwarecenter/utils.py 2012-05-11 22:58:19 +0000
@@ -655,6 +655,43 @@
655 os.close(lock)655 os.close(lock)
656656
657657
658def make_string_from_list(base_str, item_list):
659 """ This function takes a list of items and builds a nice human readable
660 string with it of the form. Note that the base string needs a "%s".
661 Example return:
662 The base string with the list items a,b and c in it.
663 Note that base_str needs to be a ngettext string already, so the
664 example usage is:
665 l = ["foo", "bar"]
666 base_str = ngettext("This list: %s.", "This list: %s", len(l))
667 s = make_string_from_list(base_string, l)
668 """
669 list_str = item_list[0]
670 if len(item_list) > 1:
671 # TRANSLATORS: this is a generic list delimit char, e.g. "foo, bar"
672 list_str = _(", ").join(item_list[:-1])
673 # TRANSLATORS: this is the last part of a list, e.g. "foo, bar and baz"
674 list_str = _("%s and %s") % (list_str,
675 item_list[-1])
676 s = base_str % list_str
677 return s
678
679
680def ensure_file_writable_and_delete_if_not(file_path):
681 """ This function checks for writeable access to a file and attempts to
682 correct it if it is found to indeed be set as unwriteable
683 """
684 if os.path.exists(file_path) and not os.access(file_path, os.W_OK):
685 try:
686 LOG.warn("encountered non-writeable file, attempting to fix "
687 "by deleting: %s",
688 file_path)
689 os.remove(file_path)
690 except Exception as e:
691 LOG.exception("failed to fix non-writeable file '%s': %s",
692 file_path, e)
693
694
658class SimpleFileDownloader(GObject.GObject):695class SimpleFileDownloader(GObject.GObject):
659696
660 LOG = logging.getLogger("softwarecenter.simplefiledownloader")697 LOG = logging.getLogger("softwarecenter.simplefiledownloader")
@@ -782,28 +819,6 @@
782 self.emit('file-download-complete', self.dest_file_path)819 self.emit('file-download-complete', self.dest_file_path)
783820
784821
785def make_string_from_list(base_str, item_list):
786 """ This function takes a list of items and builds a nice human readable
787 string with it of the form. Note that the base string needs a "%s".
788 Example return:
789 The base string with the list items a,b and c in it.
790 Note that base_str needs to be a ngettext string already, so the
791 example usage is:
792 l = ["foo", "bar"]
793 base_str = ngettext("This list: %s.", "This list: %s", len(l))
794 s = make_string_from_list(base_string, l)
795 """
796 list_str = item_list[0]
797 if len(item_list) > 1:
798 # TRANSLATORS: this is a generic list delimit char, e.g. "foo, bar"
799 list_str = _(", ").join(item_list[:-1])
800 # TRANSLATORS: this is the last part of a list, e.g. "foo, bar and baz"
801 list_str = _("%s and %s") % (list_str,
802 item_list[-1])
803 s = base_str % list_str
804 return s
805
806
807# those helpers are packaging system specific822# those helpers are packaging system specific
808from softwarecenter.db.pkginfo import get_pkg_info823from softwarecenter.db.pkginfo import get_pkg_info
809# do not call here get_pkg_info, since package switch may not have been set824# do not call here get_pkg_info, since package switch may not have been set
810825
=== modified file 'test/test_utils.py'
--- test/test_utils.py 2012-04-26 00:27:45 +0000
+++ test/test_utils.py 2012-05-11 22:58:19 +0000
@@ -4,6 +4,7 @@
4import glob4import glob
5import multiprocessing5import multiprocessing
6import os6import os
7import stat
7import subprocess8import subprocess
8import tempfile9import tempfile
9import time10import time
@@ -158,7 +159,23 @@
158 self.assertEqual(159 self.assertEqual(
159 make_string_from_list(base, l),160 make_string_from_list(base, l),
160 "There was a problem posting this review to twister, factbook, identi.catz and baz (omg!)")161 "There was a problem posting this review to twister, factbook, identi.catz and baz (omg!)")
161 162
163 def test_ensure_file_writable_and_delete_if_not(self):
164 from softwarecenter.utils import ensure_file_writable_and_delete_if_not
165 from tempfile import NamedTemporaryFile
166 # first test that a non-writable file is deleted
167 test_file_not_writeable = NamedTemporaryFile()
168 os.chmod(test_file_not_writeable.name, stat.S_IRUSR)
169 self.assertFalse(os.access(test_file_not_writeable.name, os.W_OK))
170 ensure_file_writable_and_delete_if_not(test_file_not_writeable.name)
171 self.assertFalse(os.path.exists(test_file_not_writeable.name))
172 # then test that a writable file is not deleted
173 test_file_writeable = NamedTemporaryFile()
174 os.chmod(test_file_writeable.name, stat.S_IRUSR|stat.S_IWUSR)
175 self.assertTrue(os.access(test_file_writeable.name, os.W_OK))
176 ensure_file_writable_and_delete_if_not(test_file_writeable.name)
177 self.assertTrue(os.path.exists(test_file_writeable.name))
178
162179
163class TestExpungeCache(unittest.TestCase):180class TestExpungeCache(unittest.TestCase):
164181

Subscribers

People subscribed via source and target branches