ssh://: explain a remote borg serve that fails to start

If the first request to the remote "borg serve" fails (e.g. a borg 1.x
borg serve does not know --rest, or borg is not installed there), raise
an Error showing the server's stderr and a hint (BORG_REMOTE_PATH,
--from-borg1) instead of a "Local Exception" traceback.

Repository.close() does not raise from closing the store while unwinding
an error anymore (like the lock release before it): a dead store fails
to close, too, which masked the original error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-09-24 15:54:57 +02:00
co-authored by Claude Opus 5.5
parent 669b43980d
commit 93481e0fb8
2 changed files with 48 additions and 13 deletions
+28 -12
View File
@@ -1007,18 +1007,28 @@ class Repository:
return f"<{self.__class__.__name__} {self._location}>"
def __enter__(self):
if self.do_create:
self.do_create = False
self.create()
self.created = True
try:
self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)
except Exception:
self.close(aborting=True)
if self.created:
# we just created the store, but could not open it: do not leave it behind (see create()).
self.store.destroy()
raise
if self.do_create:
self.do_create = False
self.create()
self.created = True
try:
self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)
except Exception:
self.close(aborting=True)
if self.created:
# we just created the store, but could not open it: do not leave it behind (see create()).
self.store.destroy()
raise
except StoreBackendError as e:
if self._location.proto != "ssh":
raise
# the first request to the remote "borg serve" failed, usually because it did not start.
raise Error(
f"Could not access the repository via borg serve on the remote host: {e}\n"
"Is borg 2 installed there (see BORG_REMOTE_PATH)? "
"For a borg 1.x repository, use --from-borg1."
) from None
return self
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -1387,7 +1397,13 @@ class Repository:
logger.warning("failed to release the lock during close: %s", exc)
self.lock = None
if self.store_opened:
self.store.close()
try:
self.store.close()
except Exception as exc:
if not unwinding:
raise
# when the store is dead (e.g. the remote borg serve exited), closing it fails, too.
logger.debug("failed to close the store during close: %s", exc)
self.store_opened = False
self.opened = False
self._pack_cache.clear()
+20 -1
View File
@@ -8,6 +8,7 @@ from collections import namedtuple
import pytest
from borghash import HashTableNT
from borgstore.backends.rest import REST
from ..crypto.key import store_hash
from .. import repository as repository_module
@@ -15,7 +16,7 @@ from ..cache import chunkindex_is_invalid, delete_chunkindex_from_repo, write_ch
from ..compress import CNONE
from ..constants import MAX_CLOCK_SKEW, ROBJ_FILE_STREAM
from ..crypto.key import AESOCBKey, AuthenticatedKey, Blake3AuthenticatedKey, CHPOKey
from ..helpers import IntegrityError, Location, bin_to_hex
from ..helpers import Error, IntegrityError, Location, bin_to_hex
from ..hashindex import ChunkIndex, ChunkIndexEntry
from ..repository import Repository, MAX_DATA_SIZE, MAX_VALIDATED_META_SIZE, propagate_rsh, rest_serve_command
from ..repository import PackWriter, PackReader, PackTracker, superseded_gap_ranges
@@ -97,6 +98,24 @@ def test_open_nonexistent_repository(tmp_path, proto):
assert not os.path.exists(path)
@pytest.mark.parametrize("create", [False, True])
def test_remote_serve_fails_to_start(tmp_path, monkeypatch, create):
# e.g. a borg 1.x "borg serve" on the remote host does not know --rest: the user gets an error
# showing the server's stderr and a hint, not a traceback of the dead store's close().
def failing_rest_backend(location):
error = "borg serve: error: unrecognized arguments: --rest"
command = [sys.executable, "-c", f"import sys; sys.stderr.write({error!r} + '\\n'); sys.exit(2)"]
# no waiting between the reconnect attempts, to keep the test fast.
return REST(base_url="http://stdio-backend", command=command, reconnect_wait=0)
monkeypatch.setattr(repository_module, "build_rest_backend", failing_rest_backend)
location = Location("ssh://__testsuite__/" + os.fspath(tmp_path / "repository"))
with pytest.raises(Error, match="use --from-borg1") as exc_info:
with Repository(location, exclusive=True, create=create):
pass
assert "unrecognized arguments: --rest" in str(exc_info.value)
@pytest.fixture()
def repository(tmp_path):
repository_location = os.fspath(tmp_path / "repository")