codex-mobile-bridge 0.3.7

Remote bridge and service manager for codex-mobile.
Documentation
use std::sync::Arc;

use anyhow::Result;
use serde_json::{Value, json};

use super::handshake::{default_opt_out_notification_methods, parse_initialize_info};
use super::{
    APP_SERVER_EXPERIMENTAL_API_ENABLED, AppServerInbound, AppServerManager, RunningAppServer,
};

impl AppServerManager {
    pub fn new(
        launch_config: super::AppServerLaunchConfig,
        inbound_tx: tokio::sync::mpsc::UnboundedSender<AppServerInbound>,
    ) -> Self {
        Self {
            launch_config,
            inbound_tx,
            inner: tokio::sync::Mutex::new(None),
        }
    }

    pub fn runtime_id(&self) -> &str {
        &self.launch_config.runtime_id
    }

    pub async fn start(&self) -> Result<()> {
        self.ensure_started().await.map(|_| ())
    }

    pub async fn stop(&self) -> Result<()> {
        let existing = {
            let mut guard = self.inner.lock().await;
            guard.take()
        };

        if let Some(existing) = existing {
            existing.stop().await?;
            for _ in 0..30 {
                if !existing.is_alive() {
                    break;
                }
                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
            }
        }

        Ok(())
    }

    pub async fn restart(&self) -> Result<()> {
        self.stop().await?;
        self.start().await
    }

    pub async fn request(&self, method: &str, params: Value) -> Result<Value> {
        let running = self.ensure_started().await?;
        running.request(method, params).await
    }

    pub async fn respond(&self, id: Value, result: Value) -> Result<()> {
        let running = self.ensure_started().await?;
        running.respond(id, result).await
    }

    pub async fn respond_error(&self, id: Value, code: i64, message: &str) -> Result<()> {
        let running = self.ensure_started().await?;
        running.respond_error(id, code, message).await
    }

    async fn ensure_started(&self) -> Result<Arc<RunningAppServer>> {
        {
            let guard = self.inner.lock().await;
            if let Some(existing) = guard.as_ref().filter(|existing| existing.is_alive()) {
                return Ok(Arc::clone(existing));
            }
        }

        let opt_out_notification_methods = default_opt_out_notification_methods();
        let _ = self.inbound_tx.send(AppServerInbound::Starting {
            runtime_id: self.launch_config.runtime_id.clone(),
        });
        let running =
            RunningAppServer::spawn(self.launch_config.clone(), self.inbound_tx.clone()).await?;
        {
            let mut guard = self.inner.lock().await;
            *guard = Some(Arc::clone(&running));
        }
        let _ = self.inbound_tx.send(AppServerInbound::Initializing {
            runtime_id: self.launch_config.runtime_id.clone(),
            experimental_api_enabled: APP_SERVER_EXPERIMENTAL_API_ENABLED,
            opt_out_notification_methods: opt_out_notification_methods.clone(),
        });

        let init_result = match running
            .request(
                "initialize",
                json!({
                    "clientInfo": {
                        "name": "codex-mobile-bridge",
                        "title": "Codex Mobile Bridge",
                        "version": env!("CARGO_PKG_VERSION"),
                    },
                    "capabilities": {
                        "experimentalApi": APP_SERVER_EXPERIMENTAL_API_ENABLED,
                        "optOutNotificationMethods": opt_out_notification_methods,
                    }
                }),
            )
            .await
        {
            Ok(result) => result,
            Err(error) => {
                self.abort_startup(
                    &running,
                    APP_SERVER_EXPERIMENTAL_API_ENABLED,
                    &default_opt_out_notification_methods(),
                    format!("initialize 失败: {error}"),
                )
                .await;
                return Err(error);
            }
        };

        let info = match parse_initialize_info(&init_result) {
            Ok(info) => info,
            Err(error) => {
                self.abort_startup(
                    &running,
                    APP_SERVER_EXPERIMENTAL_API_ENABLED,
                    &default_opt_out_notification_methods(),
                    format!("initialize 响应解析失败: {error}"),
                )
                .await;
                return Err(error);
            }
        };
        if let Err(error) = running.notify_initialized().await {
            self.abort_startup(
                &running,
                APP_SERVER_EXPERIMENTAL_API_ENABLED,
                &default_opt_out_notification_methods(),
                format!("initialized 发送失败: {error}"),
            )
            .await;
            return Err(error);
        }
        let _ = self.inbound_tx.send(AppServerInbound::Initialized {
            runtime_id: self.launch_config.runtime_id.clone(),
            info,
            experimental_api_enabled: APP_SERVER_EXPERIMENTAL_API_ENABLED,
            opt_out_notification_methods: default_opt_out_notification_methods(),
        });
        Ok(running)
    }

    async fn abort_startup(
        &self,
        running: &Arc<RunningAppServer>,
        experimental_api_enabled: bool,
        opt_out_notification_methods: &[String],
        message: String,
    ) {
        let _ = self.inbound_tx.send(AppServerInbound::HandshakeFailed {
            runtime_id: self.launch_config.runtime_id.clone(),
            message,
            experimental_api_enabled,
            opt_out_notification_methods: opt_out_notification_methods.to_vec(),
        });
        let _ = running.abort().await;
        let mut guard = self.inner.lock().await;
        if guard
            .as_ref()
            .map(|existing| Arc::ptr_eq(existing, running))
            .unwrap_or(false)
        {
            *guard = None;
        }
    }
}