|
@@ -1,9 +1,9 @@
|
|
import errno
|
|
import errno
|
|
import functools
|
|
import functools
|
|
import inspect
|
|
import inspect
|
|
-import json
|
|
|
|
import logging
|
|
import logging
|
|
import os
|
|
import os
|
|
|
|
+import queue
|
|
import select
|
|
import select
|
|
import shlex
|
|
import shlex
|
|
import shutil
|
|
import shutil
|
|
@@ -26,7 +26,7 @@ from .helpers import sysinfo
|
|
from .helpers import format_file_size
|
|
from .helpers import format_file_size
|
|
from .helpers import safe_unlink
|
|
from .helpers import safe_unlink
|
|
from .helpers import prepare_subprocess_env, ignore_sigint
|
|
from .helpers import prepare_subprocess_env, ignore_sigint
|
|
-from .logger import create_logger
|
|
|
|
|
|
+from .logger import create_logger, borg_serve_log_queue
|
|
from .helpers import msgpack
|
|
from .helpers import msgpack
|
|
from .repository import Repository
|
|
from .repository import Repository
|
|
from .version import parse_version, format_version
|
|
from .version import parse_version, format_version
|
|
@@ -36,7 +36,7 @@ from .helpers.datastruct import EfficientCollectionQueue
|
|
logger = create_logger(__name__)
|
|
logger = create_logger(__name__)
|
|
|
|
|
|
BORG_VERSION = parse_version(__version__)
|
|
BORG_VERSION = parse_version(__version__)
|
|
-MSGID, MSG, ARGS, RESULT = "i", "m", "a", "r"
|
|
|
|
|
|
+MSGID, MSG, ARGS, RESULT, LOG = "i", "m", "a", "r", "l"
|
|
|
|
|
|
MAX_INFLIGHT = 100
|
|
MAX_INFLIGHT = 100
|
|
|
|
|
|
@@ -154,25 +154,28 @@ class RepositoryServer: # pragma: no cover
|
|
def serve(self):
|
|
def serve(self):
|
|
stdin_fd = sys.stdin.fileno()
|
|
stdin_fd = sys.stdin.fileno()
|
|
stdout_fd = sys.stdout.fileno()
|
|
stdout_fd = sys.stdout.fileno()
|
|
- stderr_fd = sys.stdout.fileno()
|
|
|
|
os.set_blocking(stdin_fd, False)
|
|
os.set_blocking(stdin_fd, False)
|
|
os.set_blocking(stdout_fd, True)
|
|
os.set_blocking(stdout_fd, True)
|
|
- os.set_blocking(stderr_fd, True)
|
|
|
|
unpacker = get_limited_unpacker("server")
|
|
unpacker = get_limited_unpacker("server")
|
|
while True:
|
|
while True:
|
|
|
|
+ # before processing any new RPCs, send out all pending log output
|
|
|
|
+ while True:
|
|
|
|
+ try:
|
|
|
|
+ # lr_dict contents see BorgQueueHandler
|
|
|
|
+ lr_dict = borg_serve_log_queue.get_nowait()
|
|
|
|
+ except queue.Empty:
|
|
|
|
+ break
|
|
|
|
+ else:
|
|
|
|
+ msg = msgpack.packb({LOG: lr_dict})
|
|
|
|
+ os_write(stdout_fd, msg)
|
|
|
|
+
|
|
|
|
+ # process new RPCs
|
|
r, w, es = select.select([stdin_fd], [], [], 10)
|
|
r, w, es = select.select([stdin_fd], [], [], 10)
|
|
if r:
|
|
if r:
|
|
data = os.read(stdin_fd, BUFSIZE)
|
|
data = os.read(stdin_fd, BUFSIZE)
|
|
if not data:
|
|
if not data:
|
|
if self.repository is not None:
|
|
if self.repository is not None:
|
|
self.repository.close()
|
|
self.repository.close()
|
|
- else:
|
|
|
|
- os_write(
|
|
|
|
- stderr_fd,
|
|
|
|
- "Borg {}: Got connection close before repository was opened.\n".format(
|
|
|
|
- __version__
|
|
|
|
- ).encode(),
|
|
|
|
- )
|
|
|
|
return
|
|
return
|
|
unpacker.feed(data)
|
|
unpacker.feed(data)
|
|
for unpacked in unpacker:
|
|
for unpacked in unpacker:
|
|
@@ -726,10 +729,18 @@ class RemoteRepository:
|
|
self.rx_bytes += len(data)
|
|
self.rx_bytes += len(data)
|
|
self.unpacker.feed(data)
|
|
self.unpacker.feed(data)
|
|
for unpacked in self.unpacker:
|
|
for unpacked in self.unpacker:
|
|
- if isinstance(unpacked, dict):
|
|
|
|
- msgid = unpacked[MSGID]
|
|
|
|
- else:
|
|
|
|
|
|
+ if not isinstance(unpacked, dict):
|
|
raise UnexpectedRPCDataFormatFromServer(data)
|
|
raise UnexpectedRPCDataFormatFromServer(data)
|
|
|
|
+
|
|
|
|
+ lr_dict = unpacked.get(LOG)
|
|
|
|
+ if lr_dict is not None:
|
|
|
|
+ # Re-emit remote log messages locally.
|
|
|
|
+ _logger = logging.getLogger(lr_dict["name"])
|
|
|
|
+ if _logger.isEnabledFor(lr_dict["level"]):
|
|
|
|
+ _logger.handle(logging.LogRecord(**lr_dict))
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ msgid = unpacked[MSGID]
|
|
if msgid in self.ignore_responses:
|
|
if msgid in self.ignore_responses:
|
|
self.ignore_responses.remove(msgid)
|
|
self.ignore_responses.remove(msgid)
|
|
# async methods never return values, but may raise exceptions.
|
|
# async methods never return values, but may raise exceptions.
|
|
@@ -755,8 +766,14 @@ class RemoteRepository:
|
|
if lines and not lines[-1].endswith((b"\r", b"\n")):
|
|
if lines and not lines[-1].endswith((b"\r", b"\n")):
|
|
self.stderr_received = lines.pop()
|
|
self.stderr_received = lines.pop()
|
|
# now we have complete lines in <lines> and any partial line in self.stderr_received.
|
|
# now we have complete lines in <lines> and any partial line in self.stderr_received.
|
|
|
|
+ _logger = logging.getLogger()
|
|
for line in lines:
|
|
for line in lines:
|
|
- handle_remote_line(line.decode()) # decode late, avoid partial utf-8 sequences
|
|
|
|
|
|
+ # borg serve (remote/server side) should not emit stuff on stderr,
|
|
|
|
+ # but e.g. the ssh process (local/client side) might output errors there.
|
|
|
|
+ assert line.endswith((b"\r", b"\n"))
|
|
|
|
+ # something came in on stderr, log it to not lose it.
|
|
|
|
+ # decode late, avoid partial utf-8 sequences.
|
|
|
|
+ _logger.warning("stderr: " + line.decode().strip())
|
|
if w:
|
|
if w:
|
|
while (
|
|
while (
|
|
(len(self.to_send) <= maximum_to_send)
|
|
(len(self.to_send) <= maximum_to_send)
|
|
@@ -886,57 +903,6 @@ class RemoteRepository:
|
|
self.preload_ids += ids
|
|
self.preload_ids += ids
|
|
|
|
|
|
|
|
|
|
-def handle_remote_line(line):
|
|
|
|
- """
|
|
|
|
- Handle a remote log line.
|
|
|
|
-
|
|
|
|
- This function is remarkably complex because it handles multiple wire formats.
|
|
|
|
- """
|
|
|
|
- assert line.endswith(("\r", "\n"))
|
|
|
|
- if line.startswith("{"):
|
|
|
|
- msg = json.loads(line)
|
|
|
|
-
|
|
|
|
- if msg["type"] not in ("progress_message", "progress_percent", "log_message"):
|
|
|
|
- logger.warning("Dropped remote log message with unknown type %r: %s", msg["type"], line)
|
|
|
|
- return
|
|
|
|
-
|
|
|
|
- if msg["type"] == "log_message":
|
|
|
|
- # Re-emit log messages on the same level as the remote to get correct log suppression and verbosity.
|
|
|
|
- level = getattr(logging, msg["levelname"], logging.CRITICAL)
|
|
|
|
- assert isinstance(level, int)
|
|
|
|
- target_logger = logging.getLogger(msg["name"])
|
|
|
|
- msg["message"] = "Remote: " + msg["message"]
|
|
|
|
- # In JSON mode, we manually check whether the log message should be propagated.
|
|
|
|
- if logging.getLogger("borg").json and level >= target_logger.getEffectiveLevel():
|
|
|
|
- sys.stderr.write(json.dumps(msg) + "\n")
|
|
|
|
- else:
|
|
|
|
- target_logger.log(level, "%s", msg["message"])
|
|
|
|
- elif msg["type"].startswith("progress_"):
|
|
|
|
- # Progress messages are a bit more complex.
|
|
|
|
- # First of all, we check whether progress output is enabled. This is signalled
|
|
|
|
- # through the effective level of the borg.output.progress logger
|
|
|
|
- # (also see ProgressIndicatorBase in borg.helpers).
|
|
|
|
- progress_logger = logging.getLogger("borg.output.progress")
|
|
|
|
- if progress_logger.getEffectiveLevel() == logging.INFO:
|
|
|
|
- # When progress output is enabled, we check whether the client is in
|
|
|
|
- # --log-json mode, as signalled by the "json" attribute on the "borg" logger.
|
|
|
|
- if logging.getLogger("borg").json:
|
|
|
|
- # In --log-json mode we re-emit the progress JSON line as sent by the server,
|
|
|
|
- # with the message, if any, prefixed with "Remote: ".
|
|
|
|
- if "message" in msg:
|
|
|
|
- msg["message"] = "Remote: " + msg["message"]
|
|
|
|
- sys.stderr.write(json.dumps(msg) + "\n")
|
|
|
|
- elif "message" in msg:
|
|
|
|
- # In text log mode we write only the message to stderr and terminate with \r
|
|
|
|
- # (carriage return, i.e. move the write cursor back to the beginning of the line)
|
|
|
|
- # so that the next message, progress or not, overwrites it. This mirrors the behaviour
|
|
|
|
- # of local progress displays.
|
|
|
|
- sys.stderr.write("Remote: " + msg["message"] + "\r")
|
|
|
|
- else:
|
|
|
|
- # We don't know what priority the line had.
|
|
|
|
- logging.getLogger("").warning("stderr/remote: " + line.strip())
|
|
|
|
-
|
|
|
|
-
|
|
|
|
class RepositoryNoCache:
|
|
class RepositoryNoCache:
|
|
"""A not caching Repository wrapper, passes through to repository.
|
|
"""A not caching Repository wrapper, passes through to repository.
|
|
|
|
|