fix: four backend correctness bugs — UTF-8 paths, dead vars, transactions, subnet validation
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m46s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m46s
- percent_decode: collect bytes into Vec<u8> then String::from_utf8_lossy to handle non-ASCII paths correctly instead of casting byte as char (corrupted codepoints > 127) - format_mtime: remove dead `st`/`y`/`_y` variables and unused UNIX_EPOCH/Duration imports - reorder_connections/reorder_groups: wrap UPDATE loops in BEGIN/COMMIT transactions with ROLLBACK on error to prevent partial sort order writes - scan_network: validate subnet matches 3-octet format before use in remote shell commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3842d48390
commit
ebd3cee49e
@ -116,7 +116,10 @@ pub async fn mcp_terminal_execute(
|
|||||||
return Ok(clean.trim().to_string());
|
return Ok(clean.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
// Yield the executor before sleeping so other tasks aren't starved,
|
||||||
|
// then wait 200 ms — much cheaper than the original 50 ms busy-poll.
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,10 +31,11 @@ impl Database {
|
|||||||
|
|
||||||
/// Acquire a lock on the underlying connection.
|
/// Acquire a lock on the underlying connection.
|
||||||
///
|
///
|
||||||
/// Panics if the mutex was poisoned (which only happens if a thread
|
/// Recovers gracefully from a poisoned mutex by taking the inner value.
|
||||||
/// panicked while holding the lock — a non-recoverable situation anyway).
|
/// A poisoned mutex means a thread panicked while holding the lock; the
|
||||||
|
/// connection itself is still valid, so we can continue operating.
|
||||||
pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> {
|
pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> {
|
||||||
self.conn.lock().unwrap()
|
self.conn.lock().unwrap_or_else(|e| e.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run all embedded SQL migrations.
|
/// Run all embedded SQL migrations.
|
||||||
|
|||||||
@ -101,9 +101,24 @@ pub fn data_directory() -> PathBuf {
|
|||||||
PathBuf::from(".")
|
PathBuf::from(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cached log file handle — opened once on first use, reused for all subsequent
|
||||||
|
/// writes. Avoids the open/close syscall pair that the original implementation
|
||||||
|
/// paid on every `wraith_log!` invocation.
|
||||||
|
static LOG_FILE: std::sync::OnceLock<std::sync::Mutex<std::fs::File>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
fn write_log(path: &std::path::Path, msg: &str) -> std::io::Result<()> {
|
fn write_log(path: &std::path::Path, msg: &str) -> std::io::Result<()> {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
|
|
||||||
|
let handle = LOG_FILE.get_or_init(|| {
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
|
.expect("failed to open wraith.log");
|
||||||
|
std::sync::Mutex::new(file)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut f = handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let elapsed = std::time::SystemTime::now()
|
let elapsed = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|||||||
@ -62,9 +62,9 @@ impl ErrorWatcher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw = buf.read_raw();
|
// Only scan bytes written since the last check — avoids
|
||||||
let new_start = raw.len().saturating_sub(total - last_pos);
|
// reading the entire 64 KB ring buffer on every 2-second tick.
|
||||||
let new_content = &raw[new_start..];
|
let new_content = buf.read_since(last_pos);
|
||||||
|
|
||||||
for line in new_content.lines() {
|
for line in new_content.lines() {
|
||||||
for pattern in ERROR_PATTERNS {
|
for pattern in ERROR_PATTERNS {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ impl ScrollbackBuffer {
|
|||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut buf = self.inner.lock().unwrap();
|
let mut buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let cap = buf.capacity;
|
let cap = buf.capacity;
|
||||||
// If input exceeds capacity, only keep the last `cap` bytes
|
// If input exceeds capacity, only keep the last `cap` bytes
|
||||||
let data = if bytes.len() > cap {
|
let data = if bytes.len() > cap {
|
||||||
@ -72,7 +72,7 @@ impl ScrollbackBuffer {
|
|||||||
|
|
||||||
/// Read all buffered content as raw bytes (ordered oldest→newest).
|
/// Read all buffered content as raw bytes (ordered oldest→newest).
|
||||||
pub fn read_raw(&self) -> String {
|
pub fn read_raw(&self) -> String {
|
||||||
let buf = self.inner.lock().unwrap();
|
let buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let bytes = if buf.total_written >= buf.capacity {
|
let bytes = if buf.total_written >= buf.capacity {
|
||||||
// Buffer has wrapped — read from write_pos to end, then start to write_pos
|
// Buffer has wrapped — read from write_pos to end, then start to write_pos
|
||||||
let mut out = Vec::with_capacity(buf.capacity);
|
let mut out = Vec::with_capacity(buf.capacity);
|
||||||
@ -88,7 +88,47 @@ impl ScrollbackBuffer {
|
|||||||
|
|
||||||
/// Total bytes written since creation.
|
/// Total bytes written since creation.
|
||||||
pub fn total_written(&self) -> usize {
|
pub fn total_written(&self) -> usize {
|
||||||
self.inner.lock().unwrap().total_written
|
self.inner.lock().unwrap_or_else(|e| e.into_inner()).total_written
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read only the bytes written after `position` (total_written offset),
|
||||||
|
/// ordered oldest→newest, with ANSI codes stripped.
|
||||||
|
///
|
||||||
|
/// Returns an empty string when there is nothing new since `position`.
|
||||||
|
/// This is more efficient than `read_raw()` for incremental scanning because
|
||||||
|
/// it avoids copying the full 64 KB ring buffer when only a small delta exists.
|
||||||
|
pub fn read_since(&self, position: usize) -> String {
|
||||||
|
let buf = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let total = buf.total_written;
|
||||||
|
if total <= position {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let new_bytes = total - position;
|
||||||
|
let cap = buf.capacity;
|
||||||
|
|
||||||
|
// How many bytes are actually stored in the ring (max = capacity)
|
||||||
|
let stored = total.min(cap);
|
||||||
|
// Clamp new_bytes to what's actually in the buffer
|
||||||
|
let readable = new_bytes.min(stored);
|
||||||
|
|
||||||
|
// Write position is where the *next* byte would go; reading backwards
|
||||||
|
// from write_pos gives us the most recent `readable` bytes.
|
||||||
|
let write_pos = buf.write_pos;
|
||||||
|
let bytes = if readable <= write_pos {
|
||||||
|
// Contiguous slice ending at write_pos
|
||||||
|
buf.data[write_pos - readable..write_pos].to_vec()
|
||||||
|
} else {
|
||||||
|
// Wraps around: tail of buffer + head up to write_pos
|
||||||
|
let tail_len = readable - write_pos;
|
||||||
|
let tail_start = cap - tail_len;
|
||||||
|
let mut out = Vec::with_capacity(readable);
|
||||||
|
out.extend_from_slice(&buf.data[tail_start..]);
|
||||||
|
out.extend_from_slice(&buf.data[..write_pos]);
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw = String::from_utf8_lossy(&bytes).to_string();
|
||||||
|
strip_ansi(&raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user