ckriti/HuggingClaw
0
1#!/usr/bin/env python32"""3OpenClaw Sync Manager for Hugging Face Spaces4==============================================5 6This script manages the complete lifecycle of OpenClaw in a Hugging Face Space:71. Restores state on startup (load)82. Runs periodic backups (save)93. Ensures clean shutdown with final backup10 11This is the main entry point for running OpenClaw in Hugging Face Spaces.12 13Usage:14 python3 openclaw_sync.py15 16Environment Variables:17 HF_TOKEN - Hugging Face access token18 OPENCLAW_DATASET_REPO - Dataset for persistence (e.g., "username/openclaw")19 OPENCLAW_HOME - OpenClaw home directory (default: ~/.openclaw)20 SYNC_INTERVAL - Seconds between automatic backups (default: 300)21"""22 23import os24import sys25import time26import signal27import subprocess28import threading29import json30from datetime import datetime31from pathlib import Path32 33# Add parent directory to path for imports34sys.path.insert(0, str(Path(__file__).parent))35 36from openclaw_persist import OpenClawPersistence, Config, log37 38 39class SyncManager:40 """Manages sync and app lifecycle"""41 42 def __init__(self):43 # Configuration44 self.sync_interval = int(os.environ.get("SYNC_INTERVAL", "300")) # 5 minutes default45 self.app_dir = Path(os.environ.get("OPENCLAW_APP_DIR", "/app/openclaw"))46 self.node_path = os.environ.get("NODE_PATH", f"{self.app_dir}/node_modules")47 48 # State49 self.running = False50 self.stop_event = threading.Event()51 self.app_process = None52 self.aux_processes = []53 54 # Persistence55 self.persist = None56 try:57 self.persist = OpenClawPersistence()58 log("INFO", "Persistence initialized",59 sync_interval=self.sync_interval)60 except Exception as e:61 log("WARNING", "Persistence not available, running without backup",62 error=str(e))63 64 # -----------------------------------------------------------------------65 # Lifecycle Management66 # -----------------------------------------------------------------------67 68 def start(self):69 """Main entry point - restore, run app, sync loop"""70 log("INFO", "Starting OpenClaw Sync Manager")71 72 # 1. Initial restore73 self.restore_state()74 75 # 2. Setup signal handlers76 self._setup_signals()77 78 # 3. Start aux services (if enabled)79 self.start_aux_services()80 81 # 4. Start application82 self.start_application()83 84 # 5. Start background sync85 self.start_background_sync()86 87 # 6. Wait for completion88 self.wait_for_exit()89 90 def restore_state(self):91 """Restore state from dataset on startup"""92 if not self.persist:93 log("INFO", "Skipping restore (persistence not configured)")94 # Still need to ensure config exists95 self._ensure_default_config()96 return97 98 log("INFO", "Restoring state from dataset...")99 100 result = self.persist.load(force=False)101 102 if result.get("success"):103 if result.get("restored"):104 log("INFO", "State restored successfully",105 backup_file=result.get("backup_file"))106 else:107 log("INFO", "No previous state found, starting fresh")108 # Ensure default config for fresh start109 self._ensure_default_config()110 else:111 log("ERROR", "State restore failed", error=result.get("error"))112 113 def _ensure_default_config(self):114 """Ensure openclaw.json exists with valid config"""115 import json116 from openclaw_persist import Config117 118 config_path = Config.OPENCLAW_HOME / "openclaw.json"119 default_config_path = Path(__file__).parent / "openclaw.json.default"120 121 if config_path.exists():122 log("INFO", "Config file exists, skipping")123 return124 125 log("INFO", "No config found, creating default")126 127 config_path.parent.mkdir(parents=True, exist_ok=True)128 129 # Try to load default config130 if default_config_path.exists():131 try:132 with open(default_config_path, 'r') as f:133 config = json.load(f)134 with open(config_path, 'w') as f:135 json.dump(config, f, indent=2)136 log("INFO", "Default config created from template")137 return138 except Exception as e:139 log("WARNING", "Could not load default config template", error=str(e))140 141 # Create minimal config142 minimal_config = {143 "gateway": {144 "mode": "local",145 "bind": "lan",146 "port": 7860,147 "auth": {"token": "openclaw-space-default"},148 "controlUi": {149 "allowInsecureAuth": True,150 "allowedOrigins": [151 "https://huggingface.co"152 ]153 }154 },155 "session": {"scope": "global"},156 "models": {157 "mode": "merge",158 "providers": {}159 },160 "agents": {161 "defaults": {162 "workspace": "~/.openclaw/workspace"163 }164 }165 }166 167 with open(config_path, 'w') as f:168 json.dump(minimal_config, f, indent=2)169 log("INFO", "Minimal config created")170 171 def start_application(self):172 """Start the main OpenClaw application"""173 log("INFO", "Starting OpenClaw application")174 175 # Prepare environment176 env = os.environ.copy()177 env["NODE_PATH"] = self.node_path178 env["NODE_ENV"] = "production"179 180 # Prepare command - use shell with tee for log capture181 cmd_str = "node dist/entry.js gateway"182 183 log("INFO", "Executing command",184 cmd=cmd_str,185 cwd=str(self.app_dir))186 187 # Start process with shell=True for proper output handling188 self.app_process = subprocess.Popen(189 cmd_str,190 shell=True,191 cwd=str(self.app_dir),192 env=env,193 stdout=sys.stdout,194 stderr=sys.stderr,195 )196 197 log("INFO", "Application started", pid=self.app_process.pid)198 199 def start_aux_services(self):200 """Start auxiliary services like WA guardian and QR manager"""201 env = os.environ.copy()202 env["NODE_PATH"] = self.node_path203 204 # Only start if explicitly enabled205 if os.environ.get("ENABLE_AUX_SERVICES", "false").lower() == "true":206 # WA Login Guardian207 wa_guardian = Path(__file__).parent / "wa-login-guardian.cjs"208 if wa_guardian.exists():209 try:210 p = subprocess.Popen(211 ["node", str(wa_guardian)],212 env=env,213 stdout=sys.stdout,214 stderr=sys.stderr215 )216 self.aux_processes.append(p)217 log("INFO", "WA Guardian started", pid=p.pid)218 except Exception as e:219 log("WARNING", "Could not start WA Guardian", error=str(e))220 221 # QR Detection Manager222 qr_manager = Path(__file__).parent / "qr-detection-manager.cjs"223 space_host = os.environ.get("SPACE_HOST", "")224 if qr_manager.exists():225 try:226 p = subprocess.Popen(227 ["node", str(qr_manager), space_host],228 env=env,229 stdout=sys.stdout,230 stderr=sys.stderr231 )232 self.aux_processes.append(p)233 log("INFO", "QR Manager started", pid=p.pid)234 except Exception as e:235 log("WARNING", "Could not start QR Manager", error=str(e))236 else:237 log("INFO", "Aux services disabled")238 239 def start_background_sync(self):240 """Start periodic backup in background"""241 if not self.persist:242 log("INFO", "Skipping background sync (persistence not configured)")243 return244 245 self.running = True246 247 def sync_loop():248 while not self.stop_event.is_set():249 # Wait for interval or stop250 if self.stop_event.wait(timeout=self.sync_interval):251 break252 253 # Perform backup254 log("INFO", "Periodic backup triggered")255 self.do_backup()256 257 thread = threading.Thread(target=sync_loop, daemon=True)258 thread.start()259 log("INFO", "Background sync started",260 interval_seconds=self.sync_interval)261 262 def do_backup(self):263 """Perform a backup operation"""264 if not self.persist:265 return266 267 try:268 result = self.persist.save()269 if result.get("success"):270 log("INFO", "Backup completed successfully",271 operation_id=result.get("operation_id"),272 remote_path=result.get("remote_path"))273 else:274 log("ERROR", "Backup failed", error=result.get("error"))275 except Exception as e:276 log("ERROR", "Backup exception", error=str(e), exc_info=True)277 278 def wait_for_exit(self):279 """Wait for app process to exit"""280 if not self.app_process:281 log("ERROR", "No app process to wait for")282 return283 284 log("INFO", "Waiting for application to exit...")285 286 exit_code = self.app_process.wait()287 log("INFO", f"Application exited with code {exit_code}")288 289 # Stop sync290 self.stop_event.set()291 292 # Terminate aux processes293 for p in self.aux_processes:294 try:295 p.terminate()296 p.wait(timeout=2)297 except subprocess.TimeoutExpired:298 p.kill()299 except Exception:300 pass301 302 # Final backup303 log("INFO", "Performing final backup...")304 self.do_backup()305 306 sys.exit(exit_code)307 308 def _setup_signals(self):309 """Setup signal handlers for graceful shutdown"""310 def handle_signal(signum, frame):311 log("INFO", f"Received signal {signum}, initiating shutdown...")312 313 # Stop sync314 self.stop_event.set()315 316 # Terminate app317 if self.app_process:318 log("INFO", "Terminating application...")319 self.app_process.terminate()320 try:321 self.app_process.wait(timeout=5)322 except subprocess.TimeoutExpired:323 self.app_process.kill()324 325 # Terminate aux326 for p in self.aux_processes:327 try:328 p.terminate()329 p.wait(timeout=2)330 except subprocess.TimeoutExpired:331 p.kill()332 except Exception:333 pass334 335 # Final backup336 if self.persist:337 log("INFO", "Performing final backup on shutdown...")338 self.do_backup()339 340 sys.exit(0)341 342 signal.signal(signal.SIGINT, handle_signal)343 signal.signal(signal.SIGTERM, handle_signal)344 345 346# ============================================================================347# Main Entry Point348# ============================================================================349 350def main():351 """Main entry point"""352 log("INFO", "OpenClaw Sync Manager starting...")353 log("INFO", "Configuration",354 home_dir=str(Config.OPENCLAW_HOME),355 repo_id=os.environ.get("OPENCLAW_DATASET_REPO", "not set"),356 sync_interval=os.environ.get("SYNC_INTERVAL", "300"))357 358 manager = SyncManager()359 manager.start()360 361 362if __name__ == "__main__":363 main()364 