Map-Editor Selbst-Update gehärtet (t_1457e935): KEIN Credential-Dialog mehr — git-Läufe nie interaktiv — Root-Cause: der systemd-User-Manager exportiert SSH_ASKPASS (ksshaskpass); ein nackter git-Spawn im Editor hat damit mitten in der Update-Prüfung das KDE-Login-Fenster geöffnet, und der Credential-Store (~/.git-credentials) war leer (0 Bytes). Fix (Belt AND Braces, EditorUpdater.spawn_argv): JEDE git-Ausführung läuft über /usr/bin/env mit GIT_TERMINAL_PROMPT=0 + GIT_ASKPASS=/bin/true + HOME aus der eigenen Umgebung (kein Hardcode), dazu -c credential.helper= (Helper-Reset, sonst öffnet z. B. der Windows-Credential-Manager sein GUI) + -c credential.helper=store + core.askPass=/bin/true (Askpass-Fallback tot, auch ohne env-Wrapper). Askpass-Präzedenz am PC verifiziert: core.askPass/GIT_ASKPASS schlagen geerbtes SSH_ASKPASS — Fake-Askpass startet nicht, sauberes Authentication failed Exit 128 statt GUI. Neuer Status auth_failed mit klarem UI-Text („Updates nicht prüfbar — Zugangsdaten fehlen (kein Login-Dialog geöffnet)“), Auth-Erkennung pure (looks_like_auth_error: could-not-read-Username / Authentication failed / terminal prompts disabled / Unauthorized — Connection refused ist KEIN Auth-Fehler); pull-Fehler bekommen denselben Zusatz-Hinweis. Credential-Store am PC befüllt (Forgejo-Token für 127.0.0.1:13020, chmod 600) — ls-remote/pull/ls-remote-push laufen stimmlos. Tests: test_updater +10 Checks (auth-Erkennung 4, spawn_argv-Zusammensetzung 6) → 47/47 ALL OK; komplette Suite 12/13 grün (test_testrun: 1/14 FAILED — PRE-EXISTING, per git-stash-Verfahren verifiziert, Ursache unabhängig von diesem Diff, Follow-up-Karte folgt). docs/tasten.md: Abschnitt Updates um „Zugangsdaten fehlen“ ergänzt.

This commit is contained in:
slyphax 2026-10-17 22:49:42 +02:00
parent f2b4f6a824
commit 73847ed1ce
4 changed files with 123 additions and 13 deletions

View file

@ -95,6 +95,12 @@ kein Programmdownload, nur „Versionsstände holen“:
- **Offline / Tunnel weg:** kein Banner, keine Meldung — der Editor bleibt
einfach so, wie er ist. Prüfen kann man trotzdem: die Meldung sagt es dann
in der Statuszeile.
- **Zugangsdaten fehlen** (Karte t_1457e935): die Update-Prüfung läuft
bewusst NIE interaktiv — es öffnet sich **niemals** ein Login-Fenster im
Editor. Stattdessen kommt in der Statuszeile „Updates nicht prüfbar —
Zugangsdaten fehlen“. Die Zugangsdaten liegen im Git-Credential-Store
(`~/.git-credentials`, Host `127.0.0.1:13020`); wenn sie fehlen, füllt sie
ein Eintrag der Form `http://<user>:<token>@127.0.0.1:13020` nach.
- **Fehlgeschlagener Import:** bricht das Update mit Klartext-Meldung ab —
der Editor läuft UNVERÄNDERT weiter (kein Neustart in einen kaputten Stand,
Lektion 17.10. class_name-Cache).

View file

@ -20,23 +20,79 @@ extends RefCounted
## 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)."
## Ein Git-Kommando im Projektordner ausführen. Rückgabe {ok, code, output}.
## 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 full := PackedStringArray(["-C", project_dir])
full.append_array(args)
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(git_exe, full, output, true)
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:
@ -59,7 +115,7 @@ static func head_sha(git_exe: String, project_dir: String) -> String:
## Remote-SHA per git ls-remote (cheap, lesend — keine lokalen Referenzen
## verändert). "" = nicht erreichbar / Branch fehlt.
## 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]))
@ -73,18 +129,30 @@ static func remote_sha(git_exe: String, project_dir: String, branch: String) ->
## Update-Check: lokaler HEAD vs. origin/main.
## Rückgabe {status, local, remote, error} — status:
## "update_available" | "up_to_date" | "unreachable" | "error"
## "error" = kein Repo/git nicht da (Editor-spezifisches Problem)
## "unreachable"= Origin nicht erreichbar (offline, Tunnel weg)
## "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 remote := remote_sha(git_exe, project_dir, BRANCH)
if remote.is_empty():
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 := text2.split("\n")[0].strip_edges().split("\t")[0]
if local == remote:
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
return {"status": "update_available", "local": local, "remote": remote, "error": ""}
@ -101,17 +169,20 @@ static func banner_text(local_sha: String, remote_sha: String) -> String:
## Update ausführen: git pull --ff-only (Klon zieht nur nach, dreht nichts
## um). Rückgabe {ok, output, errors}.
## um). Rückgabe {ok, output, errors}. Auth-Fehler bekommen einen klaren
## Zusatz-Hinweis (kein Login-Prompt — Karte t_1457e935).
static func pull(git_exe: String, project_dir: String) -> Dictionary:
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"]))
var errors: Array = []
var text := str(r["output"]).strip_edges()
if not bool(r["ok"]):
errors.append("git pull --ff-only fehlgeschlagen (Exit-Code %s):" % str(r["code"]))
var text := str(r["output"]).strip_edges()
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).")
return {"ok": bool(r["ok"]), "output": str(r["output"]), "errors": errors}

View file

@ -1154,6 +1154,11 @@ func _updater_check_done(result: Dictionary, report: bool) -> void:
"unreachable":
if report:
_set_status("Update-Check nicht erreichbar (offline oder Tunnel weg) — später nochmal probieren.", true)
"auth_failed":
# Karte t_1457e935: Zugangsdaten fehlen → klarer Hinweis statt
# GUI-Login-Dialog (Askpass ist im Updater abgeschaltet).
if report:
_set_status(EditorUpdater.AUTH_ERROR_TEXT, true)
_:
if report:
_set_status("Update-Check fehlgeschlagen: %s" % str(result.get("error", "")), true)

View file

@ -143,6 +143,34 @@ func _run_tests() -> void:
var bad := EditorUpdater.check_update("gibt-es-nicht-xyz", work)
_check(str(bad["status"]) == "error", "git nicht gefunden → error (nicht unreachable)")
print("== Credential-Guard (Karte t_1457e935): git wird NIE interaktiv ==")
_check(EditorUpdater.looks_like_auth_error(
"fatal: could not read Username for 'http://127.0.0.1:13020': No such device or address"),
"auth-Erkennung: could-not-read-Username")
_check(EditorUpdater.looks_like_auth_error(
"remote: Unauthorized\nfatal: Authentication failed for 'http://x/'"),
"auth-Erkennung: Authentication failed / Unauthorized")
_check(EditorUpdater.looks_like_auth_error("fatal: could not read Username; terminal prompts disabled"),
"auth-Erkennung: terminal prompts disabled")
_check(not EditorUpdater.looks_like_auth_error(
"fatal: unable to access 'http://127.0.0.1:1/': Connection refused"),
"auth-Erkennung: Connection refused ist KEIN Auth-Fehler")
var spawn := EditorUpdater.spawn_argv("git", "/projekt", PackedStringArray(["ls-remote", "origin"]))
if OS.has_feature("linux"):
_check(str(spawn[0]) == "/usr/bin/env", "argv: POSIX-Spawn über /usr/bin/env")
_check(spawn.has("GIT_TERMINAL_PROMPT=0"), "argv: GIT_TERMINAL_PROMPT=0 (kein Terminal-Prompt)")
_check(spawn.has("GIT_ASKPASS=/bin/true"), "argv: GIT_ASKPASS=/bin/true (überstimmt geerbtes SSH_ASKPASS)")
_check(spawn.has("-c") and spawn.has("credential.helper=") and spawn.has("credential.helper=store"),
"argv: Helper-Reset + ausschließlich store")
_check(spawn.has("core.askPass=/bin/true"), "argv: core.askPass-Fallback gesetzt")
var hat_home := false
for eintrag in spawn:
if str(eintrag).begins_with("HOME="):
hat_home = true
_check(hat_home, "argv: HOME aus der Umgebung mitgegeben (kein Hardcode)")
_check(str(spawn[spawn.size() - 2]) == "ls-remote" and str(spawn[spawn.size() - 1]) == "origin",
"argv: originale git-Argumente bleiben hinten")
print("== Pull ==")
_git(work, ["remote", "set-url", "origin", origin])
var pulled := EditorUpdater.pull(git, work)