app-server/src/skills_watcher.rs
154 lines
use std::sync::Arc;use std::sync::Mutex;use std::time::Duration;use crate::outgoing_message::OutgoingMessageSender;use codex_app_server_protocol::ServerNotification;use codex_app_server_protocol::SkillsChangedNotification;use codex_core::ThreadManager;use codex_core::config::Config;use codex_core::skills::SkillsLoadInput;use codex_core::skills::SkillsManager;use codex_file_watcher::FileWatcher;use codex_file_watcher::FileWatcherSubscriber;use codex_file_watcher::Receiver;use codex_file_watcher::ThrottledWatchReceiver;use codex_file_watcher::WatchPath;use codex_file_watcher::WatchRegistration;use codex_protocol::protocol::TurnEnvironmentSelection;use codex_utils_absolute_path::AbsolutePathBuf;use tokio_util::sync::CancellationToken;use tokio_util::sync::DropGuard;use tracing::warn;#[cfg(not(test))]const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_secs(10);#[cfg(test)]const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_millis(50);pub(crate) struct SkillsWatcher { subscriber: FileWatcherSubscriber, runtime_extra_roots_registration: Mutex<WatchRegistration>, shutdown_token: CancellationToken, _shutdown_drop_guard: DropGuard,}impl SkillsWatcher { pub(crate) fn new( skills_manager: Arc<SkillsManager>, outgoing: Arc<OutgoingMessageSender>, ) -> Arc<Self> { let file_watcher = match FileWatcher::new() { Ok(file_watcher) => Arc::new(file_watcher), Err(err) => { warn!("failed to initialize skills file watcher: {err}"); Arc::new(FileWatcher::noop()) } }; let (subscriber, rx) = file_watcher.add_subscriber(); let shutdown_token = CancellationToken::new(); let shutdown_drop_guard = shutdown_token.clone().drop_guard(); Self::spawn_event_loop(rx, skills_manager, outgoing, shutdown_token.child_token()); Arc::new(Self { subscriber, runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()), shutdown_token, _shutdown_drop_guard: shutdown_drop_guard, }) } pub(crate) fn shutdown(&self) { self.shutdown_token.cancel(); } pub(crate) fn register_runtime_extra_roots(&self, extra_roots: &[AbsolutePathBuf]) { let roots = extra_roots .iter() .map(|root| WatchPath { path: root.clone().into_path_buf(), recursive: true, }) .collect(); let registration = self.subscriber.register_paths(roots); let mut guard = self .runtime_extra_roots_registration .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *guard = registration; } pub(crate) async fn register_thread_config( &self, config: &Config, thread_manager: &ThreadManager, environments: &[TurnEnvironmentSelection], ) -> WatchRegistration { let Some(environment_selection) = environments.first() else { return WatchRegistration::default(); }; let Some(environment) = thread_manager .environment_manager() .get_environment(&environment_selection.environment_id) else { warn!( "failed to register skills watcher for unknown environment `{}`", environment_selection.environment_id ); return WatchRegistration::default(); }; if environment.is_remote() { return WatchRegistration::default(); } let plugins_input = config.plugins_config_input(); let plugins_manager = thread_manager.plugins_manager(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let skills_input = SkillsLoadInput::new( config.cwd.clone(), plugin_outcome.effective_plugin_skill_roots(), config.config_layer_stack.clone(), config.bundled_skills_enabled(), ); let roots = thread_manager .skills_manager() .skill_roots_for_config(&skills_input, Some(environment.get_filesystem())) .await .into_iter() .map(|root| WatchPath { path: root.path.into_path_buf(), recursive: true, }) .collect(); self.subscriber.register_paths(roots) } fn spawn_event_loop( rx: Receiver, skills_manager: Arc<SkillsManager>, outgoing: Arc<OutgoingMessageSender>, shutdown_token: CancellationToken, ) { let mut rx = ThrottledWatchReceiver::new(rx, WATCHER_THROTTLE_INTERVAL); let Ok(handle) = tokio::runtime::Handle::try_current() else { warn!("skills watcher listener skipped: no Tokio runtime available"); return; }; handle.spawn(async move { loop { let event = tokio::select! { _ = shutdown_token.cancelled() => break, event = rx.recv() => event, }; if event.is_none() { break; } skills_manager.clear_cache(); outgoing .send_server_notification(ServerNotification::SkillsChanged( SkillsChangedNotification {}, )) .await; } }); }}