libobs_wrapper\bootstrap/
mod.rs1use std::{env, path::PathBuf, process};
2
3use anyhow::Context;
4use async_stream::stream;
5use async_trait::async_trait;
6use download::DownloadStatus;
7use extract::ExtractStatus;
8use futures_core::Stream;
9use futures_util::{pin_mut, StreamExt};
10use lazy_static::lazy_static;
11use libobs::{LIBOBS_API_MAJOR_VER, LIBOBS_API_MINOR_VER, LIBOBS_API_PATCH_VER};
12use tokio::{fs::File, io::AsyncWriteExt, process::Command};
13
14use crate::context::ObsContext;
15
16mod download;
17mod extract;
18mod github_types;
19mod options;
20pub mod status_handler;
21mod version;
22
23pub use options::ObsBootstrapperOptions;
24
25pub enum BootstrapStatus {
26 Downloading(f32, String),
28
29 Extracting(f32, String),
31 Error(anyhow::Error),
32 RestartRequired,
38}
39
40#[async_trait]
41pub trait ObsBootstrap {
57 fn is_valid_installation() -> anyhow::Result<bool>;
58 fn is_update_available() -> anyhow::Result<bool>;
59}
60
61lazy_static! {
62 pub(crate) static ref LIBRARY_OBS_VERSION: String = format!(
63 "{}.{}.{}",
64 LIBOBS_API_MAJOR_VER, LIBOBS_API_MINOR_VER, LIBOBS_API_PATCH_VER
65 );
66}
67
68pub const UPDATER_SCRIPT: &'static str = include_str!("./updater.ps1");
69
70fn get_obs_dll_path() -> anyhow::Result<PathBuf> {
71 let executable = env::current_exe()?;
72 let obs_dll = executable
73 .parent()
74 .ok_or_else(|| anyhow::anyhow!("Failed to get parent directory"))?
75 .join("obs.dll");
76
77 Ok(obs_dll)
78}
79
80#[cfg_attr(feature="blocking", remove_async_await::remove_async_await)]
81pub(crate) async fn bootstrap(
82 options: &options::ObsBootstrapperOptions,
83) -> anyhow::Result<Option<impl Stream<Item = BootstrapStatus>>> {
84 let repo = options.repository.to_string();
85
86 log::trace!("Checking for update...");
87 let update = if options.update {
88 ObsContext::is_update_available()?
89 } else {
90 ObsContext::is_valid_installation()?
91 };
92
93 if !update {
94 log::debug!("No update needed.");
95 return Ok(None);
96 }
97
98 let options = options.clone();
99 Ok(Some(stream! {
100 log::debug!("Downloading OBS from {}", repo);
101 let download_stream = download::download_obs(&repo).await;
102 if let Err(err) = download_stream {
103 yield BootstrapStatus::Error(err);
104 return;
105 }
106
107 let download_stream = download_stream.unwrap();
108 pin_mut!(download_stream);
109
110 let mut file = None;
111 while let Some(item) = download_stream.next().await {
112 match item {
113 DownloadStatus::Error(err) => {
114 yield BootstrapStatus::Error(err);
115 return;
116 }
117 DownloadStatus::Progress(progress, message) => {
118 yield BootstrapStatus::Downloading(progress, message);
119 }
120 DownloadStatus::Done(path) => {
121 file = Some(path)
122 }
123 }
124 }
125
126 let archive_file = file.ok_or_else(|| anyhow::anyhow!("OBS Archive could not be downloaded."));
127 if let Err(err) = archive_file {
128 yield BootstrapStatus::Error(err);
129 return;
130 }
131
132 log::debug!("Extracting OBS to {:?}", archive_file);
133 let archive_file = archive_file.unwrap();
134 let extract_stream = extract::extract_obs(&archive_file).await;
135 if let Err(err) = extract_stream {
136 yield BootstrapStatus::Error(err);
137 return;
138 }
139
140 let extract_stream = extract_stream.unwrap();
141 pin_mut!(extract_stream);
142
143 while let Some(item) = extract_stream.next().await {
144 match item {
145 ExtractStatus::Error(err) => {
146 yield BootstrapStatus::Error(err);
147 return;
148 }
149 ExtractStatus::Progress(progress, message) => {
150 yield BootstrapStatus::Extracting(progress, message);
151 }
152 }
153 }
154
155 let r = spawn_updater(options).await;
156 if let Err(err) = r {
157 yield BootstrapStatus::Error(err);
158 return;
159 }
160
161 yield BootstrapStatus::RestartRequired;
162 }))
163}
164
165pub(crate) async fn spawn_updater(options: options::ObsBootstrapperOptions) -> anyhow::Result<()> {
166 let pid = process::id();
167 let args = env::args().collect::<Vec<_>>();
168 let args = args.into_iter().skip(1).collect::<Vec<_>>();
170
171 let updater_path = env::temp_dir().join("libobs_updater.ps1");
172 let mut updater_file = File::create(&updater_path)
173 .await
174 .context("Creating updater script")?;
175
176 updater_file
177 .write_all(UPDATER_SCRIPT.as_bytes())
178 .await
179 .context("Writing updater script")?;
180
181 let mut command = Command::new("powershell");
182 command
183 .arg("-ExecutionPolicy")
184 .arg("Bypass")
185 .arg("-NoProfile")
186 .arg("-WindowStyle")
187 .arg("Hidden")
188 .arg("-File")
189 .arg(updater_path)
190 .arg("-processPid")
191 .arg(pid.to_string())
192 .arg("-binary")
193 .arg(env::current_exe()?.to_string_lossy().to_string());
194
195 if options.restart_after_update {
196 command.arg("-restart");
197 }
198
199 if !args.is_empty() {
201 command.arg("-arguments");
202 command.arg(format!("({})", args.join(",").replace("\"", "`\"")));
203 }
204
205 command.spawn().context("Spawning updater process")?;
206
207 Ok(())
208}
209
210#[async_trait]
211impl ObsBootstrap for ObsContext {
212 fn is_valid_installation() -> anyhow::Result<bool> {
213 let installed = version::get_installed_version(&get_obs_dll_path()?)?;
214
215 Ok(installed.is_some())
216 }
217
218 fn is_update_available() -> anyhow::Result<bool> {
219 let installed = version::get_installed_version(&get_obs_dll_path()?)?;
220 if installed.is_none() {
221 return Ok(true);
222 }
223
224 let installed = installed.unwrap();
225 Ok(version::should_update(&installed)?)
226 }
227}