Merge lp:~cmiller/desktopcouch/getport-at-call-time into lp:desktopcouch

Proposed by Chad Miller
Status: Merged
Approved by: Elliot Murphy
Approved revision: 44
Merged at revision: not available
Proposed branch: lp:~cmiller/desktopcouch/getport-at-call-time
Merge into: lp:desktopcouch
Diff against target: None lines
To merge this branch: bzr merge lp:~cmiller/desktopcouch/getport-at-call-time
Reviewer Review Type Date Requested Status
Elliot Murphy (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+10105@code.launchpad.net

This proposal supersedes a proposal from 2009-08-13.

Commit message

Use subprocess.Popen and ourselves to the wait()ing, since subprocess.call() is buggy. There's still an EINTR bug in subprocess, though.

Occasionally stop couchdb in tests, so we exercise the automatic starting code. This will lead to spurious errors because of the aforementioned subprocess bug, but it's the right thing to do.

Abstract away some of the linuxisms and complain if we're run on an unsupported OS.

Fix a race condition in the process-testing code.

Replace the TestCase module with one that doesn't complain of dirty twisted reactors.

Add a means of stopping the desktop couchdb daemon.

Add an additional check that a found PID and process named correctly is indeed a process that this user started, so we don't try to talk to other local users' desktop couchdbs.

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

Tests run, code changes look good to me.

review: Approve
Revision history for this message
Elliot Murphy (statik) wrote :

yay, desperately needed cleanup.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'desktopcouch/__init__.py'
2--- desktopcouch/__init__.py 2009-08-04 11:15:52 +0000
3+++ desktopcouch/__init__.py 2009-08-13 12:12:22 +0000
4@@ -21,52 +21,66 @@
5 import errno
6 import time
7
8-def find_pid():
9+def find_pid(start_if_not_running=True):
10 # Work out whether CouchDB is running by looking at its pid file
11- from desktopcouch import local_files
12- pid = ''
13- try:
14- fp = open(local_files.FILE_PID)
15- pid = int(fp.read())
16- fp.close()
17- except IOError:
18- pass
19+ def get_pid():
20+ from desktopcouch import local_files
21+ try:
22+ pid_file = local_files.FILE_PID
23+ with open(pid_file) as fp:
24+ try:
25+ return int(fp.read())
26+ except ValueError:
27+ return None
28+ except IOError:
29+ return None
30
31- if not is_couchdb(pid):
32+ pid = get_pid()
33+ if not process_is_couchdb(pid) and start_if_not_running:
34 # pidfile is stale
35 # start CouchDB by running the startup script
36- print "Desktop CouchDB is not running; starting it."
37+ print "Desktop CouchDB is not running; starting it.",
38 from desktopcouch import start_local_couchdb
39 start_local_couchdb.start_couchdb()
40- time.sleep(2) # give the process a chance to start
41+ for timeout in xrange(1000):
42+ pid = get_pid()
43+ if process_is_couchdb(pid):
44+ break
45+ print ".",
46+ time.sleep(0.1)
47
48- # get the pid
49- try:
50- with open(local_files.FILE_PID) as pid_file:
51- pid = int(pid_file.read().strip())
52- except IOError, e:
53- if e.errno == ENOENT:
54+ if process_is_couchdb(pid):
55+ print " done."
56+ else:
57+ print " failed."
58 raise RuntimeError("desktop-couch not started")
59- else:
60- raise
61+
62 return pid
63
64-def is_couchdb(pid):
65+def process_is_couchdb__linux(pid):
66+ if not isinstance(pid, int):
67+ return False
68+
69 proc_dir = "/proc/%s" % (pid,)
70
71- # check to make sure that the process still exists
72- if not os.path.isdir(proc_dir):
73- return False
74-
75- # check to make sure it is actually a desktop-couch instance
76- with open(os.path.join(proc_dir, 'cmdline')) as cmd_file:
77- cmd = cmd_file.read()
78- if re.search('desktop-couch', cmd) is None:
79+ try:
80+ # check to make sure it is actually a desktop-couch instance
81+ with open(os.path.join(proc_dir, 'cmdline')) as cmd_file:
82+ cmd = cmd_file.read()
83+ if '/desktop-couch' not in cmd:
84+ return False
85+
86+ # make sure it's our process.
87+ if not os.access(os.path.join(proc_dir, "mem"), os.W_OK):
88+ return False
89+
90+ except IOError:
91 return False
92
93 return True
94
95-def find_port(pid):
96+
97+def find_port__linux(pid):
98 # Look in the CouchDB log to find the port number, someday.
99 # Currently, we have to grovel around in /proc instead.
100 # Oh, the huge manatee... (this replaced an lsof shell recipe
101@@ -76,8 +90,12 @@
102
103 # enumerate the process' file descriptors
104 fd_dir = os.path.join(proc_dir, 'fd')
105- fd_paths = [os.readlink(os.path.join(fd_dir, fd))
106- for fd in os.listdir(fd_dir)]
107+ try:
108+ fd_paths = [os.readlink(os.path.join(fd_dir, fd))
109+ for fd in os.listdir(fd_dir)]
110+ except OSError:
111+ raise RuntimeError("Unable to find file descriptors in /proc")
112+
113
114 # identify socket fds
115 socket_matches = [re.match('socket:\\[([0-9]+)\\]', p) for p in fd_paths]
116@@ -108,9 +126,24 @@
117 for line in tcp_file:
118 match = listening_regexp.match(line)
119 if match is not None:
120- port = int(match.group(1), 16)
121+ port = str(int(match.group(1), 16))
122 break
123 if port is None:
124 raise RuntimeError("Unable to find listening port")
125
126- return str(port)
127+ return port
128+
129+
130+
131+import platform
132+os_name = platform.system()
133+try:
134+ process_is_couchdb = {
135+ "Linux":process_is_couchdb__linux
136+ } [os_name]
137+
138+ find_port = {
139+ "Linux":find_port__linux
140+ } [os_name]
141+except KeyError:
142+ raise NotImplementedError("os %r is not yet supported" % (os_name,))
143
144=== modified file 'desktopcouch/records/tests/test_field_registry.py'
145--- desktopcouch/records/tests/test_field_registry.py 2009-07-08 17:48:11 +0000
146+++ desktopcouch/records/tests/test_field_registry.py 2009-08-13 16:06:20 +0000
147@@ -18,7 +18,7 @@
148 """Test cases for field mapping"""
149
150 import copy
151-from twisted.trial.unittest import TestCase as TwistedTestCase
152+from testtools import TestCase
153 from desktopcouch.records.field_registry import (
154 SimpleFieldMapping, MergeableListFieldMapping, Transformer)
155 from desktopcouch.records.record import Record
156@@ -53,7 +53,7 @@
157 super(AppTransformer, self).__init__('Test App', field_registry)
158
159
160-class TestFieldMapping(TwistedTestCase):
161+class TestFieldMapping(TestCase):
162 """Test Case for FieldMapping objects."""
163
164 def setUp(self):
165@@ -85,7 +85,7 @@
166 self.assertEqual(None, mapping.getValue(record))
167
168
169-class TestTransformer(TwistedTestCase):
170+class TestTransformer(TestCase):
171 """Test application specific transformer classes"""
172
173 def setUp(self):
174
175=== modified file 'desktopcouch/records/tests/test_record.py'
176--- desktopcouch/records/tests/test_record.py 2009-07-08 17:48:11 +0000
177+++ desktopcouch/records/tests/test_record.py 2009-08-13 16:06:20 +0000
178@@ -18,7 +18,7 @@
179
180 """Tests for the RecordDict object on which the Contacts API is built."""
181
182-from twisted.trial.unittest import TestCase as TwistedTestCase
183+from testtools import TestCase
184
185 # pylint does not like relative imports from containing packages
186 # pylint: disable-msg=F0401
187@@ -26,7 +26,7 @@
188 record_factory, IllegalKeyException, validate)
189
190
191-class TestRecords(TwistedTestCase):
192+class TestRecords(TestCase):
193 """Test the record functionality"""
194
195 def setUp(self):
196@@ -180,7 +180,7 @@
197 self.record.record_type)
198
199
200-class TestRecordFactory(TwistedTestCase):
201+class TestRecordFactory(TestCase):
202 """Test Record/Mergeable List factories."""
203
204 def setUp(self):
205
206=== modified file 'desktopcouch/records/tests/test_server.py'
207--- desktopcouch/records/tests/test_server.py 2009-08-10 21:32:52 +0000
208+++ desktopcouch/records/tests/test_server.py 2009-08-12 14:26:40 +0000
209@@ -19,7 +19,8 @@
210 """testing database/contact.py module"""
211
212 import testtools
213-
214+import random
215+from desktopcouch.stop_local_couchdb import stop_couchdb
216 from desktopcouch.records.server import CouchDatabase
217 from desktopcouch.records.record import Record
218
219@@ -49,6 +50,9 @@
220 def tearDown(self):
221 """tear down each test"""
222 del self.database._server[self.dbname]
223+ if random.choice([1,2,3,4]) == 3: # don't harass it unnecessarily
224+ print u"\u2620", # death
225+ stop_couchdb()
226
227 def test_get_records_by_record_type_save_view(self):
228 """Test getting mutliple records by type"""
229
230=== modified file 'desktopcouch/start_local_couchdb.py' (properties changed: -x to +x)
231--- desktopcouch/start_local_couchdb.py 2009-07-27 18:16:19 +0000
232+++ desktopcouch/start_local_couchdb.py 2009-08-13 12:14:05 +0000
233@@ -32,10 +32,12 @@
234 """
235
236 from __future__ import with_statement
237-import os, subprocess, sys
238+import os, sys
239+import subprocess
240 import desktopcouch
241 from desktopcouch import local_files
242 import xdg.BaseDirectory
243+import errno
244 import time
245
246 def dump_ini(data, filename):
247@@ -52,8 +54,6 @@
248 fd.write("\n")
249 fd.close()
250
251-
252-
253 def create_ini_file():
254 """Write CouchDB ini file if not already present"""
255 # FIXME add update trigger folder
256@@ -89,7 +89,16 @@
257 """Actually start the CouchDB process"""
258 local_exec = local_files.COUCH_EXEC_COMMAND + ['-b']
259 try:
260- retcode = subprocess.call(local_exec, shell=False)
261+ # subprocess is buggy. Chad patched, but that takes time to propagate.
262+ proc = subprocess.Popen(local_exec)
263+ while True:
264+ try:
265+ retcode = proc.wait()
266+ break
267+ except OSError, e:
268+ if e.errno == errno.EINTR:
269+ continue
270+ raise
271 if retcode < 0:
272 print >> sys.stderr, "Child was terminated by signal", -retcode
273 elif retcode > 0:
274@@ -118,15 +127,30 @@
275 html = fp.read()
276 fp.close()
277
278- time.sleep(1)
279- pid = desktopcouch.find_pid()
280- port = desktopcouch.find_port(pid)
281+ port = None
282+ for retry in xrange(10000, 0, -1):
283+ pid = desktopcouch.find_pid(start_if_not_running=False)
284+ try:
285+ port = desktopcouch.find_port(pid)
286+ break
287+ except RuntimeError, e:
288+ if retry == 1:
289+ raise
290+ time.sleep(0.01)
291+ continue
292
293- fp = open(bookmark_file, "w")
294- fp.write(html.replace("[[COUCHDB_PORT]]", port))
295- fp.close()
296- print "Browse your desktop CouchDB at file://%s" % \
297- os.path.realpath(bookmark_file)
298+ if port is None:
299+ print "We couldn't find desktop-CouchDB's network port. Bookmark file not written."
300+ try:
301+ os.remove(bookmark_file)
302+ except OSError:
303+ pass
304+ else:
305+ fp = open(bookmark_file, "w")
306+ fp.write(html.replace("[[COUCHDB_PORT]]", port))
307+ fp.close()
308+ print "Browse your desktop CouchDB at file://%s" % \
309+ os.path.realpath(bookmark_file)
310
311 def start_couchdb():
312 """Execute each step to start a desktop CouchDB"""
313@@ -135,6 +159,7 @@
314 update_design_documents()
315 write_bookmark_file()
316
317+
318 if __name__ == "__main__":
319 start_couchdb()
320 print "Desktop CouchDB started"
321
322=== added file 'desktopcouch/stop_local_couchdb.py'
323--- desktopcouch/stop_local_couchdb.py 1970-01-01 00:00:00 +0000
324+++ desktopcouch/stop_local_couchdb.py 2009-08-12 14:26:14 +0000
325@@ -0,0 +1,49 @@
326+#!/usr/bin/python
327+# Copyright 2009 Canonical Ltd.
328+#
329+# This file is part of desktopcouch.
330+#
331+# desktopcouch is free software: you can redistribute it and/or modify
332+# it under the terms of the GNU Lesser General Public License version 3
333+# as published by the Free Software Foundation.
334+#
335+# desktopcouch is distributed in the hope that it will be useful,
336+# but WITHOUT ANY WARRANTY; without even the implied warranty of
337+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
338+# GNU Lesser General Public License for more details.
339+#
340+# You should have received a copy of the GNU Lesser General Public License
341+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
342+#
343+# Author: Chad Miller <chad.miller@canonical.com>
344+"""
345+Stop local CouchDB server.
346+"""
347+
348+import os
349+import desktopcouch
350+import time
351+import signal
352+import errno
353+
354+def stop_couchdb():
355+ pid = desktopcouch.find_pid(start_if_not_running=False)
356+ while pid is not None:
357+ try:
358+ os.kill(pid, signal.SIGTERM)
359+ except OSError, e:
360+ if e.errno == errno.ESRCH:
361+ break
362+ raise
363+
364+ for retry in xrange(300):
365+ try:
366+ os.kill(pid, 0) # test existence. sig-zero is special.
367+ except OSError:
368+ break
369+ time.sleep(0.01)
370+
371+ pid = desktopcouch.find_pid(start_if_not_running=False)
372+
373+if __name__ == "__main__":
374+ stop_couchdb()

Subscribers

People subscribed via source and target branches