]> arthur.barton.de Git - bup.git/blob - lib/bup/client.py
'make stupid' stopped working when I moved subcommands into their own dir.
[bup.git] / lib / bup / client.py
1 import re, struct, errno, select
2 from bup import git
3 from bup.helpers import *
4 from subprocess import Popen, PIPE
5
6
7 class ClientError(Exception):
8     pass
9
10
11 class Client:
12     def __init__(self, remote, create=False):
13         self._busy = None
14         self.p = None
15         self.conn = None
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)
20         if len(rs) == 1:
21             (host, dir) = ('NONE', remote)
22             def fixenv():
23                 os.environ['PATH'] = ':'.join([nicedir,
24                                                os.environ.get('PATH', '')])
25             argv = ['bup', 'server']
26         else:
27             (host, dir) = rs
28             fixenv = None
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
35             # stuff here.
36             escapedir = re.sub(r'([^\w/])', r'\\\\\\\1', nicedir)
37             cmd = r"""
38                        sh -c PATH=%s:'$PATH bup server'
39                    """ % escapedir
40             argv = ['ssh', host, '--', cmd.strip()]
41             #log('argv is: %r\n' % argv)
42         (self.host, self.dir) = (host, dir)
43         self.cachedir = git.repo('index-cache/%s'
44                                  % re.sub(r'[^@\w]', '_', 
45                                           "%s:%s" % (host, dir)))
46         try:
47             self.p = p = Popen(argv, stdin=PIPE, stdout=PIPE, preexec_fn=fixenv)
48         except OSError, e:
49             raise ClientError, 'exec %r: %s' % (argv[0], e), sys.exc_info()[2]
50         self.conn = conn = Conn(p.stdout, p.stdin)
51         if dir:
52             dir = re.sub(r'[\r\n]', ' ', dir)
53             if create:
54                 conn.write('init-dir %s\n' % dir)
55             else:
56                 conn.write('set-dir %s\n' % dir)
57             self.check_ok()
58         self.sync_indexes_del()
59
60     def __del__(self):
61         try:
62             self.close()
63         except IOError, e:
64             if e.errno == errno.EPIPE:
65                 pass
66             else:
67                 raise
68
69     def close(self):
70         if self.conn and not self._busy:
71             self.conn.write('quit\n')
72         if self.p:
73             self.p.stdin.close()
74             while self.p.stdout.read(65536):
75                 pass
76             self.p.stdout.close()
77             self.p.wait()
78             rv = self.p.wait()
79             if rv:
80                 raise ClientError('server tunnel returned exit code %d' % rv)
81         self.conn = None
82         self.p = None
83
84     def check_ok(self):
85         rv = self.p.poll()
86         if rv != None:
87             raise ClientError('server exited unexpectedly with code %r' % rv)
88         try:
89             return self.conn.check_ok()
90         except Exception, e:
91             raise ClientError, e, sys.exc_info()[2]
92
93     def check_busy(self):
94         if self._busy:
95             raise ClientError('already busy with command %r' % self._busy)
96         
97     def _not_busy(self):
98         self._busy = None
99
100     def sync_indexes_del(self):
101         self.check_busy()
102         conn = self.conn
103         conn.write('list-indexes\n')
104         packdir = git.repo('objects/pack')
105         all = {}
106         needed = {}
107         for line in linereader(conn):
108             if not line:
109                 break
110             all[line] = 1
111             assert(line.find('/') < 0)
112             if not os.path.exists(os.path.join(self.cachedir, line)):
113                 needed[line] = 1
114         self.check_ok()
115
116         mkdirp(self.cachedir)
117         for f in os.listdir(self.cachedir):
118             if f.endswith('.idx') and not f in all:
119                 log('pruning old index: %r\n' % f)
120                 os.unlink(os.path.join(self.cachedir, f))
121
122     def sync_index(self, name):
123         #log('requesting %r\n' % name)
124         mkdirp(self.cachedir)
125         self.conn.write('send-index %s\n' % name)
126         n = struct.unpack('!I', self.conn.read(4))[0]
127         assert(n)
128         fn = os.path.join(self.cachedir, name)
129         f = open(fn + '.tmp', 'w')
130         count = 0
131         progress('Receiving index: %d/%d\r' % (count, n))
132         for b in chunkyreader(self.conn, n):
133             f.write(b)
134             count += len(b)
135             progress('Receiving index: %d/%d\r' % (count, n))
136         progress('Receiving index: %d/%d, done.\n' % (count, n))
137         self.check_ok()
138         f.close()
139         os.rename(fn + '.tmp', fn)
140
141     def _make_objcache(self):
142         ob = self._busy
143         self._busy = None
144         #self.sync_indexes()
145         self._busy = ob
146         return git.PackIdxList(self.cachedir)
147
148     def _suggest_pack(self, indexname):
149         log('received index suggestion: %s\n' % indexname)
150         ob = self._busy
151         if ob:
152             assert(ob == 'receive-objects')
153             self._busy = None
154             self.conn.write('\xff\xff\xff\xff')  # suspend receive-objects
155             self.conn.drain_and_check_ok()
156         self.sync_index(indexname)
157         if ob:
158             self.conn.write('receive-objects\n')
159             self._busy = ob
160
161     def new_packwriter(self):
162         self.check_busy()
163         self._busy = 'receive-objects'
164         return PackWriter_Remote(self.conn,
165                                  objcache_maker = self._make_objcache,
166                                  suggest_pack = self._suggest_pack,
167                                  onclose = self._not_busy)
168
169     def read_ref(self, refname):
170         self.check_busy()
171         self.conn.write('read-ref %s\n' % refname)
172         r = self.conn.readline().strip()
173         self.check_ok()
174         if r:
175             assert(len(r) == 40)   # hexified sha
176             return r.decode('hex')
177         else:
178             return None   # nonexistent ref
179
180     def update_ref(self, refname, newval, oldval):
181         self.check_busy()
182         self.conn.write('update-ref %s\n%s\n%s\n' 
183                         % (refname, newval.encode('hex'),
184                            (oldval or '').encode('hex')))
185         self.check_ok()
186
187     def cat(self, id):
188         self.check_busy()
189         self._busy = 'cat'
190         self.conn.write('cat %s\n' % re.sub(r'[\n\r]', '_', id))
191         while 1:
192             sz = struct.unpack('!I', self.conn.read(4))[0]
193             if not sz: break
194             yield self.conn.read(sz)
195         e = self.check_ok()
196         self._not_busy()
197         if e:
198             raise KeyError(str(e))
199
200
201 class PackWriter_Remote(git.PackWriter):
202     def __init__(self, conn, objcache_maker, suggest_pack, onclose):
203         git.PackWriter.__init__(self, objcache_maker)
204         self.file = conn
205         self.filename = 'remote socket'
206         self.suggest_pack = suggest_pack
207         self.onclose = onclose
208         self._packopen = False
209
210     def _open(self):
211         if not self._packopen:
212             self._make_objcache()
213             self.file.write('receive-objects\n')
214             self._packopen = True
215
216     def _end(self):
217         if self._packopen and self.file:
218             self.file.write('\0\0\0\0')
219             self._packopen = False
220             while True:
221                 line = self.file.readline().strip()
222                 if line.startswith('index '):
223                     pass
224                 else:
225                     break
226             id = line
227             self.file.check_ok()
228             self.objcache = None
229             if self.onclose:
230                 self.onclose()
231             if self.suggest_pack:
232                 self.suggest_pack(id)
233             return id
234
235     def close(self):
236         id = self._end()
237         self.file = None
238         return id
239
240     def abort(self):
241         raise GitError("don't know how to abort remote pack writing")
242
243     def _raw_write(self, datalist):
244         assert(self.file)
245         if not self._packopen:
246             self._open()
247         data = ''.join(datalist)
248         assert(len(data))
249         self.file.write(struct.pack('!I', len(data)) + data)
250         self.outbytes += len(data)
251         self.count += 1
252
253         if self.file.has_input():
254             line = self.file.readline().strip()
255             assert(line.startswith('index '))
256             idxname = line[6:]
257             if self.suggest_pack:
258                 self.suggest_pack(idxname)
259                 self.objcache.refresh()