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 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
66pub struct DefaultCmdRunContext;
68
69impl IOContext for DefaultCmdRunContext {
70 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 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 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}