Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ jobs:

- name: Build Rust targets for analysis
if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
env:
TOUCHAI_OPTIONAL_BUNDLED_DOWNLOAD: '1'
run: cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets --profile ci-check

- name: Analyze
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ clipboard-rs = "0.3.4"
html5gum = { version = "0.8.3", default-features = false }
sha2 = "0.10"
velopack = { version = "=0.0.1589-ga2c5a97", features = ["public-utils"] }
uuid = { version = "1.23.1", features = ["v4"] }
zip = { version = "2.4", default-features = false, features = ["deflate"] }

[dev-dependencies]
tempfile = "3"
Expand Down Expand Up @@ -116,6 +118,7 @@ windows = { version = "0.58", features = [
] }
webview2-com = "0.38.0"
windows-core = "0.61.2"
winreg = "0.10.1"

[target.'cfg(target_os = "macos")'.dependencies]
block2 = "0.6"
Expand Down
75 changes: 69 additions & 6 deletions apps/desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ use std::{
fs,
io::{Cursor, Read},
path::{Path, PathBuf},
thread,
time::Duration,
};

use quote::ToTokens;
use tauri_codegen::embedded_assets::{AssetOptions, EmbeddedAssets};

const BUNDLED_DOWNLOAD_MAX_ATTEMPTS: usize = 6;
const BUNDLED_DOWNLOAD_RETRY_BASE_DELAY_MS: u64 = 1_500;
const BUNDLED_DOWNLOAD_RETRY_MAX_DELAY_MS: u64 = 30_000;

#[derive(Debug, Deserialize)]
struct BundledManifest {
version: String,
Expand Down Expand Up @@ -175,7 +181,17 @@ fn prepare_bundled(name: &str) -> Result<(), Box<dyn std::error::Error>> {
.join(&target_triple);

if let Some(target) = manifest.targets.get(&target_triple) {
let binary_path = materialize_binary(name, target, &cache_dir)?;
let binary_path = match materialize_binary(name, target, &cache_dir) {
Ok(binary_path) => binary_path,
Err(error) if bundled_downloads_are_optional() => {
println!(
"cargo:warning={name}: bundled binary unavailable in optional mode; generating empty asset: {error}"
);
generate_empty_asset_module(name, &out_dir)?;
return Ok(());
}
Err(error) => return Err(error),
};
let binary_hash = if !target.binary_digest.is_empty() {
target.binary_digest.clone()
} else {
Expand Down Expand Up @@ -232,11 +248,7 @@ fn materialize_binary(
}
}

let response = ureq::get(&target.url).call()?;
let bytes = response
.into_reader()
.bytes()
.collect::<Result<Vec<_>, _>>()?;
let bytes = download_bundled_archive(name, &target.url)?;
if bytes.len() as u64 != target.size {
return Err(format!("{name}: download size mismatch for {}", target.url).into());
}
Expand All @@ -251,6 +263,57 @@ fn materialize_binary(
Ok(binary_path)
}

fn download_bundled_archive(name: &str, url: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut last_error = None;

for attempt in 1..=BUNDLED_DOWNLOAD_MAX_ATTEMPTS {
match download_bundled_archive_once(url) {
Ok(bytes) => return Ok(bytes),
Err(error) => {
last_error = Some(error);
if attempt < BUNDLED_DOWNLOAD_MAX_ATTEMPTS {
let delay_ms = bundled_download_retry_delay_ms(attempt);
println!(
"cargo:warning={name}: bundled download attempt {attempt}/{BUNDLED_DOWNLOAD_MAX_ATTEMPTS} failed for {url}; retrying in {delay_ms}ms"
);
thread::sleep(Duration::from_millis(delay_ms));
}
}
}
}

Err(format!(
"{name}: failed to download bundled archive {url} after {BUNDLED_DOWNLOAD_MAX_ATTEMPTS} attempts: {}",
last_error.unwrap_or_else(|| "unknown error".to_string())
)
.into())
}

fn bundled_downloads_are_optional() -> bool {
matches!(
std::env::var("TOUCHAI_OPTIONAL_BUNDLED_DOWNLOAD"),
Ok(value) if value == "1" || value.eq_ignore_ascii_case("true")
)
}

fn bundled_download_retry_delay_ms(attempt: usize) -> u64 {
let exponent = attempt.saturating_sub(1).min(4) as u32;
BUNDLED_DOWNLOAD_RETRY_BASE_DELAY_MS
.saturating_mul(1_u64 << exponent)
.min(BUNDLED_DOWNLOAD_RETRY_MAX_DELAY_MS)
}

fn download_bundled_archive_once(url: &str) -> Result<Vec<u8>, String> {
let response = ureq::get(url)
.call()
.map_err(|error| format!("request failed: {error}"))?;
response
.into_reader()
.bytes()
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("response body read failed: {error}"))
}

fn generate_asset_module(
name: &str,
out_dir: &Path,
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/src-tauri/src/commands/app_use.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2026. Qian Cheng. Licensed under GPL v3

use crate::core::app_use::{
AppUseActRequest, AppUseActResponse, AppUseAuthorizeActRequest, AppUseAuthorizeActResponse,
AppUseObserveRequest, AppUseObserveResponse, AppUseRuntime, AppUseSessionRequest,
AppUseSessionResponse,
};
use tauri::State;

#[tauri::command]
pub fn app_use_session(
request: AppUseSessionRequest,
runtime: State<'_, AppUseRuntime>,
) -> Result<AppUseSessionResponse, String> {
Ok(runtime.session(request))
}

#[tauri::command]
pub fn app_use_observe(
request: AppUseObserveRequest,
runtime: State<'_, AppUseRuntime>,
) -> Result<AppUseObserveResponse, String> {
Ok(runtime.observe(request))
}

#[tauri::command]
pub fn app_use_authorize_act(
request: AppUseAuthorizeActRequest,
runtime: State<'_, AppUseRuntime>,
) -> Result<AppUseAuthorizeActResponse, String> {
Ok(runtime.authorize_act(request))
}

#[tauri::command]
pub fn app_use_act(
request: AppUseActRequest,
runtime: State<'_, AppUseRuntime>,
) -> Result<AppUseActResponse, String> {
Ok(runtime.act(request))
}
5 changes: 5 additions & 0 deletions apps/desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2026. 千诚. Licensed under GPL v3.

//! 命令入口模块。
pub mod app_use;
pub mod autostart;
pub mod built_in_tools;
pub mod clipboard;
Expand Down Expand Up @@ -76,5 +77,9 @@ pub fn invoke_handler<R: tauri::Runtime>(
updater::updater_check_for_updates,
updater::updater_download_update,
updater::updater_install_update,
app_use::app_use_session,
app_use::app_use_observe,
app_use::app_use_authorize_act,
app_use::app_use_act,
]
}
Loading
Loading