Merge lp:~gary-lasker/software-center/fix-makedirs-race-crashes into lp:software-center

Proposed by Gary Lasker
Status: Merged
Merged at revision: 3006
Proposed branch: lp:~gary-lasker/software-center/fix-makedirs-race-crashes
Merge into: lp:software-center
Diff against target: 174 lines (+82/-27)
4 files modified
softwarecenter/config.py (+2/-2)
softwarecenter/log.py (+3/-3)
softwarecenter/utils.py (+42/-22)
test/test_logging.py (+35/-0)
To merge this branch: bzr merge lp:~gary-lasker/software-center/fix-makedirs-race-crashes
Reviewer Review Type Date Requested Status
software-store-developers Pending
Review via email: mp+105389@code.launchpad.net

Description of the change

This branch fixes one of the most common software-center crashes as reported at errors.ubuntu.com, bug 743003, as well as bug 621182. These crashes appear to occur in new installs of Ubuntu, and are the result of a race between different processes creating the cache directory (for bug 743003) and the config directory (for bug 621182). The crash occurs because os.makedirs will throw an OSError if it is called and the directory is found to already exist. The fix for both of these use a new method in utils called safe_makedirs.

I also added a new unit test, test_logging.py, as suggested in comment #3 of bug 743003. This unit test checks the case where the cache directory is found to be not writable, and so is moved aside and a new cache directory created. I'm not sure if the code in log.py itself needs a change as it seems to work fine. It has been modified to use the new safe_makedirs method in any case.

Thanks for your review!

To post a comment you must log in.
3007. By Gary Lasker

make test_logging.py a little better

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

Thanks for the branch, it looks good, but there is a issue with the test.

I noticed that you use "os.chmod(cache_dir, 600)" here. As this is decimal 600 (and not octal) its confusing for me to read and will result in a odd permission on disk (see my comment for https://code.launchpad.net/~gary-lasker/software-center/fix-shutdown-crash-lp996333/+merge/105027). Please use either 0400 or stat.S_IRUSR.

It would be nice if safe_makedirs() would get a individual test in test_utils.py (even though its pretty trivial).

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

Oh sure, thanks, I'll fix that (the value) for both merges. Often, with very trivial details like this, I'll just make the tweak and do the merge, and then just mention the fix here. That way we expedite the merge (reduce the back and forth), and still share knowledge and best practices. I've seen you do that yourself for many merges, so I hope you will feel free to do the same for trivial cases like this.

As far as a test for safe_makedirs, of course my impulse is to write it but I think there comes a point where a test gets to be overkill, and this is one of those cases imo. Meaning, the test itself will probably be 2-3x the lines of code of the function it's actually testing (which is pretty trivial, as you said, really little more than a single line protected by a try/except block). But it's not a problem, I can add it if you really want me to.

3008. By Gary Lasker

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

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

Hi Michael, I updated the unit test to use stat.S_IRUSR as this definitely seem more proper (as I mentioned in the other MP). Thanks again!

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

I took a quick look at making a unit test for safe_makedirs, but I don't immediately see a way to simulate the race condition that causes the error. If you would like a test for that and have any ideas about how to simulate the condition, please let me know and I'll be happy to write the test. As I mentioned, though, I think it's a trivial enough method that a unit test for it is probably overkill.

Thanks again!

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

Thanks for updating the branch. Its fine now and I merged it into 5.2

I wrote a small unittest that does not exactly simulate the race, but gets close by patching os.path.exists() which will lead to calling the makedir code in any case so we can test that it does not crash for existing dirs and also propergate regular errors still.

As for the earlier comment, I did not want to fix the decimal vs octal issue during the merge because to me its not a trivial detail as it touched the test in such a way that the logic had to be inverted and it also touched two branches.

For small stuff I still do it (and you are always free to correct me here of course when I overdo it or make mistakes).

Small stuff to me is something like:
             LOG.warn("encountered non-writeable file, attempting to fix "
- "by deleting: %s",
- file_path)
+ "by deleting: %s" % file_path)
(not the "," vs the "%s") which is not really as issue as LOG will happily deal with the "," but the "%" is
more python. But no logic changed etc.

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

I need to correct myself here about my LOG.warn() change. As Natalia pointed out in another branch its actually preferable to use "," in the logging module than to use "%" as it means that on string format mismatches there
will be no expection in the logging. This is a good reason to use "," and we should keep that in mind for
future branches as a good policy.

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:48:19 +0000
@@ -31,8 +31,8 @@
3131
32 def __init__(self, config):32 def __init__(self, config):
33 SafeConfigParser.__init__(self)33 SafeConfigParser.__init__(self)
34 if not os.path.exists(os.path.dirname(config)):34 from utils import safe_makedirs
35 os.makedirs(os.path.dirname(config))35 safe_makedirs(os.path.dirname(config))
36 # we always want this section, even on fresh installs36 # we always want this section, even on fresh installs
37 self.add_section("general")37 self.add_section("general")
38 # read the config38 # read the config
3939
=== modified file 'softwarecenter/log.py'
--- softwarecenter/log.py 2012-03-15 10:43:13 +0000
+++ softwarecenter/log.py 2012-05-11 22:48: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 safe_makedirs
2627
27""" setup global logging for software-center """28""" setup global logging for software-center """
2829
@@ -92,8 +93,7 @@
92handler.addFilter(NullFilterThatWarnsAboutRootLoggerUsage())93handler.addFilter(NullFilterThatWarnsAboutRootLoggerUsage())
9394
94# create log file95# create log file
95if not os.path.exists(SOFTWARE_CENTER_CACHE_DIR):96safe_makedirs(SOFTWARE_CENTER_CACHE_DIR)
96 os.makedirs(SOFTWARE_CENTER_CACHE_DIR)
97logfile_path = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "software-center.log")97logfile_path = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "software-center.log")
9898
99# try to fix inaccessible s-c directory (#688682)99# try to fix inaccessible s-c directory (#688682)
@@ -107,7 +107,7 @@
107 if not os.path.exists(target):107 if not os.path.exists(target):
108 os.rename(SOFTWARE_CENTER_CACHE_DIR, target)108 os.rename(SOFTWARE_CENTER_CACHE_DIR, target)
109 break109 break
110 os.makedirs(SOFTWARE_CENTER_CACHE_DIR)110 safe_makedirs(SOFTWARE_CENTER_CACHE_DIR)
111111
112# according to bug 688682 many people have a non-writeable logfile112# 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):113if os.path.exists(logfile_path) and not os.access(logfile_path, os.W_OK):
114114
=== modified file 'softwarecenter/utils.py'
--- softwarecenter/utils.py 2012-04-25 08:19:42 +0000
+++ softwarecenter/utils.py 2012-05-11 22:48:19 +0000
@@ -28,6 +28,7 @@
28import traceback28import traceback
29import time29import time
30import xml.sax.saxutils30import xml.sax.saxutils
31import errno
3132
32# py3 compat33# py3 compat
33try:34try:
@@ -655,6 +656,47 @@
655 os.close(lock)656 os.close(lock)
656657
657658
659def make_string_from_list(base_str, item_list):
660 """ This function takes a list of items and builds a nice human readable
661 string with it of the form. Note that the base string needs a "%s".
662 Example return:
663 The base string with the list items a,b and c in it.
664 Note that base_str needs to be a ngettext string already, so the
665 example usage is:
666 l = ["foo", "bar"]
667 base_str = ngettext("This list: %s.", "This list: %s", len(l))
668 s = make_string_from_list(base_string, l)
669 """
670 list_str = item_list[0]
671 if len(item_list) > 1:
672 # TRANSLATORS: this is a generic list delimit char, e.g. "foo, bar"
673 list_str = _(", ").join(item_list[:-1])
674 # TRANSLATORS: this is the last part of a list, e.g. "foo, bar and baz"
675 list_str = _("%s and %s") % (list_str,
676 item_list[-1])
677 s = base_str % list_str
678 return s
679
680
681def safe_makedirs(dir_path):
682 """ This function can be used in place of a straight os.makedirs to
683 handle the possibility of a race condition when more than one
684 process may potentially be creating the same directory (if this occurs,
685 it will throw an OSError, see for example LP: #743003)
686 """
687 if not os.path.exists(dir_path):
688 try:
689 os.makedirs(dir_path)
690 except OSError as e:
691 if e.errno == errno.EEXIST:
692 # it seems that another process has already created this
693 # directory in the meantime, that's ok
694 pass
695 else:
696 # the error is due to something else, so we want to raise it
697 raise
698
699
658class SimpleFileDownloader(GObject.GObject):700class SimpleFileDownloader(GObject.GObject):
659701
660 LOG = logging.getLogger("softwarecenter.simplefiledownloader")702 LOG = logging.getLogger("softwarecenter.simplefiledownloader")
@@ -782,28 +824,6 @@
782 self.emit('file-download-complete', self.dest_file_path)824 self.emit('file-download-complete', self.dest_file_path)
783825
784826
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 specific827# those helpers are packaging system specific
808from softwarecenter.db.pkginfo import get_pkg_info828from softwarecenter.db.pkginfo import get_pkg_info
809# do not call here get_pkg_info, since package switch may not have been set829# do not call here get_pkg_info, since package switch may not have been set
810830
=== added file 'test/test_logging.py'
--- test/test_logging.py 1970-01-01 00:00:00 +0000
+++ test/test_logging.py 2012-05-11 22:48:19 +0000
@@ -0,0 +1,35 @@
1#!/usr/bin/python
2
3import os
4import stat
5import unittest
6
7from testutils import setup_test_env
8setup_test_env()
9
10
11class TestLogging(unittest.TestCase):
12 """ tests the sc logging facilities """
13
14 def test_no_write_access_for_cache_dir(self):
15 """ test for bug LP: #688682 """
16 # make the test cache dir non-writeable
17 import softwarecenter.paths
18 cache_dir = softwarecenter.paths.SOFTWARE_CENTER_CACHE_DIR
19 os.chmod(cache_dir, stat.S_IRUSR)
20 self.assertFalse(os.access(cache_dir, os.W_OK))
21 # and then start up the logger
22 import softwarecenter.log
23 softwarecenter.log # pyflakes
24 # check that the old directory was moved aside (renamed with a ".0" appended)
25 self.assertTrue(os.path.exists(cache_dir + ".0"))
26 self.assertFalse(os.path.exists(cache_dir + ".1"))
27 # and check that the new directory was created and is now writable
28 self.assertTrue(os.path.exists(cache_dir))
29 self.assertTrue(os.access(cache_dir, os.W_OK))
30
31
32if __name__ == "__main__":
33 import logging
34 logging.basicConfig(level=logging.DEBUG)
35 unittest.main()

Subscribers

People subscribed via source and target branches