fix: robust payload download, multi-endpoint check and auto-recovery

This commit is contained in:
2026-10-28 20:19:40 -07:00
parent 9b9d0dfc49
commit a4b3a77153
4 changed files with 159 additions and 37 deletions

View File

@@ -64,4 +64,9 @@ Force-stop and clear app data:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell am force-stop com.vyla.go
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell pm clear com.vyla.go
```
```
Fix emulator DNS timeouts (if emulator gets network timeouts connecting to GitLab):
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell settings put global private_dns_mode off
```

1
src-tauri/Cargo.lock generated
View File

@@ -4593,6 +4593,7 @@ dependencies = [
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tokio",
"urlencoding",
"zip",
]

View File

@@ -23,3 +23,4 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
zip = { version = "2.2", default-features = false, features = ["deflate"] }
futures-util = "0.3"
urlencoding = "2.1"
tokio = { version = "1", features = ["time"] }

View File

@@ -352,44 +352,151 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
let base_dir = get_app_dir(&app);
std::fs::create_dir_all(&base_dir).map_err(|e| format!("Storage dir error: {}", e))?;
let marker = base_dir.join(".installed");
let frontend_index = base_dir.join("frontend").join("index.html");
if frontend_index.exists() {
let server_script = base_dir.join("player").join("server.js");
if marker.exists() && frontend_index.exists() && server_script.exists() {
android_log("App already installed, launching...");
apply_patches(&base_dir);
spawn_service_manager(&base_dir);
update_ui(&window, "Launching Vyla Go...", Some(100));
return Ok(base_dir);
}
update_ui(&window, "Connecting to GitLab...", Some(10));
// 1. Check if a valid local archive already exists on device
let local_payload_candidates = [
base_dir.join("update-payload.zip"),
base_dir.join("temp-payload.zip"),
PathBuf::from("/sdcard/Download/update-payload.zip"),
PathBuf::from("/data/local/tmp/update-payload.zip"),
];
for candidate in &local_payload_candidates {
if candidate.exists() {
if let Ok(meta) = std::fs::metadata(candidate) {
if meta.len() > 10_000_000 {
if let Ok(f) = File::open(candidate) {
if let Ok(archive) = zip::ZipArchive::new(f) {
if archive.len() > 0 {
android_log(&format!("Found valid local payload: {}", candidate.display()));
update_ui(&window, "Unpacking local package...", Some(80));
let extract_dir = base_dir.clone();
let candidate_path = candidate.clone();
let extract_ok = tauri::async_runtime::spawn_blocking(move || -> Result<(), String> {
let zip_file = File::open(&candidate_path).map_err(|e| format!("Open zip error: {}", e))?;
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
apply_patches(&extract_dir);
Ok(())
})
.await
.map_err(|e| e.to_string())?;
if extract_ok.is_ok() {
spawn_service_manager(&base_dir);
update_ui(&window, "Launching Vyla Go...", Some(100));
return Ok(base_dir);
}
}
}
}
}
}
}
}
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Android) VylaGo/1.0")
.connect_timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(15))
.timeout(std::time::Duration::from_secs(180))
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.map_err(|e| format!("Client error: {}", e))?;
let releases_url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases";
let res = client.get(releases_url).send().await.map_err(|e| format!("Release check failed: {}", e))?;
let json: serde_json::Value = res.json().await.map_err(|e| format!("JSON error: {}", e))?;
let release_endpoints = [
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases",
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases",
];
let download_url = json.get(0)
.and_then(|r| r.get("assets"))
.and_then(|a| a.get("links"))
.and_then(|l| l.as_array())
.and_then(|links| {
links.iter().find(|item| {
item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip")
})
})
.and_then(|asset| asset.get("url"))
.and_then(|u| u.as_str())
.ok_or_else(|| "Could not locate update-payload.zip link in release assets".to_string())?;
let mut download_url: Option<String> = None;
let mut last_error = String::new();
const MAX_RETRIES: u32 = 4;
for attempt in 1..=MAX_RETRIES {
let msg = if attempt == 1 {
"Connecting to GitLab...".to_string()
} else {
format!("Connecting to GitLab (attempt {}/{})...", attempt, MAX_RETRIES)
};
update_ui(&window, &msg, Some(10 + (attempt - 1) * 3));
for endpoint in &release_endpoints {
android_log(&format!("Checking endpoint: {} (attempt {})", endpoint, attempt));
match client.get(*endpoint).send().await {
Ok(res) => {
if res.status().is_success() {
if let Ok(json) = res.json::<serde_json::Value>().await {
if let Some(releases) = json.as_array() {
for rel in releases {
if let Some(links) = rel.get("assets").and_then(|a| a.get("links")).and_then(|l| l.as_array()) {
for item in links {
if item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip") {
if let Some(url) = item.get("url").and_then(|u| u.as_str()) {
download_url = Some(url.to_string());
break;
}
}
}
}
if download_url.is_some() {
break;
}
}
}
}
} else {
last_error = format!("HTTP {}", res.status());
}
}
Err(e) => {
android_log(&format!("Release check failed for {}: {}", endpoint, e));
last_error = format!("{}", e);
}
}
if download_url.is_some() {
break;
}
}
if download_url.is_some() {
break;
}
if attempt < MAX_RETRIES {
let backoff = attempt * 2;
update_ui(
&window,
&format!("Connection pending, retrying in {}s...", backoff),
Some(10 + attempt * 3),
);
tokio::time::sleep(std::time::Duration::from_secs(backoff as u64)).await;
}
}
let download_url = download_url.ok_or_else(|| {
format!(
"Could not connect to release server ({})",
if last_error.is_empty() { "timeout" } else { &last_error }
)
})?;
update_ui(&window, "Downloading update payload...", Some(25));
android_log(&format!("Downloading from: {}", download_url));
let response = client.get(download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?; android_log(&format!("Response status: {}", response.status()));
let response = client.get(&download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?;
android_log(&format!("Response status: {}", response.status()));
let total_size = response.content_length().unwrap_or(0) as usize;
let temp_zip_path = base_dir.join("temp-payload.zip");
@@ -408,7 +515,7 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
downloaded += chunk.len();
let percent = if total_size > 0 {
10 + ((downloaded as f64 / total_size as f64) * 75.0) as u32
25 + ((downloaded as f64 / total_size as f64) * 65.0) as u32
} else {
35
};
@@ -431,7 +538,8 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
let zip_file = File::open(&zip_path_clone).map_err(|e| format!("Open zip error: {}", e))?;
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
let _ = std::fs::remove_file(zip_path_clone);
let _ = std::fs::remove_file(&zip_path_clone);
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
apply_patches(&extract_dir);
Ok(())
})
@@ -554,25 +662,32 @@ pub fn run() {
}
tauri::async_runtime::spawn(async move {
std::thread::sleep(std::time::Duration::from_millis(400));
std::thread::sleep(std::time::Duration::from_millis(600));
if let Some(window) = handle.get_webview_window("main") {
update_ui(&window, "Initializing Vyla Go engine...", Some(5));
match download_and_extract_payload(handle.clone(), window.clone()).await {
Ok(_) => {
std::thread::sleep(std::time::Duration::from_millis(400));
let _ = window.eval(&format!(
"{}; window.location.replace('http://vyla.localhost/frontend/index.html');",
ANDROID_VIDEO_FIX_SCRIPT
));
}
Err(err) => {
update_ui(
&window,
&format!("Error: {}", err),
Some(0),
);
loop {
match download_and_extract_payload(handle.clone(), window.clone()).await {
Ok(_) => {
std::thread::sleep(std::time::Duration::from_millis(400));
let _ = window.eval(&format!(
"{}; window.location.replace('http://vyla.localhost/frontend/index.html');",
ANDROID_VIDEO_FIX_SCRIPT
));
break;
}
Err(err) => {
android_log(&format!("Installer error: {}", err));
for remaining in (1..=8).rev() {
update_ui(
&window,
&format!("Error: {}. Retrying in {}s...", err, remaining),
Some(0),
);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}
}
}