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 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 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 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 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 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}