1+ import requests , gdown , zipfile , os , ctypes , tempfile , shutil , subprocess , sys , json , logging
2+ from tkinter import messagebox , filedialog
3+
4+ def terminate ():
5+ logger .error ("Terminating the program" )
6+ sys .exit ()
7+
8+ def get_appdata_path ():
9+ appdata_path = os .getenv ('APPDATA' )
10+ folder_path = os .path .join (appdata_path , 'Keychron mice updater' )
11+
12+ if not os .path .exists (folder_path ):
13+ os .makedirs (folder_path )
14+
15+ return folder_path
16+
17+ def setup_logger ():
18+ folder_path = get_appdata_path ()
19+ log_file_path = os .path .join (folder_path , 'updater.log' )
20+
21+ logging .basicConfig (
22+ filename = log_file_path ,
23+ format = '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)d - %(message)s' ,
24+ level = logging .INFO ,
25+ encoding = 'utf-8'
26+ )
27+
28+ logger = logging .getLogger (__name__ )
29+ return logger
30+
31+ logger = setup_logger ()
32+
33+ def config_manager ():
34+ folder_path = get_appdata_path ()
35+ config_file = os .path .join (folder_path , "config.json" )
36+
37+ if os .path .exists (config_file ):
38+ with open (config_file , "r" ) as f :
39+ install_path = json .load (f )["install_path" ]
40+ if os .path .exists (os .path .join (install_path , 'config.xml' )):
41+ return install_path
42+ else :
43+ logger .warning ('config.xml not found in install_path' )
44+
45+ install_path = get_install_path ()
46+ with open (config_file , "w" ) as f :
47+ json .dump ({"install_path" : install_path }, f )
48+ logger .info ('install_path written to config.json' )
49+
50+ return install_path
51+
52+ def get_install_path ():
53+ default_path = r"C:\Program Files (x86)\Keychron"
54+ if not os .path .exists (os .path .join (default_path , 'config.xml' )):
55+ logger .warning (f"Keychron software is not installed in the default location: { default_path } " )
56+ messagebox .showerror ("Error" , f"Keychron software is not installed in the default location: { default_path } " )
57+ messagebox .showinfo ("Info" , "Please select the Keychron software installation folder" )
58+ install_path = filedialog .askdirectory ().replace ("/" , "\\ " )
59+ logger .info (f"User selected installation folder: { install_path } " )
60+ return install_path
61+ return default_path
62+
63+ def get_installed_version (install_path ):
64+ try :
65+ with open (install_path + "\\ config.xml" ) as f :
66+ installed_version = f .read ().split ('<software caption="Keychron" version="' )[1 ].split ('"' )[0 ]
67+ logger .info (f"Installed version found: { installed_version } " )
68+ return installed_version
69+ except Exception as e :
70+ logger .error (f"Failed to get installed version: { e } " )
71+ messagebox .showerror ("Error" , f"Failed to get installed version: { e } " )
72+ terminate ()
73+
74+ def get_online_version_and_url ():
75+ try :
76+ download_site = requests .get ("https://www.keychron.com/pages/learn-more-how-to-use-keychron-mouse-software" ).text
77+ download_id = download_site .split ('drive.google.com/file/d/' )[1 ].split ('/' )[0 ].strip ()
78+ download_url = f"https://drive.google.com/uc?id={ download_id } "
79+ logging .info (f"Download url obtained: { download_url } " )
80+
81+ online_version = download_site .splitlines ()
82+ for line in online_version :
83+ if "Version" in line and "updated on" in line :
84+ online_version = line .split ('Version ' )[1 ].split (' ' )[0 ].strip ()
85+ logging .info (f"Online version obtained: { online_version } " )
86+ break
87+
88+ return online_version , download_url
89+ except Exception as e :
90+ logging .error (f"Failed to get online version and url: { e } " )
91+ messagebox .showerror ("Error" , f"Failed to get online version and url: { e } " )
92+ terminate ()
93+
94+ def download_and_extract_file (download_url , tmp_path ):
95+ try :
96+ gdown .download (download_url , tmp_path + '\\ Keychron.zip' , quiet = True )
97+ with zipfile .ZipFile (tmp_path + '\\ Keychron.zip' , 'r' ) as zip_ref :
98+ zip_ref .extractall (tmp_path )
99+ except Exception as e :
100+ logging .error (f"Failed to download and extract file: { e } " )
101+ messagebox .showerror ("Error" , f"Failed to download and extract file: { e } " )
102+ terminate ()
103+
104+ def run_exe (tmp_path ):
105+ try :
106+ for root , dirs , files in os .walk (tmp_path ):
107+ for file in files :
108+ if file .endswith (".exe" ):
109+ process = subprocess .Popen ([os .path .join (root , file )], shell = True )
110+ process .wait ()
111+ except Exception as e :
112+ logger .error (f"Failed to run exe: { e } " )
113+ messagebox .showerror ("Error" , f"Failed to run exe: { e } " )
114+ terminate ()
115+
116+ def main ():
117+ logger .info ("Starting the updater" )
118+ install_path = config_manager ()
119+ try :
120+ installed_version = get_installed_version (install_path )
121+ online_version , download_url = get_online_version_and_url ()
122+
123+ if installed_version != online_version :
124+ MessageBox = ctypes .windll .user32 .MessageBoxW
125+ result = MessageBox (None , f'Version { online_version } of the Keychron software is available. Do you want to download it?' , 'New version available' , 1 )
126+ if result == 1 :
127+ tmp_path = tempfile .mkdtemp ()
128+ download_and_extract_file (download_url , tmp_path )
129+ run_exe (tmp_path )
130+ shutil .rmtree (tmp_path )
131+ except Exception as e :
132+ logging .error (f"An error occurred in main function: { e } " )
133+ messagebox .showerror ("Error" , f"An error occurred in main function: { e } " )
134+ terminate ()
135+ logger .info ("Updater finished" )
136+
137+ if __name__ == "__main__" :
138+ main ()
0 commit comments