Skip to content

Commit ba55465

Browse files
Merge pull request #1: implement unix domain socket support
Merged from original PR #7615 Original: borgbackup/borg#7615
2 parents 5cdb847 + ebf9e69 commit ba55465

File tree

10 files changed

+363
-122
lines changed

10 files changed

+363
-122
lines changed

src/borg/archiver/_common.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030

3131
def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args):
32-
if location.proto == "ssh":
32+
if location.proto in ("ssh", "socket"):
3333
repository = RemoteRepository(
3434
location,
3535
create=create,
@@ -573,6 +573,16 @@ def define_common_options(add_common_option):
573573
action=Highlander,
574574
help="Use this command to connect to the 'borg serve' process (default: 'ssh')",
575575
)
576+
add_common_option(
577+
"--socket",
578+
metavar="PATH",
579+
dest="use_socket",
580+
default=False,
581+
const=True,
582+
nargs="?",
583+
action=Highlander,
584+
help="Use UNIX DOMAIN (IPC) socket at PATH for client/server communication with socket: protocol.",
585+
)
576586
add_common_option(
577587
"-r",
578588
"--repo",

src/borg/archiver/serve_cmd.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def do_serve(self, args):
1919
restrict_to_repositories=args.restrict_to_repositories,
2020
append_only=args.append_only,
2121
storage_quota=args.storage_quota,
22+
use_socket=args.use_socket,
2223
).serve()
2324
return EXIT_SUCCESS
2425

@@ -27,7 +28,17 @@ def build_parser_serve(self, subparsers, common_parser, mid_common_parser):
2728

2829
serve_epilog = process_epilog(
2930
"""
30-
This command starts a repository server process. This command is usually not used manually.
31+
This command starts a repository server process.
32+
33+
borg serve can currently support:
34+
35+
- Getting automatically started via ssh when the borg client uses a ssh://...
36+
remote repository. In this mode, `borg serve` will live until that ssh connection
37+
gets terminated.
38+
39+
- Getting started by some other means (not by the borg client) as a long-running socket
40+
server to be used for borg clients using a socket://... repository (see the `--socket`
41+
option if you do not want to use the default path for the socket and pid file).
3142
"""
3243
)
3344
subparser = subparsers.add_parser(

src/borg/helpers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from .checks import check_extension_modules, check_python
1212
from .datastruct import StableDict, Buffer, EfficientCollectionQueue
1313
from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError
14-
from .fs import ensure_dir, get_security_dir, get_keys_dir, get_base_dir, join_base_dir, get_cache_dir, get_config_dir
14+
from .fs import ensure_dir, join_base_dir, get_socket_filename
15+
from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
1516
from .fs import dir_is_tagged, dir_is_cachedir, make_path_safe, scandir_inorder
1617
from .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, umount
1718
from .fs import O_, flags_root, flags_dir, flags_special_follow, flags_special, flags_base, flags_normal, flags_noatime

src/borg/helpers/fs.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ def get_data_dir(*, legacy=False):
106106
return data_dir
107107

108108

109+
def get_runtime_dir(*, legacy=False):
110+
"""Determine where to store runtime files, like sockets, PID files, ..."""
111+
assert legacy is False, "there is no legacy variant of the borg runtime dir"
112+
runtime_dir = os.environ.get(
113+
"BORG_RUNTIME_DIR", join_base_dir(".cache", "borg", legacy=legacy) or platformdirs.user_runtime_dir("borg")
114+
)
115+
116+
# Create path if it doesn't exist yet
117+
ensure_dir(runtime_dir)
118+
return runtime_dir
119+
120+
121+
def get_socket_filename():
122+
return os.path.join(get_runtime_dir(), "borg.sock")
123+
124+
109125
def get_cache_dir(*, legacy=False):
110126
"""Determine where to repository keys and cache"""
111127

src/borg/helpers/parseformat.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ class Location:
390390
# path must not contain :: (it ends at :: or string end), but may contain single colons.
391391
# to avoid ambiguities with other regexes, it must also not start with ":" nor with "//" nor with "ssh://".
392392
local_path_re = r"""
393-
(?!(:|//|ssh://)) # not starting with ":" or // or ssh://
393+
(?!(:|//|ssh://|socket://)) # not starting with ":" or // or ssh:// or socket://
394394
(?P<path>([^:]|(:(?!:)))+) # any chars, but no "::"
395395
"""
396396

@@ -429,6 +429,14 @@ class Location:
429429
re.VERBOSE,
430430
) # path
431431

432+
socket_re = re.compile(
433+
r"""
434+
(?P<proto>socket):// # socket://
435+
"""
436+
+ abs_path_re,
437+
re.VERBOSE,
438+
) # path
439+
432440
file_re = re.compile(
433441
r"""
434442
(?P<proto>file):// # file://
@@ -493,6 +501,11 @@ def normpath_special(p):
493501
self.path = normpath_special(m.group("path"))
494502
return True
495503
m = self.file_re.match(text)
504+
if m:
505+
self.proto = m.group("proto")
506+
self.path = normpath_special(m.group("path"))
507+
return True
508+
m = self.socket_re.match(text)
496509
if m:
497510
self.proto = m.group("proto")
498511
self.path = normpath_special(m.group("path"))
@@ -516,7 +529,7 @@ def __str__(self):
516529

517530
def to_key_filename(self):
518531
name = re.sub(r"[^\w]", "_", self.path).strip("_")
519-
if self.proto != "file":
532+
if self.proto not in ("file", "socket"):
520533
name = re.sub(r"[^\w]", "_", self.host) + "__" + name
521534
if len(name) > 100:
522535
# Limit file names to some reasonable length. Most file systems
@@ -535,7 +548,7 @@ def host(self):
535548
return self._host.lstrip("[").rstrip("]")
536549

537550
def canonical_path(self):
538-
if self.proto == "file":
551+
if self.proto in ("file", "socket"):
539552
return self.path
540553
else:
541554
if self.path and self.path.startswith("~"):

src/borg/logger.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@
2828
2929
* what is output on INFO level is additionally controlled by commandline
3030
flags
31+
32+
Logging setup is a bit complicated in borg, as it needs to work under misc. conditions:
33+
- purely local, not client/server (easy)
34+
- client/server: RemoteRepository ("borg serve" process) writes log records into a global
35+
queue, which is then sent to the client side by the main serve loop (via the RPC protocol,
36+
either over ssh stdout, more directly via process stdout without ssh [used in the tests]
37+
or via a socket. On the client side, the log records are fed into the clientside logging
38+
system. When remote_repo.close() is called, server side must send all queued log records
39+
via the RPC channel before returning the close() call's return value (as the client will
40+
then shut down the connection).
41+
- progress output is always given as json to the logger (including the plain text inside
42+
the json), but then formatted by the logging system's formatter as either plain text or
43+
json depending on the cli args given (--log-json?).
44+
- tests: potentially running in parallel via pytest-xdist, capturing borg output into a
45+
given stream.
46+
- logging might be short-lived (e.g. when invoking a single borg command via the cli)
47+
or long-lived (e.g. borg serve --socket or when running the tests)
48+
- logging is global and exists only once per process.
3149
"""
3250

3351
import inspect
@@ -115,10 +133,14 @@ def remove_handlers(logger):
115133
logger.removeHandler(handler)
116134

117135

118-
def teardown_logging():
119-
global configured
120-
logging.shutdown()
121-
configured = False
136+
def flush_logging():
137+
# make sure all log output is flushed,
138+
# this is especially important for the "borg serve" RemoteRepository logging:
139+
# all log output needs to be sent via the ssh / socket connection before closing it.
140+
for logger_name in "borg.output.progress", "":
141+
logger = logging.getLogger(logger_name)
142+
for handler in logger.handlers:
143+
handler.flush()
122144

123145

124146
def setup_logging(

0 commit comments

Comments
 (0)