256 lines
8.8 KiB
Python
256 lines
8.8 KiB
Python
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 <base_url>")
|
|
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()
|