From 73847ed1ce88877ea808b7aee75e6d61ebc99a1e Mon Sep 17 00:00:00 2001 From: slyphax Date: Sat, 17 Oct 2026 22:49:42 +0200 Subject: [PATCH] =?UTF-8?q?Map-Editor=20Selbst-Update=20geh=C3=A4rtet=20(t?= =?UTF-8?q?=5F1457e935):=20KEIN=20Credential-Dialog=20mehr=20=E2=80=94=20g?= =?UTF-8?q?it-L=C3=A4ufe=20nie=20interaktiv=20=E2=80=94=20Root-Cause:=20de?= =?UTF-8?q?r=20systemd-User-Manager=20exportiert=20SSH=5FASKPASS=20(ksshas?= =?UTF-8?q?kpass);=20ein=20nackter=20git-Spawn=20im=20Editor=20hat=20damit?= =?UTF-8?q?=20mitten=20in=20der=20Update-Pr=C3=BCfung=20das=20KDE-Login-Fe?= =?UTF-8?q?nster=20ge=C3=B6ffnet,=20und=20der=20Credential-Store=20(~/.git?= =?UTF-8?q?-credentials)=20war=20leer=20(0=20Bytes).=20Fix=20(Belt=20AND?= =?UTF-8?q?=20Braces,=20EditorUpdater.spawn=5Fargv):=20JEDE=20git-Ausf?= =?UTF-8?q?=C3=BChrung=20l=C3=A4uft=20=C3=BCber=20/usr/bin/env=20mit=20GIT?= =?UTF-8?q?=5FTERMINAL=5FPROMPT=3D0=20+=20GIT=5FASKPASS=3D/bin/true=20+=20?= =?UTF-8?q?HOME=20aus=20der=20eigenen=20Umgebung=20(kein=20Hardcode),=20da?= =?UTF-8?q?zu=20-c=20credential.helper=3D=20(Helper-Reset,=20sonst=20?= =?UTF-8?q?=C3=B6ffnet=20z.=20B.=20der=20Windows-Credential-Manager=20sein?= =?UTF-8?q?=20GUI)=20+=20-c=20credential.helper=3Dstore=20+=20core.askPass?= =?UTF-8?q?=3D/bin/true=20(Askpass-Fallback=20tot,=20auch=20ohne=20env-Wra?= =?UTF-8?q?pper).=20Askpass-Pr=C3=A4zedenz=20am=20PC=20verifiziert:=20core?= =?UTF-8?q?.askPass/GIT=5FASKPASS=20schlagen=20geerbtes=20SSH=5FASKPASS=20?= =?UTF-8?q?=E2=80=94=20Fake-Askpass=20startet=20nicht,=20sauberes=20Authen?= =?UTF-8?q?tication=20failed=20Exit=20128=20statt=20GUI.=20Neuer=20Status?= =?UTF-8?q?=20auth=5Ffailed=20mit=20klarem=20UI-Text=20(=E2=80=9EUpdates?= =?UTF-8?q?=20nicht=20pr=C3=BCfbar=20=E2=80=94=20Zugangsdaten=20fehlen=20(?= =?UTF-8?q?kein=20Login-Dialog=20ge=C3=B6ffnet)=E2=80=9C),=20Auth-Erkennun?= =?UTF-8?q?g=20pure=20(looks=5Flike=5Fauth=5Ferror:=20could-not-read-Usern?= =?UTF-8?q?ame=20/=20Authentication=20failed=20/=20terminal=20prompts=20di?= =?UTF-8?q?sabled=20/=20Unauthorized=20=E2=80=94=20Connection=20refused=20?= =?UTF-8?q?ist=20KEIN=20Auth-Fehler);=20pull-Fehler=20bekommen=20denselben?= =?UTF-8?q?=20Zusatz-Hinweis.=20Credential-Store=20am=20PC=20bef=C3=BCllt?= =?UTF-8?q?=20(Forgejo-Token=20f=C3=BCr=20127.0.0.1:13020,=20chmod=20600)?= =?UTF-8?q?=20=E2=80=94=20ls-remote/pull/ls-remote-push=20laufen=20stimmlo?= =?UTF-8?q?s.=20Tests:=20test=5Fupdater=20+10=20Checks=20(auth-Erkennung?= =?UTF-8?q?=204,=20spawn=5Fargv-Zusammensetzung=206)=20=E2=86=92=2047/47?= =?UTF-8?q?=20ALL=20OK;=20komplette=20Suite=2012/13=20gr=C3=BCn=20(test=5F?= =?UTF-8?q?testrun:=201/14=20FAILED=20=E2=80=94=20PRE-EXISTING,=20per=20gi?= =?UTF-8?q?t-stash-Verfahren=20verifiziert,=20Ursache=20unabh=C3=A4ngig=20?= =?UTF-8?q?von=20diesem=20Diff,=20Follow-up-Karte=20folgt).=20docs/tasten.?= =?UTF-8?q?md:=20Abschnitt=20Updates=20um=20=E2=80=9EZugangsdaten=20fehlen?= =?UTF-8?q?=E2=80=9C=20erg=C3=A4nzt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tasten.md | 6 +++ src/editor/updater.gd | 97 +++++++++++++++++++++++++++++++++++++------ src/ui/main.gd | 5 +++ tests/test_updater.gd | 28 +++++++++++++ 4 files changed, 123 insertions(+), 13 deletions(-) diff --git a/docs/tasten.md b/docs/tasten.md index f0a369f..56103ce 100644 --- a/docs/tasten.md +++ b/docs/tasten.md @@ -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://:@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). diff --git a/src/editor/updater.gd b/src/editor/updater.gd index 381ffba..e0c470a 100644 --- a/src/editor/updater.gd +++ b/src/editor/updater.gd @@ -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} @@ -179,4 +250,4 @@ static func resolve_restart(cfg: ConfigFile = null) -> Dictionary: ## Projektordner im Editor-Modus („--editor“ ist nötig — ein nacktes ## „--path“ würde das Projekt als SPIEL starten, nicht als Editor). static func self_restart_args(project_dir: String) -> PackedStringArray: - return PackedStringArray(["--editor", "--path", project_dir]) + return PackedStringArray(["--editor", "--path", project_dir]) \ No newline at end of file diff --git a/src/ui/main.gd b/src/ui/main.gd index 32388a2..8eb63c8 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -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) diff --git a/tests/test_updater.gd b/tests/test_updater.gd index 19dcaf7..c937e93 100644 --- a/tests/test_updater.gd +++ b/tests/test_updater.gd @@ -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)