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
from pathlib import Path

View File

@@ -1,8 +1,21 @@
# Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later
class RemoteMountedError(BaseException):
from typing import NoReturn
import sys
class RemoteMountedError(Exception):
pass
class InvalidMountError(BaseException):
class InvalidMountError(Exception):
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 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.config import load_config
from unisync.synchroniser import Synchroniser
@@ -38,7 +39,10 @@ def main():
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__":

View File

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

View File

@@ -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, FatalSyncError
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.
@@ -105,8 +105,14 @@ 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).
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 +127,15 @@ class Synchroniser:
# TODO: Raise an exception instead of changing the return value
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:
@@ -149,18 +154,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,
@@ -168,16 +173,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,
@@ -187,7 +192,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:
@@ -206,8 +211,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:
@@ -241,7 +249,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.