#!/bin/zsh # ============================================================================= # GeoTurtl_install_v0.9.7-beta.sh # Selbstentpackendes Installationsscript für GeoTurtl v0.9.7-beta # # VERWENDUNG: # chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh # ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh # ============================================================================= set -e VERSION="0.9.7-beta" TARGET="/Applications/Scripte/python/GeoTurtl" SCRIPT="$TARGET/GeoTurtl.py" README_HTML="$TARGET/README.html" README_MD="$TARGET/README.md" STARTER="$TARGET/geoturtl_start.sh" STOPPER="$TARGET/geoturtl_stop.sh" PLIST_DIR="$HOME/Library/LaunchAgents" PLIST="$PLIST_DIR/com.user.geoturtl.plist" LOGFILE="$HOME/Library/Logs/GeoTurtl.log" PIDFILE="$HOME/Library/Application Support/GeoTurtl.pid" # Ordner des Installationsscripts (hier liegen die README-Dateien) INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)" echo "" echo "╔══════════════════════════════════════════════════╗" echo "║ 🐢 GeoTurtl v$VERSION – Installation ║" echo "╚══════════════════════════════════════════════════╝" echo "" # ── Haftungsausschluss ──────────────────────────────────────────────────────── echo "┌─────────────────────────────────────────────────────┐" echo "│ ⚠️ HAFTUNGSAUSSCHLUSS │" echo "├─────────────────────────────────────────────────────┤" echo "│ GeoTurtl wurde nach bestem Wissen und Gewissen │" echo "│ entwickelt und kostenlos zur Verfügung gestellt. │" echo "│ │" echo "│ • Die Software wird 'so wie sie ist' (as is) │" echo "│ bereitgestellt – ohne jegliche Garantie. │" echo "│ • H. Linz übernimmt keine Haftung für Schäden, │" echo "│ Datenverluste oder Beeinträchtigungen jeglicher │" echo "│ Art durch Nutzung dieser Software. │" echo "│ • Drittdienste (ipinfo.io, ipify.org) liegen │" echo "│ außerhalb der Verantwortung des Autors. │" echo "│ • Jede Person nutzt diese Software auf eigene │" echo "│ Gefahr und eigene Verantwortung. │" echo "│ │" echo "│ Durch Fortfahren erkennst du diese Bedingungen an. │" echo "└─────────────────────────────────────────────────────┘" echo "" echo -n " Installation fortsetzen? [j/N] " read CONFIRM if [[ "$CONFIRM" != "j" && "$CONFIRM" != "J" ]]; then echo " Installation abgebrochen." exit 0 fi echo "" # ── Alte Versionen entfernen ────────────────────────────────────────────────── echo "→ Prüfe auf alte Versionen …" # Alle laufenden ddIP/GeoTurtl-Prozesse beenden for PROCNAME in "ddIP.py" "GeoTurtl.py"; do PIDS=$(pgrep -f "$PROCNAME" 2>/dev/null || true) if [ -n "$PIDS" ]; then echo " ✓ Beende laufende Instanz: $PROCNAME (PID $PIDS)" pkill -f "$PROCNAME" 2>/dev/null || true sleep 1 fi done # Alte PID-Files entfernen for PIDFILE in "$HOME/Library/Application Support/ddIP.pid" "$HOME/Library/Application Support/GeoTurtl.pid"; do [ -f "$PIDFILE" ] && rm -f "$PIDFILE" && echo " ✓ PID-File entfernt: $PIDFILE" done # Alte Lock-Files entfernen for LOCKFILE in "$HOME/Library/Application Support/GeoTurtl.lock"; do [ -f "$LOCKFILE" ] && rm -f "$LOCKFILE" && echo " ✓ Lock-File entfernt: $LOCKFILE" done # Alte launchd-Agenten entfernen for PLIST in "$HOME/Library/LaunchAgents/com.user.ddip.plist" "$HOME/Library/LaunchAgents/com.user.geoturtl.plist"; do if [ -f "$PLIST" ]; then launchctl unload "$PLIST" 2>/dev/null || true rm -f "$PLIST" echo " ✓ launchd-Agent entfernt: $(basename $PLIST)" fi done # Alte Programmordner entfernen for DIR in "/Applications/Scripte/python/ddIP" "/Applications/Scripte/python/GeoTurtl"; do if [ -d "$DIR" ] && [ "$DIR" != "$TARGET" ]; then rm -rf "$DIR" echo " ✓ Alter Programmordner entfernt: $DIR" fi done # Alte Einzeldateien entfernen (ddIP ohne Unterordner) for FILE in "/Applications/Scripte/python/ddIP.py" "/Applications/Scripte/python/ddip_start.sh" "/Applications/Scripte/python/ddip_stop.sh" "/Applications/Scripte/python/geoturtl_start.sh" "/Applications/Scripte/python/geoturtl_stop.sh"; do if [ -f "$FILE" ]; then rm -f "$FILE" echo " ✓ Alte Datei entfernt: $FILE" fi done # Alte Aliases aus ~/.zshrc entfernen if grep -qE "alias (ddip|geoturtl)" ~/.zshrc 2>/dev/null; then sed -i '' '/alias ddip/d' ~/.zshrc sed -i '' '/alias geoturtl/d' ~/.zshrc echo " ✓ Alte Aliases aus ~/.zshrc entfernt" fi echo " ✓ Aufräumen abgeschlossen" echo "" # ── Python-Pakete ───────────────────────────────────────────────────────────── echo "→ Installiere Python-Pakete …" pip3 install --quiet rumps pyobjc-framework-CoreLocation echo " ✓ Pakete installiert" # ── Zielordner ──────────────────────────────────────────────────────────────── if [ ! -d "$TARGET" ]; then echo "→ Lege Ordner an: $TARGET" mkdir -p "$TARGET" else echo "→ Ordner existiert bereits: $TARGET" fi # ── README-Dateien kopieren (liegen neben dem Installationsscript) ──────────── echo "→ Kopiere Dokumentation …" if [ -f "$INSTALL_DIR/README.html" ]; then cp "$INSTALL_DIR/README.html" "$README_HTML" echo " ✓ README.html kopiert" else echo " ⚠️ README.html nicht gefunden – wird übersprungen" fi if [ -f "$INSTALL_DIR/README.md" ]; then cp "$INSTALL_DIR/README.md" "$README_MD" echo " ✓ README.md kopiert" else echo " ⚠️ README.md nicht gefunden – wird übersprungen" fi # ── GeoTurtl.py erzeugen ───────────────────────────────────────────────────── echo "→ Erzeuge GeoTurtl.py …" cat > "$SCRIPT" << 'PYEOF' #!/usr/bin/env python3 # -*- coding: utf-8 -*- # ============================================================================= # yaml # name: GeoTurtl # version: 0.9.7-beta # description: macOS Menüleisten-App – Standort + öffentliche IP-Adressen # author: H. Linz # contact: linz.on.tour@gmail.com # license: MIT # requires_python: ">=3.6" # dependencies: # - rumps # - pyobjc-framework-CoreLocation # platform: macOS (Apple Silicon & Intel) # created: 2026-06-07 # disclaimer: > # Diese Software wird ohne jegliche Garantie bereitgestellt. # Nutzung auf eigene Gefahr. Keine Haftung für Schäden. # ============================================================================= import json import os import socket import subprocess import sys import threading import time import urllib.request from datetime import datetime import objc import rumps from CoreLocation import CLGeocoder, CLLocation, CLLocationManager from Foundation import NSBundle, NSDate, NSRunLoop APP_TITLE = "🐢" APP_VERSION = "0.9.7-beta" AUTHOR = "H. Linz" CONTACT = "linz.on.tour@gmail.com" UPDATE_INTERVALS = {"Aus": 0, "1 Minute": 60, "5 Minuten": 300, "15 Minuten": 900} DEFAULT_INTERVAL = "5 Minuten" TIMEOUT = 8 LOC_TIMEOUT = 15 README_HTML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.html") # ── CoreLocation Delegate ────────────────────────────────────────────────────── class LocationDelegate(objc.lookUpClass("NSObject")): def init(self): self = objc.super(LocationDelegate, self).init() if self is None: return None self.location = None self.error = None self.done = threading.Event() return self def locationManager_didUpdateLocations_(self, manager, locations): if locations: self.location = locations[-1] manager.stopUpdatingLocation() self.done.set() def locationManager_didFailWithError_(self, manager, error): self.error = str(error) manager.stopUpdatingLocation() self.done.set() def locationManagerDidChangeAuthorization_(self, manager): status = manager.authorizationStatus() if status in (1, 2): self.error = "Ortungsdienste verweigert" self.done.set() def get_location(retries=4, delay=3): """ Versucht den Standort zu ermitteln. Bei kCLErrorLocationUnknown (Code=0) wird bis zu `retries`-mal neu gestartet – typisch wenn WLAN-Ortung kurz nach dem Login noch nicht bereit ist. Fehlercodes: Code=0 LocationUnknown – kurz warten, nochmal versuchen Code=1 Denied – Ortungsdienste verweigert Code=2 Network – Netzwerkfehler bei Ortung """ for attempt in range(1, retries + 1): delegate = LocationDelegate.alloc().init() manager = CLLocationManager.alloc().init() manager.setDelegate_(delegate) manager.setDesiredAccuracy_(10) manager.requestWhenInUseAuthorization() manager.startUpdatingLocation() deadline = time.time() + LOC_TIMEOUT while not delegate.done.is_set() and time.time() < deadline: NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2)) if not delegate.done.is_set(): raise TimeoutError( f"Standort-Timeout nach {LOC_TIMEOUT}s (Versuch {attempt}/{retries})") if delegate.error: err = delegate.error # Code=0: vorübergehend unbekannt → retry if "Code=0" in err: if attempt < retries: _log(f"Standort: kCLErrorLocationUnknown (Versuch {attempt}/{retries}) " f"– warte {delay}s …", "WARN") time.sleep(delay) continue raise RuntimeError( "Standort nach mehreren Versuchen nicht ermittelbar " "(WLAN-Ortung liefert kein Signal)") # Code=1: Berechtigung verweigert elif "Code=1" in err: raise RuntimeError( "Ortungsdienste verweigert – bitte in Systemeinstellungen " "→ Datenschutz & Sicherheit → Ortungsdienste → Terminal aktivieren") # Code=2: Netzwerkfehler elif "Code=2" in err: raise RuntimeError( "Netzwerkfehler bei Standortermittlung (kCLErrorNetwork)") else: raise RuntimeError(f"CoreLocation-Fehler: {err}") return delegate.location raise RuntimeError("Standort nach mehreren Versuchen nicht ermittelbar") def reverse_geocode(lat, lon): result = {"address": None, "done": threading.Event()} cl_loc = CLLocation.alloc().initWithLatitude_longitude_(lat, lon) geocoder = CLGeocoder.alloc().init() def handler(placemarks, error): if placemarks: pm = placemarks[0] parts = [] if pm.thoroughfare(): street = pm.thoroughfare() if pm.subThoroughfare(): street += f" {pm.subThoroughfare()}" parts.append(street) if pm.postalCode(): parts.append(pm.postalCode()) if pm.locality(): parts.append(pm.locality()) result["address"] = ", ".join(parts) if parts else "Unbekannte Adresse" else: result["address"] = "Adresse nicht verfügbar" result["done"].set() geocoder.reverseGeocodeLocation_completionHandler_(cl_loc, handler) deadline = time.time() + 10 while not result["done"].is_set() and time.time() < deadline: NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2)) return result.get("address") or "—" # ── IP-Dienste ───────────────────────────────────────────────────────────────── IPV4_SERVICES = [ {"url": "https://ipinfo.io/json", "parse": lambda d: {"ip": d.get("ip"), "org": d.get("org"), "city": d.get("city"), "region": d.get("region"), "country": d.get("country")}}, {"url": "https://api4.ipify.org?format=json", "parse": lambda d: {"ip": d.get("ip"), "org": None, "city": None, "region": None, "country": None}}, ] IPV6_SERVICES = [ {"url": "https://api6.ipify.org?format=json", "parse": lambda d: {"ip": d.get("ip")}}, {"url": "https://ipv6.icanhazip.com", "raw": True, "parse": lambda d: {"ip": d.get("ip")}}, ] def fetch_ip(service): try: req = urllib.request.Request( service["url"], headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"}) with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: body = resp.read().decode().strip() return service["parse"]({"ip": body} if service.get("raw") else json.loads(body)) except Exception: return None def get_ips(): v4, v6 = None, None def do_v4(): nonlocal v4 for svc in IPV4_SERVICES: r = fetch_ip(svc) if r and r.get("ip"): v4 = r; break def do_v6(): nonlocal v6 for svc in IPV6_SERVICES: r = fetch_ip(svc) if r and r.get("ip"): v6 = r; break t4, t6 = threading.Thread(target=do_v4), threading.Thread(target=do_v6) t4.start(); t6.start() t4.join(); t6.join() return v4, v6 # ── Logging ─────────────────────────────────────────────────────────────────── def _log(msg, level="INFO"): """Schreibt eine Zeile mit Zeitstempel und Level nach stdout (→ Log-Datei).""" ts = datetime.now().strftime("%d.%m.%Y %H:%M:%S") print(f"[{ts}] [{level}] {msg}") sys.stdout.flush() # ── Netzwerk-Diagnose ────────────────────────────────────────────────────────── def check_internet(): """ Gestaffelte Netzwerkprüfung: 1. TCP-Verbindung zu 8.8.8.8:53 → grundlegende IP-Konnektivität 2. DNS-Auflösung von google.com → DNS funktioniert 3. HTTP-Request zu ipinfo.io → Internet + Firewall OK Gibt (ok: bool, detail: str) zurück. """ # Stufe 1: TCP try: sock = socket.create_connection(("8.8.8.8", 53), timeout=3) sock.close() except OSError: return False, "Keine Netzwerkverbindung (TCP zu 8.8.8.8:53 fehlgeschlagen)" # Stufe 2: DNS try: socket.getaddrinfo("google.com", 80) except socket.gaierror: return False, "Netzwerk erreichbar, aber DNS nicht funktionsfähig" # Stufe 3: HTTP try: req = urllib.request.Request( "https://ipinfo.io/json", headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"}) with urllib.request.urlopen(req, timeout=5): pass except Exception as e: return False, f"DNS OK, aber HTTP-Anfragen blockiert oder kein Internet ({e})" return True, "OK" def diagnose_network(): """Führt Netzwerkdiagnose durch und gibt lesbaren Status zurück.""" ok, detail = check_internet() if ok: _log("Netzwerk: Verbindung OK") else: _log(f"Netzwerk: {detail}", "WARN") return ok # ── Menüleisten-App ──────────────────────────────────────────────────────────── class GeoTurtlApp(rumps.App): def __init__(self): super().__init__(APP_TITLE, quit_button=None) self._loc_data = None self._ip_data = None self._updating = False self._interval = DEFAULT_INTERVAL self._timer = None self._v4_str = None self._v6_str = None self._build_menu() threading.Thread(target=self._update_all, daemon=True).start() def _build_menu(self): self.menu.clear() self.menu.add(rumps.MenuItem("🔄 Alles aktualisieren", callback=self._on_refresh)) self.menu.add(rumps.separator) # ── Standort ────────────────────────────────────────────────────────── self.menu.add(rumps.MenuItem("📍 STANDORT", callback=None)) self._item_address = rumps.MenuItem(" Adresse …") self._item_coords = rumps.MenuItem(" Koordinaten …") self._item_altitude = rumps.MenuItem(" Höhe …") self._item_accuracy = rumps.MenuItem(" Genauigkeit …") self.menu.add(self._item_address) self.menu.add(self._item_coords) self.menu.add(self._item_altitude) self.menu.add(self._item_accuracy) self.menu.add(rumps.separator) self._item_gmaps = rumps.MenuItem(" 🗺 Google Maps öffnen", callback=self._open_gmaps) self._item_amaps = rumps.MenuItem(" 🍎 Apple Maps öffnen", callback=self._open_amaps) self._item_osm = rumps.MenuItem(" 🌍 OpenStreetMap öffnen", callback=self._open_osm) self.menu.add(self._item_gmaps) self.menu.add(self._item_amaps) self.menu.add(self._item_osm) self.menu.add(rumps.separator) # ── IP-Adressen ─────────────────────────────────────────────────────── self.menu.add(rumps.MenuItem("🌐 IP-ADRESSEN", callback=None)) self._item_v4 = rumps.MenuItem(" IPv4 …") self._item_v4_loc = rumps.MenuItem(" Standort …") self._item_v4_org = rumps.MenuItem(" Provider …") self._item_copy_v4 = rumps.MenuItem(" 📋 IPv4 kopieren", callback=self._copy_v4) self._item_v6 = rumps.MenuItem(" IPv6 …") self._item_copy_v6 = rumps.MenuItem(" 📋 IPv6 kopieren", callback=self._copy_v6) self.menu.add(self._item_v4) self.menu.add(self._item_v4_loc) self.menu.add(self._item_v4_org) self.menu.add(self._item_copy_v4) self.menu.add(rumps.separator) self.menu.add(self._item_v6) self.menu.add(self._item_copy_v6) self.menu.add(rumps.separator) # ── Auto-Update ─────────────────────────────────────────────────────── auto_menu = rumps.MenuItem("⏱ Auto-Update") for label in UPDATE_INTERVALS: item = rumps.MenuItem( f" {label}" + (" ✓" if label == self._interval else ""), callback=self._on_interval) auto_menu.add(item) self.menu.add(auto_menu) self.menu.add(rumps.separator) # ── Hilfe ───────────────────────────────────────────────────────────── hilfe_menu = rumps.MenuItem("❓ Hilfe") hilfe_menu.add(rumps.MenuItem(f" GeoTurtl v{APP_VERSION}")) hilfe_menu.add(rumps.separator) hilfe_menu.add(rumps.MenuItem(" 📖 Dokumentation öffnen", callback=self._open_readme)) hilfe_menu.add(rumps.separator) hilfe_menu.add(rumps.MenuItem(" TERMINAL-BEFEHLE")) hilfe_menu.add(rumps.MenuItem(" ▶ Starten: geoturtl_start")) hilfe_menu.add(rumps.MenuItem(" ■ Stoppen: geoturtl_stop")) hilfe_menu.add(rumps.MenuItem(" 📄 Log: tail -f ~/Library/Logs/GeoTurtl.log")) hilfe_menu.add(rumps.MenuItem(" 📝 Log öffnen", callback=self._open_log)) hilfe_menu.add(rumps.separator) hilfe_menu.add(rumps.MenuItem(" KONTAKT")) hilfe_menu.add(rumps.MenuItem(f" {AUTHOR}")) hilfe_menu.add(rumps.MenuItem(f" ✉️ {CONTACT}", callback=self._open_mail)) self.menu.add(hilfe_menu) self.menu.add(rumps.separator) # ── Zeitstempel + Beenden ───────────────────────────────────────────── self._item_updated = rumps.MenuItem(" Noch nicht aktualisiert") self.menu.add(self._item_updated) self.menu.add(rumps.separator) self.menu.add(rumps.MenuItem("✖ Beenden", callback=self._on_quit)) # ── Callbacks ───────────────────────────────────────────────────────────── def _on_refresh(self, _): if not self._updating: threading.Thread(target=self._update_all, daemon=True).start() def _on_interval(self, sender): label = sender.title.strip().rstrip(" ✓").strip() self._interval = label for item in self.menu["⏱ Auto-Update"].values(): clean = item.title.strip().rstrip(" ✓").strip() item.title = f" {clean}" + (" ✓" if clean == label else "") self._restart_timer() def _on_quit(self, _): rumps.quit_application() def _open_readme(self, _): if os.path.exists(README_HTML): os.system(f"open '{README_HTML}'") else: rumps.notification("GeoTurtl", "Dokumentation nicht gefunden", README_HTML) def _open_gmaps(self, _): if self._loc_data and "lat" in self._loc_data: lat, lon = self._loc_data["lat"], self._loc_data["lon"] os.system(f"open 'https://maps.google.com/?q={lat},{lon}'") def _open_amaps(self, _): if self._loc_data and "lat" in self._loc_data: lat, lon = self._loc_data["lat"], self._loc_data["lon"] os.system(f"open 'https://maps.apple.com/?ll={lat},{lon}&q=GeoTurtl'") def _open_osm(self, _): if self._loc_data and "lat" in self._loc_data: lat, lon = self._loc_data["lat"], self._loc_data["lon"] os.system(f"open 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=16'") def _open_log(self, _): log = os.path.expanduser("~/Library/Logs/GeoTurtl.log") if not os.path.exists(log): # Leere Log-Datei anlegen damit der Editor sie öffnen kann open(log, "a").close() os.system(f"open -e '{log}'") # -e = immer TextEdit def _open_mail(self, _): os.system(f"open 'mailto:{CONTACT}?subject=GeoTurtl%20v{APP_VERSION}'") def _copy_v4(self, _): if self._v4_str: subprocess.run("pbcopy", input=self._v4_str.encode(), check=True) rumps.notification("GeoTurtl", "IPv4 kopiert", self._v4_str) def _copy_v6(self, _): if self._v6_str: subprocess.run("pbcopy", input=self._v6_str.encode(), check=True) rumps.notification("GeoTurtl", "IPv6 kopiert", self._v6_str) # ── Timer ────────────────────────────────────────────────────────────────── def _restart_timer(self): if self._timer: self._timer.stop() self._timer = None secs = UPDATE_INTERVALS.get(self._interval, 0) if secs > 0: self._timer = rumps.Timer(self._timer_tick, secs) self._timer.start() def _timer_tick(self, _): if not self._updating: threading.Thread(target=self._update_all, daemon=True).start() # ── Daten aktualisieren ──────────────────────────────────────────────────── def _update_all(self): self._updating = True self.title = "🐢 ⟳" loc_thread = threading.Thread(target=self._fetch_location, daemon=True) ip_thread = threading.Thread(target=self._fetch_ips, daemon=True) loc_thread.start(); ip_thread.start() loc_thread.join(); ip_thread.join() self._refresh_menu() self._updating = False self._restart_timer() def _fetch_location(self): try: loc = get_location() lat = loc.coordinate().latitude lon = loc.coordinate().longitude self._loc_data = { "lat": lat, "lon": lon, "alt": loc.altitude(), "acc": loc.horizontalAccuracy(), "address": reverse_geocode(lat, lon), } _log(f"Standort: {self._loc_data['address']} " f"({lat:.6f}, {lon:.6f}), " f"Genauigkeit ±{loc.horizontalAccuracy():.0f}m") except Exception as e: _log(f"Standort: {e}", "ERROR") self._loc_data = {"error": str(e)} def _fetch_ips(self): v4, v6 = get_ips() self._ip_data = {"v4": v4, "v6": v6} self._v4_str = v4["ip"] if v4 else None self._v6_str = v6["ip"] if v6 else None if v4: _log(f"IPv4: {v4['ip']} – {v4.get('org') or '—'}") else: # Gestaffelte Diagnose nur wenn IP-Abruf fehlschlug _log("IPv4: nicht ermittelbar – starte Netzwerkdiagnose …", "WARN") net_ok = diagnose_network() if not net_ok: self._ip_data["net_error"] = "Keine Internetverbindung" _log("Kein Internet – IP-Lokalisierung nicht möglich", "ERROR") else: self._ip_data["net_error"] = "IP-Dienste nicht erreichbar" _log("Internet OK, aber alle IP-Dienste nicht erreichbar", "WARN") if v6: _log(f"IPv6: {v6['ip']}") else: _log("IPv6: nicht verfügbar (Provider unterstützt kein IPv6)") # ── Menü aktualisieren ───────────────────────────────────────────────────── def _refresh_menu(self): loc = self._loc_data or {} ips = self._ip_data or {} v4 = ips.get("v4") or {} v6 = ips.get("v6") or {} loc_error = "error" in (self._loc_data or {}) ip_error = not (self._ip_data or {}).get("v4") and not (self._ip_data or {}).get("v6") self.title = "🐢 ⚠️" if (loc_error or ip_error) else "🐢" if "error" in loc: self._item_address.title = f" ⚠️ {loc['error']}" self._item_coords.title = " Koordinaten: —" self._item_altitude.title = " Höhe: —" self._item_accuracy.title = " Genauigkeit: —" else: self._item_address.title = f" {loc.get('address', '—')}" self._item_coords.title = (f" {loc['lat']:.6f}°, {loc['lon']:.6f}°" if "lat" in loc else " —") alt = loc.get('alt', 0) alt_str = f"{alt:.0f} m ü. NN" if alt and abs(alt) > 0.5 else "nicht verfügbar (WLAN-Ortung)" self._item_altitude.title = f" Höhe: {alt_str}" self._item_accuracy.title = f" Genauigkeit: ± {loc.get('acc', 0):.0f} m" if v4.get("ip"): self._item_v4.title = f" IPv4: {v4['ip']}" loc_parts = [p for p in [v4.get("city"), v4.get("region"), v4.get("country")] if p] self._item_v4_loc.title = f" Standort: {', '.join(loc_parts) if loc_parts else '—'}" self._item_v4_org.title = f" Provider: {v4.get('org') or '—'}" else: net_err = ips.get("net_error", "") if "Kein Internet" in net_err or "Keine Internet" in net_err: self._item_v4.title = " ⚠️ Kein Internet" self._item_v4_loc.title = " Prüfe deine Netzwerkverbindung" self._item_v4_org.title = " —" elif net_err: self._item_v4.title = " ⚠️ IP-Dienste nicht erreichbar" self._item_v4_loc.title = " Internet OK, Dienste blockiert?" self._item_v4_org.title = " —" else: self._item_v4.title = " IPv4: nicht verfügbar" self._item_v4_loc.title = " Standort: —" self._item_v4_org.title = " Provider: —" if v6.get("ip"): ip6 = v6["ip"] self._item_v6.title = f" IPv6: {ip6 if len(ip6) <= 30 else ip6[:27] + '…'}" else: self._item_v6.title = " IPv6: nicht verfügbar" self._item_updated.title = f" Aktualisiert: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}" # ── Start ────────────────────────────────────────────────────────────────────── if __name__ == "__main__": import platform import fcntl # ── Single-Instance-Lock: verhindert doppelte Prozesse ──────────────────── LOCK_FILE = os.path.expanduser("~/Library/Application Support/GeoTurtl.lock") os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True) try: _lock_fh = open(LOCK_FILE, "w") fcntl.flock(_lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] [WARN] " f"GeoTurtl läuft bereits – zweite Instanz wird beendet.") sys.exit(0) # ── Dock-Icon unterdrücken ──────────────────────────────────────────────── info = NSBundle.mainBundle().infoDictionary() info["LSUIElement"] = "1" _log(f"GeoTurtl v{APP_VERSION} gestartet " f"(PID {os.getpid()}, Python {platform.python_version()}, " f"macOS {platform.mac_ver()[0]})") app = GeoTurtlApp() app.run() PYEOF chmod +x "$SCRIPT" echo " ✓ GeoTurtl.py erzeugt" # ── Starter ─────────────────────────────────────────────────────────────────── echo "→ Erzeuge Starter …" cat > "$STARTER" << STARTEOF #!/bin/zsh LOGFILE="$LOGFILE" PIDFILE="$PIDFILE" mkdir -p "\$(dirname \$PIDFILE)" if [ -f "\$PIDFILE" ]; then OLD_PID=\$(cat "\$PIDFILE") if kill -0 "\$OLD_PID" 2>/dev/null; then echo "Beende laufende GeoTurtl-Instanz (PID \$OLD_PID) …" kill "\$OLD_PID" && sleep 1 fi rm -f "\$PIDFILE" fi nohup $(which python3) "$SCRIPT" > "\$LOGFILE" 2>&1 & echo \$! > "\$PIDFILE" echo "GeoTurtl gestartet (PID \$!)" echo "Log: \$LOGFILE" STARTEOF chmod +x "$STARTER" echo " ✓ Starter erzeugt" # ── Stopper ─────────────────────────────────────────────────────────────────── echo "→ Erzeuge Stopper …" cat > "$STOPPER" << STOPEOF #!/bin/zsh PIDFILE="$PIDFILE" if [ -f "\$PIDFILE" ]; then PID=\$(cat "\$PIDFILE") if kill -0 "\$PID" 2>/dev/null; then kill "\$PID" && echo "GeoTurtl beendet (PID \$PID)." else echo "GeoTurtl lief nicht mehr." fi rm -f "\$PIDFILE" else echo "Kein PID-File — GeoTurtl läuft nicht." fi STOPEOF chmod +x "$STOPPER" echo " ✓ Stopper erzeugt" # ── launchd-Autostart ───────────────────────────────────────────────────────── echo "→ Richte Autostart via launchd ein …" mkdir -p "$PLIST_DIR" PYTHON_BIN=$(which python3) cat > "$PLIST" << PLISTEOF Label com.user.geoturtl ProgramArguments $PYTHON_BIN $SCRIPT RunAtLoad KeepAlive StandardOutPath $LOGFILE StandardErrorPath $LOGFILE PLISTEOF launchctl unload "$PLIST" 2>/dev/null || true launchctl load "$PLIST" echo " ✓ Autostart eingerichtet" # ── Aliases ─────────────────────────────────────────────────────────────────── for ENTRY in "geoturtl_start=$STARTER" "geoturtl_stop=$STOPPER"; do ANAME="${ENTRY%%=*}" AVAL="${ENTRY#*=}" ALINE="alias $ANAME='$AVAL'" if grep -q "alias $ANAME=" ~/.zshrc 2>/dev/null; then echo "→ Alias '$ANAME' bereits vorhanden, wird übersprungen." else echo "$ALINE" >> ~/.zshrc echo "→ Alias '$ANAME' eingetragen." fi done source ~/.zshrc 2>/dev/null || true # ── Fertig ──────────────────────────────────────────────────────────────────── echo "" echo "╔══════════════════════════════════════════════════╗" echo "║ ✓ GeoTurtl v$VERSION – Installation fertig! ║" echo "║ ║" echo "║ 🐢 Starten: geoturtl_start ║" echo "║ ■ Stoppen: geoturtl_stop ║" echo "║ 📄 Log: ~/Library/Logs/GeoTurtl.log ║" echo "║ 📖 Doku: $TARGET/README.html ║" echo "╚══════════════════════════════════════════════════╝" echo "" echo "HINWEIS: Beim ersten Start Ortungsdienste erlauben:" echo " Systemeinstellungen → Datenschutz & Sicherheit" echo " → Ortungsdienste → Terminal → Beim Verwenden" echo "" echo "→ Starte GeoTurtl jetzt im Hintergrund …" "$STARTER"