feat: add Unix socket control interface with command parsing
Made-with: Cursor
This commit is contained in:
128
src/control.rs
Normal file
128
src/control.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::events::{BridgeStatus, ControlCommand};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ParsedCommand {
|
||||
IrcConnect,
|
||||
IrcDisconnect,
|
||||
IrcReconnect,
|
||||
OwncastConnect,
|
||||
OwncastDisconnect,
|
||||
OwncastReconnect,
|
||||
Status,
|
||||
Quit,
|
||||
}
|
||||
|
||||
fn parse_command(input: &str) -> Option<ParsedCommand> {
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"irc connect" => Some(ParsedCommand::IrcConnect),
|
||||
"irc disconnect" => Some(ParsedCommand::IrcDisconnect),
|
||||
"irc reconnect" => Some(ParsedCommand::IrcReconnect),
|
||||
"owncast connect" => Some(ParsedCommand::OwncastConnect),
|
||||
"owncast disconnect" => Some(ParsedCommand::OwncastDisconnect),
|
||||
"owncast reconnect" => Some(ParsedCommand::OwncastReconnect),
|
||||
"status" => Some(ParsedCommand::Status),
|
||||
"quit" => Some(ParsedCommand::Quit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_control_socket(
|
||||
socket_path: &str,
|
||||
control_tx: mpsc::Sender<ControlCommand>,
|
||||
) -> anyhow::Result<()> {
|
||||
let _ = std::fs::remove_file(socket_path);
|
||||
let listener = UnixListener::bind(socket_path)?;
|
||||
info!(path = %socket_path, "Control socket listening");
|
||||
|
||||
loop {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let control_tx = control_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut line = String::new();
|
||||
|
||||
if reader.read_line(&mut line).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
match parse_command(&line) {
|
||||
Some(ParsedCommand::Status) => {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
let cmd = ControlCommand::Status { reply: reply_tx };
|
||||
if control_tx.send(cmd).await.is_ok() {
|
||||
if let Ok(status) = reply_rx.await {
|
||||
let json = serde_json::to_string_pretty(&status).unwrap_or_default();
|
||||
let _ = writer.write_all(json.as_bytes()).await;
|
||||
let _ = writer.write_all(b"\n").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(parsed) => {
|
||||
let cmd = match parsed {
|
||||
ParsedCommand::IrcConnect => ControlCommand::IrcConnect,
|
||||
ParsedCommand::IrcDisconnect => ControlCommand::IrcDisconnect,
|
||||
ParsedCommand::IrcReconnect => ControlCommand::IrcReconnect,
|
||||
ParsedCommand::OwncastConnect => ControlCommand::OwncastConnect,
|
||||
ParsedCommand::OwncastDisconnect => ControlCommand::OwncastDisconnect,
|
||||
ParsedCommand::OwncastReconnect => ControlCommand::OwncastReconnect,
|
||||
ParsedCommand::Quit => ControlCommand::Quit,
|
||||
ParsedCommand::Status => unreachable!(),
|
||||
};
|
||||
if control_tx.send(cmd).await.is_ok() {
|
||||
let _ = writer.write_all(b"OK\n").await;
|
||||
} else {
|
||||
let _ = writer.write_all(b"ERROR: channel closed\n").await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let _ = writer.write_all(b"ERROR: unknown command\n").await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_irc_commands() {
|
||||
assert!(matches!(parse_command("irc connect"), Some(ParsedCommand::IrcConnect)));
|
||||
assert!(matches!(parse_command("irc disconnect"), Some(ParsedCommand::IrcDisconnect)));
|
||||
assert!(matches!(parse_command("irc reconnect"), Some(ParsedCommand::IrcReconnect)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_owncast_commands() {
|
||||
assert!(matches!(parse_command("owncast connect"), Some(ParsedCommand::OwncastConnect)));
|
||||
assert!(matches!(parse_command("owncast disconnect"), Some(ParsedCommand::OwncastDisconnect)));
|
||||
assert!(matches!(parse_command("owncast reconnect"), Some(ParsedCommand::OwncastReconnect)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_status_and_quit() {
|
||||
assert!(matches!(parse_command("status"), Some(ParsedCommand::Status)));
|
||||
assert!(matches!(parse_command("quit"), Some(ParsedCommand::Quit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_unknown() {
|
||||
assert!(parse_command("unknown command").is_none());
|
||||
assert!(parse_command("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_case_insensitive_trimmed() {
|
||||
assert!(matches!(parse_command(" IRC CONNECT "), Some(ParsedCommand::IrcConnect)));
|
||||
assert!(matches!(parse_command("QUIT\n"), Some(ParsedCommand::Quit)));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod config;
|
||||
mod control;
|
||||
mod events;
|
||||
mod health;
|
||||
mod html;
|
||||
|
||||
Reference in New Issue
Block a user