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:
parent
135e75b812
commit
7dc51c5e0f
@ -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(() => [])
|
||||||
|
|||||||
@ -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(¤t_data_folder) {
|
||||||
|
return Err(
|
||||||
|
"New data folder cannot be a subdirectory of the current data folder".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
copy_dir_recursive(¤t_data_folder, &new_data_folder_path)
|
copy_dir_recursive(¤t_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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user