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)