PageRenderTime 33ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/bup/client.py

http://github.com/apenwarr/bup
Python | 347 lines | 314 code | 21 blank | 12 comment | 55 complexity | 71a6233cde8e6aad3f1a8eb59efa82a1 MD5 | raw file
Possible License(s): LGPL-2.0
  1. import re, struct, errno, time, zlib
  2. from bup import git, ssh
  3. from bup.helpers import *
  4. bwlimit = None
  5. class ClientError(Exception):
  6. pass
  7. def _raw_write_bwlimit(f, buf, bwcount, bwtime):
  8. if not bwlimit:
  9. f.write(buf)
  10. return (len(buf), time.time())
  11. else:
  12. # We want to write in reasonably large blocks, but not so large that
  13. # they're likely to overflow a router's queue. So our bwlimit timing
  14. # has to be pretty granular. Also, if it takes too long from one
  15. # transmit to the next, we can't just make up for lost time to bring
  16. # the average back up to bwlimit - that will risk overflowing the
  17. # outbound queue, which defeats the purpose. So if we fall behind
  18. # by more than one block delay, we shouldn't ever try to catch up.
  19. for i in xrange(0,len(buf),4096):
  20. now = time.time()
  21. next = max(now, bwtime + 1.0*bwcount/bwlimit)
  22. time.sleep(next-now)
  23. sub = buf[i:i+4096]
  24. f.write(sub)
  25. bwcount = len(sub) # might be less than 4096
  26. bwtime = next
  27. return (bwcount, bwtime)
  28. def parse_remote(remote):
  29. protocol = r'([a-z]+)://'
  30. host = r'(?P<sb>\[)?((?(sb)[0-9a-f:]+|[^:/]+))(?(sb)\])'
  31. port = r'(?::(\d+))?'
  32. path = r'(/.*)?'
  33. url_match = re.match(
  34. '%s(?:%s%s)?%s' % (protocol, host, port, path), remote, re.I)
  35. if url_match:
  36. assert(url_match.group(1) in ('ssh', 'bup', 'file'))
  37. return url_match.group(1,3,4,5)
  38. else:
  39. rs = remote.split(':', 1)
  40. if len(rs) == 1 or rs[0] in ('', '-'):
  41. return 'file', None, None, rs[-1]
  42. else:
  43. return 'ssh', rs[0], None, rs[1]
  44. class Client:
  45. def __init__(self, remote, create=False, compression_level=1):
  46. self._busy = self.conn = None
  47. self.sock = self.p = self.pout = self.pin = None
  48. self.compression_level = compression_level
  49. is_reverse = os.environ.get('BUP_SERVER_REVERSE')
  50. if is_reverse:
  51. assert(not remote)
  52. remote = '%s:' % is_reverse
  53. (self.protocol, self.host, self.port, self.dir) = parse_remote(remote)
  54. self.cachedir = git.repo('index-cache/%s'
  55. % re.sub(r'[^@\w]', '_',
  56. "%s:%s" % (self.host, self.dir)))
  57. if is_reverse:
  58. self.pout = os.fdopen(3, 'rb')
  59. self.pin = os.fdopen(4, 'wb')
  60. self.conn = Conn(self.pout, self.pin)
  61. else:
  62. if self.protocol in ('ssh', 'file'):
  63. try:
  64. # FIXME: ssh and file shouldn't use the same module
  65. self.p = ssh.connect(self.host, self.port, 'server')
  66. self.pout = self.p.stdout
  67. self.pin = self.p.stdin
  68. self.conn = Conn(self.pout, self.pin)
  69. except OSError, e:
  70. raise ClientError, 'connect: %s' % e, sys.exc_info()[2]
  71. elif self.protocol == 'bup':
  72. self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  73. self.sock.connect((self.host, atoi(self.port) or 1982))
  74. self.sockw = self.sock.makefile('wb')
  75. self.conn = DemuxConn(self.sock.fileno(), self.sockw)
  76. if self.dir:
  77. self.dir = re.sub(r'[\r\n]', ' ', self.dir)
  78. if create:
  79. self.conn.write('init-dir %s\n' % self.dir)
  80. else:
  81. self.conn.write('set-dir %s\n' % self.dir)
  82. self.check_ok()
  83. self.sync_indexes()
  84. def __del__(self):
  85. try:
  86. self.close()
  87. except IOError, e:
  88. if e.errno == errno.EPIPE:
  89. pass
  90. else:
  91. raise
  92. def close(self):
  93. if self.conn and not self._busy:
  94. self.conn.write('quit\n')
  95. if self.pin:
  96. self.pin.close()
  97. if self.sock and self.sockw:
  98. self.sockw.close()
  99. self.sock.shutdown(socket.SHUT_WR)
  100. if self.conn:
  101. self.conn.close()
  102. if self.pout:
  103. self.pout.close()
  104. if self.sock:
  105. self.sock.close()
  106. if self.p:
  107. self.p.wait()
  108. rv = self.p.wait()
  109. if rv:
  110. raise ClientError('server tunnel returned exit code %d' % rv)
  111. self.conn = None
  112. self.sock = self.p = self.pin = self.pout = None
  113. def check_ok(self):
  114. if self.p:
  115. rv = self.p.poll()
  116. if rv != None:
  117. raise ClientError('server exited unexpectedly with code %r'
  118. % rv)
  119. try:
  120. return self.conn.check_ok()
  121. except Exception, e:
  122. raise ClientError, e, sys.exc_info()[2]
  123. def check_busy(self):
  124. if self._busy:
  125. raise ClientError('already busy with command %r' % self._busy)
  126. def ensure_busy(self):
  127. if not self._busy:
  128. raise ClientError('expected to be busy, but not busy?!')
  129. def _not_busy(self):
  130. self._busy = None
  131. def sync_indexes(self):
  132. self.check_busy()
  133. conn = self.conn
  134. mkdirp(self.cachedir)
  135. # All cached idxs are extra until proven otherwise
  136. extra = set()
  137. for f in os.listdir(self.cachedir):
  138. debug1('%s\n' % f)
  139. if f.endswith('.idx'):
  140. extra.add(f)
  141. needed = set()
  142. conn.write('list-indexes\n')
  143. for line in linereader(conn):
  144. if not line:
  145. break
  146. assert(line.find('/') < 0)
  147. parts = line.split(' ')
  148. idx = parts[0]
  149. if len(parts) == 2 and parts[1] == 'load' and idx not in extra:
  150. # If the server requests that we load an idx and we don't
  151. # already have a copy of it, it is needed
  152. needed.add(idx)
  153. # Any idx that the server has heard of is proven not extra
  154. extra.discard(idx)
  155. self.check_ok()
  156. debug1('client: removing extra indexes: %s\n' % extra)
  157. for idx in extra:
  158. os.unlink(os.path.join(self.cachedir, idx))
  159. debug1('client: server requested load of: %s\n' % needed)
  160. for idx in needed:
  161. self.sync_index(idx)
  162. git.auto_midx(self.cachedir)
  163. def sync_index(self, name):
  164. #debug1('requesting %r\n' % name)
  165. self.check_busy()
  166. mkdirp(self.cachedir)
  167. fn = os.path.join(self.cachedir, name)
  168. if os.path.exists(fn):
  169. msg = "won't request existing .idx, try `bup bloom --check %s`" % fn
  170. raise ClientError(msg)
  171. self.conn.write('send-index %s\n' % name)
  172. n = struct.unpack('!I', self.conn.read(4))[0]
  173. assert(n)
  174. f = open(fn + '.tmp', 'w')
  175. count = 0
  176. progress('Receiving index from server: %d/%d\r' % (count, n))
  177. for b in chunkyreader(self.conn, n):
  178. f.write(b)
  179. count += len(b)
  180. qprogress('Receiving index from server: %d/%d\r' % (count, n))
  181. progress('Receiving index from server: %d/%d, done.\n' % (count, n))
  182. self.check_ok()
  183. f.close()
  184. os.rename(fn + '.tmp', fn)
  185. def _make_objcache(self):
  186. return git.PackIdxList(self.cachedir)
  187. def _suggest_packs(self):
  188. ob = self._busy
  189. if ob:
  190. assert(ob == 'receive-objects-v2')
  191. self.conn.write('\xff\xff\xff\xff') # suspend receive-objects-v2
  192. suggested = []
  193. for line in linereader(self.conn):
  194. if not line:
  195. break
  196. debug2('%s\n' % line)
  197. if line.startswith('index '):
  198. idx = line[6:]
  199. debug1('client: received index suggestion: %s\n'
  200. % git.shorten_hash(idx))
  201. suggested.append(idx)
  202. else:
  203. assert(line.endswith('.idx'))
  204. debug1('client: completed writing pack, idx: %s\n'
  205. % git.shorten_hash(line))
  206. suggested.append(line)
  207. self.check_ok()
  208. if ob:
  209. self._busy = None
  210. idx = None
  211. for idx in suggested:
  212. self.sync_index(idx)
  213. git.auto_midx(self.cachedir)
  214. if ob:
  215. self._busy = ob
  216. self.conn.write('%s\n' % ob)
  217. return idx
  218. def new_packwriter(self):
  219. self.check_busy()
  220. def _set_busy():
  221. self._busy = 'receive-objects-v2'
  222. self.conn.write('receive-objects-v2\n')
  223. return PackWriter_Remote(self.conn,
  224. objcache_maker = self._make_objcache,
  225. suggest_packs = self._suggest_packs,
  226. onopen = _set_busy,
  227. onclose = self._not_busy,
  228. ensure_busy = self.ensure_busy,
  229. compression_level = self.compression_level)
  230. def read_ref(self, refname):
  231. self.check_busy()
  232. self.conn.write('read-ref %s\n' % refname)
  233. r = self.conn.readline().strip()
  234. self.check_ok()
  235. if r:
  236. assert(len(r) == 40) # hexified sha
  237. return r.decode('hex')
  238. else:
  239. return None # nonexistent ref
  240. def update_ref(self, refname, newval, oldval):
  241. self.check_busy()
  242. self.conn.write('update-ref %s\n%s\n%s\n'
  243. % (refname, newval.encode('hex'),
  244. (oldval or '').encode('hex')))
  245. self.check_ok()
  246. def cat(self, id):
  247. self.check_busy()
  248. self._busy = 'cat'
  249. self.conn.write('cat %s\n' % re.sub(r'[\n\r]', '_', id))
  250. while 1:
  251. sz = struct.unpack('!I', self.conn.read(4))[0]
  252. if not sz: break
  253. yield self.conn.read(sz)
  254. e = self.check_ok()
  255. self._not_busy()
  256. if e:
  257. raise KeyError(str(e))
  258. class PackWriter_Remote(git.PackWriter):
  259. def __init__(self, conn, objcache_maker, suggest_packs,
  260. onopen, onclose,
  261. ensure_busy,
  262. compression_level=1):
  263. git.PackWriter.__init__(self, objcache_maker)
  264. self.file = conn
  265. self.filename = 'remote socket'
  266. self.suggest_packs = suggest_packs
  267. self.onopen = onopen
  268. self.onclose = onclose
  269. self.ensure_busy = ensure_busy
  270. self._packopen = False
  271. self._bwcount = 0
  272. self._bwtime = time.time()
  273. self.compression_level = compression_level
  274. def _open(self):
  275. if not self._packopen:
  276. self.onopen()
  277. self._packopen = True
  278. def _end(self):
  279. if self._packopen and self.file:
  280. self.file.write('\0\0\0\0')
  281. self._packopen = False
  282. self.onclose() # Unbusy
  283. self.objcache = None
  284. return self.suggest_packs() # Returns last idx received
  285. def close(self):
  286. id = self._end()
  287. self.file = None
  288. return id
  289. def abort(self):
  290. raise ClientError("don't know how to abort remote pack writing")
  291. def _raw_write(self, datalist, sha):
  292. assert(self.file)
  293. if not self._packopen:
  294. self._open()
  295. self.ensure_busy()
  296. data = ''.join(datalist)
  297. assert(data)
  298. assert(sha)
  299. crc = zlib.crc32(data) & 0xffffffff
  300. outbuf = ''.join((struct.pack('!I', len(data) + 20 + 4),
  301. sha,
  302. struct.pack('!I', crc),
  303. data))
  304. try:
  305. (self._bwcount, self._bwtime) = _raw_write_bwlimit(
  306. self.file, outbuf, self._bwcount, self._bwtime)
  307. except IOError, e:
  308. raise ClientError, e, sys.exc_info()[2]
  309. self.outbytes += len(data)
  310. self.count += 1
  311. if self.file.has_input():
  312. self.suggest_packs()
  313. self.objcache.refresh()
  314. return sha, crc