import os import subprocess import shlex import gradio as gr import shutil import time import threading import re # Configuration REPO_DIR = "levanter" DEFAULT_REPO = "https://github.com/priscy82/levanter.git" PM2_APP_NAME = "whatsapp-bot" def run_command(command, cwd=None, timeout=300): """Execute a command and capture output with enhanced environment""" try: env = os.environ.copy() # Set critical environment variables env['PATH'] = '/usr/local/bin:/usr/bin:/bin' env['PM2_HOME'] = '/tmp/.pm2' env['NPM_CONFIG_CACHE'] = '/tmp/npm_cache' env['YARN_CACHE_FOLDER'] = '/tmp/yarn_cache' env['NODE_OPTIONS'] = '--max-old-space-size=512' # Use shlex to properly handle command parsing process = subprocess.run( shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd, timeout=timeout, env=env ) output = process.stdout if process.stderr: output += "\nERROR: " + process.stderr return output except subprocess.TimeoutExpired: return "Command timed out after {} seconds".format(timeout) except Exception as e: return f"Command failed: {str(e)}" def clone_and_install(repo_url): """Clone repository and install dependencies with robust error handling""" # Remove existing repo if os.path.exists(REPO_DIR): try: shutil.rmtree(REPO_DIR) except Exception as e: return f"❌ Failed to remove existing directory: {str(e)}" # Clone repo with retries clone_attempts = 0 clone_output = "" while clone_attempts < 3: clone_cmd = f"git clone {repo_url} {REPO_DIR}" clone_output = run_command(clone_cmd) if os.path.exists(REPO_DIR): break clone_attempts += 1 time.sleep(2) if not os.path.exists(REPO_DIR): return f"❌ Clone failed after 3 attempts!\n{clone_output}" # Navigate to repo and install dependencies os.chdir(REPO_DIR) # Check for lock files to determine package manager package_manager = "yarn" if os.path.exists("yarn.lock") else "npm" # Install dependencies install_output = "" if package_manager == "yarn": install_output += run_command("yarn install --frozen-lockfile --network-timeout 300000") else: install_output += run_command("npm install --legacy-peer-deps --no-audit") # Handle common errors automatically if "ERR_PNPM" in install_output or "ERR!" in install_output: install_output += "\n⚠️ Detected installation issues. Trying fallback..." install_output += run_command("rm -rf node_modules") install_output += run_command("rm -f package-lock.json yarn.lock") install_output += run_command("npm cache clean --force") install_output += run_command("npm install --legacy-peer-deps --force") os.chdir("..") return f"✅ Repository cloned!\n{clone_output}\n\n💿 Dependencies installed ({package_manager})!\n{install_output}" def start_bot(): """Start the bot using the appropriate package manager with PM2""" if not os.path.exists(REPO_DIR): return "❌ Repository not found! Clone first." os.chdir(REPO_DIR) # Determine package manager package_manager = "yarn" if os.path.exists("yarn.lock") else "npm" # Start the bot with PM2 if package_manager == "yarn": start_cmd = f"pm2 start yarn --name {PM2_APP_NAME} -- start --watch --restart-delay=5000 --max-memory-restart 300M" else: start_cmd = f"pm2 start npm --name {PM2_APP_NAME} -- start --watch --restart-delay=5000 --max-memory-restart 300M" start_output = run_command(start_cmd, timeout=60) # Save PM2 process list run_command("pm2 save") os.chdir("..") return f"🤖 Bot started with PM2!\n{start_output}" def stop_bot(): """Stop the bot""" if not os.path.exists(REPO_DIR): return "❌ No active bot process" os.chdir(REPO_DIR) stop_output = run_command(f"pm2 stop {PM2_APP_NAME}") os.chdir("..") return f"🛑 Bot stopped!\n{stop_output}" def get_logs(): """Get bot logs""" if not os.path.exists(REPO_DIR): return "No logs available" os.chdir(REPO_DIR) logs = run_command(f"pm2 logs {PM2_APP_NAME} --nostream --lines 100") os.chdir("..") return logs or "No logs available" def get_bot_status(): """Get bot status with detailed information""" if not os.path.exists(REPO_DIR): return "Not deployed" os.chdir(REPO_DIR) status = run_command(f"pm2 status {PM2_APP_NAME}") os.chdir("..") # Enhance status output if status: if "online" in status: return f"🟢 Bot is running!\n{status}" elif "stopped" in status or "errored" in status: return f"🔴 Bot is stopped!\n{status}" elif "restarting" in status: return f"🟡 Bot is restarting!\n{status}" return "Status unknown" def run_ssh_command(command): """Execute custom command in repo directory""" if not os.path.exists(REPO_DIR): return "❌ Repository not found! Clone first." os.chdir(REPO_DIR) output = run_command(command) os.chdir("..") return output def deploy_sequence(repo_url): """Full deployment sequence with error recovery""" output = clone_and_install(repo_url) if "❌" in output: return output output += "\n\n" + start_bot() return output def monitor_bot(): """Background thread to monitor and restart bot""" while True: try: if os.path.exists(REPO_DIR): status = get_bot_status() if "🔴" in status or "errored" in status: print("Bot crashed! Restarting...") start_bot() except Exception as e: print(f"Monitor error: {str(e)}") time.sleep(30) # Start monitoring thread monitor_thread = threading.Thread(target=monitor_bot, daemon=True) monitor_thread.start() # Gradio Interface with gr.Blocks(title="WhatsApp Bot Deployer", theme="soft") as demo: gr.Markdown(""" # 🤖 WhatsApp Bot Deployment Panel **Repository:** https://github.com/priscy82/levanter.git **Node Version:** 23 | **PM2:** Installed | **Auto-Restart:** Enabled """) with gr.Tab("🚀 Deploy Bot"): repo_input = gr.Textbox( label="Git Repository URL", value=DEFAULT_REPO, interactive=False ) deploy_btn = gr.Button("🚀 Clone & Deploy", variant="primary") deploy_output = gr.Textbox(label="Deployment Logs", interactive=False, lines=12) with gr.Tab("⚙️ Bot Controls"): with gr.Row(): status_btn = gr.Button("🔄 Refresh Status", variant="secondary") start_btn = gr.Button("▶️ Start Bot", variant="primary") stop_btn = gr.Button("⏹️ Stop Bot", variant="stop") restart_btn = gr.Button("🔁 Restart Bot", variant="primary") status_output = gr.Textbox(label="Current Status", interactive=False, lines=4) controls_output = gr.Textbox(label="Action Output", interactive=False, lines=4) gr.Markdown("### 📋 Log Viewer") with gr.Row(): log_lines = gr.Slider(50, 500, value=100, label="Log Lines") log_btn = gr.Button("📋 Get Latest Logs") log_output = gr.Textbox(label="Bot Logs", interactive=False, lines=18) with gr.Tab("💻 Terminal"): gr.Markdown("### ⚡ Run Custom Commands") cmd_input = gr.Textbox( label="Enter command", placeholder="npm install package-name", lines=1 ) run_btn = gr.Button("▶️ Run", variant="primary") terminal_output = gr.Textbox(label="Output", interactive=False, lines=12) gr.Markdown("### 🚀 Common Commands") examples = gr.Examples( examples=[ ["npm install --legacy-peer-deps"], ["yarn install --frozen-lockfile"], ["git pull"], ["node --version"], ["npm run build"], ["pm2 status"], ["pm2 logs"] ], inputs=cmd_input, label="Click to insert command" ) # Deployment tab actions deploy_btn.click( deploy_sequence, inputs=repo_input, outputs=deploy_output ) # Status and control actions status_btn.click( get_bot_status, outputs=status_output ) start_btn.click( start_bot, outputs=controls_output ).then( get_bot_status, outputs=status_output ) stop_btn.click( stop_bot, outputs=controls_output ).then( get_bot_status, outputs=status_output ) restart_btn.click( lambda: run_ssh_command(f"pm2 restart {PM2_APP_NAME}"), outputs=controls_output ).then( get_bot_status, outputs=status_output ) # Log actions log_btn.click( get_logs, outputs=log_output ) # Terminal actions run_btn.click( run_ssh_command, inputs=cmd_input, outputs=terminal_output ) if __name__ == "__main__": demo.launch(server_port=7860, server_name="0.0.0.0")