mirror of
https://gitlab.com/veilid/veilid.git
synced 2024-10-01 01:26:08 -04:00
add interactive mode to veilid-cli
This commit is contained in:
parent
931d145719
commit
e98877fc71
46
Cargo.lock
generated
46
Cargo.lock
generated
@ -1255,6 +1255,23 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot 0.12.1",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
@ -1314,7 +1331,7 @@ dependencies = [
|
||||
"async-std",
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"crossterm 0.25.0",
|
||||
"cursive_core",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@ -4324,6 +4341,22 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline-async"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6eb06391513b2184f0a5405c11a4a0a5302e8be442f4c5c35267187c2b37d5"
|
||||
dependencies = [
|
||||
"crossterm 0.27.0",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"thingbuf",
|
||||
"thiserror",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.16"
|
||||
@ -4917,6 +4950,16 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thingbuf"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4706f1bfb859af03f099ada2de3cea3e515843c2d3e93b7893f16d94a37f9415"
|
||||
dependencies = [
|
||||
"parking_lot 0.12.1",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.56"
|
||||
@ -5641,6 +5684,7 @@ dependencies = [
|
||||
"lru",
|
||||
"owning_ref",
|
||||
"parking_lot 0.12.1",
|
||||
"rustyline-async",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serial_test",
|
||||
|
@ -12,6 +12,7 @@ resolver = "2"
|
||||
[patch.crates-io]
|
||||
cursive = { git = "https://gitlab.com/veilid/cursive.git" }
|
||||
cursive_core = { git = "https://gitlab.com/veilid/cursive.git" }
|
||||
nom = { git = "https://gitlab.com/emixa-d/ansi-parser.git" }
|
||||
|
||||
# For local development
|
||||
# keyvaluedb = { path = "../keyvaluedb/keyvaluedb" }
|
||||
|
@ -67,6 +67,7 @@ chrono = "0.4.31"
|
||||
owning_ref = "0.4.1"
|
||||
unicode-width = "0.1.11"
|
||||
lru = "0.10.1"
|
||||
rustyline-async = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "^2"
|
||||
|
@ -80,7 +80,7 @@ impl ClientApiConnection {
|
||||
async fn process_veilid_update(&self, update: json::JsonValue) {
|
||||
let comproc = self.inner.lock().comproc.clone();
|
||||
let Some(kind) = update["kind"].as_str() else {
|
||||
comproc.log_message(Level::Error, format!("missing update kind: {}", update));
|
||||
comproc.log_message(Level::Error, &format!("missing update kind: {}", update));
|
||||
return;
|
||||
};
|
||||
match kind {
|
||||
@ -110,7 +110,7 @@ impl ClientApiConnection {
|
||||
comproc.update_value_change(&update);
|
||||
}
|
||||
_ => {
|
||||
comproc.log_message(Level::Error, format!("unknown update kind: {}", update));
|
||||
comproc.log_message(Level::Error, &format!("unknown update kind: {}", update));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ impl ConnectionState {
|
||||
}
|
||||
|
||||
struct CommandProcessorInner {
|
||||
ui_sender: UISender,
|
||||
ui_sender: Box<dyn UISender>,
|
||||
capi: Option<ClientApiConnection>,
|
||||
reconnect: bool,
|
||||
finished: bool,
|
||||
@ -60,7 +60,7 @@ pub struct CommandProcessor {
|
||||
}
|
||||
|
||||
impl CommandProcessor {
|
||||
pub fn new(ui_sender: UISender, settings: &Settings) -> Self {
|
||||
pub fn new(ui_sender: Box<dyn UISender>, settings: &Settings) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(CommandProcessorInner {
|
||||
ui_sender,
|
||||
@ -86,8 +86,8 @@ impl CommandProcessor {
|
||||
fn inner_mut(&self) -> MutexGuard<CommandProcessorInner> {
|
||||
self.inner.lock()
|
||||
}
|
||||
fn ui_sender(&self) -> UISender {
|
||||
self.inner.lock().ui_sender.clone()
|
||||
fn ui_sender(&self) -> Box<dyn UISender> {
|
||||
self.inner.lock().ui_sender.clone_uisender()
|
||||
}
|
||||
fn capi(&self) -> ClientApiConnection {
|
||||
self.inner.lock().capi.as_ref().unwrap().clone()
|
||||
@ -126,7 +126,7 @@ impl CommandProcessor {
|
||||
|
||||
ui.add_node_event(
|
||||
Level::Info,
|
||||
format!(
|
||||
&format!(
|
||||
r#"Client Commands:
|
||||
exit/quit exit the client
|
||||
disconnect disconnect the client from the Veilid node
|
||||
@ -190,11 +190,11 @@ Server Debug Commands:
|
||||
spawn_detached_local(async move {
|
||||
match capi.server_debug(command_line).await {
|
||||
Ok(output) => {
|
||||
ui.add_node_event(Level::Info, output);
|
||||
ui.add_node_event(Level::Info, &output);
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
Err(e) => {
|
||||
ui.add_node_event(Level::Error, e.to_string());
|
||||
ui.add_node_event(Level::Error, &e);
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
}
|
||||
@ -215,7 +215,7 @@ Server Debug Commands:
|
||||
let log_level = match convert_loglevel(&rest.unwrap_or_default()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
ui.add_node_event(Level::Error, format!("Failed to change log level: {}", e));
|
||||
ui.add_node_event(Level::Error, &format!("Failed to change log level: {}", e));
|
||||
ui.send_callback(callback);
|
||||
return;
|
||||
}
|
||||
@ -228,7 +228,7 @@ Server Debug Commands:
|
||||
Err(e) => {
|
||||
ui.display_string_dialog(
|
||||
"Server command 'change_log_level' failed",
|
||||
e.to_string(),
|
||||
&e,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
@ -247,11 +247,11 @@ Server Debug Commands:
|
||||
match flag.as_str() {
|
||||
"app_messages" => {
|
||||
this.inner.lock().enable_app_messages = true;
|
||||
ui.add_node_event(Level::Info, format!("flag enabled: {}", flag));
|
||||
ui.add_node_event(Level::Info, &format!("flag enabled: {}", flag));
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
_ => {
|
||||
ui.add_node_event(Level::Error, format!("unknown flag: {}", flag));
|
||||
ui.add_node_event(Level::Error, &format!("unknown flag: {}", flag));
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
}
|
||||
@ -269,11 +269,11 @@ Server Debug Commands:
|
||||
match flag.as_str() {
|
||||
"app_messages" => {
|
||||
this.inner.lock().enable_app_messages = false;
|
||||
ui.add_node_event(Level::Info, format!("flag disabled: {}", flag));
|
||||
ui.add_node_event(Level::Info, &format!("flag disabled: {}", flag));
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
_ => {
|
||||
ui.add_node_event(Level::Error, format!("unknown flag: {}", flag));
|
||||
ui.add_node_event(Level::Error, &format!("unknown flag: {}", flag));
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
}
|
||||
@ -413,13 +413,13 @@ Server Debug Commands:
|
||||
// calls into ui
|
||||
////////////////////////////////////////////
|
||||
|
||||
pub fn log_message(&self, log_level: Level, message: String) {
|
||||
pub fn log_message(&self, log_level: Level, message: &str) {
|
||||
self.inner().ui_sender.add_node_event(log_level, message);
|
||||
}
|
||||
|
||||
pub fn update_attachment(&self, attachment: &json::JsonValue) {
|
||||
self.inner_mut().ui_sender.set_attachment_state(
|
||||
attachment["state"].as_str().unwrap_or_default().to_owned(),
|
||||
attachment["state"].as_str().unwrap_or_default(),
|
||||
attachment["public_internet_ready"]
|
||||
.as_bool()
|
||||
.unwrap_or_default(),
|
||||
@ -458,7 +458,7 @@ Server Debug Commands:
|
||||
));
|
||||
}
|
||||
if !out.is_empty() {
|
||||
self.inner().ui_sender.add_node_event(Level::Info, out);
|
||||
self.inner().ui_sender.add_node_event(Level::Info, &out);
|
||||
}
|
||||
}
|
||||
pub fn update_value_change(&self, value_change: &json::JsonValue) {
|
||||
@ -475,7 +475,7 @@ Server Debug Commands:
|
||||
datastr,
|
||||
if truncated { "..." } else { "" }
|
||||
);
|
||||
self.inner().ui_sender.add_node_event(Level::Info, out);
|
||||
self.inner().ui_sender.add_node_event(Level::Info, &out);
|
||||
}
|
||||
|
||||
pub fn update_log(&self, log: &json::JsonValue) {
|
||||
@ -483,7 +483,7 @@ Server Debug Commands:
|
||||
Level::from_str(log["log_level"].as_str().unwrap_or("error")).unwrap_or(Level::Error);
|
||||
self.inner().ui_sender.add_node_event(
|
||||
log_level,
|
||||
format!(
|
||||
&format!(
|
||||
"{}: {}{}",
|
||||
log["log_level"].as_str().unwrap_or("???"),
|
||||
log["message"].as_str().unwrap_or("???"),
|
||||
@ -530,7 +530,7 @@ Server Debug Commands:
|
||||
|
||||
self.inner().ui_sender.add_node_event(
|
||||
Level::Info,
|
||||
format!(
|
||||
&format!(
|
||||
"AppMessage ({:?}): {}{}",
|
||||
msg["sender"],
|
||||
strmsg,
|
||||
@ -570,7 +570,7 @@ Server Debug Commands:
|
||||
|
||||
self.inner().ui_sender.add_node_event(
|
||||
Level::Info,
|
||||
format!(
|
||||
&format!(
|
||||
"AppCall ({:?}) id = {:016x} : {}{}",
|
||||
call["sender"],
|
||||
id,
|
||||
|
1430
veilid-cli/src/cursive_ui.rs
Normal file
1430
veilid-cli/src/cursive_ui.rs
Normal file
File diff suppressed because it is too large
Load Diff
189
veilid-cli/src/interactive_ui.rs
Normal file
189
veilid-cli/src/interactive_ui.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use std::io::Write;
|
||||
|
||||
use crate::command_processor::*;
|
||||
use crate::settings::*;
|
||||
use crate::tools::*;
|
||||
use crate::ui::*;
|
||||
|
||||
use flexi_logger::writers::LogWriter;
|
||||
use rustyline_async::SharedWriter;
|
||||
use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
|
||||
|
||||
pub type InteractiveUICallback = Box<dyn FnMut() + Send>;
|
||||
|
||||
pub struct InteractiveUIInner {
|
||||
cmdproc: Option<CommandProcessor>,
|
||||
stdout: Option<SharedWriter>,
|
||||
error: Option<String>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InteractiveUI {
|
||||
inner: Arc<Mutex<InteractiveUIInner>>,
|
||||
}
|
||||
|
||||
impl InteractiveUI {
|
||||
pub fn new(_settings: &Settings) -> (Self, InteractiveUISender) {
|
||||
// Create the UI object
|
||||
let this = Self {
|
||||
inner: Arc::new(Mutex::new(InteractiveUIInner {
|
||||
cmdproc: None,
|
||||
stdout: None,
|
||||
error: None,
|
||||
done: false,
|
||||
})),
|
||||
};
|
||||
|
||||
let ui_sender = InteractiveUISender {
|
||||
inner: this.inner.clone(),
|
||||
};
|
||||
|
||||
(this, ui_sender)
|
||||
}
|
||||
|
||||
pub async fn command_loop(&self) {
|
||||
let (mut readline, mut stdout) =
|
||||
match Readline::new("> ".to_owned()).map_err(|e| e.to_string()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.inner.lock().stdout = Some(stdout.clone());
|
||||
|
||||
loop {
|
||||
if self.inner.lock().done {
|
||||
break;
|
||||
}
|
||||
if let Some(e) = self.inner.lock().error.clone() {
|
||||
println!("Error: {:?}", e);
|
||||
break;
|
||||
}
|
||||
match readline.readline().await {
|
||||
Ok(ReadlineEvent::Line(line)) => {
|
||||
let line = line.trim();
|
||||
if line == "clear" {
|
||||
if let Err(e) = readline.clear() {
|
||||
println!("Error: {:?}", e);
|
||||
}
|
||||
} else if !line.is_empty() {
|
||||
readline.add_history_entry(line.to_string());
|
||||
let cmdproc = self.inner.lock().cmdproc.clone();
|
||||
if let Some(cmdproc) = &cmdproc {
|
||||
if let Err(e) = cmdproc.run_command(
|
||||
line,
|
||||
UICallback::Interactive(Box::new({
|
||||
//let mut stdout = stdout.clone();
|
||||
move || {
|
||||
// if let Err(e) = writeln!(stdout) {
|
||||
// println!("Error: {:?}", e);
|
||||
// }
|
||||
}
|
||||
})),
|
||||
) {
|
||||
if let Err(e) = writeln!(stdout, "Error: {}", e) {
|
||||
println!("Error: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ReadlineEvent::Interrupted) => {
|
||||
break;
|
||||
}
|
||||
Ok(ReadlineEvent::Eof) => {
|
||||
break;
|
||||
}
|
||||
Err(ReadlineError::Closed) => {}
|
||||
Err(ReadlineError::IO(e)) => {
|
||||
println!("IO Error: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = readline.flush();
|
||||
}
|
||||
}
|
||||
|
||||
impl UI for InteractiveUI {
|
||||
fn set_command_processor(&mut self, cmdproc: CommandProcessor) {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.cmdproc = Some(cmdproc);
|
||||
}
|
||||
// Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this
|
||||
fn run_async(&mut self) -> Pin<Box<dyn core::future::Future<Output = ()>>> {
|
||||
let this = self.clone();
|
||||
Box::pin(async move {
|
||||
this.command_loop().await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InteractiveUISender {
|
||||
inner: Arc<Mutex<InteractiveUIInner>>,
|
||||
}
|
||||
|
||||
impl UISender for InteractiveUISender {
|
||||
fn clone_uisender(&self) -> Box<dyn UISender> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
fn as_logwriter(&self) -> Option<Box<dyn LogWriter>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn display_string_dialog(&self, title: &str, text: &str, close_cb: UICallback) {
|
||||
println!("{}: {}", title, text);
|
||||
if let UICallback::Interactive(mut close_cb) = close_cb {
|
||||
close_cb()
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.inner.lock().done = true;
|
||||
}
|
||||
|
||||
fn send_callback(&self, callback: UICallback) {
|
||||
if let UICallback::Interactive(mut callback) = callback {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
fn set_attachment_state(
|
||||
&mut self,
|
||||
_state: &str,
|
||||
_public_internet_ready: bool,
|
||||
_local_network_ready: bool,
|
||||
) {
|
||||
//
|
||||
}
|
||||
fn set_network_status(
|
||||
&mut self,
|
||||
_started: bool,
|
||||
_bps_down: u64,
|
||||
_bps_up: u64,
|
||||
mut _peers: Vec<json::JsonValue>,
|
||||
) {
|
||||
//
|
||||
}
|
||||
fn set_config(&mut self, _config: &json::JsonValue) {
|
||||
//
|
||||
}
|
||||
fn set_connection_state(&mut self, _state: ConnectionState) {
|
||||
//
|
||||
}
|
||||
|
||||
fn add_node_event(&self, _log_color: Level, event: &str) {
|
||||
let Some(mut stdout) = self.inner.lock().stdout.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = writeln!(stdout, "{}", event) {
|
||||
self.inner.lock().error = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
#![deny(unused_must_use)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
use crate::{settings::NamedSocketAddrs, tools::*};
|
||||
use crate::{settings::NamedSocketAddrs, tools::*, ui::*};
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use flexi_logger::*;
|
||||
@ -11,6 +11,8 @@ use std::path::PathBuf;
|
||||
mod cached_text_view;
|
||||
mod client_api_connection;
|
||||
mod command_processor;
|
||||
mod cursive_ui;
|
||||
mod interactive_ui;
|
||||
mod peers_table_view;
|
||||
mod settings;
|
||||
mod tools;
|
||||
@ -31,7 +33,7 @@ struct CmdlineArgs {
|
||||
#[arg(long, short = 'p')]
|
||||
ipc_path: Option<PathBuf>,
|
||||
/// Subnode index to use when connecting
|
||||
#[arg(long, short = 'i', default_value = "0")]
|
||||
#[arg(long, default_value = "0")]
|
||||
subnode_index: usize,
|
||||
/// Address to connect to
|
||||
#[arg(long, short = 'a')]
|
||||
@ -45,6 +47,23 @@ struct CmdlineArgs {
|
||||
/// log level
|
||||
#[arg(value_enum)]
|
||||
log_level: Option<LogLevel>,
|
||||
/// interactive
|
||||
#[arg(long, short = 'i', group = "execution_mode")]
|
||||
interactive: bool,
|
||||
/// evaluate
|
||||
#[arg(long, short = 'e', group = "execution_mode")]
|
||||
evaluate: Option<String>,
|
||||
/// show log
|
||||
#[arg(long, short = 'l', group = "execution_mode")]
|
||||
show_log: bool,
|
||||
/// read commands from file
|
||||
#[arg(
|
||||
long,
|
||||
short = 'f',
|
||||
group = "execution_mode",
|
||||
value_name = "COMMAND_FILE"
|
||||
)]
|
||||
command_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
@ -78,8 +97,29 @@ fn main() -> Result<(), String> {
|
||||
settings.logging.terminal.enabled = true;
|
||||
}
|
||||
|
||||
// If we are running in interactive mode disable some things
|
||||
let mut enable_cursive = true;
|
||||
if args.interactive || args.show_log || args.command_file.is_some() || args.evaluate.is_some() {
|
||||
settings.logging.terminal.enabled = false;
|
||||
enable_cursive = false;
|
||||
}
|
||||
|
||||
// Create UI object
|
||||
let (mut sivui, uisender) = ui::UI::new(settings.interface.node_log.scrollback, &settings);
|
||||
let (mut ui, uisender) = if enable_cursive {
|
||||
let (ui, uisender) = cursive_ui::CursiveUI::new(&settings);
|
||||
(
|
||||
Box::new(ui) as Box<dyn UI>,
|
||||
Box::new(uisender) as Box<dyn UISender>,
|
||||
)
|
||||
} else if args.interactive {
|
||||
let (ui, uisender) = interactive_ui::InteractiveUI::new(&settings);
|
||||
(
|
||||
Box::new(ui) as Box<dyn UI>,
|
||||
Box::new(uisender) as Box<dyn UISender>,
|
||||
)
|
||||
} else {
|
||||
panic!("unknown ui mode");
|
||||
};
|
||||
|
||||
// Set up loggers
|
||||
{
|
||||
@ -105,13 +145,13 @@ fn main() -> Result<(), String> {
|
||||
FileSpec::default()
|
||||
.directory(settings.logging.file.directory.clone())
|
||||
.suppress_timestamp(),
|
||||
Box::new(uisender.clone()),
|
||||
uisender.as_logwriter().unwrap(),
|
||||
)
|
||||
.start()
|
||||
.expect("failed to initialize logger!");
|
||||
} else {
|
||||
logger
|
||||
.log_to_writer(Box::new(uisender.clone()))
|
||||
.log_to_writer(uisender.as_logwriter().unwrap())
|
||||
.start()
|
||||
.expect("failed to initialize logger!");
|
||||
}
|
||||
@ -195,7 +235,8 @@ fn main() -> Result<(), String> {
|
||||
// Create command processor
|
||||
debug!("Creating Command Processor ");
|
||||
let comproc = command_processor::CommandProcessor::new(uisender, &settings);
|
||||
sivui.set_command_processor(comproc.clone());
|
||||
|
||||
ui.set_command_processor(comproc.clone());
|
||||
|
||||
// Create client api client side
|
||||
info!("Starting API connection");
|
||||
@ -221,7 +262,7 @@ fn main() -> Result<(), String> {
|
||||
block_on(async move {
|
||||
// Start UI
|
||||
let ui_future = async move {
|
||||
sivui.run_async().await;
|
||||
ui.run_async().await;
|
||||
|
||||
// When UI quits, close connection and command processor cleanly
|
||||
comproc2.quit();
|
||||
|
1394
veilid-cli/src/ui.rs
1394
veilid-cli/src/ui.rs
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user