Compare commits

...

10 Commits

Author SHA1 Message Date
ae0beac9e0 Catch UnknownSSHError and do not use return codes
The runners were still checking for the return codes of
create_ssh_master_connection instead of catching the exception.
We now catch the exceptions we calling the runners in main.
2026-01-31 12:30:47 +01:00
072c2a26e6 errors : Add program ending function
Add a function that quits the program using sys.exit.
This is useful when we enconter a fatal error.
2026-01-31 11:57:09 +01:00
b0c165b8b0 Revert "paths : make TimeoutExpired handling clearer"
This reverts commit 041ede22e1.

There is no point in using "as e" and "raise e", the original version was
better.
2026-01-31 11:56:51 +01:00
6b8686351a defaults : Capitalize the c of Copyright 2026-01-30 19:12:37 +01:00
dcca9c5167 Merge branch 'errors' into dev
Raise Exceptions instead of using return codes
2026-01-30 17:43:01 +01:00
041ede22e1 paths : make TimeoutExpired handling clearer 2026-01-30 17:41:53 +01:00
adfded92d0 synchroniser : raise error instead of returning a value
Raise a FatalSyncError when the synchronisation fails instead of
returning the unison return code.
2026-01-30 17:40:41 +01:00
7fae1b154a errors : Derive Errors from Exception
According to the docs user defined Exceptions should derive from
Exception not BaseException.
2026-01-30 17:39:28 +01:00
a922eaa542 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.
2026-01-20 22:23:09 +01:00
8836a0120b errors : adds UnknownSSHError
This error is used to report that an unknown error happened during an
invocation of ssh.
2026-01-20 22:22:00 +01:00
5 changed files with 57 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
# copyright (c) 2026 paul retourné # Copyright (c) 2026 paul retourné
# spdx-license-identifier: gpl-3.0-or-later # spdx-license-identifier: gpl-3.0-or-later
from pathlib import Path from pathlib import Path

View File

@@ -1,8 +1,21 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
class RemoteMountedError(BaseException): from typing import NoReturn
import sys
class RemoteMountedError(Exception):
pass pass
class InvalidMountError(BaseException): class InvalidMountError(Exception):
pass pass
class UnknownSSHError(Exception):
pass
class FatalSyncError(Exception):
pass
def unisync_exit_fatal(reason:str) -> NoReturn:
print(reason)
sys.exit(1)

View File

@@ -4,6 +4,7 @@
from pathlib import Path from pathlib import Path
from unisync.argparser import create_argparser from unisync.argparser import create_argparser
from unisync.errors import UnknownSSHError, unisync_exit_fatal
from unisync.runners import unisync_sync, unisync_add, unisync_mount from unisync.runners import unisync_sync, unisync_add, unisync_mount
from unisync.config import load_config from unisync.config import load_config
from unisync.synchroniser import Synchroniser from unisync.synchroniser import Synchroniser
@@ -38,7 +39,10 @@ def main():
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path) paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
cli_args.func(synchroniser, paths_manager, config) try:
cli_args.func(synchroniser, paths_manager, config)
except UnknownSSHError:
unisync_exit_fatal("Connection failed quitting")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -5,11 +5,11 @@ from unisync.synchroniser import Synchroniser
from unisync.paths import PathsManager from unisync.paths import PathsManager
from unisync.config import Config from unisync.config import Config
def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config): def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
del config # The function signature must be the same for all runners del config # The function signature must be the same for all runners
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting") synchroniser.create_ssh_master_connection()
return 1
print("Connected to the remote.") print("Connected to the remote.")
synchroniser.sync_files(paths_manager.get_paths_to_sync()) synchroniser.sync_files(paths_manager.get_paths_to_sync())
@@ -24,9 +24,8 @@ def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config:
def unisync_add(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config): def unisync_add(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
del config # The function signature must be the same for all runners del config # The function signature must be the same for all runners
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting") synchroniser.create_ssh_master_connection()
return 1
print("Connected to the remote.") print("Connected to the remote.")
# TODO config or cli to skip this first sync # TODO config or cli to skip this first sync

View File

@@ -16,7 +16,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from unisync.errors import RemoteMountedError, InvalidMountError from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError, FatalSyncError
from unisync.config import BackupConfig from unisync.config import BackupConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -92,7 +92,7 @@ class Synchroniser:
f"Name {backup.backupprefix[:-1]}" 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. """Creates an ssh master connection.
It is used so the user only has to authenticate once to the remote server. It is used so the user only has to authenticate once to the remote server.
@@ -105,8 +105,14 @@ class Synchroniser:
connection_timeout: connection_timeout:
Time given to the user to authenticate to the remote server. Time given to the user to authenticate to the remote server.
On slow connections one might want to increase this. On slow connections one might want to increase this.
Returns:
An error code (0 success, 1 TimeoutExpired, 2 KeyboardInterrupt). 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) self.control_path = os.path.expanduser(control_path)
command = [ command = [
@@ -121,16 +127,15 @@ class Synchroniser:
# TODO: Raise an exception instead of changing the return value # TODO: Raise an exception instead of changing the return value
try: try:
ret_code = master_ssh.wait(timeout=connection_timeout) ret_code = master_ssh.wait(timeout=connection_timeout)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired as e:
print("Time to login expired", file=sys.stderr) print("Time to login expired", file=sys.stderr)
return 1 raise e
except KeyboardInterrupt: except KeyboardInterrupt as e:
return 2 raise e
if ret_code != 0: if ret_code != 0:
print("Login to remote failed", file=sys.stderr) print("Login to remote failed", file=sys.stderr)
return ret_code raise UnknownSSHError
return 0
def close_ssh_master_connection(self) -> int: def close_ssh_master_connection(self) -> int:
@@ -149,18 +154,18 @@ class Synchroniser:
close = subprocess.Popen(command) close = subprocess.Popen(command)
return close.wait() 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. """Synchronises the files.
Args: Args:
paths: List of paths to synchronise. paths: List of paths to synchronise.
force: Force the changes from remote to local. force: Force the changes from remote to local.
Returns: Raises:
The return code of sync. 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", f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/.data",
self.local, self.local,
paths=paths, paths=paths,
@@ -168,16 +173,16 @@ class Synchroniser:
other=self.files_extra 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. """Synchronises the links, they must exist already.
Args: Args:
ignore: List of paths to ignore. ignore: List of paths to ignore.
Returns: Raises:
The return code of sync. 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", f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/links",
self.local, self.local,
ignore=ignore, ignore=ignore,
@@ -187,7 +192,7 @@ class Synchroniser:
def sync(self, remote_root:str, local_root:str, def sync(self, remote_root:str, local_root:str,
paths:list=[], ignore:list=[], force:bool=False, paths:list=[], ignore:list=[], force:bool=False,
other:list=[] other:list=[]
) -> int: ) -> None:
"""Performs the synchronisation by calling unison. """Performs the synchronisation by calling unison.
Args: Args:
@@ -206,8 +211,11 @@ class Synchroniser:
They will be added to the command as is no - in front. They will be added to the command as is no - in front.
For exemple backups are implemented using this argument. For exemple backups are implemented using this argument.
Returns: Raises:
the unison return code see section 6.11 of the documentation 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 ] command = [ "/usr/bin/unison", "-root", remote_root, "-root", local_root ]
for arg in self.args_bool: for arg in self.args_bool:
@@ -241,7 +249,8 @@ class Synchroniser:
proc = subprocess.Popen(command) proc = subprocess.Popen(command)
ret_code = proc.wait() 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): def update_links(self, background:bool=True):
"""Updates the links on the remote. """Updates the links on the remote.