Files
openhuman/app/src-tauri/src/core_update.rs
T
Steven EnamakelandGitHub 99d61f93a7 feat(webui-messaging): multi-provider webview accounts, scanners, and chat runtime (#629)
* feat(accounts): implement accounts management with webview integration

- Added a new Accounts page for managing user accounts, including the ability to add and remove accounts.
- Introduced AddAccountModal for selecting account providers and initiating account setup.
- Implemented WebviewHost to display third-party web applications (e.g., WhatsApp Web) within the app.
- Enhanced routing to include a protected route for the Accounts page.
- Updated the BottomTabBar to include an Accounts tab for easy navigation.
- Integrated Redux for state management of accounts, messages, and logs, ensuring a seamless user experience.
- Updated dependencies in Cargo.lock to version 0.52.9 for compatibility.

This commit significantly enhances the application's functionality by allowing users to manage accounts directly within the app, improving overall user engagement and experience.

* refactor(accounts): clean up Accounts component layout and improve readability

- Removed unnecessary comments and simplified the structure of the Accounts component for better clarity.
- Adjusted the rendering logic to enhance the layout of the active account section, improving user experience.
- Reformatted text in the no accounts message for better readability.
- Streamlined the import statements by consolidating related imports, enhancing code organization.

* feat(accounts): enhance account management with new providers and routing updates

- Introduced support for additional account providers: Telegram, LinkedIn, Gmail, and Slack, expanding user options for account management.
- Updated routing to replace the old /conversations path with /chat, streamlining navigation and improving user experience.
- Refactored the App component to include an AppShell for better layout management, ensuring the bottom tab bar visibility aligns with the selected account.
- Enhanced the BottomTabBar component to reflect the new routing and account options, improving accessibility and usability.
- Implemented fullscreen logic for accounts, allowing for a more immersive experience when interacting with selected accounts.
- Added utility functions for managing fullscreen states and account provider icons, enhancing code organization and maintainability.

This commit significantly improves the application's account management capabilities, providing users with a more flexible and engaging experience.

* feat(cef): integrate Chromium Embedded Framework support and enhance webview functionality

- Added support for the Chromium Embedded Framework (CEF) as an alternative runtime, allowing for improved webview capabilities.
- Updated Cargo.toml to include new dependencies and features for CEF integration, ensuring compatibility with existing Tauri plugins.
- Enhanced the WhatsApp recipe to include ghost-text autocomplete functionality, improving user experience during message composition.
- Implemented WebSocket observation in the WhatsApp recipe to capture and forward relevant message frames, enhancing real-time interaction.
- Introduced user agent spoofing for specific providers to bypass fingerprinting checks, ensuring better compatibility with services like Slack and LinkedIn.
- Refactored various components to accommodate the new runtime and improve overall code organization and maintainability.

This commit significantly enhances the application's webview capabilities and user interaction with messaging services, providing a more robust and flexible experience.

* feat(cef): add development command for CEF and update configuration

- Introduced a new development command `dev:cef` in both package.json files to streamline the development process for the Chromium Embedded Framework (CEF).
- Updated Cargo.toml to include the `tauri/devtools` feature alongside `tauri/cef`, enhancing debugging capabilities.
- Modified tauri.conf.json to adjust visibility settings for the application window and refined the Content Security Policy (CSP) for improved security.
- Enhanced resource paths in tauri.conf.json to support recursive file inclusion for better resource management.
- Updated the Rust code to bypass macOS Keychain prompts when using CEF, improving user experience during development.

This commit enhances the development workflow for CEF integration, providing better tools and configurations for developers.

* fix(cef): update development command for CEF to include signing identity

- Modified the `dev:cef` command in package.json to include the `APPLE_SIGNING_IDENTITY` environment variable, enhancing the development process for CEF on macOS.
- This change improves the build process by ensuring proper code signing during development, streamlining the workflow for developers working with CEF integration.

* feat(cef): enhance development command for CEF with safe storage setup

- Updated the `dev:cef` command in package.json to include a call to a new script, `setup-chromium-safe-storage.sh`, which pre-seeds the "Chromium Safe Storage" keychain entry with a permissive ACL.
- Added the `setup-chromium-safe-storage.sh` script to ensure that CEF/Chromium can read the keychain entry without prompting, improving the development experience on macOS.
- This change streamlines the setup process for developers working with CEF integration, ensuring a smoother workflow.

* feat(whatsapp): enhance message ingestion and IndexedDB integration

- Introduced a new `IngestMessage` interface to standardize message structure for WhatsApp.
- Updated `IngestPayload` to include additional fields for better message handling, including `provider`, `chatId`, and `day`.
- Implemented a new function `persistWhatsappChatDay` to handle the ingestion of chat messages by day, improving data organization and retrieval.
- Enhanced the WhatsApp recipe to utilize IndexedDB for direct data access, eliminating the need for DOM scraping and improving performance.
- Updated the Tauri configuration to enable development tools for easier debugging of webview accounts.

This commit significantly improves the application's ability to manage and ingest WhatsApp messages, providing a more robust and efficient user experience.

* feat(cdp): integrate IndexedDB scanner for WhatsApp via Chrome DevTools Protocol

- Added a new module for scanning IndexedDB using the Chrome DevTools Protocol (CDP), enabling direct access to WhatsApp data without DOM scraping.
- Implemented a scanner that communicates with the embedded CEF instance to read and decrypt messages stored in IndexedDB.
- Updated the Tauri application to manage the new scanner, ensuring it operates seamlessly with existing webview accounts.
- Enhanced the Cargo.toml and Cargo.lock files to include necessary dependencies such as `tokio-tungstenite` and `futures-util` for asynchronous operations.
- Refactored the WhatsApp recipe to utilize the new scanning capabilities, improving performance and data handling.

This commit significantly enhances the application's ability to interact with WhatsApp's IndexedDB, providing a more efficient and robust user experience.

* feat(cdp): enhance message diagnostics in IndexedDB scanner

- Updated the ScanSnapshot struct to include new fields for message diagnostics: `messageKeyUnion`, `messageTypeBreakdown`, and `sampleByType`, providing a comprehensive overview of message structures and types.
- Modified the scanner logic to capture and log detailed information about message types and their shapes, improving debugging capabilities.
- Refactored the JavaScript scanner to aggregate message key signatures and counts, enhancing the analysis of message records.

This commit significantly improves the application's ability to analyze and log message data from WhatsApp's IndexedDB, facilitating better debugging and data handling.

* feat(cdp): implement fast DOM scraping and crypto key extraction for WhatsApp

- Introduced a new fast-tick DOM scraping mechanism to extract rendered WhatsApp message bodies, enabling near real-time message updates without relying on IndexedDB.
- Added scripts for capturing and logging CryptoKey operations within WhatsApp's workers, allowing for better analysis of key derivations and decryptions.
- Enhanced the CDP scanner to interleave fast DOM scans with full IndexedDB scans, optimizing data retrieval and reducing UI spamming during idle periods.
- Updated the ScanSnapshot struct to include new fields for DOM-scraped messages and crypto operation statistics, improving the overall diagnostic capabilities of the application.

This commit significantly enhances the application's ability to interact with WhatsApp's messaging system, providing a more efficient and responsive user experience.

* feat(whatsapp): replace cdp_indexeddb with whatsapp_scanner for enhanced message handling

- Replaced the `cdp_indexeddb` module with `whatsapp_scanner` to streamline the scanning process for WhatsApp messages.
- Updated the application to manage the new `ScannerRegistry` for WhatsApp, improving the integration with the Chrome DevTools Protocol.
- Introduced new scripts for fast DOM scraping and full IndexedDB scanning, optimizing data retrieval and enhancing real-time message updates.
- Added a new `dom_scan.js` for efficient extraction of rendered message bodies directly from the DOM, reducing reliance on IndexedDB.
- Enhanced the `ScanSnapshot` struct to accommodate new fields for DOM-scraped messages, improving diagnostic capabilities.

This commit significantly improves the application's ability to interact with WhatsApp, providing a more efficient and responsive user experience.

* refactor(whatsapp): replace JavaScript DOM scanning with Rust-based DOM snapshot

- Removed the `dom_scan.js` script and replaced it with a new Rust module `dom_snapshot.rs` that captures DOM snapshots directly via the Chrome DevTools Protocol, enhancing performance and reliability.
- Introduced a new `idb.rs` module for scanning WhatsApp's IndexedDB, streamlining data retrieval and improving integration with the Rust backend.
- Updated the `ScanSnapshot` struct to accommodate changes in data handling, ensuring compatibility with the new scanning methods.
- Enhanced overall message handling capabilities, providing a more efficient and responsive user experience.

This commit significantly improves the application's ability to interact with WhatsApp, leveraging Rust for better performance and reducing reliance on JavaScript for DOM operations.

* feat(docs): add webview integration playbook for third-party messaging

- Introduced a comprehensive playbook detailing the process for integrating third-party webviews (e.g., Instagram, Messenger) into the application.
- Documented architecture, workflow, and best practices for building and debugging new integrations, leveraging Rust and Chrome DevTools Protocol.
- Included step-by-step instructions for setting up scanners, monitoring logs, and optimizing message handling, ensuring a streamlined development experience for future integrations.

This addition enhances the documentation, providing developers with a clear guide to implement and maintain webview integrations effectively.

* docs(webview): improve table formatting for clarity

- Enhanced the formatting of tables in the webview integration playbook to improve readability and consistency.
- Adjusted column headers and alignment for better presentation of job intervals and costs, ensuring clearer communication of scanning processes and common pitfalls.

This update aims to provide a more user-friendly documentation experience for developers integrating third-party webviews.

* feat(slack): integrate Slack scanner for message extraction and management

- Updated the OpenHuman package version to 0.52.15 in both Cargo.lock files.
- Introduced a new Slack scanner module to extract messages, users, and channels from Slack's IndexedDB using the Chrome DevTools Protocol.
- Added functionality to manage Slack accounts within the application, allowing for automatic opening of Slack webviews based on environment variables.
- Enhanced the existing webview account management to support Slack integration, ensuring seamless interaction with the Slack API.

This commit significantly improves the application's ability to interact with Slack, providing a robust framework for message handling and account management.

* fix(slack): one-doc-per-channel + omit CDP indexName param

CEF 146's IndexedDB.requestData rejects `indexName: ""` with "Could not
get index"; the CDP spec says empty string means the primary-key index
but this backend only accepts the field unset. Omit it entirely so the
Slack Redux-persist dump actually comes back.

Also switch memory grouping from (channel, day) → channel. Each Slack
channel is now one long-running memory doc keyed by channel name
(e.g. `general`, `team-product`, `elvin516`), falling back to channel
id for non-slug names. Every transcript line carries its own
`YYYY-MM-DD HH:MM` stamp and the header records the full date range.

`infer_team_id` updated to Slack's real DB naming pattern
`objectStore-<TEAM>-<USER>` (not `ReduxPersistIDB:` as initially
assumed).

* fix(onboarding): update navigation and test descriptions in OnboardingOverlay tests

- Changed the navigation path from '/conversations' to '/chat' in the OnboardingOverlay tests to reflect the updated routing logic.
- Updated test descriptions for clarity, ensuring they accurately describe the functionality being tested.

These changes enhance the accuracy and readability of the onboarding tests, aligning them with the current application flow.

* fix(conversations): add return statement to Conversations component

- Introduced a return statement in the Conversations component to ensure proper rendering of the sidebar or page variant.
- This change enhances the component's functionality by ensuring it returns the expected JSX structure.

These modifications improve the overall structure and behavior of the Conversations component.

* feat(accounts): add Discord integration and enhance AddAccountModal

- Introduced Discord as a new account provider, including its icon and service details.
- Updated the AddAccountModal to filter out already connected providers, improving user experience.
- Enhanced the UI to display a message when all providers are connected, ensuring clarity for users.
- Implemented context menu functionality for account management, allowing users to log out directly from the accounts list.

These changes expand the application's capabilities by integrating Discord and refining account management features.

* feat(discord): integrate Discord scanner for HTTP and WebSocket monitoring

- Added a new `discord_scanner` module to capture Discord API calls and WebSocket frames using the Chrome DevTools Protocol (CDP).
- Updated the `lib.rs` to manage the new Discord scanner alongside existing WhatsApp and Slack scanners.
- Enhanced the `webview_accounts` module to support Discord account management, including scanner registration and cleanup.

These changes expand the application's capabilities by enabling real-time monitoring of Discord interactions, enhancing user experience and functionality.

* feat(google-meet): integrate Google Meet as a new account provider

- Added Google Meet as a supported account provider, including its icon and service details.
- Updated the account management logic to handle Google Meet interactions, including recipe integration for call monitoring and notifications.
- Enhanced the UI to accommodate the new provider, ensuring a seamless user experience when managing accounts.

These changes expand the application's capabilities by integrating Google Meet, allowing users to join calls and receive notifications directly within the app.

* refactor(runtime): remove notification handling and composer autocomplete

- Eliminated the notification interception logic and associated functions, streamlining the runtime code.
- Removed composer autocomplete features, transferring responsibility for ghost-text overlays to the UI host.
- Updated comments to reflect the changes and clarify the remaining functionality.

These modifications simplify the runtime script, focusing on core features while delegating UI responsibilities.

* feat(google-meet): enhance Google Meet integration with lifecycle event handling

- Implemented lifecycle event handling for Google Meet, including events for call start, captions, and call end.
- Introduced in-memory storage for caption snapshots during meetings, allowing for the generation of markdown transcripts upon call completion.
- Added interfaces for payload structures related to Google Meet events, improving type safety and clarity in the codebase.
- Updated the webview account service to manage active meetings and flush transcripts to memory, ensuring a seamless user experience.

These changes significantly enhance the Google Meet integration, enabling real-time caption handling and transcript generation, thereby improving the overall functionality of the application.

* feat(cef): integrate CEF-based notification handling and update dependencies

- Added support for native OS notifications through the `tauri-runtime-cef` crate, enabling interception of browser notifications in embedded webviews.
- Introduced a new submodule for `tauri-cef` to manage CEF dependencies and facilitate notification handling.
- Updated the `.gitignore` to exclude CEF-related build artifacts and lock files.
- Removed the deprecated `notification_scanner` module, streamlining the codebase and focusing on the new CEF integration.
- Enhanced the `webview_accounts` module to register and manage CEF browser notifications, improving user experience with real-time alerts.

These changes significantly enhance the application's notification capabilities, leveraging CEF for a more integrated and responsive user experience.

* feat(cef): update CEF integration and dependency management

- Added `cef` and `tauri-runtime-cef` as dependencies to enhance CEF support.
- Updated `Cargo.toml` to reference `tauri-runtime-cef` from a local path, ensuring proper integration with the vendored CEF submodule.
- Removed direct Git references for Tauri packages, streamlining dependency management by using local paths.

These changes improve the application's CEF capabilities and simplify the dependency structure, facilitating better integration and maintenance.

* feat(browserscan): add BrowserScan as a new account provider with associated resources

- Introduced BrowserScan as a development-only account provider, including its icon and service details.
- Updated the account provider types and management logic to accommodate BrowserScan, enhancing the application's capabilities.
- Added a new recipe and manifest for BrowserScan, ensuring it integrates seamlessly into the existing webview account lifecycle.
- Enhanced the UI to display BrowserScan, providing users with a bot-detection sandbox for testing purposes.

These changes expand the application's functionality by integrating BrowserScan, allowing for improved testing and development workflows.

* feat(google-meet): enhance Google Meet integration with new transcript handling and session recovery

- Introduced a new service to handle Google Meet transcripts, enabling structured note extraction and proactive follow-up actions.
- Implemented session recovery logic to manage in-progress meetings when navigating away from the call.
- Updated the webview account service to log call events and captions, improving monitoring and debugging capabilities.
- Enhanced the Google Meet recipe to persist meeting state across navigations, ensuring seamless user experience.

These changes significantly improve the Google Meet integration, allowing for better management of meeting transcripts and user interactions.

* refactor(App): reorganize imports and clean up code structure

- Removed unused imports from App.tsx to streamline the code.
- Adjusted the import order for better readability and consistency.
- Enhanced the BottomTabBar component by simplifying the button rendering logic.
- Cleaned up the AddAccountModal component by consolidating prop destructuring.
- Improved formatting in various components for better code clarity.

These changes enhance code maintainability and readability across the application.

* update tauri

* feat(google-meet): improve caption handling and speaker identification logic

- Enhanced the logic for extracting speaker names from Google Meet rows, adding checks to filter out icon ligatures and irrelevant text.
- Updated the caption processing to better identify and score caption regions, ensuring more accurate transcript generation.
- Introduced new utility functions to differentiate between real captions and icon names, improving the overall reliability of the captioning feature.

These changes significantly enhance the accuracy and usability of the Google Meet integration, providing users with clearer and more relevant caption data.

* chore(dependencies): update Cargo.lock with new package versions and remove obsolete entries

- Bump OpenHuman version to 0.52.20.
- Add new dependencies: cef, futures-util, tauri-plugin-notification, tauri-runtime-cef, tokio-tungstenite, bzip2, clap, console, cookie_store, data-encoding, dioxus-debug-cell, document-features, download-cef, encode_unicode, filetime.
- Remove obsolete packages: alloc-no-stdlib, alloc-stdlib, brotli, brotli-decompressor, gdkx11, gdkx11-sys.
- Update existing dependencies to their latest versions where applicable.

* chore(dependencies): update submodule URLs and pin plugin versions

- Changed the tauri-cef submodule URL from SSH to HTTPS for consistency.
- Updated Cargo.toml to pin tauri plugins to a specific commit for reproducibility.
- Adjusted Cargo.lock to reflect the new plugin source format with pinned revisions.
- Modified the builder configuration in lib.rs to conditionally enable devtools in debug builds only, enhancing security in release builds.
- Updated webview_accounts to restrict devtools access based on build type, ensuring better control over webview inspection.

* feat(ui): enhance BottomTabBar and AddAccountModal interactions

- Added focus and blur event handlers to BottomTabBar for improved visibility management.
- Implemented keyboard accessibility in AddAccountModal by closing the modal on Escape key press and focusing the close button when opened.
- Updated Content Security Policy in tauri.conf.json for enhanced security.
- Improved Gmail recipe to use stable message IDs for better message tracking.
- Introduced account ID sanitization in webview_accounts to prevent path traversal vulnerabilities.

* fix(AddAccountModal): add missing import for improved functionality

* chore(dependencies): bump OpenHuman version to 0.52.20 in Cargo.lock
2026-04-17 15:55:50 -07:00

497 lines
16 KiB
Rust

//! Core sidecar version checking and auto-update logic.
//!
//! After the Tauri shell starts the core sidecar, it queries `core.version` via
//! JSON-RPC. If the running core is older than the minimum expected version, the
//! shell downloads the latest release from GitHub, stages it, kills the old
//! process, and restarts with the new binary.
use std::io::Write;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::core_process::CoreProcessHandle;
/// The minimum core version this Tauri build expects.
/// Bump this when the app depends on new core RPC methods.
pub const MINIMUM_CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
/// GitHub owner/repo for releases.
const GITHUB_OWNER: &str = "tinyhumansai";
const GITHUB_REPO: &str = "openhuman";
#[derive(Debug, Deserialize)]
struct RpcResponse {
result: Option<serde_json::Value>,
error: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct GitHubRelease {
tag_name: String,
assets: Vec<GitHubAsset>,
}
#[derive(Debug, Deserialize)]
struct GitHubAsset {
name: String,
browser_download_url: String,
}
/// Returned by `check_core_update` Tauri command.
#[derive(Debug, Clone, Serialize)]
pub struct CoreUpdateInfo {
pub running_version: String,
pub minimum_version: String,
/// True if running < minimum (compatibility issue).
pub outdated: bool,
/// Latest version on GitHub Releases (if fetch succeeded).
pub latest_version: Option<String>,
/// True if running < latest (newer release available).
pub update_available: bool,
}
/// Query the running core's version via JSON-RPC.
pub async fn query_core_version(rpc_url: &str) -> Result<String, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| format!("http client error: {e}"))?;
let body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "core.version",
"params": {}
});
let resp = client
.post(rpc_url)
.json(&body)
.send()
.await
.map_err(|e| format!("failed to query core.version: {e}"))?;
let rpc: RpcResponse = resp
.json()
.await
.map_err(|e| format!("failed to parse core.version response: {e}"))?;
if let Some(err) = rpc.error {
return Err(format!("core.version RPC error: {err}"));
}
let version = rpc
.result
.and_then(|v| v.get("version").and_then(|v| v.as_str()).map(String::from))
.ok_or_else(|| "core.version response missing 'version' field".to_string())?;
Ok(version)
}
/// Compare two version strings. Returns true if `running` is older than `target`.
pub fn is_outdated(running: &str, target: &str) -> bool {
let parse = |v: &str| -> Option<semver::Version> {
semver::Version::parse(v.trim_start_matches('v')).ok()
};
match (parse(running), parse(target)) {
(Some(r), Some(t)) => r < t,
_ => {
log::warn!("[core-update] could not parse versions running={running} target={target}");
false
}
}
}
/// Full check: query running version, compare against minimum AND latest GitHub release.
pub async fn check_full(rpc_url: &str) -> Result<CoreUpdateInfo, String> {
let running = query_core_version(rpc_url).await?;
let minimum = MINIMUM_CORE_VERSION;
let outdated = is_outdated(&running, minimum);
// Best-effort fetch of latest release — don't fail the whole check if GitHub is unreachable.
let (latest_version, update_available) = match fetch_latest_release().await {
Ok(release) => {
let latest = release.tag_name.trim_start_matches('v').to_string();
let available = is_outdated(&running, &latest);
(Some(latest), available)
}
Err(e) => {
log::warn!("[core-update] could not fetch latest release: {e}");
(None, false)
}
};
Ok(CoreUpdateInfo {
running_version: running,
minimum_version: minimum.to_string(),
outdated,
latest_version,
update_available,
})
}
/// Build the platform triple for asset matching.
fn platform_triple() -> &'static str {
#[cfg(all(target_arch = "x86_64", target_os = "macos"))]
{
"x86_64-apple-darwin"
}
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
{
"aarch64-apple-darwin"
}
#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
{
"x86_64-unknown-linux-gnu"
}
#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
{
"aarch64-unknown-linux-gnu"
}
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
{
"x86_64-pc-windows-msvc"
}
#[cfg(all(target_arch = "aarch64", target_os = "windows"))]
{
"aarch64-pc-windows-msvc"
}
}
/// Find the right asset for this platform.
fn find_platform_asset(assets: &[GitHubAsset]) -> Option<&GitHubAsset> {
let triple = platform_triple();
let expected = format!("openhuman-core-{triple}");
assets
.iter()
.find(|a| a.name == expected || a.name == format!("{expected}.exe"))
.or_else(|| assets.iter().find(|a| a.name.starts_with(&expected)))
}
/// Fetch the latest release from GitHub.
async fn fetch_latest_release() -> Result<GitHubRelease, String> {
let url = format!("https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest");
let client = reqwest::Client::builder()
.user_agent("openhuman-tauri-updater")
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| format!("http client error: {e}"))?;
let resp = client
.get(&url)
.header("Accept", "application/vnd.github+json")
.send()
.await
.map_err(|e| format!("failed to fetch latest release: {e}"))?;
if !resp.status().is_success() {
return Err(format!("GitHub API error: {}", resp.status()));
}
resp.json()
.await
.map_err(|e| format!("failed to parse release: {e}"))
}
/// Download a binary from `url` and stage it at `dest`.
///
/// Uses a unique temp file (UUID-based) to avoid conflicts from concurrent downloads.
/// Sets executable permissions on Unix before the atomic rename.
async fn download_binary(url: &str, dest: &PathBuf) -> Result<(), String> {
let client = reqwest::Client::builder()
.user_agent("openhuman-tauri-updater")
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|e| format!("http client error: {e}"))?;
let resp = client
.get(url)
.send()
.await
.map_err(|e| format!("download failed: {e}"))?;
if !resp.status().is_success() {
return Err(format!("download returned status {}", resp.status()));
}
let bytes = resp
.bytes()
.await
.map_err(|e| format!("failed to read download: {e}"))?;
log::info!(
"[core-update] downloaded {} bytes to {}",
bytes.len(),
dest.display()
);
// Use a unique temp filename to avoid collisions from concurrent writes.
let tmp_name = format!(
".openhuman-update-{}.tmp",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
);
let tmp = dest
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(tmp_name);
{
let mut file = std::fs::File::create(&tmp).map_err(|e| format!("create temp file: {e}"))?;
file.write_all(&bytes)
.map_err(|e| format!("write temp file: {e}"))?;
file.flush().map_err(|e| format!("flush temp file: {e}"))?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("set permissions: {e}"))?;
}
std::fs::rename(&tmp, dest).map_err(|e| {
// Best-effort cleanup of temp file on rename failure.
let _ = std::fs::remove_file(&tmp);
format!("rename staged binary: {e}")
})?;
Ok(())
}
/// The main auto-update flow, called after the core process starts.
///
/// When `force` is false (startup auto-check), only updates if the running core
/// is older than `MINIMUM_CORE_VERSION`. When `force` is true (manual trigger),
/// updates whenever GitHub has a newer version than what's currently running.
///
/// Emits Tauri events so the frontend can show progress.
pub async fn check_and_update_core(
handle: CoreProcessHandle,
app: Option<tauri::AppHandle<crate::AppRuntime>>,
force: bool,
) -> Result<(), String> {
let rpc_url = handle.rpc_url();
log::info!(
"[core-update] checking core version at {} (minimum: {}, force: {})",
rpc_url,
MINIMUM_CORE_VERSION,
force
);
// Step 1: Query running version.
let running_version = match query_core_version(&rpc_url).await {
Ok(v) => v,
Err(e) => {
log::warn!("[core-update] could not query core version: {e}");
return Err(e);
}
};
log::info!(
"[core-update] running core version: {} (minimum: {})",
running_version,
MINIMUM_CORE_VERSION
);
let below_app_minimum = is_outdated(&running_version, MINIMUM_CORE_VERSION);
if below_app_minimum {
log::warn!(
"[core-update] sidecar is OLDER than this app build (running {running_version}, need >= {min}). \
UI features (e.g. channel connect) may not match RPC until the core is updated.",
min = MINIMUM_CORE_VERSION
);
}
// Step 2: Fetch latest release from GitHub (needed to download a replacement binary).
emit_event(&app, "core-update:status", "checking");
let release = match fetch_latest_release().await {
Ok(r) => r,
Err(e) => {
if force {
log::warn!("[core-update] could not fetch latest release: {e}");
return Err(e);
}
if below_app_minimum {
log::error!(
"[core-update] cannot auto-update core (GitHub unreachable): {e}\n\
→ Stop any other `openhuman` / OpenHuman using RPC port {}.\n\
→ From repo root: `cargo build --manifest-path Cargo.toml --bin openhuman` then `cd app && yarn core:stage`, restart the app.\n\
→ Or fix network access to https://api.github.com (VPN/DNS/firewall).",
handle.port()
);
emit_event(&app, "core-update:status", "error");
return Err(e);
}
log::warn!(
"[core-update] could not fetch latest release (non-fatal; core meets minimum): {e}"
);
emit_event(&app, "core-update:status", "up_to_date");
return Ok(());
}
};
let latest_version = release.tag_name.trim_start_matches('v').to_string();
log::info!("[core-update] latest release: {latest_version}");
// Decide whether to proceed with the update.
let needs_update = if force {
// Manual trigger: update if GitHub has anything newer than what's running.
is_outdated(&running_version, &latest_version)
} else {
// Auto-check: only update if running is below the minimum the app requires.
is_outdated(&running_version, MINIMUM_CORE_VERSION)
};
if !needs_update {
log::info!("[core-update] no update needed (running: {running_version}, latest: {latest_version}, force: {force})");
emit_event(&app, "core-update:status", "up_to_date");
return Ok(());
}
log::warn!(
"[core-update] updating core {} → {} (force: {})",
running_version,
latest_version,
force
);
let asset = find_platform_asset(&release.assets).ok_or_else(|| {
format!(
"no matching asset for platform '{}' in release {}",
platform_triple(),
latest_version
)
})?;
log::info!(
"[core-update] found asset: {} ({})",
asset.name,
asset.browser_download_url
);
emit_event(&app, "core-update:status", "downloading");
// Step 3: Determine staging directory.
let staging_dir = resolve_staging_dir();
if let Some(ref dir) = staging_dir {
if !dir.exists() {
std::fs::create_dir_all(dir).map_err(|e| format!("create staging dir: {e}"))?;
}
}
let dest = staging_dir
.as_ref()
.map(|d| d.join(&asset.name))
.unwrap_or_else(|| PathBuf::from(&asset.name));
// Step 4: Acquire restart lock, shutdown old process, download, stage, restart.
// Hold the lock across download + staging + restart to prevent concurrent updates.
{
let _guard = handle.restart_lock().await;
log::debug!("[core-update] acquired restart lock");
// Shutdown old process first so the binary isn't in use during staging.
handle.shutdown().await;
// Wait for port to free.
let mut waited = 0u64;
while waited < 10_000 {
if !port_open(handle.port()).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
waited += 50;
}
// Download and stage the new binary.
download_binary(&asset.browser_download_url, &dest).await?;
log::info!("[core-update] staged new binary at {}", dest.display());
// Point the handle at the new binary so ensure_running launches it.
handle.set_core_bin(dest).await;
emit_event(&app, "core-update:status", "restarting");
// Restart with the new binary.
handle.ensure_running().await?;
}
log::info!(
"[core-update] core updated from {} to {} and restarted",
running_version,
latest_version
);
emit_event(&app, "core-update:status", "updated");
Ok(())
}
/// Resolve the directory where staged sidecar binaries are placed.
/// Mirrors the discovery logic in `core_process::default_core_bin()`.
fn resolve_staging_dir() -> Option<PathBuf> {
// Dev: src-tauri/binaries/
#[cfg(debug_assertions)]
{
let binaries_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("binaries");
if binaries_dir.exists() {
return Some(binaries_dir);
}
}
// Production: next to the executable, or Resources/ on macOS.
let exe = std::env::current_exe().ok()?;
let exe_dir = exe.parent()?;
#[cfg(target_os = "macos")]
{
if let Some(resources) = exe_dir.parent().map(|p| p.join("Resources")) {
if resources.exists() {
return Some(resources);
}
}
}
Some(exe_dir.to_path_buf())
}
async fn port_open(port: u16) -> bool {
matches!(
tokio::time::timeout(
std::time::Duration::from_millis(150),
tokio::net::TcpStream::connect(("127.0.0.1", port)),
)
.await,
Ok(Ok(_))
)
}
fn emit_event(app: &Option<tauri::AppHandle<crate::AppRuntime>>, event: &str, payload: &str) {
if let Some(app) = app {
use tauri::Emitter;
if let Err(e) = app.emit(event, payload) {
log::warn!("[core-update] failed to emit {event}: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outdated_detection() {
assert!(is_outdated("0.49.17", "0.51.8"));
assert!(is_outdated("0.50.0", "0.51.0"));
assert!(!is_outdated("0.51.8", "0.51.8"));
assert!(!is_outdated("0.52.0", "0.51.8"));
assert!(!is_outdated("1.0.0", "0.51.8"));
}
}