From 42ab6374bdfca937eaa0a14ebacb12687b5ac605 Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 19 Jan 2026 10:02:05 +0900 Subject: [PATCH 1/2] fix: resolve clippy warning for redundant pattern matching --- src-tauri/Cargo.lock | 15 +++++- src-tauri/Cargo.toml | 3 +- src-tauri/src/commands.rs | 71 ++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 + src-tauri/src/network/client.rs | 92 +++++++++++++++++++++++++++++++- src-tauri/src/network/server.rs | 33 ++++++++++-- src/components/MasterControl.tsx | 40 +++++++------- src/components/OBSSourceList.tsx | 59 ++++++++++++++++---- src/components/SlaveMonitor.tsx | 16 ------ src/hooks/useNetworkStatus.ts | 33 +++++++++++- 10 files changed, 307 insertions(+), 57 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 439878e..f8eaa83 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2060,6 +2060,18 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "network-interface" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +dependencies = [ + "cc", + "libc", + "thiserror 2.0.17", + "winapi", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2335,10 +2347,11 @@ name = "obs-sync-temp" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", "chrono", "futures", "futures-util", + "network-interface", "obws", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 31a0de4..cee7b84 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,7 +31,8 @@ uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } anyhow = "1" thiserror = "1" -base64 = "0.21" +base64 = "0.22" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono"] } +network-interface = "2.0.5" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4d66ae8..1a365c1 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -438,6 +438,21 @@ pub async fn connect_to_master( // Create SlaveClient let slave_client = Arc::new(SlaveClient::new(config.host.clone(), config.port)); + // Set up connection status callback to emit Tauri events + let app_handle_for_callback = state.app_handle.clone(); + slave_client + .set_connection_status_callback(move |is_connected| { + let app_handle = app_handle_for_callback.clone(); + tokio::spawn(async move { + if let Some(handle) = app_handle.read().await.as_ref() { + if let Err(e) = handle.emit("slave-connection-status", is_connected) { + eprintln!("Failed to emit slave connection status event: {}", e); + } + } + }); + }) + .await; + // Connect to master and get sync message receiver and sender let (sync_rx, send_tx) = slave_client .connect() @@ -536,6 +551,15 @@ pub async fn disconnect_from_master(state: State<'_, AppState>) -> Result<(), St Ok(()) } +#[tauri::command] +pub async fn is_slave_connected(state: State<'_, AppState>) -> Result { + if let Some(client) = state.slave_client.read().await.as_ref() { + Ok(client.is_connected().await) + } else { + Ok(false) + } +} + #[tauri::command] pub async fn get_slave_reconnection_status( state: State<'_, AppState>, @@ -692,6 +716,53 @@ pub async fn get_performance_metrics( Ok(state.performance_monitor.get_metrics().await) } +#[tauri::command] +pub fn get_local_ip_address() -> Result { + use network_interface::{NetworkInterface, NetworkInterfaceConfig}; + + let interfaces = + NetworkInterface::show().map_err(|e| format!("Failed to get network interfaces: {}", e))?; + + // Prefer Ethernet interfaces (eth*, en* on Linux/macOS, ETHERNET on Windows) + // Fallback to any non-loopback IPv4 address + for iface in &interfaces { + let name_lower = iface.name.to_lowercase(); + + // Skip loopback interfaces (lo, loopback, etc.) + if name_lower.contains("loopback") || name_lower.starts_with("lo") { + continue; + } + + // Check for IPv4 address (addr is Vec in v2.0.5) + for addr in &iface.addr { + if let network_interface::Addr::V4(v4_addr) = addr { + // Prefer Ethernet interfaces (starts with "eth", "en", or contains "ethernet") + if name_lower.starts_with("eth") + || name_lower.starts_with("en") + || name_lower.contains("ethernet") + { + return Ok(v4_addr.ip.to_string()); + } + } + } + } + + // Fallback: return first non-loopback IPv4 address + for iface in interfaces { + let name_lower = iface.name.to_lowercase(); + + if !name_lower.contains("loopback") && !name_lower.starts_with("lo") { + for addr in &iface.addr { + if let network_interface::Addr::V4(v4_addr) = addr { + return Ok(v4_addr.ip.to_string()); + } + } + } + } + + Err("No network interface with IPv4 address found".to_string()) +} + #[tauri::command] pub fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a0ec853..c128514 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,6 +55,7 @@ pub fn run() { commands::stop_master_server, commands::connect_to_master, commands::disconnect_from_master, + commands::is_slave_connected, commands::set_sync_targets, commands::get_connected_clients_count, commands::get_connected_clients_info, @@ -69,6 +70,7 @@ pub fn run() { commands::get_log_file_path, commands::open_log_file, commands::get_performance_metrics, + commands::get_local_ip_address, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/network/client.rs b/src-tauri/src/network/client.rs index 062a8fd..d669401 100644 --- a/src-tauri/src/network/client.rs +++ b/src-tauri/src/network/client.rs @@ -19,6 +19,9 @@ pub struct ReconnectionStatus { pub last_error: Option, } +type ConnectionStatusCallback = Arc; + +#[derive(Clone)] pub struct SlaveClient { host: String, port: u16, @@ -29,6 +32,8 @@ pub struct SlaveClient { sync_message_tx: Arc>>>, reconnection_status: Arc>, current_attempt: Arc, + is_connected: Arc, + connection_status_callback: Arc>>, } impl SlaveClient { @@ -48,6 +53,33 @@ impl SlaveClient { last_error: None, })), current_attempt: Arc::new(AtomicU32::new(0)), + is_connected: Arc::new(AtomicBool::new(false)), + connection_status_callback: Arc::new(RwLock::new(None)), + } + } + + pub async fn set_connection_status_callback(&self, callback: F) + where + F: Fn(bool) + Send + Sync + 'static, + { + *self.connection_status_callback.write().await = Some(Arc::new(callback)); + } + + pub async fn is_connected(&self) -> bool { + self.is_connected.load(Ordering::SeqCst) + } + + async fn set_connected(&self, connected: bool) { + let old_value = self.is_connected.swap(connected, Ordering::SeqCst); + // Only notify if status actually changed + if old_value != connected { + let callback_opt = { + let callback = self.connection_status_callback.read().await; + callback.clone() + }; + if let Some(cb) = callback_opt.as_ref() { + cb(connected); + } } } @@ -85,6 +117,10 @@ impl SlaveClient { let message_tx_for_send = self.message_tx.clone(); let sync_message_tx_for_store = self.sync_message_tx.clone(); + // Channel to notify when first connection is established + let (first_connection_tx, mut first_connection_rx) = + mpsc::unbounded_channel::>(); + // Spawn task to handle sending messages (will be connected when WebSocket is ready) let send_tx_for_sending = send_tx.clone(); let (send_ready_tx, mut send_ready_rx) = @@ -127,8 +163,11 @@ impl SlaveClient { // Spawn connection task with auto-reconnect let reconnection_status_for_task = self.reconnection_status.clone(); let current_attempt_for_task = self.current_attempt.clone(); + let first_connection_tx_for_task = first_connection_tx.clone(); + let client_for_status = Arc::new(self.clone()); tokio::spawn(async move { let mut attempt = 0; + let mut is_first_connection = true; loop { if !should_reconnect.load(Ordering::SeqCst) { @@ -140,6 +179,7 @@ impl SlaveClient { status.last_error = None; } current_attempt_for_task.store(0, Ordering::SeqCst); + client_for_status.clone().set_connected(false).await; break; } @@ -177,6 +217,14 @@ impl SlaveClient { )); } current_attempt_for_task.store(0, Ordering::SeqCst); + client_for_status.clone().set_connected(false).await; + // Notify first connection failure + if is_first_connection { + let _ = first_connection_tx_for_task.send(Err(format!( + "Failed to connect after {} attempts", + max_attempts + ))); + } break; } @@ -185,7 +233,8 @@ impl SlaveClient { Ok((ws_stream, _)) => { println!("Connected to master: {}", url); attempt = 0; // Reset attempt counter on successful connection - // Update status: connected successfully + client_for_status.clone().set_connected(true).await; + // Update status: connected successfully { let mut status = reconnection_status_for_task.write().await; status.is_reconnecting = false; @@ -194,6 +243,12 @@ impl SlaveClient { } current_attempt_for_task.store(0, Ordering::SeqCst); + // Notify first connection success + if is_first_connection { + is_first_connection = false; + let _ = first_connection_tx_for_task.send(Ok(())); + } + let (ws_sender, mut ws_receiver) = ws_stream.split(); let tx_clone = tx.clone(); @@ -211,6 +266,7 @@ impl SlaveClient { let message_tx_for_cleanup = message_tx_for_send.clone(); let sync_message_tx_for_cleanup = sync_message_tx_for_store.clone(); let reconnection_status_for_incoming = reconnection_status_for_task.clone(); + let client_for_disconnect = client_for_status.clone(); tokio::spawn(async move { while let Some(msg) = ws_receiver.next().await { match msg { @@ -260,6 +316,7 @@ impl SlaveClient { status.attempt_count = 0; status.last_error = Some("Connection lost".to_string()); } + client_for_disconnect.set_connected(false).await; }); // Wait for connection to break @@ -280,17 +337,48 @@ impl SlaveClient { status.last_error = Some(format!("{}", e)); } current_attempt_for_task.store(attempt, Ordering::SeqCst); + client_for_status.clone().set_connected(false).await; + // Notify first connection failure + if is_first_connection && attempt >= max_attempts { + let _ = first_connection_tx_for_task.send(Err(format!("{}", e))); + } } } } }); - Ok((rx, send_tx)) + // Wait for first connection to be established (with timeout) + let timeout = tokio::time::Duration::from_secs(30); + match tokio::time::timeout(timeout, first_connection_rx.recv()).await { + Ok(Some(Ok(()))) => { + // Connection established successfully + Ok((rx, send_tx)) + } + Ok(Some(Err(e))) => { + // Connection failed + self.set_connected(false).await; + Err(anyhow::anyhow!("Failed to connect to master: {}", e)) + } + Ok(None) => { + // Channel closed unexpectedly + self.set_connected(false).await; + Err(anyhow::anyhow!("Connection attempt was cancelled")) + } + Err(_) => { + // Timeout + self.set_connected(false).await; + Err(anyhow::anyhow!( + "Connection timeout after {} seconds", + timeout.as_secs() + )) + } + } } pub async fn disconnect(&self) { // Stop reconnection attempts self.should_reconnect.store(false, Ordering::SeqCst); + self.set_connected(false).await; // Update status: not reconnecting { diff --git a/src-tauri/src/network/server.rs b/src-tauri/src/network/server.rs index 2e99258..b8ed5e9 100644 --- a/src-tauri/src/network/server.rs +++ b/src-tauri/src/network/server.rs @@ -42,6 +42,7 @@ pub struct MasterServer { shutdown: Arc, tasks: Arc>>>, initial_state_callback: Arc>>, + listener: Arc>>, } impl MasterServer { @@ -54,6 +55,7 @@ impl MasterServer { shutdown: Arc::new(AtomicBool::new(false)), tasks: Arc::new(RwLock::new(Vec::new())), initial_state_callback: Arc::new(RwLock::new(None)), + listener: Arc::new(RwLock::new(None)), } } @@ -73,6 +75,14 @@ impl MasterServer { // Signal shutdown self.shutdown.store(true, Ordering::SeqCst); + // Close TcpListener to stop accepting new connections + { + let mut listener = self.listener.write().await; + if listener.take().is_some() { + println!("TcpListener closed"); + } + } + // Abort all tasks let tasks = self.tasks.write().await; for task in tasks.iter() { @@ -97,10 +107,14 @@ impl MasterServer { .await .context(format!("Failed to bind to {}", addr))?; + // Store listener for cleanup + *self.listener.write().await = Some(listener); + println!("Master server listening on: {}", addr); let clients = self.clients.clone(); let shutdown = self.shutdown.clone(); + let listener_for_accept = self.listener.clone(); // Broadcast sync messages to all connected clients let broadcast_task = tokio::spawn(async move { @@ -150,8 +164,17 @@ impl MasterServer { break; } - match listener.accept().await { - Ok((stream, addr)) => { + // Get listener from Arc and accept connection + let accept_result = { + let listener_guard = listener_for_accept.read().await; + match listener_guard.as_ref() { + Some(l) => Some(l.accept().await), + None => None, // Listener was closed + } + }; + + match accept_result { + Some(Ok((stream, addr))) => { println!("New connection from: {}", addr); let clients = clients_for_accept.clone(); let client_info = client_info_for_accept.clone(); @@ -166,10 +189,14 @@ impl MasterServer { callback, )); } - Err(e) => { + Some(Err(e)) => { eprintln!("Failed to accept connection: {}", e); break; } + None => { + // Listener was closed + break; + } } } }); diff --git a/src/components/MasterControl.tsx b/src/components/MasterControl.tsx index 8de7f37..6b036c8 100644 --- a/src/components/MasterControl.tsx +++ b/src/components/MasterControl.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { invoke } from "@tauri-apps/api/core"; import { useNetworkStatus } from "../hooks/useNetworkStatus"; import { ConnectionState } from "../types/network"; @@ -8,8 +8,22 @@ export const MasterControl = () => { const [port, setPort] = useState(8080); const [isStarting, setIsStarting] = useState(false); const [isStopping, setIsStopping] = useState(false); + const [localIpAddress, setLocalIpAddress] = useState(null); const { status, clients, slaveStatuses, performanceMetrics, startMasterServer, stopMasterServer } = useNetworkStatus(); + useEffect(() => { + const fetchLocalIp = async () => { + try { + const ip = await invoke("get_local_ip_address"); + setLocalIpAddress(ip); + } catch (error) { + console.error("Failed to get local IP address:", error); + setLocalIpAddress(null); + } + }; + fetchLocalIp(); + }, []); + const handleStart = async () => { setIsStarting(true); try { @@ -126,10 +140,6 @@ export const MasterControl = () => {

サーバー起動中

-
- ポート: - {port} -
接続中のクライアント: @@ -138,7 +148,9 @@ export const MasterControl = () => {
接続URL: - ws://<your-ip>:{port} + + ws://{localIpAddress || ""}:{port} +
@@ -152,22 +164,6 @@ export const MasterControl = () => { {performanceMetrics.averageLatencyMs.toFixed(2)} ms -
- 総メッセージ数: - {performanceMetrics.totalMessages} -
-
- メッセージ/秒: - - {performanceMetrics.messagesPerSecond.toFixed(2)} - -
-
- 総転送バイト数: - - {(performanceMetrics.totalBytes / 1024).toFixed(2)} KB - -
)} diff --git a/src/components/OBSSourceList.tsx b/src/components/OBSSourceList.tsx index 5addfa3..34e984a 100644 --- a/src/components/OBSSourceList.tsx +++ b/src/components/OBSSourceList.tsx @@ -15,23 +15,60 @@ export const OBSSourceList = ({ sources }: OBSSourceListProps) => { return (
-

OBSソース一覧

- -
-
- ソース名 - タイプ - 種類 -
- +
{sources.map((source, index) => ( -
+
{source.sourceName} {source.sourceType} - {source.sourceKind}
))}
+ +
); }; diff --git a/src/components/SlaveMonitor.tsx b/src/components/SlaveMonitor.tsx index c794bc4..10e2131 100644 --- a/src/components/SlaveMonitor.tsx +++ b/src/components/SlaveMonitor.tsx @@ -156,22 +156,6 @@ export const SlaveMonitor = () => { {performanceMetrics.averageLatencyMs.toFixed(2)} ms
-
- 総メッセージ数: - {performanceMetrics.totalMessages} -
-
- メッセージ/秒: - - {performanceMetrics.messagesPerSecond.toFixed(2)} - -
-
- 総転送バイト数: - - {(performanceMetrics.totalBytes / 1024).toFixed(2)} KB - -
)} diff --git a/src/hooks/useNetworkStatus.ts b/src/hooks/useNetworkStatus.ts index b7c713f..e17781b 100644 --- a/src/hooks/useNetworkStatus.ts +++ b/src/hooks/useNetworkStatus.ts @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import { NetworkStatus, ConnectionState, ClientInfo, SlaveStatus, ReconnectionStatus } from "../types/network"; interface NetworkConfig { @@ -141,7 +142,7 @@ export const useNetworkStatus = () => { try { setStatus({ state: ConnectionState.Connecting }); await invoke("connect_to_master", { config }); - setStatus({ state: ConnectionState.Connected }); + // Status will be updated via Tauri event setError(null); // Start polling for reconnection status and performance metrics @@ -188,6 +189,36 @@ export const useNetworkStatus = () => { } }, []); + // Listen for slave connection status events + useEffect(() => { + let unlistenFn: (() => void) | null = null; + + const setupListener = async () => { + const unlisten = await listen("slave-connection-status", (event) => { + const isConnected = event.payload; + setStatus((prev) => { + if (isConnected) { + return { state: ConnectionState.Connected }; + } else { + if (prev.state === ConnectionState.Connected) { + setError("接続が切断されました"); + } + return { state: ConnectionState.Disconnected }; + } + }); + }); + unlistenFn = unlisten; + }; + + setupListener(); + + return () => { + if (unlistenFn) { + unlistenFn(); + } + }; + }, []); + // Cleanup polling on unmount useEffect(() => { return () => { From 6230911e37e17444a1189120ee6a9acca111c16d Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 19 Jan 2026 10:12:33 +0900 Subject: [PATCH 2/2] fix as i64 warning --- src-tauri/src/obs/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/obs/commands.rs b/src-tauri/src/obs/commands.rs index 8907903..c710317 100644 --- a/src-tauri/src/obs/commands.rs +++ b/src-tauri/src/obs/commands.rs @@ -35,7 +35,7 @@ impl OBSCommands { .await .context("Failed to create scene item")?; - Ok(item_id as i64) + Ok(item_id) } pub async fn remove_scene_item(