#!/usr/bin/env python3 """ Digital Signage Client Setup Script Setup script for desktop Ubuntu client devices This script: 1. Downloads the latest client agent from GitHub 2. Installs VLC media player if needed 3. Asks user for configuration details 4. Sets up environment and systemd service 5. Tests the connection Usage: curl -L https://raw.githubusercontent.com/tbnobed/signage/main/setup_client.py | python3 # OR wget https://raw.githubusercontent.com/tbnobed/signage/main/setup_client.py && python3 setup_client.py """ import os import sys import subprocess import urllib.request import urllib.error import json import getpass import platform import shutil import time from pathlib import Path # Configuration GITHUB_REPO = "https://raw.githubusercontent.com/tbnobed/signage/main" CLIENT_SCRIPT_URL = f"{GITHUB_REPO}/client_agent.py" class SignageSetup: def __init__(self): # Default configuration self.server_url = "" self.device_id = "" self.check_interval = 60 self.screen_index = 0 # Always initialize with user home directory - will be updated if running as root current_user = os.getenv('USER', 'user') self.target_user = current_user self.target_uid = None self.target_gid = None # Set default setup directory if os.geteuid() == 0: # Use SUDO_USER when running as root sudo_user = os.getenv('SUDO_USER', 'user') self.setup_dir = Path(f"/home/{sudo_user}/signage") else: self.setup_dir = Path.home() / "signage" # Initialize paths self.config_file = self.setup_dir / ".env" self.client_script = self.setup_dir / "client_agent.py" self.service_file = "/etc/systemd/system/signage-client.service" def print_header(self): print("=" * 60) print(" Digital Signage Client Setup") print("=" * 60) print() print("This script will help you set up a digital signage client device.") print("It will download the latest client software and configure your system.") print() def check_system(self): """Check system requirements""" print("๐Ÿ” Checking system requirements...") # Check if running as root for systemd setup if os.geteuid() == 0: print("โš ๏ธ Warning: Running as root. This is okay for initial setup.") print(" The service will run as a regular user for security.") print() # Check Python version if sys.version_info < (3, 6): print("โŒ Error: Python 3.6 or higher is required") print(f" Current version: {sys.version}") sys.exit(1) print(f"โœ… Python {sys.version.split()[0]} - OK") # Check for required commands required_commands = ['systemctl', 'wget', 'curl'] missing_commands = [] for cmd in required_commands: if not shutil.which(cmd): missing_commands.append(cmd) if missing_commands: print(f"โš ๏ธ Missing commands: {', '.join(missing_commands)}") print(" You may need to install these manually.") print() def install_dependencies(self): """Install required dependencies for desktop Ubuntu""" print("๐Ÿ“ฆ Installing dependencies for desktop Ubuntu...") # Check if we have sudo access has_sudo = self.check_sudo_access() if not has_sudo: print("โŒ This setup requires sudo access to install VLC and other packages.") print(" Please run: sudo python3 setup_client.py") sys.exit(1) # Install VLC and Python requirements self.install_desktop_packages() # Install Python requests module self.install_python_requests() # Verify VLC installation print("\n๐ŸŽฌ Verifying VLC installation...") if shutil.which('vlc'): print(" โœ… VLC media player installed") else: print(" โŒ VLC not found after installation!") if not self.ask_yes_no("Continue anyway?", default=False): print("Setup cancelled.") sys.exit(1) print() def detect_package_manager(self): """Detect available package manager""" managers = ['apt', 'yum', 'dnf', 'pacman'] for manager in managers: if shutil.which(manager): return manager return None def check_sudo_access(self): """Check if we have sudo access""" try: subprocess.run(['sudo', '-n', 'true'], check=True, capture_output=True) return True except (subprocess.CalledProcessError, FileNotFoundError): print("โš ๏ธ No sudo access detected. Some installations may fail.") print(" Re-run with sudo for automatic package installation.") print(" Continuing with limited functionality...") return False def install_desktop_packages(self): """Install packages for desktop Ubuntu""" print(" Installing packages for desktop Ubuntu...") # Update package list print(" Updating package list...") try: subprocess.run(['sudo', 'apt', 'update'], check=True, capture_output=True, timeout=60) print(" โœ… Package list updated") except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: print(f" โš ๏ธ Package update had issues: {e}") # Essential packages for desktop Ubuntu packages = [ 'vlc', # VLC media player 'python3-pip', # Python package manager 'python3-requests', # Python HTTP library 'openssh-server', # SSH server for remote access 'git', # Git for client updates ] for package in packages: print(f" Installing {package}...") try: subprocess.run(['sudo', 'apt', 'install', '-y', package], check=True, capture_output=True, timeout=120) print(f" โœ… {package} installed") except subprocess.CalledProcessError as e: print(f" โš ๏ธ Failed to install {package}") except subprocess.TimeoutExpired: print(f" โฐ {package} installation timed out") def install_python_requests(self): """Install Python requests module""" # First check if requests is already available try: import requests print(" โœ… Python requests module already available") return except ImportError: pass # Try to install requests if not available try: # Try user install first subprocess.run([sys.executable, '-m', 'pip', 'install', '--user', 'requests'], check=True, capture_output=True) print(" โœ… Python requests module installed (user)") except (subprocess.CalledProcessError, FileNotFoundError): try: # Try system install with sudo subprocess.run(['sudo', sys.executable, '-m', 'pip', 'install', 'requests'], check=True, capture_output=True) print(" โœ… Python requests module installed (system)") except (subprocess.CalledProcessError, FileNotFoundError): try: # Try pip3 as fallback if it exists if shutil.which('pip3'): subprocess.run(['pip3', 'install', '--user', 'requests'], check=True, capture_output=True) print(" โœ… Python requests module installed (pip3)") else: # pip not available, system package should work print(" โš ๏ธ pip not available, using system python3-requests package") print(" This should be sufficient for the client to work") except subprocess.CalledProcessError: print(" โš ๏ธ Failed to install Python requests module") print(" The system python3-requests package should work") def get_user_input(self): """Get configuration from user""" print("โš™๏ธ Configuration") print("-" * 20) # Default to interactive mode unless explicitly running non-interactive is_interactive = os.environ.get('FORCE_NON_INTERACTIVE', '0') != '1' # If running as root, ask for target user if os.geteuid() == 0: # Get target user if is_interactive: current_user = os.getenv('SUDO_USER', 'user') target_user = input(f"Username to run signage as (default: {current_user}): ").strip() or current_user else: target_user = os.getenv('SUDO_USER', 'user') print(f"Non-interactive mode: Using user '{target_user}'") try: import pwd user_info = pwd.getpwnam(target_user) self.target_user = target_user self.target_uid = user_info.pw_uid self.target_gid = user_info.pw_gid self.setup_dir = Path(user_info.pw_dir) / "signage" # Update paths with correct setup directory self.config_file = self.setup_dir / ".env" self.client_script = self.setup_dir / "client_agent.py" print(f"Setting up for user: {self.target_user}") print(f"Home directory: {user_info.pw_dir}") print() except KeyError: print(f"โŒ User '{target_user}' not found") print("Please create the user first or run as the target user") sys.exit(1) try: # Server URL if is_interactive: while True: self.server_url = input("Server URL (default: https://display.obtv.io): ").strip() if not self.server_url: self.server_url = "https://display.obtv.io" if self.server_url: # Clean up URL if not self.server_url.startswith(('http://', 'https://')): self.server_url = 'https://' + self.server_url if self.server_url.endswith('/'): self.server_url = self.server_url[:-1] break else: print("โŒ Server URL is required!") else: # Non-interactive mode: use defaults self.server_url = "https://display.obtv.io" print(f"Non-interactive mode: Using default server URL '{self.server_url}'") # Device ID if is_interactive: while True: self.device_id = input("Device ID (unique identifier): ").strip() if self.device_id: # Clean up device ID self.device_id = self.device_id.lower().replace(' ', '-') break print("โŒ Device ID is required!") else: # Non-interactive: check if device ID is provided as environment variable existing_device_id = os.environ.get('DEVICE_ID') if existing_device_id: self.device_id = existing_device_id print(f"Using device ID from environment: '{self.device_id}'") else: # Check if there's an existing config file with device ID if self.config_file.exists(): try: with open(self.config_file, 'r') as f: content = f.read() for line in content.split('\n'): if line.startswith('DEVICE_ID='): self.device_id = line.split('=', 1)[1].strip() print(f"Found existing device ID: '{self.device_id}'") break except Exception: pass if not self.device_id: print("โŒ ERROR: Device ID not found!") print("") print("To set up this client, run:") print(" DEVICE_ID=t-zyw3 python3 setup_client.py") print("") print("Or create a config file first:") print(" echo 'DEVICE_ID=t-zyw3' > .env") print(" python3 setup_client.py") sys.exit(1) # Check interval if is_interactive: while True: interval_input = input(f"Check interval in seconds (default: {self.check_interval}): ").strip() if not interval_input: break try: self.check_interval = int(interval_input) if self.check_interval < 10: print("โš ๏ธ Warning: Very short intervals may cause server load") break except ValueError: print("โŒ Please enter a valid number") else: # Non-interactive: keep default print(f"Non-interactive mode: Using default check interval {self.check_interval} seconds") # Screen index for multi-monitor HDMI targeting if is_interactive: print() print("๐Ÿ–ฅ๏ธ Multi-monitor setup:") print(" Screen 0: Primary display (usually laptop/main monitor)") print(" Screen 1: Secondary display (usually HDMI/external monitor)") print(" Screen 2+: Additional monitors if connected") while True: screen_input = input(f"Which screen for fullscreen display? (default: {self.screen_index}): ").strip() if not screen_input: break try: self.screen_index = int(screen_input) if self.screen_index < 0: print("โŒ Screen index must be 0 or higher") continue if self.screen_index > 0: print(f" Will target screen {self.screen_index} (external monitor)") break except ValueError: print("โŒ Please enter a valid number") else: # Non-interactive: keep default (primary screen) print(f"Non-interactive mode: Using default screen {self.screen_index} (primary)") except (EOFError, KeyboardInterrupt): print("\nโŒ Configuration cancelled by user or non-interactive session") print(" Device ID is required and must be registered in the dashboard first.") print("") print("๐Ÿ“‹ To complete setup:") print(" 1. Register device in dashboard at: https://display.obtv.io") print(" 2. Run setup interactively: python3 setup_client.py") print(" 3. Or download and run manually:") print(" wget https://raw.githubusercontent.com/tbnobed/signage/main/setup_client.py") print(" python3 setup_client.py") sys.exit(1) print() print("Configuration Summary:") print(f" Server URL: {self.server_url}") print(f" Device ID: {self.device_id}") print(f" Check Interval: {self.check_interval} seconds") print(f" Target Screen: {self.screen_index} {'(external/HDMI)' if self.screen_index > 0 else '(primary)'}") print() if not self.ask_yes_no("Is this correct?", default=True): print("Restarting configuration...") return self.get_user_input() def create_directory(self): """Create signage directory""" print("๐Ÿ“ Creating signage directory...") self.setup_dir.mkdir(parents=True, exist_ok=True) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(self.setup_dir, self.target_uid, self.target_gid) print(f" Set ownership to: {self.target_user}") print(f" Created: {self.setup_dir}") # Create media directory media_dir = self.setup_dir / "media" media_dir.mkdir(exist_ok=True) if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(media_dir, self.target_uid, self.target_gid) def download_client(self): """Download client script from GitHub""" print("โฌ‡๏ธ Downloading client script...") try: urllib.request.urlretrieve(CLIENT_SCRIPT_URL, self.client_script) # Make executable os.chmod(self.client_script, 0o755) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(self.client_script, self.target_uid, self.target_gid) print(f" Downloaded: {self.client_script}") except Exception as e: print(f"โŒ Failed to download client script: {e}") print(" Please check your internet connection and try again.") sys.exit(1) def create_config(self): """Create environment configuration file""" print("๐Ÿ“ Creating configuration file...") # Remove existing config file to prevent duplicates if self.config_file.exists(): self.config_file.unlink() print(" Removed existing configuration file") config_content = f"""# Digital Signage Client Configuration SIGNAGE_SERVER_URL={self.server_url} DEVICE_ID={self.device_id} CHECK_INTERVAL={self.check_interval} RAPID_CHECK_INTERVAL=2 SCREEN_INDEX={self.screen_index} MEDIA_DIR={self.setup_dir}/media LOG_FILE={self.setup_dir}/client.log """ with open(self.config_file, 'w') as f: f.write(config_content) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(self.config_file, self.target_uid, self.target_gid) print(f" Created: {self.config_file}") print(f" Device ID: {self.device_id}") print(f" Server URL: {self.server_url}") def test_connection(self): """Test connection to server""" print("๐Ÿ”Œ Testing server connection...") try: # Set environment variables for test env = os.environ.copy() env.update({ 'SIGNAGE_SERVER_URL': self.server_url, 'DEVICE_ID': self.device_id, 'CHECK_INTERVAL': str(self.check_interval) }) # Try to ping the server import urllib.request import urllib.error test_url = f"{self.server_url}/api/devices/ping" try: urllib.request.urlopen(test_url, timeout=10) print(" โœ… Server is reachable") except urllib.error.HTTPError as e: if e.code == 404: print(" โœ… Server is reachable (404 is expected for ping)") else: print(f" โš ๏ธ Server responded with error: {e.code}") except Exception as e: print(f" โŒ Cannot reach server: {e}") print(" Please check the server URL and network connection") return False except Exception as e: print(f" โŒ Connection test failed: {e}") return False return True def configure_kiosk_mode(self): """Configure kiosk mode for Ubuntu 22.04: disable notifications, power management, set background""" print("๐Ÿ–ฅ๏ธ Configuring kiosk mode for Ubuntu 22.04...") username = self.target_user or getpass.getuser() user_home = f"/home/{username}" # Download TBN logo background print(" ๐Ÿ“„ Downloading TBN logo background...") background_url = "http://msm.livestudios.tv/wp-content/uploads/2024/05/TBNLogo.png" background_path = Path(user_home) / "Pictures" / "TBNLogo.png" try: # Create Pictures directory if it doesn't exist background_path.parent.mkdir(parents=True, exist_ok=True) # Download background image urllib.request.urlretrieve(background_url, background_path) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(background_path, self.target_uid, self.target_gid) os.chown(background_path.parent, self.target_uid, self.target_gid) print(f" โœ… Background downloaded: {background_path}") except Exception as e: print(f" โš ๏ธ Failed to download background: {e}") print(" Using default background") background_path = None # Configure GNOME settings for kiosk mode gsettings_commands = [ # Disable all notifications "gsettings set org.gnome.desktop.notifications show-banners false", "gsettings set org.gnome.desktop.notifications show-in-lock-screen false", # Power settings - never suspend, never turn off screen "gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing'", "gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing'", "gsettings set org.gnome.desktop.session idle-delay 0", # Screen saver settings "gsettings set org.gnome.desktop.screensaver lock-enabled false", "gsettings set org.gnome.desktop.screensaver idle-activation-enabled false", # Disable automatic updates notifications "gsettings set org.gnome.software download-updates false", "gsettings set org.gnome.software download-updates-notify false", # Hide desktop icons and taskbar auto-hide for cleaner kiosk look "gsettings set org.gnome.desktop.background show-desktop-icons false", "gsettings set org.gnome.shell.extensions.dash-to-dock autohide true", "gsettings set org.gnome.shell.extensions.dash-to-dock dock-fixed false", # Disable screen lock "gsettings set org.gnome.desktop.lockdown disable-lock-screen true", ] # Set background if download was successful if background_path and background_path.exists(): gsettings_commands.extend([ f"gsettings set org.gnome.desktop.background picture-uri 'file://{background_path}'", f"gsettings set org.gnome.desktop.background picture-uri-dark 'file://{background_path}'", "gsettings set org.gnome.desktop.background picture-options 'centered'", "gsettings set org.gnome.desktop.background primary-color '#000000'", ]) # Execute gsettings commands print(" โš™๏ธ Configuring GNOME settings...") for cmd in gsettings_commands: try: if os.geteuid() == 0: # Running as root, execute as target user import pwd user_uid = pwd.getpwnam(username).pw_uid user_env = { 'XDG_RUNTIME_DIR': f'/run/user/{user_uid}', 'HOME': user_home, 'USER': username, 'DISPLAY': ':0', # Ensure we can access the display 'DBUS_SESSION_BUS_ADDRESS': f'unix:path=/run/user/{user_uid}/bus' } subprocess.run(['sudo', '-u', username] + [f'{k}={v}' for k, v in user_env.items()] + cmd.split(), check=True, capture_output=True, env={**os.environ, **user_env}, timeout=10) else: # Running as regular user subprocess.run(cmd.split(), check=True, capture_output=True, timeout=10) except subprocess.CalledProcessError as e: print(f" โš ๏ธ Warning: Failed to execute: {cmd}") except subprocess.TimeoutExpired: print(f" โš ๏ธ Warning: Timeout executing: {cmd}") except Exception as e: print(f" โš ๏ธ Warning: Error with {cmd}: {e}") # Configure additional power management settings via systemd print(" ๐Ÿ”‹ Configuring power management...") power_commands = [ # Prevent system suspend "sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target", # Configure logind to not suspend on lid close (for laptops) "sudo bash -c \"echo 'HandleLidSwitch=ignore' >> /etc/systemd/logind.conf\"", "sudo bash -c \"echo 'HandleLidSwitchExternalPower=ignore' >> /etc/systemd/logind.conf\"", "sudo bash -c \"echo 'IdleAction=ignore' >> /etc/systemd/logind.conf\"", ] for cmd in power_commands: try: subprocess.run(cmd, shell=True, check=True, capture_output=True, timeout=15) except subprocess.CalledProcessError as e: print(f" โš ๏ธ Warning: Power command failed: {cmd}") except subprocess.TimeoutExpired: print(f" โš ๏ธ Warning: Power command timeout: {cmd}") # Disable Ubuntu's unattended upgrades to prevent reboot prompts print(" ๐Ÿ“ฆ Disabling automatic updates...") try: subprocess.run(['sudo', 'systemctl', 'stop', 'unattended-upgrades'], check=True, capture_output=True, timeout=10) subprocess.run(['sudo', 'systemctl', 'disable', 'unattended-upgrades'], check=True, capture_output=True, timeout=10) print(" โœ… Automatic updates disabled") except subprocess.CalledProcessError: print(" โš ๏ธ Could not disable automatic updates") except subprocess.TimeoutExpired: print(" โš ๏ธ Timeout disabling automatic updates") # Create a script to re-apply kiosk settings on login (in case they get reset) kiosk_script_path = Path(user_home) / ".local" / "bin" / "kiosk-setup.sh" kiosk_script_path.parent.mkdir(parents=True, exist_ok=True) kiosk_script_content = f"""#!/bin/bash # Kiosk mode settings - run on login # Generated by signage setup # Wait for desktop to load sleep 5 # Re-apply critical kiosk settings gsettings set org.gnome.desktop.notifications show-banners false gsettings set org.gnome.desktop.screensaver lock-enabled false gsettings set org.gnome.desktop.screensaver idle-activation-enabled false gsettings set org.gnome.desktop.session idle-delay 0 # Set background if exists if [ -f "{background_path}" ]; then gsettings set org.gnome.desktop.background picture-uri 'file://{background_path}' gsettings set org.gnome.desktop.background picture-uri-dark 'file://{background_path}' fi # Hide cursor after 3 seconds of inactivity (optional) # unclutter -idle 3 & """ try: with open(kiosk_script_path, 'w') as f: f.write(kiosk_script_content) os.chmod(kiosk_script_path, 0o755) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(kiosk_script_path, self.target_uid, self.target_gid) print(f" โœ… Kiosk settings script created: {kiosk_script_path}") except Exception as e: print(f" โš ๏ธ Failed to create kiosk script: {e}") # Add the kiosk script to autostart autostart_dir = Path(user_home) / ".config" / "autostart" autostart_dir.mkdir(parents=True, exist_ok=True) autostart_file = autostart_dir / "kiosk-setup.desktop" autostart_content = f"""[Desktop Entry] Type=Application Name=Kiosk Setup Exec={kiosk_script_path} Hidden=false NoDisplay=false X-GNOME-Autostart-enabled=true Comment=Apply kiosk mode settings on login """ try: with open(autostart_file, 'w') as f: f.write(autostart_content) # Set ownership if running as root if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(autostart_file, self.target_uid, self.target_gid) os.chown(autostart_dir, self.target_uid, self.target_gid) print(" โœ… Kiosk setup added to autostart") except Exception as e: print(f" โš ๏ธ Failed to create autostart entry: {e}") print(" โœ… Kiosk mode configuration completed!") print(" ๐Ÿ“‹ Kiosk features applied:") print(" โ€ข All notifications disabled") print(" โ€ข Power management disabled (never sleep/suspend)") print(" โ€ข Screen saver and lock disabled") print(" โ€ข TBN logo set as background") print(" โ€ข Automatic updates disabled") print(" โ€ข Settings will re-apply on each login") print() def install_teamviewer(self): """Download and install TeamViewer Host for remote management""" print("๐Ÿ“ฑ Installing TeamViewer Host for remote management...") # TeamViewer Host download URL (for unattended kiosk access) teamviewer_url = "https://download.teamviewer.com/download/linux/teamviewer-host_amd64.deb" # Download path download_dir = Path("/tmp") teamviewer_deb = download_dir / "teamviewer-host_amd64.deb" try: print(" โฌ‡๏ธ Downloading TeamViewer package...") # Create a request with browser-like headers to bypass 403 blocking request = urllib.request.Request(teamviewer_url) request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36') request.add_header('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') request.add_header('Accept-Language', 'en-US,en;q=0.5') request.add_header('Referer', 'https://www.teamviewer.com/') # Download with proper headers with urllib.request.urlopen(request) as response, open(teamviewer_deb, 'wb') as out_file: out_file.write(response.read()) print(f" โœ… Downloaded: {teamviewer_deb}") # Verify file was downloaded and has reasonable size if teamviewer_deb.exists() and teamviewer_deb.stat().st_size > 1024: # At least 1KB file_size_mb = teamviewer_deb.stat().st_size / (1024 * 1024) print(f" ๐Ÿ“ฆ Package size: {file_size_mb:.1f} MB") else: print(" โŒ Downloaded file appears invalid") return False # Install TeamViewer using dpkg print(" ๐Ÿ“ฆ Installing TeamViewer package...") try: # First try to install the package subprocess.run(['sudo', 'dpkg', '-i', str(teamviewer_deb)], check=True, capture_output=True, timeout=60) print(" โœ… TeamViewer package installed") except subprocess.CalledProcessError as e: print(" โš ๏ธ Package installation had dependency issues, fixing...") # Try to fix dependency issues try: subprocess.run(['sudo', 'apt', 'install', '-f', '-y'], check=True, capture_output=True, timeout=120) print(" โœ… Dependencies resolved") except subprocess.CalledProcessError: print(" โŒ Could not resolve dependencies automatically") return False # Verify TeamViewer was installed if shutil.which('teamviewer'): print(" โœ… TeamViewer installed successfully") # Enable TeamViewer daemon to start on boot try: subprocess.run(['sudo', 'systemctl', 'enable', 'teamviewerd'], check=True, capture_output=True, timeout=10) print(" โœ… TeamViewer daemon enabled for auto-start") except subprocess.CalledProcessError: print(" โš ๏ธ Could not enable TeamViewer daemon (this is usually ok)") # Start TeamViewer daemon try: subprocess.run(['sudo', 'systemctl', 'start', 'teamviewerd'], check=True, capture_output=True, timeout=15) print(" โœ… TeamViewer daemon started") except subprocess.CalledProcessError: print(" โš ๏ธ Could not start TeamViewer daemon (will start on reboot)") # Configure TeamViewer for unattended kiosk access print(" โš™๏ธ Configuring TeamViewer for kiosk mode...") # Get username and home directory username = self.target_user or getpass.getuser() user_home = f"/home/{username}" # Step 1: Accept TeamViewer license automatically print(" ๐Ÿ“ Accepting TeamViewer license...") try: result = subprocess.run(['sudo', 'teamviewer', 'license', 'accept'], capture_output=True, text=True, timeout=15) if result.returncode == 0: print(" โœ… TeamViewer license accepted") else: print(" โš ๏ธ License may already be accepted or command unavailable") except Exception as e: print(f" โš ๏ธ License acceptance error: {e}") # Step 2: Disable Wayland and enable X11 for reliable remote access print(" ๐Ÿ–ฅ๏ธ Configuring display server for reliable remote access...") try: # Check current session type current_session = os.environ.get('XDG_SESSION_TYPE', 'unknown') print(f" ๐Ÿ’ก Current session type: {current_session}") gdm_config_path = '/etc/gdm3/custom.conf' wayland_disabled = False if os.path.exists(gdm_config_path): with open(gdm_config_path, 'r') as f: gdm_config = f.read() # Check if WaylandEnable=false is already active (not commented) if 'WaylandEnable=false' in gdm_config and not gdm_config.count('#WaylandEnable=false'): lines = gdm_config.split('\n') for line in lines: if 'WaylandEnable=false' in line and not line.strip().startswith('#'): wayland_disabled = True break if not wayland_disabled: print(" ๐Ÿ”„ Disabling Wayland in favor of X11...") # Method 1: Try to uncomment existing line result1 = subprocess.run(['sudo', 'sed', '-i', 's/#WaylandEnable=false/WaylandEnable=false/', gdm_config_path], capture_output=True, timeout=10) # Method 2: If no commented line found, add it to [daemon] section with open(gdm_config_path, 'r') as f: updated_config = f.read() if 'WaylandEnable=false' not in updated_config or '#WaylandEnable=false' in updated_config: print(" ๐Ÿ“ Adding WaylandEnable=false to GDM config...") # Add WaylandEnable=false under [daemon] section if '[daemon]' in updated_config: # Insert after [daemon] line subprocess.run(['sudo', 'sed', '-i', '/^\[daemon\]/a WaylandEnable=false', gdm_config_path], check=True, timeout=10) else: # Add [daemon] section with WaylandEnable=false with open('/tmp/gdm_append.txt', 'w') as f: f.write('\n[daemon]\nWaylandEnable=false\n') subprocess.run(['sudo', 'tee', '-a', gdm_config_path], stdin=open('/tmp/gdm_append.txt', 'r'), check=True, timeout=10) os.remove('/tmp/gdm_append.txt') print(" โœ… Wayland disabled, X11 will be used after reboot") # Verify the change with open(gdm_config_path, 'r') as f: final_config = f.read() if 'WaylandEnable=false' in final_config and not final_config.count('WaylandEnable=false') == final_config.count('#WaylandEnable=false'): print(" โœ… Configuration verified successfully") else: print(" โš ๏ธ Configuration verification failed") else: print(" โœ… Wayland already disabled in GDM config") if current_session == 'wayland': print(" ๐Ÿ’ก Still on Wayland session - reboot required") else: print(" ๐Ÿ’ก No GDM config found - likely already using X11") except Exception as e: print(f" โš ๏ธ Display server config error: {e}") # Fallback - try direct file modification try: print(" ๐Ÿ”„ Trying fallback configuration method...") subprocess.run(['sudo', 'bash', '-c', 'echo -e "\\n[daemon]\\nWaylandEnable=false" >> /etc/gdm3/custom.conf'], check=True, timeout=10) print(" โœ… Fallback configuration applied") except Exception as fallback_e: print(f" โŒ Fallback configuration also failed: {fallback_e}") # Step 3: Set TeamViewer password (prompt user for security) print(" ๐Ÿ” Setting up TeamViewer unattended access...") teamviewer_password = None # Ask user for TeamViewer password while not teamviewer_password: try: teamviewer_password = input(" Enter TeamViewer password for unattended access (8+ chars): ").strip() if len(teamviewer_password) < 8: print(" โŒ Password must be at least 8 characters") teamviewer_password = None except KeyboardInterrupt: print("\n Using default password for kiosk setup...") teamviewer_password = "Kiosk2024!" break # Set the password (handle shell special characters safely) try: # Use shell=False and pass password as separate argument to avoid shell expansion result = subprocess.run(['sudo', 'teamviewer', 'passwd', teamviewer_password], capture_output=True, text=True, timeout=15) # Check various success indicators if (result.returncode == 0 or 'ok' in result.stdout.lower() or 'password set' in result.stdout.lower() or len(result.stdout.strip()) == 0): # Sometimes no output means success print(" โœ… TeamViewer password set successfully") print(f" ๐Ÿ”‘ Password: {teamviewer_password}") else: print(f" โš ๏ธ Password setting unclear - return code: {result.returncode}") print(f" ๐Ÿ“„ Output: {result.stdout}") print(f" ๐Ÿ“„ Error: {result.stderr}") # Try alternative method print(" ๐Ÿ”„ Trying alternative password method...") alt_result = subprocess.run(['sudo', 'bash', '-c', f'echo "{teamviewer_password}" | teamviewer --passwd'], capture_output=True, text=True, timeout=15) if alt_result.returncode == 0: print(" โœ… Alternative password method succeeded") else: print(" โš ๏ธ Alternative method also unclear") except subprocess.TimeoutExpired: print(" โš ๏ธ Password setting timed out - this might be due to special characters") print(" ๐Ÿ’ก TeamViewer might still work, test the connection after reboot") except Exception as e: print(f" โš ๏ธ Password setting error: {e}") print(" ๐Ÿ’ก You can set the password manually after reboot with:") print(f" ๐Ÿ’ก sudo teamviewer passwd 'your_password_here'") # Step 4: Restart TeamViewer daemon print(" ๐Ÿ”„ Restarting TeamViewer daemon...") try: subprocess.run(['sudo', 'systemctl', 'restart', 'teamviewerd'], check=True, capture_output=True, timeout=15) print(" โœ… TeamViewer daemon restarted") except subprocess.CalledProcessError as e: print(" โš ๏ธ Daemon restart failed - will work after system reboot") # Step 5: Show TeamViewer ID and connection info print(" ๐Ÿ†” Getting TeamViewer connection information...") try: result = subprocess.run(['sudo', 'teamviewer', 'info'], capture_output=True, text=True, timeout=10) if result.returncode == 0 and "TeamViewer ID:" in result.stdout: for line in result.stdout.split('\n'): if 'TeamViewer ID:' in line: teamviewer_id = line.strip().split(':')[-1].strip() print(f" ๐Ÿ†” TeamViewer ID: {teamviewer_id}") print(f" ๐Ÿ”‘ Password: {teamviewer_password}") print(" ๐Ÿ’ก Save these credentials for remote access!") break else: print(" โš ๏ธ TeamViewer ID not available yet (will show after reboot)") print(" ๐Ÿ’ก Run 'sudo teamviewer info' after reboot to get ID") except Exception as e: print(" โš ๏ธ Could not get TeamViewer ID - check after reboot") print(" ๐Ÿ’ก Run 'sudo teamviewer info' after reboot") # Create unattended access setup instructions instructions_file = Path(user_home) / "teamviewer-setup.txt" instructions_content = f"""TeamViewer Host Setup for Digital Signage IMPORTANT: TeamViewer Host is specifically designed for unattended remote access. This is the lightweight version perfect for digital signage kiosks. TO SET UP UNATTENDED ACCESS: 1. Get your TeamViewer ID: teamviewer --info 2. In TeamViewer GUI (first run): - Go to Extras โ†’ Options โ†’ Security - Set "Unattended Access" password - Enable "Enable remote control" - Set "Random password" to "Disabled" 3. In your TeamViewer Management Console: - Add this computer using the TeamViewer ID - Configure for unattended access - Set up Easy Access for password-free connections 4. For better reliability (optional): - Switch from Wayland to X11 session type - Log out and select "Ubuntu on Xorg" at login CURRENT SETUP: - Device: {self.device_id} - Server: {self.server_url} - User: {username} Run 'teamviewer --info' to get your TeamViewer ID after reboot. """ try: with open(instructions_file, 'w') as f: f.write(instructions_content) if os.geteuid() == 0 and self.target_uid is not None and self.target_gid is not None: os.chown(instructions_file, self.target_uid, self.target_gid) print(f" ๐Ÿ“„ Setup instructions saved: {instructions_file}") except Exception as e: print(f" โš ๏ธ Could not save instructions: {e}") else: print(" โŒ TeamViewer installation failed") return False # Clean up downloaded package try: teamviewer_deb.unlink() print(" ๐Ÿงน Cleaned up installation package") except Exception: pass return True except urllib.error.URLError as e: print(f" โŒ Failed to download TeamViewer: {e}") print(" Check your internet connection and try again") return False except subprocess.TimeoutExpired: print(" โŒ TeamViewer installation timed out") return False except Exception as e: print(f" โŒ TeamViewer installation error: {e}") return False def configure_ssh_server(self): """Configure SSH server for remote access""" print("๐Ÿ” Configuring SSH server for remote access...") try: # Enable SSH service to start on boot print(" โš™๏ธ Enabling SSH service...") subprocess.run(['sudo', 'systemctl', 'enable', 'ssh'], check=True, capture_output=True, timeout=10) print(" โœ… SSH service enabled for auto-start") # Start SSH service now print(" ๐Ÿš€ Starting SSH service...") subprocess.run(['sudo', 'systemctl', 'start', 'ssh'], check=True, capture_output=True, timeout=15) print(" โœ… SSH service started") # Configure SSH for better security (optional hardening) ssh_config_path = "/etc/ssh/sshd_config" print(" ๐Ÿ”’ Configuring SSH security settings...") # Allow password authentication but recommend key-based auth ssh_config_changes = [ "# Digital Signage SSH Configuration", "PasswordAuthentication yes", "PubkeyAuthentication yes", "PermitRootLogin no", "MaxAuthTries 3", "ClientAliveInterval 300", "ClientAliveCountMax 2" ] # Append our config to sshd_config config_text = "\n".join(ssh_config_changes) subprocess.run(['sudo', 'sh', '-c', f'echo "\n{config_text}" >> {ssh_config_path}'], check=True, capture_output=True, timeout=10) # Restart SSH to apply changes subprocess.run(['sudo', 'systemctl', 'restart', 'ssh'], check=True, capture_output=True, timeout=10) print(" โœ… SSH security settings configured") # Get the IP address for user information try: ip_result = subprocess.run(['hostname', '-I'], capture_output=True, text=True, timeout=5) if ip_result.returncode == 0: ip_address = ip_result.stdout.strip().split()[0] username = self.target_user or os.getenv('USER', 'user') print(f" ๐Ÿ“ SSH access: ssh {username}@{ip_address}") else: print(" ๐Ÿ“ SSH is now accessible via this device's IP address") except: print(" ๐Ÿ“ SSH is now accessible via this device's IP address") return True except subprocess.CalledProcessError as e: print(f" โŒ Failed to configure SSH server: {e}") return False except subprocess.TimeoutExpired: print(" โŒ SSH server configuration timed out") return False except Exception as e: print(f" โŒ SSH server configuration error: {e}") return False def configure_teamviewer_sudo(self): """Configure passwordless sudo for TeamViewer --info command""" print("๐Ÿ”’ Configuring sudo permissions for TeamViewer ID detection...") # Get the target username username = self.target_user or getpass.getuser() # Create sudoers rule for TeamViewer --info command sudoers_rule = f"{username} ALL=(ALL) NOPASSWD: /usr/bin/teamviewer --info" sudoers_file = "/etc/sudoers.d/teamviewer-info" try: # Create the sudoers file subprocess.run(['sudo', 'sh', '-c', f'echo "{sudoers_rule}" > {sudoers_file}'], check=True, capture_output=True, timeout=10) # Set correct permissions (440 is read-only for root and group) subprocess.run(['sudo', 'chmod', '440', sudoers_file], check=True, capture_output=True, timeout=5) # Test the sudo rule works test_result = subprocess.run(['sudo', '-n', '/usr/bin/teamviewer', '--info'], capture_output=True, timeout=10) if test_result.returncode == 0: print(" โœ… TeamViewer sudo permissions configured successfully") print(f" ๐Ÿ“‹ Rule: {sudoers_rule}") return True else: print(" โš ๏ธ TeamViewer sudo rule created but test failed") print(f" ๐Ÿ“‹ Rule: {sudoers_rule}") return False except subprocess.CalledProcessError as e: print(f" โŒ Failed to configure TeamViewer sudo permissions: {e}") return False except subprocess.TimeoutExpired: print(" โŒ TeamViewer sudo configuration timed out") return False except Exception as e: print(f" โŒ TeamViewer sudo configuration error: {e}") return False def configure_sudo_permissions(self): """Configure passwordless sudo for reboot commands""" print("๐Ÿ”’ Configuring sudo permissions for reboot functionality...") # Get the target username username = self.target_user or getpass.getuser() # Define sudoers file and content sudoers_file = f"/etc/sudoers.d/signage-reboot-{username}" sudoers_content = f"""# Allow {username} to reboot without password for digital signage {username} ALL=(ALL) NOPASSWD: /sbin/reboot, /usr/sbin/reboot, /bin/systemctl reboot, /usr/bin/systemctl reboot """ # Create temporary file for validation import tempfile try: # Create temporary file with sudoers content with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp', delete=False) as temp_file: temp_file.write(sudoers_content) temp_file_path = temp_file.name # Validate the sudoers content using visudo print(" Validating sudoers configuration...") try: subprocess.run(['sudo', 'visudo', '-cf', temp_file_path], check=True, capture_output=True, timeout=10) print(" โœ… Sudoers configuration is valid") except subprocess.CalledProcessError as e: print(f" โŒ Invalid sudoers configuration: {e}") os.unlink(temp_file_path) # Clean up temp file return False # Install the validated file with proper ownership and permissions try: subprocess.run(['sudo', 'install', '-m', '440', '-o', 'root', '-g', 'root', temp_file_path, sudoers_file], check=True, capture_output=True, timeout=10) print(f" โœ… Sudo permissions installed: {sudoers_file}") print(f" User '{username}' can now reboot without password") except subprocess.CalledProcessError as e: print(f" โŒ Failed to install sudoers file: {e}") os.unlink(temp_file_path) # Clean up temp file return False # Clean up temporary file os.unlink(temp_file_path) # Verify the configuration works try: result = subprocess.run(['sudo', '-l', '-U', username], capture_output=True, text=True, timeout=5) # Check if any of the required commands are listed required_commands = ['/sbin/reboot', '/usr/sbin/reboot', '/bin/systemctl reboot', '/usr/bin/systemctl reboot'] found_commands = [cmd for cmd in required_commands if cmd in result.stdout] if found_commands: print(f" โœ… Sudo configuration verified: {', '.join(found_commands)}") else: print(" โš ๏ธ Sudo configuration created but verification unclear") except Exception: print(" โš ๏ธ Could not verify sudo configuration, but file was installed") return True except Exception as e: print(f" โŒ Failed to configure sudo permissions: {e}") print(" Manual configuration required. Add this line to /etc/sudoers:") print(f" {username} ALL=(ALL) NOPASSWD: /sbin/reboot, /usr/sbin/reboot, /bin/systemctl reboot, /usr/bin/systemctl reboot") return False def create_systemd_service(self): """Create systemd user service for desktop Ubuntu with Wayland support""" print("๐Ÿš€ Setting up auto-start user service...") # Use the target user we already determined username = self.target_user or getpass.getuser() # Create user systemd directory user_systemd_dir = Path(f"/home/{username}/.config/systemd/user") user_systemd_dir.mkdir(parents=True, exist_ok=True) # Update service file path to user directory self.service_file = user_systemd_dir / "signage-client.service" # Service configuration for desktop Ubuntu user service service_content = f"""[Unit] Description=Digital Signage Client After=graphical-session.target network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory={self.setup_dir} EnvironmentFile={self.config_file} ExecStart=/usr/bin/python3 {self.client_script} Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=graphical-session.target """ try: # Write service file directly to user directory (no sudo needed) with open(self.service_file, 'w') as f: f.write(service_content) print(f" โœ… Service created: {self.service_file}") # Set ownership and permissions if running as root if os.geteuid() == 0: # If running as root, change ownership to user import pwd user_uid = pwd.getpwnam(username).pw_uid user_gid = pwd.getpwnam(username).pw_gid os.chown(self.service_file, user_uid, user_gid) # Also ensure the .config/systemd/user directory is owned by user os.chown(user_systemd_dir, user_uid, user_gid) os.chown(user_systemd_dir.parent, user_uid, user_gid) # .config/systemd os.chown(user_systemd_dir.parent.parent, user_uid, user_gid) # .config # Reload user systemd and enable service in proper user context if os.geteuid() == 0: # If running as root, run as target user import pwd user_uid = pwd.getpwnam(username).pw_uid user_env = { 'XDG_RUNTIME_DIR': f'/run/user/{user_uid}', 'HOME': f'/home/{username}', 'USER': username } subprocess.run(['sudo', '-u', username] + [f'{k}={v}' for k, v in user_env.items()] + ['systemctl', '--user', 'daemon-reload'], check=True, env={**os.environ, **user_env}) subprocess.run(['sudo', '-u', username] + [f'{k}={v}' for k, v in user_env.items()] + ['systemctl', '--user', 'enable', 'signage-client.service'], check=True, env={**os.environ, **user_env}) else: # Running as regular user subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True) subprocess.run(['systemctl', '--user', 'enable', 'signage-client.service'], check=True) print(" โœ… User service enabled for auto-start") # Enable lingering so service starts on boot even without login try: subprocess.run(['sudo', 'loginctl', 'enable-linger', username], check=True, capture_output=True) print(" โœ… User lingering enabled (starts on boot)") except subprocess.CalledProcessError: print(" โš ๏ธ Could not enable user lingering") print(" Service will start when user logs in") except Exception as e: print(f" โŒ Service setup error: {e}") return False return True def start_service(self): """Start the user signage service""" username = self.target_user or getpass.getuser() if self.ask_yes_no("Start the signage client now?", default=True): try: # Start user service in proper user context if os.geteuid() == 0: # If running as root, run as target user import pwd user_uid = pwd.getpwnam(username).pw_uid user_env = { 'XDG_RUNTIME_DIR': f'/run/user/{user_uid}', 'HOME': f'/home/{username}', 'USER': username } subprocess.run(['sudo', '-u', username] + [f'{k}={v}' for k, v in user_env.items()] + ['systemctl', '--user', 'start', 'signage-client.service'], check=True, env={**os.environ, **user_env}) print(" โœ… User service started") # Show service status print("\n๐Ÿ“Š Service Status:") subprocess.run(['sudo', '-u', username] + [f'{k}={v}' for k, v in user_env.items()] + ['systemctl', '--user', 'status', 'signage-client.service', '--no-pager', '-l'], check=False, env={**os.environ, **user_env}) else: # Running as regular user subprocess.run(['systemctl', '--user', 'start', 'signage-client.service'], check=True) print(" โœ… User service started") # Show service status print("\n๐Ÿ“Š Service Status:") subprocess.run(['systemctl', '--user', 'status', 'signage-client.service', '--no-pager', '-l'], check=False) except subprocess.CalledProcessError: print(" โŒ Failed to start user service") print(f" Try manually: systemctl --user start signage-client.service") print(f" Or as root: sudo -u {username} systemctl --user start signage-client.service") def show_completion_info(self): """Show completion information""" print("\n" + "=" * 60) print("๐ŸŽ‰ Setup Complete!") print("=" * 60) print() print("Your digital signage client is now configured in kiosk mode!") print() print("๐Ÿ–ฅ๏ธ Kiosk Mode Features:") print(" โ€ข All notifications disabled") print(" โ€ข Power management disabled (never sleep/suspend)") print(" โ€ข Screen saver and lock screen disabled") print(" โ€ข TBN logo set as desktop background") print(" โ€ข Automatic system updates disabled") print(" โ€ข Settings auto-restore on each login") print(" โ€ข SSH server configured for remote access") print(" โ€ข TeamViewer Host installed for remote management") print() print("๐Ÿ“‹ Next Steps:") print("1. Register this device in your web dashboard:") print(f" - Server: {self.server_url}") print(f" - Device ID: {self.device_id}") print("2. Create playlists and assign them to this device") print("3. Media will automatically download and play in fullscreen") print("4. System will automatically reboot to apply changes") print("5. TeamViewer will be fully functional after reboot") print() print("๐Ÿ”ง Useful Commands:") print(" sudo systemctl status signage-client # Check status") print(" sudo systemctl restart signage-client # Restart service") print(" sudo systemctl stop signage-client # Stop service") print(f" tail -f {self.setup_dir}/client.log # View logs") print(" sudo systemctl status ssh # Check SSH status") print(" sudo teamviewer info # Get TeamViewer ID") print(" echo $XDG_SESSION_TYPE # Verify X11 (after reboot)") print() print("๐Ÿ“ Files Created:") print(f" {self.client_script}") print(f" {self.config_file}") if os.path.exists(self.service_file): print(f" {self.service_file}") print() def reboot_system(self): """Prompt user and reboot system to apply all changes""" print("=" * 60) print("๐Ÿ”„ System Reboot Required") print("=" * 60) print() print("The following changes require a reboot to take effect:") print("โ€ข X11 display server configuration (for TeamViewer)") print("โ€ข TeamViewer Host service initialization") print("โ€ข Kiosk mode display settings") print("โ€ข Auto-login configuration") print() # Give user a countdown option if self.ask_yes_no("Reboot now to complete setup?", default=True): print("\n๐Ÿš€ Rebooting system in 10 seconds...") print(" Press Ctrl+C to cancel...") try: # Countdown for i in range(10, 0, -1): print(f" Rebooting in {i} seconds...", end='\r') time.sleep(1) print("\n๐Ÿ”„ Rebooting now...") # Use the configured sudo reboot command subprocess.run(['sudo', 'reboot'], check=True, timeout=5) except KeyboardInterrupt: print("\n\nโš ๏ธ Reboot cancelled by user") print("โš ๏ธ Remember to reboot manually to complete setup!") print(" Run: sudo reboot") except subprocess.CalledProcessError as e: print(f"\nโŒ Reboot command failed: {e}") print(" Try manually: sudo reboot") except Exception as e: print(f"\nโŒ Reboot error: {e}") print(" Try manually: sudo reboot") else: print("\nโš ๏ธ Reboot skipped - remember to reboot manually!") print(" Run: sudo reboot") print(" TeamViewer will be fully functional after reboot.") def ask_yes_no(self, question, default=True): """Ask yes/no question""" if default: prompt = f"{question} [Y/n]: " else: prompt = f"{question} [y/N]: " try: while True: answer = input(prompt).strip().lower() if answer in ['y', 'yes']: return True elif answer in ['n', 'no']: return False elif answer == '': return default else: print("Please answer yes or no (y/n)") except (EOFError, KeyboardInterrupt): # Handle non-interactive execution print(f"\nUsing default: {'yes' if default else 'no'}") return default def run(self): """Run the complete setup process""" try: self.print_header() self.check_system() self.install_dependencies() self.get_user_input() self.create_directory() self.download_client() self.create_config() # Configure sudo permissions for remote reboot self.configure_sudo_permissions() # Configure kiosk mode for Ubuntu 22.04 self.configure_kiosk_mode() # Configure SSH server for remote access self.configure_ssh_server() # Install TeamViewer for remote management self.install_teamviewer() # Configure TeamViewer sudo permissions for ID detection self.configure_teamviewer_sudo() # Always try to create systemd service regardless of connection test connection_ok = self.test_connection() if self.create_systemd_service(): if connection_ok: self.start_service() else: print(" โš ๏ธ Service created but not started due to connection issues") if not connection_ok: print("โš ๏ธ Setup completed but connection test failed.") print(" Please verify your server URL and network settings.") print(" The service was created and can be started once connection is working.") self.show_completion_info() # Auto-reboot to apply all changes self.reboot_system() except KeyboardInterrupt: print("\n\nโš ๏ธ Setup cancelled by user") sys.exit(1) except Exception as e: print(f"\nโŒ Setup failed: {e}") sys.exit(1) def main(): """Main entry point""" setup = SignageSetup() setup.run() if __name__ == '__main__': main()