Dieses Skript zeichnet auf, welches Programm (und welches Fenster/welche Datei) wie lange auf welchem Rechner von welchem Benutzer geöffnet war. Es funktioniert plattformübergreifend
unter Windows, macOS und Linux. Die Log-Dateien werden täglich getrennt (z.B. "activity_log_2026-06-08.csv") im Ordner "Documents/ActivityTrackerLog" bzw. "Dokumente/ActivityTrackerLog" gespeichert.
Features:
- Dateierkennung: Erkennt automatisch geöffnete Dateinamen aus dem Fenstertitel und loggt diese separat.
- Täglicher Wechsel: Automatische Erstellung einer neuen Logdatei um Mitternacht.
- Standby-/Ruhezustand-Erkennung: Erkennt Standby-Phasen und loggt diese als System-Event.
- Shutdown/Neustart-Erkennung: Fängt Systembeendigungen und Neustarts ab und schreibt entsprechende Einträge.
- Ausfallsicher: Setzt Logs nahtlos fort und hängt Daten an bereits existierende Tagesdateien an.
Voraussetzungen/Abhängigkeiten:
- Windows: Keine (verwendet das integrierte ctypes-Modul).
- macOS: Keine (verwendet das integrierte osascript/AppleScript).
- Linux (X11): Erfordert 'xprop' (meist vorinstalliert, z.B. über apt-get install x11-utils).
Verwendung:
python3 activity_tracker.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import csv
import socket
import getpass
import datetime
import subprocess
import re
import signal
# Falls Windows aktiv ist, ctypes für direkten Win32-API-Zugriff laden
if sys.platform == "win32":
import ctypes
from ctypes import wintypes
def get_active_window_windows():
"""Gibt das aktive Programm und den Fenstertitel unter Windows zurück."""
try:
hwnd = ctypes.windll.user32.GetForegroundWindow()
if not hwnd:
return "Sperrbildschirm / Idle", "Kein aktives Fenster"
# Fenstertitel abfragen
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
title = ""
if length > 0:
buf = ctypes.create_unicode_buffer(length + 1)
ctypes.windll.user32.GetWindowTextW(hwnd, buf, length + 1)
title = buf.value
# Prozess-ID abfragen
pid = ctypes.c_ulong()
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
# Programmname (.exe) abfragen
h_process = ctypes.windll.kernel32.OpenProcess(0x1000, False, pid)
app_name = "Unbekannt"
if h_process:
try:
buf_size = ctypes.c_ulong(260)
buf_path = ctypes.create_unicode_buffer(buf_size.value)
if ctypes.windll.kernel32.QueryFullProcessImageNameW(h_process, 0, buf_path, ctypes.byref(buf_size)):
app_name = os.path.basename(buf_path.value)
finally:
ctypes.windll.kernel32.CloseHandle(h_process)
return app_name, title
except Exception as e:
return "Windows System", f"Fehler beim Ermitteln des Fensters: {str(e)}"
def get_active_window_mac():
"""Gibt das aktive Programm und den Fenstertitel unter macOS zurück."""
try:
script = """
global frontApp, windowTitle
set windowTitle to ""
tell application "System Events"
set frontApp to name of first application process whose frontmost is true
try
tell process frontApp
set windowTitle to name of window 1
end tell
on error
set windowTitle to ""
end try
end tell
return frontApp & "|||" & windowTitle
"""
proc = subprocess.Popen(
['osascript', '-e', script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = proc.communicate(timeout=2)
if proc.returncode == 0:
parts = stdout.strip().split("|||")
app_name = parts[0] if len(parts) > 0 else "Unbekannt"
window_title = parts[1] if len(parts) > 1 else ""
if not window_title:
window_title = "Kein aktiver Fenstertitel"
return app_name, window_title
else:
return "macOS System", "Hintergrund oder gesperrt"
except subprocess.TimeoutExpired:
return "macOS System", "Timeout beim Abfragen des Fensters"
except FileNotFoundError:
return "Fehler", "osascript nicht gefunden (nicht auf macOS?)"
except Exception as e:
return "macOS System", f"Fehler: {str(e)}"
def get_active_window_linux():
"""Gibt das aktive Programm und den Fenstertitel unter Linux (X11) zurück."""
try:
root_proc = subprocess.Popen(
['xprop', '-root', '-len', '14', '_NET_ACTIVE_WINDOW'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, _ = root_proc.communicate(timeout=2)
if root_proc.returncode != 0:
return "Linux System", "Kein aktives X11-Fenster gefunden (evtl. Wayland?)"
match = re.search(r"window id # (0x[0-9a-fA-F]+)", stdout)
if not match:
return "Idle / Desktop", "Kein aktives Fenster"
win_id = match.group(1)
class_proc = subprocess.Popen(
['xprop', '-id', win_id, 'WM_CLASS', 'WM_NAME'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, _ = class_proc.communicate(timeout=2)
app_name = "Unbekannt"
window_title = "Unbekannt"
class_match = re.search(r'WM_CLASS\(STRING\) = "(.*?)"', stdout)
if class_match:
app_name = class_match.group(1)
name_match = re.search(r'WM_NAME.*? = "(.*?)"', stdout)
if name_match:
window_title = name_match.group(1)
return app_name, window_title
except FileNotFoundError:
return "Fehler", "Bitte installiere 'x11-utils' für xprop"
except subprocess.TimeoutExpired:
return "Linux System", "Timeout beim Abfragen des Fensters"
except Exception as e:
return "Linux System", f"Fehler: {str(e)}"
def get_active_window():
"""Wählt je nach Betriebssystem die richtige Methode aus."""
platform = sys.platform
if platform == "win32":
return get_active_window_windows()
elif platform == "darwin":
return get_active_window_mac()
elif platform.startswith("linux"):
return get_active_window_linux()
else:
return "Unbekanntes OS", "Nicht unterstützt"
def extract_filename(title):
"""Extrahiert einen eventuellen Dateinamen aus dem Fenstertitel."""
if not title:
return ""
# Überprüfen, ob es sich wahrscheinlich um eine Webseite handelt (URLs ausschließen)
is_web = any(web in title.lower() for web in ["http:", "https:", "www.", ".com", ".org", ".net", "google", "firefox", "safari", "chrome"])
# 1. Pfade (absolut oder relativ) nach Dateinamen durchsuchen
path_match = re.search(r'([a-zA-Z]:\\[\\\w\-\._\s]+|/[\w\-\._\s/]+)', title)
if path_match and not is_web:
potential_path = path_match.group(1)
basename = os.path.basename(potential_path)
if "." in basename and len(basename.split(".")[-1]) <= 5:
ext = basename.split(".")[-1]
if len(ext) >= 2 and not ext.isdigit():
return basename
# 2. Regulärer Dateiname mit Endung (z.B. Bericht.docx, main.py, test.xlsx)
match = re.search(r'\b([\w\-äöüÄÖÜß\s]+\.[a-zA-Z0-9]{2,5})\b', title)
if match:
filename = match.group(1).strip()
ext = os.path.splitext(filename.lower())[1]
exclusions = {'.com', '.de', '.org', '.net', '.info', '.gov', '.edu', '.co'}
if ext in exclusions:
return ""
if len(ext) >= 3:
return filename
return ""
def get_daily_log_filepath(log_dir, date_obj):
"""Erstellt den Dateipfad für den heutigen Tag und schreibt ggf. den Header."""
date_str = date_obj.strftime("%Y-%m-%d")
log_file = os.path.join(log_dir, f"activity_log_{date_str}.csv")
if not os.path.exists(log_file):
try:
with open(log_file, mode="a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"Start_Time",
"End_Time",
"Duration_Seconds",
"Hostname",
"Username",
"Program",
"Window_Title",
"Active_File"
])
except Exception as e:
sys.stderr.write(f"Fehler beim Erstellen der täglichen Log-Datei: {e}\n")
return log_file
def log_activity(log_dir, start_time, end_time, duration, program, title):
"""Schreibt einen Aktivitätseintrag in die tägliche CSV-Datei."""
log_file = get_daily_log_filepath(log_dir, start_time.date())
hostname = socket.gethostname()
username = getpass.getuser()
# Datei-Name aus dem Titel extrahieren
active_file = extract_filename(title)
# Zeiten formatieren
start_str = start_time.strftime("%Y-%m-%d %H:%M:%S")
end_str = end_time.strftime("%Y-%m-%d %H:%M:%S")
# Dauer runden
duration_rounded = round(duration, 1)
try:
with open(log_file, mode="a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
start_str,
end_str,
duration_rounded,
hostname,
username,
program,
title,
active_file
])
# Ausgabe auf der Konsole
file_msg = f" | Datei: '{active_file}'" if active_file else ""
print(f"[{start_str} -> {end_str}] {duration_rounded:5.1f}s | {hostname} | {username} | {program} | {title}{file_msg}")
except Exception as e:
sys.stderr.write(f"Fehler beim Schreiben in die Log-Datei: {e}\n")
class ActivityTracker:
def __init__(self):
self.interval = 1.0
self.log_dir = self.setup_log_directory()
self.current_app = "System"
self.current_title = "Initialisierung..."
self.start_time = datetime.datetime.now()
self.is_running = True
def setup_log_directory(self):
home = os.path.expanduser("~")
possible_docs_dirs = [
os.path.join(home, "Documents"),
os.path.join(home, "Dokumente")
]
docs_dir = possible_docs_dirs[0]
for path in possible_docs_dirs:
if os.path.exists(path) and os.path.isdir(path):
docs_dir = path
break
log_dir = os.path.join(docs_dir, "ActivityTrackerLog")
try:
os.makedirs(log_dir, exist_ok=True)
except Exception as e:
log_dir = os.path.dirname(os.path.abspath(__file__))
print(f"Hinweis: Zielordner konnte nicht erstellt werden. Speichere lokal. ({e})")
return log_dir
def log_system_event(self, event_type, description, event_time=None):
"""Schreibt ein wichtiges Systemereignis (Startup, Shutdown, Standby) in das Log."""
if event_time is None:
event_time = datetime.datetime.now()
log_file = get_daily_log_filepath(self.log_dir, event_time.date())
hostname = socket.gethostname()
username = getpass.getuser()
time_str = event_time.strftime("%Y-%m-%d %H:%M:%S")
try:
with open(log_file, mode="a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
time_str,
time_str,
0.0,
hostname,
username,
event_type,
description,
""
])
print(f"[{time_str}] >>> {event_type}: {description} <<<")
except Exception as e:
sys.stderr.write(f"Fehler beim Schreiben des Systemereignisses: {e}\n")
def stop_and_log_final(self, event_type="SYSTEM_EVENT", description="HERUNTERGEFAHREN / RECHNER-NEUSTART"):
"""Beendet den Tracker sauber und schreibt den letzten Logeintrag."""
if not self.is_running:
return
self.is_running = False
end_time = datetime.datetime.now()
duration = (end_time - self.start_time).total_seconds()
if duration >= 0.5:
log_activity(self.log_dir, self.start_time, end_time, duration, self.current_app, self.current_title)
self.log_system_event(event_type, description, event_time=end_time)
def run(self):
# Startup event loggen
self.log_system_event("SYSTEM_EVENT", "SYSTEM_STARTUP (Tracker gestartet / Rechner hochgefahren)")
self.current_app, self.current_title = get_active_window()
self.start_time = datetime.datetime.now()
print("=" * 80)
print(" AKTIVITÄTS-TRACKER PRO GESTARTET")
print(f" Log-Ordner: {self.log_dir}")
print(f" Intervall: {self.interval} Sekunde(n)")
print(" Beenden mit: Strg + C")
print("=" * 80)
try:
while self.is_running:
loop_start = time.time()
time.sleep(self.interval)
actual_elapsed = time.time() - loop_start
# 1. STANDBY- / RUHEZUSTAND-ERKENNUNG
# Wenn der Schleifendurchlauf viel länger gedauert hat als das Intervall (z.B. > 5s bei 1s)
if actual_elapsed > self.interval * 5:
# Loggen bis zum Standby-Start
end_time = self.start_time + datetime.timedelta(seconds=self.interval)
duration = self.interval
if duration >= 0.5:
log_activity(self.log_dir, self.start_time, end_time, duration, self.current_app, self.current_title)
# Logge Standby Start und Standby Ende
self.log_system_event("SYSTEM_EVENT", "SYSTEM_STANDBY_START (Rechner ging in Ruhezustand)", event_time=end_time)
wakeup_time = datetime.datetime.now()
self.log_system_event("SYSTEM_EVENT", "SYSTEM_STANDBY_END / WAKEUP (Rechner aufgewacht)", event_time=wakeup_time)
# Zustand nach dem Wakeup neu setzen
self.start_time = wakeup_time
self.current_app, self.current_title = get_active_window()
continue
# 2. TAGESWECHSEL-ERKENNUNG (Mitternachts-Übergang)
current_date = datetime.date.today()
if current_date != self.start_time.date():
# Wir schließen den alten Tag exakt um 23:59:59.999
end_of_day = datetime.datetime.combine(self.start_time.date(), datetime.time(23, 59, 59, 999999))
duration = (end_of_day - self.start_time).total_seconds()
if duration >= 0.5:
log_activity(self.log_dir, self.start_time, end_of_day, duration, self.current_app, self.current_title)
# Logge den Tageswechsel als Event
self.log_system_event("SYSTEM_EVENT", "TAGESWECHSEL (Ende des Tages)", event_time=end_of_day)
start_of_new_day = datetime.datetime.combine(current_date, datetime.time(0, 0, 0, 0))
self.log_system_event("SYSTEM_EVENT", "TAGESWECHSEL (Beginn des Tages)", event_time=start_of_new_day)
# Neuer Zustand für den frischen Tag
self.start_time = start_of_new_day
self.current_app, self.current_title = get_active_window()
continue
# 3. REGULÄRES FENSTER-TRACKING
new_app, new_title = get_active_window()
if (new_app != self.current_app) or (new_title != self.current_title):
end_time = datetime.datetime.now()
duration = (end_time - self.start_time).total_seconds()
if duration >= 0.5:
log_activity(self.log_dir, self.start_time, end_time, duration, self.current_app, self.current_title)
# Zustand aktualisieren
self.current_app, self.current_title = new_app, new_title
self.start_time = end_time
except KeyboardInterrupt:
self.stop_and_log_final("SYSTEM_EVENT", "SYSTEM_SHUTDOWN_OR_TERMINATION (Manuell durch Strg+C beendet)")
def main():
tracker = ActivityTracker()
# Registriere Signal-Handler für sauberes Beenden bei Shutdown/Reboots
def make_signal_handler(sig_name):
def signal_handler(signum, frame):
tracker.stop_and_log_final("SYSTEM_EVENT", f"HERUNTERGEFAHREN / RECHNER-NEUSTART (Signal: {sig_name})")
sys.exit(0)
return signal_handler
try:
signal.signal(signal.SIGTERM, make_signal_handler("SIGTERM"))
except ValueError:
pass
try:
signal.signal(signal.SIGINT, make_signal_handler("SIGINT (Strg+C)"))
except ValueError:
pass
if sys.platform != "win32":
try:
signal.signal(signal.SIGHUP, make_signal_handler("SIGHUP"))
except ValueError:
pass
# Starte den Tracker Loop
tracker.run()
if __name__ == "__main__":
main()
Alles anzeigen
Kommentare