Gentoo Archives: gentoo-portage-dev

From: Brian Harring <ferringb@g.o>
To: gentoo-portage-dev@l.g.o
Subject: Re: [gentoo-portage-dev] Plugin backport PATCH (1/2)/(2/2)
Date: Fri, 18 Nov 2005 23:48:43
Message-Id: 20051118234710.GA7073@nightcrawler
In Reply to: Re: [gentoo-portage-dev] Plugin backport PATCH (1/2)/(2/2) by Jason Stubbs
1 On Sat, Nov 19, 2005 at 12:43:43AM +0900, Jason Stubbs wrote:
2 > +++ ./pym/portage_file.py       2005-11-16 01:56:19.157073160 -0500
3 >
4 > +                       # if the dir perms would lack +wx, we have to force it
5 > +                       force_temp_perms = ((mode & 0300) != 0300)
6 > +                       resets = []
7 > +                       apath = normpath(os.path.abspath(path))
8 > +                       sticky_parent = False
9 > +                       creating = False
10 > +                       
11 > +                       for dir in apath.split(os.path.sep):
12 > +                               base = os.path.join(base,dir)
13 > +                               try:
14 > +                                       st = os.stat(base)
15 > +                                       # something exists.  why are we doing isdir?
16 > +                                       if not stat.S_ISDIR(st.st_mode):
17 > +                                               return False
18 >
19 > What's with the comment here?
20 I blame crack.
21 Rhetorical question in the code, which isn't very clear in hindsight
22
23 >
24 > +                                       # if it's a subdir, we need +wx at least
25 > +                                       if apath != base:
26 > +                                               if ((st.st_mode & 0300) != 0300):
27 > +                                                       try:                    os.chmod(base, (st.st_mode | 0300))
28 > +                                                       except OSError: return False
29 > +                                                       resets.append((base, st.st_mode))
30 > +                                               sticky_parent = (st.st_gid & stat.S_ISGID)
31 >
32 > <snip>
33 >
34 > +                       try:
35 > +                               for base, m in reversed(resets):
36 > +                                       os.chmod(base, m)
37 > +                               if uid != -1 or gid != -1:
38 > +                                       os.chown(base, uid, gid)
39 > +                       except OSError:
40 > +                               return False
41 >
42 > If for some reason the parent dir originally has perms 0000, this code will
43 > incorrectly change its uid and gid.
44 That's buggy anyways, since it won't play well with /blah
45
46 > +++ ./pym/portage_locks.py      2005-11-16 01:56:25.152161768 -0500
47 > @@ -358,3 +359,91 @@
48 > +# should the fd be left open indefinitely?
49 > +# IMO, it shouldn't, but opening/closing everytime around is expensive
50 >
51 > It should be closed when locks are released, I think. Otherwise the file may
52 > be deleted and recreated while one FsLock instance has it open which could
53 > create situations where two FsLocks are trying to lock the same path but
54 > using different fds.
55 Leave it open, on locking do a inode verification of the existing
56 handle- if it's behind, close the fd, open, resume locking (offhand).
57
58 Race potential, but that can be addressed by getting the read lock,
59 then doing a second (or first, if it's not re-upping a lock) inode
60 verification
61
62 > +class FsLock(object):
63 > +       __slots__ = ["path", "fd", "create"]
64 > +       def __init__(self, path, create=False):
65 > +               """path specifies the fs path for the lock
66 > +               create controls whether the file will be create if the file doesn't exist
67 > +               if create is true, the base dir must exist, and it will create a file
68 > +               
69 > +               If you want a directory yourself, create it.
70 > +               """
71 > +               self.path = path
72 > +               self.fd = None
73 > +               self.create = create
74 > +               if not create:
75 > +                       if not os.path.exists(path):
76 > +                               raise NonExistant(path)
77 > +
78 > +       def _acquire_fd(self):
79 > +               if self.create:
80 > +                       try:    self.fd = os.open(self.path, os.R_OK|os.O_CREAT)
81 > +                       except OSError, oe:
82 > +                               raise GenericFailed(self.path, oe)
83 > +               else:
84 > +                       try:    self.fd = os.open(self.path, os.R_OK)
85 > +                       except OSError, oe:     raise NonExistant(self.path, oe)
86 > +       
87 > +       def _enact_change(self, flags, blocking):
88 > +               if self.fd == None:
89 > +                       self._acquire_fd()
90 > +               # we do it this way, due to the fact try/except is a bit of a hit
91 >
92 > Instead of?
93 Two fcntl calls instead of one.
94 It's minor offhand, just how it was originally written.
95
96 >
97 > +               if not blocking:
98 > +                       try:    fcntl.flock(self.fd, flags|fcntl.LOCK_NB)
99 > +                       except IOError, ie:
100 > +                               if ie.errno == 11:
101 >
102 > 11 ?
103 42 is better? ;)
104
105 Should be using errno.EAGAIN instead of hardcoding though...
106
107 > +                                       return False
108 > +                               raise GenericFailed(self.path, ie)
109 > +               else:
110 > +                       fcntl.flock(self.fd, flags)
111 > +               return True
112 > +
113 > +       def acquire_write_lock(self, blocking=True):
114 > +               """Acquire an exclusive lock
115 > +               Returns True if lock is acquired, False if not.
116 > +               Note if you have a read lock, it implicitly upgrades atomically"""
117 > +               return self._enact_change(fcntl.LOCK_EX, blocking)
118 > +
119 > +       def acquire_read_lock(self, blocking=True):
120 > +               """Acquire a shared lock
121 > +               Returns True if lock is acquired, False if not.
122 > +               Note, if you have a write_lock already, it'll implicitly downgrade atomically"""
123 > +               return self._enact_change(fcntl.LOCK_SH, blocking)
124 > +       
125 > +       def release_write_lock(self):
126 > +               """Release an exclusive lock if held"""
127 > +               self._enact_change(fcntl.LOCK_UN, False)
128 > +               
129 > +       def release_read_lock(self):
130 > +               self._enact_change(fcntl.LOCK_UN, False)
131 > +
132 > +       def __del__(self):
133 > +               # alright, it's 5:45am, yes this is weird code.
134 > +               try:
135 > +                       if self.fd != None:
136 > +                               self.release_read_lock()
137 > +               finally:
138 > +                       if self.fd != None:
139 > +                               os.close(self.fd)
140 >
141 > Allowing an exception to be raised during __del__ is not a good thing...
142
143 The reason it's left in is that-
144 A) try/finally ensures despite whatever crazyness might occur with
145 locking, the fd gets waxed
146 B) blow ups in the code are visible, rather then silently swallowed
147 C) exceptions in __del__ python complains about, but swallows.
148
149 So... rather then silently ignoring any crazyness, it allows some
150 noise to be made if the locking code does something weird, and also
151 ensures the fd is closed.
152
153 Should check self.fd.closed on a sidenote.
154
155 > Any held lock should be cleaned up too otherwise misbehaving callers could
156 > deadlock.
157
158 Closing the fd == release of any locks that are bound to that fd.
159 Hence the anal measures to make damn sure the fd gets closed :)
160
161 > +++ ./pym/portage_modules.py    2005-11-16 01:56:28.269687832 -0500
162 > +__import_lock = threading.Lock()
163 > +
164 > +def load_module(name):
165 > +       """load a module, throwing a FailedImport if __import__ fails"""
166 > +       __import_lock.acquire()
167 > +       try:
168 > +               if name in sys.modules:
169 > +                       return sys.modules[name]
170 > +               try:
171 > +                       m = __import__(name)
172 > +                       nl = name.split('.')
173 > +                       # __import__ returns nl[0]... so.
174 > +                       nl.pop(0)
175 > +                       while len(nl):
176 > +                               m = getattr(m, nl[0])
177 > +                               nl.pop(0)
178 > +                       return m
179 > +               # * ferringb|afk hit it at some point, sure of that
180 > +               # but no idea how, so commenting out to see if things break...
181 > +               # except AttributeError, e:
182 > +               #       raise FailedImport(name, e)
183 >
184 > I can't see how either, but comments like these don't really help. Please
185 > leave them out unless they describe how the code works.
186 Leaving comments regarding "weirdness occured via here, but went away"
187 isn't a bad thing- if it reappears, you know that at least it was hit
188 once prior.
189
190 I know how it was hit also (just dawned on me how it would happen)-
191 echo $'import os;print os.dar' > test.py
192 python -c'import test'
193
194 AttributeError...
195
196 Should nuke the module imo in that case still, since it's a failed
197 import.
198
199 > +++ ./pym/portage_plugins.py    2005-11-16 01:56:51.419168576 -0500
200 > @@ -0,0 +1,174 @@
201 > +# Copyright: 2005 Gentoo Foundation
202 > +# License: GPL2
203 > +# $Id:$
204 > +
205 > +# I don't like this.
206 > +# doesn't seem clean/right.
207 >
208 > ???
209 Someone didn't clean up the files prior to posting this...
210
211 > +               # this could be fine grained down to per plugin_type
212 > +               plug_lock = FsLock(plugins_dir)
213 >
214 > Then it probably should be. ;)
215 It's not needed though. Need a write lock on the plugins dir for when
216 new plugin types are added; the only gain from it is when read locks
217 are held long term, and parallel registration is occuring (for savior,
218 it's possible, for stable, it isn't, not within the portage process at
219 least).
220
221 >
222 > +       def query_plugins(self, plugin_type=None, locking=True, raw=False):
223 >
224 > Not sure about allowing the external callers to disable locking... That
225 > should only be used by register and deregister, right? Perhaps breaking
226 > the unlocked version into a "protected" method?
227
228 Debatable; any protection used people can stomp right past. Not much
229 for the "sitting on the porch with the shotgun" approach.
230
231 Not really seeing the gain in hiding the locking tbh; shoving it into
232 the instance namespace would require locking the attribute to maintain
233 thread safety; the only gain is just an extra hoop to jump through if
234 I'm after doing something stupid. :)
235
236
237 > Is [x for x in list] 2.2 safe by the way?
238 Yes. The reversed in the code however is not.
239
240 > +                                       d[x[:-len_exten]] = dict([(y, dict(c.items(y))) for y in c.sections()])
241 >
242 > Same here on the 2.2 bit... Although that's pretty hard to read either way.
243 Prefer a map call? :)
244
245 >
246 > +               finally:
247 > +                       if locking:
248 > +                               plug_lock.release_read_lock()
249 > +               if plugin_type != None:
250 > +                       return d[plugin_type]
251 > +               return d
252 >
253 > Not sure about having different return types...
254 Agreed.
255
256 > +registry = None
257 > +from portage.util.currying import pre_curry, pretty_docs
258 > +
259 > +def proxy_it(method, *a, **kw):
260 > +       global registry
261 > +       if registry == None:
262 > +               registry = GlobalPluginRegistry()
263 > +       return getattr(registry, method)(*a, **kw)
264 > +
265 > +for x in ["register", "deregister", "query_plugins", "get_plugin"]:
266 > +       v = pre_curry(proxy_it, x)
267 > +       doc = getattr(GlobalPluginRegistry, x).__doc__
268 > +       if doc == None:
269 > +               doc = ''
270 > +       else:
271 > +               # do this so indentation on pydoc __doc__ is sane
272 > +               doc = "\n".join(map(lambda x:x.lstrip(), doc.split("\n"))) +"\n"
273 > +       doc += "proxied call to module level registry instances %s method" % x
274 > +       globals()[x] = pretty_docs(v, doc)
275 > +
276 > +del x, v, proxy_it, doc
277 >
278 > What's this currying and pretty_docs stuff?
279 currying == arg binding to a func- in this case, proxy_it gets first
280 arg of the attr requested; the proxy_it func instantiates a
281 GlobalPluginRegistry instance if it's missing.
282
283 From there, pulls the request attribute, and passes the actual called
284 args into the getattr'd func.
285
286 basically, a call to
287 plugins.query_plugins(blah,a,sdf)
288 becomes
289 proxy_it("query_plugins", blah, a, sdf)
290
291 proxy_it raids the method name, getattr's the requested method (after
292 instantiating if needed), then passes the remaining args/opt keywords
293 to the method it retrieved.
294
295 Since curry'ing is basically wrapping/decorating the func, pretty_docs
296 is used to rebind __doc__ string crap onto the func, so it actually
297 has a sane docstring, rather then lacking any.
298 ~harring

Replies

Subject Author
Re: [gentoo-portage-dev] Plugin backport PATCH (1/2)/(2/2) Jason Stubbs <jstubbs@g.o>