1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
1
# Copyright (C) 2006-2011 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
"""On-disk mutex protecting a resource
21
21
internal locks (such as flock etc) because they can be seen across all
22
22
transports, including http.
24
Objects can be read if there is only physical read access; therefore
24
Objects can be read if there is only physical read access; therefore
25
25
readers can never be required to create a lock, though they will
26
26
check whether a writer is using the lock. Writers can't detect
27
27
whether anyone else is reading from the resource as they write.
57
57
The desired characteristics are:
59
* Locks are not reentrant. (That is, a client that tries to take a
59
* Locks are not reentrant. (That is, a client that tries to take a
60
60
lock it already holds may deadlock or fail.)
61
61
* Stale locks can be guessed at by a heuristic
62
62
* Lost locks can be broken by any client
78
78
and deadlocks will likely occur if the locks are aliased.
80
80
In the future we may add a "freshen" method which can be called
81
by a lock holder to check that their lock has not been broken, and to
81
by a lock holder to check that their lock has not been broken, and to
82
82
update the timestamp within it.
128
from bzrlib.hooks import Hooks
129
131
from bzrlib.trace import mutter, note
130
132
from bzrlib.osutils import format_delta, rand_chars, get_host_name
168
170
:param transport: Transport which will contain the lock
170
:param path: Path to the lock within the base directory of the
172
:param path: Path to the lock within the base directory of the
173
175
self.transport = transport
192
194
def create(self, mode=None):
193
195
"""Create the on-disk lock.
195
This is typically only called when the object/directory containing the
197
This is typically only called when the object/directory containing the
196
198
directory is first created. The lock is not held when it's created.
198
200
self._trace("create lock directory")
241
243
# incorrect. It's possible some other servers or filesystems will
242
244
# have a similar bug allowing someone to think they got the lock
243
245
# when it's already held.
247
# See <https://bugs.launchpad.net/bzr/+bug/498378> for one case.
249
# Strictly the check is unnecessary and a waste of time for most
250
# people, but probably worth trapping if something is wrong.
244
251
info = self.peek()
245
252
self._trace("after locking, info=%r", info)
246
if info['nonce'] != self.nonce:
254
raise LockFailed(self, "lock was renamed into place, but "
256
if info.get('nonce') != self.nonce:
247
257
self._trace("rename succeeded, "
248
258
"but lock is still held by someone else")
249
259
raise LockContention(self)
255
265
def _remove_pending_dir(self, tmpname):
256
266
"""Remove the pending directory
258
This is called if we failed to rename into place, so that the pending
268
This is called if we failed to rename into place, so that the pending
259
269
dirs don't clutter up the lockdir.
261
271
self._trace("remove %s", tmpname)
326
337
self._trace("... unlock succeeded after %dms",
327
338
(time.time() - start_time) * 1000)
328
339
result = lock.LockResult(self.transport.abspath(self.path),
330
341
for hook in self.hooks['lock_released']:
336
347
This is a UI centric function: it uses the bzrlib.ui.ui_factory to
337
348
prompt for input if a lock is detected and there is any doubt about
338
349
it possibly being still active.
351
:returns: LockResult for the broken lock.
340
353
self._check_not_locked()
341
holder_info = self.peek()
355
holder_info = self.peek()
356
except LockCorrupt, e:
357
# The lock info is corrupt.
358
if bzrlib.ui.ui_factory.get_boolean("Break (corrupt %r)" % (self,)):
359
self.force_break_corrupt(e.file_data)
342
361
if holder_info is not None:
343
362
lock_info = '\n'.join(self._format_lock_info(holder_info))
344
if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):
345
self.force_break(holder_info)
363
if bzrlib.ui.ui_factory.confirm_action(
364
"Break %(lock_info)s", 'bzrlib.lockdir.break',
365
dict(lock_info=lock_info)):
366
result = self.force_break(holder_info)
367
bzrlib.ui.ui_factory.show_message(
368
"Broke lock %s" % result.lock_url)
347
370
def force_break(self, dead_holder_info):
348
371
"""Release a lock held by another process.
357
380
LockBreakMismatch is raised.
359
382
After the lock is broken it will not be held by any process.
360
It is possible that another process may sneak in and take the
383
It is possible that another process may sneak in and take the
361
384
lock before the breaking process acquires it.
386
:returns: LockResult for the broken lock.
363
388
if not isinstance(dead_holder_info, dict):
364
389
raise ValueError("dead_holder_info: %r" % dead_holder_info)
372
397
tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
373
398
self.transport.rename(self._held_dir, tmpname)
374
399
# check that we actually broke the right lock, not someone else;
375
# there's a small race window between checking it and doing the
400
# there's a small race window between checking it and doing the
377
402
broken_info_path = tmpname + self.__INFO_NAME
378
403
broken_info = self._read_info_file(broken_info_path)
380
405
raise LockBreakMismatch(self, broken_info, dead_holder_info)
381
406
self.transport.delete(broken_info_path)
382
407
self.transport.rmdir(tmpname)
408
result = lock.LockResult(self.transport.abspath(self.path),
409
current_info.get('nonce'))
410
for hook in self.hooks['lock_broken']:
414
def force_break_corrupt(self, corrupt_info_lines):
415
"""Release a lock that has been corrupted.
417
This is very similar to force_break, it except it doesn't assume that
418
self.peek() can work.
420
:param corrupt_info_lines: the lines of the corrupted info file, used
421
to check that the lock hasn't changed between reading the (corrupt)
422
info file and calling force_break_corrupt.
424
# XXX: this copes with unparseable info files, but what about missing
425
# info files? Or missing lock dirs?
426
self._check_not_locked()
427
tmpname = '%s/broken.%s.tmp' % (self.path, rand_chars(20))
428
self.transport.rename(self._held_dir, tmpname)
429
# check that we actually broke the right lock, not someone else;
430
# there's a small race window between checking it and doing the
432
broken_info_path = tmpname + self.__INFO_NAME
433
broken_content = self.transport.get_bytes(broken_info_path)
434
broken_lines = osutils.split_lines(broken_content)
435
if broken_lines != corrupt_info_lines:
436
raise LockBreakMismatch(self, broken_lines, corrupt_info_lines)
437
self.transport.delete(broken_info_path)
438
self.transport.rmdir(tmpname)
439
result = lock.LockResult(self.transport.abspath(self.path))
440
for hook in self.hooks['lock_broken']:
384
443
def _check_not_locked(self):
385
444
"""If the lock is held by this instance, raise an error."""
393
452
or if the lock has been affected by a bug.
395
454
If the lock is not thought to be held, raises LockNotHeld. If
396
the lock is thought to be held but has been broken, raises
455
the lock is thought to be held but has been broken, raises
399
458
if not self._lock_held:
405
464
if info.get('nonce') != self.nonce:
406
465
# there is a lock, but not ours
407
466
raise LockBroken(self)
409
468
def _read_info_file(self, path):
410
469
"""Read one given info file.
412
471
peek() reads the info file of the lock holder, if any.
414
return self._parse_info(self.transport.get(path))
473
return self._parse_info(self.transport.get_bytes(path))
417
476
"""Check if the lock is held by anyone.
419
If it is held, this returns the lock info structure as a rio Stanza,
478
If it is held, this returns the lock info structure as a dict
420
479
which contains some information about the current lock holder.
421
480
Otherwise returns None.
433
492
# XXX: is creating this here inefficient?
434
493
config = bzrlib.config.GlobalConfig()
436
user = config.user_email()
437
except errors.NoEmailInUsername:
438
495
user = config.username()
496
except errors.NoWhoami:
497
user = osutils.getuser_unicode()
439
498
s = rio.Stanza(hostname=get_host_name(),
440
499
pid=str(os.getpid()),
441
500
start_time=str(int(time.time())),
445
504
return s.to_string()
447
def _parse_info(self, info_file):
448
return rio.read_stanza(info_file.readlines()).as_dict()
506
def _parse_info(self, info_bytes):
507
lines = osutils.split_lines(info_bytes)
509
stanza = rio.read_stanza(lines)
510
except ValueError, e:
511
mutter('Corrupt lock info file: %r', lines)
512
raise LockCorrupt("could not parse lock info file: " + str(e),
515
# see bug 185013; we fairly often end up with the info file being
516
# empty after an interruption; we could log a message here but
517
# there may not be much we can say
520
return stanza.as_dict()
450
522
def attempt_lock(self):
451
523
"""Take the lock; fail if it's already held.
453
525
If you wish to block until the lock can be obtained, call wait_lock()
465
537
hook(hook_result)
540
def lock_url_for_display(self):
541
"""Give a nicely-printable representation of the URL of this lock."""
542
# As local lock urls are correct we display them.
543
# We avoid displaying remote lock urls.
544
lock_url = self.transport.abspath(self.path)
545
if lock_url.startswith('file://'):
546
lock_url = lock_url.split('.bzr/')[0]
468
551
def wait_lock(self, timeout=None, poll=None, max_attempts=None):
469
552
"""Wait a certain period for a lock.
518
602
if deadline_str is None:
519
603
deadline_str = time.strftime('%H:%M:%S',
520
604
time.localtime(deadline))
521
lock_url = self.transport.abspath(self.path)
522
self._report_function('%s %s\n'
524
'%s\n' # locked ... ago
525
'Will continue to try until %s, unless '
527
'If you\'re sure that it\'s not being '
528
'modified, use bzr break-lock %s',
605
user, hostname, pid, time_ago = formatted_info
606
msg = ('%s lock %s ' # lock_url
610
'[process #%s], ' # pid
611
'acquired %s.') # time ago
612
msg_args = [start, lock_url, user, hostname, pid, time_ago]
614
msg += ('\nWill continue to try until %s, unless '
616
msg_args.append(deadline_str)
617
msg += '\nSee "bzr help break-lock" for more.'
618
self._report_function(msg, *msg_args)
536
619
if (max_attempts is not None) and (attempt_count >= max_attempts):
537
620
self._trace("exceeded %d attempts")
538
621
raise LockContention(self)
540
623
self._trace("waiting %ss", poll)
626
# As timeout is always 0 for remote locks
627
# this block is applicable only for local
543
629
self._trace("timeout after waiting %ss", timeout)
544
raise LockContention(self)
630
raise LockContention('(local)', lock_url)
546
632
def leave_in_place(self):
547
633
self._locked_via_token = True
552
638
def lock_write(self, token=None):
553
639
"""Wait for and acquire the lock.
555
641
:param token: if this is already locked, then lock_write will fail
556
642
unless the token matches the existing lock.
557
643
:returns: a token if this instance supports tokens, otherwise None.
578
664
def lock_read(self):
579
665
"""Compatibility-mode shared lock.
581
LockDir doesn't support shared read-only locks, so this
667
LockDir doesn't support shared read-only locks, so this
582
668
just pretends that the lock is taken but really does nothing.
584
# At the moment Branches are commonly locked for read, but
670
# At the moment Branches are commonly locked for read, but
585
671
# we can't rely on that remotely. Once this is cleaned up,
586
# reenable this warning to prevent it coming back in
672
# reenable this warning to prevent it coming back in
587
673
# -- mbp 20060303
588
674
## warn("LockDir.lock_read falls back to write lock")
589
675
if self._lock_held or self._fake_read_lock:
593
679
def _format_lock_info(self, info):
594
680
"""Turn the contents of peek() into something for the user"""
595
lock_url = self.transport.abspath(self.path)
596
delta = time.time() - int(info['start_time'])
681
start_time = info.get('start_time')
682
if start_time is None:
683
time_ago = '(unknown)'
685
time_ago = format_delta(time.time() - int(info['start_time']))
686
user = info.get('user', '<unknown>')
687
hostname = info.get('hostname', '<unknown>')
688
pid = info.get('pid', '<unknown>')
598
'lock %s' % (lock_url,),
599
'held by %(user)s on host %(hostname)s [process #%(pid)s]' % info,
600
'locked %s' % (format_delta(delta),),
603
696
def validate_token(self, token):