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:
VirtualHotBar
2026-06-02 07:50:01 +08:00
parent 39b9cef253
commit 79a2b4fde5
8 changed files with 204 additions and 10 deletions

View File

@@ -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",

View File

@@ -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": "安装失败",

View File

@@ -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
View 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())
}

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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 }

View File

@@ -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