From 8836a0120ba982fcc40f47fbd0438c5bcbc6c8f2 Mon Sep 17 00:00:00 2001 From: furtest Date: Tue, 20 Jan 2026 22:22:00 +0100 Subject: [PATCH 1/5] errors : adds UnknownSSHError This error is used to report that an unknown error happened during an invocation of ssh. --- src/unisync/errors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/unisync/errors.py b/src/unisync/errors.py index 08e25de..988d4d3 100644 --- a/src/unisync/errors.py +++ b/src/unisync/errors.py @@ -6,3 +6,6 @@ class RemoteMountedError(BaseException): class InvalidMountError(BaseException): pass + +class UnknownSSHError(BaseException): + pass From a922eaa5429abff4bdb3e2f1aaf1491946769f01 Mon Sep 17 00:00:00 2001 From: furtest Date: Tue, 20 Jan 2026 22:23:09 +0100 Subject: [PATCH 2/5] synchroniser : use exception instead of return codes In create_ssh_master_connection return codes where used instead of proper error handling with exception, replace these codes with the raising of an appropriate exception. --- src/unisync/synchroniser.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/unisync/synchroniser.py b/src/unisync/synchroniser.py index 7d0e912..42e79c2 100644 --- a/src/unisync/synchroniser.py +++ b/src/unisync/synchroniser.py @@ -16,7 +16,7 @@ import logging from pathlib import Path from typing import cast -from unisync.errors import RemoteMountedError, InvalidMountError +from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError from unisync.config import BackupConfig logger = logging.getLogger(__name__) @@ -92,7 +92,7 @@ class Synchroniser: f"Name {backup.backupprefix[:-1]}" ]) - def create_ssh_master_connection(self, control_path:str="~/.ssh/control_%C", connection_timeout:int=60) -> int: + def create_ssh_master_connection(self, control_path:str="~/.ssh/control_%C", connection_timeout:int=60) -> None: """Creates an ssh master connection. It is used so the user only has to authenticate once to the remote server. @@ -108,6 +108,14 @@ class Synchroniser: Returns: An error code (0 success, 1 TimeoutExpired, 2 KeyboardInterrupt). TODO change that to raising the exception. + + Raises: + subprocess.TimeoutExpired: + The user didn't finish loging in in time. + KeyboardInterrupt: + The user interrupted the process. + UnknownSSHError: + An error occured during the connection. """ self.control_path = os.path.expanduser(control_path) command = [ @@ -121,16 +129,15 @@ class Synchroniser: master_ssh = subprocess.Popen(command) try: ret_code = master_ssh.wait(timeout=connection_timeout) - except subprocess.TimeoutExpired: + except subprocess.TimeoutExpired as e: print("Time to login expired", file=sys.stderr) - return 1 - except KeyboardInterrupt: - return 2 + raise e + except KeyboardInterrupt as e: + raise e if ret_code != 0: print("Login to remote failed", file=sys.stderr) - return ret_code - return 0 + raise UnknownSSHError def close_ssh_master_connection(self) -> int: From 7fae1b154acdad66550ca6fed34a40431a5842e5 Mon Sep 17 00:00:00 2001 From: furtest Date: Fri, 30 Jan 2026 17:39:28 +0100 Subject: [PATCH 3/5] errors : Derive Errors from Exception According to the docs user defined Exceptions should derive from Exception not BaseException. --- src/unisync/errors.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/unisync/errors.py b/src/unisync/errors.py index 988d4d3..d52a724 100644 --- a/src/unisync/errors.py +++ b/src/unisync/errors.py @@ -1,11 +1,14 @@ # Copyright (C) 2025-2026 Paul Retourné # SPDX-License-Identifier: GPL-3.0-or-later -class RemoteMountedError(BaseException): +class RemoteMountedError(Exception): pass -class InvalidMountError(BaseException): +class InvalidMountError(Exception): pass -class UnknownSSHError(BaseException): +class UnknownSSHError(Exception): + pass + +class FatalSyncError(Exception): pass From adfded92d0c80da761839f57f822a2361ad5eee2 Mon Sep 17 00:00:00 2001 From: furtest Date: Fri, 30 Jan 2026 17:40:41 +0100 Subject: [PATCH 4/5] synchroniser : raise error instead of returning a value Raise a FatalSyncError when the synchronisation fails instead of returning the unison return code. --- src/unisync/synchroniser.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/unisync/synchroniser.py b/src/unisync/synchroniser.py index 42e79c2..d5b689a 100644 --- a/src/unisync/synchroniser.py +++ b/src/unisync/synchroniser.py @@ -16,7 +16,7 @@ import logging from pathlib import Path from typing import cast -from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError +from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError, FatalSyncError from unisync.config import BackupConfig logger = logging.getLogger(__name__) @@ -105,9 +105,6 @@ class Synchroniser: connection_timeout: Time given to the user to authenticate to the remote server. On slow connections one might want to increase this. - Returns: - An error code (0 success, 1 TimeoutExpired, 2 KeyboardInterrupt). - TODO change that to raising the exception. Raises: subprocess.TimeoutExpired: @@ -156,18 +153,18 @@ class Synchroniser: close = subprocess.Popen(command) return close.wait() - def sync_files(self, paths:list, force:bool=False) -> int: + def sync_files(self, paths:list, force:bool=False) -> None: """Synchronises the files. Args: paths: List of paths to synchronise. force: Force the changes from remote to local. - Returns: - The return code of sync. + Raises: + FatalSyncError: A fatal error occured during the synchronisation. """ - return self.sync( + self.sync( f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/.data", self.local, paths=paths, @@ -175,16 +172,16 @@ class Synchroniser: other=self.files_extra ) - def sync_links(self, ignore:list) -> int: + def sync_links(self, ignore:list) -> None: """Synchronises the links, they must exist already. Args: ignore: List of paths to ignore. - Returns: - The return code of sync. + Raises: + FatalSyncError: A fatal error occured during the synchronisation. """ - return self.sync( + self.sync( f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/links", self.local, ignore=ignore, @@ -194,7 +191,7 @@ class Synchroniser: def sync(self, remote_root:str, local_root:str, paths:list=[], ignore:list=[], force:bool=False, other:list=[] - ) -> int: + ) -> None: """Performs the synchronisation by calling unison. Args: @@ -213,8 +210,11 @@ class Synchroniser: They will be added to the command as is no - in front. For exemple backups are implemented using this argument. - Returns: - the unison return code see section 6.11 of the documentation + Raises: + FatalSyncError: + If unison returns 3 it means either a fatal error occured or the synchronisation + was interrupted. + If this happens propagate the error to unisync. """ command = [ "/usr/bin/unison", "-root", remote_root, "-root", local_root ] for arg in self.args_bool: @@ -247,7 +247,8 @@ class Synchroniser: proc = subprocess.Popen(command) ret_code = proc.wait() - return ret_code + if ret_code == 3: + raise FatalSyncError("Synchronisation could not be completed") def update_links(self, background:bool=True): """Updates the links on the remote. From 041ede22e1b036ff52a3c9ac16c0e3bde30b7969 Mon Sep 17 00:00:00 2001 From: furtest Date: Fri, 30 Jan 2026 17:41:53 +0100 Subject: [PATCH 5/5] paths : make TimeoutExpired handling clearer --- src/unisync/paths.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unisync/paths.py b/src/unisync/paths.py index a13bc50..7865bba 100644 --- a/src/unisync/paths.py +++ b/src/unisync/paths.py @@ -73,9 +73,9 @@ class PathsManager: try: paths = self.user_select_files() break - except subprocess.TimeoutExpired: + except subprocess.TimeoutExpired as e: if input("Timeout expired do you want to retry (y/n): ") != "y": - raise + raise e self.write_new_paths(paths) def get_paths_to_sync(self) -> list[str]: