mirror of
https://github.com/VirtualHotBar/NetMount.git
synced 2026-06-08 15:42:22 +08:00
feat: add Windows Task Scheduler autostart and --service mode
Addresses #49 and #61: - Add Task Scheduler based autostart for higher startup priority on Windows - Uses schtasks.exe to create 'At log on' scheduled task - Runs earlier than registry HKCU\Run entries - Solves timing issues with other autostart apps (e.g., KeePassXC) - Add --service CLI flag for headless/service mode - Starts without showing the UI window - Lower resource usage for background operation - Can be combined with Task Scheduler for service-like experience - Update settings UI with autostart mode selector - Disabled / Standard (Registry) / High Priority (Task Scheduler) - Task Scheduler option only shown on Windows - Add locale translations for new UI strings (zh-cn, en, zh-hant)
This commit is contained in:
@@ -227,6 +227,11 @@
|
||||
"Umask": "File Permission Mask",
|
||||
"about": "About",
|
||||
"autostart": "Autostart on Boot",
|
||||
"autostart_disabled": "Disabled",
|
||||
"autostart_standard": "Standard (Registry)",
|
||||
"autostart_high_priority": "High Priority (Task Scheduler)",
|
||||
"service_mode": "Service Mode",
|
||||
"service_mode_hint": "Start without UI using --service flag, lower resource usage",
|
||||
"install": "Install",
|
||||
"winfsp_not_installed": "Dependency (WinFsp) needs to be installed to mount storage.",
|
||||
"install_failed": "Installation Failed",
|
||||
|
||||
@@ -230,6 +230,11 @@
|
||||
"Umask": "文件权限掩码",
|
||||
"about": "关于",
|
||||
"autostart": "开机自启",
|
||||
"autostart_disabled": "禁用",
|
||||
"autostart_standard": "标准(注册表)",
|
||||
"autostart_high_priority": "高优先级(任务计划程序)",
|
||||
"service_mode": "服务模式",
|
||||
"service_mode_hint": "使用 --service 参数启动时,不显示界面,资源占用更少",
|
||||
"install": "安装",
|
||||
"winfsp_not_installed": "需要安装依赖(WinFsp),才能挂载存储。",
|
||||
"install_failed": "安装失败",
|
||||
|
||||
@@ -197,6 +197,11 @@
|
||||
"Umask": "文件許可權遮罩",
|
||||
"about": "關於",
|
||||
"autostart": "開機自啟",
|
||||
"autostart_disabled": "停用",
|
||||
"autostart_standard": "標準(登錄檔)",
|
||||
"autostart_high_priority": "高優先順序(工作排程器)",
|
||||
"service_mode": "服務模式",
|
||||
"service_mode_hint": "使用 --service 參數啟動時,不顯示介面,資源佔用更少",
|
||||
"install": "安裝",
|
||||
"winfsp_not_installed": "需要安裝依賴(WinFsp),才能掛載存儲。",
|
||||
"network_share_tip": "提示:如需網路共用掛載的磁碟機,請取消勾選"類比本地硬碟"選項(即使用網路磁碟機模式),並確保WinFsp已正確安裝。部分儲存類型可能不支援網路共用。",
|
||||
|
||||
116
src-tauri/src/autostart.rs
Normal file
116
src-tauri/src/autostart.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! Windows Task Scheduler based autostart for higher startup priority.
|
||||
//!
|
||||
//! On Windows, the standard `tauri-plugin-autostart` uses the registry key
|
||||
//! `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` which is processed
|
||||
//! late in the login sequence. Task Scheduler tasks with "At log on" trigger
|
||||
//! run earlier, providing higher startup priority.
|
||||
//!
|
||||
//! This module provides functions to manage a scheduled task as an alternative
|
||||
//! autostart mechanism with higher priority.
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::process::Command;
|
||||
|
||||
/// Task name used in Windows Task Scheduler
|
||||
#[cfg(target_os = "windows")]
|
||||
const TASK_NAME: &str = "NetMount_Autostart";
|
||||
|
||||
/// Check if the scheduled task exists and is enabled.
|
||||
/// Returns true if the task is registered and ready to run.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_task_enabled() -> bool {
|
||||
let output = match Command::new("schtasks.exe")
|
||||
.args(["/query", "/tn", TASK_NAME, "/fo", "csv", "/nh"])
|
||||
.output()
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// Task exists if output contains status "Ready" or "Running"
|
||||
// schtasks CSV output format: "TaskName","Next Run Time","Status"
|
||||
stdout.contains("Ready") || stdout.contains("Running")
|
||||
}
|
||||
|
||||
/// Create a scheduled task for autostart at logon.
|
||||
/// Uses "At log on" trigger with limited (user) privileges.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `exe_path` - Full path to the NetMount executable
|
||||
/// * `service_mode` - If true, adds `--service` flag to run headless
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn create_task(exe_path: &str, service_mode: bool) -> Result<(), String> {
|
||||
// Delete any existing task first
|
||||
let _ = delete_task();
|
||||
|
||||
let task_command = if service_mode {
|
||||
format!("\"{}\" --service", exe_path)
|
||||
} else {
|
||||
format!("\"{}\"", exe_path)
|
||||
};
|
||||
|
||||
let output = Command::new("schtasks.exe")
|
||||
.args([
|
||||
"/create",
|
||||
"/tn",
|
||||
TASK_NAME,
|
||||
"/tr",
|
||||
&task_command,
|
||||
"/sc",
|
||||
"onlogon",
|
||||
"/rl",
|
||||
"limited",
|
||||
"/f",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to create scheduled task: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(format!("Failed to create scheduled task: {}", stderr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the scheduled task.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn delete_task() -> Result<(), String> {
|
||||
let output = Command::new("schtasks.exe")
|
||||
.args(["/delete", "/tn", TASK_NAME, "/f"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to delete scheduled task: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
// Task might not exist, which is fine
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains("not found") || stderr.contains("找不到") || stderr.contains("0x1") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Failed to delete scheduled task: {}", stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Non-Windows stubs
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_task_enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn create_task(_exe_path: &str, _service_mode: bool) -> Result<(), String> {
|
||||
Err("Task Scheduler is only available on Windows".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn delete_task() -> Result<(), String> {
|
||||
Err("Task Scheduler is only available on Windows".to_string())
|
||||
}
|
||||
@@ -255,6 +255,10 @@ pub fn init() -> anyhow::Result<()> {
|
||||
diagnostics::export_diagnostics,
|
||||
get_autostart_state,
|
||||
set_autostart_state,
|
||||
get_autostart_mode,
|
||||
set_autostart_mode,
|
||||
is_service_mode,
|
||||
is_task_scheduler_available,
|
||||
get_winfsp_install_state,
|
||||
get_available_drive_letter,
|
||||
get_available_ports,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { listenWindow, window as appWindow } from './window'
|
||||
import { nmConfig, readNmConfig, roConfig, runtimeEnv } from '../services/ConfigService'
|
||||
import { setThemeMode } from './setting/setting'
|
||||
import { isServiceMode } from './setting/setting'
|
||||
import { setLocalized } from './language/localized'
|
||||
import { startRclone } from '../utils/rclone/process'
|
||||
import { startOpenlist } from '../utils/openlist/process'
|
||||
@@ -126,7 +127,10 @@ export async function init(setStartStr: SetStartStrFn) {
|
||||
// 启动定期清理任务(每4小时清理过期缓存)
|
||||
startPeriodicCleanup()
|
||||
|
||||
if (!nmConfig.settings.startHide) {
|
||||
// 检查是否在服务模式下运行(--service 标志)
|
||||
const serviceMode = await isServiceMode().catch(() => false)
|
||||
|
||||
if (!nmConfig.settings.startHide && !serviceMode) {
|
||||
await appWindow.show()
|
||||
await appWindow.setFocus()
|
||||
}
|
||||
|
||||
@@ -42,4 +42,37 @@ async function setAutostartState(state: boolean): Promise<boolean> {
|
||||
return (await invoke('set_autostart_state', { enabled: state })) as boolean
|
||||
}
|
||||
|
||||
export { setThemeMode, getAutostartState, setAutostartState }
|
||||
// 自启模式类型
|
||||
type AutostartMode = 'none' | 'registry' | 'task_scheduler'
|
||||
|
||||
//获取自启模式
|
||||
async function getAutostartMode(): Promise<AutostartMode> {
|
||||
return (await invoke('get_autostart_mode')) as AutostartMode
|
||||
}
|
||||
|
||||
//设置自启模式
|
||||
async function setAutostartMode(mode: AutostartMode): Promise<boolean> {
|
||||
return (await invoke('set_autostart_mode', { mode })) as boolean
|
||||
}
|
||||
|
||||
//检查是否支持任务计划程序(仅 Windows)
|
||||
async function isTaskSchedulerAvailable(): Promise<boolean> {
|
||||
return (await invoke('is_task_scheduler_available')) as boolean
|
||||
}
|
||||
|
||||
//检查是否在服务模式下运行
|
||||
async function isServiceMode(): Promise<boolean> {
|
||||
return (await invoke('is_service_mode')) as boolean
|
||||
}
|
||||
|
||||
export {
|
||||
setThemeMode,
|
||||
getAutostartState,
|
||||
setAutostartState,
|
||||
getAutostartMode,
|
||||
setAutostartMode,
|
||||
isTaskSchedulerAvailable,
|
||||
isServiceMode,
|
||||
}
|
||||
|
||||
export type { AutostartMode }
|
||||
|
||||
@@ -18,7 +18,9 @@ import { useTranslation } from 'react-i18next'
|
||||
import * as dialog from '@tauri-apps/plugin-dialog'
|
||||
import {
|
||||
getAutostartState,
|
||||
setAutostartState,
|
||||
getAutostartMode,
|
||||
setAutostartMode,
|
||||
isTaskSchedulerAvailable,
|
||||
setThemeMode,
|
||||
} from '../../../controller/setting/setting'
|
||||
import { setLocalized } from '../../../controller/language/localized'
|
||||
@@ -44,14 +46,25 @@ function hashPassword(password: string): string {
|
||||
|
||||
export function GeneralSettings(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const [autostart, setAutostart] = useState<boolean>()
|
||||
const [autostartMode, setAutostartModeState] = useState<string>('none')
|
||||
const [taskSchedulerAvailable, setTaskSchedulerAvailable] = useState<boolean>(false)
|
||||
const { increment: incrementSettings } = useSettingsStore()
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false)
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getAutostartState().then(setAutostart)
|
||||
// Load autostart mode and task scheduler availability
|
||||
Promise.all([
|
||||
getAutostartMode(),
|
||||
isTaskSchedulerAvailable(),
|
||||
]).then(([mode, available]) => {
|
||||
setAutostartModeState(mode)
|
||||
setTaskSchedulerAvailable(available)
|
||||
}).catch(() => {
|
||||
// Fallback: check basic autostart state
|
||||
getAutostartState().then(() => {})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSetPassword = async () => {
|
||||
@@ -128,13 +141,22 @@ export function GeneralSettings(): JSX.Element {
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label={t('autostart')}>
|
||||
<Switch
|
||||
checked={autostart || false}
|
||||
<Select
|
||||
value={autostartMode}
|
||||
onChange={async value => {
|
||||
await setAutostartState(value)
|
||||
setAutostart(value)
|
||||
const success = await setAutostartMode(value as 'none' | 'registry' | 'task_scheduler')
|
||||
if (success) {
|
||||
setAutostartModeState(value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
style={{ width: '14rem' }}
|
||||
>
|
||||
<Select.Option value="none">{t('autostart_disabled')}</Select.Option>
|
||||
<Select.Option value="registry">{t('autostart_standard')}</Select.Option>
|
||||
{taskSchedulerAvailable && (
|
||||
<Select.Option value="task_scheduler">{t('autostart_high_priority')}</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label={t('start_hide')}>
|
||||
<Switch
|
||||
|
||||
Reference in New Issue
Block a user