[PATCH 7/7] bup-on: restrict local server actions based on command

0 views
Skip to first unread message

Rob Browning

unread,
Mar 14, 2026, 6:31:21 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
We know, for example, that "bup on host restore" should never write to
a local repository. We also know that "bup on host save" must only
update a --name ref, that the ref must be a branch, and that the
update must only add a commit to the end of the branch.

Arrange to run the local server with server command restrictions
reflecting the requested operation, so that the remote bup command's
access is tailored more closely to what that command needs, rather
than allowing it to do anything.

Add a commands argument to Server() to control the set of available
server commands, and add vet_init_dir, vet_set_dir, and vet_update_ref
options to allow specifying functions to allow/disallow related
invocations. Use commands and the vet arguments from bup-on to
implement the server restrictions.

Thanks to Johannes Berg for proposing related server restrictions that
we'll likely pursue, and for which these changes may provide some
support.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
Documentation/bup-on.1.md | 37 +++++++------------
lib/bup/cmd/on.py | 78 +++++++++++++++++++++++++++++++++------
lib/bup/cmd/save.py | 8 ++--
lib/bup/cmd/split.py | 8 ++--
lib/bup/protocol.py | 60 +++++++++++++++++++++++-------
note/main.md | 10 +++++
6 files changed, 143 insertions(+), 58 deletions(-)

diff --git a/Documentation/bup-on.1.md b/Documentation/bup-on.1.md
index 8d84d22c..426fc06a 100644
--- a/Documentation/bup-on.1.md
+++ b/Documentation/bup-on.1.md
@@ -4,7 +4,7 @@

# NAME

-bup-on - run a bup server locally and client remotely
+bup-on - run a bup on a remote host, communicating with a local server

# SYNOPSIS

@@ -21,29 +21,18 @@ bup on [*user*@]*host*[:*port*] get ...

# DESCRIPTION

-`bup on` runs the given bup command on the given host using
-ssh. It runs a bup server on the local machine, so that
-commands like `bup save` on the remote machine can back up
-to the local machine. (You don't need to provide a
-`--remote` option to `bup save` in order for this to work.)
-
-See `bup-index`(1), `bup-save`(1), and so on for details of
-how each subcommand works.
-
-This 'reverse mode' operation is useful when the machine
-being backed up isn't supposed to be able to ssh into the
-backup server. For example, your backup server can be
-hidden behind a one-way firewall on a private or dynamic IP
-address; using an ssh key, it can be authorized to ssh into
-each of your important machines. After connecting to each
-destination machine, it initiates a backup, receiving the
-resulting data and storing in its local repository.
-
-For example, if you run several virtual private Linux
-machines on a remote hosting provider, you could back them
-up to a local (much less expensive) computer in your
-basement.
-
+`bup on` runs the command on the remote host via ssh, connected to a
+bup server running on the local machine, so that remote commands like
+`bup save` can back up to the local repository. See `bup-index`(1),
+`bup-save`(1), and so on for the command details.
+
+Note that you don't need to (and shouldn't) provide a `--remote`
+option. This "reverse mode" is useful when the machine being backed
+up isn't supposed to be able to ssh into the backup server. Instead,
+the backup server, even if hidden behind a one-way firewall on a
+private or dynamic IP address, can connect to each of your important
+machines using an ssh key, and initiate a backup that saves the remote
+data to the local repository.

# EXAMPLES

diff --git a/lib/bup/cmd/on.py b/lib/bup/cmd/on.py
index abdca567..d5b65258 100644
--- a/lib/bup/cmd/on.py
+++ b/lib/bup/cmd/on.py
@@ -1,16 +1,21 @@

+from binascii import hexlify
from subprocess import PIPE, Popen
# Python upstream deprecated this and then undeprecated it...
# pylint: disable-next=deprecated-module
import getopt
-import struct, sys
+import os, struct, sys

from bup import git, options, ssh, protocol
+from bup.cmd.save import opts_from_cmdline as parse_save_args
+from bup.cmd.split import opts_from_cmdline as parse_split_args
from bup.compat import argv_bytes
+from bup.git import check_repo_or_die, parse_commit
from bup.helpers import Conn, stopped
from bup.io import path_msg as pm
+from bup.protocol import CommandDenied
from bup.repo import LocalRepo
-import bup.path
+import bup.cmd.save, bup.cmd.split, bup.path

optspec = """
bup on <[user@]host[:port]> <index|save|split|get> ...
@@ -50,35 +55,84 @@ bup on <[user@]host[:port]> <index|save|split|get> ...
# +------------------+
#

-def run_server(on_srv):
+def run_server(on_srv, config):
class ServerRepo(LocalRepo):
def __init__(self, repo_dir, server):
self.closed = True # subclass' __del__ can run before its init
git.check_repo_or_die(repo_dir)
LocalRepo.__init__(self, repo_dir, server=server)
with Conn(on_srv.stdout, on_srv.stdin) as conn, \
- protocol.Server(conn, ServerRepo) as server:
+ protocol.Server(conn, ServerRepo, **config) as server:
server.handle()

+def restricted_repo_config():
+ repo = git.repo()
+ def init_dir(repo_, path):
+ if not os.path.samefile(path, repo):
+ raise CommandDenied(f'disallowing unexpected init-dir {pm(path)}')
+ def set_dir(repo_, path):
+ if not os.path.samefile(path, repo):
+ raise CommandDenied(f'disallowing unexpected set-dir {pm(path)}')
+ return {'vet_init_dir': init_dir, 'vet_set_dir': set_dir}
+
+def save_or_split_server_config(argv, opt_parser):
+ cmd = pm(argv[0])
+ opt = opt_parser(argv)[0]
+ def vet_update_ref(repo, ref, new_oid, prev_oid):
+ # Ensure split/save adds only a new commit, with prev_oid (if
+ # any) as its parent.
+ cd = CommandDenied
+ if ref != b'refs/heads/' + opt.name:
+ raise cd(f'unexpected {cmd} ref update: {pm(ref)}')
+ _, prev_kind, _, it = repo.cat(hexlify(prev_oid))
+ if it:
+ for _ in it: pass
+ if prev_kind not in (None, b'commit'):
+ raise cd(f'{cmd}: existing {pm(ref)} {prev_oid.hex()} is not a commit')
+ _, kind, _, it = repo.cat(hexlify(new_oid))
+ if kind != b'commit':
+ raise cd(f'{cmd}: {pm(ref)} update {new_oid.hex()} is not a commit')
+ info = parse_commit(b''.join(it))
+ if prev_kind and hexlify(prev_oid) not in info.parents:
+ raise cd(f'{cmd}: {pm(ref)} update {new_oid.hex()} is not child of {prev_oid.hex()}')
+ return {'commands': (b'config-get',
+ b'read-ref',
+ b'receive-objects-v2',
+ b'update-ref'),
+ 'vet_update_ref': vet_update_ref,
+ **restricted_repo_config()}
+
def main(argv):
o = options.Options(optspec, optfunc=getopt.getopt)
extra = o.parse_bytes(argv[1:])[2]
if len(extra) < 2:
- o.fatal('must specify index, save, split, or get command')
+ o.fatal('must specify a command to run on the host')
dest, *argv = [argv_bytes(x) for x in extra]
dest, colon, port = dest.rpartition(b':')
if not colon:
dest, port = port, None
+
cmd = argv[0]
if cmd == b'init':
o.fatal('init is not supported; ssh or run "bup init -r ..." instead')
if cmd in (b'features', b'help', b'index', b'version'):
- want_server = False
- elif cmd in (b'get', b'restore', b'save', b'split'):
- want_server = True
+ srv_config = None
else:
- o.fatal(f'{pm(cmd)} is not currently supported')
- assert False # (so pylint won't think srv_config might be unset)
+ check_repo_or_die()
+ if cmd == b'restore':
+ srv_config = {'commands': (b'cat-batch', b'config-get',
+ b'list-indexes', b'resolve'),
+ **restricted_repo_config()}
+ elif cmd == b'get':
+ srv_config = {'commands': 'all', **restricted_repo_config()}
+ elif cmd == b'save':
+ srv_config = save_or_split_server_config(argv, parse_save_args)
+ elif cmd == b'split':
+ srv_config = save_or_split_server_config(argv, parse_split_args)
+ else:
+ o.fatal(f'{pm(cmd)} is not currently supported')
+ assert False # (so pylint won't think srv_config might be unset)
+
sys.stdout.flush()
sys.stderr.flush()
with ssh.connect(dest, port, b'on--server', stderr=PIPE) as on_srv:
@@ -88,7 +142,7 @@ def main(argv):
# write on--server's stdout/stderr to our stdout/stderr via demux
with stopped(Popen((bup.path.exe(), b'demux'), stdin=on_srv.stderr),
timeout=1) as demux:
- if want_server:
- run_server(on_srv)
+ if srv_config:
+ run_server(on_srv, srv_config)
demux.wait() # finish the output
return on_srv.returncode
diff --git a/lib/bup/cmd/save.py b/lib/bup/cmd/save.py
index 25112bbf..4d09b06d 100644
--- a/lib/bup/cmd/save.py
+++ b/lib/bup/cmd/save.py
@@ -66,7 +66,8 @@ def before_saving_regular_file(name_):
return


-def opts_from_cmdline(o, argv):
+def opts_from_cmdline(argv):
+ o = options.Options(optspec)
opt, flags, extra = o.parse_bytes(argv[1:])

if opt.indexfile:
@@ -125,7 +126,7 @@ def opts_from_cmdline(o, argv):
if opt.name and not valid_save_name(opt.name):
o.fatal("'%s' is not a valid branch name" % path_msg(opt.name))

- return opt
+ return opt, o

def save_tree(opt, reader, hlink_db, msr, repo, split_cfg):
# Metadata is stored in a file named .bupm in each directory. The
@@ -441,8 +442,7 @@ def commit_tree(tree, parent, date, argv, repo):

def main(argv):
handle_ctrl_c()
- opt_parser = options.Options(optspec)
- opt = opts_from_cmdline(opt_parser, argv)
+ opt, opt_parser = opts_from_cmdline(argv)
client.bwlimit = opt.bwlimit

try:
diff --git a/lib/bup/cmd/split.py b/lib/bup/cmd/split.py
index fd0028ae..fe97498d 100644
--- a/lib/bup/cmd/split.py
+++ b/lib/bup/cmd/split.py
@@ -54,7 +54,8 @@ bwlimit= maximum bytes/sec to transmit to server
"""


-def opts_from_cmdline(o, argv):
+def opts_from_cmdline(argv):
+ o = options.Options(optspec)
opt, flags_, extra = o.parse_bytes(argv[1:])
opt.sources = extra

@@ -99,7 +100,7 @@ def opts_from_cmdline(o, argv):
if opt.name and not valid_save_name(opt.name):
o.fatal("'%r' is not a valid branch name." % opt.name)

- return opt
+ return opt, o


def split(opt, files, parent, out, split_cfg, *,
@@ -162,8 +163,7 @@ def split(opt, files, parent, out, split_cfg, *,
return commit

def main(argv):
- opt_parser = options.Options(optspec)
- opt = opts_from_cmdline(opt_parser, argv)
+ opt, opt_parser = opts_from_cmdline(argv)
if opt.verbose >= 2:
git.verbose = opt.verbose - 1
if opt.fanout:
diff --git a/lib/bup/protocol.py b/lib/bup/protocol.py
index c091386f..f812da46 100644
--- a/lib/bup/protocol.py
+++ b/lib/bup/protocol.py
@@ -156,23 +156,50 @@ def _command(fn):
fn.bup_server_command = True
return fn

+class CommandDenied(Exception): pass
+
class Server:
- def __init__(self, conn, backend):
+ def __init__(self, conn, backend, *, commands='all',
+ vet_init_dir=None, vet_set_dir=None, vet_update_ref=None):
+ """When commands is a sequence of command names (bytes),
+ enable those commands. When it is 'all', enable all commands.
+ The help and quit commands are always enabled, and the
+ init-dir and set-dir commands are also enabled, at least for
+ now, because earlier versions of the client unconditionally
+ required them during initialization.
+
+ The vet_* arguments can provide a function that should accept
+ COMMAND's decoded arguments, and raise a CommandDenied
+ exception if the invocation is not acceptable.
+
+ """
+ all_cmds = self._get_commands()
+ if commands == 'all':
+ self._commands = frozenset(all_cmds)
+ else:
+ for cmd in commands: assert cmd in all_cmds, commands
+ # init-dir, set-dir, list-indexes, and send-index were
+ # unconditionally required by Client() before 0.34, so
+ # always include them for now.
+ self._commands = frozenset((b'help', b'quit',
+ b'init-dir', b'set-dir',
+ b'list-indexes', b'send-index',
+ *commands))
self.conn = conn
self._backend = backend
- self._commands = self._get_commands()
self.suspended = False
self.repo = None
self._deduplicate_writes = True
+ self._vet_init_dir = vet_init_dir
+ self._vet_set_dir = vet_set_dir
+ self._vet_update_ref = vet_update_ref

def _get_commands(self):
commands = []
for name in dir(self):
fn = getattr(self, name)
-
if getattr(fn, 'bup_server_command', False):
commands.append(name.replace('_', '-').encode('ascii'))
-
return commands

@_command
@@ -200,14 +227,16 @@ class Server:
path_msg(self.repo.repo_dir)))

@_command
- def init_dir(self, arg):
- self._backend.create(arg)
- self.init_session(arg)
+ def init_dir(self, path):
+ if self._vet_init_dir: self._vet_init_dir(self.repo, path)
+ self._backend.create(path)
+ self.init_session(path)
self.conn.ok()

@_command
- def set_dir(self, arg):
- self.init_session(arg)
+ def set_dir(self, path):
+ if self._vet_set_dir: self._vet_set_dir(self.repo, path)
+ self.init_session(path)
self.conn.ok()

@_command
@@ -308,10 +337,12 @@ class Server:

@_command
def update_ref(self, refname):
+ newval = unhexlify(self.conn.readline().strip())
+ oldval = unhexlify(self.conn.readline().strip())
+ if self._vet_update_ref:
+ self._vet_update_ref(self.repo, refname, newval, oldval)
self.init_session()
- newval = self.conn.readline().strip()
- oldval = self.conn.readline().strip()
- self.repo.update_ref(refname, unhexlify(newval), unhexlify(oldval))
+ self.repo.update_ref(refname, newval, oldval)
self.conn.ok()

@_command
@@ -365,11 +396,12 @@ class Server:
@_command
def rev_list(self, _):
self.init_session()
+ # (broken) count support was removed in
+ # 89cd30c0d82c2b71e6fe14de7ad210f635b38f91
count = self.conn.readline()
if not count:
raise Exception('Unexpected EOF while reading rev-list count')
- assert count == b'\n'
- count = None
+ assert count == b'\n', count
fmt = self.conn.readline()
if not fmt:
raise Exception('Unexpected EOF while reading rev-list format')
diff --git a/note/main.md b/note/main.md
index 1e13ae7b..6571ee44 100644
--- a/note/main.md
+++ b/note/main.md
@@ -141,6 +141,16 @@ General
Combined with `--source-url`, this further decreases the trust
required in a remote.

+* `bup on` is substantially stricter. Now it only allows a subset of
+ bup's subcommands, but all of those that were documented in
+ `bup-on`(1) still work. Previously all comands we allowed, even
+ those that wouldn't work correctly. The remaining commands, except
+ `get`, require less trust in the remote because the local server now
+ rejects write operations for commands that shouldn't be writing,
+ (e.g. `bup on HOST restore`), and it also disallows updates to any
+ ref other than the `--name` for save and split. Please let us know
+ if these changes cause difficulties.
+
* The default pack compression level can now be configured via either
`pack.compression` or `core.compression`. See `bup-config`(5) for
additional information.
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 6:31:21 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Proposed for main.

The main intent is is to require less trust in the remote when running
"bup on HOST ...". For example, "bup on HOST restore ..." should not
be allowed to write to the local repository. This also includes
restrictions on which commands bup-on allows. Previously it allowed
anything, which was just wrong. See the commit mesages and note.md
changes for additional information.

This is also related to Johannes' proposed "server --mode" changes,
which we're likely to want in some form, though perhaps after 0.34,
since the bar is a bit higher there, given the public API.

--
Rob Browning
rlb @defaultvalue.org and @debian.org
GPG as of 2011-07-10 E6A9 DA3C C9FD 1FF8 C676 D2C4 C0F0 39E9 ED1B 597A
GPG as of 2002-11-03 14DD 432F AE39 534D B592 F9A0 25C8 D377 8C7E 73A4

Rob Browning

unread,
Mar 14, 2026, 6:31:22 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Only allow features, get, help, index, restore, save, split, and
version. Previously we allowed anything, including for example, on
itself, gc, validate-*, and some of the now excluded commands either
won't work, or aren't likely to behave as imagined when run via on.

Thanks to Johannes Berg for help, and for proposing related changes.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
Documentation/bup-on.1.md | 12 +++++++-----
lib/bup/cmd/on.py | 7 +++++++
2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/Documentation/bup-on.1.md b/Documentation/bup-on.1.md
index 5db69b99..8d84d22c 100644
--- a/Documentation/bup-on.1.md
+++ b/Documentation/bup-on.1.md
@@ -8,11 +8,13 @@ bup-on - run a bup server locally and client remotely

# SYNOPSIS

-bup on [*user*@]*host*[:*port*] index ...
-
-bup on [*user*@]*host*[:*port*] save ...
-
-bup on [*user*@]*host*[:*port*] split ...
+bup on [*user*@]*host*[:*port*] index ...
+bup on [*user*@]*host*[:*port*] save ...
+bup on [*user*@]*host*[:*port*] restore ...
+bup on [*user*@]*host*[:*port*] split ...
+bup on [*user*@]*host*[:*port*] version ...
+bup on [*user*@]*host*[:*port*] features ...
+bup on [*user*@]*host*[:*port*] help ...

bup on [*user*@]*host*[:*port*] get ...
(Prefer `bup get --source-url ssh://<hostname>...`)
diff --git a/lib/bup/cmd/on.py b/lib/bup/cmd/on.py
index 98639518..5ffafcc4 100644
--- a/lib/bup/cmd/on.py
+++ b/lib/bup/cmd/on.py
@@ -8,6 +8,7 @@ import struct, sys
from bup import git, options, ssh, protocol
from bup.compat import argv_bytes
from bup.helpers import Conn, stopped
+from bup.io import path_msg as pm
from bup.repo import LocalRepo
import bup.path

@@ -58,6 +59,12 @@ def main(argv):
dest, colon, port = dest.rpartition(b':')
if not colon:
dest, port = port, None
+ cmd = argv[0]
+ if cmd == b'init':
+ o.fatal('init is not supported; ssh or run "bup init -r ..." instead')
+ if cmd not in (b'features', b'get', b'help', b'index', b'restore', b'save',
+ b'split', b'version'):
+ o.fatal(f'{pm(cmd)} is not currently supported')
sys.stdout.flush()
sys.stderr.flush()
with ssh.connect(dest, port, b'on--server', stderr=PIPE) as on_srv:
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 6:31:22 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Instead of yielding a differing number of items, where the first is
summary information (oidx, type, size), followed by "all the data" if
and only if include_data is true, just return (oidx, type, size,
data_iterator) where the data_iterator is None when include_data is
false. The caller is still required to consume the iterator, before
issuing any other requests so that the remote call can be finished
properly.

For CatPipe and a local repo the "remote" call is to the git cat-file
subprocess, and for a RemoteRepo, it's to the bup server.

This simplifies the calls a bit, as can be seen in the changes.

Thanks to Johannes Berg for help figuring out what we wanted.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
lib/bup/cmd/get.py | 13 ++++----
lib/bup/cmd/split.py | 3 +-
lib/bup/cmd/validate_object_links.py | 3 +-
lib/bup/gc.py | 6 ++--
lib/bup/git.py | 47 +++++++++++++---------------
lib/bup/protocol.py | 7 ++---
lib/bup/repo/base.py | 8 +++--
lib/bup/repo/local.py | 7 +----
lib/bup/repo/remote.py | 41 ++++++++++++------------
lib/bup/rewrite.py | 4 +--
lib/bup/rm.py | 2 +-
lib/bup/vfs.py | 6 ++--
test/int/test_commit.py | 4 +--
test/int/test_git.py | 16 +++++-----
14 files changed, 76 insertions(+), 91 deletions(-)

diff --git a/lib/bup/cmd/get.py b/lib/bup/cmd/get.py
index cf648121..63ea45f1 100644
--- a/lib/bup/cmd/get.py
+++ b/lib/bup/cmd/get.py
@@ -12,7 +12,7 @@ import os, re, sys, textwrap, time
from bup import client, compat, git, hashsplit, vfs
from bup.commit import commit_message
from bup.compat import dataclass, dataclass_frozen_for_testing, get_argvb
-from bup.git import MissingObject, get_cat_data, parse_commit, walk_object
+from bup.git import MissingObject, get_commit_items, walk_object
from bup.helpers import \
(EXIT_FAILURE,
EXIT_RECOVERED,
@@ -315,7 +315,7 @@ def get_random_item(hash, src_repo, dest_repo, ignore_missing):
return dest_repo.exists(unhexlify(oid))
def get_ref(oidx, include_data=False):
assert include_data
- yield from src_repo.cat(oidx)
+ return src_repo.cat(oidx)
for item in walk_object(get_ref, hash, stop_at=already_seen,
include_data=True, result='item'):
assert isinstance(item, git.WalkItem)
@@ -357,7 +357,7 @@ class GetResult:

def transfer_commit(hash, parent, src_repo, dest_repo, ignore_missing):
now = time.time()
- items = parse_commit(get_cat_data(src_repo.cat(hash), b'commit'))
+ items = get_commit_items(hash, src_repo.cat)
tree = unhexlify(items.tree)
author = b'%s <%s>' % (items.author_name, items.author_mail)
committer = b'%s <%s@%s>' % (userfullname(), username(), hostname())
@@ -437,8 +437,7 @@ def append_commits(src_loc, dest_hash, src_repo, dest_repo, rewriter, excludes,
GitLoc = namedtuple('GitLoc', ('ref', 'hash', 'type'))

def find_git_item(ref, repo):
- it = repo.cat(ref)
- oidx, typ, _ = next(it)
+ oidx, typ, _, it = repo.cat(ref)
# FIXME: don't include_data once repo supports it
for _ in it: pass
if not oidx:
@@ -609,7 +608,7 @@ def handle_ff(item, src_repo, dest_repo):
if not dest_oidx or dest_oidx in src_repo.rev_list(src_oidx):
# Can fast forward.
get_random_item(src_oidx, src_repo, dest_repo, item.spec.ignore_missing)
- commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
+ commit_items = get_commit_items(src_oidx, src_repo.cat)
return GetResult(item.src.hash, unhexlify(commit_items.tree))
misuse('destination is not an ancestor of source for %s'
% spec_msg(item.spec))
@@ -772,7 +771,7 @@ def handle_replace(item, src_repo, dest_repo):
assert(item.dest.type == 'branch' or not item.dest.type)
src_oidx = hexlify(item.src.hash)
get_random_item(src_oidx, src_repo, dest_repo, item.spec.ignore_missing)
- commit_items = parse_commit(get_cat_data(src_repo.cat(src_oidx), b'commit'))
+ commit_items = get_commit_items(src_oidx, src_repo.cat)
return GetResult(item.src.hash, unhexlify(commit_items.tree))


diff --git a/lib/bup/cmd/split.py b/lib/bup/cmd/split.py
index 55fc6619..fd0028ae 100644
--- a/lib/bup/cmd/split.py
+++ b/lib/bup/cmd/split.py
@@ -213,8 +213,7 @@ def main(argv):
if line:
line = line.strip()
try:
- it = cp.get(line.strip())
- next(it, None) # skip the file info
+ it = cp.get(line.strip())[3] # skip the file info
except KeyError as e:
add_error('error: %s' % e)
continue
diff --git a/lib/bup/cmd/validate_object_links.py b/lib/bup/cmd/validate_object_links.py
index 75ad5213..5855dbc5 100644
--- a/lib/bup/cmd/validate_object_links.py
+++ b/lib/bup/cmd/validate_object_links.py
@@ -63,8 +63,7 @@ class Pack:
data = zlib.decompress(data)
yield oid, git._typermap[kind], data
elif kind in (5, 6, 7): # reserved obj_ofs_delta obj_ref_delta
- it = self._cp.get(hexlify(oid))
- _, tp, _ = next(it, None) # cannot return None
+ _, tp, _, it = self._cp.get(hexlify(oid))
data = b''.join(it)
if tp == b'blob':
continue
diff --git a/lib/bup/gc.py b/lib/bup/gc.py
index 8ffe73b3..531e3f0d 100644
--- a/lib/bup/gc.py
+++ b/lib/bup/gc.py
@@ -182,8 +182,7 @@ def sweep(live_objects, live_trees, existing_count, cat_pipe, threshold,
must_rewrite = False
live_in_this_pack = set()
for sha in idx:
- tmp_it = cat_pipe.get(hexlify(sha), include_data=False)
- _, typ, _ = next(tmp_it)
+ typ = cat_pipe.get(hexlify(sha), include_data=False)[1]
if typ != b'blob':
is_live = sha in live_trees
if not is_live:
@@ -218,8 +217,7 @@ def sweep(live_objects, live_trees, existing_count, cat_pipe, threshold,
reprogress()
for sha in idx:
if sha in live_in_this_pack:
- item_it = cat_pipe.get(hexlify(sha))
- _, typ, _ = next(item_it)
+ _, typ, _, item_it = cat_pipe.get(hexlify(sha))
repo.just_write(sha, typ, b''.join(item_it))
assert idx_name.endswith(b'.idx')
stale_packs.append(idx_name[:-4])
diff --git a/lib/bup/git.py b/lib/bup/git.py
index 10ba638e..d6dd8df1 100644
--- a/lib/bup/git.py
+++ b/lib/bup/git.py
@@ -95,15 +95,11 @@ def git_config_get(path, option, *, opttype=None):
raise GitError('%r returned %d' % (cmd, rc))


-def get_cat_data(cat_iterator, expected_type):
- _, kind, _ = next(cat_iterator)
- if kind != expected_type:
- raise Exception('expected %r, saw %r' % (expected_type, kind))
- return b''.join(cat_iterator)
-
-
-def get_commit_items(id, cp):
- return parse_commit(get_cat_data(cp.get(id), b'commit'))
+def get_commit_items(ref, get_ref):
+ _, kind, _, it = get_ref(ref)
+ if kind != b'commit':
+ raise Exception(f'{path_msg(ref)} is {kind}, not commit')
+ return parse_commit(b''.join(it))


def repo(sub = b'', repo_dir=None):
@@ -1393,8 +1389,9 @@ class CatPipe:
env=_gitenv(self.repo_dir))

def get(self, ref, include_data=True):
- """Yield (oidx, type, size), followed by the data referred to by ref.
- If ref does not exist, only yield (None, None, None).
+ """Return (oidx, type, size, data_iterator). When
+ include_data is true, the data_iterator will be None. When
+ the ref is missing, all elements will be None.

"""
if not self.p or self.p.poll() is not None:
@@ -1425,8 +1422,7 @@ class CatPipe:
% (ref, p.poll() or 'none'))
if hdr.endswith(b' missing\n'):
self.inprogress = None
- yield None, None, None
- return
+ return None, None, None, None
info = hdr.split(b' ')
if len(info) != 3 or len(info[0]) != 40:
raise GitError('expected object (id, type, size), got %r' % info)
@@ -1435,18 +1431,18 @@ class CatPipe:

if not include_data:
self.inprogress = None
- yield oidx, typ, size
- return
+ return oidx, typ, size, None

- try:
- yield oidx, typ, size
- yield from chunkyreader(p.stdout, size)
- readline_result = p.stdout.readline()
- assert readline_result == b'\n'
- self.inprogress = None
- except Exception as ex:
- self.close()
- raise ex
+ def data_iterator():
+ try:
+ yield from chunkyreader(p.stdout, size)
+ readline_result = p.stdout.readline()
+ assert readline_result == b'\n'
+ self.inprogress = None
+ except Exception as ex:
+ self.close()
+ raise ex
+ return oidx, typ, size, data_iterator()


_catpipe_for = {}
@@ -1542,8 +1538,7 @@ def walk_object(get_ref, oidx, *, stop_at=None, include_data=None,

# must have data for commits, trees, or unknown
got_data = (exp_typ in (b'commit', b'tree', None)) or include_data
- item_it = get_ref(oidx, include_data=got_data)
- get_oidx, typ, _ = next(item_it, None) # cannot return None
+ get_oidx, typ, _, item_it = get_ref(oidx, include_data=got_data)
if not get_oidx:
item = WalkItem(oid=unhexlify(oidx), type=exp_typ, name=name,
mode=mode, data=False)
diff --git a/lib/bup/protocol.py b/lib/bup/protocol.py
index 855ae400..c091386f 100644
--- a/lib/bup/protocol.py
+++ b/lib/bup/protocol.py
@@ -338,12 +338,11 @@ class Server:
# For now, avoid potential deadlock by just reading them all
for ref in tuple(lines_until_sentinel(self.conn, b'\n', Exception)):
ref = ref[:-1]
- it = self.repo.cat(ref)
- info = next(it)
- if not info[0]:
+ oidx, kind, size, it = self.repo.cat(ref)
+ if not oidx:
self.conn.write(b'missing\n')
continue
- self.conn.write(b'%s %s %d\n' % info)
+ self.conn.write(b'%s %s %d\n' % (oidx, kind, size))
for buf in it:
self.conn.write(buf)
self.conn.ok()
diff --git a/lib/bup/repo/base.py b/lib/bup/repo/base.py
index bfe49972..f45e5e53 100644
--- a/lib/bup/repo/base.py
+++ b/lib/bup/repo/base.py
@@ -69,9 +69,11 @@ class RepoProtocol:

@notimplemented
def cat(self, ref):
- """
- If ref does not exist, yield (None, None, None). Otherwise yield
- (oidx, type, size), and then all of the data associated with ref.
+ """Return (oidx, type, size, data_iterator). When the ref is
+ missing, all elements will be None. For some repositories
+ (like RemoteRepo), the iterator must be consumed before
+ calling other repository methods.
+
"""

@notimplemented
diff --git a/lib/bup/repo/local.py b/lib/bup/repo/local.py
index 6fe2c2f5..2fffc866 100644
--- a/lib/bup/repo/local.py
+++ b/lib/bup/repo/local.py
@@ -110,12 +110,7 @@ class LocalRepo(RepoProtocol):
return git.update_ref(refname, newval, oldval, repo_dir=self.repo_dir)

def cat(self, ref):
- it = self._cp.get(ref)
- info = next(it, None) # cannot return None
- oidx = info[0]
- yield info
- if oidx: yield from it
- assert not next(it, None)
+ return self._cp.get(ref)

def join(self, ref):
return vfs.join(self, ref)
diff --git a/lib/bup/repo/remote.py b/lib/bup/repo/remote.py
index 9f6e9de7..a399a3a1 100644
--- a/lib/bup/repo/remote.py
+++ b/lib/bup/repo/remote.py
@@ -62,27 +62,28 @@ class RemoteRepo(RepoProtocol):
def is_remote(self): return True

def cat(self, ref):
- # Yield all the data here so that we don't finish the
- # cat_batch iterator (triggering its cleanup) until all of the
- # data has been read. Otherwise we'd be out of sync with the
- # server. If the ref is 40 hex digits, then assume it's an
- # oid, and verify that the data provided by the remote
+ # The data iterator must be consumed before any other client
+ # interactions. If the ref is 40 hex digits, then assume it's
+ # an oid, and verify that the data provided by the remote
# actually has that oid. If not, throw.
- items = self.client.cat_batch((ref,))
- oidx, typ, size, it = info = next(items, None) # cannot return None
- yield info[:-1]
- if oidx:
- if not _oidx_rx.fullmatch(ref):
- yield from it
- else:
- actual_oid = git.start_sha1(typ, size)
- for data in it:
- actual_oid.update(data)
- yield data
- actual_oid = actual_oid.digest()
- if hexlify(actual_oid) != ref:
- raise Exception(f'received {actual_oid.hex()}, expected oid {ref}')
- assert not next(items, None)
+ def hash_checked_data(kind, size, it, expected, batch):
+ actual_oid = git.start_sha1(kind, size)
+ for data in it:
+ actual_oid.update(data)
+ yield data
+ actual_oid = actual_oid.digest()
+ if hexlify(actual_oid) != expected:
+ raise Exception(f'received {actual_oid.hex()}, expected oid {expected}')
+ # causes client to finish the call
+ assert not next(batch, None)
+
+ batch = self.client.cat_batch((ref,))
+ oidx, typ, size, it = items = next(batch, None) # cannot return None
+ if not oidx:
+ return items
+ if not _oidx_rx.fullmatch(ref):
+ return items
+ return *items[:-1], hash_checked_data(typ, size, it, ref, batch)

def write_commit(self, tree, parent,
author, adate_sec, adate_tz,
diff --git a/lib/bup/rewrite.py b/lib/bup/rewrite.py
index eec972f1..f7d83648 100755
--- a/lib/bup/rewrite.py
+++ b/lib/bup/rewrite.py
@@ -11,7 +11,7 @@ import sqlite3, sys, time
from bup import hashsplit, metadata, vfs
from bup.commit import commit_message
from bup.compat import dataclass
-from bup.git import get_cat_data, parse_commit
+from bup.git import get_commit_items
from bup.hashsplit import \
(GIT_MODE_EXEC,
GIT_MODE_FILE,
@@ -604,7 +604,7 @@ class Rewriter:
tree = stack.pop() # and the root to get the tree

save_oidx = hexlify(save_path[2][1].coid)
- ci = parse_commit(get_cat_data(srcrepo.cat(save_oidx), b'commit'))
+ ci = get_commit_items(save_oidx, srcrepo.cat)
author = ci.author_name + b' <' + ci.author_mail + b'>'
committer = b'%s <%s@%s>' % (userfullname(), username(), hostname())
trailers = repairs.repair_trailers(repairs.id)
diff --git a/lib/bup/rm.py b/lib/bup/rm.py
index 66cb371f..3d26efc8 100644
--- a/lib/bup/rm.py
+++ b/lib/bup/rm.py
@@ -8,7 +8,7 @@ from bup.helpers import add_error, die_if_errors, log, saved_errors
from bup.io import path_msg

def append_commit(hash, parent, cp, writer):
- ci = get_commit_items(hash, cp)
+ ci = get_commit_items(hash, cp.get)
tree = unhexlify(ci.tree)
author = b'%s <%s>' % (ci.author_name, ci.author_mail)
committer = b'%s <%s>' % (ci.committer_name, ci.committer_mail)
diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py
index 799e217a..d12f1219 100644
--- a/lib/bup/vfs.py
+++ b/lib/bup/vfs.py
@@ -144,8 +144,7 @@ def get_ref(repo, ref):
If ref is missing, yield (None, None, None, None).

"""
- it = repo.cat(ref)
- found_oidx, obj_t, size = next(it)
+ found_oidx, obj_t, size, it = repo.cat(ref)
if not found_oidx:
return None, None, None, None
return found_oidx, obj_t, size, it
@@ -589,8 +588,7 @@ def root_items(repo, names=None, want_meta=True):
for ref in names:
if ref in (b'.', b'.tag'):
continue
- it = repo.cat(b'refs/heads/' + ref)
- oidx, typ, size_ = next(it, None) # cannot return None
+ oidx, typ, size_, it = repo.cat(b'refs/heads/' + ref)
if not oidx:
continue
assert typ == b'commit'
diff --git a/test/int/test_commit.py b/test/int/test_commit.py
index 6a4ec41f..49c6c8e7 100644
--- a/test/int/test_commit.py
+++ b/test/int/test_commit.py
@@ -58,7 +58,7 @@ def test_commit_parsing(tmpdir):
coff = (int(coffs[-4:-2]) * 60 * 60) + (int(coffs[-2:]) * 60)
if coffs[-5] == b'-'[0]:
coff = - coff
- commit_items = git.get_commit_items(commit, git.catpipe())
+ commit_items = git.get_commit_items(commit, git.catpipe().get)
assert parents == b''
WVPASSEQ(commit_items.parents, [])
WVPASSEQ(commit_items.tree, tree)
@@ -77,7 +77,7 @@ def test_commit_parsing(tmpdir):
exc(b'git', b'commit', b'-am', b'Do something else')
child = exo(b'git', b'show-ref', b'-s', b'main').strip()
parents = showval(child, b'%P')
- commit_items = git.get_commit_items(child, git.catpipe())
+ commit_items = git.get_commit_items(child, git.catpipe().get)
WVPASSEQ(commit_items.parents, [commit])
finally:
os.chdir(orig_cwd)
diff --git a/test/int/test_git.py b/test/int/test_git.py
index 453c0090..a5229596 100644
--- a/test/int/test_git.py
+++ b/test/int/test_git.py
@@ -302,7 +302,7 @@ def test_new_commit(tmpdir):
cdate_sec, cdate_tz_sec,
b'There is a small mailbox here')

- commit_items = git.get_commit_items(hexlify(commit), git.catpipe())
+ commit_items = git.get_commit_items(hexlify(commit), git.catpipe().get)
local_author_offset = localtime(adate_sec).tm_gmtoff
local_committer_offset = localtime(cdate_sec).tm_gmtoff
WVPASSEQ(tree, unhexlify(commit_items.tree))
@@ -317,7 +317,7 @@ def test_new_commit(tmpdir):
WVPASSEQ(cdate_sec, commit_items.committer_sec)
WVPASSEQ(local_committer_offset, commit_items.committer_offset)

- commit_items = git.get_commit_items(hexlify(commit_off), git.catpipe())
+ commit_items = git.get_commit_items(hexlify(commit_off), git.catpipe().get)
WVPASSEQ(tree, unhexlify(commit_items.tree))
WVPASSEQ(1, len(commit_items.parents))
WVPASSEQ(parent, unhexlify(commit_items.parents[0]))
@@ -397,16 +397,16 @@ def test_cat_pipe(tmpdir):
size = int(exo(b'git', b'--git-dir', bupdir,
b'cat-file', b'-s', b'src'))

- it = git.catpipe().get(b'src')
- assert (oidx, typ, size) == next(it)
- data = b''.join(it)
+ info = git.catpipe().get(b'src')
+ assert (oidx, typ, size) == info[:-1]
+ data = b''.join(info[3])
assert data.startswith(b'tree ')
assert b'\nauthor ' in data
assert b'\ncommitter ' in data

- it = git.catpipe().get(b'src', include_data=False)
- assert (oidx, typ, size) == next(it)
- assert b'' == b''.join(it)
+ info = git.catpipe().get(b'src', include_data=False)
+ assert (oidx, typ, size) == info[:-1]
+ assert info[3] is None


def _create_idx(d, i):
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 6:31:22 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
Only run the local server for the commands that need one, i.e. get,
restore, save, and split, and not for features, help, index, and
version.

Thanks to Johannes Berg for proposing the change.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
lib/bup/cmd/on.py | 33 ++++++++++++++++++++-------------
1 file changed, 20 insertions(+), 13 deletions(-)

diff --git a/lib/bup/cmd/on.py b/lib/bup/cmd/on.py
index 5ffafcc4..abdca567 100644
--- a/lib/bup/cmd/on.py
+++ b/lib/bup/cmd/on.py
@@ -50,6 +50,16 @@ bup on <[user@]host[:port]> <index|save|split|get> ...
# +------------------+
#

+def run_server(on_srv):
+ class ServerRepo(LocalRepo):
+ def __init__(self, repo_dir, server):
+ self.closed = True # subclass' __del__ can run before its init
+ git.check_repo_or_die(repo_dir)
+ LocalRepo.__init__(self, repo_dir, server=server)
+ with Conn(on_srv.stdout, on_srv.stdin) as conn, \
+ protocol.Server(conn, ServerRepo) as server:
+ server.handle()
+
def main(argv):
o = options.Options(optspec, optfunc=getopt.getopt)
extra = o.parse_bytes(argv[1:])[2]
@@ -62,26 +72,23 @@ def main(argv):
cmd = argv[0]
if cmd == b'init':
o.fatal('init is not supported; ssh or run "bup init -r ..." instead')
- if cmd not in (b'features', b'get', b'help', b'index', b'restore', b'save',
- b'split', b'version'):
+ if cmd in (b'features', b'help', b'index', b'version'):
+ want_server = False
+ elif cmd in (b'get', b'restore', b'save', b'split'):
+ want_server = True
+ else:
o.fatal(f'{pm(cmd)} is not currently supported')
+ assert False # (so pylint won't think srv_config might be unset)
sys.stdout.flush()
sys.stderr.flush()
with ssh.connect(dest, port, b'on--server', stderr=PIPE) as on_srv:
argvs = b'\0'.join([b'bup'] + argv)
on_srv.stdin.write(struct.pack('!I', len(argvs)) + argvs)
on_srv.stdin.flush()
-
- class ServerRepo(LocalRepo):
- def __init__(self, repo_dir, server):
- self.closed = True # subclass' __del__ can run before its init
- git.check_repo_or_die(repo_dir)
- LocalRepo.__init__(self, repo_dir, server=server)
-
# write on--server's stdout/stderr to our stdout/stderr via demux
- with stopped(Popen((bup.path.exe(), b'demux'), stdin=on_srv.stderr), 1) as demux, \
- Conn(on_srv.stdout, on_srv.stdin) as conn, \
- protocol.Server(conn, ServerRepo) as server:
- server.handle()
+ with stopped(Popen((bup.path.exe(), b'demux'), stdin=on_srv.stderr),
+ timeout=1) as demux:
+ if want_server:
+ run_server(on_srv)
demux.wait() # finish the output
return on_srv.returncode
--
2.47.3

Rob Browning

unread,
Mar 14, 2026, 6:31:22 PM (10 days ago) Mar 14
to bup-...@googlegroups.com
This allows us to create the Server directly, making it easier to
control/customize it without being required to introduce some way to
communicate the desired arrangement with a subprocess.

Add "bup demux" to allow the switch, and denote it "internal" to
indicate that we're not promising to provide it, or to provide a
stable interface, i.e. we don't consider it part of the public API.

Use stopped() to simplify the subprocess handling, and context manage
"everything".

Drop the bespoke signal handling under the assumption that it's no
longer needed.

For the time being, just copy the relevant ServerRepo, etc. from
bup.cmd.server, since it's minimal, and bup-on and bup-server may
diverge with respect to customizations, assuming we add support
bup-server restrictions.

Thanks to Johannes Berg for help improving the diagram.

Signed-off-by: Rob Browning <r...@defaultvalue.org>
Tested-by: Rob Browning <r...@defaultvalue.org>
---
Documentation/bup-demux.1.md | 29 +++++++++
lib/bup/cmd/demux.py | 28 +++++++++
lib/bup/cmd/on.py | 114 ++++++++++++++++++-----------------
3 files changed, 115 insertions(+), 56 deletions(-)
create mode 100644 Documentation/bup-demux.1.md
create mode 100644 lib/bup/cmd/demux.py

diff --git a/Documentation/bup-demux.1.md b/Documentation/bup-demux.1.md
new file mode 100644
index 00000000..f09ca13b
--- /dev/null
+++ b/Documentation/bup-demux.1.md
@@ -0,0 +1,29 @@
+% bup-demux(1) Bup %BUP_VERSION%
+% Rob Browning <r...@defaultvalue.org>
+% %BUP_DATE%
+
+# NAME
+
+bup-demux - demultiplexes data and error streams from standard input
+
+# SYNOPSIS
+
+bup demux
+
+# DESCRIPTION
+
+Note: this is an internal command, and may be removed or changed at
+any time.
+
+`bup demux` reads standard input as a bup "multiplexed" stream of
+data from standard output and standard error, and reproduces that
+output on its own standard output and standard error. It's primary
+purpose is to support `bup-on`(1).
+
+# SEE ALSO
+
+`bup-on`(1), `bup-mux`(1)
+
+# BUP
+
+Part of the `bup`(1) suite.
diff --git a/lib/bup/cmd/demux.py b/lib/bup/cmd/demux.py
new file mode 100644
index 00000000..69787803
--- /dev/null
+++ b/lib/bup/cmd/demux.py
@@ -0,0 +1,28 @@
+
+import os, sys
+
+from bup import options
+from bup.helpers import DemuxConn
+from bup.io import byte_stream, path_msg as pm
+
+
+optspec = """
+bup demux # internal command (may be removed or changed at any time)
+--
+"""
+
+def main(argv):
+ o = options.Options(optspec)
+ extra = o.parse_bytes(argv[1:])[2]
+ if extra:
+ o.fatal(f'unexpected arguments: {" ".join(pm(x) for x in extra)}')
+ sys.stdout.flush()
+ sys.stderr.flush()
+ out = byte_stream(sys.stdout)
+ try:
+ with DemuxConn(sys.stdin.fileno(), open(os.devnull, "wb")) as dmc:
+ for line in iter(dmc.readline, b''):
+ out.write(line)
+ finally: # just in case
+ out.flush()
+ sys.stderr.flush()
diff --git a/lib/bup/cmd/on.py b/lib/bup/cmd/on.py
index bddeee26..98639518 100644
--- a/lib/bup/cmd/on.py
+++ b/lib/bup/cmd/on.py
@@ -1,78 +1,80 @@

-from subprocess import PIPE
+from subprocess import PIPE, Popen
# Python upstream deprecated this and then undeprecated it...
# pylint: disable-next=deprecated-module
import getopt
-import os, signal, struct, subprocess, sys
+import struct, sys

-from bup import options, ssh, path
+from bup import git, options, ssh, protocol
from bup.compat import argv_bytes
-from bup.helpers import DemuxConn, log
-from bup.io import byte_stream
-
+from bup.helpers import Conn, stopped
+from bup.repo import LocalRepo
+import bup.path

optspec = """
bup on <[user@]host[:port]> <index|save|split|get> ...
"""

+# Run the given given command on the host via ssh while reproducing
+# its stdout, stderr, and exit status locally by multiplexing its
+# stdout and stderr over the ssh connections stderr, and
+# demultiplexing that back to the local stdout/stderr via bup-demux
+# like so ("on" is this process, i.e. running the bup server via
+# main() below):
+#
+# +----------+
+# stdin ----> | on | --- stdout -----------------+-----> local stdout
+# | (server) | --- stderr -----------------|-+---> local stderr
+# +----------+ | |
+# | ^ | |
+# | | | |
+# | | | |
+# | | | |
+# | | | |
+# | | | |
+# +-- ssh stdin --+ | | |
+# | (server input) | | |
+# | | | |
+# | +-- ssh stdout --+ | |
+# | | (server output) | |
+# | | | |
+# | | +-------+ --- stdout ---+ |
+# | | +--- ssh stderr --->| demux | --- stderr -----+
+# | | | (mux out/err) +-------+
+# | | |
+# v | |
+# +------------------+
+# | on--server |
+# | (index/save/...) |
+# +------------------+
+#
+
def main(argv):
o = options.Options(optspec, optfunc=getopt.getopt)
extra = o.parse_bytes(argv[1:])[2]
if len(extra) < 2:
o.fatal('must specify index, save, split, or get command')
-
- class SigException(Exception):
- def __init__(self, signum):
- self.signum = signum
- Exception.__init__(self, 'signal %d received' % signum)
- def handler(signum, frame):
- raise SigException(signum)
-
- dest, *argv = (argv_bytes(x) for x in extra)
+ dest, *argv = [argv_bytes(x) for x in extra]
dest, colon, port = dest.rpartition(b':')
if not colon:
dest, port = port, None
-
- signal.signal(signal.SIGTERM, handler)
- signal.signal(signal.SIGINT, handler)
-
sys.stdout.flush()
- out = byte_stream(sys.stdout)
+ sys.stderr.flush()
+ with ssh.connect(dest, port, b'on--server', stderr=PIPE) as on_srv:
+ argvs = b'\0'.join([b'bup'] + argv)
+ on_srv.stdin.write(struct.pack('!I', len(argvs)) + argvs)
+ on_srv.stdin.flush()

- try:
- sp = None
- p = None
- ret = 99
- p = ssh.connect(dest, port, b'on--server', stderr=PIPE)
- try:
- argvs = b'\0'.join([b'bup'] + argv)
- p.stdin.write(struct.pack('!I', len(argvs)) + argvs)
- p.stdin.flush()
- # pylint: disable-next=consider-using-with
- sp = subprocess.Popen([path.exe(), b'server'],
- stdin=p.stdout, stdout=p.stdin)
- p.stdin.close()
- p.stdout.close()
- # Demultiplex remote client's stderr (back to stdout/stderr).
- with DemuxConn(p.stderr.fileno(), open(os.devnull, "wb")) as dmc:
- for line in iter(dmc.readline, b''):
- out.write(line)
- finally:
- while 1:
- # if we get a signal while waiting, we have to keep waiting, just
- # in case our child doesn't die.
- try:
- ret = p.wait()
- if sp:
- sp.wait()
- break
- except SigException as e:
- log('\nbup on: %s\n' % e)
- os.kill(p.pid, e.signum)
- ret = 84
- except SigException as e:
- if ret == 0:
- ret = 99
- log('\nbup on: %s\n' % e)
+ class ServerRepo(LocalRepo):
+ def __init__(self, repo_dir, server):
+ self.closed = True # subclass' __del__ can run before its init
+ git.check_repo_or_die(repo_dir)
+ LocalRepo.__init__(self, repo_dir, server=server)

- sys.exit(ret)
+ # write on--server's stdout/stderr to our stdout/stderr via demux
+ with stopped(Popen((bup.path.exe(), b'demux'), stdin=on_srv.stderr), 1) as demux, \
+ Conn(on_srv.stdout, on_srv.stdin) as conn, \
+ protocol.Server(conn, ServerRepo) as server:
+ server.handle()
+ demux.wait() # finish the output
+ return on_srv.returncode
--
2.47.3

Reply all
Reply to author
Forward
0 new messages