Fix systemd oneshot upgrade handoff deadlock
This commit is contained in:
1 parent
56fbcdad74
commit
898df2e926
5 files changed
+190
-20
No files matched your search
+15
-2
@@ -261,6 +261,17 @@ Dateisperre. Der Starter übergibt den langen Ablauf an eine systemd-Unit; das
|
||||
Ergebnis bleibt im Migrationsordner erhalten. Ein kombinierter Rollback stellt
|
||||
Paketdateien und die vorherige Hauptkonfiguration wieder her.
|
||||
|
||||
Die Übergabe nutzt `systemd-run --no-block` mit `Type=oneshot`: Der Starter wartet
|
||||
nur auf die Annahme des Auftrags, gibt danach zuerst die Backup-Sperre und dann
|
||||
die Update-Sperre frei. Der Worker erwirbt beide selbst und darf nur mit einem
|
||||
weiterhin als `QUEUED` freigegebenen Migrationsstand beginnen. Bei einer
|
||||
fehlgeschlagenen Übergabe wird diese Freigabe unter gehaltener Sperre entzogen.
|
||||
Der Worker richtet Fehlerprotokoll und EXIT-Trap vor dem maximal 60-sekündigen
|
||||
Warten auf die Update-Sperre ein. Erst `SUCCESS` belegt das Gesamtresultat.
|
||||
Das behebt das gegenseitige Warten in den früheren Paketen: Ohne `--no-block`
|
||||
wartete der Starter auf den oneshot-Abschluss, während der Worker auf seine
|
||||
Sperre wartete. Ursache, Diagnose und Wiederanlauf beschreibt die Update-Anleitung.
|
||||
|
||||
Der Abnahmelauf nutzt `app-backup.sh --no-retention`: keinerlei lokale oder entfernte
|
||||
Löschung alter Sicherungssätze. Nach Upload/Rücklesevergleich prüft zusätzlich
|
||||
`app-restore.sh --remote-run ... --verify-only` den erneut heruntergeladenen Satz.
|
||||
@@ -304,7 +315,7 @@ Die Entfernung der drei DB-Dateien aus dem aktuellen Baum löscht deren Historie
|
||||
|
||||
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
||||
|
||||
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **34 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
||||
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **37 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion.
|
||||
|
||||
Geprüfte Fälle umfassen:
|
||||
|
||||
@@ -321,6 +332,8 @@ Geprüfte Fälle umfassen:
|
||||
- Paketupdate mit Erhalt von Config/Zeitplan und ausführbarem Rollback.
|
||||
- Selbstentpackendes Transferpaket mit erfolgreichem Komplettlauf und kombiniertem Konfigurations-/Paket-Rollback.
|
||||
- Abbruch vor Änderungen bei Vorprüfungsfehlern, gestoppter Timer bei fehlgeschlagenem Abnahmebackup, Ablehnung beschädigter Pakete.
|
||||
- Übergabe an einen vom Starter unabhängigen Prozess (systemd-run im Test simuliert), ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
||||
- Übergabe an einen vom Starter unabhängigen Prozess: Das systemd-run-Testmodell wartet bei `Type=oneshot` ohne `--no-block` auf das Worker-Ende. Der Regressionstest scheiterte am bisherigen gegenseitigen Sperrwarten und besteht nach dem Fix.
|
||||
- Abgewiesene systemd-Übergabe, zurückgezogene Freigabe bei bereits eingereihtem Auftrag und persistente Fehlerprotokolle bei Sperr-Timeout; jeweils ohne Installationsänderung oder Timer-Stopp.
|
||||
- Ausdrückliche Cron-Policy und Writer-Wartezeit/Timeout.
|
||||
|
||||
**Serverabnahme steht separat aus:** echte Rocky-Linux-10-Umgebung, SELinux-/ACL-Verhalten auf den verwendeten Dateisystemen, installierte Nextcloud-/Gitea-Versionen, vollständige Writer-/Pfadliste, Berechtigungen des echten DB-Backupaccounts, tatsächlicher OneDrive-Upload großer Archive, Laufzeit/Speicherbedarf und fachliche Restore-Probe. Es wurde kein Produktivbackup ausgeführt und keine Serverinstallation verändert.
|
||||
@@ -21,6 +21,20 @@ anschließend als systemd-Auftrag unabhängig von der SSH-Verbindung. Bei unklar
|
||||
Gitea-/Cron-/rclone-Einstellungen fragt der Starter gezielt nach. Die Bedienung und
|
||||
der kombinierte Rückweg stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md).
|
||||
|
||||
Der Starter bestätigt nur die Übergabe. Erst `SUCCESS` in der genannten
|
||||
`result.txt` bestätigt Installation, Backup und Remote-Prüfung. Den Verlauf ohne
|
||||
Terminal-Pager anzeigen:
|
||||
|
||||
```bash
|
||||
sudo journalctl -fu app-backup-upgrade.service --no-pager
|
||||
```
|
||||
|
||||
Ein Startabbruch nach genau 60 Sekunden in den Paketen `8e6c9a058fa9` und
|
||||
`56fbcdad74f4` ist durch die asynchrone Sperrübergabe behoben. Bei diesem Fehler
|
||||
hatte die Installation noch nicht begonnen; das neue Paket erneut verwenden.
|
||||
Die kurzlebige Unit kann nach einem Fehler verschwinden, das Journal bleibt
|
||||
abfragbar. Diagnose und Details stehen in der Update-Anleitung.
|
||||
|
||||
Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates.
|
||||
|
||||
## Wesentliche Änderungen
|
||||
|
||||
+53
-7
@@ -24,7 +24,8 @@ Backups mit zeitweiliger Unterbrechung der konfigurierten Anwendungen.
|
||||
5. Crypt-Konfiguration und DB-Dateien prüfen; vorhandene Schlüssel/Passwörter erhalten.
|
||||
6. Vorprüfung mit geschützten Kopien durchführen. Bei Fehlern bleibt das installierte
|
||||
Paket samt Anwendungskonfiguration und Timer unverändert.
|
||||
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` starten.
|
||||
7. Einen von SSH unabhängigen systemd-Auftrag `app-backup-upgrade.service` mit
|
||||
`--no-block` einreihen und die Sperren an ihn übergeben.
|
||||
8. Backup-Timer stoppen, Paket mit Rollback-Inventar installieren, Konfiguration
|
||||
atomar übernehmen und Rechte der aktiven root-eigenen Zugangsdaten korrigieren.
|
||||
9. Vollständiges Backup ohne lokale/Remote-Retention durchführen. Anschließend den
|
||||
@@ -37,7 +38,7 @@ Remote-Prüflauf belegt Archivlesbarkeit, ersetzt aber keinen fachlichen Test-Re
|
||||
## Verlauf und Ergebnis
|
||||
|
||||
```bash
|
||||
sudo journalctl -fu app-backup-upgrade.service
|
||||
sudo journalctl -fu app-backup-upgrade.service --no-pager
|
||||
```
|
||||
|
||||
Der Starter nennt außerdem den **konkreten** Ergebnisordner:
|
||||
@@ -47,9 +48,24 @@ sudo cat /var/backups/app-backup-upgrade-XXXXXXXX/result.txt
|
||||
sudo less /var/backups/app-backup-upgrade-XXXXXXXX/update.log
|
||||
```
|
||||
|
||||
Erwartet wird `SUCCESS`. Solange `RUNNING` steht, nicht parallel erneut starten.
|
||||
Nach Ende verschwindet die kurzlebige systemd-Unit; Log und Ergebnis bleiben erhalten.
|
||||
Bei `QUEUED` wurde der Auftrag übergeben und wartet auf den Start.
|
||||
Erwartet wird `SUCCESS`. Bei `QUEUED` oder `RUNNING` nicht parallel erneut starten.
|
||||
`QUEUED` bedeutet, dass der Auftrag zur Übergabe vorbereitet wurde und noch kein
|
||||
Worker den Start bestätigt hat. Ein erfolgreicher Starter bestätigt nur die
|
||||
Annahme durch systemd; Installation und Backup können anschließend noch scheitern.
|
||||
Nach Ende verschwindet die kurzlebige systemd-Unit durch `--collect` auch bei
|
||||
Fehlern. `Unit ... could not be found` ist daher kein Erfolgsnachweis und verhindert
|
||||
die Journalabfrage nicht. Log und Ergebnis bleiben erhalten. Diagnose ohne Pager:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u app-backup-upgrade.service -n 100 --no-pager -l
|
||||
sudo systemctl status app-backup-upgrade.service --no-pager -l
|
||||
```
|
||||
|
||||
Bleibt `QUEUED` stehen, zuerst das Journal prüfen: Ein Fehler noch vor dem Start
|
||||
des Bash-Workers (z. B. ein nicht ausführbarer Paketpfad) kann noch nicht in
|
||||
`update.log` oder `result.txt` durch den Worker protokolliert werden. Ein späterer
|
||||
Sperr-Timeout erscheint dagegen in beiden Dateien. Nicht allein auf den Exit-Code
|
||||
von `systemd-run` vertrauen.
|
||||
`--foreground` führt den Vorgang stattdessen im aktuellen Terminal aus und ist
|
||||
für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
|
||||
|
||||
@@ -107,6 +123,30 @@ bleiben erhalten; beim Aktivieren kann `Persistent=true` einen Lauf nachholen.
|
||||
|
||||
## Fehler und Rückweg
|
||||
|
||||
### Startfehler nach genau 60 Sekunden bei älteren Paketen
|
||||
|
||||
Die Pakete `8e6c9a058fa9` und `56fbcdad74f4` enthielten einen Fehler in der
|
||||
systemd-Übergabe: Der Starter hielt die Update-Sperre und wartete synchron auf
|
||||
den Abschluss des Startauftrags einer `Type=oneshot`-Unit. Dieser Startauftrag
|
||||
endet erst mit dem Worker; der Worker wartete aber selbst auf die Sperre des
|
||||
Starters. Nach 60 Sekunden brach seine Sperranforderung ab, noch vor Installation
|
||||
und Timer-Stopp. In diesem Ablauf ist kein Rollback erforderlich. Der ursprüngliche
|
||||
Worker hatte vor dem Sperrwarten noch kein eigenes Log eingerichtet, daher konnte
|
||||
die Fehlermeldung auf den systemd-Exit-Code beschränkt bleiben.
|
||||
|
||||
Das korrigierte Paket verwendet `systemd-run --no-block` und gibt zuerst die
|
||||
Backup-Sperre, dann die Update-Sperre frei. Eine fehlgeschlagene Übergabe entzieht
|
||||
auch einem möglicherweise schon eingereihten Worker die Freigabe. Die Sperren
|
||||
bleiben als Schutz vor parallelen Backups/Updates erhalten. Erneut das **neue**
|
||||
Paket starten; weder Sperrdateien löschen noch alte Worker manuell nachstarten.
|
||||
|
||||
Das Verhalten von `--no-block` und `--collect` ist in der
|
||||
[systemd-Dokumentation](https://github.com/systemd/systemd/blob/v256/man/systemd-run.xml)
|
||||
beschrieben, der Startabschluss von `oneshot` in
|
||||
[systemd.service](https://github.com/systemd/systemd/blob/v256/man/systemd.service.xml).
|
||||
|
||||
### Fehlende Mailablage und erneuter Start
|
||||
|
||||
Ein Abbruch `Missing source: /var/vmail` bei der **Vorprüfung**, wie beim Paket
|
||||
`8e6c9a058fa9`, erfordert kein Rollback. Mit dem korrigierten Paket erneut starten
|
||||
und die Mailablage klären. Die bisherige Konfiguration und der Timer sind erhalten;
|
||||
@@ -134,6 +174,8 @@ Vorlage vollständig kopiert, sofern es auf demselben Dateisystem liegt und kein
|
||||
Verweis auf eine externe Ablage ist. Weitere Benutzer müssen in `MAIL_HOME_DIRS`
|
||||
stehen; das Vorhandensein eines einzelnen Postfachs belegt nicht deren Abdeckung.
|
||||
|
||||
### Fehler nach Installationsbeginn
|
||||
|
||||
Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
|
||||
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
|
||||
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls
|
||||
@@ -165,8 +207,12 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
||||
|
||||
## Verifikation der Lieferung
|
||||
|
||||
34 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
||||
37 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
||||
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
||||
systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche
|
||||
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
||||
berücksichtigt jetzt das Warten auf den oneshot-Startauftrag: Der Regressionstest
|
||||
scheiterte mit dem alten Code und war nach dem Fix erfolgreich. Geprüft sind auch
|
||||
abgewiesene Übergaben, eingereihte Worker trotz fehlgeschlagener Antwort und
|
||||
persistente Fehlerprotokolle bei belegter Update-Sperre. Die tatsächliche
|
||||
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
||||
auf einem separaten Testsystem bleibt erforderlich.
|
||||
+81
-3
@@ -541,20 +541,38 @@ QUIESCE_SERVICES=(nc-cron)
|
||||
result=self.run_script('app-backup.sh','--check',ok=False,APP_BACKUP_INHERIT_LOCK='1')
|
||||
self.assertIn('Inherited lock descriptor',result.stdout)
|
||||
|
||||
def test_detached_package_handoff_finishes_after_starter_exits(self):
|
||||
self.configure_upgrade()
|
||||
def systemd_launcher(self):
|
||||
"""Model start-job waiting, detached FDs, and ambiguous submit failures."""
|
||||
launcher=self.bin/'systemd-run'
|
||||
launcher.write_text('''#!/usr/bin/env python3
|
||||
import os,subprocess,sys
|
||||
from pathlib import Path
|
||||
args=sys.argv[1:]
|
||||
assert '--unit=app-backup-upgrade' in args
|
||||
assert '--property=Type=oneshot' in args
|
||||
command=args[args.index('/bin/bash'):]
|
||||
state=Path(command[-1])
|
||||
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'reject':
|
||||
sys.exit('systemd refused submission')
|
||||
with open(state/'test-worker.log','w') as log:
|
||||
subprocess.Popen(command,stdin=subprocess.DEVNULL,stdout=log,stderr=subprocess.STDOUT,env=os.environ,close_fds=True,start_new_session=True)
|
||||
worker=subprocess.Popen(command,stdin=subprocess.DEVNULL,stdout=log,stderr=subprocess.STDOUT,env=os.environ,close_fds=True,start_new_session=True)
|
||||
if os.environ.get('TEST_SYSTEMD_SUBMIT') == 'accepted-error':
|
||||
sys.exit('simulated reply failure after systemd accepted the job')
|
||||
# Type=oneshot remains starting until ExecStart exits. Real systemd-run waits
|
||||
# for that start job unless --no-block is used; our old stub missed this.
|
||||
if '--no-block' not in args:
|
||||
try:
|
||||
sys.exit(worker.wait(timeout=3))
|
||||
except subprocess.TimeoutExpired:
|
||||
worker.terminate()
|
||||
worker.wait(timeout=5)
|
||||
sys.exit('oneshot start job still waiting: launcher/worker lock deadlock')
|
||||
''')
|
||||
launcher.chmod(0o755)
|
||||
|
||||
def test_detached_package_handoff_finishes_after_starter_exits(self):
|
||||
self.configure_upgrade()
|
||||
self.systemd_launcher()
|
||||
package=self.build_package()
|
||||
result=subprocess.run(['bash',str(package)],env=self.env,capture_output=True,text=True,timeout=60)
|
||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||
@@ -566,6 +584,66 @@ with open(state/'test-worker.log','w') as log:
|
||||
self.assertTrue(status.startswith('SUCCESS'),status+(state/'test-worker.log').read_text())
|
||||
self.assertIn('unabhaengig von SSH',result.stdout)
|
||||
|
||||
def test_systemd_submission_failure_preserves_installation_and_reports_result(self):
|
||||
canonical=self.configure_upgrade()
|
||||
original=canonical.read_bytes()
|
||||
self.systemd_launcher()
|
||||
package=self.build_package()
|
||||
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='reject'),capture_output=True,text=True,timeout=60)
|
||||
self.assertNotEqual(result.returncode,0)
|
||||
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||
self.assertIn('FAILED: systemd submission failed',(state/'result.txt').read_text())
|
||||
self.assertIn(str(state/'result.txt'),result.stdout)
|
||||
self.assertEqual(canonical.read_bytes(),original)
|
||||
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||
|
||||
def test_failed_submission_revokes_even_an_already_queued_worker(self):
|
||||
canonical=self.configure_upgrade()
|
||||
original=canonical.read_bytes()
|
||||
self.systemd_launcher()
|
||||
package=self.build_package()
|
||||
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='accepted-error'),capture_output=True,text=True,timeout=60)
|
||||
self.assertNotEqual(result.returncode,0)
|
||||
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||
for _ in range(100):
|
||||
log=(state/'test-worker.log').read_text()
|
||||
if 'Installation not started' in log: break
|
||||
time.sleep(.1)
|
||||
self.assertIn('submission was cancelled',log)
|
||||
self.assertIn('Installation not started',log)
|
||||
self.assertTrue((state/'result.txt').read_text().startswith('FAILED'))
|
||||
self.assertEqual(canonical.read_bytes(),original)
|
||||
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||
self.assertFalse((state/'package-rollback').exists())
|
||||
|
||||
def test_worker_lock_timeout_is_persisted_before_installation(self):
|
||||
import fcntl
|
||||
canonical=self.configure_upgrade()
|
||||
original=canonical.read_bytes()
|
||||
self.systemd_launcher()
|
||||
package=self.build_package()
|
||||
# Obtain a complete prepared state without starting an actual worker.
|
||||
result=subprocess.run(['bash',str(package)],env=dict(self.env,TEST_SYSTEMD_SUBMIT='reject'),capture_output=True,text=True,timeout=60)
|
||||
self.assertNotEqual(result.returncode,0)
|
||||
state=Path(result.stdout.split('Update-Sicherung und Protokoll: ')[1].splitlines()[0])
|
||||
(state/'result.txt').write_text('QUEUED\n')
|
||||
wrapper=self.bin/'flock'
|
||||
# Keep real flock contention; shorten only the 60s timeout in this test.
|
||||
wrapper.write_text('#!/usr/bin/env bash\nif [[ $1 == -w && $2 == 60 ]]; then shift 2; exec /usr/bin/flock -w 0 "$@"; fi\nexec /usr/bin/flock "$@"\n')
|
||||
wrapper.chmod(0o755)
|
||||
with open('/run/app-backup-upgrade.lock','a') as lock:
|
||||
fcntl.flock(lock,fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
failed=subprocess.run(['bash',str(REPO/'update.sh'),'--worker',str(state)],env=self.env,capture_output=True,text=True,timeout=10)
|
||||
self.assertNotEqual(failed.returncode,0)
|
||||
self.assertIn('Timed out waiting for update coordinator lock',(state/'update.log').read_text())
|
||||
self.assertIn('Installation not started',(state/'result.txt').read_text())
|
||||
self.assertTrue((state/'result.txt').read_text().startswith('FAILED'))
|
||||
self.assertEqual(canonical.read_bytes(),original)
|
||||
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||
|
||||
def test_cron_inventory_requires_explicit_policy_and_sorts_timer_first(self):
|
||||
canonical=self.configure_upgrade()
|
||||
cron=Path('/etc/cron.d/app-backup-test-nextcloud')
|
||||
|
||||
@@ -40,7 +40,8 @@ done
|
||||
need systemctl; need flock
|
||||
# This lock excludes two coordinators, independently from ordinary backup jobs.
|
||||
exec 8>/run/app-backup-upgrade.lock
|
||||
if [[ $UP_WORKER == true ]]; then flock -w 60 8; else flock -n 8 || die 'Another updater is preparing/running'; fi
|
||||
# Workers install their persistent error reporting before waiting on this lock.
|
||||
if [[ $UP_WORKER == false ]]; then flock -n 8 || die 'Another updater is preparing/running'; fi
|
||||
|
||||
prompt() {
|
||||
local answer
|
||||
@@ -244,20 +245,34 @@ if [[ $UP_WORKER == false ]]; then
|
||||
sha256sum "$UP_STATE/candidate.conf" | cut -d ' ' -f 1 > "$UP_STATE/candidate.sha256"
|
||||
echo "Vorpruefung erfolgreich. Writer: $(tr '\n' ' ' < "$UP_STATE/writers")"
|
||||
echo 'Jetzt folgen Installation, ein vollstaendiges Backup und Remote-Restore-Pruefung.'
|
||||
printf 'QUEUED\n' > "$UP_STATE/result.txt"
|
||||
if [[ $UP_FOREGROUND == true ]]; then
|
||||
flock -u 9; flock -u 8
|
||||
exec bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE"
|
||||
fi
|
||||
# A new systemd service inherits no shell FDs; it acquires both locks itself.
|
||||
printf 'QUEUED\n' > "$UP_STATE/result.txt"
|
||||
systemd-run --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
||||
# A oneshot start job completes only when ExecStart exits. Waiting for that
|
||||
# job while holding FD 8 deadlocks with the worker waiting for the same lock.
|
||||
# Only enqueue here; result.txt, not systemd-run's exit code, proves success.
|
||||
# The launcher must not keep copies of our locks; systemd starts a fresh worker.
|
||||
systemd-run --no-block --unit=app-backup-upgrade --collect --property=Type=oneshot \
|
||||
--property=TimeoutStartSec=24h --property=TimeoutStopSec=5min --property=KillMode=mixed \
|
||||
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" || {
|
||||
printf 'FAILED: systemd worker could not be started; installed package/config/timer unchanged.\n' > "$UP_STATE/result.txt"
|
||||
/bin/bash "$UPDATE_ROOT/update.sh" --worker "$UP_STATE" 8>&- 9>&- || {
|
||||
# Revoke the queued state while still holding the coordinator lock. Even
|
||||
# if systemd accepted the job but its reply failed, it must not install.
|
||||
printf 'FAILED: systemd submission failed; installed package/config/timer unchanged.\n' > "$UP_STATE/result.txt"
|
||||
cat "$UP_STATE/result.txt"
|
||||
echo "Details: sudo journalctl -u app-backup-upgrade.service -n 100 --no-pager"
|
||||
echo "Ergebnis: sudo cat $UP_STATE/result.txt"
|
||||
exit 1
|
||||
}
|
||||
echo 'Update laeuft unabhaengig von SSH weiter.'
|
||||
echo 'Verlauf: sudo journalctl -fu app-backup-upgrade.service'
|
||||
# Release the backup lock FIRST. Once FD 8 is free, the worker may acquire
|
||||
# both immediately; reversing the order can make its backup lock check fail.
|
||||
flock -u 9
|
||||
flock -u 8
|
||||
exec 9>&- 8>&-
|
||||
echo 'Update-Auftrag an systemd uebergeben; Ausfuehrung unabhaengig von SSH.'
|
||||
echo 'Abgeschlossen ist das Update erst bei SUCCESS in der Ergebnisdatei.'
|
||||
echo 'Verlauf: sudo journalctl -fu app-backup-upgrade.service --no-pager'
|
||||
echo "Ergebnis: sudo cat $UP_STATE/result.txt"
|
||||
exit 0
|
||||
fi
|
||||
@@ -289,6 +304,10 @@ worker_finish() {
|
||||
trap worker_finish EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
# Record lock timeouts in update.log/result.txt as well as journald. Previously
|
||||
# this wait happened before logging/traps, leaving only systemd's exit status.
|
||||
flock -w 60 8 || die 'Timed out waiting for update coordinator lock (60s); installation not started'
|
||||
[[ $(cat "$UP_STATE/result.txt") == QUEUED ]] || die 'Upgrade is not queued or submission was cancelled; rerun updater'
|
||||
printf 'RUNNING\n' > "$UP_STATE/result.txt"
|
||||
acquire_backup_lock
|
||||
export APP_BACKUP_INHERIT_LOCK=1
|
||||
|
||||
Reference in new issue
Block a user