Skip to content

Commit 93c043c

Browse files
authored
Merge pull request #1 from Pyenb:feature/configs
Feature/configs
2 parents a9e9353 + a86c8c6 commit 93c043c

7 files changed

Lines changed: 169 additions & 81 deletions

File tree

.gitignore

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
build/
22
dist/
3-
app.log
4-
Keychron_mice_updater.spec
5-
innosetup/Keychron_mice_updater_setup.exe
6-
innosetup/Keychron_mice_updater_setup.zip
3+
upx/
4+
5+
*.log
6+
*.spec
7+
*.json
8+
9+
innosetup/*.exe
10+
innosetup/*.zip

Keychron_mice_updater.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

build.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pyinstaller --onefile --noconsole Keychron_mice_updater.py
1+
pyinstaller --onefile --noconsole -n "Keychron mice updater" -i images/logo.ico --upx-dir=upx/ updater.py

images/logo.ico

8.09 KB
Binary file not shown.

innosetup/setup.iss

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1+
#define AppName "Keychron mice updater"
2+
#define ExeName AppName + ".exe"
3+
#define Version "1.1"
4+
#define AppPublisher "Pyenb"
5+
16
[Setup]
2-
AppName=Keychron Mice Software Updater
3-
AppVersion=1.0
4-
DefaultDirName={commonpf}\Keychron Mice Software Updater
5-
DefaultGroupName=Keychron Mice Software Updater
6-
UninstallDisplayIcon={app}\keychron.exe
7+
AppName={#AppName}
8+
AppVersion={#Version}
9+
AppPublisher={#AppPublisher}
10+
DefaultDirName={commonpf}\{#AppName}
11+
DefaultGroupName={#AppName}
12+
UninstallDisplayIcon={app}\{#ExeName}
713
OutputDir=.
8-
OutputBaseFilename=Keychron_mice_updater_setup
14+
OutputBaseFilename="{#AppName} SETUP"
915
Compression=lzma
1016
SolidCompression=yes
17+
SetupIconFile=..\images\logo.ico
1118

1219
[Files]
13-
Source: "..\dist\Keychron_mice_updater.exe"; DestDir: "{app}"; Flags: ignoreversion
20+
Source: "..\dist\{#ExeName}"; DestDir: "{app}"; Flags: ignoreversion
1421

1522
[Icons]
16-
Name: "{group}\Keychron Mice Software Updater"; Filename: "{app}\Keychron_mice_updater.exe"
17-
Name: "{group}\Uninstall Keychron Mice Software Updater"; Filename: "{uninstallexe}"
18-
Name: "{commonstartup}\Keychron Mice Software Updater"; Filename: "{app}\Keychron_mice_updater.exe"; Tasks: autostart
23+
Name: "{group}\{#AppName}"; Filename: "{app}\{#ExeName}"
24+
Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}"
25+
Name: "{commonstartup}\{#AppName}"; Filename: "{app}\{#ExeName}"; Tasks: autostart
1926

2027
[Run]
21-
Filename: "{app}\Keychron_mice_updater.exe"; Description: "Launch the application"; Flags: nowait postinstall skipifsilent
28+
Filename: "{app}\{#ExeName}"; Description: "Launch the application"; Flags: nowait postinstall skipifsilent
2229

2330
[Tasks]
2431
Name: "autostart"; Description: "Start the application when Windows starts"; GroupDescription: "Additional tasks"; Flags: checkedonce

readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ A: The executable is so big because it contains the python interpreter and all t
7777

7878
A: I used [Inno Setup](https://jrsoftware.org/isinfo.php) to create the installer. This is because I wanted to make the installation process as easy as possible. Also it makes uninstalling and starting the software on startup easier.
7979

80+
**Q: Why does the software need admin privileges?**
81+
82+
A: The official Keychron software needs the admin privileges to install their software.
83+
8084
## Thanks to
8185

8286
- [wkentaro](https://github.com/wkentaro) for making [gdown](https://github.com/wkentaro/gdown)

updater.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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

Comments
 (0)