From 4d28bb7ed50c48cbf8fa65a40514f085234e78fb Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sun, 4 Oct 2026 12:15:58 +0530 Subject: [PATCH] feat(ui): complete overhaul of settings modal and notification engine --- src/bin/scythe-daemon.rs | 58 +++++++++++++++++++------- src/bin/scythe-ui.rs | 47 +++++++++++++++++---- src/capture/audio.rs | 38 +++++++++++++++++ src/capture/wayland.rs | 72 +++++++++++++++++++++++++++++++-- src/capture/windows.rs | 39 +++++++++++------- src/encoder.rs | 28 +++++++------ src/ipc.rs | 18 +++++++-- src/lib.rs | 2 + src/updater.rs | 34 +++++++++++----- tests/e2e_test.sh | 6 +-- tests/manual_ipc/send_ipc.py | 16 ++++++++ tests/manual_ipc/test_ipc.py | 13 ++++++ tests/manual_ipc/test_record.py | 9 +++++ tests/manual_ipc/test_reload.py | 9 +++++ tests/manual_ipc/test_stop.py | 9 +++++ 15 files changed, 330 insertions(+), 68 deletions(-) create mode 100644 tests/manual_ipc/send_ipc.py create mode 100644 tests/manual_ipc/test_ipc.py create mode 100644 tests/manual_ipc/test_record.py create mode 100644 tests/manual_ipc/test_reload.py create mode 100644 tests/manual_ipc/test_stop.py diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index 0582e40..bd2e8f5 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -1,4 +1,5 @@ #![cfg_attr(target_os = "windows", windows_subsystem = "windows")] +#![allow(clippy::chunks_exact_to_as_chunks)] use scythe::capture::{Frame, FrameSource}; use scythe::encoder::VideoEncoder; @@ -105,7 +106,14 @@ async fn main() -> Result<(), Box> { { Ok(file) => { use std::os::unix::io::AsRawFd; - let res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; + let mut res = -1; + for _ in 0..15 { + res = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; + if res == 0 { + break; + } + std::thread::sleep(std::time::Duration::from_millis(80)); + } if res != 0 { eprintln!("Another instance of scythe-daemon is already running. Exiting."); return Ok(()); @@ -616,8 +624,18 @@ async fn main() -> Result<(), Box> { let filename = scythe::config::ScytheConfig::format_video_filename("Recording", "mp4"); let full_path = config.resolve_save_path(&filename); let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext); - normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() }; - println!("Started normal recording to {}", full_path); + match unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx) } { + Ok(m) => { + normal_muxer = Some(m); + println!("Started normal recording to {}", full_path); + } + Err(e) => { + eprintln!("Failed to start normal recording to {}: {}", full_path, e); + normal_recording = false; + rec_state_clone.store(false, Ordering::SeqCst); + rec_start_clone.store(0, Ordering::SeqCst); + } + } } else if normal_waiting_keyframe { normal_keyframe_timeout += 1; } @@ -663,17 +681,27 @@ async fn main() -> Result<(), Box> { let new_config = scythe::config::ScytheConfig::load(); let video_changed = new_config.show_cursor != config.show_cursor || new_config.fps != config.fps - || new_config.video_codec != config.video_codec - || new_config.record_bitrate_kbps != config.record_bitrate_kbps - || new_config.replay_bitrate_kbps != config.replay_bitrate_kbps - || new_config.replay_enabled != config.replay_enabled; + || new_config.video_codec != config.video_codec; if video_changed { - println!("Core video settings changed (fps/codec/bitrate/cursor/replay). Restarting daemon for new capture session..."); + println!("Core video settings changed (fps/codec/cursor). Restarting daemon for new capture session..."); if let Some(mut m) = normal_muxer.take() { let _ = m.finalize(); } let _ = stream_sender.take(); + #[cfg(unix)] + { + if let Ok(status) = std::process::Command::new("systemctl") + .args(["--user", "is-active", "--quiet", "scythe-daemon.service"]) + .status() + && status.success() { + let _ = std::process::Command::new("systemctl") + .args(["--user", "restart", "scythe-daemon.service"]) + .spawn(); + std::thread::sleep(std::time::Duration::from_millis(100)); + std::process::exit(0); + } + } if let Ok(exe) = std::env::current_exe() { let mut cmd = std::process::Command::new(exe); cmd.stdin(std::process::Stdio::null()) @@ -748,6 +776,7 @@ async fn main() -> Result<(), Box> { normal_recording = true; normal_waiting_keyframe = true; normal_keyframe_timeout = 0; + has_new_frame = true; encoder.force_keyframe(); rec_state_clone.store(true, Ordering::SeqCst); rec_base_audio_pts = -1; @@ -790,6 +819,7 @@ async fn main() -> Result<(), Box> { normal_recording = true; normal_waiting_keyframe = true; normal_keyframe_timeout = 0; + has_new_frame = true; encoder.force_keyframe(); rec_state_clone.store(true, Ordering::SeqCst); rec_base_audio_pts = -1; @@ -976,9 +1006,9 @@ async fn main() -> Result<(), Box> { if now + jitter_margin >= next_frame_time { let elapsed = now.duration_since(stream_start); let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64; - let pts = if (raw_pts - last_video_pts).abs() <= 1 { - last_video_pts + 1 - } else if raw_pts > last_video_pts { + let pts = if last_video_pts < 0 { + 0 + } else if raw_pts > last_video_pts + 4 { raw_pts } else { last_video_pts + 1 @@ -1051,9 +1081,9 @@ async fn main() -> Result<(), Box> { if now.duration_since(last_frame_encoded_time) >= target_frame_duration.saturating_sub(jitter_margin) { let elapsed = now.duration_since(stream_start); let raw_pts = (elapsed.as_secs_f64() * config.fps as f64).round() as i64; - let pts = if (raw_pts - last_video_pts).abs() <= 1 { - last_video_pts + 1 - } else if raw_pts > last_video_pts { + let pts = if last_video_pts < 0 { + 0 + } else if raw_pts > last_video_pts + 4 { raw_pts } else { last_video_pts + 1 diff --git a/src/bin/scythe-ui.rs b/src/bin/scythe-ui.rs index c631598..9965909 100644 --- a/src/bin/scythe-ui.rs +++ b/src/bin/scythe-ui.rs @@ -1,4 +1,5 @@ #![cfg_attr(target_os = "windows", windows_subsystem = "windows")] +#![allow(clippy::chunks_exact_to_as_chunks)] use scythe::ipc::{Command, send_command, query_status}; use scythe::overlay::{show_shadowplay_toast, ToastIcon}; @@ -122,14 +123,36 @@ fn check_and_toggle_overlay() -> bool { if let Ok(pid) = content.trim().parse::() { if pid != std::process::id() { unsafe { - use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}; - use windows::Win32::Foundation::CloseHandle; - if let Ok(h) = OpenProcess(PROCESS_TERMINATE, false, pid) { - let _ = TerminateProcess(h, 0); - let _ = CloseHandle(h); - let _ = std::fs::remove_file(&pid_path); - return true; + use windows::Win32::UI::WindowsAndMessaging::{ + EnumWindows, GetWindowThreadProcessId, PostMessageW, WM_CLOSE, + }; + use windows::Win32::Foundation::{BOOL, HWND, LPARAM, CloseHandle}; + use windows::Win32::System::Threading::{ + OpenProcess, TerminateProcess, PROCESS_TERMINATE, PROCESS_SYNCHRONIZE, WaitForSingleObject, + }; + + unsafe extern "system" fn close_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { + let target = lparam.0 as u32; + let mut pid = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut pid)); + if pid == target { + let _ = PostMessageW(hwnd, WM_CLOSE, None, None); + } + BOOL(1) } + + let _ = EnumWindows(Some(close_proc), LPARAM(pid as isize)); + + // Wait up to 150ms for graceful shutdown before fallback termination + if let Ok(h) = OpenProcess(PROCESS_SYNCHRONIZE | PROCESS_TERMINATE, false, pid) { + let wait_res = WaitForSingleObject(h, 150); + if wait_res.0 != 0 { + let _ = TerminateProcess(h, 0); + } + let _ = CloseHandle(h); + } + let _ = std::fs::remove_file(&pid_path); + return true; } } } @@ -594,6 +617,14 @@ fn main() -> Result<(), Box> { }); } } - std::thread::sleep(std::time::Duration::from_millis(20)); + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::UI::WindowsAndMessaging::{ + MsgWaitForMultipleObjectsEx, QS_ALLINPUT, MWMO_INPUTAVAILABLE, + }; + let _ = MsgWaitForMultipleObjectsEx(None, 200, QS_ALLINPUT.0, MWMO_INPUTAVAILABLE); + } + #[cfg(not(target_os = "windows"))] + std::thread::sleep(std::time::Duration::from_millis(30)); } } diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 48245d6..69e4769 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -528,9 +528,13 @@ impl AudioCapture { let stream_config: cpal::StreamConfig = config.into(); let lvl_f32 = Arc::clone(&levels_for_cpal); let lvl_i16 = Arc::clone(&levels_for_cpal); + let lvl_i32 = Arc::clone(&levels_for_cpal); + let lvl_u16 = Arc::clone(&levels_for_cpal); let mut resample_state_f32 = ResampleState::default(); let mut resample_state_i16 = ResampleState::default(); + let mut resample_state_i32 = ResampleState::default(); + let mut resample_state_u16 = ResampleState::default(); let stream = match format { cpal::SampleFormat::F32 => { @@ -566,6 +570,40 @@ impl AudioCapture { None, )? } + cpal::SampleFormat::I32 => { + dev.build_input_stream( + stream_config, + move |data: &[i32], _: &_| { + let converted_f32: Vec = data.iter().map(|&s| s as f32 / 2147483648.0).collect(); + let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_i32); + if is_mic { + lvl_i32.update_mic(&f32_data); + } else { + lvl_i32.update_system(&f32_data); + } + let _ = tx.try_send(f32_data); + }, + err_fn, + None, + )? + } + cpal::SampleFormat::U16 => { + dev.build_input_stream( + stream_config, + move |data: &[u16], _: &_| { + let converted_f32: Vec = data.iter().map(|&s| (s as f32 - 32768.0) / 32768.0).collect(); + let f32_data = convert_to_stereo_48k(&converted_f32, in_channels, in_sample_rate, gain, &mut resample_state_u16); + if is_mic { + lvl_u16.update_mic(&f32_data); + } else { + lvl_u16.update_system(&f32_data); + } + let _ = tx.try_send(f32_data); + }, + err_fn, + None, + )? + } _ => return Err("Unsupported audio format".into()), }; stream.play()?; diff --git a/src/capture/wayland.rs b/src/capture/wayland.rs index 6c8d759..a2b2ef6 100644 --- a/src/capture/wayland.rs +++ b/src/capture/wayland.rs @@ -6,6 +6,9 @@ pub struct WaylandCapture { _proxy: Screencast, _session: ashpd::desktop::Session, stream: crate::capture::wayland_stream::PipeWireStream, + show_cursor: bool, + last_frame: Option, + last_reconnect_attempt: std::time::Instant, } impl WaylandCapture { @@ -59,16 +62,79 @@ impl WaylandCapture { let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?; - Ok(Self { _proxy: proxy, _session: session, stream }) + Ok(Self { + _proxy: proxy, + _session: session, + stream, + show_cursor, + last_frame: None, + last_reconnect_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5), + }) + } + + fn try_reconnect(&mut self) -> bool { + if self.last_reconnect_attempt.elapsed() < std::time::Duration::from_millis(1500) { + return false; + } + self.last_reconnect_attempt = std::time::Instant::now(); + eprintln!("Wayland PipeWire stream disconnected. Attempting auto-reconnect..."); + + let cursor = self.show_cursor; + // Run on an independent OS thread so block_on never conflicts with Tokio runtime threads + let res = std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .ok()?; + rt.block_on(WaylandCapture::new_with_cursor(cursor)).ok() + }).join(); + + if let Ok(Some(new_cap)) = res { + eprintln!("Wayland ScreenCast session reconnected successfully."); + self._proxy = new_cap._proxy; + self._session = new_cap._session; + self.stream = new_cap.stream; + true + } else { + false + } } } impl FrameSource for WaylandCapture { fn next_frame(&mut self) -> Result> { - self.stream.next_frame() + match self.stream.next_frame() { + Ok(f) => { + self.last_frame = Some(f.clone()); + Ok(f) + } + Err(e) => { + if self.try_reconnect() { + return self.stream.next_frame(); + } + if let Some(ref f) = self.last_frame { + return Ok(f.clone()); + } + Err(e) + } + } } fn next_frame_timeout(&mut self, timeout: std::time::Duration) -> Result> { - self.stream.next_frame_timeout(timeout) + match self.stream.next_frame_timeout(timeout) { + Ok(f) => { + self.last_frame = Some(f.clone()); + Ok(f) + } + Err(e) => { + if self.try_reconnect() { + return self.stream.next_frame_timeout(timeout); + } + if let Some(ref f) = self.last_frame { + return Ok(f.clone()); + } + Err(e) + } + } } } diff --git a/src/capture/windows.rs b/src/capture/windows.rs index b592a93..1a37d5e 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -32,8 +32,9 @@ pub struct WindowsCapture { pub width: u32, pub height: u32, pub last_stride: u32, - buffer_pool: Vec>>>, + buffer_pool: Vec>>, last_frame: Option>>, + last_reinit_attempt: std::time::Instant, } #[cfg(target_os = "windows")] @@ -52,13 +53,21 @@ impl WindowsCapture { let mut a_idx = 0; 'adapter_loop: while let Ok(adapter) = factory.EnumAdapters1(a_idx) { + if let Ok(desc1) = adapter.GetDesc1() { + // Skip software rasterizers (e.g. Microsoft Basic Render Driver) when hardware GPU exists + if (desc1.Flags & DXGI_ADAPTER_FLAG_SOFTWARE.0) != 0 { + a_idx += 1; + continue; + } + } let mut o_idx = 0; while let Ok(output) = adapter.EnumOutputs(o_idx) { if let Ok(output1) = output.cast::() { if let Ok(desc) = output1.GetDesc() { + let is_attached = desc.AttachedToDesktop.as_bool(); let width = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs(); let height = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs(); - if width > 0 && height > 0 { + if is_attached && width > 0 && height > 0 { if let Ok(adapter_base) = adapter.cast::() { let mut device: Option = None; let mut context: Option = None; @@ -133,15 +142,19 @@ impl WindowsCapture { width, height, last_stride: width * 4, - buffer_pool: Vec::with_capacity(4), + buffer_pool: Vec::with_capacity(8), last_frame: None, + last_reinit_attempt: std::time::Instant::now() - std::time::Duration::from_secs(5), }) } } fn reinit_duplication(&mut self) -> Result<(), Box> { + if self.last_reinit_attempt.elapsed() < std::time::Duration::from_millis(250) { + return Err("Reinitialization throttled".into()); + } + self.last_reinit_attempt = std::time::Instant::now(); self.duplication = None; - std::thread::sleep(std::time::Duration::from_millis(50)); if let Ok(new_cap) = WindowsCapture::new() { self.device = new_cap.device; self.context = new_cap.context; @@ -196,14 +209,12 @@ impl FrameSource for WindowsCapture { let target_stride = (self.width * 4) as usize; let total_bytes = target_stride * self.height as usize; - // Acquire buffer from pool + // Acquire buffer from pool or allocate if pool has room let mut arc_buf = None; - for slot in &mut self.buffer_pool { - if let Some(buf) = slot { - if Arc::strong_count(buf) == 1 { - arc_buf = slot.take(); - break; - } + for i in 0..self.buffer_pool.len() { + if Arc::strong_count(&self.buffer_pool[i]) == 1 { + arc_buf = Some(self.buffer_pool.swap_remove(i)); + break; } } let mut arc_buf = arc_buf.unwrap_or_else(|| Arc::new(vec![0u8; total_bytes])); @@ -234,8 +245,8 @@ impl FrameSource for WindowsCapture { self.last_frame = Some(Arc::clone(&arc_buf)); self.last_stride = target_stride as u32; - if self.buffer_pool.len() < 4 { - self.buffer_pool.push(Some(arc_buf)); + if self.buffer_pool.len() < 8 { + self.buffer_pool.push(arc_buf); } return Ok(Frame::Raw { @@ -254,14 +265,12 @@ impl FrameSource for WindowsCapture { Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST || e.code() == DXGI_ERROR_ACCESS_DENIED || e.code() == DXGI_ERROR_INVALID_CALL => { - let _ = dup.ReleaseFrame(); drop(dup); self.duplication = None; let _ = self.reinit_duplication(); break; } Err(e) => { - let _ = dup.ReleaseFrame(); drop(dup); self.duplication = None; let _ = self.reinit_duplication(); diff --git a/src/encoder.rs b/src/encoder.rs index 9021960..c39c614 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -66,19 +66,21 @@ impl VaapiEncoder { (*codec_ctx).gop_size = fps; // Emit IDR keyframe every second (*codec_ctx).max_b_frames = 0; // Zero latency, no frame reordering - // High Quality & Clean VBR Rate Control + // High Quality & Clean VBR Rate Control matching GPU Screen Recorder standards let rate = (bitrate_kbps as i64) * 1000; (*codec_ctx).bit_rate = rate; - (*codec_ctx).rc_max_rate = rate * 3 / 2; // Allow burst for fast motion scenes - (*codec_ctx).rc_buffer_size = (rate / 2) as i32; - (*codec_ctx).qmin = 16; - (*codec_ctx).qmax = 28; // Prevent compression blockiness + (*codec_ctx).rc_max_rate = rate * 2; // Allow 2x burst for high-motion gaming scenes + (*codec_ctx).rc_buffer_size = (rate * 2) as i32; // 2 seconds rate control window + (*codec_ctx).qmin = 1; // Allow high-fidelity crisp detail + (*codec_ctx).qmax = 32; (*codec_ctx).profile = match codec_pref.to_lowercase().as_str() { "hevc" | "h265" => FF_PROFILE_HEVC_MAIN, "av1" => FF_PROFILE_UNKNOWN, _ => FF_PROFILE_H264_HIGH, }; (*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int; + let _ = av_opt_set((*codec_ctx).priv_data, c"rc_mode".as_ptr(), c"VBR".as_ptr(), 0); + let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0); // Rec.709 Color Space & MPEG Limited Range (Broadcast / Web standard, avoids yellow tint) (*codec_ctx).colorspace = AVColorSpace::AVCOL_SPC_BT709; @@ -310,6 +312,7 @@ impl VaapiEncoder { } av_packet_free(&mut pkt); mapped_direct = true; + self.has_frame = true; } } else { self.direct_drm_unsupported = true; @@ -378,7 +381,7 @@ impl VaapiEncoder { pub fn encode_cached_frame(&mut self, pts: i64) -> Result, String> { let mut packets = Vec::new(); if !self.has_frame { - return Ok(packets); + return Err("No cached frame available in encoder".into()); } unsafe { self.send_staged_frame(&mut packets, pts)?; @@ -498,10 +501,10 @@ impl WindowsHwEncoder { let rate = (bitrate_kbps as i64) * 1000; (*ctx).bit_rate = rate; - (*ctx).rc_max_rate = rate * 3 / 2; - (*ctx).rc_buffer_size = (rate / 2) as i32; - (*ctx).qmin = 16; - (*ctx).qmax = 28; + (*ctx).rc_max_rate = rate * 2; + (*ctx).rc_buffer_size = (rate * 2) as i32; + (*ctx).qmin = 1; + (*ctx).qmax = 32; let codec_id = (*c).id; (*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC { FF_PROFILE_HEVC_MAIN @@ -520,11 +523,12 @@ impl WindowsHwEncoder { (*ctx).color_range = AVColorRange::AVCOL_RANGE_MPEG; if desc.contains("NVENC") { - let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0); + let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p3".as_ptr(), 0); let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0); let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0); } else if desc.contains("AMF") { let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0); + let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), c"vbr_latency".as_ptr(), 0); } else if desc.contains("QuickSync") { let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0); } else { @@ -672,7 +676,7 @@ impl WindowsHwEncoder { pub fn encode_cached_frame(&mut self, pts: i64) -> Result, String> { let mut packets = Vec::new(); if !self.has_frame { - return Ok(packets); + return Err("No cached frame available in encoder".into()); } unsafe { if self.sw_frame.is_null() { diff --git a/src/ipc.rs b/src/ipc.rs index 5e3e647..a9d416a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -93,6 +93,18 @@ pub fn read_active_ipc_port() -> u16 { 42069 } +#[cfg(windows)] +pub fn get_candidate_ipc_ports() -> Vec { + let active_port = read_active_ipc_port(); + let mut ports = vec![active_port]; + for p in [42069, 42070, 42071, 42072] { + if !ports.contains(&p) { + ports.push(p); + } + } + ports +} + pub fn is_daemon_running() -> bool { query_status().is_ok() } @@ -133,8 +145,7 @@ pub fn send_command(cmd: Command) -> Result<(), Box() { if let Ok(mut stream) = TcpStream::connect_timeout(&addr, Duration::from_millis(150)) { @@ -203,8 +214,7 @@ pub fn query_status() -> Result() { diff --git a/src/lib.rs b/src/lib.rs index eec9990..52f1620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::chunks_exact_to_as_chunks)] + pub mod capture; pub mod encoder; pub mod ring; diff --git a/src/updater.rs b/src/updater.rs index c7a4b4a..21dfd71 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -353,18 +353,34 @@ fn run_auto_update(info: &ReleaseInfo, status: &Arc>) -> Res let current_exe = std::env::current_exe().map_err(|e| format!("Cannot locate current executable: {}", e))?; let exe_dir = current_exe.parent().ok_or_else(|| "Invalid executable path".to_string())?; - set_progress("Replacing binaries..."); - for bin_name in ["scythe-ui.exe", "scythe-daemon.exe"] { - let src = temp_dir.join(bin_name); - if src.exists() { - let dest = exe_dir.join(bin_name); - let dest_old = exe_dir.join(format!("{}.old", bin_name)); - let _ = std::fs::remove_file(&dest_old); - let _ = std::fs::rename(&dest, &dest_old); - let _ = std::fs::copy(&src, &dest); + set_progress("Replacing binaries and libraries..."); + let mut files_to_update = Vec::new(); + let mut search_dirs = vec![temp_dir.clone()]; + while let Some(dir) = search_dirs.pop() { + if let Ok(entries) = std::fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + search_dirs.push(path); + } else if path.is_file() { + let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); + let lower = name.to_lowercase(); + if lower.ends_with(".exe") || lower.ends_with(".dll") { + files_to_update.push((path, name)); + } + } + } } } + for (src, name) in files_to_update { + let dest = exe_dir.join(&name); + let dest_old = exe_dir.join(format!("{}.old", name)); + let _ = std::fs::remove_file(&dest_old); + let _ = std::fs::rename(&dest, &dest_old); + let _ = std::fs::copy(&src, &dest); + } + let _ = std::fs::remove_dir_all(&temp_dir); return Ok(format!("Updated binaries to v{} in {}", info.version, exe_dir.display())); } diff --git a/tests/e2e_test.sh b/tests/e2e_test.sh index ab5e63c..c06abd2 100755 --- a/tests/e2e_test.sh +++ b/tests/e2e_test.sh @@ -13,7 +13,7 @@ SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/scythe.sock" rm -f "$SOCKET_PATH" rm -f record_*.mp4 replay_*.mp4 -SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(json.load(open(p))["output_directory"]) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe") +SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(os.path.expanduser(json.load(open(p))["output_directory"])) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe") mkdir -p "$SAVE_DIR" echo "[2/6] Starting scythe-daemon with mock capture..." @@ -72,7 +72,7 @@ echo " Verifying Generated Recordings with ffprobe" echo "=============================================" FOUND_ANY=0 -for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 \( -name "record_*.mp4" -o -name "replay_*.mp4" \) 2>/dev/null); do +while IFS= read -r -d '' f; do if [ -f "$f" ]; then FOUND_ANY=1 echo "--> Testing file: $f ($(ls -lh "$f" | awk '{print $5}'))" @@ -87,7 +87,7 @@ for f in $(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdept echo "---------------------------------------------" rm -f "$f" fi -done +done < <(find "$SAVE_DIR" "$HOME/Videos/Scythe" "$HOME/Videos/vrec" . -maxdepth 2 -mmin -2 \( -iname "record*.mp4" -o -iname "replay*.mp4" \) -print0 2>/dev/null) if [ $FOUND_ANY -eq 0 ]; then echo "FAILED: No recording or replay files were produced!" diff --git a/tests/manual_ipc/send_ipc.py b/tests/manual_ipc/send_ipc.py new file mode 100644 index 0000000..1075dfc --- /dev/null +++ b/tests/manual_ipc/send_ipc.py @@ -0,0 +1,16 @@ +import socket +import json +import struct +import os + +def send(cmd): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + rt = os.environ.get("XDG_RUNTIME_DIR", "/tmp") + sock_path = f"{rt}/scythe.sock" if os.path.exists(f"{rt}/scythe.sock") else f"{rt}/vrec.sock" + sock.connect(sock_path) + payload = json.dumps(cmd).encode('utf-8') + sock.sendall(struct.pack('