tenseleyflow/hyprkvm / 2590f42

Browse files

feat: complete CLI expansion with barrier, logging, and reload

Barrier integration:
- Add barrier checks to all edge transfer points
- Keyboard navigation, mouse edge, cursor edge all respect barrier
- IPC Move command blocked when barrier enabled (except return)

File logging:
- Add tracing-appender for dual output (stderr + file)
- Logs written to ~/.local/share/hyprkvm/daemon.log.YYYY-MM-DD
- Daily rotation with automatic file discovery in logs command

Reload command:
- Re-reads and validates config file
- Reports changes detected and whether restart required
- Checks machine name, listen port, and neighbor count

All CLI commands now functional:
- status, peers, ping (diagnostics)
- switch, return (control transfer)
- release, barrier on/off (input management)
- disconnect, reconnect (connection management)
- config show, reload, shutdown, logs (daemon management)
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
2590f42d88a5aec97377dae36583cb31bbf8cd2f
Parents
226565d
Tree
1b37322

4 changed files

StatusFile+-
M Cargo.lock 61 7
M Cargo.toml 1 0
M hyprkvm-daemon/Cargo.toml 1 0
M hyprkvm-daemon/src/main.rs 207 84
Cargo.lockmodified
@@ -162,7 +162,7 @@ dependencies = [
162162
  "polling",
163163
  "rustix 0.38.44",
164164
  "slab",
165
- "thiserror",
165
+ "thiserror 1.0.69",
166166
 ]
167167
 
168168
 [[package]]
@@ -259,6 +259,15 @@ dependencies = [
259259
  "crossbeam-utils",
260260
 ]
261261
 
262
+[[package]]
263
+name = "crossbeam-channel"
264
+version = "0.5.15"
265
+source = "registry+https://github.com/rust-lang/crates.io-index"
266
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
267
+dependencies = [
268
+ "crossbeam-utils",
269
+]
270
+
262271
 [[package]]
263272
 name = "crossbeam-utils"
264273
 version = "0.8.21"
@@ -339,7 +348,7 @@ dependencies = [
339348
  "cfg-if",
340349
  "libc",
341350
  "nix",
342
- "thiserror",
351
+ "thiserror 1.0.69",
343352
 ]
344353
 
345354
 [[package]]
@@ -430,7 +439,7 @@ version = "0.1.0"
430439
 dependencies = [
431440
  "serde",
432441
  "serde_json",
433
- "thiserror",
442
+ "thiserror 1.0.69",
434443
 ]
435444
 
436445
 [[package]]
@@ -451,11 +460,12 @@ dependencies = [
451460
  "serde",
452461
  "serde_json",
453462
  "smithay-client-toolkit",
454
- "thiserror",
463
+ "thiserror 1.0.69",
455464
  "tokio",
456465
  "tokio-rustls",
457466
  "toml",
458467
  "tracing",
468
+ "tracing-appender",
459469
  "tracing-subscriber",
460470
  "wayland-client",
461471
  "wayland-protocols 0.31.2",
@@ -777,7 +787,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
777787
 dependencies = [
778788
  "getrandom 0.2.16",
779789
  "libredox",
780
- "thiserror",
790
+ "thiserror 1.0.69",
781791
 ]
782792
 
783793
 [[package]]
@@ -993,7 +1003,7 @@ dependencies = [
9931003
  "memmap2 0.9.9",
9941004
  "pkg-config",
9951005
  "rustix 0.38.44",
996
- "thiserror",
1006
+ "thiserror 1.0.69",
9971007
  "wayland-backend",
9981008
  "wayland-client",
9991009
  "wayland-csd-frame",
@@ -1050,7 +1060,16 @@ version = "1.0.69"
10501060
 source = "registry+https://github.com/rust-lang/crates.io-index"
10511061
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
10521062
 dependencies = [
1053
- "thiserror-impl",
1063
+ "thiserror-impl 1.0.69",
1064
+]
1065
+
1066
+[[package]]
1067
+name = "thiserror"
1068
+version = "2.0.17"
1069
+source = "registry+https://github.com/rust-lang/crates.io-index"
1070
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
1071
+dependencies = [
1072
+ "thiserror-impl 2.0.17",
10541073
 ]
10551074
 
10561075
 [[package]]
@@ -1064,6 +1083,17 @@ dependencies = [
10641083
  "syn",
10651084
 ]
10661085
 
1086
+[[package]]
1087
+name = "thiserror-impl"
1088
+version = "2.0.17"
1089
+source = "registry+https://github.com/rust-lang/crates.io-index"
1090
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
1091
+dependencies = [
1092
+ "proc-macro2",
1093
+ "quote",
1094
+ "syn",
1095
+]
1096
+
10671097
 [[package]]
10681098
 name = "thread_local"
10691099
 version = "1.1.9"
@@ -1080,10 +1110,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
10801110
 checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
10811111
 dependencies = [
10821112
  "deranged",
1113
+ "itoa",
10831114
  "num-conv",
10841115
  "powerfmt",
10851116
  "serde",
10861117
  "time-core",
1118
+ "time-macros",
10871119
 ]
10881120
 
10891121
 [[package]]
@@ -1092,6 +1124,16 @@ version = "0.1.6"
10921124
 source = "registry+https://github.com/rust-lang/crates.io-index"
10931125
 checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
10941126
 
1127
+[[package]]
1128
+name = "time-macros"
1129
+version = "0.2.24"
1130
+source = "registry+https://github.com/rust-lang/crates.io-index"
1131
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
1132
+dependencies = [
1133
+ "num-conv",
1134
+ "time-core",
1135
+]
1136
+
10951137
 [[package]]
10961138
 name = "tokio"
10971139
 version = "1.49.0"
@@ -1182,6 +1224,18 @@ dependencies = [
11821224
  "tracing-core",
11831225
 ]
11841226
 
1227
+[[package]]
1228
+name = "tracing-appender"
1229
+version = "0.2.4"
1230
+source = "registry+https://github.com/rust-lang/crates.io-index"
1231
+checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
1232
+dependencies = [
1233
+ "crossbeam-channel",
1234
+ "thiserror 2.0.17",
1235
+ "time",
1236
+ "tracing-subscriber",
1237
+]
1238
+
11851239
 [[package]]
11861240
 name = "tracing-attributes"
11871241
 version = "0.1.31"
Cargo.tomlmodified
@@ -30,6 +30,7 @@ anyhow = "1"
3030
 # Logging
3131
 tracing = "0.1"
3232
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
33
+tracing-appender = "0.2"
3334
 
3435
 # CLI
3536
 clap = { version = "4", features = ["derive"] }
hyprkvm-daemon/Cargo.tomlmodified
@@ -28,6 +28,7 @@ anyhow = { workspace = true }
2828
 # Logging
2929
 tracing = { workspace = true }
3030
 tracing-subscriber = { workspace = true }
31
+tracing-appender = { workspace = true }
3132
 
3233
 # CLI
3334
 clap = { workspace = true }
hyprkvm-daemon/src/main.rsmodified
@@ -8,7 +8,7 @@
88
 
99
 use clap::{Parser, Subcommand};
1010
 use tracing::{info, Level};
11
-use tracing_subscriber::FmtSubscriber;
11
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
1212
 
1313
 mod config;
1414
 mod hyprland;
@@ -95,17 +95,43 @@ enum ConfigAction {
9595
 async fn main() -> anyhow::Result<()> {
9696
     let cli = Cli::parse();
9797
 
98
-    // Set up logging
98
+    // Set up logging with dual output (stderr + file)
9999
     let log_level = match cli.verbose {
100100
         0 => Level::INFO,
101101
         1 => Level::DEBUG,
102102
         _ => Level::TRACE,
103103
     };
104104
 
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)
109135
 
110136
     // Load configuration
111137
     let config_path = cli.config.unwrap_or_else(|| {
@@ -686,14 +712,18 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
686712
                                     .map(|c| (c.x, c.y))
687713
                                     .unwrap_or((0, 0));
688714
 
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
+                                    }
697727
                                 }
698728
                             } else if !at_edge {
699729
                                 // 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<()> {
755785
                             }
756786
                         }
757787
 
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
+                            }
772811
                         }
773812
                     } else {
774813
                         tracing::debug!(
@@ -852,18 +891,25 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
852891
                                                         }
853892
                                                     }
854893
 
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
+                                                        }
867913
                                                     }
868914
                                                 } else {
869915
                                                     tracing::debug!(
@@ -1295,6 +1341,31 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
12951341
                                     }
12961342
                                 } else {
12971343
                                     // 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 {
12981369
                                     // Initiate new transfer
12991370
                                     let cursor_pos = hypr_client.cursor_pos().await
13001371
                                         .map(|c| (c.x, c.y))
@@ -1311,22 +1382,6 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
13111382
                                         IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
13121383
                                     }
13131384
                                 }
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
-                                }
13301385
                             }
13311386
                         } else {
13321387
                             // 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<()> {
16871742
                     }
16881743
 
16891744
                     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
+                            }
16931799
                         }
16941800
                     }
16951801
 
@@ -1706,34 +1812,51 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
17061812
                     }
17071813
 
17081814
                     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()
17111817
                             .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
+                                    }
17321854
                                 }
17331855
                             }
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
+                                }
17371860
                             }
17381861
                         }
17391862
                     }