CoolFace
Apppublic

ckriti/HuggingClaw

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
openclaw_sync.py364 linesDownload Raw Back to scripts
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