-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
updater.py
454 lines (390 loc) · 21.4 KB
/
updater.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
"""
This is the module for the Updater program of Temp_Cleaner GUI
Licensed under the same license as Temp_Cleaner GUI
"""
# imports
from tkinter import *
from tkinter import ttk, messagebox, scrolledtext
from customtkinter import *
from sys import argv, exit
import urllib.request
import configparser
from colorama import Style, Fore, init
from translations import *
from PIL import ImageTk, Image
import webbrowser
import os
import threading
try:
# initializing colorama
init(autoreset=True)
except Exception as _errorColoramaInit:
print(f"[ERROR]: updater module: failed to initialize colorama:{_errorColoramaInit}")
pass
# initializing a variable containing the path where program executable is stored.
application_path = ''
# a quick check whether if program is a compiled bundle by pyinstaller or a simple python file.
if getattr(sys, 'frozen', False):
# pyinstaller creates a sys attribute frozen=True during startup of pyinstaller bootloader to indicate
# that pyinstaller has compiled (frozen) this program, then it creates a sys constant _MEIPASS containing the path
# where the executable is found.
application_path = sys._MEIPASS
else: # if program is running as a python script via python terminal
application_path = os.path.dirname(os.path.abspath(__file__))
# a variable containing text file name and path.
# versionFilePath = f'{application_path}\\progver.txt'
# releaseNotesSaveFilePath = f'{application_path}\\releaseNotes.txt'
mrxTCGFilePath = "https://raw.githubusercontent.com/InsertX2k/software-versions/main/tcg.txt"
releaseNotesFilePath = "https://raw.githubusercontent.com/InsertX2k/software-versions/main/release_notes_tcg.txt"
downloadLink = "https://insertx2k.github.io/temp_cleaner_gui/downloads.html"
# a global variable for return values of multithreaded functions.
_RetVal = None
try:
# a variable containing a string of the current version.
versionNumber = configparser.ConfigParser()
versionNumber.read(f"{application_path}\\Config.ini")
currentVersion = str(versionNumber['ProgConfig']['version'])
print(f"[DEBUG]: Current version is:{currentVersion}")
except Exception as _errorReadingFromCFGFile:
print(f"[ERROR]: updater module: Couldn't read from Config.ini due to this error:\n{_errorReadingFromCFGFile}")
# ------------------------
try:
# opening a configparser session for reading from the file 'Config.ini'
GetConfig = configparser.ConfigParser()
GetConfig.read(f"{application_path}\\Config.ini")
# ------------------------
except Exception as _errorReadingCFGParser:
print(f"[ERROR]: updater module: couldn't read from Config.ini file\nerror details are:\n{_errorReadingCFGParser}")
raise SystemExit(255)
# attempts to get the current language for the ui mode of the updater program.
def getCurrentLanguage(currentLanguageStr=GetConfig["ProgConfig"]["languagesetting"]):
"""
Gets the current language from the config file 'Config.ini'
Should return `en` class if the current language is set to en, and so on.
"""
try:
if str(currentLanguageStr) == "en":
return en
elif str(currentLanguageStr) == "ar":
return ar
else:
return en
except Exception as exception_reading_config_file:
messagebox.showerror("An ERROR has occured",f"Couldn't read from 'Config.ini'\nException details:\n{exception_reading_config_file}\nPress OK to close this program")
return None
# ------------------------
def latestVersionNumber():
"""
Gets the latest version number from server, then returns it. (with a timeout of 25 seconds)
If it fails for some reason, it only returns None
"""
global _RetVal
try:
with urllib.request.urlopen(mrxTCGFilePath, timeout=25) as file:
text = file.read().decode('utf-8')[0:6].replace(' ', '').replace('\n', '')
_RetVal = text
return str(text)
except Exception:
print("[ERROR]: couldn't download from the server.")
_RetVal = None
return None
# ------------------------
# Attempting to get the latest version number.
# threading.Thread(target=latestVersionNumber).start()
# latestVerNum = None
# def multiThreadedlatestVer3Up7():
# """
# A function for updating the global variable `latestVerNum` using multithreading to avoid long time imports
# """
# global latestVerNum
# latestVerNum = latestVersionNumber()
# threading.Thread(target=latestVerNum).start()
# ------------------------
# def downloadAndSaveVersionFile():
# """
# A function used to download and save the text file containing the latest version number.
# """
# global versionFilePath, mrxTCGFilePath
# try:
# with urllib.request.urlopen(mrxTCGFilePath) as file:
# text = file.read().decode('utf-8')
# except Exception:
# return False
# try:
# with open(versionFilePath, mode='w', encoding='utf-8') as saveFile:
# saveFile.write(text)
# saveFile.close()
# except Exception:
# return False
# return None
# def downloadAndSaveReleaseNotesFile():
# """
# A function used to download and save the text file containing the release notes.
# """
# global versionFilePath, mrxTCGFilePath, releaseNotesFilePath, releaseNotesSaveFilePath
# try:
# with urllib.request.urlopen(releaseNotesFilePath) as file:
# text = file.read().decode('utf-8')
# except Exception:
# return False
# try:
# with open(releaseNotesSaveFilePath, mode='w', encoding='utf-8') as saveFile:
# saveFile.write(text)
# saveFile.close()
# except Exception as savingDownloadedReleaseNotesFileError:
# return False
# return None
def readVersion():
"""
Reads the version number from the variable text and compares it with the current version.
If the current version is the latest one, it returns None
If there is an update, it should return True
"""
global versionNumber, latestVersionNumber, currentVersion, _RetVal
version = latestVersionNumber()
if version is not None:
if version == currentVersion:
# readFile.close()
print(f"[DEBUG]: Server version is : {str(version)}")
print("You are currently running the latest version of the program. from readVersion func")
return None
else:
# readFile.close()
print(f"[DEBUG]: Server version is : {str(version)}")
print("There is an update available. from readVersion func")
return True
else:
return None
# def readReleaseNotesFromFile():
# """
# Reads the release notes file and returns the text in it.
# """
# global mrxTCGFilePath, releaseNotesFilePath
# try:
# with open(releaseNotesSaveFilePath, 'r', encoding='utf-8') as releaseNotesTextFile:
# releaseNotesTextInMemory = releaseNotesTextFile.read()
# releaseNotesTextFile.close()
# return releaseNotesTextInMemory
# except Exception as reading_release_notes_file_error:
# messagebox.showerror("Runtime error", f"Couldn't read the release notes file due to the error:\n{reading_release_notes_file_error}\nThe program will close.")
# return False
# class for updater program ui window.
class updaterProgramUI(Toplevel):
def __init__(self):
global versionNumber, application_path, downloadLink, readVersion, latestVersionNumber, currentVersion
# a variable containing the latest version number from the server.
_SrvrVerNum = latestVersionNumber()
# a simple check cuz if it is None the window should close.
if _SrvrVerNum == None:
# messagebox.showerror(getCurrentLanguage().updater_title, getCurrentLanguage().couldnt_download)
self.destroy()
else:
pass
super().__init__()
deactivate_automatic_dpi_awareness() # deactivating automatic dpi awareness in updater program.
self.title(getCurrentLanguage().updater_title)
self.attributes('-topmost',True)
try: # attempt to load the icon bitmap for this window.
self.iconbitmap(f"{application_path}\\updater.ico")
except Exception as load_iconbitmap_error:
messagebox.showerror("Runtime error", f"Couldn't load iconbitmap for this window due to the following error\n{load_iconbitmap_error}\nThe window will continue to appear but without an icon bitmap")
pass
# trying to load the style.json file to apply customtkinter styles.
try:
set_default_color_theme(f"{application_path}\\style.json")
except Exception as style_json_file_loader_tryone_error:
print(f"{Fore.RED}{Style.BRIGHT}[ERROR]: couldn't read from file 'style.json' in the current directory due to the error:\n{style_json_file_loader_tryone_error}")
self.destroy()
return # error 299 is for error loading style.json file from current and previous directories.
# ---------------------------
# adjusting window manager related properties
self.geometry('600x300')
self.minsize(600, 300)
self.maxsize(600, 300)
self.resizable(False, False)
# checking if program is running in light or dark mode.
try:
if int(GetConfig["ProgConfig"]['appearancemode']) == 2: # dark mode
self.configure(background='#333')
else:
set_appearance_mode("light")
except Exception as reading_appearanace_mode_error:
messagebox.showerror("Unhandleable Runtime error", f"Couldn't read the appearance mode or apply it due to the following error\n{reading_appearanace_mode_error}\nThe program will exit")
self.destroy()
return
def getCurrentAppearanceMode():
"""
Gets the current appearance mode to apply to the background of the widgets.
Returns a tuple containing the values of text color and background color for widgets.
Order goes like that:
```py
(background_color, text_color)
```
"""
global GetConfig
try:
if str(GetConfig["ProgConfig"]['appearancemode']) == "1": # 1 is for light mode
return (None, 'black')
elif str(GetConfig["ProgConfig"]['appearancemode']) == "2": # 2 is for dark mode
return ('#333', "white")
else:
return (None, "black")
except Exception as exception_reading_appearance_mode:
messagebox.showerror("An ERROR has occured", f"{exception_reading_appearance_mode}")
return False
return False
# declaring variables for dimensions of updater logo
image_width = 160
image_height = 150
# attempting to get the current UI mode to choose the appropriate updater logo
try:
if str(GetConfig["ProgConfig"]['appearancemode']) == '2': # dark mode ui
# loading the 'updater.png' file in memory.
self.updaterLoader = Image.open(f"{application_path}\\updater_dark.png").resize((image_width,image_height), Image.LANCZOS)
self.updaterLoader = ImageTk.PhotoImage(self.updaterLoader)
else: # light mode ui
self.updaterLoader = Image.open(f"{application_path}\\updater.png").resize((image_width,image_height), Image.LANCZOS)
self.updaterLoader = ImageTk.PhotoImage(self.updaterLoader)
except Exception as loading_updater_logo_error:
messagebox.showerror("Runtime error", f"Couldn't load the updater logo due to the following error\n{loading_updater_logo_error}\nThe program will close.")
return
def dontUpdateAndCloseProgram():
"""
The function for "Don't Update" button.
"""
self.destroy() # destroying window
# try:
# os.remove(f"{application_path}\\progver.txt")
# os.remove(f"{application_path}\\releaseNotes.txt")
# except Exception as exception_deleting_unneeded_downloads: # couldn't delete them.
# messagebox.showerror("An error has occured", f"Couldn't delete the unnecessary files 'releaseNotes.txt' and 'progver.txt' due to a runtime error\nError details are:\n{exception_deleting_unneeded_downloads}\n\nIf you are a user and you see this message please create an issue on Temp_Cleaner GUI's official Github page and MAKE SURE to provide a screenshot of this messagebox\nPress OK to continue.")
return None
def openWebBrowserWindow():
"""
Opens a new tab on your default browser that contains the official website for Temp_Cleaner GUI
"""
webbrowser.open_new(downloadLink)
messagebox.showinfo(getCurrentLanguage().update_btn_updater, getCurrentLanguage().opened_webbrowser_dialog)
self.destroy()
# try:
# os.remove(f"{application_path}\\progver.txt")
# os.remove(f"{application_path}\\releaseNotes.txt")
# except Exception as exception_deleting_unneeded_downloads: # couldn't delete them.
# messagebox.showerror("An error has occured", f"Couldn't delete the unnecessary files 'releaseNotes.txt' and 'progver.txt' due to a runtime error\nError details are:\n{exception_deleting_unneeded_downloads}\n\nIf you are a user and you see this message please create an issue on Temp_Cleaner GUI's official Github page and MAKE SURE to provide a screenshot of this messagebox\nPress OK to continue.")
return None
def getReleaseNotes():
"""
A function used to get the content of the release notes file and insert it into the scrolledtext.ScrolledText widget of the release notes.
the timeout for the download packet sent is 25 seconds.
"""
# if downloadAndSaveReleaseNotesFile() == False:
# messagebox.showerror("Runtime exception", "Couldn't read from release notes file")
# return False
# else:
# pass
try:
self.release_notes_widget.configure(state='normal') # i want it to be editable.
self.release_notes_widget.delete(1.0, END) # clearing the release notes dialogbox.
self.release_notes_widget.insert(END, getCurrentLanguage().checking_for_updates)
self.release_notes_widget.configure(state='disabled')
except:
pass
try:
with urllib.request.urlopen(releaseNotesFilePath, timeout=25) as file:
releaseNotesText = file.read().decode('utf-8')
try:
self.release_notes_widget.configure(state='normal')
self.release_notes_widget.delete(1.0, END) # clearing the release notes dialogbox.
self.release_notes_widget.insert(END, releaseNotesText)
self.release_notes_widget.configure(state='disabled')
except:
pass
except Exception:
self.release_notes_widget.configure(state='normal')
self.release_notes_widget.delete(1.0, END) # clearing the release notes dialogbox.
self.release_notes_widget.insert(END, getCurrentLanguage().couldnt_download)
self.release_notes_widget.configure(state='disabled')
return None
# a label for showing the updater icon
self.updaterIcon = Label(self, text='', background=getCurrentAppearanceMode()[0], image=self.updaterLoader)
self.updaterIcon.place(x=0, y=60)
# other informative labels.
self.lbl0 = Label(self, text=getCurrentLanguage().new_update_tcg, background=getCurrentAppearanceMode()[0], foreground=getCurrentAppearanceMode()[1], font=("Arial", 14))
self.lbl0.place(x=170, y=5)
self.lbl1 = Label(self, text=f"{getCurrentLanguage().new_version_is}{_SrvrVerNum}{getCurrentLanguage().current_version_is}{currentVersion}", background=getCurrentAppearanceMode()[0], foreground=getCurrentAppearanceMode()[1], font=("Arial", 9))
self.lbl1.place(x=170, y=35)
self.release_notes_widget = scrolledtext.ScrolledText(self, background=getCurrentAppearanceMode()[0], foreground=getCurrentAppearanceMode()[1], selectbackground='blue', state='disabled', font=("Arial", 10))
self.release_notes_widget.place(x=170, y=55, relwidth=0.7, relheight=0.63)
self.closebtn = CTkButton(self, text=getCurrentLanguage().close_btn_updater, command=dontUpdateAndCloseProgram, cursor='hand2')
self.closebtn.place(x=170, y=260)
self.downloadbtn = CTkButton(self, text=getCurrentLanguage().update_btn_updater, command=openWebBrowserWindow, cursor='hand2')
self.downloadbtn.place(y=260, x=430)
# Checking if there is an available update or not
# if there is one, the window will continue to appear, otherwise, the window will close.
if readVersion() == None:
self.attributes('-topmost',False)
messagebox.showwarning(getCurrentLanguage().update_btn_updater, getCurrentLanguage().running_latest_version_dialog)
self.attributes('-topmost',True)
self.destroy()
return # exiting program
elif readVersion() == False:
self.destroy()
return
else:
pass
# if getReleaseNotes() == False:
# self.destroy()
# return
# else:
# pass
# updating release notes.
threading.Thread(target=getReleaseNotes).start()
# ----------------------
if __name__ == '__main__': # if program wasn't imported as a Python 3.x.x Module file.
if len(argv) == 1:
print(f"[DEBUG]: sys.argv is: {argv}")
# run the gui updater program normally, the program was executed with no arguments.
guiProcess = updaterProgramUI()
guiProcess.mainloop()
raise SystemExit(0) # gui program ran very well.
elif len(argv) == 2:
if str(argv[1]) == "--silent":
# don't inform user if program is on latest version, if it isn't, show the updater ui.
try:
isThereAnyUpdatesAvailable = readVersion()
if isThereAnyUpdatesAvailable == True:
guiProcess = updaterProgramUI()
guiProcess.mainloop()
raise SystemExit(0)
else:
# there are no updates available, don't run gui program.
print(f"{Fore.GREEN}{Style.BRIGHT}[INFO]: you are running the latest version of the program, no need to update it.")
raise SystemExit(0) # exit code 0 means there is no update available.
except Exception as reading_server_version_error:
print(f"{Fore.RED}{Style.BRIGHT}[ERROR]: couldn't download from Mr.X's github server due to an error\nerror details:\n{reading_server_version_error}")
raise SystemExit(20) # exit code 20 is for errors downloading version text file.
raise SystemExit(1111111111) # error code 1111111111 is for unhandled or unexpected statement end
else:
print(f"{Fore.RED}{Style.BRIGHT}[ERROR]: invalid argument: {str(argv[1])}, the only valid option is: --silent")
raise SystemExit(2) # exit code 2 is for invalid argument.
else:
print(f"{Fore.YELLOW}{Style.BRIGHT}[WARNING]: one or more unnecessary command line arguments found\nother options will be ignored'")
if str(argv[1]) == "--silent":
# don't inform user if program is on latest version, if it isn't, show the updater ui.
try:
isThereAnyUpdatesAvailable = readVersion()
if isThereAnyUpdatesAvailable == True:
guiProcess = updaterProgramUI()
guiProcess.mainloop()
raise SystemExit(0)
else:
# there are no updates available, don't run gui program.
print(f"{Fore.GREEN}{Style.BRIGHT}[INFO]: you are running the latest version of the program, no need to update it.")
raise SystemExit(0) # exit code 0 means there is no update available.
except Exception as reading_server_version_error:
print(f"{Fore.RED}{Style.BRIGHT}[ERROR]: couldn't download from Mr.X's github server due to an error\nerror details:\n{reading_server_version_error}")
raise SystemExit(20) # exit code 20 is for errors downloading version text file.
raise SystemExit(1111111111) # error code 1111111111 is for unhandled or unexpected statement end
else:
print(f"{Fore.RED}{Style.BRIGHT}[ERROR]: invalid argument: {str(argv[1])}, the only valid option is: --silent")
raise SystemExit(2) # exit code 2 is for invalid argument.