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