1 import re, struct, errno, select
3 from bup.helpers import *
4 from subprocess import Popen, PIPE
7 class ClientError(Exception):
12 def __init__(self, remote, create=False):
16 rs = remote.split(':', 1)
17 main_exe = os.environ.get('BUP_MAIN_EXE') or sys.argv[0]
18 nicedir = os.path.split(os.path.abspath(main_exe))[0]
19 nicedir = re.sub(r':', "_", nicedir)
21 (host, dir) = ('NONE', remote)
23 os.environ['PATH'] = ':'.join([nicedir,
24 os.environ.get('PATH', '')])
25 argv = ['bup', 'server']
29 # WARNING: shell quoting security holes are possible here, so we
30 # have to be super careful. We have to use 'sh -c' because
31 # csh-derived shells can't handle PATH= notation. We can't
32 # set PATH in advance, because ssh probably replaces it. We
33 # can't exec *safely* using argv, because *both* ssh and 'sh -c'
34 # allow shellquoting. So we end up having to double-shellquote
36 escapedir = re.sub(r'([^\w/])', r'\\\\\\\1', nicedir)
38 sh -c PATH=%s:'$PATH bup server'
40 argv = ['ssh', host, '--', cmd.strip()]
41 #log('argv is: %r\n' % argv)
46 (self.host, self.dir) = (host, dir)
47 self.cachedir = git.repo('index-cache/%s'
48 % re.sub(r'[^@\w]', '_',
49 "%s:%s" % (host, dir)))
51 self.p = p = Popen(argv, stdin=PIPE, stdout=PIPE, preexec_fn=setup)
53 raise ClientError, 'exec %r: %s' % (argv[0], e), sys.exc_info()[2]
54 self.conn = conn = Conn(p.stdout, p.stdin)
56 dir = re.sub(r'[\r\n]', ' ', dir)
58 conn.write('init-dir %s\n' % dir)
60 conn.write('set-dir %s\n' % dir)
62 self.sync_indexes_del()
68 if e.errno == errno.EPIPE:
74 if self.conn and not self._busy:
75 self.conn.write('quit\n')
78 while self.p.stdout.read(65536):
84 raise ClientError('server tunnel returned exit code %d' % rv)
91 raise ClientError('server exited unexpectedly with code %r' % rv)
93 return self.conn.check_ok()
95 raise ClientError, e, sys.exc_info()[2]
99 raise ClientError('already busy with command %r' % self._busy)
101 def ensure_busy(self):
103 raise ClientError('expected to be busy, but not busy?!')
108 def sync_indexes_del(self):
111 conn.write('list-indexes\n')
112 packdir = git.repo('objects/pack')
115 for line in linereader(conn):
119 assert(line.find('/') < 0)
120 if not os.path.exists(os.path.join(self.cachedir, line)):
124 mkdirp(self.cachedir)
125 for f in os.listdir(self.cachedir):
126 if f.endswith('.idx') and not f in all:
127 log('pruning old index: %r\n' % f)
128 os.unlink(os.path.join(self.cachedir, f))
130 def sync_index(self, name):
131 #log('requesting %r\n' % name)
133 mkdirp(self.cachedir)
134 self.conn.write('send-index %s\n' % name)
135 n = struct.unpack('!I', self.conn.read(4))[0]
137 fn = os.path.join(self.cachedir, name)
138 f = open(fn + '.tmp', 'w')
140 progress('Receiving index: %d/%d\r' % (count, n))
141 for b in chunkyreader(self.conn, n):
144 progress('Receiving index: %d/%d\r' % (count, n))
145 progress('Receiving index: %d/%d, done.\n' % (count, n))
148 os.rename(fn + '.tmp', fn)
150 def _make_objcache(self):
155 return git.PackIdxList(self.cachedir)
157 def _suggest_pack(self, indexname):
158 log('received index suggestion: %s\n' % indexname)
161 assert(ob == 'receive-objects')
162 self.conn.write('\xff\xff\xff\xff') # suspend receive-objects
164 self.conn.drain_and_check_ok()
165 self.sync_index(indexname)
168 self.conn.write('receive-objects\n')
170 def new_packwriter(self):
173 self._busy = 'receive-objects'
174 self.conn.write('receive-objects\n')
175 return PackWriter_Remote(self.conn,
176 objcache_maker = self._make_objcache,
177 suggest_pack = self._suggest_pack,
179 onclose = self._not_busy,
180 ensure_busy = self.ensure_busy)
182 def read_ref(self, refname):
184 self.conn.write('read-ref %s\n' % refname)
185 r = self.conn.readline().strip()
188 assert(len(r) == 40) # hexified sha
189 return r.decode('hex')
191 return None # nonexistent ref
193 def update_ref(self, refname, newval, oldval):
195 self.conn.write('update-ref %s\n%s\n%s\n'
196 % (refname, newval.encode('hex'),
197 (oldval or '').encode('hex')))
203 self.conn.write('cat %s\n' % re.sub(r'[\n\r]', '_', id))
205 sz = struct.unpack('!I', self.conn.read(4))[0]
207 yield self.conn.read(sz)
211 raise KeyError(str(e))
214 class PackWriter_Remote(git.PackWriter):
215 def __init__(self, conn, objcache_maker, suggest_pack,
218 git.PackWriter.__init__(self, objcache_maker)
220 self.filename = 'remote socket'
221 self.suggest_pack = suggest_pack
223 self.onclose = onclose
224 self.ensure_busy = ensure_busy
225 self._packopen = False
228 if not self._packopen:
229 self._make_objcache()
232 self._packopen = True
235 if self._packopen and self.file:
236 self.file.write('\0\0\0\0')
237 self._packopen = False
239 line = self.file.readline().strip()
240 if line.startswith('index '):
249 if id and self.suggest_pack:
250 self.suggest_pack(id)
259 raise GitError("don't know how to abort remote pack writing")
261 def _raw_write(self, datalist):
263 if not self._packopen:
267 data = ''.join(datalist)
269 self.file.write(struct.pack('!I', len(data)) + data)
270 self.outbytes += len(data)
273 if self.file.has_input():
274 line = self.file.readline().strip()
275 assert(line.startswith('index '))
277 if self.suggest_pack:
278 self.suggest_pack(idxname)
279 self.objcache.refresh()