interactive.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{io::ErrorKind, mem::zeroed, ptr::null_mut, str::FromStr};
  19. use libc::{fd_set, select, timeval, FD_SET, FD_ZERO};
  20. use linenoise_rs::{
  21. linenoise_history_add, linenoise_history_load, linenoise_history_save,
  22. linenoise_set_completion_callback, linenoise_set_hints_callback, LinenoiseState,
  23. };
  24. use darkfi::{cli_desc, system::StoppableTask, util::path::expand_path};
  25. use crate::{
  26. cli_util::{generate_completions, kaching},
  27. Drk,
  28. };
  29. // TODO:
  30. // 1. add rest commands handling, along with their completions, hints and help message.
  31. // 2. add input definitions, so you input from files not just stdin.
  32. // 3. add output definitions, so you can output to files not just stdout.
  33. // 4. create a transactions cache in the wallet db, so you can use it to handle them.
  34. /// Auxiliary function to print the help message.
  35. fn help() {
  36. println!("{}", cli_desc!());
  37. println!("Commands:");
  38. println!("\thelp: Prints the help message");
  39. println!("\tkaching: Fun");
  40. println!("\tping: Send a ping request to the darkfid RPC endpoint");
  41. println!("\tcompletions: Generate a SHELL completion script and print to stdout");
  42. println!(
  43. "\tsubscribe: Perform a scan and then subscribe to darkfid to listen for incoming blocks"
  44. );
  45. println!("\tunsubscribe: Stops the background subscription, if its active");
  46. println!("\tscan: Scan the blockchain and parse relevant transactions");
  47. }
  48. /// Auxiliary function to define the interactive shell completions.
  49. fn completion(buf: &str, lc: &mut Vec<String>) {
  50. if buf.starts_with("h") {
  51. lc.push("help".to_string());
  52. return
  53. }
  54. if buf.starts_with("k") {
  55. lc.push("kaching".to_string());
  56. return
  57. }
  58. if buf.starts_with("p") {
  59. lc.push("ping".to_string());
  60. return
  61. }
  62. if buf.starts_with("c") {
  63. lc.push("completions".to_string());
  64. return
  65. }
  66. if buf.starts_with("su") {
  67. lc.push("subscribe".to_string());
  68. return
  69. }
  70. if buf.starts_with("u") {
  71. lc.push("unsubscribe".to_string());
  72. return
  73. }
  74. if buf.starts_with("sc") {
  75. lc.push("scan".to_string());
  76. }
  77. }
  78. /// Auxiliary function to define the interactive shell hints.
  79. fn hints(buf: &str) -> Option<(String, i32, bool)> {
  80. match buf {
  81. "completions " => Some(("{shell}".to_string(), 35, false)), // 35 = magenta
  82. "scan " => Some(("--reset {height}".to_string(), 35, false)), // 35 = magenta
  83. _ => None,
  84. }
  85. }
  86. /// Auxiliary function to start provided Drk as an interactive shell.
  87. /// Only sane/linenoise terminals are suported.
  88. pub async fn interactive(drk: &Drk, history_path: &str) {
  89. // Expand the history file path
  90. let history_path = match expand_path(history_path) {
  91. Ok(p) => p,
  92. Err(e) => {
  93. eprintln!("Error while expanding history file path: {e}");
  94. return
  95. }
  96. };
  97. let history_path = history_path.into_os_string();
  98. let history_file = history_path.to_str().unwrap();
  99. // Set the completion callback. This will be called every time the
  100. // user uses the <tab> key.
  101. linenoise_set_completion_callback(completion);
  102. // Set the shell hints
  103. linenoise_set_hints_callback(hints);
  104. // Load history from file.The history file is just a plain text file
  105. // where entries are separated by newlines.
  106. let _ = linenoise_history_load(history_file);
  107. // Create a detached task to use for block subscription
  108. let mut subscription_active = false;
  109. let subscription_task = StoppableTask::new();
  110. // Create two bounded smol channels, so we can have 2 way
  111. // communication between the shell thread and the background task.
  112. let (_shell_sender, shell_receiver) = smol::channel::bounded::<()>(1);
  113. let (background_sender, _background_receiver) = smol::channel::bounded::<()>(1);
  114. // Start the interactive shell
  115. loop {
  116. // Generate the linoise state structure
  117. let mut state = match LinenoiseState::edit_start(-1, -1, "drk> ") {
  118. Ok(s) => s,
  119. Err(e) => {
  120. eprintln!("Error while generating linenoise state: {e}");
  121. break
  122. }
  123. };
  124. // Read until we get a line to process
  125. let mut line = None;
  126. loop {
  127. let retval = unsafe {
  128. // Setup read buffers
  129. let mut readfds: fd_set = zeroed();
  130. FD_ZERO(&mut readfds);
  131. FD_SET(state.get_fd(), &mut readfds);
  132. // Setup a 1 second timeout to check if background
  133. // process wants to print.
  134. let mut tv = timeval { tv_sec: 1, tv_usec: 0 };
  135. // Wait timeout or input
  136. select(state.get_fd() + 1, &mut readfds, null_mut(), null_mut(), &mut tv)
  137. };
  138. // Handle error
  139. if retval == -1 {
  140. eprintln!("Error while reading linenoise buffers");
  141. break
  142. }
  143. // Check if background process wants to print anything
  144. if shell_receiver.is_full() {
  145. // Consume the channel message
  146. if let Err(e) = shell_receiver.recv().await {
  147. eprintln!("Error while reading shell receiver channel: {e}");
  148. break
  149. }
  150. // Signal background task it can start printing
  151. let _ = state.hide();
  152. if let Err(e) = background_sender.send(()).await {
  153. eprintln!("Error while sending to background task channel: {e}");
  154. break
  155. }
  156. // Wait signal that it finished
  157. if let Err(e) = shell_receiver.recv().await {
  158. eprintln!("Error while reading shell receiver channel: {e}");
  159. break
  160. }
  161. let _ = state.show();
  162. }
  163. // Check if we have a line to process
  164. if retval <= 0 {
  165. continue
  166. }
  167. // Process linenoise feed
  168. match state.edit_feed() {
  169. Ok(Some(l)) => line = Some(l),
  170. Ok(None) => { /* Do nothing */ }
  171. Err(e) if e.kind() == ErrorKind::Interrupted => { /* Do nothing */ }
  172. Err(e) if e.kind() == ErrorKind::WouldBlock => {
  173. // Need more input, continue
  174. continue;
  175. }
  176. Err(e) => eprintln!("Error while reading linenoise feed: {e}"),
  177. }
  178. break
  179. }
  180. let _ = state.edit_stop();
  181. // Grab input or end if Ctrl-D or Ctrl-C was pressed
  182. let Some(line) = line else { break };
  183. // Check if line is empty
  184. if line.is_empty() {
  185. continue
  186. }
  187. // Add line to history
  188. linenoise_history_add(&line);
  189. // Parse command parts
  190. let parts: Vec<&str> = line.split_whitespace().collect();
  191. if parts.is_empty() {
  192. continue
  193. }
  194. // Handle command
  195. match parts[0] {
  196. "help" => help(),
  197. "kaching" => kaching().await,
  198. "ping" => handle_ping(drk).await,
  199. "completions" => handle_completions(&parts),
  200. "subscribe" => {
  201. handle_subscribe(drk, &mut subscription_active, &subscription_task).await
  202. }
  203. "unsubscribe" => handle_unsubscribe(&mut subscription_active, &subscription_task).await,
  204. "scan" => handle_scan(drk, &subscription_active, &parts).await,
  205. _ => println!("Unreconized command: {}", parts[0]),
  206. }
  207. }
  208. // Stop the subscription task if its active
  209. if subscription_active {
  210. subscription_task.stop().await;
  211. }
  212. // Write history file
  213. let _ = linenoise_history_save(history_file);
  214. }
  215. /// Auxiliary function to define the ping command handling.
  216. async fn handle_ping(drk: &Drk) {
  217. if let Err(e) = drk.ping().await {
  218. println!("Error while executing ping command: {e}")
  219. }
  220. }
  221. /// Auxiliary function to define the completions command handling.
  222. fn handle_completions(parts: &[&str]) {
  223. if parts.len() != 2 {
  224. println!("Malformed `completions` command");
  225. println!("Usage: completions {{shell}}");
  226. return
  227. }
  228. if let Err(e) = generate_completions(parts[1]) {
  229. println!("Error while executing completions command: {e}")
  230. }
  231. }
  232. /// Auxiliary function to define the subscribe command handling.
  233. async fn handle_subscribe(
  234. drk: &Drk,
  235. subscription_active: &mut bool,
  236. _subscription_task: &StoppableTask,
  237. ) {
  238. if *subscription_active {
  239. println!("Subscription is already active!")
  240. }
  241. if let Err(e) = drk.scan_blocks().await {
  242. println!("Failed during scanning: {e:?}");
  243. return
  244. }
  245. println!("Finished scanning blockchain");
  246. // TODO: subscribe
  247. *subscription_active = true;
  248. }
  249. /// Auxiliary function to define the unsubscribe command handling.
  250. async fn handle_unsubscribe(subscription_active: &mut bool, subscription_task: &StoppableTask) {
  251. if !*subscription_active {
  252. println!("Subscription is already inactive!")
  253. }
  254. subscription_task.stop().await;
  255. *subscription_active = false;
  256. }
  257. /// Auxiliary function to define the scan command handling.
  258. async fn handle_scan(drk: &Drk, subscription_active: &bool, parts: &[&str]) {
  259. if *subscription_active {
  260. println!("Subscription is already active!");
  261. return
  262. }
  263. // Check correct command structure
  264. if parts.len() != 1 && parts.len() != 3 {
  265. println!("Malformed `scan` command");
  266. return
  267. }
  268. // Check if reset was requested
  269. if parts.len() == 3 {
  270. if parts[1] != "--reset" {
  271. println!("Malformed `scan` command");
  272. println!("Usage: scan --reset {{height}}");
  273. return
  274. }
  275. let height = match u32::from_str(parts[2]) {
  276. Ok(h) => h,
  277. Err(e) => {
  278. println!("Invalid reset height: {e:?}");
  279. return
  280. }
  281. };
  282. if let Err(e) = drk.reset_to_height(height) {
  283. println!("Failed during wallet reset: {e:?}");
  284. return
  285. }
  286. }
  287. if let Err(e) = drk.scan_blocks().await {
  288. println!("Failed during scanning: {e:?}");
  289. return
  290. }
  291. println!("Finished scanning blockchain");
  292. }