fix: crash GUI su Windows (OverflowError anno 2038) — schede/menu BAL rotti (#3)
* fix(gui): voci di menu BAL duplicate/condensate dopo riavvio o cambio wallet
Sintomo (Windows 11): dopo aver riavviato Electrum o cambiato wallet, le
schede Will/Heirs sparivano dalla tab bar e dal menu, e compariva una voce
di menu condensata/illeggibile (icona + testo sovrapposti) sotto il logo di
Electrum, accanto a 'Portafogli'.
Causa: init_menubar_tools veniva eseguito DUE volte sulla stessa finestra.
Con il plugin gia abilitato, al riavvio Electrum invoca sia l'hook
init_menubar sia il percorso di init a caldo (init_qt -> _setup_window),
entrambi chiamano init_menubar_tools -> addTab/addAction duplicati.
Nell'originale init_qt faceva return (chiedendo il riavvio) e quindi i menu
venivano creati una sola volta; rimuovendo quel return (fix B3) e' emersa la
doppia inizializzazione.
Fix:
- BalWindow._menubar_initialized: guardia di idempotenza.
- init_menubar_tools: se gia inizializzato, esce subito (niente duplicati).
- on_close: resetta il flag dopo aver rimosso tab/azioni, cosi la stessa
finestra puo essere riusata per un altro wallet.
- tests/gui_fixes_test.py: regressione che verifica la guardia in __init__,
init_menubar_tools e on_close.
Logica di business invariata (nessuna modifica a bal/core/*).
* fix(gui): ripristina create_status_bar come no-op (come originale)
L'elemento di menu condensato/illeggibile sotto il logo di Electrum era
causato dal StatusBarButton aggiunto da create_status_bar.
Nell'originale Gitea questo hook aveva un 'return' subito dopo il log, PRIMA
di costruire il bottone: era quindi disabilitato di proposito. Durante la
pulizia del 'dead code' nel refactoring quel return era stato rimosso,
riattivando la creazione del bottone -> elemento icona+testo renderizzato
nel punto sbagliato dopo riavvio/cambio wallet.
Fix: create_status_bar torna a essere un no-op (return), fedele all'originale.
Le impostazioni restano raggiungibili da Strumenti -> Plugin.
Regressione: gui_fixes_test verifica che create_status_bar non chiami
addPermanentWidget.
* fix(core): OverflowError su Windows (anno 2038) che rompeva tab/menu BAL
CAUSA VERA (dal log Electrum dell'utente, Windows 11):
OverflowError: Python int too large to convert to C int
window.py __init__ -> create_heirs_tab -> WillSettingsWidget
-> on_locktime_change -> BalTimestamp.to_date
-> datetime.fromtimestamp(NLOCKTIME_MAX)
Su Windows time_t e' a 32 bit, quindi datetime.fromtimestamp() solleva
OverflowError per qualsiasi timestamp oltre il 2038 (es. NLOCKTIME_MAX =
2**32-1 = 4294967295, usato come locktime di default/sentinella). Su Linux
64-bit la stessa chiamata funziona: per questo il bug si vedeva solo su
Windows e i test su Linux non lo intercettavano.
L'eccezione interrompeva BalWindow.__init__ durante init_menubar/load_wallet,
lasciando le schede Will/Heirs e la voce di menu a meta' costruzione ->
l'elemento grafico condensato/illeggibile sotto il logo di Electrum.
FIX (comportamento invariato per tutti i valori normali):
- BalTimestamp._safe_fromtimestamp(): datetime.fromtimestamp con clamp a
INT32_MAX in caso di OverflowError/OSError/ValueError, esattamente come la
funzione get_max_allowed_timestamp() dell'originale (Electrum issue #6170).
- Usato in to_date / to_timestamp / __str__ / __repr__ di BalTimestamp.
- widgets.py set_value: usa il converter sicuro.
- util.py timestamp_minus: stessa protezione inline con clamp a INT32_MAX.
I valori entro il 2038 (date assolute normali, durate relative come 90d/5y)
producono lo stesso identico risultato di prima.
TEST: tests/windows_overflow_test.py riproduce il limite 32-bit di Windows
(monkeypatch di datetime.fromtimestamp) e dimostra che senza il fix si ottiene
lo stesso OverflowError del log, mentre col fix passa. Verificato anche che il
test FALLISCE senza il fix.
* docs(it): documenta il fix OverflowError Windows (anno 2038) nel changelog
Aggiunge la sezione §13 al CHANGELOG_REFACTOR.md che descrive:
- sintomo (schede/menu rotti su Windows dopo riavvio/cambio wallet)
- causa vera dal log (datetime.fromtimestamp(NLOCKTIME_MAX) -> OverflowError
su time_t 32-bit di Windows)
- fix con _safe_fromtimestamp (clamp a INT32_MAX, come Electrum #6170)
- test di regressione windows_overflow_test.py
Aggiornata anche la cronologia (§12) con PR #3.
---------
Co-authored-by: GenSpark AI Developer <ai@genspark.dev>
This commit is contained in:
committed by
GitHub
parent
c8a98e2ace
commit
3c44a29f84
@@ -398,3 +398,60 @@ Suddivisione del vecchio `qt.py` (3777 righe) nei moduli GUI:
|
|||||||
vita (Fase A).
|
vita (Fase A).
|
||||||
- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download
|
- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download
|
||||||
lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10).
|
lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10).
|
||||||
|
- **PR #3** — fix **OverflowError su Windows (anno 2038)** che rompeva le schede
|
||||||
|
Will/Heirs e la voce di menu (sezione §13).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. CORREZIONE BUG: OverflowError su Windows (limite anno 2038)
|
||||||
|
|
||||||
|
### Sintomo (Windows 11)
|
||||||
|
Dopo aver **riavviato Electrum** o **cambiato wallet**, le schede **Will** e
|
||||||
|
**Heirs** sparivano e compariva una **voce di menu condensata/illeggibile**
|
||||||
|
(icona + testo sovrapposti) sotto il logo di Electrum, accanto a *Portafogli*.
|
||||||
|
Su Linux il problema non si manifestava.
|
||||||
|
|
||||||
|
### Causa vera (dal log di Electrum dell'utente)
|
||||||
|
```
|
||||||
|
OverflowError: Python int too large to convert to C int
|
||||||
|
window.py __init__ -> create_heirs_tab -> WillSettingsWidget
|
||||||
|
-> on_locktime_change -> BalTimestamp.to_date
|
||||||
|
-> datetime.fromtimestamp(NLOCKTIME_MAX)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` viene usato come locktime di
|
||||||
|
**default/sentinella**.
|
||||||
|
- Su **Windows** `time_t` è a **32 bit**, quindi `datetime.fromtimestamp(ts)`
|
||||||
|
solleva **`OverflowError`** per qualsiasi timestamp oltre il **2038**.
|
||||||
|
- Su **Linux 64-bit** la stessa chiamata **funziona**: ecco perché il bug si
|
||||||
|
vedeva solo su Windows e i test su Linux non lo intercettavano.
|
||||||
|
- L'eccezione interrompeva `BalWindow.__init__` durante `init_menubar` /
|
||||||
|
`load_wallet`, lasciando le schede Will/Heirs e la voce di menu **a metà
|
||||||
|
costruzione** → l'elemento grafico condensato/illeggibile sotto il logo.
|
||||||
|
|
||||||
|
> Nota: i due primi tentativi di correzione (status-bar no-op e idempotenza di
|
||||||
|
> `init_menubar_tools`) **non** centravano la causa; sono stati comunque
|
||||||
|
> mantenuti perché innocui e leggermente migliorativi, ma il vero colpevole era
|
||||||
|
> questo crash a monte.
|
||||||
|
|
||||||
|
### Fix (comportamento invariato per tutti i valori normali)
|
||||||
|
- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` con
|
||||||
|
**clamp a INT32_MAX** (anno 2038) in caso di `OverflowError`/`OSError`/
|
||||||
|
`ValueError`, **esattamente** come la funzione `get_max_allowed_timestamp()`
|
||||||
|
dell'originale (workaround per Electrum issue **#6170**).
|
||||||
|
- Usato in `to_date` / `to_timestamp` / `__str__` / `__repr__` di
|
||||||
|
`BalTimestamp`.
|
||||||
|
- `gui/qt/widgets.py` (`set_value`): usa il converter sicuro.
|
||||||
|
- `core/util.py` (`timestamp_minus`): stessa protezione inline con clamp a
|
||||||
|
INT32_MAX.
|
||||||
|
|
||||||
|
I valori entro il 2038 (date assolute normali, durate relative come `90d`/`5y`)
|
||||||
|
producono **lo stesso identico risultato** di prima.
|
||||||
|
|
||||||
|
### Test
|
||||||
|
- `tests/windows_overflow_test.py` riproduce il limite 32-bit di Windows
|
||||||
|
(monkeypatch di `datetime.fromtimestamp`) e dimostra che **senza** il fix si
|
||||||
|
ottiene lo **stesso** `OverflowError` del log, mentre **con** il fix passa.
|
||||||
|
Verificato anche che il test **fallisce** senza il fix.
|
||||||
|
|
||||||
|
Confermato dall'utente: **"si ora funziona"**.
|
||||||
|
|||||||
@@ -315,6 +315,28 @@ class BalTimestamp:
|
|||||||
"""Return the duration expressed in days (years are ``*365``)."""
|
"""Return the duration expressed in days (years are ``*365``)."""
|
||||||
return self.value * 365 if self.unit == 'y' else self.value
|
return self.value * 365 if self.unit == 'y' else self.value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_fromtimestamp(ts):
|
||||||
|
"""``datetime.fromtimestamp`` that never raises ``OverflowError``.
|
||||||
|
|
||||||
|
On Windows ``time_t`` is 32-bit, so ``datetime.fromtimestamp`` raises
|
||||||
|
``OverflowError: Python int too large to convert to C int`` for any
|
||||||
|
timestamp past the year-2038 limit (e.g. ``NLOCKTIME_MAX = 2**32 - 1``,
|
||||||
|
used as the default/sentinel locktime). On 64-bit Linux the same call
|
||||||
|
succeeds, which is why this only crashed on the user's Windows build.
|
||||||
|
|
||||||
|
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
|
||||||
|
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
|
||||||
|
"""
|
||||||
|
INT32_MAX = 2 ** 31 - 1
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(ts)
|
||||||
|
except (OSError, OverflowError, ValueError):
|
||||||
|
try:
|
||||||
|
return datetime.fromtimestamp(min(int(ts), INT32_MAX))
|
||||||
|
except (OSError, OverflowError, ValueError):
|
||||||
|
return datetime.fromtimestamp(INT32_MAX)
|
||||||
|
|
||||||
def to_date(self, from_date=None, reverse=False):
|
def to_date(self, from_date=None, reverse=False):
|
||||||
"""Resolve to a ``datetime``.
|
"""Resolve to a ``datetime``.
|
||||||
|
|
||||||
@@ -323,16 +345,22 @@ class BalTimestamp:
|
|||||||
``from_date`` (defaulting to *now*), normalised to midnight.
|
``from_date`` (defaulting to *now*), normalised to midnight.
|
||||||
"""
|
"""
|
||||||
if self.unit is None:
|
if self.unit is None:
|
||||||
return datetime.fromtimestamp(self.value)
|
return self._safe_fromtimestamp(self.value)
|
||||||
else:
|
else:
|
||||||
if from_date is None:
|
if from_date is None:
|
||||||
from_date = datetime.now()
|
from_date = datetime.now()
|
||||||
if isinstance(from_date, (int, float)):
|
if isinstance(from_date, (int, float)):
|
||||||
from_date = datetime.fromtimestamp(from_date)
|
from_date = self._safe_fromtimestamp(from_date)
|
||||||
reverse = 1 if not reverse else -1
|
reverse = 1 if not reverse else -1
|
||||||
|
try:
|
||||||
return (
|
return (
|
||||||
from_date + (reverse * timedelta(days=self.duration_to_days()))
|
from_date + (reverse * timedelta(days=self.duration_to_days()))
|
||||||
).replace(hour=0, minute=0, second=0, microsecond=0)
|
).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
except (OverflowError, OSError, ValueError):
|
||||||
|
# Duration overflowed datetime's range; clamp to INT32_MAX.
|
||||||
|
return self._safe_fromtimestamp(2 ** 31 - 1).replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
|
|
||||||
def to_timestamp(self, from_date=None, reverse=False):
|
def to_timestamp(self, from_date=None, reverse=False):
|
||||||
"""Same as :meth:`to_date` but returns a UNIX timestamp."""
|
"""Same as :meth:`to_date` but returns a UNIX timestamp."""
|
||||||
@@ -340,12 +368,12 @@ class BalTimestamp:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.unit is None:
|
if self.unit is None:
|
||||||
return datetime.fromtimestamp(self.value).isoformat()
|
return self._safe_fromtimestamp(self.value).isoformat()
|
||||||
else:
|
else:
|
||||||
return f"{self.value}{self.unit}"
|
return f"{self.value}{self.unit}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.unit is None:
|
if self.unit is None:
|
||||||
return datetime.fromtimestamp(self.value).to_date().timestamp()
|
return self._safe_fromtimestamp(self.value).isoformat()
|
||||||
else:
|
else:
|
||||||
return f"{self.value}{self.unit}"
|
return f"{self.value}{self.unit}"
|
||||||
|
|||||||
@@ -363,7 +363,12 @@ class Util:
|
|||||||
out = 0
|
out = 0
|
||||||
if locktime > LOCKTIME_THRESHOLD:
|
if locktime > LOCKTIME_THRESHOLD:
|
||||||
seconds = blocks * 600 + hours * 3600 + days * 86400
|
seconds = blocks * 600 + hours * 3600 + days * 86400
|
||||||
|
# On Windows datetime.fromtimestamp raises OverflowError past 2038
|
||||||
|
# (e.g. NLOCKTIME_MAX); clamp to INT32_MAX (Electrum issue #6170).
|
||||||
|
try:
|
||||||
dt = datetime.fromtimestamp(locktime)
|
dt = datetime.fromtimestamp(locktime)
|
||||||
|
except (OverflowError, OSError, ValueError):
|
||||||
|
dt = datetime.fromtimestamp(min(locktime, 2 ** 31 - 1))
|
||||||
dt -= timedelta(seconds=seconds)
|
dt -= timedelta(seconds=seconds)
|
||||||
out = dt.timestamp()
|
out = dt.timestamp()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -88,14 +88,15 @@ class Plugin(BalPlugin):
|
|||||||
|
|
||||||
@hook
|
@hook
|
||||||
def create_status_bar(self, sb):
|
def create_status_bar(self, sb):
|
||||||
_logger.info("HOOK create status bar")
|
# NOTE: intentionally a no-op, matching the original plugin. The
|
||||||
b = StatusBarButton(
|
# original code had an early ``return`` before building the
|
||||||
read_QIcon_from_bytes(self.read_file("icons/bal32x32.png")),
|
# StatusBarButton, i.e. the button was deliberately disabled. Adding
|
||||||
"Bal " + _("Bitcoin After Life"),
|
# the button here caused a stray, condensed icon+text element to be
|
||||||
partial(self.settings_dialog, sb),
|
# rendered in the wrong place (near the top, under the Electrum logo)
|
||||||
sb.height(),
|
# after a restart / wallet switch. Settings are already reachable via
|
||||||
)
|
# Tools -> Plugins, so we keep the original behaviour.
|
||||||
sb.addPermanentWidget(b)
|
_logger.info("HOOK create status bar (no-op)")
|
||||||
|
return
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def init_menubar(self, window):
|
def init_menubar(self, window):
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
x = QDateTime.currentDateTime().timestamp()
|
x = QDateTime.currentDateTime().timestamp()
|
||||||
finally:
|
finally:
|
||||||
_dt = datetime.fromtimestamp(x)
|
# Use the overflow-safe converter: on Windows datetime.fromtimestamp
|
||||||
|
# raises OverflowError for timestamps past 2038 (e.g. NLOCKTIME_MAX).
|
||||||
|
_dt = BalTimestamp._safe_fromtimestamp(x)
|
||||||
#if self.alarm != dt:
|
#if self.alarm != dt:
|
||||||
self.setDateTime(_dt)
|
self.setDateTime(_dt)
|
||||||
self.alarm = _dt
|
self.alarm = _dt
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ class BalWindow:
|
|||||||
self.will_settings = None
|
self.will_settings = None
|
||||||
self.ok = False
|
self.ok = False
|
||||||
self.disable_plugin = True
|
self.disable_plugin = True
|
||||||
|
# Guard against wiring the menu/tabs more than once for the same window.
|
||||||
|
# Electrum may invoke both the ``init_menubar`` hook and our hot-init
|
||||||
|
# path (``init_qt`` -> ``_setup_window``) for the same window, e.g. when
|
||||||
|
# Electrum restarts with the plugin already enabled. Calling
|
||||||
|
# ``init_menubar_tools`` twice would add the Heirs/Will tabs and the
|
||||||
|
# menu actions twice, producing the garbled/condensed menu entry.
|
||||||
|
self._menubar_initialized = False
|
||||||
self.bal_plugin.get_decimal_point = self.window.get_decimal_point
|
self.bal_plugin.get_decimal_point = self.window.get_decimal_point
|
||||||
|
|
||||||
if self.window.wallet:
|
if self.window.wallet:
|
||||||
@@ -49,6 +56,15 @@ class BalWindow:
|
|||||||
self.will_tab.wallet = self.wallet
|
self.will_tab.wallet = self.wallet
|
||||||
|
|
||||||
def init_menubar_tools(self, tools_menu):
|
def init_menubar_tools(self, tools_menu):
|
||||||
|
# Idempotent: only wire the tabs + menu actions once per window.
|
||||||
|
# A second call (e.g. init_menubar hook *and* the hot-init path both
|
||||||
|
# firing) would otherwise duplicate the Heirs/Will tabs and the
|
||||||
|
# Will-Executors / toggle actions, which Qt renders as a broken,
|
||||||
|
# condensed menu entry under the Electrum logo.
|
||||||
|
if self._menubar_initialized:
|
||||||
|
_logger.info("init_menubar_tools: already initialised, skipping")
|
||||||
|
return
|
||||||
|
self._menubar_initialized = True
|
||||||
self.tools_menu = tools_menu
|
self.tools_menu = tools_menu
|
||||||
|
|
||||||
def add_optional_tab(tabs, tab, icon, description):
|
def add_optional_tab(tabs, tab, icon, description):
|
||||||
@@ -707,6 +723,9 @@ class BalWindow:
|
|||||||
self.willexecutors = {}
|
self.willexecutors = {}
|
||||||
self.disable_plugin = True
|
self.disable_plugin = True
|
||||||
self.ok = False
|
self.ok = False
|
||||||
|
# The tabs/menu actions were removed above; allow init_menubar_tools to
|
||||||
|
# re-wire them if this same window is reused for another wallet.
|
||||||
|
self._menubar_initialized = False
|
||||||
|
|
||||||
def ask_password_and_sign_transactions(self, callback=None):
|
def ask_password_and_sign_transactions(self, callback=None):
|
||||||
def on_success(txs):
|
def on_success(txs):
|
||||||
|
|||||||
@@ -98,6 +98,40 @@ def main(pkg: str) -> int:
|
|||||||
"BalDialog.hideEvent must not stop the thread (drops download result)")
|
"BalDialog.hideEvent must not stop the thread (drops download result)")
|
||||||
print("[OK] BalDialog.closeEvent/hideEvent do not kill the task thread")
|
print("[OK] BalDialog.closeEvent/hideEvent do not kill the task thread")
|
||||||
|
|
||||||
|
# REGRESSION: init_menubar_tools must be idempotent. Electrum can invoke
|
||||||
|
# both the init_menubar hook and the hot-init path (init_qt -> _setup_window)
|
||||||
|
# for the same window (e.g. on restart with the plugin already enabled);
|
||||||
|
# wiring the tabs/menu actions twice produces a garbled, condensed menu
|
||||||
|
# entry under the Electrum logo. Verify the guard flag is in place.
|
||||||
|
bal_window_cls = win_mod.BalWindow
|
||||||
|
menubar_src = inspect.getsource(bal_window_cls.init_menubar_tools)
|
||||||
|
assert "_menubar_initialized" in menubar_src, (
|
||||||
|
"init_menubar_tools must guard against double initialisation")
|
||||||
|
init_src = inspect.getsource(bal_window_cls.__init__)
|
||||||
|
assert "_menubar_initialized" in init_src, (
|
||||||
|
"_menubar_initialized must be initialised in BalWindow.__init__")
|
||||||
|
onclose_src = inspect.getsource(bal_window_cls.on_close)
|
||||||
|
assert "_menubar_initialized" in onclose_src, (
|
||||||
|
"on_close must reset _menubar_initialized so the window can be reused")
|
||||||
|
print("[OK] init_menubar_tools is idempotent (no duplicate tabs/menu)")
|
||||||
|
|
||||||
|
# REGRESSION: create_status_bar must stay a no-op, like the original plugin
|
||||||
|
# (whose body had an early ``return`` before building the StatusBarButton).
|
||||||
|
# Re-adding the status-bar button made a stray condensed icon+text element
|
||||||
|
# appear in the wrong place after restart / wallet switch.
|
||||||
|
csb_src = inspect.getsource(plugin_mod.Plugin.create_status_bar)
|
||||||
|
csb_active = _active_source_without_strings(plugin_mod) # whole module sans strings
|
||||||
|
csb_body = inspect.getsource(plugin_mod.Plugin.create_status_bar)
|
||||||
|
# The executable body must not add a permanent widget / build the button.
|
||||||
|
# Strip comments to avoid matching the explanatory note.
|
||||||
|
csb_code = "\n".join(
|
||||||
|
line for line in csb_body.splitlines()
|
||||||
|
if not line.lstrip().startswith("#")
|
||||||
|
)
|
||||||
|
assert "addPermanentWidget" not in csb_code, (
|
||||||
|
"create_status_bar must not add a status-bar widget (original is a no-op)")
|
||||||
|
print("[OK] create_status_bar is a no-op (matches original)")
|
||||||
|
|
||||||
print(f"\n[OK] all GUI-fix checks passed for package {pkg!r}")
|
print(f"\n[OK] all GUI-fix checks passed for package {pkg!r}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
97
tests/windows_overflow_test.py
Normal file
97
tests/windows_overflow_test.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Regression test for the Windows year-2038 OverflowError crash.
|
||||||
|
|
||||||
|
Background
|
||||||
|
----------
|
||||||
|
On Windows ``time_t`` is 32-bit, so ``datetime.fromtimestamp(ts)`` raises
|
||||||
|
``OverflowError: Python int too large to convert to C int`` for any timestamp
|
||||||
|
past 2038 (e.g. ``NLOCKTIME_MAX = 2**32 - 1``, used as the default/sentinel
|
||||||
|
locktime). On 64-bit Linux the same call succeeds, which is why the bug only
|
||||||
|
showed up on the user's Windows build: ``BalWindow.__init__`` ->
|
||||||
|
``create_heirs_tab`` -> ``WillSettingsWidget`` -> ``on_locktime_change`` ->
|
||||||
|
``BalTimestamp.to_date`` -> ``datetime.fromtimestamp(NLOCKTIME_MAX)`` crashed,
|
||||||
|
which aborted ``init_menubar`` / ``load_wallet`` and left the Will/Heirs tabs
|
||||||
|
and the menu entry half-built (the garbled/condensed element under the logo).
|
||||||
|
|
||||||
|
This test forces ``datetime.fromtimestamp`` to behave like the Windows 32-bit
|
||||||
|
implementation, then exercises ``BalTimestamp`` with NLOCKTIME_MAX to prove the
|
||||||
|
overflow-safe conversion no longer raises and clamps to INT32_MAX.
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/windows_overflow_test.py <PLUGIN_IMPORT_NAME>
|
||||||
|
"""
|
||||||
|
import datetime as _datetime_mod
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PKG = sys.argv[1] if len(sys.argv) > 1 else "electrum.plugins.bal"
|
||||||
|
|
||||||
|
INT32_MAX = 2 ** 31 - 1
|
||||||
|
NLOCKTIME_MAX = 2 ** 32 - 1 # 4294967295, the value seen in the crash log
|
||||||
|
|
||||||
|
_real_datetime = _datetime_mod.datetime
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsLikeDatetime(_real_datetime):
|
||||||
|
"""A datetime subclass whose fromtimestamp emulates Windows' 32-bit limit."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromtimestamp(cls, ts, tz=None):
|
||||||
|
if tz is None and (ts > INT32_MAX or ts < 0):
|
||||||
|
raise OverflowError("Python int too large to convert to C int")
|
||||||
|
return _real_datetime.fromtimestamp(ts, tz)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
plugin_base = importlib.import_module(f"{PKG}.core.plugin_base")
|
||||||
|
BalTimestamp = plugin_base.BalTimestamp
|
||||||
|
|
||||||
|
# 1) Sanity: with the real (Linux 64-bit) datetime, large ts works already.
|
||||||
|
bt = BalTimestamp(NLOCKTIME_MAX)
|
||||||
|
d = bt.to_date()
|
||||||
|
assert isinstance(d, _real_datetime), d
|
||||||
|
print("[OK] BalTimestamp(NLOCKTIME_MAX).to_date() works on this platform")
|
||||||
|
|
||||||
|
# 2) Now emulate Windows: patch datetime in the plugin_base module so that
|
||||||
|
# fromtimestamp raises OverflowError past 2038, exactly like Windows.
|
||||||
|
original = plugin_base.datetime
|
||||||
|
plugin_base.datetime = _WindowsLikeDatetime
|
||||||
|
try:
|
||||||
|
# 2a) Absolute sentinel timestamp (the exact crash path from the log).
|
||||||
|
bt = BalTimestamp(NLOCKTIME_MAX)
|
||||||
|
d = bt.to_date() # must NOT raise OverflowError anymore
|
||||||
|
assert d.year <= 2038, f"expected clamp to <=2038, got {d!r}"
|
||||||
|
print("[OK] to_date(NLOCKTIME_MAX) no longer raises (clamped to INT32_MAX)")
|
||||||
|
|
||||||
|
# 2b) to_timestamp must also be safe.
|
||||||
|
ts = bt.to_timestamp()
|
||||||
|
assert ts <= INT32_MAX, ts
|
||||||
|
print("[OK] to_timestamp(NLOCKTIME_MAX) clamped & safe")
|
||||||
|
|
||||||
|
# 2c) __str__ / __repr__ must not raise either.
|
||||||
|
_ = str(bt)
|
||||||
|
_ = repr(bt)
|
||||||
|
print("[OK] str()/repr() on out-of-range timestamp are safe")
|
||||||
|
|
||||||
|
# 2d) Relative durations that overflow when added (e.g. huge 'd').
|
||||||
|
bt_rel = BalTimestamp(f"{10 ** 9}d") # ~2.7M years -> overflow
|
||||||
|
d2 = bt_rel.to_date()
|
||||||
|
assert d2 is not None
|
||||||
|
print("[OK] huge relative duration no longer raises")
|
||||||
|
|
||||||
|
# 2e) Normal values are unchanged (behaviour-preserving check).
|
||||||
|
bt_norm = BalTimestamp("90d")
|
||||||
|
d3 = bt_norm.to_date()
|
||||||
|
# 90 days from now, normalised to midnight
|
||||||
|
assert d3.hour == 0 and d3.minute == 0 and d3.second == 0
|
||||||
|
print("[OK] normal '90d' value still resolves to a midnight datetime")
|
||||||
|
finally:
|
||||||
|
plugin_base.datetime = original
|
||||||
|
|
||||||
|
print(f"\n[OK] Windows overflow regression passed for package {PKG!r}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user