Skip to content

Commit 27eb61b

Browse files
author
Binity Bot
committed
fix(updater): improve robustness and UI consistency (v3.3.1)
1 parent 09a8e09 commit 27eb61b

8 files changed

Lines changed: 359 additions & 28 deletions

File tree

release_helper.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@
1212
BUILD_CMD = 'pyinstaller --noconsole --onefile --icon=icons/bin_full.ico --add-data "icons;icons" --add-data "sounds;sounds" --name "Binity" main.py'
1313

1414
RELEASES = [
15+
{
16+
"tag": "v3.3.1",
17+
"prev": "v3.3.0",
18+
"name": "Binity v3.3.1",
19+
"body": """## 🛠️ Updater Stability & UI Polish (v3.3.1)
20+
Update focused on making the auto-updater rock solid and improving visual consistency.
21+
22+
### 🔄 Updater Improvements
23+
- **Robustness**: Updater script now has fallback mechanisms. If replacing the EXE fails (e.g. locked file), it runs from a staging area to ensure the app still launches.
24+
- **Fail-Safe**: New handshake system ensures the new version started successfully; otherwise, it reverts or retries.
25+
- **Visibility**: Added a progress bar dialog during download so you know what's happening.
26+
- **Logs**: Updater now writes to `update.log` in the local app data folder for easier troubleshooting.
27+
28+
### 🎨 UI & Polish
29+
- **Icons**: Fixed missing icons in dialogs (e.g. "Already running", "Confirm Delete") to ensure no empty window headers.
30+
- **Release Notes**: Cleaned up the "What's New" text in the update dialog to remove raw Markdown symbols (###, **) for a cleaner look.
31+
32+
### 📝 Notes
33+
- This release ensures seamless updates for future versions.
34+
"""
35+
},
1536
{
1637
"tag": "v3.3.0",
1738
"prev": "v3.2.1",

src/core/updater.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ def _cleanup_runtime_leftovers(self) -> None:
348348
try:
349349
update_dir = self._update_dir()
350350
if update_dir.exists():
351-
for pattern in ("next-*.exe", "*.tmp", "*.old"):
351+
for pattern in ("next-*.exe", "ready-*.flag", "*.tmp", "*.old"):
352352
for path in update_dir.glob(pattern):
353353
try:
354354
path.unlink()
@@ -380,6 +380,8 @@ def apply_update(self, downloaded_exe: Path) -> bool:
380380

381381
script_path = update_dir / "_binity-update.cmd"
382382
flag_file = update_dir / "applied.flag"
383+
log_file = update_dir / "update.log"
384+
ready_file = update_dir / f"ready-{int(current_pid)}.flag"
383385

384386
script_template = """@echo off
385387
setlocal enableextensions
@@ -388,6 +390,9 @@ def apply_update(self, downloaded_exe: Path) -> bool:
388390
set "DOWNLOADED=@@DOWNLOADED_EXE@@"
389391
set "FINAL=@@FINAL_EXE@@"
390392
set "FLAG=@@FLAG_FILE@@"
393+
set "LOG=@@LOG_FILE@@"
394+
set "READY=@@READY_FILE@@"
395+
set "RUN_TARGET="
391396
392397
set "PYINSTALLER_RESET_ENVIRONMENT=1"
393398
set "_MEIPASS2="
@@ -396,6 +401,8 @@ def apply_update(self, downloaded_exe: Path) -> bool:
396401
set "_PYI_PARENT_PROCESS_LEVEL="
397402
set "_PYI_SPLASH_IPC="
398403
404+
call :log Updater started
405+
399406
for /L %%A in (1,1,25) do (
400407
tasklist /FI "PID eq %PID%" 2>NUL | find "%PID%" >NUL
401408
if errorlevel 1 goto wait_done
@@ -405,24 +412,79 @@ def apply_update(self, downloaded_exe: Path) -> bool:
405412
timeout /t 1 /nobreak >NUL
406413
407414
:wait_done
408-
if not exist "%DOWNLOADED%" goto cleanup
415+
if not exist "%DOWNLOADED%" (
416+
call :log Downloaded file not found
417+
goto cleanup
418+
)
419+
420+
set "RUN_TARGET=%DOWNLOADED%"
409421
410422
if /I not "%DOWNLOADED%"=="%FINAL%" (
411423
copy /Y /B "%DOWNLOADED%" "%FINAL%" >NUL
412-
if errorlevel 1 goto cleanup
424+
if errorlevel 1 (
425+
call :log Copy to final location failed, fallback to staged executable
426+
) else (
427+
set "RUN_TARGET=%FINAL%"
428+
)
429+
) else (
430+
set "RUN_TARGET=%FINAL%"
413431
)
414432
415-
start "" "%FINAL%" --show-after-update
416-
echo 1>"%FLAG%"
433+
if "%RUN_TARGET%"=="" (
434+
call :log Empty run target
435+
goto cleanup
436+
)
417437
418-
if /I not "%DOWNLOADED%"=="%FINAL%" (
438+
if not exist "%RUN_TARGET%" (
439+
call :log Run target does not exist: %RUN_TARGET%
440+
goto cleanup
441+
)
442+
443+
call :start_target "%RUN_TARGET%"
444+
if errorlevel 1 (
445+
call :log First launch attempt failed
446+
if /I not "%RUN_TARGET%"=="%DOWNLOADED%" if exist "%DOWNLOADED%" (
447+
call :log Trying fallback launch from staged executable
448+
set "RUN_TARGET=%DOWNLOADED%"
449+
call :start_target "%DOWNLOADED%"
450+
)
451+
)
452+
453+
if exist "%READY%" (
454+
echo 1>"%FLAG%"
455+
) else (
456+
call :log Ready flag was not received
457+
)
458+
459+
if /I "%RUN_TARGET%"=="%FINAL%" if /I not "%DOWNLOADED%"=="%FINAL%" (
419460
del /F /Q "%DOWNLOADED%" >NUL 2>&1
420461
)
421462
463+
call :log Updater finished
464+
422465
:cleanup
423466
(goto) 2>NUL & del "%~f0"
424467
endlocal
425468
exit /b 0
469+
470+
:log
471+
set "MSG=%*"
472+
>>"%LOG%" echo [%date% %time%] %MSG%
473+
exit /b 0
474+
475+
:start_target
476+
set "TARGET=%~1"
477+
if "%TARGET%"=="" exit /b 1
478+
if not exist "%TARGET%" exit /b 1
479+
if exist "%READY%" del /F /Q "%READY%" >NUL 2>&1
480+
481+
call :log Starting: %TARGET%
482+
start "" "%TARGET%" --show-after-update --update-ready-flag "%READY%"
483+
for /L %%R in (1,1,20) do (
484+
if exist "%READY%" exit /b 0
485+
timeout /t 1 /nobreak >NUL
486+
)
487+
exit /b 1
426488
"""
427489

428490
script = (
@@ -432,6 +494,8 @@ def apply_update(self, downloaded_exe: Path) -> bool:
432494
.replace("@@DOWNLOADED_EXE@@", str(downloaded_exe))
433495
.replace("@@FINAL_EXE@@", str(final_exe))
434496
.replace("@@FLAG_FILE@@", str(flag_file))
497+
.replace("@@LOG_FILE@@", str(log_file))
498+
.replace("@@READY_FILE@@", str(ready_file))
435499
)
436500
script_path.write_text(script, encoding="cp866", errors="ignore")
437501

src/main.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import os
55
import sys
66

7+
from PyQt6.QtGui import QIcon
78
from PyQt6.QtWidgets import QApplication, QMessageBox
89

910
from src.core.i18n import I18n
11+
from src.core.resources import resource_path
1012
from src.core.settings import Settings
1113
from src.core.single_instance import acquire_single_instance_lock
1214
from src.ui.tray.tray_app import TrayApp
@@ -29,26 +31,79 @@ def _consume_switch(flag: str) -> bool:
2931
return False
3032

3133

34+
def _consume_arg(flag: str) -> str:
35+
if flag not in sys.argv:
36+
return ""
37+
idx = sys.argv.index(flag)
38+
value = ""
39+
if idx + 1 < len(sys.argv):
40+
value = sys.argv[idx + 1]
41+
del sys.argv[idx:idx + 2]
42+
else:
43+
del sys.argv[idx]
44+
return value
45+
46+
47+
def _write_ready_flag(path: str) -> None:
48+
if not path:
49+
return
50+
try:
51+
folder = os.path.dirname(path)
52+
if folder:
53+
os.makedirs(folder, exist_ok=True)
54+
with open(path, "w", encoding="utf-8") as fh:
55+
fh.write("ready")
56+
except Exception:
57+
pass
58+
59+
60+
def _resolve_app_icon() -> QIcon:
61+
icon = QIcon()
62+
if getattr(sys, "frozen", False):
63+
exe_icon = QIcon(sys.executable)
64+
if not exe_icon.isNull():
65+
icon = exe_icon
66+
67+
if icon.isNull():
68+
fallback_path = resource_path("icons/bin_full.ico")
69+
if os.path.exists(fallback_path):
70+
icon = QIcon(fallback_path)
71+
72+
return icon
73+
74+
3275
def main() -> int:
3376
_set_windows_app_id()
3477
show_after_update = _consume_switch("--show-after-update")
78+
update_ready_flag = _consume_arg("--update-ready-flag")
3579

3680
app = QApplication(sys.argv)
3781
app.setQuitOnLastWindowClosed(False)
3882
app.setApplicationName(__app_name__)
3983

84+
app_icon = _resolve_app_icon()
85+
if not app_icon.isNull():
86+
app.setWindowIcon(app_icon)
87+
4088
settings = Settings()
4189
i18n = I18n(settings.language)
4290

4391
lock = acquire_single_instance_lock()
4492
if lock is None:
45-
QMessageBox.information(None, __app_name__, i18n.tr("already_running"))
93+
msg = QMessageBox()
94+
msg.setIcon(QMessageBox.Icon.Information)
95+
msg.setWindowTitle(__app_name__)
96+
msg.setText(i18n.tr("already_running"))
97+
if not app_icon.isNull():
98+
msg.setWindowIcon(app_icon)
99+
msg.exec()
46100
return 0
47101

48102
app._instance_lock = lock # type: ignore[attr-defined]
49103

50104
tray_app = TrayApp(settings=settings, i18n=i18n, show_after_update=show_after_update)
51105
app._tray_app = tray_app # type: ignore[attr-defined]
106+
_write_ready_flag(update_ready_flag)
52107

53108
return int(app.exec())
54109

src/ui/dialogs/confirm_dialog.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
from __future__ import annotations
1+
from __future__ import annotations
22

33
from PyQt6.QtCore import Qt
4-
from PyQt6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
4+
from PyQt6.QtGui import QIcon
5+
from PyQt6.QtWidgets import QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
56

67
from src.core.i18n import I18n
8+
from src.core.resources import resource_path
79

810

911
class ConfirmDialog(QDialog):
@@ -15,6 +17,14 @@ def __init__(self, i18n: I18n, message_override: str | None = None, parent=None)
1517
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
1618
self.setModal(True)
1719
self.setMinimumWidth(420)
20+
21+
app = QApplication.instance()
22+
app_icon = app.windowIcon() if app else QIcon()
23+
if app_icon.isNull():
24+
app_icon = QIcon(resource_path("icons/bin_full.ico"))
25+
if not app_icon.isNull():
26+
self.setWindowIcon(app_icon)
27+
1828
self.setStyleSheet(
1929
"""
2030
QDialog { background: #171a23; color: #f3f4f6; }

0 commit comments

Comments
 (0)