import os import sys import urllib.request import urllib.error import hashlib import json import wx import subprocess import time import psutil MAIN_APP = "YourApp.exe" def file_hash(path): """ Calculate and return the SHA-1 hash of the specified file. """ h = hashlib.sha1() with open(path, 'rb') as f: while chunk := f.read(8192): h.update(chunk) return h.hexdigest() def load_remote_manifest(): """ Load and return the remote manifest file as a JSON object. Exit the program if any network or HTTP error occurs. """ try: with urllib.request.urlopen(MANIFEST_URL) as response: if response.status == 404: raise Exception(f"Manifest file not found: {MANIFEST_URL}") return json.load(response) except urllib.error.HTTPError as e: if e.code == 404: wx.MessageBox(f"Error 404: Manifest not found at {MANIFEST_URL}", "File Not Found", wx.OK | wx.ICON_ERROR) else: wx.MessageBox(f"HTTP error: {e.code} - {e.reason}", "HTTP Error", wx.OK | wx.ICON_ERROR) sys.exit(0) except urllib.error.URLError as e: wx.MessageBox(f"Network error: {str(e)}\nPlease check your internet connection and try again.", "Network Error", wx.OK | wx.ICON_ERROR) sys.exit(0) except Exception as e: wx.MessageBox(f"An error occurred: {str(e)}", "Error", wx.OK | wx.ICON_ERROR) sys.exit(0) def download_file(filename, on_progress=None): """ Download a file from the remote URL. Optionally updates a progress callback. """ url = BASE_DOWNLOAD_URL + filename try: with urllib.request.urlopen(url) as response: total_size = int(response.headers.get("Content-Length", 0)) downloaded = 0 with open(filename, 'wb') as out_file: while True: chunk = response.read(8192) if not chunk: break out_file.write(chunk) downloaded += len(chunk) if on_progress and total_size > 0: percent = int(downloaded / total_size * 100) on_progress(percent) except urllib.error.URLError as e: wx.MessageBox(f"Network error: {str(e)}\nPlease check your internet connection and try again.", "Network Error", wx.OK | wx.ICON_ERROR) sys.exit(0) except Exception as e: wx.MessageBox(f"Download error: {str(e)}", "Download Error", wx.OK | wx.ICON_ERROR) sys.exit(0) def terminate_main_app(): """ Terminates the main application process if it is currently running. """ try: for proc in psutil.process_iter(['pid', 'name']): if proc.info['name'] and proc.info['name'].lower() == MAIN_APP.lower(): try: proc.terminate() proc.wait(timeout=5) except psutil.NoSuchProcess: pass except Exception as e: wx.MessageBox(f"Failed to terminate {MAIN_APP}: {str(e)}", "Error", wx.OK | wx.ICON_ERROR) sys.exit(0) except Exception as e: wx.MessageBox(f"Error while checking for {MAIN_APP}: {str(e)}", "Error", wx.OK | wx.ICON_ERROR) sys.exit(0) class NoUpdateDialog(wx.Dialog): """ Dialog shown when no update is available. """ def __init__(self, parent): super().__init__(parent, title="Updater", size=(300, 120)) panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) label = wx.TextCtrl(panel, value="No updates available. Come back again later!", style=wx.TE_READONLY | wx.BORDER_NONE ) label.SetBackgroundColour(panel.GetBackgroundColour()) label.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) label.SetName("Update info") vbox.Add(label, 0, wx.ALL | wx.EXPAND, 15) close_btn = wx.Button(panel, label="Close") close_btn.Bind(wx.EVT_BUTTON, self.on_close) vbox.Add(close_btn, 0, wx.ALL | wx.CENTER, 10) panel.SetSizer(vbox) def on_close(self, event): """ Closes the dialog when the button is clicked. """ self.Close() def get_remote_file_size(filename): """ Returns the size of a remote file in bytes. """ url = BASE_DOWNLOAD_URL + filename with urllib.request.urlopen(url) as response: return int(response.headers.get("Content-Length", 0)) class UpdateDialog(wx.Dialog): """ Dialog that displays changelog and allows user to choose files to update. """ def __init__(self, parent, changelog, files): super().__init__(parent, title="Update Available", size=(550, 600)) panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) changelog_area = wx.TextCtrl(panel, value=changelog, style=wx.TE_READONLY | wx.TE_MULTILINE, size=(500, 200)) vbox.Add(wx.StaticText(panel, label="Changelog:"), 0, wx.ALL, 10) vbox.Add(changelog_area, 0, wx.ALL, 10) vbox.Add(wx.StaticText(panel, label="Select files to update:"), 0, wx.ALL, 10) filelist_box = wx.BoxSizer(wx.VERTICAL) self.update_checkboxes = [] for file in files: size = self.format_size(get_remote_file_size(file)) checkbox = wx.CheckBox(panel, label=file) checkbox.SetValue(True) item_hbox = wx.BoxSizer(wx.HORIZONTAL) item_hbox.Add(checkbox, 0, wx.ALIGN_CENTER_VERTICAL) item_hbox.Add(wx.StaticText(panel, label=size), 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) filelist_box.Add(item_hbox, 0, wx.ALL, 5) self.update_checkboxes.append((checkbox, file)) vbox.Add(filelist_box, 1, wx.EXPAND | wx.ALL, 10) hbox = wx.BoxSizer(wx.HORIZONTAL) update_button = wx.Button(panel, label="&Update Now") update_button.Bind(wx.EVT_BUTTON, self.on_update_files) hbox.Add(update_button, 0, wx.ALL, 5) cancel_button = wx.Button(panel, label="&Cancel") cancel_button.Bind(wx.EVT_BUTTON, self.on_cancel) hbox.Add(cancel_button, 0, wx.ALL, 5) vbox.Add(hbox, 0, wx.ALIGN_CENTER) self.progress = wx.Gauge(panel, range=100, size=(500, 20)) vbox.Add(wx.StaticText(panel, label="Progress:"), 0, wx.LEFT | wx.TOP, 10) vbox.Add(self.progress, 0, wx.ALL | wx.EXPAND, 10) panel.SetSizer(vbox) def format_size(self, size): """ Convert a size in bytes to a human-readable string. """ for unit in ['B', 'KB', 'MB', 'GB']: if size < 1024: return f"{size:.1f} {unit}" size /= 1024 return f"{size:.1f} TB" def on_update_files(self, event): """ Download selected update files and relaunch the main application. """ terminate_main_app() try: for checkbox, filename in self.update_checkboxes: if checkbox.GetValue(): download_file(filename, on_progress=self.progress.SetValue) self.progress.SetValue(0) self.Close() os.startfile(MAIN_APP) sys.exit(0) except Exception as e: wx.MessageBox(f"Update failed:\n{str(e)}", "Update Error", wx.OK | wx.ICON_ERROR) def on_cancel(self, event): """ Cancel the update and exit the application. """ self.Close() sys.exit(0) def set_base_urls(base_url): """ Set global URLs used for manifest, changelog, and downloads. """ global BASE_URL, MANIFEST_URL, CHANGELOG_URL, BASE_DOWNLOAD_URL BASE_URL = base_url.rstrip('/') + '/' MANIFEST_URL = BASE_URL + "manifest.json" CHANGELOG_URL = BASE_URL + "changelog.txt" BASE_DOWNLOAD_URL = BASE_URL def main(): """ Entry point for the updater. Checks for updates and launches the appropriate dialog. """ if len(sys.argv) < 2: print("Usage: python3 updater.py ") sys.exit(0) set_base_urls(sys.argv[1]) try: app = wx.App(False) manifest = load_remote_manifest() manifest.pop("manifest.json", None) to_update = [ fname for fname, remote_hash in manifest.items() if not os.path.exists(fname) or file_hash(fname) != remote_hash ] if not to_update: dialog = NoUpdateDialog(None) dialog.ShowModal() dialog.Destroy() sys.exit(0) else: with urllib.request.urlopen(CHANGELOG_URL) as res: changelog = res.read().decode() dialog = UpdateDialog(None, changelog, to_update) dialog.ShowModal() app.MainLoop() except Exception as e: print(f"Updater Error: {e}") if __name__ == "__main__": main()