borg/attic/remote.py

272 lines
9.3 KiB
Python
Raw Normal View History

2010-11-15 21:18:47 +00:00
import fcntl
import msgpack
import os
import select
from subprocess import Popen, PIPE
2010-11-15 21:18:47 +00:00
import sys
import getpass
2013-06-24 20:41:05 +00:00
from .repository import Repository
from .lrucache import LRUCache
2010-11-15 21:18:47 +00:00
BUFSIZE = 10 * 1024 * 1024
2011-07-05 19:29:15 +00:00
class ConnectionClosed(Exception):
"""Connection closed by remote host
"""
2013-06-20 10:44:58 +00:00
class RepositoryServer(object):
2010-11-15 21:18:47 +00:00
def __init__(self):
2013-06-20 10:44:58 +00:00
self.repository = None
2010-11-15 21:18:47 +00:00
def serve(self):
# Make stdin non-blocking
fl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, fl | os.O_NONBLOCK)
# Make stdout blocking
fl = fcntl.fcntl(sys.stdout.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, fl & ~os.O_NONBLOCK)
unpacker = msgpack.Unpacker(use_list=False)
2010-11-15 21:18:47 +00:00
while True:
r, w, es = select.select([sys.stdin], [], [], 10)
if r:
data = os.read(sys.stdin.fileno(), BUFSIZE)
if not data:
return
unpacker.feed(data)
for type, msgid, method, args in unpacker:
2013-06-03 11:45:48 +00:00
method = method.decode('ascii')
2010-11-15 21:18:47 +00:00
try:
2010-11-17 21:40:39 +00:00
try:
f = getattr(self, method)
except AttributeError:
2013-06-20 10:44:58 +00:00
f = getattr(self.repository, method)
2010-11-17 21:40:39 +00:00
res = f(*args)
2013-06-03 11:45:48 +00:00
except Exception as e:
sys.stdout.buffer.write(msgpack.packb((1, msgid, e.__class__.__name__, None)))
2010-11-15 21:18:47 +00:00
else:
2013-06-03 11:45:48 +00:00
sys.stdout.buffer.write(msgpack.packb((1, msgid, None, res)))
2010-11-15 21:18:47 +00:00
sys.stdout.flush()
if es:
return
2011-09-12 19:51:23 +00:00
def negotiate(self, versions):
return 1
2010-11-17 21:40:39 +00:00
def open(self, path, create=False):
2013-06-03 11:45:48 +00:00
path = os.fsdecode(path)
2010-11-17 21:40:39 +00:00
if path.startswith('/~'):
path = path[1:]
2013-06-20 10:44:58 +00:00
self.repository = Repository(os.path.expanduser(path), create)
return self.repository.id
2010-11-17 21:40:39 +00:00
2010-11-15 21:18:47 +00:00
2013-06-20 10:44:58 +00:00
class RemoteRepository(object):
2010-11-15 21:18:47 +00:00
class RPCError(Exception):
def __init__(self, name):
self.name = name
def __init__(self, location, create=False):
2013-05-28 12:35:55 +00:00
self.p = None
self.cache = LRUCache(256)
2013-06-03 11:45:48 +00:00
self.to_send = b''
self.extra = {}
self.pending = {}
self.unpacker = msgpack.Unpacker(use_list=False)
2010-11-15 21:18:47 +00:00
self.msgid = 0
self.received_msgid = 0
if location.host == '__testsuite__':
2013-07-08 21:38:27 +00:00
args = [sys.executable, '-m', 'attic.archiver', 'serve']
else:
2013-07-08 21:38:27 +00:00
args = ['ssh', '-p', str(location.port), '%s@%s' % (location.user or getpass.getuser(), location.host), 'attic', 'serve']
self.p = Popen(args, bufsize=0, stdin=PIPE, stdout=PIPE)
self.stdin_fd = self.p.stdin.fileno()
self.stdout_fd = self.p.stdout.fileno()
fcntl.fcntl(self.stdin_fd, fcntl.F_SETFL, fcntl.fcntl(self.stdin_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
fcntl.fcntl(self.stdout_fd, fcntl.F_SETFL, fcntl.fcntl(self.stdout_fd, fcntl.F_GETFL) | os.O_NONBLOCK)
self.r_fds = [self.stdout_fd]
self.x_fds = [self.stdin_fd, self.stdout_fd]
version = self.call('negotiate', (1,))
2011-09-12 19:51:23 +00:00
if version != 1:
raise Exception('Server insisted on using unsupported protocol version %d' % version)
2012-12-09 22:06:33 +00:00
try:
self.id = self.call('open', (location.path, create))
2013-06-03 11:45:48 +00:00
except self.RPCError as e:
if e.name == b'DoesNotExist':
2013-06-20 10:44:58 +00:00
raise Repository.DoesNotExist
2013-06-03 11:45:48 +00:00
elif e.name == b'AlreadyExists':
2013-06-20 10:44:58 +00:00
raise Repository.AlreadyExists
def __del__(self):
2012-11-30 20:47:35 +00:00
self.close()
def call(self, cmd, args, wait=True):
self.msgid += 1
to_send = msgpack.packb((1, self.msgid, cmd, args))
w_fds = [self.stdin_fd]
while wait or to_send:
r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1)
2012-11-30 20:47:35 +00:00
if x:
raise Exception('FD exception occured')
if r:
2012-12-09 22:06:33 +00:00
data = os.read(self.stdout_fd, BUFSIZE)
if not data:
raise ConnectionClosed()
2012-12-09 22:06:33 +00:00
self.unpacker.feed(data)
2012-11-30 20:47:35 +00:00
for type, msgid, error, res in self.unpacker:
if msgid == self.msgid:
assert msgid == self.msgid
self.received_msgid = msgid
if error:
raise self.RPCError(error)
else:
return res
else:
args = self.pending.pop(msgid, None)
if args is not None:
self.cache[args] = msgid, res, error
if w:
if to_send:
n = os.write(self.stdin_fd, to_send)
assert n > 0
2013-06-03 11:45:48 +00:00
to_send = memoryview(to_send)[n:]
else:
w_fds = []
def _read(self):
data = os.read(self.stdout_fd, BUFSIZE)
if not data:
2012-12-09 22:06:33 +00:00
raise Exception('Remote host closed connection')
self.unpacker.feed(data)
to_yield = []
for type, msgid, error, res in self.unpacker:
self.received_msgid = msgid
2012-11-30 20:47:35 +00:00
args = self.pending.pop(msgid, None)
if args is not None:
self.cache[args] = msgid, res, error
for args, resp, error in self.extra.pop(msgid, []):
if not resp and not error:
resp, error = self.cache[args][1:]
to_yield.append((resp, error))
for res, error in to_yield:
if error:
raise self.RPCError(error)
2012-11-30 20:47:35 +00:00
else:
yield res
2012-11-30 20:47:35 +00:00
def gen_request(self, cmd, argsv, wait):
data = []
m = self.received_msgid
for args in argsv:
2012-11-30 20:47:35 +00:00
# Make sure to invalidate any existing cache entries for non-get requests
if not args in self.cache:
self.msgid += 1
msgid = self.msgid
self.pending[msgid] = args
2012-11-30 20:47:35 +00:00
self.cache[args] = msgid, None, None
data.append(msgpack.packb((1, msgid, cmd, args)))
2012-11-30 20:47:35 +00:00
if wait:
msgid, resp, error = self.cache[args]
m = max(m, msgid)
self.extra.setdefault(m, []).append((args, resp, error))
2013-06-03 11:45:48 +00:00
return b''.join(data)
def gen_cache_requests(self, cmd, peek):
data = []
while True:
try:
args = (peek()[0],)
except StopIteration:
break
if args in self.cache:
continue
self.msgid += 1
msgid = self.msgid
self.pending[msgid] = args
2012-11-30 20:47:35 +00:00
self.cache[args] = msgid, None, None
data.append(msgpack.packb((1, msgid, cmd, args)))
2013-06-03 11:45:48 +00:00
return b''.join(data)
def call_multi(self, cmd, argsv, wait=True, peek=None):
w_fds = [self.stdin_fd]
left = len(argsv)
2012-11-30 20:47:35 +00:00
data = self.gen_request(cmd, argsv, wait)
self.to_send += data
2012-11-30 20:47:35 +00:00
for args, resp, error in self.extra.pop(self.received_msgid, []):
left -= 1
2012-11-30 20:47:35 +00:00
if not resp and not error:
resp, error = self.cache[args][1:]
if error:
raise self.RPCError(error)
else:
yield resp
while left:
r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1)
if x:
raise Exception('FD exception occured')
if r:
for res in self._read():
left -= 1
yield res
if w:
if not self.to_send and peek:
self.to_send = self.gen_cache_requests(cmd, peek)
if self.to_send:
n = os.write(self.stdin_fd, self.to_send)
assert n > 0
2013-06-03 11:45:48 +00:00
# self.to_send = memoryview(self.to_send)[n:]
self.to_send = self.to_send[n:]
else:
w_fds = []
2012-11-30 20:47:35 +00:00
if not wait:
return
2011-09-05 19:20:17 +00:00
def commit(self, *args):
self.call('commit', args)
2010-11-15 21:18:47 +00:00
def rollback(self, *args):
2012-11-30 20:47:35 +00:00
self.cache.clear()
self.pending.clear()
self.extra.clear()
return self.call('rollback', args)
2010-11-15 21:18:47 +00:00
def get(self, id):
2010-11-15 21:18:47 +00:00
try:
2012-11-30 20:47:35 +00:00
for res in self.call_multi('get', [(id, )]):
return res
2013-06-03 11:45:48 +00:00
except self.RPCError as e:
if e.name == b'DoesNotExist':
2013-06-20 10:44:58 +00:00
raise Repository.DoesNotExist
raise
def get_many(self, ids, peek=None):
return self.call_multi('get', [(id, ) for id in ids], peek=peek)
2012-11-30 20:47:35 +00:00
def _invalidate(self, id):
key = (id, )
if key in self.cache:
self.pending.pop(self.cache.pop(key)[0], None)
def put(self, id, data, wait=True):
2012-12-09 22:06:33 +00:00
resp = self.call('put', (id, data), wait=wait)
self._invalidate(id)
return resp
2010-11-15 21:18:47 +00:00
def delete(self, id, wait=True):
2012-11-30 20:47:35 +00:00
resp = self.call('delete', (id, ), wait=wait)
self._invalidate(id)
return resp
def close(self):
if self.p:
self.p.stdin.close()
self.p.stdout.close()
self.p.wait()
self.p = None