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 config;
|
||||||
|
mod control;
|
||||||
mod events;
|
mod events;
|
||||||
mod health;
|
mod health;
|
||||||
mod html;
|
mod html;
|
||||||
|
|||||||
Reference in New Issue
Block a user