diff --git a/README.md b/README.md index a83de4c..927c579 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ _ / / / / /_/ / / / / /_/ / /__ / /_/ / /_/ // /_/ // __/ ``` Cross Platform device manager for driving and flight simulators, for use with common simulator software titles. +📚 **Documentation:** [spacefreak18.github.io/simapi/](https://spacefreak18.github.io/simapi/) + ## Features - Updates at 60 frames per seconds. - Modular design for support with various titles and devices. @@ -18,6 +20,34 @@ Cross Platform device manager for driving and flight simulators, for use with co - Convincing shaker effects for noise tranducers for wheel slip, wheel lock, and abs, as well as engine rpm and gear shifts. - Choice of Portaudio or Pulseaudio (libpulse) backend. +## Quick Install + +**One-Line Installation:** +```bash +curl -fsSL https://raw.githubusercontent.com/Spacefreak18/monocoque/master/install.sh | bash +``` + +**Or download and review first:** +```bash +wget https://raw.githubusercontent.com/Spacefreak18/monocoque/master/install.sh +chmod +x install.sh +./install.sh +``` + +**TUI Manager:** +After installation, use the interactive manager: +```bash +monocoque-manager +``` + +**Supported Distributions:** +- ✅ Arch Linux (with AUR support) +- ✅ Debian/Ubuntu +- ✅ Fedora/RHEL/CentOS +- ✅ openSUSE + +For manual installation or troubleshooting, see [HOW-TO-USE.md](HOW-TO-USE.md). + ## Supported Games ( see [simapi](https://github.com/spacefreak18/simapi) for more details of what is supported from each sim ) - Using [SimSHMBridge](https://github.com/spacefreak18/simshmbridge) - Asseto Corsa diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a4836cf --- /dev/null +++ b/install.sh @@ -0,0 +1,554 @@ +#!/bin/bash +set -e + +# Monocoque Universal Installer +# Works on: Arch, Debian/Ubuntu, Fedora, and other major distros +# Version: 1.0.0 + +SCRIPT_VERSION="1.0.0" +INSTALL_DIR="${MONOCOQUE_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/monocoque}" +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}" +BIN_DIR="${HOME}/.local/bin" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ Monocoque Universal Installer v${SCRIPT_VERSION} ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + echo "" +} + +# Detect distribution +detect_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + DISTRO=$ID + DISTRO_VERSION=$VERSION_ID + DISTRO_LIKE=$ID_LIKE + elif [ -f /etc/arch-release ]; then + DISTRO="arch" + else + DISTRO="unknown" + fi + + # Normalize Arch-based distributions + if [[ "$DISTRO_LIKE" == *"arch"* ]] || [[ "$DISTRO" == "manjaro" ]] || [[ "$DISTRO" == "garuda" ]] || [[ "$DISTRO" == "endeavouros" ]]; then + log_info "Detected distribution: $DISTRO (Arch-based)" + DISTRO="arch" + else + log_info "Detected distribution: $DISTRO" + fi +} + +# Check if running as root +check_root() { + if [ "$EUID" -eq 0 ]; then + log_error "Please do not run this script as root" + exit 1 + fi +} + +# Check for required commands +check_requirements() { + local missing_commands=() + + for cmd in git cmake make gcc; do + if ! command -v $cmd &> /dev/null; then + missing_commands+=($cmd) + fi + done + + if [ ${#missing_commands[@]} -ne 0 ]; then + log_error "Missing required commands: ${missing_commands[*]}" + log_info "Please install them first" + exit 1 + fi +} + +# Install dependencies based on distro +install_dependencies() { + log_info "Installing dependencies for $DISTRO..." + + case $DISTRO in + arch|manjaro) + local deps="yder libuv argtable libserialport libconfig hidapi lua libxdg-basedir mingw-w64-gcc" + + # Check if yay is available for AUR + if command -v yay &> /dev/null; then + log_info "Using yay to check for AUR packages..." + echo "" + echo "You can install monocoque from AUR instead:" + echo " yay -S simapi-git simd-git monocoque-git" + echo "" + read -p "Install from AUR? (recommended) [Y/n]: " use_aur + + if [[ ! $use_aur =~ ^[Nn]$ ]]; then + log_info "Installing from AUR..." + yay -S --needed simapi-git simd-git monocoque-git + + # Still need simshmbridge + log_info "Building simshmbridge (not in AUR)..." + build_simshmbridge_only + + # Setup configs and services + setup_configs + setup_systemd_services + # create_launcher_scripts uses SCRIPT_DIR which is set in main() + create_launcher_scripts + + log_success "Installation complete (AUR method)!" + print_next_steps + exit 0 + fi + fi + + log_info "Installing build dependencies with pacman..." + sudo pacman -S --needed --noconfirm $deps + ;; + + ubuntu|debian|linuxmint|pop) + local deps="build-essential git cmake libyder-dev libuv1-dev libargtable2-dev libserialport-dev libconfig-dev libhidapi-dev lua5.3 liblua5.3-dev libxdg-basedir-dev mingw-w64" + + log_info "Updating package lists..." + sudo apt-get update + + log_info "Installing build dependencies..." + sudo apt-get install -y $deps + ;; + + fedora|rhel|centos) + local deps="git cmake gcc gcc-c++ yder-devel libuv-devel argtable-devel libserialport-devel libconfig-devel hidapi-devel lua-devel libxdg-basedir-devel mingw64-gcc" + + log_info "Installing build dependencies..." + sudo dnf install -y $deps + ;; + + opensuse*) + local deps="git cmake gcc gcc-c++ libyder-devel libuv-devel argtable-devel libserialport-devel libconfig-devel hidapi-devel lua-devel libxdg-basedir-devel mingw64-gcc" + + log_info "Installing build dependencies..." + sudo zypper install -y $deps + ;; + + *) + log_error "Unsupported distribution: $DISTRO" + log_info "Please install dependencies manually and try again" + log_info "Required: git, cmake, gcc, yder, libuv, argtable, libserialport, libconfig, hidapi, lua, libxdg-basedir, mingw-gcc" + exit 1 + ;; + esac + + log_success "Dependencies installed" +} + +# Clone repositories +clone_repositories() { + log_info "Creating installation directory: $INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + cd "$INSTALL_DIR" + + # Clone monocoque + if [ ! -d "monocoque" ]; then + log_info "Cloning monocoque..." + git clone https://github.com/Spacefreak18/monocoque.git + cd monocoque + git submodule sync --recursive + git submodule update --init --recursive + else + log_info "monocoque already cloned, updating..." + cd monocoque + git pull + git submodule sync --recursive + git submodule update --init --recursive + fi + cd "$INSTALL_DIR" + + # Clone simapi + if [ ! -d "simapi" ]; then + log_info "Cloning simapi..." + git clone https://github.com/Spacefreak18/simapi.git + else + log_info "simapi already cloned, updating..." + cd simapi + git pull + fi + cd "$INSTALL_DIR" + + # Clone simshmbridge + if [ ! -d "simshmbridge" ]; then + log_info "Cloning simshmbridge..." + git clone https://github.com/spacefreak18/simshmbridge.git + cd simshmbridge + git submodule sync --recursive + git submodule update --init --recursive + else + log_info "simshmbridge already cloned, updating..." + cd simshmbridge + git pull + git submodule sync --recursive + git submodule update --init --recursive + fi + + log_success "Repositories cloned/updated" +} + +# Build simapi +build_simapi() { + log_info "Building simapi..." + cd "$INSTALL_DIR/simapi" + + mkdir -p build + cd build + cmake .. + make -j$(nproc) + + log_info "Installing simapi library..." + sudo make install + + # Update library cache + sudo ldconfig 2>/dev/null || true + + log_success "simapi built and installed" +} + +# Build simd +build_simd() { + log_info "Building simd..." + cd "$INSTALL_DIR/simapi/simd" + + rm -rf build + mkdir -p build + cd build + cmake .. + make -j$(nproc) + + log_success "simd built" +} + +# Build simshmbridge +build_simshmbridge() { + log_info "Building simshmbridge..." + cd "$INSTALL_DIR/simshmbridge" + + make clean || true + make -j$(nproc) + + log_success "simshmbridge built" +} + +build_simshmbridge_only() { + mkdir -p "$INSTALL_DIR" + cd "$INSTALL_DIR" + + if [ ! -d "simshmbridge" ]; then + log_info "Cloning simshmbridge..." + git clone https://github.com/spacefreak18/simshmbridge.git + cd simshmbridge + git submodule sync --recursive + git submodule update --init --recursive + fi + + build_simshmbridge +} + +# Build monocoque +build_monocoque() { + log_info "Building monocoque..." + cd "$INSTALL_DIR/monocoque" + + mkdir -p build + cd build + cmake .. + make -j$(nproc) + + log_success "monocoque built" +} + +# Setup configuration files +setup_configs() { + log_info "Setting up configuration files..." + + # simd config + mkdir -p "$CONFIG_DIR/simd" + if [ ! -f "$CONFIG_DIR/simd/simd.config" ]; then + if [ -f "$INSTALL_DIR/simapi/simd/conf/simd.config" ]; then + cp "$INSTALL_DIR/simapi/simd/conf/simd.config" "$CONFIG_DIR/simd/simd.config" + log_success "Created simd config" + fi + else + log_info "simd config already exists, skipping" + fi + + # monocoque config + mkdir -p "$CONFIG_DIR/monocoque" + if [ ! -f "$CONFIG_DIR/monocoque/monocoque.config" ]; then + cat > "$CONFIG_DIR/monocoque/monocoque.config" << 'EOF' +configs = ( + { + sim = "default"; + car = "default"; + devices = ( + // Add your devices here + // Example: Serial device (ESP32/Arduino) + /* + { + device = "Serial"; + type = "Custom"; + config = "None"; + baud = 115200; + devpath = "/dev/ttyUSB0"; + }, + */ + + // Example: Bass shaker + /* + { + device = "Sound"; + effect = "Engine"; + devid = "alsa_output.your_device_here"; + pan = 0; + fps = 60; + threshold = 0.2; + channels = 2; + volume = 70; + modulation = "frequency"; + frequency = 17; + frequencyMax = 37; + }, + */ + ); + } +); +EOF + log_success "Created monocoque config" + else + log_info "monocoque config already exists, skipping" + fi +} + +# Create launcher scripts +create_launcher_scripts() { + log_info "Creating launcher scripts in $BIN_DIR..." + mkdir -p "$BIN_DIR" + + # Find simd binary location + local SIMD_BIN="" + if [ -f "$INSTALL_DIR/simapi/simd/build/simd" ]; then + SIMD_BIN="$INSTALL_DIR/simapi/simd/build/simd" + elif command -v simd &> /dev/null; then + SIMD_BIN=$(which simd) + fi + + # Find monocoque binary location + local MONOCOQUE_BIN="" + if [ -f "$INSTALL_DIR/monocoque/build/monocoque" ]; then + MONOCOQUE_BIN="$INSTALL_DIR/monocoque/build/monocoque" + elif command -v monocoque &> /dev/null; then + MONOCOQUE_BIN=$(which monocoque) + fi + + # start-simd script + cat > "$BIN_DIR/start-simd" << EOF +#!/bin/bash +export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:/usr/local/lib:/usr/local/lib64 +exec $SIMD_BIN -vv "\$@" +EOF + chmod +x "$BIN_DIR/start-simd" + + # start-monocoque script + cat > "$BIN_DIR/start-monocoque" << EOF +#!/bin/bash +exec $MONOCOQUE_BIN play "\$@" +EOF + chmod +x "$BIN_DIR/start-monocoque" + + # test-monocoque script + cat > "$BIN_DIR/test-monocoque" << EOF +#!/bin/bash +exec $MONOCOQUE_BIN test -vv "\$@" +EOF + chmod +x "$BIN_DIR/test-monocoque" + + # monocoque-manager TUI + log_info "Looking for monocoque-manager to install..." + local MANAGER_INSTALLED=false + + # Use the SCRIPT_DIR exported from main() + # Try multiple locations + local SEARCH_PATHS=( + "$SCRIPT_DIR/monocoque-manager" + "$SCRIPT_DIR/tools/monocoque-manager" + ) + + for MANAGER_PATH in "${SEARCH_PATHS[@]}"; do + log_info "Checking: $MANAGER_PATH" + if [ -f "$MANAGER_PATH" ]; then + log_info "Found monocoque-manager at: $MANAGER_PATH" + cp "$MANAGER_PATH" "$BIN_DIR/monocoque-manager" + chmod +x "$BIN_DIR/monocoque-manager" + log_success "✓ Installed monocoque-manager TUI to $BIN_DIR/monocoque-manager" + MANAGER_INSTALLED=true + break + fi + done + + if [ "$MANAGER_INSTALLED" = false ]; then + log_error "monocoque-manager not found!" + log_warn "Searched in: ${SEARCH_PATHS[*]}" + log_warn "SCRIPT_DIR was: $SCRIPT_DIR" + log_info "Please copy it manually: cp $SCRIPT_DIR/monocoque-manager ~/.local/bin/" + fi + + # Check if BIN_DIR is in PATH + if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then + log_warn "$BIN_DIR is not in your PATH" + log_info "Add this to your shell config:" + echo " # For bash/zsh:" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "" + echo " # For fish:" + echo " fish_add_path ~/.local/bin" + fi + + log_success "Launcher scripts created" +} + +# Setup systemd user services +setup_systemd_services() { + local SYSTEMD_DIR="$HOME/.config/systemd/user" + mkdir -p "$SYSTEMD_DIR" + + log_info "Creating systemd service files..." + + # Find binary locations + local SIMD_BIN="" + if [ -f "$INSTALL_DIR/simapi/simd/build/simd" ]; then + SIMD_BIN="$INSTALL_DIR/simapi/simd/build/simd" + elif command -v simd &> /dev/null; then + SIMD_BIN=$(which simd) + fi + + # simd service + cat > "$SYSTEMD_DIR/simd.service" << EOF +[Unit] +Description=Sim Telemetry Daemon +Documentation=https://spacefreak18.github.io/simapi/ +After=network.target + +[Service] +Type=simple +Environment="LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64" +ExecStart=$SIMD_BIN +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target +EOF + + log_success "systemd services created" + log_info "To enable auto-start on boot:" + echo " systemctl --user enable simd.service" + echo " systemctl --user start simd.service" +} + +# Print next steps +print_next_steps() { + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ Installation Complete! ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + echo "" + log_success "Monocoque and all components are installed!" + echo "" + echo "📁 Installation location: $INSTALL_DIR" + echo "⚙️ Configuration: $CONFIG_DIR/simd and $CONFIG_DIR/monocoque" + echo "🚀 Launcher scripts: $BIN_DIR" + echo "" + echo "Next steps:" + echo "" + echo "1️⃣ Configure your games (REQUIRED!)" + echo " See: https://github.com/Spacefreak18/monocoque/blob/master/HOW-TO-USE.md" + echo "" + echo "2️⃣ Start the services:" + echo " Terminal 1: start-simd" + echo " Terminal 2: start-monocoque" + echo "" + echo " OR enable auto-start:" + echo " systemctl --user enable --now simd.service" + echo "" + echo "3️⃣ Configure your devices:" + echo " Edit: $CONFIG_DIR/monocoque/monocoque.config" + echo "" + echo "4️⃣ Test your setup:" + echo " test-monocoque" + echo "" + echo "🎮 Supported games:" + echo " • Assetto Corsa / ACC" + echo " • Automobilista 2" + echo " • Project Cars 2" + echo " • RFactor 2" + echo " • Euro/American Truck Simulator" + echo "" + echo "📚 Documentation: https://spacefreak18.github.io/simapi/" + echo "" +} + +# Main installation flow +main() { + # Save the directory where this script is located + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + export SCRIPT_DIR + + print_header + + check_root + detect_distro + check_requirements + + log_info "Starting installation..." + echo "" + + install_dependencies + clone_repositories + + log_info "Building components (this may take a few minutes)..." + build_simapi + build_simd + build_monocoque + build_simshmbridge + + setup_configs + create_launcher_scripts + setup_systemd_services + + print_next_steps +} + +# Run main function +main "$@" + diff --git a/tools/monocoque-manager b/tools/monocoque-manager new file mode 100755 index 0000000..361b555 --- /dev/null +++ b/tools/monocoque-manager @@ -0,0 +1,616 @@ +#!/usr/bin/env python3 +""" +Monocoque Manager - Simple TUI for managing monocoque telemetry system +""" + +import subprocess +import sys +import os +import time +from pathlib import Path + +try: + import curses +except ImportError: + print("Error: curses module not available") + sys.exit(1) + +# Constants to avoid magic numbers +MIN_TERMINAL_HEIGHT = 20 +MIN_TERMINAL_WIDTH = 60 +PROCESS_STOP_WAIT_TIME = 1 # seconds +SIMD_START_DELAY = 2 # seconds + + +class MonocoqueManager: + def __init__(self): + # Mirror the shell script's robust path detection + xdg_data = os.environ.get('XDG_DATA_HOME', os.path.join(Path.home(), ".local/share")) + self.install_dir = Path(os.environ.get('MONOCOQUE_INSTALL_DIR', os.path.join(xdg_data, "monocoque"))) + + xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(Path.home(), ".config")) + self.config_dir = Path(xdg_config) + + self.simd_running = False + self.monocoque_running = False + self.last_cleaned_pid_files = [] # Track cleaned stale PID files + + def check_processes(self): + """Check if simd and monocoque are running using careful detection""" + try: + manager_pid = os.getpid() # Our own PID to exclude + + # Method 1: Exact name matching (safest) + result = subprocess.run(['pgrep', '-x', 'simd'], + capture_output=True, text=True) + simd_exact = bool(result.stdout.strip()) + + result = subprocess.run(['pgrep', '-x', 'monocoque'], + capture_output=True, text=True) + monocoque_exact = bool(result.stdout.strip()) + + # Method 2: Careful process scanning + simd_found = False + monocoque_found = False + + ps_result = subprocess.run(['ps', 'axo', 'pid,comm,args'], + capture_output=True, text=True) + if ps_result.returncode == 0: + for line in ps_result.stdout.split('\n')[1:]: # Skip header + parts = line.strip().split(None, 2) + if len(parts) >= 3: + pid, comm, args = parts + + # Skip our own process + if pid == str(manager_pid): + continue + + # Look for simd executable + if (comm == 'simd' or + (comm.endswith('simd') and 'manager' not in comm) or + ('/simd' in args and 'manager' not in args)): + simd_found = True + + # Look for monocoque executable (not manager) + elif ((comm == 'monocoque' or + (comm.endswith('monocoque') and 'manager' not in comm)) and + 'manager' not in args and + 'monocoque-manager' not in args and + 'python' not in args): + monocoque_found = True + + # Set final status + self.simd_running = simd_exact or simd_found + self.monocoque_running = monocoque_exact or monocoque_found + + # Check for stale PID files and clean them up + self.cleanup_stale_pid_files() + + except Exception: + # Fallback: set both to False if detection fails + self.simd_running = False + self.monocoque_running = False + + def cleanup_stale_pid_files(self): + """Clean up stale PID files when processes aren't actually running""" + pid_files = [ + ('/tmp/simd.pid', self.simd_running), + ('/tmp/monocoque.pid', self.monocoque_running), + ] + + cleaned_files = [] + + for pid_file_path, process_running in pid_files: + if os.path.exists(pid_file_path) and not process_running: + try: + # Read the PID and check if that process exists + with open(pid_file_path, 'r') as f: + pid_str = f.read().strip() + + if pid_str.isdigit(): + pid = int(pid_str) + # Check if this PID actually exists and is the right process + try: + ps_result = subprocess.run(['ps', '-p', str(pid), '-o', 'comm='], + capture_output=True, text=True) + if ps_result.returncode != 0: + # Process doesn't exist, PID file is stale + os.remove(pid_file_path) + cleaned_files.append(os.path.basename(pid_file_path)) + else: + # Process exists, check if it's actually simd/monocoque + comm = ps_result.stdout.strip() + service_name = os.path.basename(pid_file_path).replace('.pid', '') + if service_name not in comm: + # Wrong process, PID file is stale + os.remove(pid_file_path) + cleaned_files.append(os.path.basename(pid_file_path)) + except: + # Error checking process, assume stale and remove + os.remove(pid_file_path) + cleaned_files.append(os.path.basename(pid_file_path)) + else: + # Invalid PID format, remove stale file + os.remove(pid_file_path) + cleaned_files.append(os.path.basename(pid_file_path)) + + except Exception: + # Error reading/processing PID file, try to remove it + try: + os.remove(pid_file_path) + cleaned_files.append(os.path.basename(pid_file_path)) + except: + pass + + # Store cleaned files for potential user feedback + self.last_cleaned_pid_files = cleaned_files + return cleaned_files + + def check_installation(self): + """Check if monocoque is installed""" + simd_exists = (self.install_dir / "simapi/simd/build/simd").exists() or \ + subprocess.run(['which', 'simd'], capture_output=True).returncode == 0 + monocoque_exists = (self.install_dir / "monocoque/build/monocoque").exists() or \ + subprocess.run(['which', 'monocoque'], capture_output=True).returncode == 0 + + return simd_exists and monocoque_exists + + def draw_header(self, stdscr): + """Draw header""" + height, width = stdscr.getmaxyx() + title = "Monocoque Manager" + stdscr.attron(curses.color_pair(2) | curses.A_BOLD) + stdscr.addstr(0, (width - len(title)) // 2, title) + stdscr.attroff(curses.color_pair(2) | curses.A_BOLD) + stdscr.addstr(1, 0, "─" * width) + + def draw_status(self, stdscr, row): + """Draw status section""" + self.check_processes() + + stdscr.attron(curses.A_BOLD) + stdscr.addstr(row, 2, "Status:") + stdscr.attroff(curses.A_BOLD) + + # simd status + status_str = "● simd: " + stdscr.addstr(row + 1, 4, status_str) + if self.simd_running: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr("RUNNING") + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.attron(curses.color_pair(3)) + stdscr.addstr("STOPPED") + stdscr.attroff(curses.color_pair(3)) + + # monocoque status + status_str = "● monocoque: " + stdscr.addstr(row + 2, 4, status_str) + if self.monocoque_running: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr("RUNNING") + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.attron(curses.color_pair(3)) + stdscr.addstr("STOPPED") + stdscr.attroff(curses.color_pair(3)) + + # Show notification about cleaned PID files + current_row = row + 3 + if self.last_cleaned_pid_files: + cleaned_msg = f"● Cleaned stale PID files: {', '.join(self.last_cleaned_pid_files)}" + stdscr.attron(curses.color_pair(4)) + stdscr.addstr(current_row, 4, cleaned_msg) + stdscr.attroff(curses.color_pair(4)) + current_row += 1 + # Clear the notification after showing it once + self.last_cleaned_pid_files = [] + + return current_row + + def draw_menu(self, stdscr, row, selected): + """Draw menu options""" + height, width = stdscr.getmaxyx() + + stdscr.addstr(row, 0, "─" * width) + row += 1 + + stdscr.attron(curses.A_BOLD) + stdscr.addstr(row, 2, "Actions:") + stdscr.attroff(curses.A_BOLD) + row += 1 + + menu_items = [ + ("1", "Start simd", not self.simd_running), + ("2", "Start monocoque", not self.monocoque_running and self.simd_running), + ("3", "Test configuration", True), + ("4", "Edit monocoque config", True), + ("5", "View logs", True), + ("6", "Restart services", self.simd_running or self.monocoque_running), + ("7", "Stop all", self.simd_running or self.monocoque_running), + ("0", "Quit", True), + ] + + for i, (key, label, enabled) in enumerate(menu_items): + if enabled: + if i == selected: + stdscr.attron(curses.A_REVERSE) + stdscr.addstr(row + i, 4, f"[{key}] {label}") + if i == selected: + stdscr.attroff(curses.A_REVERSE) + else: + stdscr.attron(curses.color_pair(4)) + stdscr.addstr(row + i, 4, f"[{key}] {label}") + stdscr.attroff(curses.color_pair(4)) + + return row + len(menu_items) + + def get_enabled_menu_items(self): + """Get list of currently enabled menu items with their indices""" + menu_items = [ + ("1", "Start simd", not self.simd_running), + ("2", "Start monocoque", not self.monocoque_running and self.simd_running), + ("3", "Test configuration", True), + ("4", "Edit monocoque config", True), + ("5", "View logs", True), + ("6", "Restart services", self.simd_running or self.monocoque_running), + ("7", "Stop all", self.simd_running or self.monocoque_running), + ("0", "Quit", True), + ] + + enabled_items = [] + for i, (key, label, enabled) in enumerate(menu_items): + if enabled: + enabled_items.append((i, key)) + return enabled_items + + def draw_footer(self, stdscr): + """Draw footer""" + height, width = stdscr.getmaxyx() + footer = "↑↓/j/k: Navigate | Enter: Select | Numbers: Direct | q/ESC: Quit" + stdscr.addstr(height - 2, 0, "─" * width) + stdscr.attron(curses.color_pair(4)) + stdscr.addstr(height - 1, (width - len(footer)) // 2, footer) + stdscr.attroff(curses.color_pair(4)) + + def run_command(self, cmd, terminal=True): + """Run a command""" + if terminal: + # Try to find a terminal emulator + terminals = ['konsole', 'gnome-terminal', 'xfce4-terminal', + 'alacritty', 'kitty', 'xterm'] + + for term in terminals: + if subprocess.run(['which', term], capture_output=True).returncode == 0: + if term == 'konsole': + subprocess.Popen([term, '-e', 'bash', '-c', cmd]) + elif term == 'gnome-terminal': + subprocess.Popen([term, '--', 'bash', '-c', cmd]) + elif term == 'kitty': + # Kitty prefers commands as positional arguments + subprocess.Popen([term, 'bash', '-c', cmd]) + elif term in ['xfce4-terminal', 'alacritty', 'xterm']: + # Ensure the whole bash command is quoted correctly for -e + subprocess.Popen([term, '-e', f"bash -c '{cmd}'"]) + return True + return False + else: + subprocess.run(cmd, shell=True) + return True + + def handle_action(self, action): + """Handle menu action""" + if action == '1': # Start simd + cmd = "start-simd || $HOME/.local/bin/start-simd; exec bash" + self.run_command(cmd) + + elif action == '2': # Start monocoque + cmd = "start-monocoque || $HOME/.local/bin/start-monocoque; exec bash" + self.run_command(cmd) + + elif action == '3': # Test configuration + cmd = "test-monocoque || $HOME/.local/bin/test-monocoque; read -p 'Press Enter to close...'" + self.run_command(cmd) + + elif action == '4': # Edit config + editor = os.environ.get('EDITOR', 'nano') + config_file = self.config_dir / "monocoque/monocoque.config" + subprocess.run([editor, str(config_file)]) + + elif action == '5': # View logs + xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(Path.home(), ".cache")) + log_dir = Path(xdg_cache) / "monocoque" + if log_dir.exists(): + cmd = f"find {log_dir} -name '*.log' -exec ls -la {{}} + 2>/dev/null; find {log_dir} -name '*.log' -exec tail -f {{}} + 2>/dev/null; read -p 'Press Enter to close...'" + self.run_command(cmd) + else: + # Create a simple message if no logs exist + cmd = f"echo 'No log directory found at {log_dir}'; echo 'Logs will appear here when monocoque runs'; read -p 'Press Enter to close...'" + self.run_command(cmd) + + elif action == '6': # Restart services + stopped = self.stop_all_services() + if stopped: + print(f"Stopped: {', '.join(stopped)}") + # Wait a moment for processes to stop + time.sleep(PROCESS_STOP_WAIT_TIME) + # Restart simd first + cmd = f"start-simd || $HOME/.local/bin/start-simd; sleep {SIMD_START_DELAY}; start-monocoque || $HOME/.local/bin/start-monocoque; exec bash" + self.run_command(cmd) + print("Restarting services in terminal window...") + + elif action == '7': # Stop all + stopped = self.stop_all_services() + if stopped: + print(f"Stopped: {', '.join(stopped)}") + else: + print("No running services found to stop") + + def stop_all_services(self): + """Carefully stop simd and monocoque processes while preserving the manager""" + stopped_services = [] + manager_pid = os.getpid() # Get our own PID to exclude it + + # Method 1: Try exact name matching (safest) + for service in ['simd', 'monocoque']: + result = subprocess.run(['pkill', '-x', service], + capture_output=True, text=True) + if result.returncode == 0: + stopped_services.append(f"{service} (exact)") + + # Method 2: Carefully search ps output and target specific processes + try: + ps_result = subprocess.run(['ps', 'axo', 'pid,ppid,comm,args'], + capture_output=True, text=True) + if ps_result.returncode == 0: + lines = ps_result.stdout.split('\n')[1:] # Skip header + for line in lines: + parts = line.strip().split(None, 3) # Split into PID, PPID, COMM, ARGS + if len(parts) >= 4: + pid, ppid, comm, args = parts + + # Skip our own process + if pid == str(manager_pid): + continue + + # Target simd: look for executables ending in 'simd' or containing '/simd' + if (comm == 'simd' or + (comm.endswith('simd') and not 'manager' in comm) or + ('/simd' in args and 'manager' not in args)): + try: + subprocess.run(['kill', '-TERM', pid], capture_output=True) + stopped_services.append(f"simd (PID {pid})") + time.sleep(0.1) # Brief pause between kills + except: + pass + + # Target monocoque: be very specific to avoid manager + elif ((comm == 'monocoque' or + (comm.endswith('monocoque') and not 'manager' in comm)) and + 'manager' not in args and + 'monocoque-manager' not in args and + 'python' not in args): # Avoid Python scripts + try: + subprocess.run(['kill', '-TERM', pid], capture_output=True) + stopped_services.append(f"monocoque (PID {pid})") + time.sleep(0.1) # Brief pause between kills + except: + pass + except: + pass + + # Wait for graceful shutdown + time.sleep(PROCESS_STOP_WAIT_TIME) + + # Method 3: Force kill stubborn processes (but still be careful) + try: + # Only force kill exact matches, never pattern matches + for service in ['simd', 'monocoque']: + subprocess.run(['pkill', '-9', '-x', service], + capture_output=True, text=True) + except: + pass + + return stopped_services + + def main(self, stdscr): + """Main TUI loop""" + try: + curses.curs_set(0) + except curses.error: + pass # Some terminals don't support cursor visibility changes + + try: + stdscr.keypad(True) # Enable keypad mode for arrow keys + except curses.error: + pass # Fallback if keypad mode fails + + # Initialize colors if available + if curses.has_colors(): + curses.start_color() + try: + curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) + except curses.error: + pass # Fallback for terminals with limited color support + + selected = 0 + + while True: + stdscr.clear() + height, width = stdscr.getmaxyx() + + # Ensure selected index is valid for current state + self.check_processes() + enabled_items = self.get_enabled_menu_items() + enabled_indices = [item[0] for item in enabled_items] + if enabled_indices and selected not in enabled_indices: + selected = enabled_indices[0] + + if height < MIN_TERMINAL_HEIGHT or width < MIN_TERMINAL_WIDTH: + stdscr.addstr(0, 0, "Terminal too small!") + stdscr.addstr(1, 0, f"Minimum: {MIN_TERMINAL_WIDTH}x{MIN_TERMINAL_HEIGHT}") + stdscr.refresh() + stdscr.getch() + continue + + self.draw_header(stdscr) + row = self.draw_status(stdscr, 3) + row = self.draw_menu(stdscr, row + 1, selected) + self.draw_footer(stdscr) + + stdscr.refresh() + + key = stdscr.getch() + + # Get currently enabled menu items + enabled_items = self.get_enabled_menu_items() + enabled_indices = [item[0] for item in enabled_items] + + # Handle arrow key navigation (with multiple key codes for compatibility) + if key in [curses.KEY_UP, 259, ord('k'), ord('K')]: # UP arrow or k/K + if enabled_indices: + current_pos = enabled_indices.index(selected) if selected in enabled_indices else 0 + new_pos = (current_pos - 1) % len(enabled_indices) + selected = enabled_indices[new_pos] + elif key in [curses.KEY_DOWN, 258, ord('j'), ord('J')]: # DOWN arrow or j/J + if enabled_indices: + current_pos = enabled_indices.index(selected) if selected in enabled_indices else 0 + new_pos = (current_pos + 1) % len(enabled_indices) + selected = enabled_indices[new_pos] + elif key in [curses.KEY_ENTER, ord('\n'), ord('\r'), 10, 13]: # Various ENTER codes + # Execute currently selected action + action_key = None + for idx, key_char in enabled_items: + if idx == selected: + action_key = key_char + break + if action_key and action_key == '0': + break + elif action_key: + self.handle_action(action_key) + # Force a refresh after action + stdscr.clear() + elif key in [ord('q'), ord('Q'), 27]: # q/Q or ESC + break + elif key == ord('1'): + self.handle_action('1') + stdscr.clear() # Force refresh after action + elif key == ord('2'): + self.handle_action('2') + stdscr.clear() # Force refresh after action + elif key == ord('3'): + self.handle_action('3') + stdscr.clear() # Force refresh after action + elif key == ord('4'): + self.handle_action('4') + stdscr.clear() # Force refresh after action + elif key == ord('5'): + self.handle_action('5') + stdscr.clear() # Force refresh after action + elif key == ord('6'): + self.handle_action('6') + stdscr.clear() # Force refresh after action + elif key == ord('7'): + self.handle_action('7') + stdscr.clear() # Force refresh after action + elif key == ord('0'): + break + + + def text_interface(self): + """Fallback text-based interface when curses is not available""" + print("Monocoque Manager - Text Mode") + print("=" * 40) + + while True: + self.check_processes() + + print("\nStatus:") + print(f" simd: {'RUNNING' if self.simd_running else 'STOPPED'}") + print(f" monocoque: {'RUNNING' if self.monocoque_running else 'STOPPED'}") + + # Show notification about cleaned PID files + if self.last_cleaned_pid_files: + print(f" Note: Cleaned stale PID files: {', '.join(self.last_cleaned_pid_files)}") + # Clear the notification after showing it once + self.last_cleaned_pid_files = [] + + print("\nActions:") + menu_items = [ + ("1", "Start simd", not self.simd_running), + ("2", "Start monocoque", not self.monocoque_running and self.simd_running), + ("3", "Test configuration", True), + ("4", "Edit monocoque config", True), + ("5", "View logs", True), + ("6", "Restart services", self.simd_running or self.monocoque_running), + ("7", "Stop all", self.simd_running or self.monocoque_running), + ("0", "Quit", True), + ] + + for key, label, enabled in menu_items: + if enabled: + print(f" [{key}] {label}") + else: + print(f" [{key}] {label} (disabled)") + + try: + choice = input("\nEnter your choice: ").strip() + if choice == '0': + break + elif choice in ['1', '2', '3', '4', '5', '6', '7']: + # Check if action is enabled + for key, label, enabled in menu_items: + if key == choice and enabled: + self.handle_action(choice) + if choice in ['4']: # Actions that don't need terminal + print("Action completed.") + elif choice in ['6', '7']: # Stop/restart actions provide their own feedback + pass # Feedback already provided by handle_action + else: + print("Command executed. Check the terminal output.") + break + elif key == choice and not enabled: + print("That action is currently disabled.") + break + else: + print("Invalid choice. Please try again.") + except (KeyboardInterrupt, EOFError): + print("\nExiting...") + break + + +def main(): + """Entry point""" + manager = MonocoqueManager() + + if not manager.check_installation(): + print("Error: Monocoque does not appear to be installed") + print("Run the installer first: ./install.sh") + sys.exit(1) + + # Check if we can use curses (proper terminal environment) + term = os.environ.get('TERM', 'dumb') + if term == 'dumb' or not sys.stdout.isatty(): + print("Note: Using text mode (terminal doesn't support full TUI)") + manager.text_interface() + else: + try: + curses.wrapper(manager.main) + except KeyboardInterrupt: + print("\nExiting...") + except curses.error as e: + print(f"Terminal error: {e}") + print("Falling back to text mode...") + manager.text_interface() + except Exception as e: + print(f"Unexpected error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/tools/uninstall.sh b/tools/uninstall.sh new file mode 100755 index 0000000..7b0d928 --- /dev/null +++ b/tools/uninstall.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Monocoque Uninstaller +# Removes monocoque and all related components + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +INSTALL_DIR="${MONOCOQUE_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/monocoque}" +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}" +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}" +BIN_DIR="$HOME/.local/bin" +SYSTEMD_DIR="$CONFIG_DIR/systemd/user" + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo "" + echo "╔══════════════════════════════════════════════════════════════════╗" + echo "║ Monocoque Uninstaller ║" + echo "╚══════════════════════════════════════════════════════════════════╝" + echo "" +} + +print_header + +log_warn "This will remove:" +echo " • Monocoque installation ($INSTALL_DIR)" +echo " • Configuration files ($CONFIG_DIR/monocoque, $CONFIG_DIR/simd)" +echo " • Launcher scripts ($BIN_DIR/start-*, test-monocoque)" + echo " • systemd service files ($SYSTEMD_DIR/simd.service)" + echo " • Log files ($CACHE_DIR/monocoque)" + echo "" +echo "This will NOT remove:" +echo " • System dependencies (yder, libuv, etc.)" +echo " • Compiled simapi library (/usr/local/lib/libsimapi.so)" +echo "" + +read -p "Continue with uninstallation? [y/N]: " confirm + +if [[ ! $confirm =~ ^[Yy]$ ]]; then + log_info "Uninstallation cancelled" + exit 0 +fi + +echo "" +log_info "Starting uninstallation..." + +# Stop running services +log_info "Stopping running services..." +pkill -x simd 2>/dev/null || true +pkill -x monocoque 2>/dev/null || true +systemctl --user stop simd.service 2>/dev/null || true +systemctl --user disable simd.service 2>/dev/null || true + +# Remove installation directory +if [ -d "$INSTALL_DIR" ]; then + log_info "Removing installation directory..." + rm -rf "$INSTALL_DIR" + log_success "Installation directory removed" +else + log_info "Installation directory not found, skipping" +fi + +# Remove configuration (ask first) +if [ -d "$CONFIG_DIR/monocoque" ] || [ -d "$CONFIG_DIR/simd" ]; then + read -p "Remove configuration files? [y/N]: " remove_config + if [[ $remove_config =~ ^[Yy]$ ]]; then + rm -rf "$CONFIG_DIR/monocoque" 2>/dev/null || true + rm -rf "$CONFIG_DIR/simd" 2>/dev/null || true + log_success "Configuration files removed" + else + log_info "Keeping configuration files" + fi +fi + +# Remove launcher scripts +log_info "Removing launcher scripts..." +rm -f "$BIN_DIR/start-simd" 2>/dev/null || true +rm -f "$BIN_DIR/start-monocoque" 2>/dev/null || true +rm -f "$BIN_DIR/test-monocoque" 2>/dev/null || true +rm -f "$BIN_DIR/monocoque-manager" 2>/dev/null || true + +# Remove systemd services +if [ -f "$SYSTEMD_DIR/simd.service" ]; then + log_info "Removing systemd service files..." + rm -f "$SYSTEMD_DIR/simd.service" + systemctl --user daemon-reload 2>/dev/null || true +fi + +# Remove logs +if [ -d "$CACHE_DIR/monocoque" ]; then + read -p "Remove log files? [y/N]: " remove_logs + if [[ $remove_logs =~ ^[Yy]$ ]]; then + rm -rf "$CACHE_DIR/monocoque" + log_success "Log files removed" + else + log_info "Keeping log files" + fi +fi + +echo "" +log_success "Uninstallation complete!" +echo "" +log_info "To remove system dependencies and simapi library:" +echo " • Remove packages: yder, libuv, argtable, libserialport, etc." +echo " • Remove library: sudo rm /usr/local/lib/libsimapi.so*" +echo " • Run: sudo ldconfig" +echo "" +