#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![allow(unknown_lints, require_safety_comments_on_unsafe)]
use std::{env, path::PathBuf, process};
use async_stream::stream;
use download::DownloadStatus;
use extract::ExtractStatus;
use futures_core::Stream;
use futures_util::{StreamExt, pin_mut};
use lazy_static::lazy_static;
use libobs::{LIBOBS_API_MAJOR_VER, LIBOBS_API_MINOR_VER, LIBOBS_API_PATCH_VER};
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
#[cfg_attr(coverage_nightly, coverage(off))]
mod download;
mod error;
#[cfg_attr(coverage_nightly, coverage(off))]
mod extract;
#[cfg_attr(coverage_nightly, coverage(off))]
mod github_types;
mod options;
pub mod status_handler;
mod version;
#[cfg(test)]
mod options_tests;
#[cfg(test)]
mod version_tests;
pub use error::ObsBootstrapError;
pub use options::ObsBootstrapperOptions;
use crate::status_handler::{ObsBootstrapConsoleHandler, ObsBootstrapStatusHandler};
pub enum BootstrapStatus {
Downloading(f32, String),
Extracting(f32, String),
Error(ObsBootstrapError),
RestartRequired,
}
pub struct ObsBootstrapper {}
lazy_static! {
pub(crate) static ref LIBRARY_OBS_VERSION: String = format!(
"{}.{}.{}",
LIBOBS_API_MAJOR_VER, LIBOBS_API_MINOR_VER, LIBOBS_API_PATCH_VER
);
}
pub const UPDATER_SCRIPT: &str = include_str!("./updater.ps1");
fn get_obs_dll_path() -> Result<PathBuf, ObsBootstrapError> {
let executable =
env::current_exe().map_err(|e| ObsBootstrapError::IoError("Getting current exe", e))?;
let obs_dll = executable
.parent()
.ok_or_else(|| {
ObsBootstrapError::IoError(
"Failed to get parent directory",
std::io::Error::from(std::io::ErrorKind::InvalidInput),
)
})?
.join("obs.dll");
Ok(obs_dll)
}
pub(crate) fn bootstrap(
options: &ObsBootstrapperOptions,
) -> Result<Option<impl Stream<Item = BootstrapStatus>>, ObsBootstrapError> {
let repo = options.repository.to_string();
log::trace!("Checking for update...");
let update = if options.update {
ObsBootstrapper::is_update_available()?
} else {
!ObsBootstrapper::is_valid_installation()?
};
if !update {
log::debug!("No update needed.");
return Ok(None);
}
let options = options.clone();
Ok(Some(stream! {
log::debug!("Downloading OBS from {}", repo);
let download_stream = download::download_obs(&repo).await;
if let Err(err) = download_stream {
yield BootstrapStatus::Error(err);
return;
}
let download_stream = download_stream.unwrap();
pin_mut!(download_stream);
let mut file = None;
while let Some(item) = download_stream.next().await {
match item {
DownloadStatus::Error(err) => {
yield BootstrapStatus::Error(err);
return;
}
DownloadStatus::Progress(progress, message) => {
yield BootstrapStatus::Downloading(progress, message);
}
DownloadStatus::Done(path) => {
file = Some(path)
}
}
}
let archive_file = file.ok_or(ObsBootstrapError::InvalidState);
if let Err(err) = archive_file {
yield BootstrapStatus::Error(err);
return;
}
log::debug!("Extracting OBS to {:?}", archive_file);
let archive_file = archive_file.unwrap();
let extract_stream = extract::extract_obs(&archive_file).await;
if let Err(err) = extract_stream {
yield BootstrapStatus::Error(err);
return;
}
let extract_stream = extract_stream.unwrap();
pin_mut!(extract_stream);
while let Some(item) = extract_stream.next().await {
match item {
ExtractStatus::Error(err) => {
yield BootstrapStatus::Error(err);
return;
}
ExtractStatus::Progress(progress, message) => {
yield BootstrapStatus::Extracting(progress, message);
}
}
}
let r = spawn_updater(options).await;
if let Err(err) = r {
yield BootstrapStatus::Error(err);
return;
}
yield BootstrapStatus::RestartRequired;
}))
}
pub(crate) async fn spawn_updater(
options: ObsBootstrapperOptions,
) -> Result<(), ObsBootstrapError> {
let pid = process::id();
let args = env::args().collect::<Vec<_>>();
let args = args.into_iter().skip(1).collect::<Vec<_>>();
let updater_path = env::temp_dir().join("libobs_updater.ps1");
let mut updater_file = File::create(&updater_path)
.await
.map_err(|e| ObsBootstrapError::IoError("Creating updater script", e))?;
updater_file
.write_all(UPDATER_SCRIPT.as_bytes())
.await
.map_err(|e| ObsBootstrapError::IoError("Writing updater script", e))?;
let mut command = Command::new("powershell");
command
.arg("-ExecutionPolicy")
.arg("Bypass")
.arg("-NoProfile")
.arg("-WindowStyle")
.arg("Hidden")
.arg("-File")
.arg(updater_path)
.arg("-processPid")
.arg(pid.to_string())
.arg("-binary")
.arg(
env::current_exe()
.map_err(|e| ObsBootstrapError::IoError("Getting current exe", e))?
.to_string_lossy()
.to_string(),
);
if options.restart_after_update {
command.arg("-restart");
}
if !args.is_empty() {
let joined = args.join("\0");
let bytes = joined.as_bytes();
let hex_str = hex::encode(bytes);
command.arg("-argumentHex");
command.arg(hex_str);
}
command
.spawn()
.map_err(|e| ObsBootstrapError::IoError("Spawning updater process", e))?;
Ok(())
}
pub enum ObsBootstrapperResult {
None,
Restart,
}
impl ObsBootstrapper {
pub fn is_valid_installation() -> Result<bool, ObsBootstrapError> {
let installed = version::get_installed_version(&get_obs_dll_path()?)?;
Ok(installed.is_some())
}
pub fn is_update_available() -> Result<bool, ObsBootstrapError> {
let installed = version::get_installed_version(&get_obs_dll_path()?)?;
if installed.is_none() {
return Ok(true);
}
let installed = installed.unwrap();
version::should_update(&installed)
}
pub async fn bootstrap(
options: &options::ObsBootstrapperOptions,
) -> Result<ObsBootstrapperResult, ObsBootstrapError> {
ObsBootstrapper::bootstrap_with_handler(
options,
Box::new(ObsBootstrapConsoleHandler::default()),
)
.await
}
pub async fn bootstrap_with_handler<E: Send + Sync + 'static + std::error::Error>(
options: &options::ObsBootstrapperOptions,
mut handler: Box<dyn ObsBootstrapStatusHandler<Error = E>>,
) -> Result<ObsBootstrapperResult, ObsBootstrapError> {
let stream = bootstrap(options)?;
if let Some(stream) = stream {
pin_mut!(stream);
log::trace!("Waiting for bootstrapper to finish");
while let Some(item) = stream.next().await {
match item {
BootstrapStatus::Downloading(progress, message) => {
handler
.handle_downloading(progress, message)
.map_err(|e| ObsBootstrapError::Abort(Box::new(e)))?;
}
BootstrapStatus::Extracting(progress, message) => {
handler
.handle_extraction(progress, message)
.map_err(|e| ObsBootstrapError::Abort(Box::new(e)))?;
}
BootstrapStatus::Error(err) => {
return Err(err);
}
BootstrapStatus::RestartRequired => {
return Ok(ObsBootstrapperResult::Restart);
}
}
}
}
Ok(ObsBootstrapperResult::None)
}
}