428 lines
No EOL
20 KiB
GDScript
428 lines
No EOL
20 KiB
GDScript
class_name EditorUpdater
|
|
extends RefCounted
|
|
## Editor-Selbstupdate (Karte t_d95f087a) — Update-Check über Git, Update-
|
|
## Ausführung (git pull + Godot-Import-Lauf) und plattformneutrale Restart-
|
|
## Auflösung. Alles static und ohne UI (tests/test_updater.gd).
|
|
##
|
|
## Regeln (Karte + Sly-Ansatz „das Programm sich selbst updaten lassen“):
|
|
## - Transport = Git (Klon des Editors), KEIN Binary-Download.
|
|
## - Keine Linux-Pfade hartkodiert: git (default "git") und ein optionaler
|
|
## Restart-Befehl kommen aus user://scopedvr_editor.cfg, Abschnitt [update]
|
|
## (git_command / restart_command). Ohne Konfiguration: unter Linux wird
|
|
## erkannt, ob eine systemd-User-Unit „map-editor“ existiert — dann klingelt
|
|
## ein Transient-Timer-Kick (systemd-run) den Unit-Neustart (der Kick läuft
|
|
## im EIGENEN Scope und überlebt die Stop-Phase). Sonst: der Editor startet
|
|
## sich selbst neu (mode "self", self_restart_args).
|
|
## - Import-Lauf-Lektion 17.10.: ein --import-Lauf kann mit Exit-Code 0
|
|
## ENDEN und trotzdem Parse-/Script-Fehler enthalten (class_name-Cache).
|
|
## Die Ausgabe wird deshalb zusätzlich gescannt — bei Fehlern wird NICHT
|
|
## neu gestartet (Abbruch mit Meldung), sonst würde Sly einen Editor ohne
|
|
## Map-Ansicht sehen.
|
|
## - Offline / Tunnel weg → status "unreachable" → die UI bleibt still
|
|
## (kein Banner, kein Fehler-Geballer).
|
|
## - Git darf NIE interaktiv werden (Karte t_1457e935): kein GUI-Credential-
|
|
## Dialog mitten im Editor! Jeder git-Lauf läuft deshalb mit
|
|
## credential.helper=store und abgeschaltetem Askpass (spawn_argv) —
|
|
## fehlen die Zugangsdaten, endet der Lauf sofort mit einer klaren
|
|
## Fehlermeldung (status "auth_failed"), nie mit einem Prompt oder Hang.
|
|
## Hintergrund 17.10.: der systemd-User-Manager exportiert SSH_ASKPASS
|
|
## (ksshaskpass) — ein nackter git-Spawn hat damit mitten im Editor das
|
|
## Login-Fenster geöffnet. GIT_ASKPASS-/core.askPass-Override schlägt das
|
|
## geerbte SSH_ASKPASS (verifiziert am PC, 17.10.).
|
|
|
|
const BRANCH := "main"
|
|
const UNIT_NAME := "map-editor"
|
|
|
|
## Statuszeilen-Text für den auth_failed-Pfad (UI + Tests).
|
|
const AUTH_ERROR_TEXT := "Updates nicht prüfbar — Zugangsdaten fehlen (kein Login-Dialog geöffnet)."
|
|
|
|
|
|
## Belt-and-braces (Karte t_1457e935): argv für einen NICHT-interaktiven
|
|
## git-Lauf bauen. Rückgabe: [0] = Programm, Rest = Argumente.
|
|
## - "-c credential.helper=" leert ERST alle geerbten Helfer (sonst öffnet
|
|
## z. B. der Windows-Credential-Manager sein GUI-Fenster), danach
|
|
## "-c credential.helper=store": ausschließlich der Credential-Store
|
|
## (~/.git-credentials per HOME).
|
|
## - POSIX: Spawn über /usr/bin/env mit GIT_TERMINAL_PROMPT=0 und
|
|
## GIT_ASKPASS=/bin/true — das überstimmt auch ein GEERBTES SSH_ASKPASS
|
|
## (KDE/GNOME-Login-Dialog!). HOME kommt aus der eigenen Umgebung
|
|
## (OS.get_environment, kein Hardcode) und wird nur mitgegeben, wenn da.
|
|
## - Fallback ohne env-Wrapper (env fehlt / kein HOME): trotzdem kein Prompt
|
|
## möglich, weil core.askPass=/bin/true das Askpass-Fallback abschaltet.
|
|
static func spawn_argv(git_exe: String, project_dir: String, args: PackedStringArray) -> PackedStringArray:
|
|
var git_args := PackedStringArray(["-C", project_dir,
|
|
"-c", "credential.helper=", "-c", "credential.helper=store"])
|
|
if not OS.has_feature("windows"):
|
|
git_args.append_array(PackedStringArray(["-c", "core.askPass=/bin/true"]))
|
|
git_args.append_array(args)
|
|
var home := OS.get_environment("HOME")
|
|
if not OS.has_feature("windows") and FileAccess.file_exists("/usr/bin/env") \
|
|
and not home.is_empty():
|
|
var out := PackedStringArray(["/usr/bin/env",
|
|
"GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=/bin/true", "HOME=" + home])
|
|
out.append(git_exe)
|
|
out.append_array(git_args)
|
|
return out
|
|
var out := PackedStringArray([git_exe])
|
|
out.append_array(git_args)
|
|
return out
|
|
|
|
|
|
## Ein Git-Kommando im Projektordner ausführen — IMMER nicht-interaktiv
|
|
## (spawn_argv). Rückgabe {ok, code, output} (output = stdout+stderr gemischt,
|
|
## damit Fehlerklassifikation sehen kann, was git gesagt hat).
|
|
static func git_run(git_exe: String, project_dir: String, args: PackedStringArray) -> Dictionary:
|
|
var argv := spawn_argv(git_exe, project_dir, args)
|
|
var exe := str(argv[0])
|
|
var rest := argv.slice(1)
|
|
var output: Array = []
|
|
var code := OS.execute(exe, rest, output, true)
|
|
var text := ""
|
|
for line in output:
|
|
text += str(line)
|
|
return {"ok": code == 0, "code": code, "output": text}
|
|
|
|
|
|
## Pure Prüfung (testbar): deutet die git-Ausgabe auf FEHLENDE Zugangsdaten?
|
|
## (Askpass ist abgeschaltet — „could not read Username“ ist dann der
|
|
## dokumentierte Nicht-Interaktiv-Fehler, kein Anlass für einen Retry.)
|
|
static func looks_like_auth_error(output: String) -> bool:
|
|
return output.contains("could not read Username") \
|
|
or output.contains("Authentication failed") \
|
|
or output.contains("terminal prompts disabled") \
|
|
or output.contains("Unauthorized")
|
|
|
|
|
|
## Git-Binary: Konfiguration (user://scopedvr_editor.cfg → [update]
|
|
## git_command) gewinnt, sonst das Standard-"git" (PATH).
|
|
static func git_exe(cfg: ConfigFile = null) -> String:
|
|
if cfg != null:
|
|
var custom := str(cfg.get_value("update", "git_command", ""))
|
|
if not custom.is_empty():
|
|
return custom
|
|
return "git"
|
|
|
|
|
|
## Lokaler HEAD-SHA. "" = kein Git-Repository (oder git nicht gefunden).
|
|
static func head_sha(git_exe: String, project_dir: String) -> String:
|
|
var r := git_run(git_exe, project_dir, PackedStringArray(["rev-parse", "HEAD"]))
|
|
if not bool(r["ok"]):
|
|
return ""
|
|
var text := str(r["output"]).strip_edges()
|
|
if text.is_empty():
|
|
return ""
|
|
return text.split("\n")[0].strip_edges()
|
|
|
|
|
|
## Remote-SHA per git ls-remote (cheap, lesend — keine lokalen Referenzen
|
|
## verändert). "" = nicht erreichbar / Branch fehlt / Credentials fehlen.
|
|
static func remote_sha(git_exe: String, project_dir: String, branch: String) -> String:
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"ls-remote", "origin", "refs/heads/%s" % branch]))
|
|
if not bool(r["ok"]):
|
|
return ""
|
|
var text := str(r["output"]).strip_edges()
|
|
if text.is_empty():
|
|
return ""
|
|
return sha_aus_ls_remote(text, branch)
|
|
|
|
|
|
## Update-Check: lokaler HEAD vs. origin/main.
|
|
## Rückgabe {status, local, remote, error} — status:
|
|
## "update_available" | "up_to_date" | "unreachable" | "error" | "auth_failed"
|
|
## "error" = kein Repo/git nicht da (Editor-spezifisches Problem)
|
|
## "unreachable" = Origin nicht erreichbar (offline, Tunnel weg)
|
|
## "auth_failed" = Zugangsdaten fehlen → UI zeigt klaren Hinweis, NIEMALS
|
|
## einen Login-Prompt (Karte t_1457e935).
|
|
static func check_update(git_exe: String, project_dir: String) -> Dictionary:
|
|
var local := head_sha(git_exe, project_dir)
|
|
if local.is_empty():
|
|
return {"status": "error", "local": "", "remote": "",
|
|
"error": "Kein Git-Repository (oder git nicht gefunden)."}
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"ls-remote", "origin", "refs/heads/%s" % BRANCH]))
|
|
if not bool(r["ok"]):
|
|
var text := str(r["output"])
|
|
if looks_like_auth_error(text):
|
|
return {"status": "auth_failed", "local": local, "remote": "",
|
|
"error": AUTH_ERROR_TEXT}
|
|
return {"status": "unreachable", "local": local, "remote": "",
|
|
"error": "Origin nicht erreichbar (offline oder Tunnel weg)."}
|
|
var text2 := str(r["output"]).strip_edges()
|
|
if text2.is_empty():
|
|
return {"status": "unreachable", "local": local, "remote": "",
|
|
"error": "Origin nicht erreichbar (offline oder Tunnel weg)."}
|
|
var remote := sha_aus_ls_remote(text2, BRANCH)
|
|
if remote.is_empty():
|
|
# Kein echter Commit in der Ausgabe (nur Warnungen/leer) — NICHT als
|
|
# Update deuten, sonst hängt das Banner endlos (Karte t_50a07d4b-Live-Bug).
|
|
return {"status": "unreachable", "local": local, "remote": "",
|
|
"error": "Origin lieferte keinen Commit (nur Warnungen)."}
|
|
if local == remote:
|
|
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
|
|
# Befund 25.10.: „Update verfügbar“ blieb nach dem Update stehen, weil nur
|
|
# HEAD != origin/main geprüft wurde. Steht der Klon auf einem anderen
|
|
# Branch oder VOR main (z. B. Feature-Branch), ist das KEIN Update:
|
|
# nur melden, wenn der Remote-Commit NICHT schon in HEAD steckt.
|
|
if remote_in_head(git_exe, project_dir, remote):
|
|
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
|
|
return {"status": "update_available", "local": local, "remote": remote, "error": ""}
|
|
|
|
|
|
## Ist `remote` schon Vorfahre von HEAD (Klon gleich oder VORAUS)? Unbekannter
|
|
## Commit (Exit 128) = noch nicht geholt = echtes Update → false.
|
|
static func remote_in_head(git_exe: String, project_dir: String, remote: String) -> bool:
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"merge-base", "--is-ancestor", remote, "HEAD"]))
|
|
return bool(r["ok"])
|
|
|
|
|
|
## Aus der gemischten ls-remote-Ausgabe den Commit für `branch` lesen.
|
|
## WARUM: `git_run` mischt stdout+stderr, und der git-Client schreibt bei einer
|
|
## HTTP-Umleitung zuerst "warning: Umleitung nach ..." — die alte Fassung nahm
|
|
## stumpf die ERSTE Zeile und hielt "warning" für den Ziel-Commit. Ergebnis war
|
|
## ein Banner "Update verfügbar: <sha> → warning", das nie verschwand, obwohl
|
|
## der Pull korrekt lief (live belegt 20.10., Klon stand auf origin/main).
|
|
## Regeln: nur Zeilen "<sha>\t<ref>" zählen, sha = echter Hex-String.
|
|
static func sha_aus_ls_remote(text: String, branch: String) -> String:
|
|
var ziel := "refs/heads/%s" % branch
|
|
var locker := ""
|
|
for zeile in text.split("\n"):
|
|
var z := zeile.strip_edges()
|
|
if z.is_empty():
|
|
continue
|
|
var klein := z.to_lower()
|
|
if klein.begins_with("warning") or klein.begins_with("hint") \
|
|
or klein.begins_with("fatal") or klein.begins_with("error"):
|
|
continue
|
|
var teile := z.split("\t")
|
|
if teile.size() < 2:
|
|
continue
|
|
var sha := teile[0].strip_edges()
|
|
if not ist_sha(sha):
|
|
continue
|
|
if teile[1].strip_edges() == ziel:
|
|
return sha
|
|
if locker.is_empty():
|
|
locker = sha
|
|
return locker
|
|
|
|
|
|
## Sieht der Text wie ein Git-Objekt-Hash aus (7-40 Hex-Zeichen)?
|
|
static func ist_sha(text: String) -> bool:
|
|
var t := text.strip_edges()
|
|
if t.length() < 7 or t.length() > 40:
|
|
return false
|
|
for i in t.length():
|
|
var c := t[i]
|
|
if c >= "0" and c <= "9":
|
|
continue
|
|
if c >= "a" and c <= "f":
|
|
continue
|
|
if c >= "A" and c <= "F":
|
|
continue
|
|
return false
|
|
return true
|
|
|
|
|
|
## Kurzer SHA für Anzeigen (7 Zeichen, wie git log --oneline).
|
|
static func short_sha(sha: String) -> String:
|
|
return sha.substr(0, 7)
|
|
|
|
|
|
## Banner-Text (pure, testbar).
|
|
static func banner_text(local_sha: String, remote_sha: String) -> String:
|
|
return "🔄 Update verfügbar: %s → %s" % [short_sha(local_sha), short_sha(remote_sha)]
|
|
|
|
|
|
## Klartext-Status für die Statuszeile (Karte t_f73a1cc5): Wenn lokale
|
|
## (meist von Godot erzeugte) Dateien das Update blockieren WÜRDEN, legt der
|
|
## Pull sie selbst beiseite — Sly liest diesen Satz statt einer rohen Git-
|
|
## Zeile („Bitte verschieben oder entfernen Sie diese…“).
|
|
const AUTO_STASH_INFO_TEXT := "Update blockiert: eigene erzeugte Dateien — werden automatisch beiseitegelegt und das Update läuft weiter."
|
|
## Ehrliche Meldung, wenn das Zurückholen kollidiert ist (Stash bleibt als
|
|
## Sicherung liegen; Godot baut die erzeugten Dateien beim Import neu).
|
|
const STASH_KEPT_TEXT := "Erzeugte Dateien bleiben beiseitegelegt — beim nächsten Start neu aufgebaut (Sicherungs-Stash bleibt liegen, nichts verloren)."
|
|
## Kennung der eigenen Auto-Stashes (Verwerfen alter Einträge, damit sie
|
|
## sich nicht anhäufen).
|
|
const AUTO_STASH_MESSAGE := "auto: Selbst-Update"
|
|
|
|
|
|
## Update ausführen: git pull --ff-only (Klon zieht nur nach, dreht nichts
|
|
## um). Vorher werden lokal Erzeugtes/Geändertes AUTOMATISCH beiseitegelegt
|
|
## (Karte t_f73a1cc5): `git stash push -u` erfasst GEÄNDERT **und** UNTRACKED
|
|
## Dateien (`--autostash` allein reicht nicht — es lässt untracked liegen;
|
|
## genau die haben am 19.10. den Pull blockiert). Nach dem Pull wird das
|
|
## Beiseitegelegte zurückgeholt; kollidiert das (Typfall: lose `.import`-
|
|
## Datei trifft auf dieselbe Datei neu getrackt), bleibt der Stash als
|
|
## Sicherung liegen und die Statuszeile bekommt den Klartext — NIE ein halb
|
|
## gepoppter Zustand. Rückgabe {ok, output, errors, info} (info = Klartext
|
|
## fürs Statuszeile-Fenster, leer wenn nichts besonderes passiert ist).
|
|
static func pull(git_exe: String, project_dir: String) -> Dictionary:
|
|
var stash_info := _stash_local_changes(git_exe, project_dir)
|
|
# Gezielt origin/main ziehen (nicht den Upstream des aktuellen Branches).
|
|
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only", "origin", BRANCH]))
|
|
var errors: Array = []
|
|
var text := str(r["output"]).strip_edges()
|
|
var info := ""
|
|
if not bool(r["ok"]):
|
|
# Pull fehlgeschlagen (z. B. offline): Beiseitegelegtes zurückholen,
|
|
# damit nichts „verschwindet", wenn der Fehler nur am Netz lag.
|
|
if not stash_info.is_empty():
|
|
_restore_stash(git_exe, project_dir)
|
|
errors.append("git pull --ff-only fehlgeschlagen (Exit-Code %s):" % str(r["code"]))
|
|
if not text.is_empty():
|
|
errors.append(text)
|
|
else:
|
|
errors.append("(keine Ausgabe — git nicht gefunden oder Verbindung sofort weg)")
|
|
if looks_like_auth_error(text):
|
|
errors.append("Zugangsdaten fehlen — es wird bewusst KEIN Login-Dialog geöffnet (Credential-Store prüfen).")
|
|
else:
|
|
if not stash_info.is_empty():
|
|
info = AUTO_STASH_INFO_TEXT
|
|
var restored := _restore_stash(git_exe, project_dir)
|
|
if not bool(restored["ok"]):
|
|
info += "\n" + STASH_KEPT_TEXT
|
|
# Der Map-Kern ist ein Submodul: ein Pull allein holt ihn nicht nach.
|
|
var sub := git_run(git_exe, project_dir, PackedStringArray(["submodule", "update", "--init", "--recursive"]))
|
|
if not bool(sub["ok"]):
|
|
errors.append("git submodule update fehlgeschlagen (Exit-Code %s):" % str(sub["code"]))
|
|
errors.append(str(sub["output"]).strip_edges())
|
|
if looks_like_auth_error(str(sub["output"])):
|
|
errors.append("Zugangsdaten fehlen — es wird bewusst KEIN Login-Dialog geöffnet (Credential-Store prüfen).")
|
|
return {"ok": false, "output": str(sub["output"]), "errors": errors, "info": info}
|
|
return {"ok": bool(r["ok"]), "output": str(r["output"]), "errors": errors, "info": info}
|
|
|
|
|
|
## Lokales Beiseitelegen vor dem Pull (Karte t_f73a1cc5). Rückgabe: Referenz
|
|
## auf den eigenen Auto-Stash (z. B. "stash@{0}") oder "" wenn es nichts zu
|
|
## beiseitelegen gab / der Stash nicht angelegt werden konnte.
|
|
## - Zuerst alte EIGENE Auto-Stashes verwerfen (gleiche Kennung) — sie
|
|
## enthalten nur erzeugte Dateien und dürfen sich nicht anhäufen.
|
|
## - "No local changes to save" endet ebenfalls mit Exit 0 → NICHT am Text
|
|
## matchen, sondern prüfen, ob ein Eintrag mit unserer Kennung entstand.
|
|
static func _stash_local_changes(git_exe: String, project_dir: String) -> String:
|
|
_drop_auto_stashes(git_exe, project_dir)
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"stash", "push", "-u", "-m", AUTO_STASH_MESSAGE]))
|
|
if not bool(r["ok"]):
|
|
return ""
|
|
return _auto_stash_ref(git_exe, project_dir)
|
|
|
|
|
|
## Referenz des eigenen Auto-Stashes ("stash@{N}") oder "".
|
|
static func _auto_stash_ref(git_exe: String, project_dir: String) -> String:
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"stash", "list", "--pretty=format:%gd|%s"]))
|
|
if not bool(r["ok"]):
|
|
return ""
|
|
for line in str(r["output"]).split("\n"):
|
|
var parts := str(line).strip_edges().split("|", true, 1)
|
|
if parts.size() == 2 and str(parts[1]).contains(AUTO_STASH_MESSAGE):
|
|
return str(parts[0])
|
|
return ""
|
|
|
|
|
|
## Alle EIGENEN Auto-Stashes (Kennung AUTO_STASH_MESSAGE) verwerfen — von
|
|
## hinten, weil die Indizes sich beim Drop verschieben.
|
|
static func _drop_auto_stashes(git_exe: String, project_dir: String) -> void:
|
|
var r := git_run(git_exe, project_dir, PackedStringArray([
|
|
"stash", "list", "--pretty=format:%gd|%s"]))
|
|
if not bool(r["ok"]):
|
|
return
|
|
var refs: Array = []
|
|
for line in str(r["output"]).split("\n"):
|
|
var parts := str(line).strip_edges().split("|", true, 1)
|
|
if parts.size() == 2 and str(parts[1]).contains(AUTO_STASH_MESSAGE):
|
|
refs.append(str(parts[0]))
|
|
refs.reverse()
|
|
for ref in refs:
|
|
git_run(git_exe, project_dir, PackedStringArray(["stash", "drop", ref]))
|
|
|
|
|
|
## Beiseitegelegtes zurückholen. apply (nicht pop), damit die Referenz nicht
|
|
## am Stack verschiebt; bei Erfolg wird der Eintrag gedroppt. Kollidiert das
|
|
## Zurückholen (Typfall: erzeugte Datei ist inzwischen getrackt), wird mit
|
|
## `git reset --hard` der halb gepoppte Zustand weggeräumt und der Stash ALS
|
|
## SICHERUNG LIEGEN GELASSEN — kein Datenverlust, Godot erzeugt die Dateien
|
|
## beim nächsten Import-Lauf neu. Bewusst KEIN `git clean`: untracked
|
|
## Nutzdaten (z. B. Maps/) dürfen nie angefasst werden.
|
|
## Rückgabe {ok, kept, output}.
|
|
static func _restore_stash(git_exe: String, project_dir: String) -> Dictionary:
|
|
var ref := _auto_stash_ref(git_exe, project_dir)
|
|
if ref.is_empty():
|
|
return {"ok": true, "kept": false, "output": ""}
|
|
var r := git_run(git_exe, project_dir, PackedStringArray(["stash", "apply", ref]))
|
|
if bool(r["ok"]):
|
|
git_run(git_exe, project_dir, PackedStringArray(["stash", "drop", ref]))
|
|
return {"ok": true, "kept": false, "output": str(r["output"])}
|
|
git_run(git_exe, project_dir, PackedStringArray(["reset", "--hard"]))
|
|
return {"ok": false, "kept": true, "output": str(r["output"])}
|
|
|
|
|
|
## Import-Lauf: <godot> --headless --path <projekt> --import. Muss NACH dem
|
|
## Pull laufen (neue class_name-Dateien in den Cache), FEHLSCHLAG = Update-
|
|
## Abbruch OHNE Neustart (Lektion 17.10.). Rückgabe {ok, errors, output}.
|
|
static func run_import(godot_exe: String, project_dir: String) -> Dictionary:
|
|
var args := PackedStringArray(["--headless", "--path", project_dir, "--import"])
|
|
var output: Array = []
|
|
var code := OS.execute(godot_exe, args, output, true)
|
|
var errors := import_errors(output)
|
|
if code != 0:
|
|
errors.append("Import-Lauf fehlgeschlagen (Exit-Code %s)" % str(code))
|
|
return {"ok": errors.is_empty(), "errors": errors, "output": _output_text(output)}
|
|
|
|
|
|
## Pure Prüfung (testbar): enthält die Import-Ausgabe Parse-/Script-Fehler?
|
|
## Exit-Code 0 allein beweist NICHT einen sauberen Import (Lektion 17.10.).
|
|
static func import_errors(output: Array) -> Array:
|
|
var errors: Array = []
|
|
for line in output:
|
|
var text := str(line)
|
|
if text.contains("Parse Error") or text.contains("SCRIPT ERROR"):
|
|
errors.append(text.strip_edges())
|
|
return errors
|
|
|
|
|
|
static func _output_text(output: Array) -> String:
|
|
var text := ""
|
|
for line in output:
|
|
text += str(line)
|
|
return text
|
|
|
|
|
|
## Restart-Auflösung (plattformneutral, getestet):
|
|
## 1. cfg [update] restart_command gesetzt → mode "command", argv = an
|
|
## Leerzeichen gesplittet. (z. B. systemd-Unit-Kick, Batch-Skript — jede
|
|
## Plattform setzt hier ihren eigenen Befehl ein.)
|
|
## 2. Linux + systemd-User-Unit „map-editor“ existiert → Transient-Timer-
|
|
## Kick (systemd-run, 3 s): der Kick läuft in einem EIGENEN Scope und
|
|
## wird beim Stoppen des Editors (Unit-Neustart) nicht mitgekillt.
|
|
## 3. Sonst: mode "self" — der Editor startet sich selbst neu (gleiche
|
|
## Godot-Binary, --editor --path <projekt>) und beendet sich danach.
|
|
static func resolve_restart(cfg: ConfigFile = null) -> Dictionary:
|
|
if cfg != null:
|
|
var cmd := str(cfg.get_value("update", "restart_command", ""))
|
|
if not cmd.is_empty():
|
|
return {"mode": "command", "argv": PackedStringArray(cmd.split(" ", false))}
|
|
if OS.has_feature("linux"):
|
|
var probe: Array = []
|
|
var code := OS.execute("systemctl", PackedStringArray([
|
|
"--user", "list-unit-files", UNIT_NAME + ".service", "--no-legend"]),
|
|
probe, true)
|
|
var text := ""
|
|
for line in probe:
|
|
text += str(line)
|
|
if code == 0 and not text.strip_edges().is_empty():
|
|
return {"mode": "command", "argv": PackedStringArray([
|
|
"systemd-run", "--user", "--on-active=3s",
|
|
"systemctl", "--user", "restart", UNIT_NAME + ".service"])}
|
|
return {"mode": "self", "argv": PackedStringArray()}
|
|
|
|
|
|
## Argumente für den Selbst-Restart: dieselbe Godot-Binary startet die
|
|
## Map-Editor-APP über deren main.tscn (kein "--editor" — das würde den
|
|
## Godot-Editor-Shell öffnen, nicht unsere App; Live-Befund 17.10.:
|
|
## Nach dem Selbst-Update ging der Godot-Editor auf statt dem Map Editor).
|
|
static func self_restart_args(project_dir: String) -> PackedStringArray:
|
|
return PackedStringArray(["--path", project_dir]) |