Update ohne Neustart, wenn nur Oberflaechendateien wechseln
Bisher verlangte jedes Update einen Home-Assistant-Neustart. Noetig ist er aber nur fuer Python-Code: HA importiert die Module einmal beim Start und haengt danach mit lebenden Objekten daran. frontend/ dagegen wird direkt von der Platte ausgeliefert (StaticPathConfig, cache_headers=False) - eine ersetzte .js ist sofort wirksam, es braucht nur ein Neuladen im Browser. aktualisierung.neustart_noetig() vergleicht die alte gegen die neue Fassung ueber sha256 je Datei und meldet "kein Neustart" nur, wenn JEDE Abweichung unter frontend/ liegt oder die manifest.json ist. Alles andere - .py, services.yaml, translations/, vorlage/ - gilt als neustartpflichtig, auch wo es das im Einzelfall nicht waere. Die Schieflage ist Absicht: ein faelschlich ausgelassener Neustart laesst neuen Python-Code nie anlaufen, und der Fehler wird woanders gesucht. manifest.json ist ausgenommen, weil sich seine Versionsnummer bei jeder Veroeffentlichung aendert - sonst waere die Unterscheidung wertlos. Weil HA das Manifest fuer die Laufzeit festhaelt (loader.py: hass.data[ DATA_INTEGRATIONS]), liest version_von_platte() die Nummer direkt von der Platte; koordinator.version_neu_lesen() zieht sie nach einem Update ohne Neustart nach, damit Panel und App die neue Fassung auch anzeigen. Panel und App zeigen im Neustart-freien Fall "Seite neu laden" statt "Installation abschliessen - Jetzt neu starten". Neun neue Testfaelle fuer die Unterscheidung, 23 Tests in der Aktualisierungs-Suite gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,10 +69,12 @@ class ErfolgreicherTausch(unittest.TestCase):
|
||||
"neue_datei.py": b"# neue Fassung\n",
|
||||
"frontend/app.js": b"// panel\n",
|
||||
})
|
||||
version = a.entpacken_pruefen_tauschen(
|
||||
ergebnis = a.entpacken_pruefen_tauschen(
|
||||
zip_bytes, self.integration, self.staging, self.backup
|
||||
)
|
||||
self.assertEqual(version, "2099.1.1.1")
|
||||
self.assertEqual(ergebnis["version"], "2099.1.1.1")
|
||||
# Das Testarchiv enthaelt .py-Dateien, also neustartpflichtig.
|
||||
self.assertTrue(ergebnis["neustart_noetig"])
|
||||
|
||||
# Live-Ordner trägt jetzt die neue Fassung.
|
||||
with open(os.path.join(self.integration, "manifest.json"), "rb") as f:
|
||||
@@ -188,6 +190,73 @@ class AbgelehnteFaelle(unittest.TestCase):
|
||||
self._unangetastet()
|
||||
|
||||
|
||||
class NeustartNoetig(unittest.TestCase):
|
||||
"""Die Unterscheidung "braucht das einen Neustart?" ist bewusst schief.
|
||||
|
||||
Sagt sie faelschlich "nein", laeuft neuer Python-Code nie an und der Fehler
|
||||
wird woanders gesucht. Ein ueberfluessiger Neustart kostet eine Minute.
|
||||
Deshalb: nur frontend/ und manifest.json duerfen ohne Neustart wechseln.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.basis = tempfile.mkdtemp()
|
||||
self.alt = os.path.join(self.basis, "alt")
|
||||
self.neu = os.path.join(self.basis, "neu")
|
||||
for ordner in (self.alt, self.neu):
|
||||
os.makedirs(os.path.join(ordner, "frontend"))
|
||||
self._schreiben(ordner, "manifest.json", '{"version": "1"}')
|
||||
self._schreiben(ordner, "__init__.py", "x = 1\n")
|
||||
self._schreiben(ordner, "frontend/app.js", "alert(1)\n")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.basis, ignore_errors=True)
|
||||
|
||||
def _schreiben(self, ordner, rel, inhalt):
|
||||
pfad = os.path.join(ordner, *rel.split("/"))
|
||||
os.makedirs(os.path.dirname(pfad), exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8") as datei:
|
||||
datei.write(inhalt)
|
||||
|
||||
def test_nichts_geaendert_braucht_keinen_neustart(self):
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_nur_frontend_braucht_keinen_neustart(self):
|
||||
self._schreiben(self.neu, "frontend/app.js", "alert(2)\n")
|
||||
self._schreiben(self.neu, "frontend/neu.css", "body{}\n")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_manifest_allein_braucht_keinen_neustart(self):
|
||||
# Die Versionsnummer aendert sich bei JEDER Veroeffentlichung - ohne
|
||||
# diese Ausnahme waere jedes Update ein Neustart-Update.
|
||||
self._schreiben(self.neu, "manifest.json", '{"version": "2"}')
|
||||
self._schreiben(self.neu, "frontend/app.js", "alert(2)\n")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_geaenderte_python_datei_braucht_neustart(self):
|
||||
self._schreiben(self.neu, "__init__.py", "x = 2\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_neue_python_datei_braucht_neustart(self):
|
||||
self._schreiben(self.neu, "neu.py", "y = 1\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_entfallene_python_datei_braucht_neustart(self):
|
||||
os.remove(os.path.join(self.neu, "__init__.py"))
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_services_yaml_braucht_neustart(self):
|
||||
# Nicht weil es zwingend waere, sondern weil die Regel pessimistisch ist.
|
||||
self._schreiben(self.neu, "services.yaml", "dienst:\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_pycache_zaehlt_nicht(self):
|
||||
self._schreiben(self.neu, "__pycache__/x.cpython-312.pyc", "egal")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_unlesbarer_ordner_gilt_als_neustart(self):
|
||||
self.assertTrue(a.neustart_noetig(os.path.join(self.basis, "gibtsnicht"), self.neu))
|
||||
|
||||
|
||||
class VersionsOrdnung(unittest.TestCase):
|
||||
"""Nur eine NACHWEISLICH neuere Fassung ist ein Update.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user