fix: factory reset fail with access denied error (#5952)
* fix: factory reset fail due to access denied error * fix: unused import * fix: tests
This commit is contained in:
parent
07421d7f53
commit
812a8082b8
@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::{fs, io, path::PathBuf};
|
use std::{fs, io, path::PathBuf};
|
||||||
use tauri::{AppHandle, Manager, Runtime, State};
|
use tauri::{AppHandle, Manager, Runtime, State};
|
||||||
|
|
||||||
|
use crate::core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
||||||
|
|
||||||
use super::{server, setup, state::AppState};
|
use super::{server, setup, state::AppState};
|
||||||
|
|
||||||
const CONFIGURATION_FILE_NAME: &str = "settings.json";
|
const CONFIGURATION_FILE_NAME: &str = "settings.json";
|
||||||
@ -104,6 +106,40 @@ pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf {
|
|||||||
get_jan_data_folder_path(app_handle).join("extensions")
|
get_jan_data_folder_path(app_handle).join("extensions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn factory_reset(app_handle: tauri::AppHandle, state: State<'_, AppState>) {
|
||||||
|
// close window
|
||||||
|
let windows = app_handle.webview_windows();
|
||||||
|
for (label, window) in windows.iter() {
|
||||||
|
window.close().unwrap_or_else(|_| {
|
||||||
|
log::warn!("Failed to close window: {:?}", label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let data_folder = get_jan_data_folder_path(app_handle.clone());
|
||||||
|
log::info!("Factory reset, removing data folder: {:?}", data_folder);
|
||||||
|
|
||||||
|
tauri::async_runtime::block_on(async {
|
||||||
|
cleanup_processes(state).await;
|
||||||
|
|
||||||
|
if data_folder.exists() {
|
||||||
|
if let Err(e) = fs::remove_dir_all(&data_folder) {
|
||||||
|
log::error!("Failed to remove data folder: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate the data folder
|
||||||
|
let _ = fs::create_dir_all(&data_folder).map_err(|e| e.to_string());
|
||||||
|
|
||||||
|
// Reset the configuration
|
||||||
|
let mut default_config = AppConfiguration::default();
|
||||||
|
default_config.data_folder = default_data_folder_path(app_handle.clone());
|
||||||
|
let _ = update_app_configuration(app_handle.clone(), default_config);
|
||||||
|
|
||||||
|
app_handle.restart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
|
pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
|
||||||
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {
|
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {
|
||||||
|
|||||||
@ -199,7 +199,7 @@ pub fn setup_mcp(app: &App) {
|
|||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
let servers = state.mcp_servers.clone();
|
let servers = state.mcp_servers.clone();
|
||||||
let app_handle: tauri::AppHandle = app.handle().clone();
|
let app_handle: tauri::AppHandle = app.handle().clone();
|
||||||
// Setup kill-mcp-servers event listener (similar to cortex kill-sidecar)
|
// Setup kill-mcp-servers event listener (similar to kill-sidecar)
|
||||||
let app_handle_for_kill = app_handle.clone();
|
let app_handle_for_kill = app_handle.clone();
|
||||||
app_handle.listen("kill-mcp-servers", move |_event| {
|
app_handle.listen("kill-mcp-servers", move |_event| {
|
||||||
let app_handle = app_handle_for_kill.clone();
|
let app_handle = app_handle_for_kill.clone();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
mod core;
|
mod core;
|
||||||
|
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
||||||
use core::{
|
use core::{
|
||||||
cmd::get_jan_data_folder_path,
|
cmd::get_jan_data_folder_path,
|
||||||
setup::{self, setup_mcp},
|
setup::{self, setup_mcp},
|
||||||
@ -8,7 +9,6 @@ use core::{
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::{Emitter, Manager};
|
||||||
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@ -58,6 +58,7 @@ pub fn run() {
|
|||||||
core::cmd::get_server_status,
|
core::cmd::get_server_status,
|
||||||
core::cmd::read_logs,
|
core::cmd::read_logs,
|
||||||
core::cmd::change_app_data_folder,
|
core::cmd::change_app_data_folder,
|
||||||
|
core::cmd::factory_reset,
|
||||||
// MCP commands
|
// MCP commands
|
||||||
core::mcp::get_tools,
|
core::mcp::get_tools,
|
||||||
core::mcp::call_tool,
|
core::mcp::call_tool,
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import { emit } from '@tauri-apps/api/event'
|
|||||||
import { stopAllModels } from '@/services/models'
|
import { stopAllModels } from '@/services/models'
|
||||||
import { SystemEvent } from '@/types/events'
|
import { SystemEvent } from '@/types/events'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { useHardware } from '@/hooks/useHardware'
|
||||||
import { getConnectedServers } from '@/services/mcp'
|
import { getConnectedServers } from '@/services/mcp'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { useMCPServers } from '@/hooks/useMCPServers'
|
import { useMCPServers } from '@/hooks/useMCPServers'
|
||||||
@ -75,6 +76,7 @@ function General() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { checkForUpdate } = useAppUpdater()
|
const { checkForUpdate } = useAppUpdater()
|
||||||
|
const { pausePolling } = useHardware()
|
||||||
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
|
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
|
||||||
@ -91,6 +93,7 @@ function General() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const resetApp = async () => {
|
const resetApp = async () => {
|
||||||
|
pausePolling()
|
||||||
// TODO: Loading indicator
|
// TODO: Loading indicator
|
||||||
await factoryReset()
|
await factoryReset()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,26 +4,26 @@ import {
|
|||||||
readLogs,
|
readLogs,
|
||||||
parseLogLine,
|
parseLogLine,
|
||||||
getJanDataFolder,
|
getJanDataFolder,
|
||||||
relocateJanDataFolder
|
relocateJanDataFolder,
|
||||||
} from '../app'
|
} from '../app'
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('@tauri-apps/api/core', () => ({
|
vi.mock('@tauri-apps/api/core', () => ({
|
||||||
invoke: vi.fn()
|
invoke: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@tauri-apps/api/event', () => ({
|
vi.mock('@tauri-apps/api/event', () => ({
|
||||||
emit: vi.fn()
|
emit: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../models', () => ({
|
vi.mock('../models', () => ({
|
||||||
stopAllModels: vi.fn()
|
stopAllModels: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@janhq/core', () => ({
|
vi.mock('@janhq/core', () => ({
|
||||||
fs: {
|
fs: {
|
||||||
rm: vi.fn()
|
rm: vi.fn(),
|
||||||
}
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock the global window object
|
// Mock the global window object
|
||||||
@ -33,22 +33,22 @@ const mockWindow = {
|
|||||||
installExtensions: vi.fn(),
|
installExtensions: vi.fn(),
|
||||||
relaunch: vi.fn(),
|
relaunch: vi.fn(),
|
||||||
getAppConfigurations: vi.fn(),
|
getAppConfigurations: vi.fn(),
|
||||||
changeAppDataFolder: vi.fn()
|
changeAppDataFolder: vi.fn(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
localStorage: {
|
localStorage: {
|
||||||
clear: vi.fn()
|
clear: vi.fn(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'core', {
|
Object.defineProperty(window, 'core', {
|
||||||
value: mockWindow.core,
|
value: mockWindow.core,
|
||||||
writable: true
|
writable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
Object.defineProperty(window, 'localStorage', {
|
Object.defineProperty(window, 'localStorage', {
|
||||||
value: mockWindow.localStorage,
|
value: mockWindow.localStorage,
|
||||||
writable: true
|
writable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('app service', () => {
|
describe('app service', () => {
|
||||||
@ -65,7 +65,7 @@ describe('app service', () => {
|
|||||||
timestamp: '2024-01-01 10:00:00Z',
|
timestamp: '2024-01-01 10:00:00Z',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
target: 'target',
|
target: 'target',
|
||||||
message: 'Test message'
|
message: 'Test message',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -83,7 +83,8 @@ describe('app service', () => {
|
|||||||
describe('readLogs', () => {
|
describe('readLogs', () => {
|
||||||
it('should read and parse logs', async () => {
|
it('should read and parse logs', async () => {
|
||||||
const { invoke } = await import('@tauri-apps/api/core')
|
const { invoke } = await import('@tauri-apps/api/core')
|
||||||
const mockLogs = '[2024-01-01][10:00:00Z][target][INFO] Test message\n[2024-01-01][10:01:00Z][target][ERROR] Error message'
|
const mockLogs =
|
||||||
|
'[2024-01-01][10:00:00Z][target][INFO] Test message\n[2024-01-01][10:01:00Z][target][ERROR] Error message'
|
||||||
vi.mocked(invoke).mockResolvedValue(mockLogs)
|
vi.mocked(invoke).mockResolvedValue(mockLogs)
|
||||||
|
|
||||||
const result = await readLogs()
|
const result = await readLogs()
|
||||||
@ -123,21 +124,18 @@ describe('app service', () => {
|
|||||||
|
|
||||||
await relocateJanDataFolder(newPath)
|
await relocateJanDataFolder(newPath)
|
||||||
|
|
||||||
expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({ newDataFolder: newPath })
|
expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({
|
||||||
|
newDataFolder: newPath,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('factoryReset', () => {
|
describe('factoryReset', () => {
|
||||||
it('should perform factory reset', async () => {
|
it('should perform factory reset', async () => {
|
||||||
const { stopAllModels } = await import('../models')
|
const { stopAllModels } = await import('../models')
|
||||||
const { emit } = await import('@tauri-apps/api/event')
|
const { invoke } = await import('@tauri-apps/api/core')
|
||||||
const { fs } = await import('@janhq/core')
|
|
||||||
|
|
||||||
vi.mocked(stopAllModels).mockResolvedValue()
|
vi.mocked(stopAllModels).mockResolvedValue()
|
||||||
mockWindow.core.api.getAppConfigurations.mockResolvedValue({ data_folder: '/path/to/jan/data' })
|
|
||||||
vi.mocked(fs.rm).mockResolvedValue()
|
|
||||||
mockWindow.core.api.installExtensions.mockResolvedValue()
|
|
||||||
mockWindow.core.api.relaunch.mockResolvedValue()
|
|
||||||
|
|
||||||
// Use fake timers
|
// Use fake timers
|
||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
@ -150,8 +148,8 @@ describe('app service', () => {
|
|||||||
await factoryResetPromise
|
await factoryResetPromise
|
||||||
|
|
||||||
expect(stopAllModels).toHaveBeenCalled()
|
expect(stopAllModels).toHaveBeenCalled()
|
||||||
expect(emit).toHaveBeenCalledWith('kill-sidecar')
|
|
||||||
expect(mockWindow.localStorage.clear).toHaveBeenCalled()
|
expect(mockWindow.localStorage.clear).toHaveBeenCalled()
|
||||||
|
expect(invoke).toHaveBeenCalledWith('factory_reset')
|
||||||
|
|
||||||
vi.useRealTimers()
|
vi.useRealTimers()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { AppConfiguration, fs } from '@janhq/core'
|
import { AppConfiguration } 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'
|
import { stopAllModels } from './models'
|
||||||
import { SystemEvent } from '@/types/events'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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.
|
||||||
@ -12,14 +10,8 @@ import { SystemEvent } from '@/types/events'
|
|||||||
export const factoryReset = async () => {
|
export const factoryReset = async () => {
|
||||||
// Kill background processes and remove data folder
|
// Kill background processes and remove data folder
|
||||||
await stopAllModels()
|
await stopAllModels()
|
||||||
emit(SystemEvent.KILL_SIDECAR)
|
window.localStorage.clear()
|
||||||
setTimeout(async () => {
|
await invoke('factory_reset')
|
||||||
const janDataFolderPath = await getJanDataFolder()
|
|
||||||
if (janDataFolderPath) await fs.rm(janDataFolderPath)
|
|
||||||
window.localStorage.clear()
|
|
||||||
await window.core?.api?.installExtensions()
|
|
||||||
await window.core?.api?.relaunch()
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user