Merge lp:~bigkevmcd/landscape-client/517453-process-race-condition into lp:~landscape/landscape-client/trunk

Proposed by Kevin McDermott
Status: Merged
Approved by: Jamu Kakar
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~bigkevmcd/landscape-client/517453-process-race-condition
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 230 lines (+114/-71)
3 files modified
landscape/lib/process.py (+64/-69)
landscape/lib/tests/test_process.py (+49/-1)
landscape/monitor/activeprocessinfo.py (+1/-1)
To merge this branch: bzr merge lp:~bigkevmcd/landscape-client/517453-process-race-condition
Reviewer Review Type Date Requested Status
Thomas Herve (community) Approve
Jamu Kakar (community) Approve
Sidnei da Silva (community) Approve
Review via email: mp+18697@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Sidnei da Silva (sidnei) wrote :

[1] tempfile and shutil don't seem to be used (at least in the patch).

Looks good, +1.

review: Approve
202. By Kevin McDermott

Remove extraneous imports...

Revision history for this message
Jamu Kakar (jkakar) wrote :

Nice work, +1!

review: Approve
Revision history for this message
Thomas Herve (therve) wrote :

[1] I think this patch will break on dapper which is using Python 2.4.

[2] There is still an uncovered part when the stat file is read. I suggest having a try/except which covers the whole function, with small try/finally to close file handles, instead of trying to catch errors one by one.

review: Needs Fixing
203. By Kevin McDermott

Add full process read try:except coverage.

204. By Kevin McDermott

Make python2.4 compatible.

205. By Kevin McDermott

Add Docstring.

Revision history for this message
Thomas Herve (therve) wrote :

Cool! Just one last thing:

[3] As you regrouped with only one finally, you need to close the file after each operation.

+1!

review: Approve
206. By Kevin McDermott

Remove the try: finally: and close files manually.

207. By Kevin McDermott

Revert the removal.

208. By Kevin McDermott

Wrap each access in try: finally: and allow the except: to handle any failure.

209. By Kevin McDermott

Add assertions to check that all files get closed after being opened.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'landscape/lib/process.py'
--- landscape/lib/process.py 2008-08-13 14:39:53 +0000
+++ landscape/lib/process.py 2010-02-08 15:30:35 +0000
@@ -47,88 +47,83 @@
47 yield process_info47 yield process_info
4848
49 def get_process_info(self, process_id):49 def get_process_info(self, process_id):
50 """
51 Parse the /proc/<pid>/cmdline and /proc/<pid>/status files for
52 information about the running process with process_id.
53
54 The /proc filesystem doesn't behave like ext2, open files can disappear
55 during the read process.
56 """
50 cmd_line_name = ""57 cmd_line_name = ""
51 process_dir = os.path.join(self._proc_dir, str(process_id))58 process_dir = os.path.join(self._proc_dir, str(process_id))
52 process_info = {"pid": process_id}59 process_info = {"pid": process_id}
5360
54 try:61 try:
55 file = open(os.path.join(process_dir, "cmdline"), "r")62 file = open(os.path.join(process_dir, "cmdline"), "r")
56 except IOError:63 try:
57 # Handle the race that happens when we find a process64 # cmdline is a \0 separated list of strings
58 # which terminates before we open the stat file.65 # We take the first, and then strip off the path, leaving us with
59 return None66 # the basename.
6067 cmd_line = file.readline()
61 try:68 cmd_line_name = os.path.basename(cmd_line.split("\0")[0])
62 # cmdline is a \0 separated list of strings69 finally:
63 # We take the first, and then strip off the path, leaving us with70 file.close()
64 # the basename.71
65 cmd_line = file.readline()
66 cmd_line_name = os.path.basename(cmd_line.split("\0")[0])
67 finally:
68 file.close()
69
70 try:
71 file = open(os.path.join(process_dir, "status"), "r")72 file = open(os.path.join(process_dir, "status"), "r")
72 except IOError:73 try:
73 # Handle the race that happens when we find a process74 for line in file:
74 # which terminates before we open the status file.75 parts = line.split(":", 1)
75 return None76 if parts[0] == "Name":
7677 process_info["name"] = (cmd_line_name.strip() or
77 try:78 parts[1].strip())
78 for line in file:79 elif parts[0] == "State":
79 parts = line.split(":", 1)80 state = parts[1].strip()
80 if parts[0] == "Name":81 process_info["state"] = STATES[state]
81 process_info["name"] = (cmd_line_name.strip() or82 elif parts[0] == "Uid":
82 parts[1].strip())83 value_parts = parts[1].split()
83 elif parts[0] == "State":84 process_info["uid"] = int(value_parts[0])
84 state = parts[1].strip()85 elif parts[0] == "Gid":
85 process_info["state"] = STATES[state]86 value_parts = parts[1].split()
86 elif parts[0] == "Uid":87 process_info["gid"] = int(value_parts[0])
87 value_parts = parts[1].split()88 elif parts[0] == "VmSize":
88 process_info["uid"] = int(value_parts[0])89 value_parts = parts[1].split()
89 elif parts[0] == "Gid":90 process_info["vm-size"] = int(value_parts[0])
90 value_parts = parts[1].split()91 break
91 process_info["gid"] = int(value_parts[0])92 finally:
92 elif parts[0] == "VmSize":93 file.close()
93 value_parts = parts[1].split()94
94 process_info["vm-size"] = int(value_parts[0])
95 break
96 finally:
97 file.close()
98
99 try:
100 file = open(os.path.join(process_dir, "stat"), "r")95 file = open(os.path.join(process_dir, "stat"), "r")
96 try:
97 # These variable names are lifted directly from proc(5)
98 # utime: The number of jiffies that this process has been scheduled in
99 # user mode.
100 # stime: The number of jiffies that this process has been scheduled in
101 # kernel mode.
102 # cutime: The number of jiffies that this process's waited-for children
103 # have been scheduled in user mode.
104 # cstime: The number of jiffies that this process's waited-for children
105 # have been scheduled in kernel mode.
106 parts = file.read().split()
107 start_time = int(parts[21])
108 utime = int(parts[13])
109 stime = int(parts[14])
110 uptime = self._uptime or get_uptime()
111 pcpu = calculate_pcpu(utime, stime, uptime,
112 start_time, self._jiffies_per_sec)
113 process_info["percent-cpu"] = pcpu
114 delta = timedelta(0, start_time // self._jiffies_per_sec)
115 if self._boot_time is None:
116 logging.warning("Skipping process (PID %s) without boot time.")
117 return None
118 process_info["start-time"] = to_timestamp(self._boot_time + delta)
119 finally:
120 file.close()
121
101 except IOError:122 except IOError:
102 # Handle the race that happens when we find a process123 # Handle the race that happens when we find a process
103 # which terminates before we open the stat file.124 # which terminates before we open the stat file.
104 return None125 return None
105126
106 # These variable names are lifted directly from proc(5)
107 # utime: The number of jiffies that this process has been scheduled in
108 # user mode.
109 # stime: The number of jiffies that this process has been scheduled in
110 # kernel mode.
111 # cutime: The number of jiffies that this process's waited-for children
112 # have been scheduled in user mode.
113 # cstime: The number of jiffies that this process's waited-for children
114 # have been scheduled in kernel mode.
115 try:
116 parts = file.read().split()
117 start_time = int(parts[21])
118 utime = int(parts[13])
119 stime = int(parts[14])
120 uptime = self._uptime or get_uptime()
121 pcpu = calculate_pcpu(utime, stime, uptime,
122 start_time, self._jiffies_per_sec)
123 process_info["percent-cpu"] = pcpu
124 delta = timedelta(0, start_time // self._jiffies_per_sec)
125 if self._boot_time is None:
126 logging.warning("Skipping process (PID %s) without boot time.")
127 return None
128 process_info["start-time"] = to_timestamp(self._boot_time + delta)
129 finally:
130 file.close()
131
132 assert("pid" in process_info and "state" in process_info127 assert("pid" in process_info and "state" in process_info
133 and "name" in process_info and "uid" in process_info128 and "name" in process_info and "uid" in process_info
134 and "gid" in process_info and "start-time" in process_info)129 and "gid" in process_info and "start-time" in process_info)
135130
=== modified file 'landscape/lib/tests/test_process.py'
--- landscape/lib/tests/test_process.py 2008-06-25 08:01:39 +0000
+++ landscape/lib/tests/test_process.py 2010-02-08 15:30:35 +0000
@@ -2,7 +2,55 @@
22
3from landscape.tests.helpers import LandscapeTest3from landscape.tests.helpers import LandscapeTest
44
5from landscape.lib.process import calculate_pcpu5from landscape.lib.process import calculate_pcpu, ProcessInformation
6
7
8class ProcessInfoTest(LandscapeTest):
9
10 def test_missing_process_race(self):
11 """
12 We use os.listdir("/proc") to get the list of active processes, if a
13 process ends before we attempt to read the process' information, then
14 this should not trigger an error.
15 """
16 listdir_mock = self.mocker.replace("os.listdir")
17 listdir_mock("/proc")
18 self.mocker.result(["12345"])
19
20 class FakeFile(object):
21
22 def __init__(self, response=""):
23 self._response = response
24 self.closed = False
25
26 def readline(self):
27 return self._response
28
29 def __iter__(self):
30 if self._response is None:
31 raise IOError("Fake file error")
32 else:
33 yield self._response
34
35 def close(self):
36 self.closed = True
37
38 open_mock = self.mocker.replace("__builtin__.open")
39 open_mock("/proc/12345/cmdline", "r")
40 fakefile1 = FakeFile("test-binary")
41 self.mocker.result(fakefile1)
42
43 open_mock("/proc/12345/status", "r")
44 fakefile2 = FakeFile(None)
45 self.mocker.result(fakefile2)
46
47 self.mocker.replay()
48
49 process_info = ProcessInformation("/proc")
50 processes = list(process_info.get_all_process_info())
51 self.assertEquals(processes, [])
52 self.assertTrue(fakefile1.closed)
53 self.assertTrue(fakefile2.closed)
654
755
8class CalculatePCPUTest(unittest.TestCase):56class CalculatePCPUTest(unittest.TestCase):
957
=== modified file 'landscape/monitor/activeprocessinfo.py'
--- landscape/monitor/activeprocessinfo.py 2009-10-06 16:27:23 +0000
+++ landscape/monitor/activeprocessinfo.py 2010-02-08 15:30:35 +0000
@@ -41,7 +41,7 @@
41 if self._first_run:41 if self._first_run:
42 message["kill-all-processes"] = True42 message["kill-all-processes"] = True
43 message.update(self._detect_process_changes())43 message.update(self._detect_process_changes())
44 44
45 if message:45 if message:
46 message["type"] = "active-process-info"46 message["type"] = "active-process-info"
47 return message47 return message

Subscribers

People subscribed via source and target branches

to all changes: