@@ -8,7 +8,7 @@ |
| 8 | 8 | |
| 9 | 9 | use clap::{Parser, Subcommand}; |
| 10 | 10 | use tracing::{info, Level}; |
| 11 | | -use tracing_subscriber::FmtSubscriber; |
| 11 | +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; |
| 12 | 12 | |
| 13 | 13 | mod config; |
| 14 | 14 | mod hyprland; |
@@ -95,17 +95,43 @@ enum ConfigAction { |
| 95 | 95 | async fn main() -> anyhow::Result<()> { |
| 96 | 96 | let cli = Cli::parse(); |
| 97 | 97 | |
| 98 | | - // Set up logging |
| 98 | + // Set up logging with dual output (stderr + file) |
| 99 | 99 | let log_level = match cli.verbose { |
| 100 | 100 | 0 => Level::INFO, |
| 101 | 101 | 1 => Level::DEBUG, |
| 102 | 102 | _ => Level::TRACE, |
| 103 | 103 | }; |
| 104 | 104 | |
| 105 | | - FmtSubscriber::builder() |
| 106 | | - .with_max_level(log_level) |
| 107 | | - .with_target(false) |
| 108 | | - .init(); |
| 105 | + // Create log directory |
| 106 | + let log_dir = dirs::data_local_dir() |
| 107 | + .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) |
| 108 | + .join("hyprkvm"); |
| 109 | + std::fs::create_dir_all(&log_dir).ok(); |
| 110 | + |
| 111 | + // File appender with daily rotation |
| 112 | + let file_appender = tracing_appender::rolling::daily(&log_dir, "daemon.log"); |
| 113 | + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); |
| 114 | + |
| 115 | + // Build subscriber with both stderr and file layers |
| 116 | + let subscriber = tracing_subscriber::registry() |
| 117 | + .with( |
| 118 | + tracing_subscriber::fmt::layer() |
| 119 | + .with_target(false) |
| 120 | + .with_writer(std::io::stderr) |
| 121 | + ) |
| 122 | + .with( |
| 123 | + tracing_subscriber::fmt::layer() |
| 124 | + .with_target(true) |
| 125 | + .with_ansi(false) |
| 126 | + .with_writer(non_blocking) |
| 127 | + ) |
| 128 | + .with( |
| 129 | + tracing_subscriber::filter::LevelFilter::from_level(log_level) |
| 130 | + ); |
| 131 | + subscriber.init(); |
| 132 | + |
| 133 | + // Keep the guard alive for the duration of the program |
| 134 | + // (it's moved into the async context below) |
| 109 | 135 | |
| 110 | 136 | // Load configuration |
| 111 | 137 | let config_path = cli.config.unwrap_or_else(|| { |
@@ -686,14 +712,18 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 686 | 712 | .map(|c| (c.x, c.y)) |
| 687 | 713 | .unwrap_or((0, 0)); |
| 688 | 714 | |
| 689 | | - info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction); |
| 690 | | - if let Err(e) = transfer_manager.initiate_transfer( |
| 691 | | - direction, |
| 692 | | - cursor_pos, |
| 693 | | - screen_height, |
| 694 | | - screen_width, |
| 695 | | - ).await { |
| 696 | | - tracing::error!("Failed to initiate transfer from recovery hotkey: {}", e); |
| 715 | + if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 716 | + info!("RECOVERY HOTKEY: Barrier enabled, blocking transfer"); |
| 717 | + } else { |
| 718 | + info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction); |
| 719 | + if let Err(e) = transfer_manager.initiate_transfer( |
| 720 | + direction, |
| 721 | + cursor_pos, |
| 722 | + screen_height, |
| 723 | + screen_width, |
| 724 | + ).await { |
| 725 | + tracing::error!("Failed to initiate transfer from recovery hotkey: {}", e); |
| 726 | + } |
| 697 | 727 | } |
| 698 | 728 | } else if !at_edge { |
| 699 | 729 | // Not at edge - need to do movefocus ourselves because libinput |
@@ -755,20 +785,29 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 755 | 785 | } |
| 756 | 786 | } |
| 757 | 787 | |
| 758 | | - info!( |
| 759 | | - "EDGE: {:?} at ({}, {}) - initiating transfer", |
| 760 | | - direction, |
| 761 | | - edge_event.position.0, |
| 762 | | - edge_event.position.1 |
| 763 | | - ); |
| 764 | | - |
| 765 | | - if let Err(e) = transfer_manager.initiate_transfer( |
| 766 | | - direction, |
| 767 | | - edge_event.position, |
| 768 | | - screen_height, |
| 769 | | - screen_width, |
| 770 | | - ).await { |
| 771 | | - tracing::warn!("Failed to initiate transfer: {}", e); |
| 788 | + if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 789 | + info!( |
| 790 | + "EDGE: {:?} at ({}, {}) - barrier enabled, blocking", |
| 791 | + direction, |
| 792 | + edge_event.position.0, |
| 793 | + edge_event.position.1 |
| 794 | + ); |
| 795 | + } else { |
| 796 | + info!( |
| 797 | + "EDGE: {:?} at ({}, {}) - initiating transfer", |
| 798 | + direction, |
| 799 | + edge_event.position.0, |
| 800 | + edge_event.position.1 |
| 801 | + ); |
| 802 | + |
| 803 | + if let Err(e) = transfer_manager.initiate_transfer( |
| 804 | + direction, |
| 805 | + edge_event.position, |
| 806 | + screen_height, |
| 807 | + screen_width, |
| 808 | + ).await { |
| 809 | + tracing::warn!("Failed to initiate transfer: {}", e); |
| 810 | + } |
| 772 | 811 | } |
| 773 | 812 | } else { |
| 774 | 813 | tracing::debug!( |
@@ -852,18 +891,25 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 852 | 891 | } |
| 853 | 892 | } |
| 854 | 893 | |
| 855 | | - info!( |
| 856 | | - "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer", |
| 857 | | - edge_dir, cx, cy |
| 858 | | - ); |
| 859 | | - |
| 860 | | - if let Err(e) = transfer_manager.initiate_transfer( |
| 861 | | - edge_dir, |
| 862 | | - (cx, cy), |
| 863 | | - screen_height, |
| 864 | | - screen_width, |
| 865 | | - ).await { |
| 866 | | - tracing::warn!("Failed to initiate transfer: {}", e); |
| 894 | + if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 895 | + info!( |
| 896 | + "CURSOR EDGE: {:?} at ({}, {}) - barrier enabled, blocking", |
| 897 | + edge_dir, cx, cy |
| 898 | + ); |
| 899 | + } else { |
| 900 | + info!( |
| 901 | + "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer", |
| 902 | + edge_dir, cx, cy |
| 903 | + ); |
| 904 | + |
| 905 | + if let Err(e) = transfer_manager.initiate_transfer( |
| 906 | + edge_dir, |
| 907 | + (cx, cy), |
| 908 | + screen_height, |
| 909 | + screen_width, |
| 910 | + ).await { |
| 911 | + tracing::warn!("Failed to initiate transfer: {}", e); |
| 912 | + } |
| 867 | 913 | } |
| 868 | 914 | } else { |
| 869 | 915 | tracing::debug!( |
@@ -1295,6 +1341,31 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1295 | 1341 | } |
| 1296 | 1342 | } else { |
| 1297 | 1343 | // At edge with peer but received control from different direction |
| 1344 | + if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 1345 | + IpcResponse::Error { message: "Barrier enabled".to_string() } |
| 1346 | + } else { |
| 1347 | + // Initiate new transfer |
| 1348 | + let cursor_pos = hypr_client.cursor_pos().await |
| 1349 | + .map(|c| (c.x, c.y)) |
| 1350 | + .unwrap_or((0, 0)); |
| 1351 | + |
| 1352 | + if let Err(e) = transfer_manager.initiate_transfer( |
| 1353 | + direction, |
| 1354 | + cursor_pos, |
| 1355 | + screen_height, |
| 1356 | + screen_width, |
| 1357 | + ).await { |
| 1358 | + IpcResponse::Error { message: format!("Transfer failed: {}", e) } |
| 1359 | + } else { |
| 1360 | + IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } |
| 1361 | + } |
| 1362 | + } |
| 1363 | + } |
| 1364 | + } else { |
| 1365 | + // Not in ReceivedControl - check barrier |
| 1366 | + if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) { |
| 1367 | + IpcResponse::Error { message: "Barrier enabled".to_string() } |
| 1368 | + } else { |
| 1298 | 1369 | // Initiate new transfer |
| 1299 | 1370 | let cursor_pos = hypr_client.cursor_pos().await |
| 1300 | 1371 | .map(|c| (c.x, c.y)) |
@@ -1311,22 +1382,6 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1311 | 1382 | IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } |
| 1312 | 1383 | } |
| 1313 | 1384 | } |
| 1314 | | - } else { |
| 1315 | | - // Not in ReceivedControl - initiate new transfer |
| 1316 | | - let cursor_pos = hypr_client.cursor_pos().await |
| 1317 | | - .map(|c| (c.x, c.y)) |
| 1318 | | - .unwrap_or((0, 0)); |
| 1319 | | - |
| 1320 | | - if let Err(e) = transfer_manager.initiate_transfer( |
| 1321 | | - direction, |
| 1322 | | - cursor_pos, |
| 1323 | | - screen_height, |
| 1324 | | - screen_width, |
| 1325 | | - ).await { |
| 1326 | | - IpcResponse::Error { message: format!("Transfer failed: {}", e) } |
| 1327 | | - } else { |
| 1328 | | - IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } |
| 1329 | | - } |
| 1330 | 1385 | } |
| 1331 | 1386 | } else { |
| 1332 | 1387 | // Either not at edge, or at edge but no peer - do local movefocus |
@@ -1687,9 +1742,60 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1687 | 1742 | } |
| 1688 | 1743 | |
| 1689 | 1744 | IpcRequest::Reload => { |
| 1690 | | - // TODO: Implement config hot-reload |
| 1691 | | - IpcResponse::Error { |
| 1692 | | - message: "Config reload not yet implemented".to_string(), |
| 1745 | + // Re-read and validate config file |
| 1746 | + match config::Config::load(&config_path) { |
| 1747 | + Ok(new_config) => { |
| 1748 | + // Check what changed |
| 1749 | + let mut changes = Vec::new(); |
| 1750 | + let mut needs_restart = false; |
| 1751 | + |
| 1752 | + if new_config.machines.self_name != config.machines.self_name { |
| 1753 | + changes.push(format!( |
| 1754 | + "machine name: {} -> {} (requires restart)", |
| 1755 | + config.machines.self_name, new_config.machines.self_name |
| 1756 | + )); |
| 1757 | + needs_restart = true; |
| 1758 | + } |
| 1759 | + |
| 1760 | + if new_config.network.listen_port != config.network.listen_port { |
| 1761 | + changes.push(format!( |
| 1762 | + "listen port: {} -> {} (requires restart)", |
| 1763 | + config.network.listen_port, new_config.network.listen_port |
| 1764 | + )); |
| 1765 | + needs_restart = true; |
| 1766 | + } |
| 1767 | + |
| 1768 | + if new_config.machines.neighbors.len() != config.machines.neighbors.len() { |
| 1769 | + changes.push(format!( |
| 1770 | + "neighbors: {} -> {} (requires restart)", |
| 1771 | + config.machines.neighbors.len(), new_config.machines.neighbors.len() |
| 1772 | + )); |
| 1773 | + needs_restart = true; |
| 1774 | + } |
| 1775 | + |
| 1776 | + if changes.is_empty() { |
| 1777 | + IpcResponse::Ok { |
| 1778 | + message: "Config unchanged".to_string(), |
| 1779 | + } |
| 1780 | + } else if needs_restart { |
| 1781 | + IpcResponse::Ok { |
| 1782 | + message: format!( |
| 1783 | + "Config changes detected (restart required):\n - {}", |
| 1784 | + changes.join("\n - ") |
| 1785 | + ), |
| 1786 | + } |
| 1787 | + } else { |
| 1788 | + IpcResponse::Ok { |
| 1789 | + message: format!( |
| 1790 | + "Config reloaded:\n - {}", |
| 1791 | + changes.join("\n - ") |
| 1792 | + ), |
| 1793 | + } |
| 1794 | + } |
| 1795 | + } |
| 1796 | + Err(e) => IpcResponse::Error { |
| 1797 | + message: format!("Failed to load config: {}", e), |
| 1798 | + } |
| 1693 | 1799 | } |
| 1694 | 1800 | } |
| 1695 | 1801 | |
@@ -1706,34 +1812,51 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 1706 | 1812 | } |
| 1707 | 1813 | |
| 1708 | 1814 | IpcRequest::GetLogs { lines, follow: _ } => { |
| 1709 | | - // Read from log file |
| 1710 | | - let log_path = dirs::data_local_dir() |
| 1815 | + // Find log files (rolling appender creates daemon.log.YYYY-MM-DD) |
| 1816 | + let log_dir = dirs::data_local_dir() |
| 1711 | 1817 | .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) |
| 1712 | | - .join("hyprkvm") |
| 1713 | | - .join("daemon.log"); |
| 1714 | | - |
| 1715 | | - if log_path.exists() { |
| 1716 | | - match std::fs::read_to_string(&log_path) { |
| 1717 | | - Ok(content) => { |
| 1718 | | - let n = lines.unwrap_or(50) as usize; |
| 1719 | | - let log_lines: Vec<String> = content |
| 1720 | | - .lines() |
| 1721 | | - .rev() |
| 1722 | | - .take(n) |
| 1723 | | - .map(|s| s.to_string()) |
| 1724 | | - .collect::<Vec<_>>() |
| 1725 | | - .into_iter() |
| 1726 | | - .rev() |
| 1727 | | - .collect(); |
| 1728 | | - IpcResponse::Logs { lines: log_lines } |
| 1729 | | - } |
| 1730 | | - Err(e) => IpcResponse::Error { |
| 1731 | | - message: format!("Failed to read log file: {}", e), |
| 1818 | + .join("hyprkvm"); |
| 1819 | + |
| 1820 | + // Find the most recent log file |
| 1821 | + let log_file = std::fs::read_dir(&log_dir) |
| 1822 | + .ok() |
| 1823 | + .and_then(|entries| { |
| 1824 | + entries |
| 1825 | + .filter_map(|e| e.ok()) |
| 1826 | + .filter(|e| { |
| 1827 | + e.file_name() |
| 1828 | + .to_string_lossy() |
| 1829 | + .starts_with("daemon.log") |
| 1830 | + }) |
| 1831 | + .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok())) |
| 1832 | + .map(|e| e.path()) |
| 1833 | + }); |
| 1834 | + |
| 1835 | + match log_file { |
| 1836 | + Some(path) => { |
| 1837 | + match std::fs::read_to_string(&path) { |
| 1838 | + Ok(content) => { |
| 1839 | + let n = lines.unwrap_or(50) as usize; |
| 1840 | + let log_lines: Vec<String> = content |
| 1841 | + .lines() |
| 1842 | + .rev() |
| 1843 | + .take(n) |
| 1844 | + .map(|s| s.to_string()) |
| 1845 | + .collect::<Vec<_>>() |
| 1846 | + .into_iter() |
| 1847 | + .rev() |
| 1848 | + .collect(); |
| 1849 | + IpcResponse::Logs { lines: log_lines } |
| 1850 | + } |
| 1851 | + Err(e) => IpcResponse::Error { |
| 1852 | + message: format!("Failed to read log file: {}", e), |
| 1853 | + } |
| 1732 | 1854 | } |
| 1733 | 1855 | } |
| 1734 | | - } else { |
| 1735 | | - IpcResponse::Logs { |
| 1736 | | - lines: vec!["Log file not found. File logging may not be configured.".to_string()], |
| 1856 | + None => { |
| 1857 | + IpcResponse::Logs { |
| 1858 | + lines: vec!["No log files found.".to_string()], |
| 1859 | + } |
| 1737 | 1860 | } |
| 1738 | 1861 | } |
| 1739 | 1862 | } |