Files
GeoTurtle/GeoTurtl_install_v0.9.7-beta.sh

834 lines
35 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.geoturtl</string>
<key>ProgramArguments</key>
<array>
<string>$PYTHON_BIN</string>
<string>$SCRIPT</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>StandardOutPath</key>
<string>$LOGFILE</string>
<key>StandardErrorPath</key>
<string>$LOGFILE</string>
</dict>
</plist>
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"