fix: relocate jan data folder (#5179)

* fix: relocate jan data folder failed

* fix: avoid infinite recursion

* chore: kill background processes to unblock factory reset

* chore: stop models before reset factory

* chore: clean up

* chore: clean up

* fix: show error

* chore: get active models should not have retry
This commit is contained in:
Louis 2025-06-03 21:23:42 +07:00 committed by GitHub
parent 135e75b812
commit 7dc51c5e0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 75 additions and 20 deletions

View File

@ -270,7 +270,13 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
async activeModels(): Promise<(object & { id: string })[]> {
return await this.apiInstance()
.then((e) => e.get('inferences/server/models'))
.then((e) =>
e.get('inferences/server/models', {
retry: {
limit: 0, // Do not retry
},
})
)
.then((e) => e.json())
.then((e) => (e as LoadedModelResponse).data ?? [])
.catch(() => [])

View File

@ -316,6 +316,12 @@ pub fn change_app_data_folder(
new_data_folder_path
);
// Check if this is a parent directory to avoid infinite recursion
if new_data_folder_path.starts_with(&current_data_folder) {
return Err(
"New data folder cannot be a subdirectory of the current data folder".to_string(),
);
}
copy_dir_recursive(&current_data_folder, &new_data_folder_path)
.map_err(|e| format!("Failed to copy data to new folder: {}", e))?;
} else {

View File

@ -212,16 +212,17 @@ pub fn setup_mcp(app: &App) {
pub fn setup_sidecar(app: &App) -> Result<(), String> {
let app_handle = app.handle().clone();
let app_handle_for_spawn = app_handle.clone();
tauri::async_runtime::spawn(async move {
const MAX_RESTARTS: u32 = 5;
const RESTART_DELAY_MS: u64 = 5000;
let app_state = app_handle.state::<AppState>();
let app_state = app_handle_for_spawn.state::<AppState>();
let cortex_restart_count_state = app_state.cortex_restart_count.clone();
let app_data_dir = get_jan_data_folder_path(app_handle.clone());
let app_data_dir = get_jan_data_folder_path(app_handle_for_spawn.clone());
let sidecar_command_builder = || {
let mut cmd = app_handle
let mut cmd = app_handle_for_spawn
.shell()
.sidecar("cortex-server")
.expect("Failed to get sidecar command")
@ -243,14 +244,17 @@ pub fn setup_sidecar(app: &App) -> Result<(), String> {
]);
#[cfg(target_os = "windows")]
{
cmd = cmd.current_dir(app_handle.path().resource_dir().unwrap());
cmd = cmd.current_dir(app_handle_for_spawn.path().resource_dir().unwrap());
}
#[cfg(not(target_os = "windows"))]
{
cmd = cmd.env("LD_LIBRARY_PATH", {
let current_app_data_dir =
app_handle.path().resource_dir().unwrap().join("binaries");
let current_app_data_dir = app_handle_for_spawn
.path()
.resource_dir()
.unwrap()
.join("binaries");
let dest = current_app_data_dir.to_str().unwrap();
let ld_path_env = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
format!("{}{}{}", ld_path_env, ":", dest)
@ -262,9 +266,15 @@ pub fn setup_sidecar(app: &App) -> Result<(), String> {
let child_process: Arc<Mutex<Option<CommandChild>>> = Arc::new(Mutex::new(None));
let child_process_clone_for_kill = child_process.clone();
let app_handle_for_kill = app_handle.clone();
app_handle.listen("kill-sidecar", move |_event| {
let app_handle = app_handle_for_kill.clone();
let child_to_kill_arc = child_process_clone_for_kill.clone();
tauri::async_runtime::spawn(async move {
let app_state = app_handle.state::<AppState>();
let mut count = app_state.cortex_restart_count.lock().await;
*count = 5;
drop(count);
log::info!("Received kill-sidecar event (processing async).");
if let Some(child) = child_to_kill_arc.lock().await.take() {
log::info!("Attempting to kill sidecar process...");
@ -286,7 +296,7 @@ pub fn setup_sidecar(app: &App) -> Result<(), String> {
"Cortex server reached maximum restart attempts ({}). Giving up.",
current_restart_count
);
if let Err(e) = app_handle.emit("cortex_max_restarts_reached", ()) {
if let Err(e) = app_handle_for_spawn.emit("cortex_max_restarts_reached", ()) {
log::error!("Failed to emit cortex_max_restarts_reached event: {}", e);
}
break;

View File

@ -42,6 +42,8 @@ import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { windowKey } from '@/constants/windows'
import { toast } from 'sonner'
import { isDev } from '@/lib/utils'
import { emit } from '@tauri-apps/api/event'
import { stopAllModels } from '@/services/models'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.general as any)({
@ -146,12 +148,24 @@ function General() {
const confirmDataFolderChange = async () => {
if (selectedNewPath) {
try {
setJanDataFolder(selectedNewPath)
await relocateJanDataFolder(selectedNewPath)
// Only relaunch if relocation was successful
window.core?.api?.relaunch()
setSelectedNewPath(null)
setIsDialogOpen(false)
await stopAllModels()
emit('kill-sidecar')
setTimeout(async () => {
try {
await relocateJanDataFolder(selectedNewPath)
setJanDataFolder(selectedNewPath)
// Only relaunch if relocation was successful
window.core?.api?.relaunch()
setSelectedNewPath(null)
setIsDialogOpen(false)
} catch (error) {
toast.error(
error instanceof Error
? error.message
: 'Failed to relocate Jan data folder'
)
}
}, 1000)
} catch (error) {
console.error('Failed to relocate data folder:', error)
// Revert the data folder path on error

View File

@ -1,5 +1,7 @@
import { AppConfiguration, fs } from '@janhq/core'
import { invoke } from '@tauri-apps/api/core'
import { emit } from '@tauri-apps/api/event'
import { stopAllModels } from './models'
/**
* @description This function is used to reset the app to its factory settings.
@ -7,11 +9,16 @@ import { invoke } from '@tauri-apps/api/core'
* @returns {Promise<void>}
*/
export const factoryReset = async () => {
const janDataFolderPath = await getJanDataFolder()
if (janDataFolderPath) await fs.rm(janDataFolderPath)
window.localStorage.clear()
await window.core?.api?.installExtensions()
await window.core?.api?.relaunch()
// Kill background processes and remove data folder
await stopAllModels()
emit('kill-sidecar')
setTimeout(async () => {
const janDataFolderPath = await getJanDataFolder()
if (janDataFolderPath) await fs.rm(janDataFolderPath)
window.localStorage.clear()
await window.core?.api?.installExtensions()
await window.core?.api?.relaunch()
}, 1000)
}
/**
@ -71,5 +78,5 @@ export const getJanDataFolder = async (): Promise<string | undefined> => {
* @param path The new path for the Jan data folder
*/
export const relocateJanDataFolder = async (path: string) => {
window.core?.api?.changeAppDataFolder({ newDataFolder: path })
await window.core?.api?.changeAppDataFolder({ newDataFolder: path })
}

View File

@ -260,6 +260,18 @@ export const stopModel = async (model: string, provider?: string) => {
}
}
/**
* Stops all active models.
* @returns
*/
export const stopAllModels = async () => {
const models = await getActiveModels()
if (models)
await Promise.all(
models.map((model: { id: string }) => stopModel(model.id))
)
}
/**
* @fileoverview Helper function to start a model.
* This function loads the model from the provider.