]> arthur.barton.de Git - bup.git/blob - lib/bup/cmd/server.py
Convert bup to binary executable and run python subcommands directly
[bup.git] / lib / bup / cmd / server.py
1
2 from __future__ import absolute_import
3 from binascii import hexlify, unhexlify
4 import os, struct, subprocess, sys
5
6 from bup import options, git, vfs, vint
7 from bup.compat import environ, hexstr
8 from bup.git import MissingObject
9 from bup.helpers import (Conn, debug1, debug2, linereader, lines_until_sentinel,
10                          log)
11 from bup.io import byte_stream, path_msg
12 from bup.repo import LocalRepo
13
14
15 suspended_w = None
16 dumb_server_mode = False
17 repo = None
18
19
20 def do_help(conn, junk):
21     conn.write(b'Commands:\n    %s\n' % b'\n    '.join(sorted(commands)))
22     conn.ok()
23
24
25 def _set_mode():
26     global dumb_server_mode
27     dumb_server_mode = os.path.exists(git.repo(b'bup-dumb-server'))
28     debug1('bup server: serving in %s mode\n' 
29            % (dumb_server_mode and 'dumb' or 'smart'))
30
31
32 def _init_session(reinit_with_new_repopath=None):
33     global repo
34     if reinit_with_new_repopath is None and git.repodir:
35         if not repo:
36             repo = LocalRepo()
37         return
38     git.check_repo_or_die(reinit_with_new_repopath)
39     if repo:
40         repo.close()
41     repo = LocalRepo()
42     # OK. we now know the path is a proper repository. Record this path in the
43     # environment so that subprocesses inherit it and know where to operate.
44     environ[b'BUP_DIR'] = git.repodir
45     debug1('bup server: bupdir is %s\n' % path_msg(git.repodir))
46     _set_mode()
47
48
49 def init_dir(conn, arg):
50     git.init_repo(arg)
51     debug1('bup server: bupdir initialized: %s\n' % path_msg(git.repodir))
52     _init_session(arg)
53     conn.ok()
54
55
56 def set_dir(conn, arg):
57     _init_session(arg)
58     conn.ok()
59
60     
61 def list_indexes(conn, junk):
62     _init_session()
63     suffix = b''
64     if dumb_server_mode:
65         suffix = b' load'
66     for f in os.listdir(git.repo(b'objects/pack')):
67         if f.endswith(b'.idx'):
68             conn.write(b'%s%s\n' % (f, suffix))
69     conn.ok()
70
71
72 def send_index(conn, name):
73     _init_session()
74     assert name.find(b'/') < 0
75     assert name.endswith(b'.idx')
76     idx = git.open_idx(git.repo(b'objects/pack/%s' % name))
77     conn.write(struct.pack('!I', len(idx.map)))
78     conn.write(idx.map)
79     conn.ok()
80
81
82 def receive_objects_v2(conn, junk):
83     global suspended_w
84     _init_session()
85     suggested = set()
86     if suspended_w:
87         w = suspended_w
88         suspended_w = None
89     else:
90         if dumb_server_mode:
91             w = git.PackWriter(objcache_maker=None)
92         else:
93             w = git.PackWriter()
94     while 1:
95         ns = conn.read(4)
96         if not ns:
97             w.abort()
98             raise Exception('object read: expected length header, got EOF\n')
99         n = struct.unpack('!I', ns)[0]
100         #debug2('expecting %d bytes\n' % n)
101         if not n:
102             debug1('bup server: received %d object%s.\n' 
103                 % (w.count, w.count!=1 and "s" or ''))
104             fullpath = w.close(run_midx=not dumb_server_mode)
105             if fullpath:
106                 (dir, name) = os.path.split(fullpath)
107                 conn.write(b'%s.idx\n' % name)
108             conn.ok()
109             return
110         elif n == 0xffffffff:
111             debug2('bup server: receive-objects suspended.\n')
112             suspended_w = w
113             conn.ok()
114             return
115             
116         shar = conn.read(20)
117         crcr = struct.unpack('!I', conn.read(4))[0]
118         n -= 20 + 4
119         buf = conn.read(n)  # object sizes in bup are reasonably small
120         #debug2('read %d bytes\n' % n)
121         _check(w, n, len(buf), 'object read: expected %d bytes, got %d\n')
122         if not dumb_server_mode:
123             oldpack = w.exists(shar, want_source=True)
124             if oldpack:
125                 assert(not oldpack == True)
126                 assert(oldpack.endswith(b'.idx'))
127                 (dir,name) = os.path.split(oldpack)
128                 if not (name in suggested):
129                     debug1("bup server: suggesting index %s\n"
130                            % git.shorten_hash(name).decode('ascii'))
131                     debug1("bup server:   because of object %s\n"
132                            % hexstr(shar))
133                     conn.write(b'index %s\n' % name)
134                     suggested.add(name)
135                 continue
136         nw, crc = w._raw_write((buf,), sha=shar)
137         _check(w, crcr, crc, 'object read: expected crc %d, got %d\n')
138     # NOTREACHED
139     
140
141 def _check(w, expected, actual, msg):
142     if expected != actual:
143         w.abort()
144         raise Exception(msg % (expected, actual))
145
146
147 def read_ref(conn, refname):
148     _init_session()
149     r = git.read_ref(refname)
150     conn.write(b'%s\n' % hexlify(r) if r else b'')
151     conn.ok()
152
153
154 def update_ref(conn, refname):
155     _init_session()
156     newval = conn.readline().strip()
157     oldval = conn.readline().strip()
158     git.update_ref(refname, unhexlify(newval), unhexlify(oldval))
159     conn.ok()
160
161 def join(conn, id):
162     _init_session()
163     try:
164         for blob in git.cp().join(id):
165             conn.write(struct.pack('!I', len(blob)))
166             conn.write(blob)
167     except KeyError as e:
168         log('server: error: %s\n' % e)
169         conn.write(b'\0\0\0\0')
170         conn.error(e)
171     else:
172         conn.write(b'\0\0\0\0')
173         conn.ok()
174
175 def cat_batch(conn, dummy):
176     _init_session()
177     cat_pipe = git.cp()
178     # For now, avoid potential deadlock by just reading them all
179     for ref in tuple(lines_until_sentinel(conn, b'\n', Exception)):
180         ref = ref[:-1]
181         it = cat_pipe.get(ref)
182         info = next(it)
183         if not info[0]:
184             conn.write(b'missing\n')
185             continue
186         conn.write(b'%s %s %d\n' % info)
187         for buf in it:
188             conn.write(buf)
189     conn.ok()
190
191 def refs(conn, args):
192     limit_to_heads, limit_to_tags = args.split()
193     assert limit_to_heads in (b'0', b'1')
194     assert limit_to_tags in (b'0', b'1')
195     limit_to_heads = int(limit_to_heads)
196     limit_to_tags = int(limit_to_tags)
197     _init_session()
198     patterns = tuple(x[:-1] for x in lines_until_sentinel(conn, b'\n', Exception))
199     for name, oid in git.list_refs(patterns=patterns,
200                                    limit_to_heads=limit_to_heads,
201                                    limit_to_tags=limit_to_tags):
202         assert b'\n' not in name
203         conn.write(b'%s %s\n' % (hexlify(oid), name))
204     conn.write(b'\n')
205     conn.ok()
206
207 def rev_list(conn, _):
208     _init_session()
209     count = conn.readline()
210     if not count:
211         raise Exception('Unexpected EOF while reading rev-list count')
212     assert count == b'\n'
213     count = None
214     fmt = conn.readline()
215     if not fmt:
216         raise Exception('Unexpected EOF while reading rev-list format')
217     fmt = None if fmt == b'\n' else fmt[:-1]
218     refs = tuple(x[:-1] for x in lines_until_sentinel(conn, b'\n', Exception))
219     args = git.rev_list_invocation(refs, format=fmt)
220     p = subprocess.Popen(args, env=git._gitenv(git.repodir),
221                          stdout=subprocess.PIPE)
222     while True:
223         out = p.stdout.read(64 * 1024)
224         if not out:
225             break
226         conn.write(out)
227     conn.write(b'\n')
228     rv = p.wait()  # not fatal
229     if rv:
230         msg = 'git rev-list returned error %d' % rv
231         conn.error(msg)
232         raise GitError(msg)
233     conn.ok()
234
235 def resolve(conn, args):
236     _init_session()
237     (flags,) = args.split()
238     flags = int(flags)
239     want_meta = bool(flags & 1)
240     follow = bool(flags & 2)
241     have_parent = bool(flags & 4)
242     parent = vfs.read_resolution(conn) if have_parent else None
243     path = vint.read_bvec(conn)
244     if not len(path):
245         raise Exception('Empty resolve path')
246     try:
247         res = list(vfs.resolve(repo, path, parent=parent, want_meta=want_meta,
248                                follow=follow))
249     except vfs.IOError as ex:
250         res = ex
251     if isinstance(res, vfs.IOError):
252         conn.write(b'\x00')  # error
253         vfs.write_ioerror(conn, res)
254     else:
255         conn.write(b'\x01')  # success
256         vfs.write_resolution(conn, res)
257     conn.ok()
258
259 optspec = """
260 bup server
261 """
262
263 commands = {
264     b'quit': None,
265     b'help': do_help,
266     b'init-dir': init_dir,
267     b'set-dir': set_dir,
268     b'list-indexes': list_indexes,
269     b'send-index': send_index,
270     b'receive-objects-v2': receive_objects_v2,
271     b'read-ref': read_ref,
272     b'update-ref': update_ref,
273     b'join': join,
274     b'cat': join,  # apocryphal alias
275     b'cat-batch' : cat_batch,
276     b'refs': refs,
277     b'rev-list': rev_list,
278     b'resolve': resolve
279 }
280
281 def main(argv):
282     o = options.Options(optspec)
283     opt, flags, extra = o.parse_bytes(argv[1:])
284
285     if extra:
286         o.fatal('no arguments expected')
287
288     debug2('bup server: reading from stdin.\n')
289
290     # FIXME: this protocol is totally lame and not at all future-proof.
291     # (Especially since we abort completely as soon as *anything* bad happens)
292     sys.stdout.flush()
293     conn = Conn(byte_stream(sys.stdin), byte_stream(sys.stdout))
294     lr = linereader(conn)
295     for _line in lr:
296         line = _line.strip()
297         if not line:
298             continue
299         debug1('bup server: command: %r\n' % line)
300         words = line.split(b' ', 1)
301         cmd = words[0]
302         rest = len(words)>1 and words[1] or b''
303         if cmd == b'quit':
304             break
305         else:
306             cmd = commands.get(cmd)
307             if cmd:
308                 cmd(conn, rest)
309             else:
310                 raise Exception('unknown server command: %r\n' % line)
311
312     debug1('bup server: done\n')