768 lines
30 KiB
Rust
768 lines
30 KiB
Rust
use std::fs;
|
|
use std::io::{BufRead, BufReader, Read};
|
|
use std::os::windows::process::CommandExt;
|
|
use std::path::PathBuf;
|
|
use std::process::{Command, Stdio};
|
|
use std::thread;
|
|
|
|
use regex::Regex;
|
|
use tauri::{AppHandle, Emitter, State};
|
|
use tokio::sync::mpsc;
|
|
use winreg::enums::*;
|
|
use winreg::RegKey;
|
|
|
|
use crate::domain::models::{InstallProgress, InstallTask, TaskEventPayload};
|
|
use crate::services::essentials_service;
|
|
use crate::services::log_service::emit_log;
|
|
use crate::winget::PostInstallStep;
|
|
|
|
pub struct AppState {
|
|
pub install_tx: mpsc::Sender<InstallTask>,
|
|
pub app_handle: AppHandle,
|
|
}
|
|
|
|
pub fn create_install_state(handle: AppHandle) -> AppState {
|
|
let (tx, mut rx) = mpsc::channel::<InstallTask>(100);
|
|
let runtime_handle = handle.clone();
|
|
|
|
tauri::async_runtime::spawn(async move {
|
|
let perc_re = Regex::new(r"(\d+)\s*%").unwrap();
|
|
let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap();
|
|
|
|
while let Some(task) = rx.recv().await {
|
|
let task_id = task.id.clone();
|
|
let task_version = task.version.clone();
|
|
let use_manifest = task.use_manifest;
|
|
let manifest_url = task.manifest_url.clone();
|
|
let enable_post_install_flag = task.enable_post_install;
|
|
|
|
let log_id = format!("install-{}", task_id);
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"running",
|
|
"installing",
|
|
0.0,
|
|
task_version.clone(),
|
|
None,
|
|
);
|
|
|
|
let mut args = vec!["install".to_string()];
|
|
let display_cmd: String;
|
|
let mut temp_manifest_path: Option<PathBuf> = None;
|
|
|
|
if use_manifest && manifest_url.is_some() {
|
|
let url = manifest_url.unwrap();
|
|
display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url);
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"running",
|
|
"downloading_manifest",
|
|
0.0,
|
|
task_version.clone(),
|
|
Some("Downloading remote manifest".to_string()),
|
|
);
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&display_cmd,
|
|
"Downloading remote manifest...",
|
|
"info",
|
|
);
|
|
|
|
let client = reqwest::Client::new();
|
|
match client
|
|
.get(&url)
|
|
.timeout(std::time::Duration::from_secs(15))
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(resp) if resp.status().is_success() => {
|
|
if let Ok(content) = resp.text().await {
|
|
let temp_dir = std::env::temp_dir();
|
|
let file_name =
|
|
format!("winget_tmp_{}.yaml", chrono::Local::now().timestamp_millis());
|
|
let local_path = temp_dir.join(file_name);
|
|
if fs::write(&local_path, content).is_ok() {
|
|
args.push("--manifest".to_string());
|
|
args.push(local_path.to_string_lossy().to_string());
|
|
temp_manifest_path = Some(local_path);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if temp_manifest_path.is_none() {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Error",
|
|
"Failed to download or save manifest.",
|
|
"error",
|
|
);
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"failed",
|
|
"manifest_error",
|
|
0.0,
|
|
task_version.clone(),
|
|
Some("Failed to download or save manifest".to_string()),
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
args.push("--id".to_string());
|
|
args.push(task_id.clone());
|
|
args.push("-e".to_string());
|
|
|
|
if let Some(v) = &task_version {
|
|
if !v.is_empty() {
|
|
args.push("--version".to_string());
|
|
args.push(v.clone());
|
|
}
|
|
}
|
|
|
|
display_cmd = match &task_version {
|
|
Some(v) if !v.is_empty() => format!("Winget Install: {} (v{})", task_id, v),
|
|
_ => format!("Winget Install: {}", task_id),
|
|
};
|
|
}
|
|
|
|
args.extend([
|
|
"--silent".to_string(),
|
|
"--accept-package-agreements".to_string(),
|
|
"--accept-source-agreements".to_string(),
|
|
"--disable-interactivity".to_string(),
|
|
]);
|
|
|
|
let full_command = format!("winget {}", args.join(" "));
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&display_cmd,
|
|
&format!("Executing: {}\n---", full_command),
|
|
"info",
|
|
);
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"running",
|
|
"invoking_winget",
|
|
0.0,
|
|
task_version.clone(),
|
|
None,
|
|
);
|
|
|
|
let child = Command::new("winget")
|
|
.args(&args)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.creation_flags(0x08000000)
|
|
.spawn();
|
|
|
|
let status_result = match child {
|
|
Ok(mut child_proc) => {
|
|
let stdout_handle = child_proc.stdout.take().map(|stdout| {
|
|
spawn_install_stream_reader(
|
|
stdout,
|
|
runtime_handle.clone(),
|
|
log_id.clone(),
|
|
task_id.clone(),
|
|
"stdout",
|
|
perc_re.clone(),
|
|
size_re.clone(),
|
|
)
|
|
});
|
|
let stderr_handle = child_proc.stderr.take().map(|stderr| {
|
|
spawn_install_stream_reader(
|
|
stderr,
|
|
runtime_handle.clone(),
|
|
log_id.clone(),
|
|
task_id.clone(),
|
|
"stderr",
|
|
perc_re.clone(),
|
|
size_re.clone(),
|
|
)
|
|
});
|
|
|
|
let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false);
|
|
if let Some(join_handle) = stdout_handle {
|
|
let _ = join_handle.join();
|
|
}
|
|
if let Some(join_handle) = stderr_handle {
|
|
let _ = join_handle.join();
|
|
}
|
|
let status_result = if exit_status { "success" } else { "error" };
|
|
|
|
if status_result == "success" && enable_post_install_flag {
|
|
let software_info = essentials_service::get_essentials(&runtime_handle)
|
|
.and_then(|repo| repo.essentials.into_iter().find(|s| s.id == task_id));
|
|
|
|
if let Some(sw) = software_info {
|
|
let mut final_steps = None;
|
|
if let Some(steps) = sw.post_install {
|
|
if !steps.is_empty() {
|
|
final_steps = Some(steps);
|
|
}
|
|
} else if let Some(url) = sw.post_install_url {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install",
|
|
"Local config not found, fetching remote config...",
|
|
"info",
|
|
);
|
|
let client = reqwest::Client::new();
|
|
if let Ok(resp) = client
|
|
.get(&url)
|
|
.timeout(std::time::Duration::from_secs(10))
|
|
.send()
|
|
.await
|
|
{
|
|
if resp.status().is_success() {
|
|
if let Ok(text) = resp.text().await {
|
|
match serde_json::from_str::<Vec<PostInstallStep>>(&text) {
|
|
Ok(steps) => {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install",
|
|
&format!(
|
|
"Successfully fetched remote config with {} steps.",
|
|
steps.len()
|
|
),
|
|
"info",
|
|
);
|
|
final_steps = Some(steps);
|
|
}
|
|
Err(e) => {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install Error",
|
|
&format!("JSON Parse Error: {}. Raw Content: {}", e, text),
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install Error",
|
|
&format!("Remote config HTTP Error: {}", resp.status()),
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(steps) = final_steps {
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"running",
|
|
"configuring",
|
|
1.0,
|
|
task_version.clone(),
|
|
Some("Starting post-installation configuration".to_string()),
|
|
);
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install",
|
|
"Starting post-installation configuration...",
|
|
"info",
|
|
);
|
|
if let Err(e) = execute_post_install(&runtime_handle, &log_id, steps).await {
|
|
emit_log(&runtime_handle, &log_id, "Post-Install Error", &e, "error");
|
|
} else {
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Post-Install",
|
|
"Post-installation configuration completed.",
|
|
"success",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
status_result
|
|
}
|
|
Err(e) => {
|
|
emit_log(&runtime_handle, &log_id, "Fatal Error", &e.to_string(), "error");
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
"failed",
|
|
"spawn_error",
|
|
0.0,
|
|
task_version.clone(),
|
|
Some(e.to_string()),
|
|
);
|
|
"error"
|
|
}
|
|
};
|
|
|
|
emit_task_event(
|
|
&runtime_handle,
|
|
&log_id,
|
|
&task_id,
|
|
"install",
|
|
if status_result == "success" { "completed" } else { "failed" },
|
|
status_result,
|
|
1.0,
|
|
task_version.clone(),
|
|
Some(format!("Execution finished: {}", status_result)),
|
|
);
|
|
emit_log(
|
|
&runtime_handle,
|
|
&log_id,
|
|
"Result",
|
|
&format!("Execution finished: {}", status_result),
|
|
if status_result == "success" {
|
|
"success"
|
|
} else {
|
|
"error"
|
|
},
|
|
);
|
|
|
|
if let Some(path) = temp_manifest_path {
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
}
|
|
});
|
|
|
|
AppState { install_tx: tx, app_handle: handle }
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn install_software(
|
|
task: InstallTask,
|
|
state: State<'_, AppState>,
|
|
) -> Result<(), String> {
|
|
let log_id = format!("install-{}", task.id);
|
|
emit_task_event(
|
|
&state.app_handle,
|
|
&log_id,
|
|
&task.id,
|
|
"install",
|
|
"queued",
|
|
"queued",
|
|
0.0,
|
|
task.version.clone(),
|
|
None,
|
|
);
|
|
state.install_tx.send(task).await.map_err(|e| e.to_string())
|
|
}
|
|
|
|
fn spawn_install_stream_reader<R: Read + Send + 'static>(
|
|
reader: R,
|
|
handle: AppHandle,
|
|
log_id: String,
|
|
task_id: String,
|
|
stream_name: &'static str,
|
|
perc_re: Regex,
|
|
size_re: Regex,
|
|
) -> thread::JoinHandle<()> {
|
|
thread::spawn(move || {
|
|
let reader = BufReader::new(reader);
|
|
for line_res in reader.split(b'\r') {
|
|
if let Ok(line_bytes) = line_res {
|
|
let line_str = String::from_utf8_lossy(&line_bytes).to_string();
|
|
let clean_line = line_str.trim();
|
|
if clean_line.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
if stream_name == "stdout" {
|
|
let mut is_progress = false;
|
|
if let Some(caps) = perc_re.captures(clean_line) {
|
|
if let Ok(p_val) = caps[1].parse::<f32>() {
|
|
let _ = handle.emit(
|
|
"install-status",
|
|
InstallProgress {
|
|
id: task_id.clone(),
|
|
status: "installing".to_string(),
|
|
progress: p_val / 100.0,
|
|
},
|
|
);
|
|
let _ = handle.emit(
|
|
"task-event",
|
|
TaskEventPayload {
|
|
task_id: log_id.clone(),
|
|
software_id: task_id.clone(),
|
|
task_type: "install".to_string(),
|
|
status: "running".to_string(),
|
|
stage: "installing".to_string(),
|
|
progress: p_val / 100.0,
|
|
target_version: None,
|
|
message: None,
|
|
},
|
|
);
|
|
is_progress = true;
|
|
}
|
|
} else if let Some(caps) = size_re.captures(clean_line) {
|
|
let current = caps[1].parse::<f32>().unwrap_or(0.0);
|
|
let total = caps[2].parse::<f32>().unwrap_or(1.0);
|
|
if total > 0.0 {
|
|
let _ = handle.emit(
|
|
"install-status",
|
|
InstallProgress {
|
|
id: task_id.clone(),
|
|
status: "installing".to_string(),
|
|
progress: (current / total).min(1.0),
|
|
},
|
|
);
|
|
let _ = handle.emit(
|
|
"task-event",
|
|
TaskEventPayload {
|
|
task_id: log_id.clone(),
|
|
software_id: task_id.clone(),
|
|
task_type: "install".to_string(),
|
|
status: "running".to_string(),
|
|
stage: "installing".to_string(),
|
|
progress: (current / total).min(1.0),
|
|
target_version: None,
|
|
message: None,
|
|
},
|
|
);
|
|
is_progress = true;
|
|
}
|
|
}
|
|
|
|
if !is_progress && clean_line.chars().count() > 1 {
|
|
emit_log(&handle, &log_id, "", clean_line, "info");
|
|
}
|
|
} else {
|
|
emit_log(&handle, &log_id, stream_name, clean_line, "error");
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn emit_task_event(
|
|
handle: &AppHandle,
|
|
task_id: &str,
|
|
software_id: &str,
|
|
task_type: &str,
|
|
status: &str,
|
|
stage: &str,
|
|
progress: f32,
|
|
target_version: Option<String>,
|
|
message: Option<String>,
|
|
) {
|
|
let _ = handle.emit(
|
|
"task-event",
|
|
TaskEventPayload {
|
|
task_id: task_id.to_string(),
|
|
software_id: software_id.to_string(),
|
|
task_type: task_type.to_string(),
|
|
status: status.to_string(),
|
|
stage: stage.to_string(),
|
|
progress,
|
|
target_version: target_version.clone(),
|
|
message,
|
|
},
|
|
);
|
|
|
|
let legacy_status = match status {
|
|
"queued" => "pending".to_string(),
|
|
"completed" => "success".to_string(),
|
|
"failed" => "error".to_string(),
|
|
_ => stage.to_string(),
|
|
};
|
|
|
|
let _ = handle.emit(
|
|
"install-status",
|
|
InstallProgress {
|
|
id: software_id.to_string(),
|
|
status: legacy_status,
|
|
progress,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn expand_win_path(path: &str) -> PathBuf {
|
|
let mut expanded = path.to_string();
|
|
let env_vars = [
|
|
"AppData",
|
|
"LocalAppData",
|
|
"ProgramData",
|
|
"SystemRoot",
|
|
"SystemDrive",
|
|
"TEMP",
|
|
"USERPROFILE",
|
|
"HOMEDRIVE",
|
|
"HOMEPATH",
|
|
];
|
|
|
|
for var in env_vars {
|
|
let re = Regex::new(&format!(r"(?i)%{}%", var)).unwrap();
|
|
if re.is_match(&expanded) {
|
|
if let Ok(val) = std::env::var(var) {
|
|
expanded = re.replace_all(&expanded, val.as_str()).to_string();
|
|
}
|
|
}
|
|
}
|
|
PathBuf::from(expanded)
|
|
}
|
|
|
|
async fn execute_post_install(
|
|
handle: &AppHandle,
|
|
log_id: &str,
|
|
steps: Vec<PostInstallStep>,
|
|
) -> Result<(), String> {
|
|
let steps_len = steps.len();
|
|
for (i, step) in steps.into_iter().enumerate() {
|
|
let step_prefix = format!("Step {}/{}: ", i + 1, steps_len);
|
|
|
|
let delay = match &step {
|
|
PostInstallStep::RegistryBatch { delay_ms, .. } => *delay_ms,
|
|
PostInstallStep::FileCopy { delay_ms, .. } => *delay_ms,
|
|
PostInstallStep::FileDelete { delay_ms, .. } => *delay_ms,
|
|
PostInstallStep::Command { delay_ms, .. } => *delay_ms,
|
|
};
|
|
|
|
match step {
|
|
PostInstallStep::RegistryBatch {
|
|
root,
|
|
base_path,
|
|
values,
|
|
..
|
|
} => {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Registry Update",
|
|
&format!("{}Applying batch registry settings to {}...", step_prefix, base_path),
|
|
"info",
|
|
);
|
|
let hive = match root.as_str() {
|
|
"HKCU" => RegKey::predef(HKEY_CURRENT_USER),
|
|
"HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE),
|
|
_ => {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Registry Error",
|
|
&format!("Unknown root hive: {}", root),
|
|
"error",
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match hive.create_subkey(&base_path) {
|
|
Ok((key, _)) => {
|
|
for (name, val) in values {
|
|
let res = match val.v_type.as_str() {
|
|
"String" => key.set_value(&name, &val.data.as_str().unwrap_or_default()),
|
|
"Dword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0) as u32)),
|
|
"Qword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0))),
|
|
"MultiString" => {
|
|
let strings: Vec<String> = val
|
|
.data
|
|
.as_array()
|
|
.map(|a| {
|
|
a.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
key.set_value(&name, &strings)
|
|
}
|
|
"ExpandString" => {
|
|
key.set_value(&name, &val.data.as_str().unwrap_or_default())
|
|
}
|
|
"Delete" => key.delete_value(&name),
|
|
_ => Err(std::io::Error::other("Unsupported type")),
|
|
};
|
|
if let Err(e) = res {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Registry Error",
|
|
&format!("Failed to apply {}: {}", name, e),
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Registry Error",
|
|
&format!("Failed to create/open key {}: {}", base_path, e),
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
PostInstallStep::FileCopy { src, dest, .. } => {
|
|
let dest_path = expand_win_path(&dest);
|
|
let src_is_url = src.starts_with("http://") || src.starts_with("https://");
|
|
|
|
if src_is_url {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Download",
|
|
&format!("{}Downloading {:?} to {:?}...", step_prefix, src, dest_path),
|
|
"info",
|
|
);
|
|
let client = reqwest::Client::new();
|
|
match client
|
|
.get(&src)
|
|
.timeout(std::time::Duration::from_secs(60))
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(resp) if resp.status().is_success() => {
|
|
if let Ok(bytes) = resp.bytes().await {
|
|
if let Some(parent) = dest_path.parent() {
|
|
let _ = fs::create_dir_all(parent);
|
|
}
|
|
if let Err(e) = fs::write(&dest_path, bytes) {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Error",
|
|
&format!("Failed to write to {:?}: {}", dest_path, e),
|
|
"error",
|
|
);
|
|
} else {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Success",
|
|
"File downloaded and saved successfully.",
|
|
"success",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(resp) => emit_log(
|
|
handle,
|
|
log_id,
|
|
"Download Error",
|
|
&format!("HTTP Status: {}", resp.status()),
|
|
"error",
|
|
),
|
|
Err(e) => emit_log(handle, log_id, "Download Error", &e.to_string(), "error"),
|
|
}
|
|
} else {
|
|
let src_path = expand_win_path(&src);
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Copy",
|
|
&format!("{}Copying {:?} to {:?}...", step_prefix, src_path, dest_path),
|
|
"info",
|
|
);
|
|
if let Some(parent) = dest_path.parent() {
|
|
let _ = fs::create_dir_all(parent);
|
|
}
|
|
if let Err(e) = fs::copy(&src_path, &dest_path) {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Error",
|
|
&format!("Failed to copy file: {}", e),
|
|
"error",
|
|
);
|
|
} else {
|
|
emit_log(handle, log_id, "Success", "File copied successfully.", "success");
|
|
}
|
|
}
|
|
}
|
|
PostInstallStep::FileDelete { path, .. } => {
|
|
let full_path = expand_win_path(&path);
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Delete",
|
|
&format!("{}Deleting {:?}...", step_prefix, full_path),
|
|
"info",
|
|
);
|
|
if full_path.exists() {
|
|
if let Err(e) = fs::remove_file(&full_path) {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"File Error",
|
|
&format!("Failed to delete file: {}", e),
|
|
"error",
|
|
);
|
|
} else {
|
|
emit_log(handle, log_id, "Success", "File deleted successfully.", "success");
|
|
}
|
|
} else {
|
|
emit_log(handle, log_id, "File Info", "File does not exist, skipping.", "info");
|
|
}
|
|
}
|
|
PostInstallStep::Command { run, .. } => {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Command Execution",
|
|
&format!("{}Executing: {}", step_prefix, run),
|
|
"info",
|
|
);
|
|
let output = Command::new("cmd")
|
|
.arg("/C")
|
|
.raw_arg(&run)
|
|
.creation_flags(0x08000000)
|
|
.output();
|
|
|
|
match output {
|
|
Ok(out) => {
|
|
if !out.status.success() {
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
emit_log(handle, log_id, "Command Failed", &err, "error");
|
|
} else {
|
|
emit_log(handle, log_id, "Success", "Command executed successfully.", "success");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
emit_log(handle, log_id, "Execution Error", &e.to_string(), "error");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ms) = delay {
|
|
if ms > 0 {
|
|
emit_log(
|
|
handle,
|
|
log_id,
|
|
"Post-Install",
|
|
&format!("Waiting for {}ms...", ms),
|
|
"info",
|
|
);
|
|
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|