diff --git a/ansible/inventory.ini b/ansible/inventory.ini index 3091ce0..c8696ab 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -2,7 +2,7 @@ thin-client-01 ansible_host=192.168.178.241 #thin-client-02 ansible_host=192.168.0.29 #thin-client-03 ansible_host=192.168.0.23 - +#thin-client-04 ansible host=192.168.0. [rdp_clients:vars] ansible_user=root #ansible_ssh_private_key_file=~/.ssh/id_rsa diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 0200de7..c86bfde 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -200,9 +200,7 @@ # Start USB automount (udiskie) udiskie --tray --automount --notify & - - # Session Watcher läuft als systemd-Service - + # Start RDP Launcher /usr/local/bin/rdp-launcher.sh & @@ -356,19 +354,6 @@ # Auto-generated by RDP Thin Client Setup force: no - # === INSTALL PYTHON DEPENDENCIES === - - name: Install python-xlib via pip - shell: pip3 install python-xlib --break-system-packages - args: - creates: /usr/local/lib/python3.11/dist-packages/Xlib - ignore_errors: yes - - - name: Alternative - Install python-xlib from apt - apt: - name: python3-xlib - state: present - ignore_errors: yes - # === COPY SCRIPTS === - name: Copy RDP Profile Manager copy: @@ -382,60 +367,6 @@ dest: /usr/local/bin/rdp-launcher.sh mode: '0755' - - name: Copy Session Watcher - copy: - src: ../files/session-watcher.py - dest: /usr/local/bin/session-watcher.py - mode: '0755' - - # === SESSION WATCHER SYSTEMD SERVICE === - - name: Create systemd user service directory - file: - path: /home/{{ thin_client_user }}/.config/systemd/user - state: directory - owner: "{{ thin_client_user }}" - group: "{{ thin_client_user }}" - mode: '0755' - - - name: Create Session Watcher systemd service - copy: - dest: /home/{{ thin_client_user }}/.config/systemd/user/session-watcher.service - owner: "{{ thin_client_user }}" - group: "{{ thin_client_user }}" - mode: '0644' - content: | - [Unit] - Description=RDP Session Watcher - Exit Hotkey Monitor - After=graphical.target - - [Service] - Type=simple - ExecStart=/usr/local/bin/session-watcher.py - Restart=always - RestartSec=3 - Environment=DISPLAY=:0 - - [Install] - WantedBy=default.target - - - name: Create default.target.wants directory - file: - path: /home/{{ thin_client_user }}/.config/systemd/user/default.target.wants - state: directory - owner: "{{ thin_client_user }}" - group: "{{ thin_client_user }}" - mode: '0755' - - - name: Create symlink to enable Session Watcher service - file: - src: /home/{{ thin_client_user }}/.config/systemd/user/session-watcher.service - dest: /home/{{ thin_client_user }}/.config/systemd/user/default.target.wants/session-watcher.service - owner: "{{ thin_client_user }}" - group: "{{ thin_client_user }}" - state: link - force: yes - ignore_errors: yes - # === BRANDING === - name: Check if branding files exist stat: diff --git a/files/branding/boot-logo.png b/files/branding/boot-logo.png index 9ff599f..2fea727 100644 Binary files a/files/branding/boot-logo.png and b/files/branding/boot-logo.png differ diff --git a/files/branding/generate-btx-logo.py b/files/branding/generate-btx-logo.py new file mode 100755 index 0000000..3c5eada --- /dev/null +++ b/files/branding/generate-btx-logo.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +BTX-Style Boot Logo Generator +Erstellt ein professionelles Boot-Logo im BTX/Bildschirmtext Terminal-Stil +""" + +from PIL import Image, ImageDraw, ImageFont +import os + +# BTX-typische Farbpalette +BTX_BLACK = (0, 0, 0) +BTX_CYAN = (0, 255, 255) +BTX_MAGENTA = (255, 0, 255) +BTX_YELLOW = (255, 255, 0) +BTX_WHITE = (255, 255, 255) +BTX_GREEN = (0, 255, 0) +BTX_BLUE = (0, 100, 200) +BTX_DARK_CYAN = (0, 180, 180) + +# Bildgröße +WIDTH = 800 +HEIGHT = 600 + +# Erstelle Bild +img = Image.new('RGB', (WIDTH, HEIGHT), BTX_BLACK) +draw = ImageDraw.Draw(img) + +# Versuche Monospace-Font zu laden (BTX-Style) +try: + # Verschiedene Monospace-Fonts ausprobieren + font_paths = [ + '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', + '/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf', + '/usr/share/fonts/truetype/liberation2/LiberationMono-Bold.ttf', + '/System/Library/Fonts/Monaco.ttf', + ] + + font_large = None + font_medium = None + font_small = None + + for font_path in font_paths: + if os.path.exists(font_path): + font_large = ImageFont.truetype(font_path, 72) + font_medium = ImageFont.truetype(font_path, 42) + font_small = ImageFont.truetype(font_path, 28) + break + + if not font_large: + raise Exception("No font found") + +except: + print("Monospace font nicht gefunden, nutze Default-Font") + font_large = ImageFont.load_default() + font_medium = ImageFont.load_default() + font_small = ImageFont.load_default() + +# BTX-Style Border (charakteristischer Block-Rahmen) +border_width = 3 +for i in range(border_width): + draw.rectangle([i, i, WIDTH-1-i, HEIGHT-1-i], outline=BTX_CYAN) + +# Terminal/Computer ASCII-Art Symbol (oben) +terminal_y = 80 +terminal_lines = [ + "╔════════════════╗", + "║ ▓▓▓▓▓▓▓▓▓▓▓▓ ║", + "║ ▓░░░░░░░░░░▓ ║", + "║ ▓░░░░░░░░░░▓ ║", + "║ ▓▓▓▓▓▓▓▓▓▓▓▓ ║", + "╚═══════╦╦═══════╝", + " ║║ " +] + +# Zeichne Terminal-Symbol zentriert +for i, line in enumerate(terminal_lines): + bbox = draw.textbbox((0, 0), line, font=font_medium) + text_width = bbox[2] - bbox[0] + x = (WIDTH - text_width) // 2 + y = terminal_y + (i * 35) + # Doppelte Zeichen für BTX-Block-Effekt + draw.text((x+2, y+2), line, font=font_medium, fill=BTX_BLUE) # Schatten + draw.text((x, y), line, font=font_medium, fill=BTX_CYAN) + +# Haupttext "RDP THIN CLIENT" +main_text = "RDP THIN CLIENT" +bbox = draw.textbbox((0, 0), main_text, font=font_large) +text_width = bbox[2] - bbox[0] +text_x = (WIDTH - text_width) // 2 +text_y = 330 + +# BTX-Style Doppel-Rendering für Glow-Effekt +draw.text((text_x+3, text_y+3), main_text, font=font_large, fill=BTX_BLUE) # Schatten +draw.text((text_x, text_y), main_text, font=font_large, fill=BTX_YELLOW) + +# Scanline-Effekt (BTX/CRT-Monitor-Look) +for y in range(0, HEIGHT, 4): + draw.line([(border_width+5, y), (WIDTH-border_width-5, y)], fill=(10, 10, 10), width=1) + +# Firmen-Branding unten +company_text = "HackerSoft™" +bbox = draw.textbbox((0, 0), company_text, font=font_medium) +text_width = bbox[2] - bbox[0] +company_x = (WIDTH - text_width) // 2 +company_y = 470 + +draw.text((company_x+2, company_y+2), company_text, font=font_medium, fill=BTX_BLUE) +draw.text((company_x, company_y), company_text, font=font_medium, fill=BTX_MAGENTA) + +# "Hacker-Net Telekommunikation" Subtext +subtext = "Hacker-Net Telekommunikation" +bbox = draw.textbbox((0, 0), subtext, font=font_small) +text_width = bbox[2] - bbox[0] +sub_x = (WIDTH - text_width) // 2 +sub_y = 520 + +draw.text((sub_x+1, sub_y+1), subtext, font=font_small, fill=BTX_BLUE) +draw.text((sub_x, sub_y), subtext, font=font_small, fill=BTX_DARK_CYAN) + +# Dekorative Ecken (BTX-Style Blocks) +block_size = 20 +positions = [ + (border_width+10, border_width+10), # Oben links + (WIDTH-border_width-30, border_width+10), # Oben rechts + (border_width+10, HEIGHT-border_width-30), # Unten links + (WIDTH-border_width-30, HEIGHT-border_width-30) # Unten rechts +] + +for x, y in positions: + draw.rectangle([x, y, x+block_size, y+block_size], fill=BTX_MAGENTA, outline=BTX_YELLOW, width=2) + +# Status-Indicator (BTX-typisch) +status_text = "● SYSTEM READY" +bbox = draw.textbbox((0, 0), status_text, font=font_small) +text_width = bbox[2] - bbox[0] +status_x = (WIDTH - text_width) // 2 +status_y = HEIGHT - 60 + +draw.text((status_x+1, status_y+1), status_text, font=font_small, fill=BTX_BLUE) +draw.text((status_x, status_y), status_text, font=font_small, fill=BTX_GREEN) + +# Speichern +output_path = os.path.join(os.path.dirname(__file__), 'boot-logo.png') +img.save(output_path, 'PNG') +print(f"BTX-Style Boot-Logo erstellt: {output_path}") +print(f"Größe: {WIDTH}x{HEIGHT}") diff --git a/files/branding/generate-terminal-logo.py b/files/branding/generate-terminal-logo.py new file mode 100644 index 0000000..799012b --- /dev/null +++ b/files/branding/generate-terminal-logo.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Minimal Terminal-Style Boot Logo Generator +Nur Text, wie in einem echten Terminal +""" + +from PIL import Image, ImageDraw, ImageFont +import os + +# Terminal-Farbschema +BG_BLACK = (12, 12, 15) +TEXT_GREEN = (0, 255, 100) +TEXT_WHITE = (230, 230, 230) +TEXT_GRAY = (120, 120, 120) +TEXT_CYAN = (100, 200, 255) + +# Bildgröße +WIDTH = 800 +HEIGHT = 600 + +# Erstelle Bild +img = Image.new('RGB', (WIDTH, HEIGHT), BG_BLACK) +draw = ImageDraw.Draw(img) + +# Versuche Monospace-Font zu laden +try: + font_paths = [ + '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', + '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', + '/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf', + '/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf', + ] + + font_large = None + font_medium = None + font_small = None + + for font_path in font_paths: + if os.path.exists(font_path): + font_large = ImageFont.truetype(font_path, 56) + font_medium = ImageFont.truetype(font_path, 28) + font_small = ImageFont.truetype(font_path, 22) + break + + if not font_large: + raise Exception("No font found") + +except: + print("Monospace font nicht gefunden, nutze Default-Font") + font_large = ImageFont.load_default() + font_medium = ImageFont.load_default() + font_small = ImageFont.load_default() + +# Start-Position +x_offset = 60 +y_offset = 120 +line_height = 40 + +# Terminal-Output simulieren +lines = [ + ("Booting system...", TEXT_GRAY, font_medium), + ("", TEXT_GRAY, font_medium), + ("RDP THIN CLIENT SYSTEM", TEXT_WHITE, font_large), + ("", TEXT_GRAY, font_medium), + ("[ OK ] Remote Desktop Protocol initialized", TEXT_GREEN, font_medium), + ("[ OK ] Graphics subsystem ready", TEXT_GREEN, font_medium), + ("[ OK ] Audio redirection enabled", TEXT_GREEN, font_medium), + ("[ OK ] USB device support active", TEXT_GREEN, font_medium), + ("[ OK ] Network services started", TEXT_GREEN, font_medium), + ("", TEXT_GRAY, font_medium), +] + +current_y = y_offset +for line_text, color, font in lines: + if line_text: # Leere Zeilen überspringen + draw.text((x_offset, current_y), line_text, font=font, fill=color) + # Größere Abstände für Title + if font == font_large: + current_y += 80 + else: + current_y += line_height + +# Cursor am Ende +cursor_y = current_y +draw.rectangle([x_offset, cursor_y, x_offset + 12, cursor_y + 22], fill=TEXT_GREEN) + +# Footer (rechts neben dem Cursor, zentriert) +footer_text = "HackerSoft · Hacker-Net Telekommunikation" +footer_x = x_offset + 20 # 20px rechts vom Cursor +draw.text((footer_x, cursor_y), footer_text, font=font_small, fill=TEXT_GRAY) + +# Speichern +output_path = os.path.join(os.path.dirname(__file__), 'boot-logo.png') +img.save(output_path, 'PNG') +print(f"Minimal Terminal Boot-Logo erstellt: {output_path}") +print(f"Größe: {WIDTH}x{HEIGHT}") diff --git a/files/session-watcher.py b/files/session-watcher.py deleted file mode 100755 index bd9d520..0000000 --- a/files/session-watcher.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -Session Watcher - Überwacht Tastenkombination zum Verlassen der RDP-Sitzung -Speichere als: files/session-watcher.py -""" - -import subprocess -import sys -import time -import configparser -from pathlib import Path -from Xlib import X, XK, display -from Xlib.ext import record -from Xlib.protocol import rq - -CONFIG_FILE = Path.home() / ".config" / "rdp-profiles" / "profiles.ini" - -class SessionWatcher: - def __init__(self): - self.display = display.Display() - self.root = self.display.screen().root - self.ctx = None - self.pressed_keys = set() - - # Default hotkey - self.exit_hotkey = "Control+Alt+q" - - # Parse hotkey from active profile (if any) - self.load_hotkey() - - def load_hotkey(self): - """Lädt Hotkey aus dem ersten aktiven Profil""" - if not CONFIG_FILE.exists(): - return - - config = configparser.ConfigParser() - config.read(CONFIG_FILE) - - # Nutze ersten Profil-Hotkey als Standard - for section in config.sections(): - hotkey = config[section].get('exit_hotkey', '').strip() - if hotkey: - self.exit_hotkey = hotkey - break - - print(f"Exit hotkey: {self.exit_hotkey}") - - def parse_hotkey(self): - """Wandelt Hotkey-String in Keycodes um""" - parts = self.exit_hotkey.lower().split('+') - - modifiers = [] - key = None - - for part in parts: - part = part.strip() - if part in ['control', 'ctrl']: - modifiers.append('Control_L') - modifiers.append('Control_R') - elif part in ['alt']: - modifiers.append('Alt_L') - modifiers.append('Alt_R') - elif part in ['shift']: - modifiers.append('Shift_L') - modifiers.append('Shift_R') - elif part in ['super', 'win', 'meta']: - modifiers.append('Super_L') - modifiers.append('Super_R') - else: - key = part - - return modifiers, key - - def check_rdp_running(self): - """Prüft ob FreeRDP läuft""" - try: - result = subprocess.run(['pgrep', '-x', 'xfreerdp'], - capture_output=True, text=True) - return result.returncode == 0 - except: - return False - - def kill_rdp(self): - """Beendet alle RDP-Verbindungen - AGGRESSIV!""" - print("Killing RDP sessions...") - try: - # Erst SIGTERM versuchen (sauber) - result = subprocess.run(['pkill', '-15', 'xfreerdp'], check=False) - - # Warte kurz - time.sleep(0.5) - - # Prüfe ob noch läuft - check = subprocess.run(['pgrep', '-x', 'xfreerdp'], - capture_output=True, check=False) - - if check.returncode == 0: - # Immer noch da? KILL IT WITH FIRE! (SIGKILL) - print("RDP process still running, using SIGKILL...") - subprocess.run(['pkill', '-9', 'xfreerdp'], check=False) - subprocess.run(['pkill', '-9', 'xfreerdp3'], check=False) - time.sleep(0.3) - - # Auch xfreerdp3 killen (falls vorhanden) - subprocess.run(['pkill', '-9', 'xfreerdp3'], check=False) - - time.sleep(0.5) - - # Starte Profile Manager - subprocess.Popen(['/usr/local/bin/rdp-profile-manager.py']) - except Exception as e: - print(f"Error killing RDP: {e}") - - def event_handler(self, reply): - """Behandelt Tastatur-Events""" - if reply.category != record.FromServer: - return - if reply.client_swapped: - return - if not len(reply.data) or reply.data[0] < 2: - return - - data = reply.data - while len(data): - event, data = rq.EventField(None).parse_binary_value( - data, self.display.display, None, None) - - if event.type == X.KeyPress: - keysym = self.display.keycode_to_keysym(event.detail, 0) - key_name = XK.keysym_to_string(keysym) - - if key_name: - self.pressed_keys.add(key_name) - self.check_hotkey() - - elif event.type == X.KeyRelease: - keysym = self.display.keycode_to_keysym(event.detail, 0) - key_name = XK.keysym_to_string(keysym) - - if key_name and key_name in self.pressed_keys: - self.pressed_keys.discard(key_name) - - def check_hotkey(self): - """Prüft ob Exit-Hotkey gedrückt wurde""" - modifiers, key = self.parse_hotkey() - - # Check if any modifier variant is pressed - modifier_pressed = False - for mod in modifiers: - if mod in self.pressed_keys: - modifier_pressed = True - break - - if not modifier_pressed: - return - - # Check key - if key and key.lower() in [k.lower() for k in self.pressed_keys]: - # Hotkey matched! - if self.check_rdp_running(): - print("Exit hotkey detected - killing RDP session") - self.pressed_keys.clear() # Prevent multiple triggers - self.kill_rdp() - - def run(self): - """Startet Event-Loop""" - # Setup record extension - self.ctx = self.display.record_create_context( - 0, - [record.AllClients], - [{ - 'core_requests': (0, 0), - 'core_replies': (0, 0), - 'ext_requests': (0, 0, 0, 0), - 'ext_replies': (0, 0, 0, 0), - 'delivered_events': (0, 0), - 'device_events': (X.KeyPress, X.KeyRelease), - 'errors': (0, 0), - 'client_started': False, - 'client_died': False, - }] - ) - - self.display.record_enable_context(self.ctx, self.event_handler) - self.display.record_free_context(self.ctx) - - print("Session watcher started - monitoring for exit hotkey") - - while True: - event = self.display.next_event() - -if __name__ == '__main__': - try: - watcher = SessionWatcher() - watcher.run() - except KeyboardInterrupt: - print("\nSession watcher stopped") - sys.exit(0) - except Exception as e: - print(f"Error: {e}") - sys.exit(1)