import sys from os import getenv, stat import hashlib import subprocess from datetime import datetime from contextlib import contextmanager import requests from numpy import linspace from scipy.interpolate import interp1d from gui_qt.Qt import QtGui, QtWidgets, QtCore @contextmanager def busy_cursor(): try: cursor = QtGui.QCursor(QtCore.Qt.ForbiddenCursor) QtWidgets.QApplication.setOverrideCursor(cursor) yield finally: QtWidgets.QApplication.restoreOverrideCursor() class RdBuCMap: # taken from Excel sheet from colorbrewer.org _rdbu = [ (103, 0, 31), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172), (5, 48, 97) ] def __init__(self, vmin=-1., vmax=1.): self.min = vmin self.max = vmax self.spline = [interp1d(linspace(self.max, self.min, num=11), [rgb[i] for rgb in RdBuCMap._rdbu]) for i in range(3)] def color(self, val: float): if val > self.max: col = QtGui.QColor.fromRgb(*RdBuCMap._rdbu[0]) elif val < self.min: col = QtGui.QColor.fromRgb(*RdBuCMap._rdbu[-1]) else: col = QtGui.QColor.fromRgb(*(float(self.spline[i](val)) for i in range(3))) return col class UpdateDialog(QtWidgets.QDialog): host = 'mirror.infra.pkm' bucket = 'nmreval' version = 'NMReval-latest-x86_64' 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._url_zsync = f'http://{self.host}/{self.bucket}/{self.version}.AppImage.zsync' self.process = QtCore.QProcess(self) self.look_for_updates() def _init_ui(self): self.setWindowTitle('Updates') layout = QtWidgets.QVBoxLayout() self.label = QtWidgets.QLabel() layout.addWidget(self.label) layout.addSpacing(10) self.dialog_button = QtWidgets.QDialogButtonBox() self.dialog_button.accepted.connect(self.accept) self.dialog_button.rejected.connect(self.reject) layout.addWidget(self.dialog_button) self.setLayout(layout) def look_for_updates(self): # Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage m_time_zsync, checksum_zsync = self.get_zsync() m_time_file, checksum_file = self.get_appimage_info() if m_time_zsync is None: label_text = '
Retrieval of version information failed.
' \ 'Please try later (or complain to people that it does not work).
' dialog_bttns = QtWidgets.QDialogButtonBox.Close else: label_text = f'Found most recent update: {m_time_zsync.strftime("%d %B %Y %H:%M")}
' if m_time_file is None: label_text += 'No AppImage file found, press Ok to downlaod 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")}
' if not ((checksum_file is not None) and (checksum_zsync is not None)): label_text += 'Could not determine if this version is newer, please update manually (if necessary).' dialog_bttns = QtWidgets.QDialogButtonBox.Close elif checksum_file != checksum_zsync: label_text += f'Newer version available. Update?
' dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel else: label_text += f'Version is already the newest
' dialog_bttns = QtWidgets.QDialogButtonBox.Close self.label.setText(label_text) self.dialog_button.setStandardButtons(dialog_bttns) def get_zsync(self): m_time_zsync = None checksum_zsync = None zsync_file = None try: response = requests.get(self._url_zsync) if response.status_code == requests.codes['\o/']: zsync_file = response.content except Exception as e: pass if zsync_file is not None: for line in zsync_file.split(b'\n'): try: kw, val = line.split(b': ') if kw == b'MTime': m_time_zsync = datetime.strptime(str(val, encoding='utf-8'), '%a, %d %b %Y %H:%M:%S %z').astimezone(None) elif kw == b'SHA-1': checksum_zsync = str(val, encoding='utf-8') except ValueError: # stop when empty line is reached break return m_time_zsync, checksum_zsync def get_appimage_info(self): if self._appfile is None: return None, None stat_mtime = stat(self._appfile).st_mtime m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0) with open(self._appfile, 'rb') as f: checksum_file = hashlib.sha1(f.read()).hexdigest() return m_time_file, checksum_file def update_appimage(self): if self._appfile is None: args = [self._url_zsync] else: args = ['-i', self._appfile, self._url_zsync] self.process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput) self.process.readyReadStandardError.connect(self.onReadyReadStandardOutput) self.process.start('zsync', args) if not self.process.waitForFinished(): return False return True def onReadyReadStandardOutput(self): result = self.process.readAllStandardOutput().data().decode() self.label.setText(result) def accept(self): if self.update_appimage(): _ = QtWidgets.QMessageBox.information(self, 'Updates', 'Download finished. Execute AppImage for new version.') super().accept() def open_bug_report(): form_entries = { 'description': 'Please state the nature of the medical emergency.', 'title': 'Everything is awesome?', 'assign[0]': 'dominik', 'subscribers[0]': 'dominik', 'tag': 'nmreval', 'priority': 'normal', 'status': 'open', } full_url = 'https://chaos3.fkp.physik.tu-darmstadt.de/maniphest/task/edit/?' for k, v in form_entries.items(): full_url += f'{k}={v}&' full_url.replace(' ', '+') import webbrowser webbrowser.open(full_url) if __name__ == '__main__': app = QtWidgets.QApplication([]) w = UpdateDialog() w.show() sys.exit(app.exec())