partner_chains_cli/
io.rs

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