# sshserver.py - ssh protocol server support for mercurial # # Copyright 2005-2007 Matt Mackall # Copyright 2006 Vadim Gelfer # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from i18n import _ from node import bin, hex import os, streamclone, sys, tempfile, util, hook class sshserver(object): def __init__(self, ui, repo): self.ui = ui self.repo = repo self.lock = None self.fin = sys.stdin self.fout = sys.stdout hook.redirect(True) sys.stdout = sys.stderr # Prevent insertion/deletion of CRs util.set_binary(self.fin) util.set_binary(self.fout) def getarg(self): argline = self.fin.readline()[:-1] arg, l = argline.split() val = self.fin.read(int(l)) return arg, val def respond(self, v): self.fout.write("%d\n" % len(v)) self.fout.write(v) self.fout.flush() def serve_forever(self): while self.serve_one(): pass sys.exit(0) def serve_one(self): cmd = self.fin.readline()[:-1] if cmd: impl = getattr(self, 'do_' + cmd, None) if impl: impl() else: self.respond("") return cmd != '' def do_lookup(self): arg, key = self.getarg() assert arg == 'key' try: r = hex(self.repo.lookup(key)) success = 1 except Exception,inst: r = str(inst) success = 0 self.respond("%s %s\n" % (success, r)) def do_heads(self): h = self.repo.heads() self.respond(" ".join(map(hex, h)) + "\n") def do_hello(self): '''the hello command returns a set of lines describing various interesting things about the server, in an RFC822-like format. Currently the only one defined is "capabilities", which consists of a line in the form: capabilities: space separated list of tokens ''' caps = ['unbundle', 'lookup', 'changegroupsubset'] if self.ui.configbool('server', 'uncompressed'): caps.append('stream=%d' % self.repo.changelog.version) self.respond("capabilities: %s\n" % (' '.join(caps),)) def do_lock(self): '''DEPRECATED - allowing remote client to lock repo is not safe''' self.lock = self.repo.lock() self.respond("") def do_unlock(self): '''DEPRECATED''' if self.lock: self.lock.release() self.lock = None self.respond("") def do_branches(self): arg, nodes = self.getarg() nodes = map(bin, nodes.split(" ")) r = [] for b in self.repo.branches(nodes): r.append(" ".join(map(hex, b)) + "\n") self.respond("".join(r)) def do_between(self): arg, pairs = self.getarg() pairs = [map(bin, p.split("-")) for p in pairs.split(" ")] r = [] for b in self.repo.between(pairs): r.append(" ".join(map(hex, b)) + "\n") self.respond("".join(r)) def do_changegroup(self): nodes = [] arg, roots = self.getarg() nodes = map(bin, roots.split(" ")) cg = self.repo.changegroup(nodes, 'serve') while True: d = cg.read(4096) if not d: break self.fout.write(d) self.fout.flush() def do_changegroupsubset(self): argmap = dict([self.getarg(), self.getarg()]) bases = [bin(n) for n in argmap['bases'].split(' ')] heads = [bin(n) for n in argmap['heads'].split(' ')] cg = self.repo.changegroupsubset(bases, heads, 'serve') while True: d = cg.read(4096) if not d: break self.fout.write(d) self.fout.flush() def do_addchangegroup(self): '''DEPRECATED''' if not self.lock: self.respond("not locked") return self.respond("") r = self.repo.addchangegroup(self.fin, 'serve', self.client_url()) self.respond(str(r)) def client_url(self): client = os.environ.get('SSH_CLIENT', '').split(' ', 1)[0] return 'remote:ssh:' + client def do_unbundle(self): their_heads = self.getarg()[1].split() def check_heads(): heads = map(hex, self.repo.heads()) return their_heads == [hex('force')] or their_heads == heads # fail early if possible if not check_heads(): self.respond(_('unsynced changes')) return self.respond('') # write bundle data to temporary file because it can be big tempname = fp = None try: fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') fp = os.fdopen(fd, 'wb+') count = int(self.fin.readline()) while count: fp.write(self.fin.read(count)) count = int(self.fin.readline()) was_locked = self.lock is not None if not was_locked: self.lock = self.repo.lock() try: if not check_heads(): # someone else committed/pushed/unbundled while we # were transferring data self.respond(_('unsynced changes')) return self.respond('') # push can proceed fp.seek(0) r = self.repo.addchangegroup(fp, 'serve', self.client_url()) self.respond(str(r)) finally: if not was_locked: self.lock.release() self.lock = None finally: if fp is not None: fp.close() if tempname is not None: os.unlink(tempname) def do_stream_out(self): try: for chunk in streamclone.stream_out(self.repo): self.fout.write(chunk) self.fout.flush() except streamclone.StreamException, inst: self.fout.write(str(inst)) self.fout.flush()