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 })[]> { async activeModels(): Promise<(object & { id: string })[]> {
return await this.apiInstance() 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.json())
.then((e) => (e as LoadedModelResponse).data ?? []) .then((e) => (e as LoadedModelResponse).data ?? [])
.catch(() => []) .catch(() => [])

View File

@ -316,6 +316,12 @@ pub fn change_app_data_folder(
new_data_folder_path 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) copy_dir_recursive(&current_data_folder, &new_data_folder_path)
.map_err(|e| format!("Failed to copy data to new folder: {}", e))?; .map_err(|e| format!("Failed to copy data to new folder: {}", e))?;
} else { } else {

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import { AppConfiguration, fs } from '@janhq/core' import { AppConfiguration, fs } from '@janhq/core'
import { invoke } from '@tauri-apps/api/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. * @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>} * @returns {Promise<void>}
*/ */
export const factoryReset = async () => { export const factoryReset = async () => {
const janDataFolderPath = await getJanDataFolder() // Kill background processes and remove data folder
if (janDataFolderPath) await fs.rm(janDataFolderPath) await stopAllModels()
window.localStorage.clear() emit('kill-sidecar')
await window.core?.api?.installExtensions() setTimeout(async () => {
await window.core?.api?.relaunch() 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 * @param path The new path for the Jan data folder
*/ */
export const relocateJanDataFolder = async (path: string) => { 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. * @fileoverview Helper function to start a model.
* This function loads the model from the provider. * This function loads the model from the provider.