partner_chains_cli/
io.rs

1use crate::cmd_traits::*;
2use crate::config::{ConfigFile, ServiceConfig};
3use crate::ogmios::{OgmiosRequest, OgmiosResponse, ogmios_request};
4use anyhow::{Context, anyhow};
5use inquire::InquireError;
6use inquire::error::InquireResult;
7use ogmios_client::jsonrpsee::{OgmiosClients, client_for_url};
8use sp_core::offchain::Timestamp;
9use std::{
10	fs,
11	io::{BufRead, BufReader, Read},
12	path::PathBuf,
13	process::Stdio,
14	time::Duration,
15};
16use tempfile::TempDir;
17
18pub trait IOContext {
19	/// It should implement all the required traits for offchain operations
20	type Offchain: GetScriptsData
21		+ InitGovernance
22		+ GetDParam
23		+ UpsertDParam
24		+ Deregister
25		+ Register
26		+ GetPermissionedCandidates
27		+ UpsertPermissionedCandidates;
28
29	fn run_command(&self, cmd: &str) -> anyhow::Result<String>;
30	fn current_executable(&self) -> anyhow::Result<String>;
31	fn print(&self, msg: &str);
32	fn eprint(&self, msg: &str);
33	fn enewline(&self);
34	fn prompt(&self, prompt: &str, default: Option<&str>) -> String;
35	fn prompt_yes_no(&self, prompt: &str, default: bool) -> bool;
36	// TODO: 	fn prompt_multi_option<T: ToString>(&self, msg: &str, options: Vec<T>) -> T;
37	fn prompt_multi_option(&self, msg: &str, options: Vec<String>) -> String;
38	fn write_file(&self, path: &str, content: &str);
39
40	fn new_tmp_dir(&self) -> PathBuf;
41	fn read_file(&self, path: &str) -> Option<String>;
42	fn file_exists(&self, path: &str) -> bool;
43	fn list_directory(&self, path: &str) -> anyhow::Result<Option<Vec<String>>>;
44	fn delete_file(&self, path: &str) -> anyhow::Result<()>;
45	fn set_env_var(&self, key: &str, value: &str);
46	fn current_timestamp(&self) -> Timestamp;
47	fn ogmios_rpc(
48		&self,
49		config: &ServiceConfig,
50		req: OgmiosRequest,
51	) -> anyhow::Result<OgmiosResponse>;
52	fn offchain_impl(&self, ogmios_config: &ServiceConfig) -> anyhow::Result<Self::Offchain>;
53	fn config_file_path(&self, file: ConfigFile) -> String;
54
55	fn chain_config_file_path(&self) -> String {
56		self.config_file_path(ConfigFile::Chain)
57	}
58}
59
60/// Default context implementation using standard IO.
61pub struct DefaultCmdRunContext;
62
63impl IOContext for DefaultCmdRunContext {
64	// Currently WsClient implements Ogmios traits, that implement all required Offchain traits
65	type Offchain = OgmiosClients;
66
67	fn run_command(&self, cmd: &str) -> anyhow::Result<String> {
68		eprintln!("running external command: {cmd}");
69
70		let mut child = std::process::Command::new("sh")
71			.arg("-c")
72			.arg(cmd)
73			.stderr(Stdio::piped())
74			.stdout(Stdio::piped())
75			.spawn()
76			.with_context(|| format!("Running executable failed: {cmd}"))?;
77
78		// pass stderr, appending a prefix
79		for line in BufReader::new(
80			(child.stderr.as_mut()).context("Failed to read child process error output stream")?,
81		)
82		.lines()
83		{
84			let line = line.context("Failed to read error output line")?;
85			self.eprint(&format!("command output: {line}"));
86		}
87
88		// capture stdout
89		let mut output = vec![];
90		child
91			.stdout
92			.as_mut()
93			.context("Failed to read child process output stream")?
94			.read_to_end(&mut output)
95			.context("Failed to read child process output stream")?;
96
97		let status = child.wait()?;
98		if !status.success() {
99			self.eprint(&format!("Running executable failed with status {}", status));
100			if let Some(127) = status.code() {
101				self.eprint("Make sure all executables are in path")
102			}
103			return Err(anyhow!("Failed to run command"));
104		}
105		Ok(String::from_utf8(output)?)
106	}
107
108	fn current_executable(&self) -> anyhow::Result<String> {
109		let exe = std::env::current_exe()?;
110		let node_executable = exe.to_str().ok_or(anyhow!("Cannot get current executable name"))?;
111		Ok(node_executable.to_string())
112	}
113
114	fn print(&self, msg: &str) {
115		println!("{msg}")
116	}
117
118	fn eprint(&self, msg: &str) {
119		eprintln!("{msg}")
120	}
121
122	fn enewline(&self) {
123		eprintln!()
124	}
125
126	fn prompt(&self, prompt: &str, default: Option<&str>) -> String {
127		let mut prompt = inquire::Text::new(prompt);
128		if let Some(default) = default {
129			prompt = prompt.with_default(default)
130		};
131
132		handle_inquire_result(prompt.prompt())
133	}
134
135	fn prompt_yes_no(&self, prompt: &str, default: bool) -> bool {
136		handle_inquire_result(inquire::Confirm::new(prompt).with_default(default).prompt())
137	}
138
139	fn prompt_multi_option(&self, msg: &str, options: Vec<String>) -> String {
140		handle_inquire_result(inquire::Select::new(msg, options).prompt()).to_string()
141	}
142
143	fn write_file(&self, path: &str, content: &str) {
144		fs::write(path, content).unwrap_or_else(|_| panic!("Failed to write file: {path}"))
145	}
146
147	fn new_tmp_dir(&self) -> PathBuf {
148		TempDir::new().expect("Failed to create temporary directory").keep()
149	}
150
151	fn read_file(&self, path: &str) -> Option<String> {
152		Some(
153			String::from_utf8(fs::read(path).ok()?)
154				.unwrap_or_else(|_| panic!("Failed to convert file from UTF-8: {path}")),
155		)
156	}
157
158	fn file_exists(&self, path: &str) -> bool {
159		fs::metadata(path).is_ok()
160	}
161	fn list_directory(&self, path: &str) -> anyhow::Result<Option<Vec<String>>> {
162		if !self.file_exists(path) {
163			return Ok(None);
164		}
165
166		let file_names = fs::read_dir(path)?
167			.flat_map(|file| -> Option<_> { file.ok()?.file_name().into_string().ok() })
168			.collect();
169
170		Ok(Some(file_names))
171	}
172	fn delete_file(&self, path: &str) -> anyhow::Result<()> {
173		fs::remove_file(path).context(format!("Failed to delete file: {path}"))
174	}
175
176	fn set_env_var(&self, key: &str, value: &str) {
177		unsafe {
178			std::env::set_var(key, value);
179		}
180	}
181
182	fn current_timestamp(&self) -> Timestamp {
183		let now = std::time::SystemTime::now();
184		let duration = now
185			.duration_since(std::time::SystemTime::UNIX_EPOCH)
186			.expect("Current time is always after unix epoch");
187		Timestamp::from_unix_millis(duration.as_millis() as u64)
188	}
189
190	fn ogmios_rpc(
191		&self,
192		config: &ServiceConfig,
193		req: OgmiosRequest,
194	) -> anyhow::Result<OgmiosResponse> {
195		ogmios_request(config, req)
196	}
197
198	fn offchain_impl(&self, ogmios_config: &ServiceConfig) -> anyhow::Result<Self::Offchain> {
199		let ogmios_address = ogmios_config.url();
200		let tokio_runtime = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
201		tokio_runtime
202			.block_on(client_for_url(
203				&ogmios_address,
204				Duration::from_secs(ogmios_config.timeout_seconds),
205			))
206			.map_err(|_| {
207				anyhow!(format!("Couldn't open connection to Ogmios at {}", ogmios_address))
208			})
209	}
210
211	fn config_file_path(&self, file: ConfigFile) -> String {
212		match file {
213			ConfigFile::Chain => {
214				std::env::var("PC_CHAIN_CONFIG_PATH").unwrap_or("pc-chain-config.json".to_owned())
215			},
216			ConfigFile::Resources => std::env::var("PC_RESOURCES_CONFIG_PATH")
217				.unwrap_or("pc-resources-config.json".to_owned()),
218		}
219	}
220}
221
222pub(crate) fn prompt_can_write<C: IOContext>(name: &str, path: &str, context: &C) -> bool {
223	!context.file_exists(path)
224		|| context.prompt_yes_no(&format!("{name} {path} exists - overwrite it?"), false)
225}
226
227fn handle_inquire_result<T>(result: InquireResult<T>) -> T {
228	match result {
229		Ok(result) => result,
230		Err(InquireError::OperationInterrupted) => {
231			eprintln!("Ctrl-C pressed. Exiting Wizard.");
232			std::process::exit(0)
233		},
234		Err(InquireError::OperationCanceled) => std::process::exit(0),
235		result => result.unwrap(),
236	}
237}