diff --git a/src/osdag/_version.py b/src/osdag/_version.py index ab6e815b2..5e987e66b 100644 --- a/src/osdag/_version.py +++ b/src/osdag/_version.py @@ -1 +1,2 @@ -__version__ = "2025.01.a.2" +__version__ = "2025.01.a.1" +__installation_type__ = "conda" \ No newline at end of file diff --git a/src/osdag/osdagMainPage.py b/src/osdag/osdagMainPage.py index 0cfc08b51..d1be6d56f 100644 --- a/src/osdag/osdagMainPage.py +++ b/src/osdag/osdagMainPage.py @@ -88,7 +88,7 @@ import time from importlib.resources import files import urllib.request -from PyQt5.QtWidgets import QMessageBox,QApplication, QDialog, QMainWindow +from PyQt5.QtWidgets import QMessageBox,QApplication, QDialog, QMainWindow, QTextEdit from .update_version_check import Update #from Thread import timer from .get_DPI_scale import scale @@ -509,12 +509,74 @@ def selection_change(self): elif loc == "Ask Us a Question": self.ask_question() elif loc == "Check for Update": - update_class = Update() - msg = update_class.notifi() - QMessageBox.information(self, 'Info',msg) + self.updater = Update() + try: + update_avl, msg = self.updater.notifi() + except ConnectionError as e: + QMessageBox.critical(self, "Network Error", str(e)) + return + # QMessageBox.information(self, 'Info',msg) + + box = QMessageBox(self) + box.setIcon(QMessageBox.Information) + box.setWindowTitle("Update Status") + box.setText(msg) + + if update_avl: + update_now_btn = box.addButton("Update Now", QMessageBox.AcceptRole) + later_btn = box.addButton("Update Later", QMessageBox.RejectRole) + else: + ok_btn = box.addButton("OK", QMessageBox.AcceptRole) + + box.exec_() + + if update_avl and box.clickedButton() == update_now_btn: + confirm_update = QMessageBox(self) + confirm_update.setIcon(QMessageBox.Information) + confirm_update.setWindowTitle("Confirm Update") + confirm_update.setText("This may take some time....\nDo you want to continue?") + confirm_update.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + confirm_update.setDefaultButton(QMessageBox.No) + + result = confirm_update.exec_() + if result == QMessageBox.Yes: + self.progress_dialog = QDialog(self) + self.progress_dialog.setWindowTitle("Updating Osdag") + layout = QVBoxLayout() + + self.progress_label = QLabel("Updating, please wait...") + layout.addWidget(self.progress_label) + + self.progress_text = QTextEdit() + self.progress_text.setReadOnly(True) + self.progress_text.verticalScrollBar().setValue( + self.progress_text.verticalScrollBar().maximum() + ) + layout.addWidget(self.progress_text) + + self.progress_dialog.setLayout(layout) + self.progress_dialog.setModal(True) + self.progress_dialog.setWindowFlags(self.progress_dialog.windowFlags() | Qt.WindowStaysOnTopHint) + self.progress_dialog.show() + + self.updater.output_signal.connect(lambda text: self.progress_text.append(text)) + self.updater.finished_signal.connect(self.handle_update_finished) + self.updater.update_to_latest() + # if update_status: + # QMessageBox.information(self, "Update", msg) + # else: + # QMessageBox.warning(self, "Update Failed", msg) + + # elif loc == "FAQ": # pass + def handle_update_finished(self,success, msg): + self.progress_dialog.close() + if success: + QMessageBox.information(self, "Update Completed", msg) + else: + QMessageBox.warning(self, "Update Failed", msg) def select_workspace_folder(self): # This function prompts the user to select the workspace folder and returns the name of the workspace folder diff --git a/src/osdag/update_version_check.py b/src/osdag/update_version_check.py index 312cff3a7..3f9184692 100644 --- a/src/osdag/update_version_check.py +++ b/src/osdag/update_version_check.py @@ -1,55 +1,169 @@ ######################### UpDateNotifi ################ + import urllib.request -import requests import re -from PyQt5.QtWidgets import QMessageBox,QMainWindow -import sys +from pathlib import Path +from packaging.version import Version, InvalidVersion +from PyQt5.QtCore import QObject, QProcess, pyqtSignal +from PyQt5.QtWidgets import QDialog, QTextEdit, QLabel +import subprocess +import sys, os +import json + + +version_file = Path(__file__).parent / "_version.py" +version_var = {} +exec(version_file.read_text(), version_var) +curr_version = version_var["__version__"] +install_type = version_var["__installation_type__"] + + + +class Update(QObject): + output_signal = pyqtSignal(str) + finished_signal = pyqtSignal(bool, str) -class Update(): def __init__(self): super().__init__() - self.old_version=self.get_current_version() - # msg = self.notifi() + self.old_version = curr_version + self.process = QProcess(self) + + def _set_exec_paths(self): + env_path = sys.prefix + env_path = Path(env_path) + base_conda_path = env_path.parents[1] + if sys.platform.startswith("win"): + conda_path = base_conda_path / "Scripts" / "conda.exe" + pixi_path = env_path / "Scripts" / "pixi.exe" + else: + conda_path = base_conda_path / "bin/conda" + pixi_path = env_path / "bin/pixi" + + self.conda_path = str(conda_path) + self.pixi_path = str(pixi_path) - def notifi(self): + def fetch_latest_version(self) -> str: + """Fetch the latest version string from Osdag downloads page.""" + self._set_exec_paths() + if install_type == "conda": + cmd = [self.conda_path, "search", "-c", "conda-forge", "osdag::osdag", "--info", "--json"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + data = json.loads(result.stdout) + except subprocess.CalledProcessError as e: + raise ConnectionError(f"Failed to fetch version info: {e.stderr}") + if data: + versions = data.get("osdag") + if versions: + return sorted(versions, key=lambda x:x["version"])[-1]["version"] + else: return None + + elif install_type == "pixi": + cmd = [self.pixi_path, "search", "osdag", "--channel", "osdag"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + for line in result.stdout.splitlines(): + if line.strip().startswith("Version"): + return line.split()[-1].strip() + + else: return None + + except subprocess.CalledProcessError as e: + raise ConnectionError(f"Failed to fetch version info: {e.stderr}") + + def notifi(self) -> str: + """Compare current version with latest version and return update message.""" try: - url = "https://raw.githubusercontent.com/osdag-admin/Osdag/master/README.md" - file = urllib.request.urlopen(url) - version = 'not found' - for line in file: - decoded_line = line.decode("utf-8") - match = re.search(r'Download the latest release version (\S+)', decoded_line) - if match: - version = match.group(1) - version = version.split("<")[0] - break - # decoded_line = line.decode("utf-8") - # new_version = decoded_line.split("=")[1] - if version != self.old_version: - msg = 'Current version: '+ self.old_version+'
'+'Latest version '+ str(version)+'
'+\ - 'Update will be available here ' + latest_version = self.fetch_latest_version() + if latest_version is None: + return False, "Could not determine latest version." + + latest_version = latest_version.lstrip("v").replace("_", ".") + if Version(latest_version) > Version(self.old_version): + return True, ( + f"Current version: {self.old_version}\n" + f"Latest version: {latest_version}\n" + ) else: - msg = 'Already up to date' - return msg - except: - return "No internet connection" + return False, "Already up to date" + + except InvalidVersion: + return False, "Could not parse version string." + + def update_to_latest(self): + """Run conda update in background using QProcess.""" + try: + latest_version = self.fetch_latest_version() + if latest_version: + latest_version = latest_version.lstrip("v").replace("_", ".") + except Exception as e: + return + try: + # cmd = ["conda", "install", "-y", f"osdag=={latest_version}"] + + if install_type == "conda": + if not Path(self.conda_path).exists(): + self.finished_signal.emit(False, f"conda not found at {self.conda_path}") + return + cmd = [self.conda_path, "update", "-y", "osdag", "--channel", "osdag"] + elif install_type == "pixi": + if not Path(self.pixi_path).exists(): + self.finished_signal.emit(False, f"pixi not found at {self.pixi_path}") + return + cmd = [self.pixi_path, "update", "-y", "osdag", "--channel", "osdag"] - def get_current_version(self): - version_file = "_version.py" - rel_path = str(sys.path[0]) - rel_path = rel_path.replace("\\", "/") - VERSIONFILE = rel_path +'/'+ version_file + # Create QProcess + self.process.setProgram(cmd[0]) + self.process.setArguments(cmd[1:]) - try: - verstrline = open(VERSIONFILE, "rt").read() - except EnvironmentError: - pass # Okay, there is no version file. + # Connect signals for output + self.process.readyReadStandardOutput.connect(self.handle_stdout) + self.process.readyReadStandardError.connect(self.handle_stderr) + self.process.finished.connect(self.handle_finished) + + # Start the process + self.process.start() + + # result = subprocess.run(cmd, capture_output=False, text=True) + # if result.returncode == 0: + # self.finished_signal.emit(True, "Update successful! Please restart Osdag.") + # else: + # self.finished_signal.emit(False, f"Update failed.\nError: {result.stderr}\n" + # "Please retry or run:\nconda install --force-reinstall osdag::osdag") + + + except Exception as e: + self.finished_signal.emit(False, f"Update failed: {e}") + return + + + def handle_stdout(self): + """Read stdout and print it, also emit signal for GUI.""" + if self.process: + output = self.process.readAllStandardOutput().data().decode("utf-8") + self.output_signal.emit(output) + print(output, end="") + # self.progress_text.append(output) + # self.progress_text.verticalScrollBar().setValue( + # self.progress_text.verticalScrollBar().maximum() + # ) + + def handle_stderr(self): + """Read stderr and print it.""" + if self.process: + error = self.process.readAllStandardError().data().decode("utf-8") + self.output_signal.emit(error) + print(error, end="") + # self.output_signal.emit(error) + # self.progress_text.append(error) + # self.progress_text.verticalScrollBar().setValue( + # self.progress_text.verticalScrollBar().maximum() + # ) + + def handle_finished(self, exit_code, exit_status): + """Handle when the process finishes.""" + if exit_code == 0: + self.finished_signal.emit(True, "Update successful! Please restart Osdag.") else: - VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" - mo = re.search(VSRE, verstrline, re.M) - if mo: - verstr = mo.group(1) - return verstr - else: - print("unable to find version in %s" % (VERSIONFILE,)) - raise RuntimeError("if %s.py exists, it is required to be well-formed" % (VERSIONFILE,)) + self.finished_signal.emit(False, f"Update failed with code {exit_code}") + self.process = None