From 3b19f421ef87870eeee8513af133066adde3f03a Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 31 Dec 2023 15:46:14 +0100 Subject: [PATCH 1/6] minimal working db --- bin/evaluate.py | 13 ++++++++ src/gui_qt/_py/basewindow.py | 4 +-- src/gui_qt/lib/backup.py | 63 +++++++++++++++++++++++++++++++++++ src/gui_qt/main/mainwindow.py | 13 ++++---- 4 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 src/gui_qt/lib/backup.py diff --git a/bin/evaluate.py b/bin/evaluate.py index a9692dd..a0870c3 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -2,6 +2,7 @@ import sys import pathlib +import os sys.path.append(str(pathlib.Path(__file__).absolute().parent.parent / 'src')) from nmreval.configs import check_for_config @@ -20,6 +21,18 @@ from gui_qt import App app = App(['Team Rocket FTW!']) from gui_qt.main.mainwindow import NMRMainWindow +from gui_qt.lib.backup import Backup + +backuping = Backup() +pid = os.getpid() + +files = backuping.search_unsaved() +print(files) + +bck_name = backuping.create_entry(pid) +print(sys.executable, sys.argv) + +app.aboutToQuit.connect(backuping.close) mplQt = NMRMainWindow() mplQt.show() diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index e1534f4..5c0bff4 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui' +# Form implementation generated from reading ui file 'resources/_ui/basewindow.ui' # -# Created by: PyQt5 UI code generator 5.15.9 +# Created by: PyQt5 UI code generator 5.15.10 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py new file mode 100644 index 0000000..5fa19ae --- /dev/null +++ b/src/gui_qt/lib/backup.py @@ -0,0 +1,63 @@ +import os +import sqlite3 + +from ..Qt import QtCore + +DB_FILE = '/autohome/dominik/nmreval/nmreval.db' + + +class Backup(QtCore.QObject): + def __init__(self): + super().__init__() + self.create_table() + self._pid = None + + def create_table(self): + con = sqlite3.connect(DB_FILE) + con.execute( + "CREATE TABLE IF NOT EXISTS sessions (pid INTEGER NOT NULL, backup_file TEXT NOT NULL );" + ) + + def create_entry(self, pid: int): + con = sqlite3.connect(DB_FILE) + con.execute('INSERT INTO sessions VALUES(?, ?);', + (pid, f'/autohome/dominik/nmreval/tmp_{pid}.nmr')) + con.commit() + con.close() + self._pid = pid + + return + + def remove_entry(self, pid: int = None): + if pid is None: + pid = self._pid + + con = sqlite3.connect(DB_FILE) + cursor = con.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) + res = cursor.execute('SELECT COUNT(sessions.pid) FROM sessions GROUP BY sessions.pid;') + con.commit() + print(res.fetchall()) + + con.close() + + def close(self): + self.remove_entry() + + def search_unsaved(self): + con = sqlite3.connect(DB_FILE) + cursor = con.cursor() + res = cursor.execute('SELECT sessions.pid, sessions.backup_file FROM sessions;') + con.commit() + data = res.fetchall() + con.close() + + missing_processes = [] + + for pid, fname in data: + try: + os.kill(pid, 0) + except ProcessLookupError: + missing_processes.append((pid, fname)) + + return missing_processes + diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 87d6e2c..0d5f0c0 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -22,6 +22,7 @@ from ..graphs.graphwindow import QGraphWindow from ..graphs.movedialog import QMover from ..io.fcbatchreader import QFCReader from ..io.filedialog import * +from ..lib.backup import Backup from ..lib.iconloading import make_action_icons, get_icon from ..lib.pg_objects import RegionItem from ..lib.starter import make_starter @@ -54,6 +55,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.management = UpperManagement(self) + self.fitlimitvalues = [None, None] self.fitpreview = [] self._fit_plot_id = None @@ -89,7 +91,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fit_timer = QtCore.QTimer() self.fit_timer.setInterval(500) self.fit_timer.timeout.connect( - lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)')) + lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)') + ) def _init_gui(self): self.setupUi(self) @@ -235,8 +238,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.actionNext_window.triggered.connect(lambda: self.area.activateNextSubWindow()) self.actionPrevious.triggered.connect(lambda: self.area.activatePreviousSubWindow()) - self.closeSignal.connect(self.close) - self.action_norm_max.triggered.connect(lambda: self.management.apply('norm', ('max',))) self.action_norm_max_abs.triggered.connect(lambda: self.management.apply('norm', ('maxabs',))) self.action_norm_first.triggered.connect(lambda: self.management.apply('norm', ('first',))) @@ -864,10 +865,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): def item_from_graph(self, item, graph_id): self.management.graphs[graph_id].remove_external(item) - def closeEvent(self, evt): - # self._write_settings() - self.close() - @QtCore.pyqtSlot(int) def request_data(self, idx): idd = self.datawidget.get_indexes(idx=idx-1) @@ -1097,6 +1094,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): # remove backup file when closing self.__backup_path.unlink(missing_ok=True) + print('Close me', self.sender()) + super().close() def read_state(self): -- 2.39.2 From 09f366f0bac03cc1c7ca86a254f8731063c83353 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 1 Jan 2024 11:25:40 +0100 Subject: [PATCH 2/6] more work --- bin/evaluate.py | 13 +++++++-- src/gui_qt/lib/backup.py | 55 ++++++++++++++++++++++++++++++----- src/gui_qt/main/mainwindow.py | 46 +++++++---------------------- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/bin/evaluate.py b/bin/evaluate.py index a0870c3..a06eb99 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -17,24 +17,31 @@ from nmreval.lib.logger import handle_exception sys.excepthook = handle_exception from gui_qt import App +from gui_qt.Qt import QtCore app = App(['Team Rocket FTW!']) from gui_qt.main.mainwindow import NMRMainWindow -from gui_qt.lib.backup import Backup +from gui_qt.lib.backup import BackupManager -backuping = Backup() +backuping = BackupManager() pid = os.getpid() files = backuping.search_unsaved() print(files) bck_name = backuping.create_entry(pid) +mplQt = NMRMainWindow(bck_file=bck_name) + print(sys.executable, sys.argv) +timer = QtCore.QTimer() + +timer.timeout.connect(mplQt._autosave) +timer.timeout.connect(backuping.update_last_save) +timer.start(3 * 1000) app.aboutToQuit.connect(backuping.close) -mplQt = NMRMainWindow() mplQt.show() sys.exit(app.exec()) diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index 5fa19ae..2e6eb2c 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -1,12 +1,15 @@ import os import sqlite3 +from datetime import datetime +from pathlib import Path -from ..Qt import QtCore +from nmreval.configs import config_paths +from ..Qt import QtCore, QtWidgets DB_FILE = '/autohome/dominik/nmreval/nmreval.db' -class Backup(QtCore.QObject): +class BackupManager(QtCore.QObject): def __init__(self): super().__init__() self.create_table() @@ -15,18 +18,21 @@ class Backup(QtCore.QObject): def create_table(self): con = sqlite3.connect(DB_FILE) con.execute( - "CREATE TABLE IF NOT EXISTS sessions (pid INTEGER NOT NULL, backup_file TEXT NOT NULL );" + "CREATE TABLE IF NOT EXISTS sessions " + "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save INTEGER);" ) def create_entry(self, pid: int): + backup_path = config_paths() / f'{datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr' + con = sqlite3.connect(DB_FILE) - con.execute('INSERT INTO sessions VALUES(?, ?);', - (pid, f'/autohome/dominik/nmreval/tmp_{pid}.nmr')) + con.execute('INSERT INTO sessions VALUES(?, ?, ?);', + (pid, str(backup_path), None)) con.commit() con.close() self._pid = pid - return + return backup_path def remove_entry(self, pid: int = None): if pid is None: @@ -36,10 +42,15 @@ class Backup(QtCore.QObject): cursor = con.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) res = cursor.execute('SELECT COUNT(sessions.pid) FROM sessions GROUP BY sessions.pid;') con.commit() - print(res.fetchall()) - + remaining_processes = res.fetchone() con.close() + if remaining_processes is None: + self.delete_db() + + def delete_db(self): + Path(DB_FILE).unlink() + def close(self): self.remove_entry() @@ -61,3 +72,31 @@ class Backup(QtCore.QObject): return missing_processes + def update_last_save(self): + con = sqlite3.connect(DB_FILE) + con.execute( + 'UPDATE sessions SET last_save = ? WHERE pid = ?', + (datetime.now(), self._pid) + ) + con.commit() + con.close() + # + # def check_for_backup(self): + # backup_by_date = sorted(backups, key=lambda x: x[1]) + # msg = (QtWidgets.QMessageBox() + # , 'Backup found',) + # f'{len(backups)} backup files in directory {backup_by_date[-1][0].parent} found.\n\n' + # f'Latest backup date: {backup_by_date[-1][1]}\n\n' + # f'Press Ok to load, Cancel to delete backup, Close to do nothing.', + # QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close + # ) + # + # if msg == QtWidgets.QMessageBox.Ok: + # self.management.load_files([str(backup_by_date[-1][0])]) + # backup_by_date[-1][0].unlink() + # elif msg == QtWidgets.QMessageBox.Cancel: + # backup_by_date[-1][0].unlink() + # else: + # pass + # + diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 0d5f0c0..a35b9c2 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -22,7 +22,6 @@ from ..graphs.graphwindow import QGraphWindow from ..graphs.movedialog import QMover from ..io.fcbatchreader import QFCReader from ..io.filedialog import * -from ..lib.backup import Backup from ..lib.iconloading import make_action_icons, get_icon from ..lib.pg_objects import RegionItem from ..lib.starter import make_starter @@ -43,7 +42,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): save_ses_sig = QtCore.pyqtSignal(str) rest_ses_sig = QtCore.pyqtSignal(str) - def __init__(self, parents=None, path=None): + def __init__(self, parents=None, path=None, bck_file=None): super().__init__(parent=parents) if path is None: @@ -81,7 +80,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): if Updater.get_update_information(os.getenv('APPIMAGE'))[0]: self.look_for_update() - self.check_for_backup() + # self.check_for_backup() self.__timer = QtCore.QTimer() self.__backup_path = config_paths() / f'{datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr' @@ -133,12 +132,15 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fitregion = RegionItem() self._fit_plot_id = None - self.setGeometry(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, - self.size(), QtWidgets.qApp.desktop().availableGeometry())) + self.setGeometry(QtWidgets.QStyle.alignedRect( + QtCore.Qt.LayoutDirection.LeftToRight, + QtCore.Qt.AlignmentFlag.AlignCenter, + self.size(), + QtWidgets.qApp.desktop().availableGeometry()), + ) self.datawidget.management = self.management self.integralwidget.management = self.management - # self.drawingswidget.graphs = self.management.graphs self.ac_group = QtWidgets.QActionGroup(self) self.ac_group.addAction(self.action_lm_fit) @@ -992,7 +994,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.editor = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self) self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions()) - self.editor.setWindowModality(QtCore.Qt.ApplicationModal) + self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) self.editor.show() @QtCore.pyqtSlot(list) @@ -1057,13 +1059,13 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): if self.sender() == self.actionLife: from ..lib.gol import QGameOfLife game = QGameOfLife(parent=self) - game.setWindowModality(QtCore.Qt.NonModal) + game.setWindowModality(QtCore.Qt.WindowModality.NonModal) game.show() elif self.sender() == self.actionMine: from ..lib.stuff import QMines game = QMines(parent=self) - game.setWindowModality(QtCore.Qt.NonModal) + game.setWindowModality(QtCore.Qt.WindowModality.NonModal) game.show() else: @@ -1128,32 +1130,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.status.setText('') - def check_for_backup(self): - backups = [] - for filename in config_paths().glob('*.nmr'): - try: - backups.append((filename, datetime.datetime.strptime(filename.stem, "%Y-%m-%d_%H%M%S"))) - except ValueError: - continue - - if backups: - backup_by_date = sorted(backups, key=lambda x: x[1]) - msg = QtWidgets.QMessageBox.information( - self, 'Backup found', - f'{len(backups)} backup files in directory {backup_by_date[-1][0].parent} found.\n\n' - f'Latest backup date: {backup_by_date[-1][1]}\n\n' - f'Press Ok to load, Cancel to delete backup, Close to do nothing.', - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close - ) - - if msg == QtWidgets.QMessageBox.Ok: - self.management.load_files([str(backup_by_date[-1][0])]) - backup_by_date[-1][0].unlink() - elif msg == QtWidgets.QMessageBox.Cancel: - backup_by_date[-1][0].unlink() - else: - pass - @QtCore.pyqtSlot(name='on_actionCreate_starter_triggered') def create_starter(self): make_starter(os.getenv('APPIMAGE')) -- 2.39.2 From c55a983b54dae9a7b903397cf37242f80ce5e834 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 1 Jan 2024 12:08:17 +0100 Subject: [PATCH 3/6] more work --- bin/evaluate.py | 17 ++++++++++++----- src/gui_qt/lib/backup.py | 22 ++++++++++++++++------ src/gui_qt/main/mainwindow.py | 27 +++++++-------------------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/bin/evaluate.py b/bin/evaluate.py index a06eb99..78093ed 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -24,20 +24,27 @@ app = App(['Team Rocket FTW!']) from gui_qt.main.mainwindow import NMRMainWindow from gui_qt.lib.backup import BackupManager + +def do_autosave(): + success = mplQt.autosave() + if success: + backuping.update_last_save() + + backuping = BackupManager() -pid = os.getpid() files = backuping.search_unsaved() print(files) +print(sys.executable, sys.argv) +pid = os.getpid() bck_name = backuping.create_entry(pid) mplQt = NMRMainWindow(bck_file=bck_name) -print(sys.executable, sys.argv) -timer = QtCore.QTimer() +do_autosave() -timer.timeout.connect(mplQt._autosave) -timer.timeout.connect(backuping.update_last_save) +timer = QtCore.QTimer() +timer.timeout.connect(do_autosave) timer.start(3 * 1000) app.aboutToQuit.connect(backuping.close) diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index 2e6eb2c..f80efc7 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -39,20 +39,28 @@ class BackupManager(QtCore.QObject): pid = self._pid con = sqlite3.connect(DB_FILE) - cursor = con.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) + cursor = con.cursor() + + res = cursor.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) + con.commit() + + self.delete_db_if_empty() + + def delete_db_if_empty(self): + con = sqlite3.connect(DB_FILE) + cursor = con.cursor() res = cursor.execute('SELECT COUNT(sessions.pid) FROM sessions GROUP BY sessions.pid;') con.commit() + remaining_processes = res.fetchone() con.close() if remaining_processes is None: - self.delete_db() - - def delete_db(self): - Path(DB_FILE).unlink() + Path(DB_FILE).unlink() def close(self): self.remove_entry() + self.delete_db_if_empty() def search_unsaved(self): con = sqlite3.connect(DB_FILE) @@ -70,7 +78,9 @@ class BackupManager(QtCore.QObject): except ProcessLookupError: missing_processes.append((pid, fname)) - return missing_processes + if missing_processes: + msg = QtWidgets.QMessageBox() + msg.exec() def update_last_save(self): con = sqlite3.connect(DB_FILE) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index a35b9c2..ec0e131 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime import os +import pathlib import re from pathlib import Path @@ -54,7 +55,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.management = UpperManagement(self) - self.fitlimitvalues = [None, None] self.fitpreview = [] self._fit_plot_id = None @@ -76,23 +76,14 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self._init_gui() self._init_signals() - if os.getenv('APPIMAGE') is not None: - if Updater.get_update_information(os.getenv('APPIMAGE'))[0]: - self.look_for_update() - - # self.check_for_backup() - - self.__timer = QtCore.QTimer() - self.__backup_path = config_paths() / f'{datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr' - self.__timer.start(3*60*1000) # every three minutes - self.__timer.timeout.connect(self._autosave) - self.fit_timer = QtCore.QTimer() self.fit_timer.setInterval(500) self.fit_timer.timeout.connect( lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)') ) + self.__backup_path = pathlib.Path(bck_file) + def _init_gui(self): self.setupUi(self) make_action_icons(self) @@ -110,8 +101,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fitlim_button.setIcon(get_icon('fit_region')) self.toolBar_fit.addWidget(self.fitlim_button) - # self.area.dragEnterEvent = self.dragEnterEvent - while self.tabWidget.count() > 2: self.tabWidget.removeTab(self.tabWidget.count()-1) @@ -166,6 +155,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.menuData.insertAction(self.actionRedo, self.actionUndo) self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter) + # noinspection PyUnresolvedReferences self.ac_group2.triggered.connect(self.change_fit_limits) self.t1action.triggered.connect(lambda: self._show_tab('t1_temp')) @@ -1093,11 +1083,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): def close(self): write_state({'History': {'recent path': str(self.path)}}) - # remove backup file when closing - self.__backup_path.unlink(missing_ok=True) - - print('Close me', self.sender()) - super().close() def read_state(self): @@ -1121,7 +1106,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): QLog(parent=self).show() - def _autosave(self): + def autosave(self) -> bool: # TODO better separate thread may it takes some time to save self.status.setText('Autosave...') success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0')) @@ -1130,6 +1115,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.status.setText('') + return success + @QtCore.pyqtSlot(name='on_actionCreate_starter_triggered') def create_starter(self): make_starter(os.getenv('APPIMAGE')) -- 2.39.2 From cbcc9308ad2f8bd7f07c924edcdd336549948f62 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 2 Jan 2024 10:25:46 +0100 Subject: [PATCH 4/6] read old autosaves --- bin/evaluate.py | 9 +- src/gui_qt/lib/backup.py | 195 +++++++++++++++++++++++++--------- src/gui_qt/lib/logger.py | 15 +-- src/gui_qt/main/mainwindow.py | 6 +- 4 files changed, 162 insertions(+), 63 deletions(-) diff --git a/bin/evaluate.py b/bin/evaluate.py index 78093ed..cc83218 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -34,8 +34,6 @@ def do_autosave(): backuping = BackupManager() files = backuping.search_unsaved() -print(files) -print(sys.executable, sys.argv) pid = os.getpid() bck_name = backuping.create_entry(pid) @@ -43,12 +41,17 @@ mplQt = NMRMainWindow(bck_file=bck_name) do_autosave() +for f in files: + mplQt.management.load_files(f) + f.unlink() + timer = QtCore.QTimer() timer.timeout.connect(do_autosave) -timer.start(3 * 1000) +timer.start(3 * 60 * 1000) app.aboutToQuit.connect(backuping.close) mplQt.show() + sys.exit(app.exec()) diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index f80efc7..b7bb39f 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -19,11 +19,11 @@ class BackupManager(QtCore.QObject): con = sqlite3.connect(DB_FILE) con.execute( "CREATE TABLE IF NOT EXISTS sessions " - "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save INTEGER);" + "(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save TEXT);" ) def create_entry(self, pid: int): - backup_path = config_paths() / f'{datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr' + backup_path = config_paths() / f'autosave_{datetime.now().strftime("%Y-%m-%d_%H%M%S")}_{pid}.nmr' con = sqlite3.connect(DB_FILE) con.execute('INSERT INTO sessions VALUES(?, ?, ?);', @@ -34,17 +34,78 @@ class BackupManager(QtCore.QObject): return backup_path - def remove_entry(self, pid: int = None): + def update_last_save(self): + con = sqlite3.connect(DB_FILE) + con.execute( + 'UPDATE sessions SET last_save = ? WHERE pid = ?', + (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self._pid) + ) + con.commit() + con.close() + + def search_unsaved(self): + con = sqlite3.connect(DB_FILE) + cursor = con.cursor() + res = cursor.execute('SELECT sessions.* FROM sessions;') + con.commit() + data = res.fetchall() + con.close() + + missing_processes = [] + + for pid, fname, save_date in data: + try: + os.kill(pid, 0) + except ProcessLookupError: + if Path(fname).exists(): + missing_processes.append((pid, fname, save_date)) + else: + # remove entries without valid file + self.remove_row(pid) + + if missing_processes: + msg = QLonelyBackupWindow() + msg.add_files(missing_processes) + msg.exec() + toberead = msg.recover + + for pid in toberead[0]: + self.remove_file(pid) + + for pid in toberead[1]: + self.remove_row(pid) + + return list(toberead[1].values()) + + return [] + + def remove_row(self, pid): + con = sqlite3.connect(DB_FILE) + con.execute('DELETE FROM sessions WHERE pid = ?', (pid,)) + con.commit() + con.close() + + def remove_file(self, pid: int = None): if pid is None: pid = self._pid con = sqlite3.connect(DB_FILE) cursor = con.cursor() - res = cursor.execute('DELETE FROM sessions WHERE pid = ?;', (pid,)) + # remove backup file + res = cursor.execute('SELECT sessions.backup_file FROM sessions WHERE pid = ?', (pid,)) con.commit() - self.delete_db_if_empty() + fname = Path(res.fetchone()[0]) + con.close() + + fname.unlink(missing_ok=True) + + # because autosave backups in a *.nmr.0 file and then moves it to *.nmr we look also for this one. + fname.with_suffix('.nmr.0').unlink(missing_ok=True) + + # after removal of file also remove entry + self.remove_row(pid) def delete_db_if_empty(self): con = sqlite3.connect(DB_FILE) @@ -54,59 +115,93 @@ class BackupManager(QtCore.QObject): remaining_processes = res.fetchone() con.close() - if remaining_processes is None: Path(DB_FILE).unlink() def close(self): - self.remove_entry() + self.remove_file() self.delete_db_if_empty() - def search_unsaved(self): - con = sqlite3.connect(DB_FILE) - cursor = con.cursor() - res = cursor.execute('SELECT sessions.pid, sessions.backup_file FROM sessions;') - con.commit() - data = res.fetchall() - con.close() - missing_processes = [] +class QLonelyBackupWindow(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent=parent) + self.recover = [[], {}] # list of pid to delete, dict of files to read {pid: file} - for pid, fname in data: - try: - os.kill(pid, 0) - except ProcessLookupError: - missing_processes.append((pid, fname)) + self.setWindowTitle('Adopt a file!') + self.resize(720, 320) - if missing_processes: - msg = QtWidgets.QMessageBox() - msg.exec() + layout = QtWidgets.QVBoxLayout(self) + self.label = QtWidgets.QLabel(self) + self.label.setText('Abandoned backup file(s) looking for a loving home!\n' + '(Files will all be loaded to same instance)') + layout.addWidget(self.label) - def update_last_save(self): - con = sqlite3.connect(DB_FILE) - con.execute( - 'UPDATE sessions SET last_save = ? WHERE pid = ?', - (datetime.now(), self._pid) - ) - con.commit() - con.close() - # - # def check_for_backup(self): - # backup_by_date = sorted(backups, key=lambda x: x[1]) - # msg = (QtWidgets.QMessageBox() - # , 'Backup found',) - # f'{len(backups)} backup files in directory {backup_by_date[-1][0].parent} found.\n\n' - # f'Latest backup date: {backup_by_date[-1][1]}\n\n' - # f'Press Ok to load, Cancel to delete backup, Close to do nothing.', - # QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close - # ) - # - # if msg == QtWidgets.QMessageBox.Ok: - # self.management.load_files([str(backup_by_date[-1][0])]) - # backup_by_date[-1][0].unlink() - # elif msg == QtWidgets.QMessageBox.Cancel: - # backup_by_date[-1][0].unlink() - # else: - # pass - # + self.table = QtWidgets.QTableWidget(self) + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(['File name', 'Last saved', 'File size', 'Action']) + self.table.setGridStyle(QtCore.Qt.PenStyle.DashLine) + # self.table.horizontalHeader().setStretchLastSection(True) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + layout.addWidget(self.table) + + self.buttons = QtWidgets.QDialogButtonBox(self) + self.buttons.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttons.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) + layout.addWidget(self.buttons) + + self.buttons.accepted.connect(self.accept) + + def add_files(self, entries): + self.table.setRowCount(len(entries)) + for i, (pid, path, date) in enumerate(entries): + path = Path(path) + item1 = QtWidgets.QTableWidgetItem(path.name) + item1.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + item1.setData(QtCore.Qt.ItemDataRole.UserRole, pid) + item1.setData(QtCore.Qt.ItemDataRole.UserRole+1, path) + self.table.setItem(i, 0, item1) + + item2 = QtWidgets.QTableWidgetItem(date) + item2.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + self.table.setItem(i, 1, item2) + + size = path.stat().st_size + size_cnt = 0 + while size > 1024: + # make file size human-readable + size /= 1024 + size_cnt += 1 + if size_cnt == 5: + break + + byte = ['bytes', 'kB', 'MiB', 'GiB', 'TiB', 'PiB'][size_cnt] + + item3 = QtWidgets.QTableWidgetItem(f'{size:.2f} {byte}') + item3.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) + self.table.setItem(i, 2, item3) + + cw = QtWidgets.QComboBox(self) + cw.addItems(['Load', 'Delete', 'Keep for later']) + self.table.setCellWidget(i, 3, cw) + + self.table.resizeColumnsToContents() + + def accept(self): + for i in range(self.table.rowCount()): + decision = self.table.cellWidget(i, 3).currentIndex() + item = self.table.item(i, 0) + pid = item.data(QtCore.Qt.ItemDataRole.UserRole) + if decision == 0: + # load file + self.recover[1][pid] = item.data(QtCore.Qt.ItemDataRole.UserRole+1) + elif decision == 1: + # delete + self.recover[0].append(pid) + else: + # do nothing + pass + + super().accept() diff --git a/src/gui_qt/lib/logger.py b/src/gui_qt/lib/logger.py index 43b6bb1..0570d84 100644 --- a/src/gui_qt/lib/logger.py +++ b/src/gui_qt/lib/logger.py @@ -1,4 +1,3 @@ -import sys from pathlib import Path from .codeeditor import _make_textformats @@ -6,12 +5,14 @@ from ..Qt import QtWidgets, QtCore, QtGui from nmreval.configs import config_paths -STYLES = {'INFO': _make_textformats('black'), - 'WARNING': _make_textformats('blue'), - 'ERROR': _make_textformats('red', 'bold'), - 'DEBUG': _make_textformats('black', 'italic'), - 'file': _make_textformats('red', 'italic'), - 'PyError': _make_textformats('red', 'bold-italic')} +STYLES = { + 'INFO': _make_textformats('black'), + 'WARNING': _make_textformats('blue'), + 'ERROR': _make_textformats('red', 'bold'), + 'DEBUG': _make_textformats('black', 'italic'), + 'file': _make_textformats('red', 'italic'), + 'PyError': _make_textformats('red', 'bold-italic'), +} class LogHighlighter(QtGui.QSyntaxHighlighter): diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index ec0e131..487c3c4 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -1,9 +1,8 @@ from __future__ import annotations -import datetime import os -import pathlib import re +import time from pathlib import Path from numpy import geomspace, linspace @@ -34,7 +33,7 @@ from ..math.smooth import QSmooth from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.t1_from_tau import QRelaxCalc from .._py.basewindow import Ui_BaseWindow -from ..lib.utils import UpdateDialog, Updater +from ..lib.utils import UpdateDialog class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @@ -1110,6 +1109,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): # TODO better separate thread may it takes some time to save self.status.setText('Autosave...') success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0')) + if success: self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path) -- 2.39.2 From 4a324b4b11c0278846c19d9af8df7c1a7db118c2 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 3 Jan 2024 13:04:57 +0100 Subject: [PATCH 5/6] update with automatic restart --- bin/evaluate.py | 3 +- src/gui_qt/lib/backup.py | 1 + src/gui_qt/lib/update.py | 288 ++++++++++++++++++++++++++++++++++ src/gui_qt/lib/utils.py | 233 --------------------------- src/gui_qt/main/mainwindow.py | 2 +- 5 files changed, 292 insertions(+), 235 deletions(-) create mode 100644 src/gui_qt/lib/update.py diff --git a/bin/evaluate.py b/bin/evaluate.py index cc83218..7ba36a6 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -17,12 +17,13 @@ from nmreval.lib.logger import handle_exception sys.excepthook = handle_exception from gui_qt import App -from gui_qt.Qt import QtCore +from gui_qt.Qt import QtCore, QtWidgets, QtGui app = App(['Team Rocket FTW!']) from gui_qt.main.mainwindow import NMRMainWindow from gui_qt.lib.backup import BackupManager +from gui_qt.lib.update import UpdateDialog def do_autosave(): diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index b7bb39f..3030f70 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -119,6 +119,7 @@ class BackupManager(QtCore.QObject): Path(DB_FILE).unlink() def close(self): + print('close db') self.remove_file() self.delete_db_if_empty() diff --git a/src/gui_qt/lib/update.py b/src/gui_qt/lib/update.py new file mode 100644 index 0000000..bb5347a --- /dev/null +++ b/src/gui_qt/lib/update.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +import hashlib +import os +import subprocess +import urllib.request +from datetime import datetime + +from os import getenv, stat +from os.path import exists +from pathlib import Path +from urllib.error import HTTPError + +from PyQt5 import QtWidgets, QtCore + +from nmreval.lib.logger import logger + + +class UpdateDialog(QtWidgets.QDialog): + startDownload = QtCore.pyqtSignal(tuple) + restartSignal = QtCore.pyqtSignal() + + def __init__(self, filename: str = None, parent=None): + super().__init__(parent=parent) + self._init_ui() + + if filename is None: + filename = getenv('APPIMAGE') + self._appfile = filename + + self.updater = Updater() + + self.thread = QtCore.QThread(self) + self.thread.start() + self.helper = Downloader() + self.startDownload.connect(self.helper.run_download) + self.helper.progressChanged.connect(self.status.setText) + self.helper.finished.connect(self.finish_update) + self.helper.started.connect(self.status.show) + self.helper.moveToThread(self.thread) + + self.look_for_updates(self._appfile) + + def _init_ui(self): + self.setWindowTitle('Updates') + + layout = QtWidgets.QVBoxLayout() + + self.label = QtWidgets.QLabel() + layout.addWidget(self.label) + + layout.addSpacing(10) + + self.status = QtWidgets.QLabel() + self.status.hide() + layout.addWidget(self.status) + layout.addSpacing(10) + + self.dialog_button = QtWidgets.QDialogButtonBox() + self.dialog_button.accepted.connect(self.update_appimage) + self.dialog_button.rejected.connect(self.close) + layout.addWidget(self.dialog_button) + + self.setLayout(layout) + + def look_for_updates(self, filename=None): + logger.info(f'Looking for updates, compare to file {filename}') + # Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage + is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename) + + label_text = '' + + if is_updateble is None: + label_text += '

Could not determine if this version is newer, please update manually (if necessary).

' + dialog_bttns = QtWidgets.QDialogButtonBox.Close + elif is_updateble: + label_text += '

Newer version available. Press Ok to download new version, Cancel to ignore.

' + dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + else: + label_text += '

Version may be already up-to-date.

' + dialog_bttns = QtWidgets.QDialogButtonBox.Close + + if m_time_zsync is None: + label_text += '

Creation date of remote version is unknown.

' + else: + label_text += f'

Date of most recent AppImage: {m_time_zsync.strftime("%d %B %Y %H:%M")}

' + + if m_time_file is None: + label_text += 'No AppImage file found, press Ok to download latest version.' + dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Close + else: + label_text += f'

Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}

' + + self.label.setText(label_text) + self.dialog_button.setStandardButtons(dialog_bttns) + + @QtCore.pyqtSlot() + def update_appimage(self): + if self._appfile is None: + args = (self.updater.zsync_url,) + else: + # this breaks the download for some reason + args = (self.updater.zsync_url, self._appfile) + + self.dialog_button.setEnabled(False) + + self.startDownload.emit(args) + self.status.show() + + @QtCore.pyqtSlot(int, str) + def finish_update(self, retcode: int, file_loc: str): + restart = QRestartWindow(state=retcode, file_loc=file_loc, parent=self) + res = restart.exec() + + self.close() + + if res == QtWidgets.QMessageBox.Ok: + self.restartSignal.emit() + + def closeEvent(self, evt): + self.thread.quit() + self.thread.wait() + + super().closeEvent(evt) + + +class QRestartWindow(QtWidgets.QMessageBox): + def __init__(self, state: int, file_loc: str, parent=None): + super().__init__(parent=parent) + self._appfile = file_loc + + if state: + self.setText('Download failed') + self.setDetailedText(f'Status code of failure is {state}') + self.setStandardButtons(QtWidgets.QMessageBox.Close) + self.setIcon(QtWidgets.QMessageBox.Warning) + else: + self.setText('Download completed!') + self.setInformativeText("Press Restart to use new version now") + self.setDetailedText(f'Location of AppImage: {file_loc}') + + self.setIcon(QtWidgets.QMessageBox.Information) + + self.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Close) + restart_button = self.button(QtWidgets.QMessageBox.Ok) + restart_button.setText('Restart') + + self.buttonClicked.connect(self.maybe_close) + + def maybe_close(self): + if self.clickedButton() == self.button(QtWidgets.QMessageBox.Ok): + app = QtWidgets.QApplication.instance() + app.quit() + subprocess.Popen(self._appfile) + + +class Downloader(QtCore.QObject): + started = QtCore.pyqtSignal() + finished = QtCore.pyqtSignal(int, str) + progressChanged = QtCore.pyqtSignal(str) + + @QtCore.pyqtSlot(tuple) + def run_download(self, args: tuple[str]): + status = 0 + appimage_location = args[0][:-6] + logger.info(f'Download {appimage_location}') + if len(args) == 2: + new_file = Path(args[1]) + else: + new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage' + + if new_file.exists(): + os.rename(new_file, new_file.with_suffix('.AppImage.old')) + + try: + with urllib.request.urlopen(appimage_location) as response: + with new_file.open('wb') as f: + f.write(response.read()) + + new_file.chmod(0o755) + except HTTPError as e: + logger.exception(f'Download failed with {e}') + status = 3 + except Exception as e: + logger.exception(f'Download failed with {e.args}') + status = 1 + + if status != 0: + logger.warning('Download failed, restore previous AppImage') + try: + os.rename(new_file.with_suffix('.AppImage.old'), new_file) + except FileNotFoundError: + pass + # zsync does not support https + + self.finished.emit(status, str(new_file)) + + +class Updater: + host = 'gitea.pkm.physik.tu-darmstadt.de/api/packages/IPKM/generic/NMReval/latest/' + version = 'NMReval-latest-x86_64' + + @property + def zsync_url(self): + return f'https://{Updater.host}/{Updater.version}.AppImage.zsync' + + @staticmethod + def get_zsync(): + url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync' + m_time_zsync = None + checksum_zsync = None + zsync_file = None + filename = None + try: + with urllib.request.urlopen(url_zsync) as response: + zsync_file = response.read() + except HTTPError as e: + logger.error(f'Request for zsync returned code {e}') + except Exception as e: + logger.exception(f'Download of zsync failed with exception {e.args}') + + if zsync_file is not None: + for line in zsync_file.split(b'\n'): + try: + kw, val = line.split(b': ') + time_string = str(val, encoding='utf-8') + time_format = '%a, %d %b %Y %H:%M:%S %z' + if kw == b'MTime': + try: + m_time_zsync = datetime.strptime(time_string, time_format).astimezone(None) + except ValueError: + logger.warning(f'zsync time "{time_string}" does not match "{time_format}"') + elif kw == b'SHA-1': + checksum_zsync = str(val, encoding='utf-8') + elif kw == b'Filename': + filename = str(val, encoding='utf-8') + + except ValueError: + # stop when empty line is reached + break + + return m_time_zsync, checksum_zsync, filename + + @staticmethod + def get_appimage_info(filename: str): + if filename is None: + return None, None + + if not exists(filename): + return None, None + + stat_mtime = stat(filename).st_mtime + m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0) + with open(filename, 'rb') as f: + checksum_file = hashlib.sha1(f.read()).hexdigest() + if checksum_file is None: + logger.warning('No checksum for AppImage calculated') + + return m_time_file, checksum_file + + @staticmethod + def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]: + m_time_zsync, checksum_zsync, appname = Updater.get_zsync() + m_time_file, checksum_file = Updater.get_appimage_info(filename) + + logger.debug(f'zsync information {m_time_zsync}, {checksum_zsync}, {appname}') + logger.debug(f'file information {m_time_file}, {checksum_file}') + + if not ((checksum_file is not None) and (checksum_zsync is not None)): + return None, m_time_file, m_time_zsync + else: + return checksum_file != checksum_zsync, m_time_file, m_time_zsync + + +if __name__ == '__main__': + import sys + from gui_qt import App + + app = App(['Team Rocket FTW!']) + + updater = UpdateDialog() + updater.show() + + sys.exit(app.exec()) + + + + diff --git a/src/gui_qt/lib/utils.py b/src/gui_qt/lib/utils.py index 1cff792..2f06820 100644 --- a/src/gui_qt/lib/utils.py +++ b/src/gui_qt/lib/utils.py @@ -1,20 +1,9 @@ from __future__ import annotations -import os -import urllib.request -from os import getenv, stat -from os.path import exists -import hashlib -import subprocess -from datetime import datetime from contextlib import contextmanager -from pathlib import Path -from urllib.error import HTTPError from numpy import linspace from scipy.interpolate import interp1d -from nmreval.lib.logger import logger - from ..Qt import QtGui, QtWidgets, QtCore @@ -63,225 +52,3 @@ class RdBuCMap: return col -class UpdateDialog(QtWidgets.QDialog): - startDownload = QtCore.pyqtSignal(tuple) - - def __init__(self, filename: str = None, parent=None): - super().__init__(parent=parent) - self._init_ui() - - if filename is None: - filename = getenv('APPIMAGE') - self._appfile = filename - - self.updater = Updater() - - self.thread = QtCore.QThread(self) - self.thread.start() - self.helper = Downloader() - self.startDownload.connect(self.helper.run_download) - self.helper.progressChanged.connect(self.status.setText) - self.helper.finished.connect(self.finish_update) - self.helper.started.connect(self.status.show) - self.helper.moveToThread(self.thread) - - self.look_for_updates(self._appfile) - - def _init_ui(self): - self.setWindowTitle('Updates') - - layout = QtWidgets.QVBoxLayout() - - self.label = QtWidgets.QLabel() - layout.addWidget(self.label) - - layout.addSpacing(10) - - self.status = QtWidgets.QLabel() - self.status.hide() - layout.addWidget(self.status) - layout.addSpacing(10) - - self.dialog_button = QtWidgets.QDialogButtonBox() - self.dialog_button.accepted.connect(self.update_appimage) - self.dialog_button.rejected.connect(self.close) - layout.addWidget(self.dialog_button) - - self.setLayout(layout) - - def look_for_updates(self, filename=None): - logger.info(f'Looking for updates, compare to file {filename}') - # Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage - is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename) - - label_text = '' - - if is_updateble is None: - label_text += '

Could not determine if this version is newer, please update manually (if necessary).

' - dialog_bttns = QtWidgets.QDialogButtonBox.Close - elif is_updateble: - label_text += '

Newer version available. Press Ok to download new version, Cancel to ignore.

' - dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel - else: - label_text += '

Version may be already up-to-date.

' - dialog_bttns = QtWidgets.QDialogButtonBox.Close - - if m_time_zsync is None: - label_text += '

Creation date of remote version is unknown.

' - else: - label_text += f'

Date of most recent AppImage: {m_time_zsync.strftime("%d %B %Y %H:%M")}

' - - if m_time_file is None: - label_text += 'No AppImage file found, press Ok to download latest version.' - dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Close - else: - label_text += f'

Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}

' - - self.label.setText(label_text) - self.dialog_button.setStandardButtons(dialog_bttns) - - @QtCore.pyqtSlot() - def update_appimage(self): - if self._appfile is None: - args = (self.updater.zsync_url,) - else: - # this breaks the download for some reason - args = (self.updater.zsync_url, self._appfile) - - self.dialog_button.setEnabled(False) - - self.startDownload.emit(args) - self.status.show() - - @QtCore.pyqtSlot(int, str) - def finish_update(self, retcode: int, file_loc: str): - if retcode == 0: - self.status.setText(f'Download complete.New AppImage lies in

{file_loc}.

') - else: - self.status.setText(f'Download failed :( with return code {retcode}.') - self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close) - self.dialog_button.setEnabled(True) - - def closeEvent(self, evt): - self.thread.quit() - self.thread.wait() - - super().closeEvent(evt) - - -class Downloader(QtCore.QObject): - started = QtCore.pyqtSignal() - finished = QtCore.pyqtSignal(int, str) - progressChanged = QtCore.pyqtSignal(str) - - @QtCore.pyqtSlot(tuple) - def run_download(self, args: tuple[str]): - status = 0 - appimage_location = args[0][:-6] - logger.info(f'Download {appimage_location}') - if len(args) == 2: - new_file = Path(args[1]) - else: - new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage' - - if new_file.exists(): - os.rename(new_file, new_file.with_suffix('.AppImage.old')) - - try: - with urllib.request.urlopen(appimage_location) as response: - with new_file.open('wb') as f: - f.write(response.read()) - - new_file.chmod(0o755) - except HTTPError as e: - logger.exception(f'Download failed with {e}') - status = 3 - except Exception as e: - logger.exception(f'Download failed with {e.args}') - status = 1 - - if status != 0: - logger.warning('Download failed, restore previous AppImage') - try: - os.rename(new_file.with_suffix('.AppImage.old'), new_file) - except FileNotFoundError: - pass - # zsync does not support https - - self.finished.emit(status, str(new_file)) - - -class Updater: - host = 'gitea.pkm.physik.tu-darmstadt.de/api/packages/IPKM/generic/NMReval/latest/' - version = 'NMReval-latest-x86_64' - - @property - def zsync_url(self): - return f'https://{Updater.host}/{Updater.version}.AppImage.zsync' - - @staticmethod - def get_zsync(): - url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync' - m_time_zsync = None - checksum_zsync = None - zsync_file = None - filename = None - try: - with urllib.request.urlopen(url_zsync) as response: - zsync_file = response.read() - except HTTPError as e: - logger.error(f'Request for zsync returned code {e}') - except Exception as e: - logger.exception(f'Download of zsync failed with exception {e.args}') - - if zsync_file is not None: - for line in zsync_file.split(b'\n'): - try: - kw, val = line.split(b': ') - time_string = str(val, encoding='utf-8') - time_format = '%a, %d %b %Y %H:%M:%S %z' - if kw == b'MTime': - try: - m_time_zsync = datetime.strptime(time_string, time_format).astimezone(None) - except ValueError: - logger.warning(f'zsync time "{time_string}" does not match "{time_format}"') - elif kw == b'SHA-1': - checksum_zsync = str(val, encoding='utf-8') - elif kw == b'Filename': - filename = str(val, encoding='utf-8') - - except ValueError: - # stop when empty line is reached - break - - return m_time_zsync, checksum_zsync, filename - - @staticmethod - def get_appimage_info(filename: str): - if filename is None: - return None, None - - if not exists(filename): - return None, None - - stat_mtime = stat(filename).st_mtime - m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0) - with open(filename, 'rb') as f: - checksum_file = hashlib.sha1(f.read()).hexdigest() - if checksum_file is None: - logger.warning('No checksum for AppImage calculated') - - return m_time_file, checksum_file - - @staticmethod - def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]: - m_time_zsync, checksum_zsync, appname = Updater.get_zsync() - m_time_file, checksum_file = Updater.get_appimage_info(filename) - - logger.debug(f'zsync information {m_time_zsync}, {checksum_zsync}, {appname}') - logger.debug(f'file information {m_time_file}, {checksum_file}') - - if not ((checksum_file is not None) and (checksum_zsync is not None)): - return None, m_time_file, m_time_zsync - else: - return checksum_file != checksum_zsync, m_time_file, m_time_zsync diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 487c3c4..acc4a5b 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -33,7 +33,7 @@ from ..math.smooth import QSmooth from ..nmr.coupling_calc import QCoupCalcDialog from ..nmr.t1_from_tau import QRelaxCalc from .._py.basewindow import Ui_BaseWindow -from ..lib.utils import UpdateDialog +from ..lib.update import UpdateDialog class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): -- 2.39.2 From b33b211b7ba6030f02e3537d5ef0217e76652083 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 3 Jan 2024 13:28:23 +0100 Subject: [PATCH 6/6] cleanup --- bin/evaluate.py | 13 ++++++++----- src/gui_qt/lib/backup.py | 3 +-- src/gui_qt/lib/update.py | 4 ++-- src/gui_qt/main/mainwindow.py | 11 +++++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/bin/evaluate.py b/bin/evaluate.py index 7ba36a6..0478b37 100755 --- a/bin/evaluate.py +++ b/bin/evaluate.py @@ -17,31 +17,35 @@ from nmreval.lib.logger import handle_exception sys.excepthook = handle_exception from gui_qt import App -from gui_qt.Qt import QtCore, QtWidgets, QtGui +from gui_qt.Qt import QtCore -app = App(['Team Rocket FTW!']) +app = App(['NMReval']) from gui_qt.main.mainwindow import NMRMainWindow from gui_qt.lib.backup import BackupManager -from gui_qt.lib.update import UpdateDialog def do_autosave(): + # autosave and update timestamp in db success = mplQt.autosave() if success: backuping.update_last_save() - +# autosave stuff: keep track of instance and their backup files backuping = BackupManager() +# look for autosaves in DB without running programs files = backuping.search_unsaved() +# tell everyone what autosave files belongs to this process pid = os.getpid() bck_name = backuping.create_entry(pid) mplQt = NMRMainWindow(bck_file=bck_name) +# one manual autosave to create the file do_autosave() +# load all selected autosaves to program for f in files: mplQt.management.load_files(f) f.unlink() @@ -54,5 +58,4 @@ app.aboutToQuit.connect(backuping.close) mplQt.show() - sys.exit(app.exec()) diff --git a/src/gui_qt/lib/backup.py b/src/gui_qt/lib/backup.py index 3030f70..29ed6f3 100644 --- a/src/gui_qt/lib/backup.py +++ b/src/gui_qt/lib/backup.py @@ -6,7 +6,7 @@ from pathlib import Path from nmreval.configs import config_paths from ..Qt import QtCore, QtWidgets -DB_FILE = '/autohome/dominik/nmreval/nmreval.db' +DB_FILE = '/tmp/nmreval.db' class BackupManager(QtCore.QObject): @@ -119,7 +119,6 @@ class BackupManager(QtCore.QObject): Path(DB_FILE).unlink() def close(self): - print('close db') self.remove_file() self.delete_db_if_empty() diff --git a/src/gui_qt/lib/update.py b/src/gui_qt/lib/update.py index bb5347a..4534693 100644 --- a/src/gui_qt/lib/update.py +++ b/src/gui_qt/lib/update.py @@ -74,7 +74,7 @@ class UpdateDialog(QtWidgets.QDialog): label_text += '

Could not determine if this version is newer, please update manually (if necessary).

' dialog_bttns = QtWidgets.QDialogButtonBox.Close elif is_updateble: - label_text += '

Newer version available. Press Ok to download new version, Cancel to ignore.

' + label_text += '

Different version available. Press Ok to download this version, Cancel to ignore.

' dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel else: label_text += '

Version may be already up-to-date.

' @@ -136,7 +136,7 @@ class QRestartWindow(QtWidgets.QMessageBox): self.setIcon(QtWidgets.QMessageBox.Warning) else: self.setText('Download completed!') - self.setInformativeText("Press Restart to use new version now") + self.setInformativeText("Press Restart to use new AppImage") self.setDetailedText(f'Location of AppImage: {file_loc}') self.setIcon(QtWidgets.QMessageBox.Information) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index acc4a5b..39fdbea 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -82,6 +82,9 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): ) self.__backup_path = pathlib.Path(bck_file) + if os.getenv('APPIMAGE'): + # ignore AppImages if not running from AppImage + self.look_for_update() def _init_gui(self): self.setupUi(self) @@ -1017,9 +1020,10 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @staticmethod @QtCore.pyqtSlot(name='on_actionDocumentation_triggered') def open_doc(): - docpath = '/autohome/dominik/auswerteprogramm3/doc/_build/html/index.html' - import webbrowser - webbrowser.open(docpath) + pass + # docpath = '/autohome/dominik/auswerteprogramm3/doc/_build/html/index.html' + # import webbrowser + # webbrowser.open(docpath) def dropEvent(self, evt): if evt.mimeData().hasUrls(): @@ -1106,7 +1110,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): QLog(parent=self).show() def autosave(self) -> bool: - # TODO better separate thread may it takes some time to save self.status.setText('Autosave...') success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0')) -- 2.39.2