Skip to content

Commit

Permalink
Merge pull request #1 from Pyenb:feature/configs
Browse files Browse the repository at this point in the history
Feature/configs
  • Loading branch information
Pyenb authored Feb 9, 2024
2 parents a9e9353 + a86c8c6 commit 93c043c
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 81 deletions.
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
build/
dist/
app.log
Keychron_mice_updater.spec
innosetup/Keychron_mice_updater_setup.exe
innosetup/Keychron_mice_updater_setup.zip
upx/

*.log
*.spec
*.json

innosetup/*.exe
innosetup/*.zip
65 changes: 0 additions & 65 deletions Keychron_mice_updater.py

This file was deleted.

2 changes: 1 addition & 1 deletion build.bat
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pyinstaller --onefile --noconsole Keychron_mice_updater.py
pyinstaller --onefile --noconsole -n "Keychron mice updater" -i images/logo.ico --upx-dir=upx/ updater.py
Binary file added images/logo.ico
Binary file not shown.
29 changes: 18 additions & 11 deletions innosetup/setup.iss
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
#define AppName "Keychron mice updater"
#define ExeName AppName + ".exe"
#define Version "1.1"
#define AppPublisher "Pyenb"

[Setup]
AppName=Keychron Mice Software Updater
AppVersion=1.0
DefaultDirName={commonpf}\Keychron Mice Software Updater
DefaultGroupName=Keychron Mice Software Updater
UninstallDisplayIcon={app}\keychron.exe
AppName={#AppName}
AppVersion={#Version}
AppPublisher={#AppPublisher}
DefaultDirName={commonpf}\{#AppName}
DefaultGroupName={#AppName}
UninstallDisplayIcon={app}\{#ExeName}
OutputDir=.
OutputBaseFilename=Keychron_mice_updater_setup
OutputBaseFilename="{#AppName} SETUP"
Compression=lzma
SolidCompression=yes
SetupIconFile=..\images\logo.ico

[Files]
Source: "..\dist\Keychron_mice_updater.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\dist\{#ExeName}"; DestDir: "{app}"; Flags: ignoreversion

[Icons]
Name: "{group}\Keychron Mice Software Updater"; Filename: "{app}\Keychron_mice_updater.exe"
Name: "{group}\Uninstall Keychron Mice Software Updater"; Filename: "{uninstallexe}"
Name: "{commonstartup}\Keychron Mice Software Updater"; Filename: "{app}\Keychron_mice_updater.exe"; Tasks: autostart
Name: "{group}\{#AppName}"; Filename: "{app}\{#ExeName}"
Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}"
Name: "{commonstartup}\{#AppName}"; Filename: "{app}\{#ExeName}"; Tasks: autostart

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

[Tasks]
Name: "autostart"; Description: "Start the application when Windows starts"; GroupDescription: "Additional tasks"; Flags: checkedonce
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ A: The executable is so big because it contains the python interpreter and all t

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.

**Q: Why does the software need admin privileges?**

A: The official Keychron software needs the admin privileges to install their software.

## Thanks to

- [wkentaro](https://github.com/wkentaro) for making [gdown](https://github.com/wkentaro/gdown)
Expand Down
138 changes: 138 additions & 0 deletions updater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import requests, gdown, zipfile, os, ctypes, tempfile, shutil, subprocess, sys, json, logging
from tkinter import messagebox, filedialog

def terminate():
logger.error("Terminating the program")
sys.exit()

def get_appdata_path():
appdata_path = os.getenv('APPDATA')
folder_path = os.path.join(appdata_path, 'Keychron mice updater')

if not os.path.exists(folder_path):
os.makedirs(folder_path)

return folder_path

def setup_logger():
folder_path = get_appdata_path()
log_file_path = os.path.join(folder_path, 'updater.log')

logging.basicConfig(
filename=log_file_path,
format='%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)d - %(message)s',
level=logging.INFO,
encoding='utf-8'
)

logger = logging.getLogger(__name__)
return logger

logger = setup_logger()

def config_manager():
folder_path = get_appdata_path()
config_file = os.path.join(folder_path, "config.json")

if os.path.exists(config_file):
with open(config_file, "r") as f:
install_path = json.load(f)["install_path"]
if os.path.exists(os.path.join(install_path, 'config.xml')):
return install_path
else:
logger.warning('config.xml not found in install_path')

install_path = get_install_path()
with open(config_file, "w") as f:
json.dump({"install_path": install_path}, f)
logger.info('install_path written to config.json')

return install_path

def get_install_path():
default_path = r"C:\Program Files (x86)\Keychron"
if not os.path.exists(os.path.join(default_path, 'config.xml')):
logger.warning(f"Keychron software is not installed in the default location: {default_path}")
messagebox.showerror("Error", f"Keychron software is not installed in the default location: {default_path}")
messagebox.showinfo("Info", "Please select the Keychron software installation folder")
install_path = filedialog.askdirectory().replace("/", "\\")
logger.info(f"User selected installation folder: {install_path}")
return install_path
return default_path

def get_installed_version(install_path):
try:
with open(install_path + "\\config.xml") as f:
installed_version = f.read().split('<software caption="Keychron" version="')[1].split('"')[0]
logger.info(f"Installed version found: {installed_version}")
return installed_version
except Exception as e:
logger.error(f"Failed to get installed version: {e}")
messagebox.showerror("Error", f"Failed to get installed version: {e}")
terminate()

def get_online_version_and_url():
try:
download_site = requests.get("https://www.keychron.com/pages/learn-more-how-to-use-keychron-mouse-software").text
download_id = download_site.split('drive.google.com/file/d/')[1].split('/')[0].strip()
download_url = f"https://drive.google.com/uc?id={download_id}"
logging.info(f"Download url obtained: {download_url}")

online_version = download_site.splitlines()
for line in online_version:
if "Version" in line and "updated on" in line:
online_version = line.split('Version ')[1].split(' ')[0].strip()
logging.info(f"Online version obtained: {online_version}")
break

return online_version, download_url
except Exception as e:
logging.error(f"Failed to get online version and url: {e}")
messagebox.showerror("Error", f"Failed to get online version and url: {e}")
terminate()

def download_and_extract_file(download_url, tmp_path):
try:
gdown.download(download_url, tmp_path + '\\Keychron.zip', quiet=True)
with zipfile.ZipFile(tmp_path + '\\Keychron.zip', 'r') as zip_ref:
zip_ref.extractall(tmp_path)
except Exception as e:
logging.error(f"Failed to download and extract file: {e}")
messagebox.showerror("Error", f"Failed to download and extract file: {e}")
terminate()

def run_exe(tmp_path):
try:
for root, dirs, files in os.walk(tmp_path):
for file in files:
if file.endswith(".exe"):
process = subprocess.Popen([os.path.join(root, file)], shell=True)
process.wait()
except Exception as e:
logger.error(f"Failed to run exe: {e}")
messagebox.showerror("Error", f"Failed to run exe: {e}")
terminate()

def main():
logger.info("Starting the updater")
install_path = config_manager()
try:
installed_version = get_installed_version(install_path)
online_version, download_url = get_online_version_and_url()

if installed_version != online_version:
MessageBox = ctypes.windll.user32.MessageBoxW
result = MessageBox(None, f'Version {online_version} of the Keychron software is available. Do you want to download it?', 'New version available', 1)
if result == 1:
tmp_path = tempfile.mkdtemp()
download_and_extract_file(download_url, tmp_path)
run_exe(tmp_path)
shutil.rmtree(tmp_path)
except Exception as e:
logging.error(f"An error occurred in main function: {e}")
messagebox.showerror("Error", f"An error occurred in main function: {e}")
terminate()
logger.info("Updater finished")

if __name__ == "__main__":
main()

0 comments on commit 93c043c

Please sign in to comment.