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