1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
use crate::{
commands::{RecvCommand, SendCommand},
traits::VmixTcpApiClient,
};
use anyhow::Result;
use std::{
io::Write,
net::{SocketAddr, TcpStream},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, SyncSender},
},
thread::JoinHandle,
time::Duration,
};
pub struct VmixApi {
pub sender: SyncSender<SendCommand>,
pub receiver: Receiver<RecvCommand>,
shutdown_signal: Arc<AtomicBool>,
error_signal: Arc<AtomicBool>, // New: shared error state
original_stream: Arc<std::sync::Mutex<Option<TcpStream>>>, // New: keep original stream for explicit shutdown
reader_handle: Option<JoinHandle<()>>,
writer_handle: Option<JoinHandle<()>>,
}
// VmixApiはマルチスレッド環境で安全に使用できる
// 内部でMutexやAtomicBoolを使用しているため
unsafe impl Send for VmixApi {}
unsafe impl Sync for VmixApi {}
impl Drop for VmixApi {
fn drop(&mut self) {
// Signal all threads to shutdown immediately
self.shutdown_signal.store(true, Ordering::SeqCst);
self.error_signal.store(true, Ordering::SeqCst);
// Explicitly close the original stream to force both reader and writer to exit
// This will cause any blocking reads/writes to return with an error
if let Ok(mut stream_guard) = self.original_stream.lock()
&& let Some(stream) = stream_guard.take()
{
let _ = stream.shutdown(std::net::Shutdown::Both);
}
// Send a final message to wake up the writer thread if it's waiting
let _ = self.sender.try_send(SendCommand::QUIT);
// Wait for threads to complete with timeout
let shutdown_timeout = Duration::from_millis(500);
// Join reader thread with timeout
if let Some(handle) = self.reader_handle.take() {
// Give the reader thread a brief moment to notice the shutdown signal
std::thread::sleep(Duration::from_millis(10));
// Simple timeout: try to join, but don't wait forever
let start_time = std::time::Instant::now();
loop {
if handle.is_finished() {
if handle.join().is_err() {
eprintln!("Warning: Reader thread panicked during shutdown");
}
break;
}
if start_time.elapsed() > shutdown_timeout {
eprintln!("Warning: Reader thread did not shut down within timeout");
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
// Join writer thread with timeout
if let Some(handle) = self.writer_handle.take() {
let start_time = std::time::Instant::now();
loop {
if handle.is_finished() {
if handle.join().is_err() {
eprintln!("Warning: Writer thread panicked during shutdown");
}
break;
}
if start_time.elapsed() > shutdown_timeout {
eprintln!("Warning: Writer thread did not shut down within timeout");
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
}
impl VmixApi {
pub fn new(remote: SocketAddr, timeout: Duration) -> Result<Self> {
// Connect with proper error handling
let stream = TcpStream::connect_timeout(&remote, timeout)
.map_err(|e| anyhow::anyhow!("Failed to connect to {}: {}", remote, e))?;
stream
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow::anyhow!("Failed to set read timeout: {}", e))?;
stream
.set_write_timeout(Some(timeout))
.map_err(|e| anyhow::anyhow!("Failed to set write timeout: {}", e))?;
// Create stream clones for reader and writer
let mut reader_stream = stream
.try_clone()
.map_err(|e| anyhow::anyhow!("Failed to clone stream for reader: {}", e))?;
// Set reader stream to non-blocking mode for better shutdown handling
reader_stream
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("Failed to set non-blocking mode: {}", e))?;
let writer_stream = stream
.try_clone()
.map_err(|e| anyhow::anyhow!("Failed to clone stream for writer: {}", e))?;
// Shared signals for coordination
let shutdown_signal = Arc::new(AtomicBool::new(false));
let error_signal = Arc::new(AtomicBool::new(false));
let original_stream = Arc::new(std::sync::Mutex::new(Some(stream)));
// Reader thread setup
let (reader_sender, reader_receiver): (SyncSender<RecvCommand>, Receiver<RecvCommand>) =
std::sync::mpsc::sync_channel(1);
let reader_shutdown = shutdown_signal.clone();
let reader_error = error_signal.clone();
let reader_handle = std::thread::spawn(move || {
loop {
// Check shutdown or error signals at the beginning of each iteration
if reader_shutdown.load(Ordering::Relaxed) || reader_error.load(Ordering::Relaxed) {
break;
}
// Try to read from stream with timeout handling
// Note: This may still block, but the stream will be closed from Drop impl
match (&mut reader_stream).try_into() {
Ok(command) => {
if reader_sender.send(command).is_err() {
// Receiver dropped, exit thread
break;
}
}
Err(err) => {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
match io_err.kind() {
std::io::ErrorKind::WouldBlock => {
// Non-blocking operation would block, sleep briefly and retry
std::thread::sleep(Duration::from_millis(10));
continue;
}
std::io::ErrorKind::TimedOut => {
// Timeout is expected, continue loop
continue;
}
std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::UnexpectedEof => {
// Connection error - signal error to writer thread
eprintln!("Connection closed by remote: {}", err);
reader_error.store(true, Ordering::Relaxed);
break;
}
_ => {
// Other IO errors - signal error to writer thread
eprintln!("IO error in reader thread: {}", err);
reader_error.store(true, Ordering::Relaxed);
break;
}
}
} else {
eprintln!("Failed to parse incoming packet: {}", err);
continue;
}
}
}
}
});
// Writer thread setup
let (writer_sender, writer_receiver): (SyncSender<SendCommand>, Receiver<SendCommand>) =
std::sync::mpsc::sync_channel(1);
let writer_shutdown = shutdown_signal.clone();
let writer_error = error_signal.clone();
let writer_handle = std::thread::spawn(move || {
let mut writer = writer_stream;
loop {
// Check shutdown or error signals
if writer_shutdown.load(Ordering::Relaxed) || writer_error.load(Ordering::Relaxed) {
break;
}
// Try to receive with timeout to allow checking signals
match writer_receiver.recv_timeout(Duration::from_millis(10)) {
Ok(command) => {
// Check for quit command
if matches!(command, SendCommand::QUIT) {
let bytes: Vec<u8> = command.into();
if writer.write_all(&bytes).is_err() {
writer_error.store(true, Ordering::Relaxed);
}
// Quit command processed, exit thread
break;
}
let bytes: Vec<u8> = command.into();
if writer.write_all(&bytes).is_err() {
eprintln!("Failed to write to stream");
writer_error.store(true, Ordering::Relaxed);
break;
}
if writer.flush().is_err() {
eprintln!("Failed to flush stream");
writer_error.store(true, Ordering::Relaxed);
break;
}
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
// Timeout, continue to check signals
continue;
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
// Channel disconnected, exit thread
break;
}
}
}
});
Ok(Self {
sender: writer_sender,
receiver: reader_receiver,
shutdown_signal,
error_signal,
original_stream,
reader_handle: Some(reader_handle),
writer_handle: Some(writer_handle),
})
}
/// Send a command to vMix
pub fn send_command(&self, command: SendCommand) -> Result<()> {
self.sender
.send(command)
.map_err(|e| anyhow::anyhow!("Failed to send command: {}", e))
}
/// Try to receive a command from vMix with timeout
pub fn try_receive_command(&self, timeout: Duration) -> Result<RecvCommand> {
self.receiver
.recv_timeout(timeout)
.map_err(|e| anyhow::anyhow!("Failed to receive command: {}", e))
}
/// Gracefully disconnect from vMix
pub fn disconnect(&self) -> Result<()> {
self.send_command(SendCommand::QUIT)
}
/// Check if the connection is still alive
pub fn is_connected(&self) -> bool {
// First check atomic flags for immediate shutdown/error detection
if self.shutdown_signal.load(Ordering::Relaxed) || self.error_signal.load(Ordering::Relaxed)
{
return false;
}
// Perform lightweight socket health check
if let Ok(stream_guard) = self.original_stream.try_lock() {
if let Some(stream) = stream_guard.as_ref() {
// Try to peek at the socket to check if it's still connected
// This is non-intrusive and won't interfere with reader/writer threads
let mut buf = [0u8; 1];
match stream.peek(&mut buf) {
Ok(_) => {
// Successfully peeked, connection appears healthy
true
}
Err(e) => match e.kind() {
std::io::ErrorKind::WouldBlock => {
// No data available to peek, but connection is fine
true
}
std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::NotConnected => {
// Connection is definitely closed
false
}
_ => {
// Other errors - assume still connected to avoid false negatives
true
}
},
}
} else {
// Stream has been taken (during shutdown)
false
}
} else {
// Couldn't acquire lock immediately, assume connected to avoid blocking
// This preserves the non-blocking nature of this method
true
}
}
}
// VmixApiはTCP専用なので、HTTP専用のVmixApiClientトレイトは実装しない
impl VmixTcpApiClient for VmixApi {
fn send_command(&self, command: crate::commands::SendCommand) -> Result<()> {
self.send_command(command)
}
fn try_receive_command(&self, timeout: Duration) -> Result<RecvCommand> {
self.try_receive_command(timeout)
}
fn is_connected(&self) -> bool {
self.is_connected()
}
fn get_sender(&self) -> &SyncSender<SendCommand> {
&self.sender
}
fn disconnect(&self) -> Result<()> {
self.disconnect()
}
}