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 tauri::{AppHandle, Manager, Runtime, State};
|
||||
|
||||
use crate::core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
||||
|
||||
use super::{server, setup, state::AppState};
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
#[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]
|
||||
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| {
|
||||
|
||||
@ -199,7 +199,7 @@ pub fn setup_mcp(app: &App) {
|
||||
let state = app.state::<AppState>();
|
||||
let servers = state.mcp_servers.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();
|
||||
app_handle.listen("kill-mcp-servers", move |_event| {
|
||||
let app_handle = app_handle_for_kill.clone();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
mod core;
|
||||
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
||||
use core::{
|
||||
cmd::get_jan_data_folder_path,
|
||||
setup::{self, setup_mcp},
|
||||
@ -8,7 +9,6 @@ use core::{
|
||||
use reqwest::Client;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{Emitter, Manager};
|
||||
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@ -58,6 +58,7 @@ pub fn run() {
|
||||
core::cmd::get_server_status,
|
||||
core::cmd::read_logs,
|
||||
core::cmd::change_app_data_folder,
|
||||
core::cmd::factory_reset,
|
||||
// MCP commands
|
||||
core::mcp::get_tools,
|
||||
core::mcp::call_tool,
|
||||
|
||||
@ -45,6 +45,7 @@ import { emit } from '@tauri-apps/api/event'
|
||||
import { stopAllModels } from '@/services/models'
|
||||
import { SystemEvent } from '@/types/events'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useHardware } from '@/hooks/useHardware'
|
||||
import { getConnectedServers } from '@/services/mcp'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useMCPServers } from '@/hooks/useMCPServers'
|
||||
@ -75,6 +76,7 @@ function General() {
|
||||
}
|
||||
}
|
||||
const { checkForUpdate } = useAppUpdater()
|
||||
const { pausePolling } = useHardware()
|
||||
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
|
||||
@ -91,6 +93,7 @@ function General() {
|
||||
}, [])
|
||||
|
||||
const resetApp = async () => {
|
||||
pausePolling()
|
||||
// TODO: Loading indicator
|
||||
await factoryReset()
|
||||
}
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
import {
|
||||
factoryReset,
|
||||
readLogs,
|
||||
parseLogLine,
|
||||
getJanDataFolder,
|
||||
relocateJanDataFolder
|
||||
relocateJanDataFolder,
|
||||
} from '../app'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: vi.fn()
|
||||
invoke: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@tauri-apps/api/event', () => ({
|
||||
emit: vi.fn()
|
||||
emit: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../models', () => ({
|
||||
stopAllModels: vi.fn()
|
||||
stopAllModels: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@janhq/core', () => ({
|
||||
fs: {
|
||||
rm: vi.fn()
|
||||
}
|
||||
rm: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the global window object
|
||||
@ -33,22 +33,22 @@ const mockWindow = {
|
||||
installExtensions: vi.fn(),
|
||||
relaunch: vi.fn(),
|
||||
getAppConfigurations: vi.fn(),
|
||||
changeAppDataFolder: vi.fn()
|
||||
}
|
||||
changeAppDataFolder: vi.fn(),
|
||||
},
|
||||
},
|
||||
localStorage: {
|
||||
clear: vi.fn()
|
||||
}
|
||||
clear: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'core', {
|
||||
value: mockWindow.core,
|
||||
writable: true
|
||||
writable: true,
|
||||
})
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockWindow.localStorage,
|
||||
writable: true
|
||||
writable: true,
|
||||
})
|
||||
|
||||
describe('app service', () => {
|
||||
@ -60,19 +60,19 @@ describe('app service', () => {
|
||||
it('should parse valid log line', () => {
|
||||
const logLine = '[2024-01-01][10:00:00Z][target][INFO] Test message'
|
||||
const result = parseLogLine(logLine)
|
||||
|
||||
|
||||
expect(result).toEqual({
|
||||
timestamp: '2024-01-01 10:00:00Z',
|
||||
level: 'info',
|
||||
target: 'target',
|
||||
message: 'Test message'
|
||||
message: 'Test message',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle invalid log line format', () => {
|
||||
const logLine = 'Invalid log line'
|
||||
const result = parseLogLine(logLine)
|
||||
|
||||
|
||||
expect(result.message).toBe('Invalid log line')
|
||||
expect(result.level).toBe('info')
|
||||
expect(result.target).toBe('info')
|
||||
@ -83,11 +83,12 @@ describe('app service', () => {
|
||||
describe('readLogs', () => {
|
||||
it('should read and parse logs', async () => {
|
||||
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)
|
||||
|
||||
|
||||
const result = await readLogs()
|
||||
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('read_logs')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].message).toBe('Test message')
|
||||
@ -97,9 +98,9 @@ describe('app service', () => {
|
||||
it('should handle empty logs', async () => {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
vi.mocked(invoke).mockResolvedValue('')
|
||||
|
||||
|
||||
const result = await readLogs()
|
||||
|
||||
|
||||
expect(result).toEqual([expect.objectContaining({ message: '' })])
|
||||
})
|
||||
})
|
||||
@ -108,9 +109,9 @@ describe('app service', () => {
|
||||
it('should get jan data folder path', async () => {
|
||||
const mockConfig = { data_folder: '/path/to/jan/data' }
|
||||
mockWindow.core.api.getAppConfigurations.mockResolvedValue(mockConfig)
|
||||
|
||||
|
||||
const result = await getJanDataFolder()
|
||||
|
||||
|
||||
expect(mockWindow.core.api.getAppConfigurations).toHaveBeenCalled()
|
||||
expect(result).toBe('/path/to/jan/data')
|
||||
})
|
||||
@ -120,40 +121,37 @@ describe('app service', () => {
|
||||
it('should relocate jan data folder', async () => {
|
||||
const newPath = '/new/path/to/jan/data'
|
||||
mockWindow.core.api.changeAppDataFolder.mockResolvedValue(undefined)
|
||||
|
||||
|
||||
await relocateJanDataFolder(newPath)
|
||||
|
||||
expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({ newDataFolder: newPath })
|
||||
|
||||
expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({
|
||||
newDataFolder: newPath,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('factoryReset', () => {
|
||||
it('should perform factory reset', async () => {
|
||||
const { stopAllModels } = await import('../models')
|
||||
const { emit } = await import('@tauri-apps/api/event')
|
||||
const { fs } = await import('@janhq/core')
|
||||
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
|
||||
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
|
||||
vi.useFakeTimers()
|
||||
|
||||
|
||||
const factoryResetPromise = factoryReset()
|
||||
|
||||
|
||||
// Advance timers and run all pending timers
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
|
||||
|
||||
await factoryResetPromise
|
||||
|
||||
|
||||
expect(stopAllModels).toHaveBeenCalled()
|
||||
expect(emit).toHaveBeenCalledWith('kill-sidecar')
|
||||
expect(mockWindow.localStorage.clear).toHaveBeenCalled()
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('factory_reset')
|
||||
|
||||
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 { emit } from '@tauri-apps/api/event'
|
||||
import { stopAllModels } from './models'
|
||||
import { SystemEvent } from '@/types/events'
|
||||
|
||||
/**
|
||||
* @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 () => {
|
||||
// Kill background processes and remove data folder
|
||||
await stopAllModels()
|
||||
emit(SystemEvent.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)
|
||||
window.localStorage.clear()
|
||||
await invoke('factory_reset')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user