use std::sync::Arc;
use parking_lot::RwLock;
use tokio::sync::broadcast;
use crate::audit::helper::AuditHelper;
use crate::audit::AuditEventSender;
use crate::auto_approve::defer::DeferRegistry;
use crate::command_sender::CommandSender;
use crate::config::Settings;
use crate::hooks::registry::{HookRegistry, SessionPaneMap};
use crate::ipc::server::IpcServer;
use crate::pty::PtyRegistry;
use crate::runtime::RuntimeAdapter;
use crate::state::SharedState;
use crate::transcript::TranscriptRegistry;
use super::events::CoreEvent;
const EVENT_CHANNEL_CAPACITY: usize = 256;
pub struct TmaiCore {
state: SharedState,
command_sender: Option<Arc<CommandSender>>,
settings: RwLock<Arc<Settings>>,
ipc_server: Option<Arc<IpcServer>>,
event_tx: broadcast::Sender<CoreEvent>,
audit_helper: AuditHelper,
hook_registry: HookRegistry,
session_pane_map: SessionPaneMap,
hook_token: Option<String>,
pty_registry: Arc<PtyRegistry>,
runtime: Option<Arc<dyn RuntimeAdapter>>,
transcript_registry: Option<TranscriptRegistry>,
defer_registry: Arc<DeferRegistry>,
}
impl TmaiCore {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
state: SharedState,
command_sender: Option<Arc<CommandSender>>,
settings: Arc<Settings>,
ipc_server: Option<Arc<IpcServer>>,
audit_tx: Option<AuditEventSender>,
hook_registry: HookRegistry,
session_pane_map: SessionPaneMap,
hook_token: Option<String>,
pty_registry: Arc<PtyRegistry>,
runtime: Option<Arc<dyn RuntimeAdapter>>,
transcript_registry: Option<TranscriptRegistry>,
defer_registry: Arc<DeferRegistry>,
) -> Self {
let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let audit_helper = AuditHelper::new(audit_tx, state.clone());
Self {
state,
command_sender,
settings: RwLock::new(settings),
ipc_server,
event_tx,
audit_helper,
hook_registry,
session_pane_map,
hook_token,
pty_registry,
runtime,
transcript_registry,
defer_registry,
}
}
#[deprecated(note = "Use TmaiCore query/action methods instead of direct state access")]
pub fn raw_state(&self) -> &SharedState {
&self.state
}
#[deprecated(note = "Use TmaiCore action methods instead of direct CommandSender access")]
pub fn raw_command_sender(&self) -> Option<&Arc<CommandSender>> {
self.command_sender.as_ref()
}
pub fn settings(&self) -> Arc<Settings> {
self.settings.read().clone()
}
pub fn reload_settings(&self) -> bool {
match Settings::load(None) {
Ok(new_settings) => {
*self.settings.write() = Arc::new(new_settings);
tracing::debug!("Settings reloaded from config.toml");
true
}
Err(e) => {
tracing::warn!(%e, "Failed to reload settings from config.toml");
false
}
}
}
pub fn ipc_server(&self) -> Option<&Arc<IpcServer>> {
self.ipc_server.as_ref()
}
pub fn event_sender(&self) -> broadcast::Sender<CoreEvent> {
self.event_tx.clone()
}
pub(crate) fn state(&self) -> &SharedState {
&self.state
}
pub(crate) fn command_sender_ref(&self) -> Option<&Arc<CommandSender>> {
self.command_sender.as_ref()
}
pub fn audit_helper(&self) -> &AuditHelper {
&self.audit_helper
}
pub fn hook_registry(&self) -> &HookRegistry {
&self.hook_registry
}
pub fn session_pane_map(&self) -> &SessionPaneMap {
&self.session_pane_map
}
pub fn hook_token(&self) -> Option<&str> {
self.hook_token.as_deref()
}
pub fn pty_registry(&self) -> &Arc<PtyRegistry> {
&self.pty_registry
}
pub fn runtime(&self) -> Option<&Arc<dyn RuntimeAdapter>> {
self.runtime.as_ref()
}
pub fn transcript_registry(&self) -> Option<&TranscriptRegistry> {
self.transcript_registry.as_ref()
}
pub fn defer_registry(&self) -> &Arc<DeferRegistry> {
&self.defer_registry
}
#[cfg(test)]
pub(crate) fn settings_mut(&self) -> parking_lot::RwLockWriteGuard<'_, Arc<Settings>> {
self.settings.write()
}
pub fn validate_hook_token(&self, token: &str) -> bool {
match &self.hook_token {
Some(expected) => {
let expected_bytes = expected.as_bytes();
let token_bytes = token.as_bytes();
let mut result: usize = expected_bytes.len() ^ token_bytes.len();
for i in 0..expected_bytes.len() {
let token_byte = if i < token_bytes.len() {
token_bytes[i]
} else {
0xFF
};
result |= (expected_bytes[i] ^ token_byte) as usize;
}
result == 0
}
None => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::AppState;
#[test]
fn test_tmai_core_creation() {
let state = AppState::shared();
let settings = Arc::new(Settings::default());
let hook_registry = crate::hooks::new_hook_registry();
let session_pane_map = crate::hooks::new_session_pane_map();
let core = TmaiCore::new(
state,
None,
settings.clone(),
None,
None,
hook_registry,
session_pane_map,
None,
crate::pty::PtyRegistry::new(),
None,
None,
DeferRegistry::new(),
);
assert_eq!(core.settings().poll_interval_ms, 500);
assert!(core.ipc_server().is_none());
assert!(core.command_sender_ref().is_none());
}
#[test]
#[allow(deprecated)]
fn test_escape_hatches() {
let state = AppState::shared();
let settings = Arc::new(Settings::default());
let hook_registry = crate::hooks::new_hook_registry();
let session_pane_map = crate::hooks::new_session_pane_map();
let core = TmaiCore::new(
state.clone(),
None,
settings,
None,
None,
hook_registry,
session_pane_map,
None,
crate::pty::PtyRegistry::new(),
None,
None,
DeferRegistry::new(),
);
let raw = core.raw_state();
assert!(Arc::ptr_eq(raw, &state));
assert!(core.raw_command_sender().is_none());
}
#[test]
fn test_hook_token_validation() {
let state = AppState::shared();
let settings = Arc::new(Settings::default());
let hook_registry = crate::hooks::new_hook_registry();
let session_pane_map = crate::hooks::new_session_pane_map();
let core = TmaiCore::new(
state,
None,
settings,
None,
None,
hook_registry,
session_pane_map,
Some("test-token-123".to_string()),
crate::pty::PtyRegistry::new(),
None,
None,
DeferRegistry::new(),
);
assert!(core.validate_hook_token("test-token-123"));
assert!(!core.validate_hook_token("wrong-token"));
}
#[test]
fn test_settings_returns_arc_clone() {
let mut custom = Settings::default();
custom.poll_interval_ms = 1234;
let core = crate::api::TmaiCoreBuilder::new(custom).build();
let s1 = core.settings();
let s2 = core.settings();
assert_eq!(s1.poll_interval_ms, 1234);
assert_eq!(s2.poll_interval_ms, 1234);
assert!(Arc::ptr_eq(&s1, &s2));
}
#[test]
fn test_reload_settings_with_tempdir() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "poll_interval_ms = 999\n").unwrap();
let initial = Settings::load(Some(&config_path)).unwrap();
assert_eq!(initial.poll_interval_ms, 999);
let core = crate::api::TmaiCoreBuilder::new(initial).build();
assert_eq!(core.settings().poll_interval_ms, 999);
std::fs::write(&config_path, "poll_interval_ms = 2000\n").unwrap();
{
let new_settings = Settings::load(Some(&config_path)).unwrap();
assert_eq!(new_settings.poll_interval_ms, 2000);
*core.settings_mut() = Arc::new(new_settings);
}
assert_eq!(core.settings().poll_interval_ms, 2000);
}
}