Dateien nach "/" hochladen
This commit is contained in:
@@ -0,0 +1,833 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user