summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDoug Anderson <dianders@google.com>2010-12-21 13:39:23 -0800
committerDoug Anderson <dianders@google.com>2010-12-21 13:39:23 -0800
commit0048b69c038306fe74408a63cdd0773b0d86a8fe (patch)
treeca5fb88b7b5a6a196d066172f4e59f37e626f5cf
parent2b8db3ce3e7344b9f3b5216637c5af0d54be5656 (diff)
downloadgit-repo-0048b69c038306fe74408a63cdd0773b0d86a8fe.tar.gz
Fixed race condition in 'repo sync -jN' that would open multiple masters.v1.7.2
This fixes the SSH Control Masters to be managed in a thread-safe fashion. This is important because "repo sync -jN" uses threads to sync more than one repository at the same time. The problem didn't show up earlier because it was masked if all of the threads tried to connect to the same host that was used on the "repo init" line.
-rw-r--r--git_config.py148
-rwxr-xr-xmain.py3
2 files changed, 90 insertions, 61 deletions
diff --git a/git_config.py b/git_config.py
index 8e3dfb1b..26fc970b 100644
--- a/git_config.py
+++ b/git_config.py
@@ -18,6 +18,10 @@ import os
18import re 18import re
19import subprocess 19import subprocess
20import sys 20import sys
21try:
22 import threading as _threading
23except ImportError:
24 import dummy_threading as _threading
21import time 25import time
22from signal import SIGTERM 26from signal import SIGTERM
23from urllib2 import urlopen, HTTPError 27from urllib2 import urlopen, HTTPError
@@ -361,76 +365,97 @@ class RefSpec(object):
361_master_processes = [] 365_master_processes = []
362_master_keys = set() 366_master_keys = set()
363_ssh_master = True 367_ssh_master = True
368_master_keys_lock = None
369
370def init_ssh():
371 """Should be called once at the start of repo to init ssh master handling.
372
373 At the moment, all we do is to create our lock.
374 """
375 global _master_keys_lock
376 assert _master_keys_lock is None, "Should only call init_ssh once"
377 _master_keys_lock = _threading.Lock()
364 378
365def _open_ssh(host, port=None): 379def _open_ssh(host, port=None):
366 global _ssh_master 380 global _ssh_master
367 381
368 # Check to see whether we already think that the master is running; if we 382 # Acquire the lock. This is needed to prevent opening multiple masters for
369 # think it's already running, return right away. 383 # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
370 if port is not None: 384 # manifest <remote fetch="ssh://xyz"> specifies a different host from the
371 key = '%s:%s' % (host, port) 385 # one that was passed to repo init.
372 else: 386 _master_keys_lock.acquire()
373 key = host 387 try:
374
375 if key in _master_keys:
376 return True
377 388
378 if not _ssh_master \ 389 # Check to see whether we already think that the master is running; if we
379 or 'GIT_SSH' in os.environ \ 390 # think it's already running, return right away.
380 or sys.platform in ('win32', 'cygwin'): 391 if port is not None:
381 # failed earlier, or cygwin ssh can't do this 392 key = '%s:%s' % (host, port)
382 # 393 else:
383 return False 394 key = host
384 395
385 # We will make two calls to ssh; this is the common part of both calls. 396 if key in _master_keys:
386 command_base = ['ssh',
387 '-o','ControlPath %s' % ssh_sock(),
388 host]
389 if port is not None:
390 command_base[1:1] = ['-p',str(port)]
391
392 # Since the key wasn't in _master_keys, we think that master isn't running.
393 # ...but before actually starting a master, we'll double-check. This can
394 # be important because we can't tell that that 'git@myhost.com' is the same
395 # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
396 check_command = command_base + ['-O','check']
397 try:
398 Trace(': %s', ' '.join(check_command))
399 check_process = subprocess.Popen(check_command,
400 stdout=subprocess.PIPE,
401 stderr=subprocess.PIPE)
402 check_process.communicate() # read output, but ignore it...
403 isnt_running = check_process.wait()
404
405 if not isnt_running:
406 # Our double-check found that the master _was_ infact running. Add to
407 # the list of keys.
408 _master_keys.add(key)
409 return True 397 return True
410 except Exception:
411 # Ignore excpetions. We we will fall back to the normal command and print
412 # to the log there.
413 pass
414
415 command = command_base[:1] + \
416 ['-M', '-N'] + \
417 command_base[1:]
418 try:
419 Trace(': %s', ' '.join(command))
420 p = subprocess.Popen(command)
421 except Exception, e:
422 _ssh_master = False
423 print >>sys.stderr, \
424 '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
425 % (host,port, str(e))
426 return False
427 398
428 _master_processes.append(p) 399 if not _ssh_master \
429 _master_keys.add(key) 400 or 'GIT_SSH' in os.environ \
430 time.sleep(1) 401 or sys.platform in ('win32', 'cygwin'):
431 return True 402 # failed earlier, or cygwin ssh can't do this
403 #
404 return False
405
406 # We will make two calls to ssh; this is the common part of both calls.
407 command_base = ['ssh',
408 '-o','ControlPath %s' % ssh_sock(),
409 host]
410 if port is not None:
411 command_base[1:1] = ['-p',str(port)]
412
413 # Since the key wasn't in _master_keys, we think that master isn't running.
414 # ...but before actually starting a master, we'll double-check. This can
415 # be important because we can't tell that that 'git@myhost.com' is the same
416 # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
417 check_command = command_base + ['-O','check']
418 try:
419 Trace(': %s', ' '.join(check_command))
420 check_process = subprocess.Popen(check_command,
421 stdout=subprocess.PIPE,
422 stderr=subprocess.PIPE)
423 check_process.communicate() # read output, but ignore it...
424 isnt_running = check_process.wait()
425
426 if not isnt_running:
427 # Our double-check found that the master _was_ infact running. Add to
428 # the list of keys.
429 _master_keys.add(key)
430 return True
431 except Exception:
432 # Ignore excpetions. We we will fall back to the normal command and print
433 # to the log there.
434 pass
435
436 command = command_base[:1] + \
437 ['-M', '-N'] + \
438 command_base[1:]
439 try:
440 Trace(': %s', ' '.join(command))
441 p = subprocess.Popen(command)
442 except Exception, e:
443 _ssh_master = False
444 print >>sys.stderr, \
445 '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
446 % (host,port, str(e))
447 return False
448
449 _master_processes.append(p)
450 _master_keys.add(key)
451 time.sleep(1)
452 return True
453 finally:
454 _master_keys_lock.release()
432 455
433def close_ssh(): 456def close_ssh():
457 global _master_keys_lock
458
434 terminate_ssh_clients() 459 terminate_ssh_clients()
435 460
436 for p in _master_processes: 461 for p in _master_processes:
@@ -449,6 +474,9 @@ def close_ssh():
449 except OSError: 474 except OSError:
450 pass 475 pass
451 476
477 # We're done with the lock, so we can delete it.
478 _master_keys_lock = None
479
452URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') 480URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
453URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/') 481URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
454 482
diff --git a/main.py b/main.py
index 70ddeffa..31a18e18 100755
--- a/main.py
+++ b/main.py
@@ -28,7 +28,7 @@ import re
28import sys 28import sys
29 29
30from trace import SetTrace 30from trace import SetTrace
31from git_config import close_ssh 31from git_config import init_ssh, close_ssh
32from command import InteractiveCommand 32from command import InteractiveCommand
33from command import MirrorSafeCommand 33from command import MirrorSafeCommand
34from command import PagedCommand 34from command import PagedCommand
@@ -214,6 +214,7 @@ def _Main(argv):
214 repo = _Repo(opt.repodir) 214 repo = _Repo(opt.repodir)
215 try: 215 try:
216 try: 216 try:
217 init_ssh()
217 repo._Run(argv) 218 repo._Run(argv)
218 finally: 219 finally:
219 close_ssh() 220 close_ssh()