use std::io;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use sysknife_daemon::dispatcher::connection_handler_with_executor;
use sysknife_daemon::executor::{ActionExecutor, ExecutionOutput, ExecutorError};
use sysknife_daemon::state::{DaemonConfig, DaemonState};
use sysknife_daemon::state_collector::CommandRunner;
use sysknife_daemon::transport::{framing::FramedStream, listen::ListenTarget};
use sysknife_types::CallerRole;
use tempfile::tempdir;
use tokio::net::UnixStream;
struct MockRunner;
impl CommandRunner for MockRunner {
fn run(&self, _program: &str, _args: &[&str]) -> Result<String, io::Error> {
Ok(String::new())
}
}
struct InstantSuccessExecutor;
#[async_trait]
impl ActionExecutor for InstantSuccessExecutor {
async fn execute(
&self,
_spec: &sysknife_daemon::actions::ActionSpec,
) -> Result<ExecutionOutput, ExecutorError> {
Ok(ExecutionOutput {
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
})
}
}
fn test_state(dir: &tempfile::TempDir) -> DaemonState {
let db_path = dir.path().join("test.db");
let sock_path = dir.path().join("test.sock");
let config = DaemonConfig::new(ListenTarget::Unix(sock_path), db_path);
DaemonState::open(config).unwrap()
}
fn spawn_handler(
state: DaemonState,
role: CallerRole,
) -> (FramedStream<UnixStream>, tokio::task::JoinHandle<()>) {
let (client, server) = UnixStream::pair().unwrap();
let runner: Arc<dyn CommandRunner + Send + Sync> = Arc::new(MockRunner);
let executor: Arc<dyn ActionExecutor> = Arc::new(InstantSuccessExecutor);
let handle = tokio::spawn(async move {
connection_handler_with_executor(server, state, runner, executor, uid_caller(role)).await;
});
(FramedStream::new(client), handle)
}
#[tokio::test(start_paused = true)]
async fn a_connection_that_never_sends_a_request_is_disconnected() {
let dir = tempdir().unwrap();
let state = test_state(&dir);
let (_client, handle) = spawn_handler(state, CallerRole::Observer);
let start = tokio::time::Instant::now();
handle
.await
.expect("handler must return once the pre-request deadline fires");
let elapsed = start.elapsed();
assert!(
elapsed <= std::time::Duration::from_secs(60),
"a silent connection squatted a connection permit for {elapsed:?}; the \
pre-request deadline must cut it loose in well under a minute"
);
}
#[tokio::test]
async fn a_served_connection_still_accepts_a_second_request() {
let dir = tempdir().unwrap();
let state = test_state(&dir);
let (mut client, _handle) = spawn_handler(state, CallerRole::Observer);
for request_id in ["first", "second"] {
let req = json!({
"type": "preview",
"request_id": request_id,
"action_name": "GetMemoryInfo",
"params": {},
});
client
.send(&serde_json::to_vec(&req).unwrap())
.await
.unwrap();
let raw = client.recv().await.unwrap();
let resp: Value = serde_json::from_slice(&raw).unwrap();
assert_eq!(
resp["type"], "preview_response",
"request {request_id} on a reused connection must still be served: {resp}"
);
}
}
fn uid_caller(role: sysknife_types::CallerRole) -> sysknife_daemon::auth::CallerAttribution {
sysknife_daemon::auth::CallerAttribution::from_peer_uid(1000, role)
}