libobs_wrapper\bootstrap/
mod.rs

1use 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 status (first is progress from 0.0 to 1.0 and second is message)
27    Downloading(f32, String),
28
29    /// Extracting status (first is progress from 0.0 to 1.0 and second is message)
30    Extracting(f32, String),
31    Error(anyhow::Error),
32    /// The application must be restarted to use the new version of OBS.
33    /// This is because the obs.dll file is in use by the application and can not be replaced while running.
34    /// Therefore the "updater" is spawned to watch for the application to exit and rename the "obs_new.dll" file to "obs.dll".
35    /// The updater will start the application again with the same arguments as the original application.
36    /// Call `ObsContext::spawn_updater()`
37    RestartRequired,
38}
39
40#[async_trait]
41/// A trait for bootstrapping OBS Studio.
42///
43/// This trait provides functionality to download, extract, and set up OBS Studio
44/// for use with libobs-rs. It also handles updates to OBS when necessary.
45///
46/// If you want to use this bootstrapper to also install required OBS binaries at runtime,
47/// do the following:
48/// - Add a `obs.dll` file to your executable directory. This file will be replaced by the obs installer.
49/// Recommended to use is the a dll dummy (found [here](https://github.com/sshcrack/libobs-builds/releases), make sure you use the correct OBS version)
50/// and rename it to `obs.dll`.
51/// - Call `ObsRuntime::new()` at the start of your application. Options must be configured. For more documentation look at the [tauri example app](https://github.com/joshprk/libobs-rs/tree/main/examples/tauri-app). This will download the latest version of OBS and extract it in the executable directory.
52/// - If BootstrapStatus::RestartRequired is returned, call `ObsContext::spawn_updater()` to spawn the updater process.
53/// - Exit the application. The updater process will wait for the application to exit and rename the `obs_new.dll` file to `obs.dll` and restart your application with the same arguments as before.
54///
55/// [Example project](https://github.com/joshprk/libobs-rs/tree/main/examples/download-at-runtime)
56pub 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    // Skip the first argument which is the executable path
169    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    // Add arguments as an array
200    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}