Skip to content
Merged
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
41 changes: 41 additions & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"src/apps/cli",
"src/apps/sdk-host",
"src/apps/desktop",
"src/apps/data-migrator",
"src/apps/server",
"src/apps/relay-server",
"src/apps/miniapp-market-server",
Expand All @@ -27,6 +28,7 @@ members = [
"src/crates/adapters/transport",
"src/crates/services/services-core",
"src/crates/services/services-integrations",
"src/crates/services/legacy-migration",
"src/crates/services/miniapp-market-service",
"src/crates/services/skin-market-service",
"src/crates/services/relay-service",
Expand Down Expand Up @@ -197,9 +199,10 @@ urlencoding = "2.1"
oxc = { version = "0.138.0", default-features = false, features = ["ast_visit", "codegen", "semantic", "transformer"] }
oxc-parse = { package = "oxc", version = "0.138.0", default-features = false }
sqlx = { version = "0.8", default-features = false }
rusqlite = { version = "0.32", default-features = false }

# Tauri (desktop only)
tauri = { version = "2.11", features = ["unstable", "macos-private-api", "tray-icon"] }
tauri = { version = "2.11" }
tauri-plugin-opener = "2.5"
tauri-plugin-dialog = "2.7"
tauri-plugin-fs = "2.5"
Expand Down
1 change: 1 addition & 0 deletions OpenBitFun-Installer/scripts/build-installer.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const STRICT_PAYLOAD_VALIDATION = !isDev;
const MIN_APP_EXE_BYTES = 5 * 1024 * 1024;
const REQUIRED_PAYLOAD_FILES = [
"openbitfun-desktop.exe",
"openbitfun-data-migrator.exe",
"frontend/dist/index.html",
"mobile-web/dist/index.html",
"resources/ext-host/extension-host.js",
Expand Down
22 changes: 22 additions & 0 deletions OpenBitFun-Installer/scripts/build-installer.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,38 @@ test("all Installer validators share the required runtime file contract", () =>
path.join(installerRoot, "src-tauri", "src", "installer", "commands.rs"),
"utf8"
);
const installerModRs = fs.readFileSync(
path.join(installerRoot, "src-tauri", "src", "installer", "mod.rs"),
"utf8"
);
for (const relativePath of REQUIRED_PAYLOAD_FILES) {
assert.match(buildRs, new RegExp(escapeRegExp(relativePath)));
if (relativePath === "openbitfun-desktop.exe") {
assert.match(commandsRs, /MAIN_APP_EXE/);
} else if (relativePath === "openbitfun-data-migrator.exe") {
assert.match(installerModRs, new RegExp(escapeRegExp(relativePath)));
assert.match(commandsRs, /DATA_MIGRATOR_EXE/);
} else {
assert.match(commandsRs, new RegExp(escapeRegExp(relativePath)));
}
}
});

test("Data Migrator launch resolves only the registered installation", () => {
const commandsRs = fs.readFileSync(
path.join(installerRoot, "src-tauri", "src", "installer", "commands.rs"),
"utf8"
);
const commandStart = commandsRs.indexOf("pub(crate) fn launch_legacy_data_migrator");
const commandEnd = commandsRs.indexOf("/// Close the installer window.", commandStart);
assert.notEqual(commandStart, -1);
assert.notEqual(commandEnd, -1);
const commandSource = commandsRs.slice(commandStart, commandEnd);
assert.doesNotMatch(commandSource, /request\.install_path/);
assert.match(commandSource, /read_existing_install_from_uninstall_registry/);
assert.match(commandSource, /read_tauri_install_location/);
});

function minorLine(version) {
assert.match(version, /^\d+\.\d+\.\d+$/);
return version.split(".").slice(0, 2).join(".");
Expand Down
3 changes: 3 additions & 0 deletions OpenBitFun-Installer/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ zip = "0.6"
openbitfun-core-types = { path = "../../src/crates/contracts/core-types" }
openbitfun-ai-adapters = { path = "../../src/crates/adapters/ai-adapters" }
openbitfun-services-core = { path = "../../src/crates/services/services-core", features = ["json-io"] }
openbitfun-legacy-migration = { path = "../../src/crates/services/legacy-migration" }
openbitfun-product-domains = { path = "../../src/crates/contracts/product-domains", features = ["legacy-migration"] }
uuid = { version = "1", features = ["v4"] }

[target.'cfg(windows)'.dependencies]
winreg = "0.52"
Expand Down
4 changes: 3 additions & 1 deletion OpenBitFun-Installer/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ use std::path::{Path, PathBuf};
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipWriter};

const REQUIRED_PAYLOAD_FILES: [&str; 5] = [
const REQUIRED_PAYLOAD_FILES: [&str; 6] = [
"openbitfun-desktop.exe",
"openbitfun-data-migrator.exe",
"frontend/dist/index.html",
"mobile-web/dist/index.html",
"resources/ext-host/extension-host.js",
"resources/worker_host.js",
];

fn main() {
println!("cargo:rerun-if-env-changed=OPENBITFUN_RELEASE_CHANNEL");
if let Err(err) = build_embedded_payload() {
panic!("failed to build embedded payload: {err}");
}
Expand Down
132 changes: 129 additions & 3 deletions OpenBitFun-Installer/src-tauri/src/installer/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@ use super::types::{
ConnectionTestResult, DiskSpaceInfo, InstallOptions, InstallProgress, ModelConfig,
RemoteModelInfo,
};
use super::MAIN_APP_EXE;
use super::{DATA_MIGRATOR_EXE, MAIN_APP_EXE};
use openbitfun_core_types::{
installer_config_handoff::{
InstallerConfigHandoff, InstallerModelHandoff, INSTALLER_CONFIG_HANDOFF_FILE_NAME,
},
product_identity::{data_namespace, hidden_data_directory},
product_identity::{data_namespace, hidden_data_directory, product_id},
};
#[cfg(target_os = "windows")]
use openbitfun_legacy_migration::{
launch_trusted_executable, probe_legacy_source, HandoffStore, MigrationOnboardingStore,
MigrationRoots, ProbeLimits, TrustedInstallationResolver,
};
#[cfg(target_os = "windows")]
use openbitfun_product_domains::legacy_migration::{
MigrationPromptChoice, MigrationSelection, MigratorHandoffRequest,
MigratorProtocolCapabilities, MigratorRequestMode, MigratorRequestOrigin,
CURRENT_MIGRATION_FORMAT_VERSION,
};
use openbitfun_services_core::json_store::JsonFileStore;
use serde::{Deserialize, Serialize};
Expand All @@ -22,6 +33,8 @@ use std::fs::File;
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
#[cfg(target_os = "windows")]
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{Emitter, Manager, Window};

#[cfg(target_os = "windows")]
Expand All @@ -35,14 +48,22 @@ struct WindowsInstallState {

const MIN_WINDOWS_APP_EXE_BYTES: u64 = 5 * 1024 * 1024;
const PAYLOAD_MANIFEST_FILE: &str = "payload-manifest.json";
const REQUIRED_PAYLOAD_FILES: [&str; 5] = [
const REQUIRED_PAYLOAD_FILES: [&str; 6] = [
MAIN_APP_EXE,
DATA_MIGRATOR_EXE,
"frontend/dist/index.html",
"mobile-web/dist/index.html",
"resources/ext-host/extension-host.js",
"resources/worker_host.js",
];
const INSTALLER_STATE_FILE: &str = "installer-state.json";
#[cfg(target_os = "windows")]
const MIGRATOR_HANDOFF_LIFETIME_MS: i64 = 10 * 60 * 1000;
#[cfg(target_os = "windows")]
const RELEASE_CHANNEL: &str = match option_env!("OPENBITFUN_RELEASE_CHANNEL") {
Some(value) => value,
None => "stable",
};
const EMBEDDED_PAYLOAD_ZIP: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/embedded_payload.zip"));

Expand Down Expand Up @@ -836,6 +857,111 @@ pub(crate) fn launch_application(install_path: String) -> Result<(), String> {
Ok(())
}

/// Create an Installer-origin onboarding handoff and launch the installed
/// standalone Data Migrator. The Installer never reads or writes migrated
/// product data itself.
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct LaunchLegacyDataMigratorRequest {}

#[tauri::command]
pub(crate) fn launch_legacy_data_migrator(
request: LaunchLegacyDataMigratorRequest,
) -> Result<bool, String> {
#[cfg(not(target_os = "windows"))]
{
let _ = request;
return Err(
"Legacy data migration is not supported by this Installer platform.".to_string(),
);
}

#[cfg(target_os = "windows")]
{
let _ = request;
let roots = MigrationRoots::resolve_current_user()
.map_err(|_| "Could not resolve the current-user migration storage.".to_string())?;
let Some(source) = probe_legacy_source(&roots, ProbeLimits::default())
.map_err(|_| "Could not safely inspect older data.".to_string())?
else {
return Ok(false);
};
if !source.supported {
return Err(
"The discovered legacy data format is not supported by this Data Migrator."
.to_string(),
);
}

let now_ms = migration_now_ms();
let capabilities = MigratorProtocolCapabilities::current();
let handoff = MigratorHandoffRequest {
protocol_version: capabilities.protocol_version,
mode: MigratorRequestMode::Onboarding,
origin: MigratorRequestOrigin::Installer,
run_id: uuid::Uuid::new_v4().to_string(),
nonce: uuid::Uuid::new_v4().to_string(),
source_id: Some(source.source_id.clone()),
source_fingerprint: Some(source.source_fingerprint.clone()),
selection: MigrationSelection::all(),
caller_process_id: std::process::id(),
product_id: product_id().to_string(),
release_channel: RELEASE_CHANNEL.to_string(),
created_at_ms: now_ms,
expires_at_ms: now_ms.saturating_add(MIGRATOR_HANDOFF_LIFETIME_MS),
required_capabilities: capabilities.capabilities,
};
HandoffStore::new(roots.clone(), product_id(), RELEASE_CHANNEL)
.write_request(&handoff, now_ms)
.map_err(|_| "Could not create a safe Data Migrator handoff.".to_string())?;
MigrationOnboardingStore::new(roots)
.update(|state| {
state.format_version = CURRENT_MIGRATION_FORMAT_VERSION;
state.source_fingerprint = source.source_fingerprint.clone();
state.detected_at_ms.get_or_insert(now_ms);
state.choice = MigrationPromptChoice::Unset;
state.last_prompted_version = Some(env!("CARGO_PKG_VERSION").to_string());
state.run_id = Some(handoff.run_id.clone());
state.handled_run_id = None;
})
.map_err(|_| "Could not persist the Data Migrator handoff state.".to_string())?;

let registered_install_path = super::registry::read_existing_install_from_uninstall_registry()
.map(|registration| registration.install_location)
.or_else(super::registry::read_tauri_install_location)
.ok_or_else(|| {
"The registered OpenBitFun installation could not be found. Repair the installation before starting Data Migrator."
.to_string()
})?;
let desktop = PathBuf::from(registered_install_path).join(MAIN_APP_EXE);
let executable = TrustedInstallationResolver::resolve_sibling(
&desktop,
MAIN_APP_EXE,
DATA_MIGRATOR_EXE,
)
.map_err(|_| {
"The installed Data Migrator is missing or failed installation layout checks. Repair the OpenBitFun installation."
.to_string()
})?;
launch_trusted_executable(
&executable,
&[std::ffi::OsStr::new(handoff.run_id.as_str())],
)
.map_err(|_| "Could not launch the installed Data Migrator.".to_string())?;
Ok(true)
}
}

#[cfg(target_os = "windows")]
fn migration_now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or(i64::MAX)
}

/// Close the installer window.
#[tauri::command]
pub(crate) fn close_installer(window: Window) {
Expand Down
1 change: 1 addition & 0 deletions OpenBitFun-Installer/src-tauri/src/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod types;

/// Windows main binary file name — must match `src/apps/desktop` `[[bin]]` and Tauri NSIS output.
const MAIN_APP_EXE: &str = "openbitfun-desktop.exe";
const DATA_MIGRATOR_EXE: &str = "openbitfun-data-migrator.exe";

#[cfg(target_os = "windows")]
mod registry;
Expand Down
1 change: 1 addition & 0 deletions OpenBitFun-Installer/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn run() {
commands::set_theme_preference,
commands::uninstall,
commands::launch_application,
commands::launch_legacy_data_migrator,
commands::close_installer,
])
.run(tauri::generate_context!())
Expand Down
1 change: 1 addition & 0 deletions OpenBitFun-Installer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function App() {
options={installer.options}
setOptions={installer.setOptions}
onLaunch={installer.launchApp}
onLaunchMigration={installer.launchLegacyDataMigrator}
onClose={installer.closeInstaller}
/>
);
Expand Down
9 changes: 8 additions & 1 deletion OpenBitFun-Installer/src/hooks/useInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface UseInstallerReturn {
saveModelConfig: () => Promise<void>;
testModelConnection: (modelConfig: ModelConfig) => Promise<ConnectionTestResult>;
launchApp: () => Promise<void>;
launchLegacyDataMigrator: () => Promise<boolean>;
closeInstaller: () => void;
refreshDiskSpace: (path: string) => Promise<void>;
clearInstallError: () => void;
Expand Down Expand Up @@ -333,6 +334,12 @@ export function useInstaller(): UseInstallerReturn {
await invoke('launch_application', { installPath: options.installPath });
}, [options.installPath]);

const launchLegacyDataMigrator = useCallback(async () => {
return invoke<boolean>('launch_legacy_data_migrator', {
request: {},
});
}, []);

const closeInstaller = useCallback(() => {
invoke('close_installer');
}, []);
Expand Down Expand Up @@ -379,7 +386,7 @@ export function useInstaller(): UseInstallerReturn {
progress, isInstalling, installationCompleted, error, diskSpace,
existingInstall, launchRegisteredUninstaller,
install, canConfirmProgress, confirmProgress, retryInstall, backToOptions,
saveModelConfig, testModelConnection, launchApp, closeInstaller, refreshDiskSpace, clearInstallError,
saveModelConfig, testModelConnection, launchApp, launchLegacyDataMigrator, closeInstaller, refreshDiskSpace, clearInstallError,
isUninstallMode, isUninstalling, uninstallCompleted, uninstallError, uninstallProgress, startUninstall,
};
}
Loading
Loading