834 lines
35 KiB
Bash
834 lines
35 KiB
Bash
#!/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"
|