Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #2120 (plus changes to status info and tray icon active duration) #2142

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/vorta/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from vorta.store.connection import cleanup_db
from vorta.store.models import BackupProfileModel, SettingsModel
from vorta.tray_menu import TrayMenu
from vorta.utils import borg_compat, parse_args
from vorta.utils import borg_compat, parse_args, AsyncRunner
from vorta.views.main_window import MainWindow

logger = logging.getLogger(__name__)
Expand All @@ -42,6 +42,8 @@ class VortaApp(QtSingleApplication):
backup_log_event = QtCore.pyqtSignal(str, dict)
backup_progress_event = QtCore.pyqtSignal(str)
check_failed_event = QtCore.pyqtSignal(dict)
pre_backup_event = QtCore.pyqtSignal(int)
post_backup_event = QtCore.pyqtSignal(int, bool)

def __init__(self, args_raw, single_app=False):
super().__init__(str(APP_ID), args_raw)
Expand Down Expand Up @@ -84,6 +86,8 @@ def __init__(self, args_raw, single_app=False):
self.message_received_event.connect(self.message_received_event_response)
self.check_failed_event.connect(self.check_failed_response)
self.backup_log_event.connect(self.react_to_log)
self.pre_backup_event.connect(self.pre_backup_event_response)
self.post_backup_event.connect(self.post_backup_event_response)
self.aboutToQuit.connect(self.quit_app_action)
self.set_borg_details_action()
if sys.platform == 'darwin':
Expand All @@ -105,12 +109,13 @@ def quit_app_action(self):
del self.tray
cleanup_db()

def create_backup_action(self, profile_id=None):
@AsyncRunner
def create_backup_action(self, profile_id=None, app=None):
if not profile_id:
profile_id = self.main_window.current_profile.id

profile = BackupProfileModel.get(id=profile_id)
msg = BorgCreateJob.prepare(profile)
msg = BorgCreateJob.prepare(profile, app=self)
if msg['ok']:
job = BorgCreateJob(msg['cmd'], msg, profile.repo.id)
self.jobs_manager.add_job(job)
Expand Down Expand Up @@ -146,6 +151,13 @@ def backup_cancelled_event_response(self):
self.jobs_manager.cancel_all_jobs()
self.tray.set_tray_icon()

def pre_backup_event_response(self, pid):
self.tray.set_tray_icon(active=True)

def post_backup_event_response(self, pid, active=False):
if not active:
self.tray.set_tray_icon()

def message_received_event_response(self, message):
if message == "open main window":
self.open_main_window_action()
Expand Down
20 changes: 14 additions & 6 deletions src/vorta/borg/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,34 @@ def started_event(self):
def finished_event(self, result):
self.app.backup_finished_event.emit(result)
self.result.emit(result)
self.pre_post_backup_cmd(self.params, cmd='post_backup_cmd', returncode=result['returncode'])
self.pre_post_backup_cmd(self.params, context='post_backup_cmd', app=self.app, returncode=result['returncode'])

@classmethod
def pre_post_backup_cmd(cls, params, cmd='pre_backup_cmd', returncode=0):
cmd = getattr(params['profile'], cmd)
def pre_post_backup_cmd(cls, params, context='pre_backup_cmd', app=None, returncode=0):
cmd = getattr(params['profile'], context)
if cmd:
profile_name = getattr(params['profile'], 'name')
env = {
**os.environ.copy(),
'repo_url': params['repo'].url,
'profile_name': params['profile'].name,
'profile_slug': params['profile'].slug(),
'returncode': str(returncode),
}
proc = subprocess.run(cmd, shell=True, env=env)
proc = subprocess.Popen(cmd, shell=True, env=env)
if context.startswith('pre'):
app.backup_progress_event.emit(f"[{profile_name}] {trans_late('messages', 'Waiting to start backup')}")
app.pre_backup_event.emit(proc.pid)
else:
app.post_backup_event.emit(proc.pid, True)
proc.wait()
app.post_backup_event.emit(None, False)
return proc.returncode
else:
return 0 # 0 if no command was run.

@classmethod
def prepare(cls, profile):
def prepare(cls, profile, app=None):
"""
`borg create` is called from different places and needs some preparation.
Centralize it here and return the required arguments to the caller.
Expand Down Expand Up @@ -133,7 +141,7 @@ def prepare(cls, profile):
ret['repo'] = profile.repo

# Run user-supplied pre-backup command
if cls.pre_post_backup_cmd(ret) != 0:
if cls.pre_post_backup_cmd(ret, app=app) != 0:
ret['message'] = trans_late('messages', 'Pre-backup command returned non-zero exit code.')
return ret

Expand Down
5 changes: 3 additions & 2 deletions src/vorta/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from vorta.i18n import translate
from vorta.notifications import VortaNotifications
from vorta.store.models import BackupProfileModel, EventLogModel
from vorta.utils import borg_compat
from vorta.utils import borg_compat, AsyncRunner

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -389,6 +389,7 @@ def next_job_for_profile(self, profile_id: int) -> ScheduleStatus:
return ScheduleStatus(ScheduleStatusType.UNSCHEDULED)
return ScheduleStatus(job['type'], time=job.get('dt'))

@AsyncRunner
def create_backup(self, profile_id):
notifier = VortaNotifications.pick()
profile = BackupProfileModel.get_or_none(id=profile_id)
Expand All @@ -410,7 +411,7 @@ def create_backup(self, profile_id):
self.tr('Starting background backup for %s.') % profile.name,
level='info',
)
msg = BorgCreateJob.prepare(profile)
msg = BorgCreateJob.prepare(profile, app=self.app)
if msg['ok']:
logger.info('Preparation for backup successful.')
msg['category'] = 'scheduled'
Expand Down
49 changes: 48 additions & 1 deletion src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import psutil
from PyQt6 import QtCore
from PyQt6.QtCore import QFileInfo, QThread, pyqtSignal
from PyQt6.QtCore import QFileInfo, QObject, QThread, pyqtSignal
from PyQt6.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon

from vorta.borg._compatibility import BorgCompatibility
Expand All @@ -31,6 +31,53 @@
_network_status_monitor = None


class AsyncRunner(QObject):
'''
Wrapper to run functions asynchronously from GUI thread, based on
https://gist.github.com/andgineer/026a617528c5740da24ec984ac282ee6#file-universal_decorator-py

NB Only apply it to void functions, otherwise return values will be lost.
'''
runner_thread = None

def __init__(self, orig_func):
super(AsyncRunner, self).__init__()
self.orig_func = orig_func
self.__name__ = "AsyncRunner"

def __call__(self, *args):
return self.orig_func(*args)

def __get__(self, wrapped_instance, owner):
return AsyncRunner.Helper(self, wrapped_instance)

class Helper(QObject):
def __init__(self, decorator_instance, wrapped_instance):
super(AsyncRunner.Helper, self).__init__()
self.decorator_instance = decorator_instance
self.wrapped_instance = wrapped_instance

def __call__(self, *args, **kwargs):
self.runner = AsyncRunner.Runner(self.decorator_instance, self.wrapped_instance, *args, **kwargs)
self.runner.finished.connect(self.runner_finished)
self.runner.start()

def runner_finished(self):
self.runner.wait(100)
self.runner = None

class Runner(QtCore.QThread):
def __init__(self, decorator_instance, wrapped_instance, *args, **kwargs):
QtCore.QThread.__init__(self)
self.decorator_instance = decorator_instance
self.wrapped_instance = wrapped_instance
self.args = args
self.kwargs = kwargs

def run(self):
self.decorator_instance(self.wrapped_instance, *self.args, **self.kwargs)


class FilePathInfoAsync(QThread):
signal = pyqtSignal(str, str, str)

Expand Down
13 changes: 13 additions & 0 deletions src/vorta/views/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ def __init__(self, parent=None):
self.app.backup_log_event.connect(self.set_log)
self.app.backup_progress_event.connect(self.set_progress)
self.app.backup_cancelled_event.connect(self.backup_cancelled_event)
self.app.pre_backup_event.connect(self.pre_backup_event)
self.app.post_backup_event.connect(self.post_backup_event)

# Init profile list
self.populate_profile_selector()
Expand Down Expand Up @@ -339,6 +341,17 @@ def backup_cancelled_event(self):
self.set_log(self.tr('Task cancelled'))
self.archiveTab.cancel_action()

def pre_backup_event(self, pid):
self._toggle_buttons(create_enabled=False)
self.set_log(self.tr(f"Running pre backup commands [PID: {pid}]"))

def post_backup_event(self, pid, active=True):
if active:
self.set_log(self.tr(f"Running post backup commands [PID: {pid}]"))
else:
self._toggle_buttons(create_enabled=True)
self.set_log('')

def closeEvent(self, event):
# Save window state in SettingsModel
SettingsModel.update({SettingsModel.str_value: str(self.width())}).where(
Expand Down