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 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 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
60pub struct DefaultCmdRunContext;
62
63impl IOContext for DefaultCmdRunContext {
64 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 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 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}