bug修复调整
This commit is contained in:
@@ -69,7 +69,7 @@ windows-sys = { version = "0.52", features = [
|
||||
"Win32_Storage_Xps",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
# Explorer 髯キ隨ャ・ヲ髮・スコ莨・・カ繝サ・ョ髯滄摩・「髮」・ス・」・つ髮趣スャ陷茨スキ繝サ・シ郢晢スサShellWindows COM郢晢スサ闔ィ螟イ・ス・シ陞「・サ繝サ・サ郢晢スサ繝サ・シ陷肴コ倥・鬨セ蛹・スス・ィ髯具スサ繝サ・ー鬨セ・ァ郢晢スサfeature郢晢スサ隴エ・ァ髢迹壼エ輔・・カ鬩帛ク帙・繝サ・ッ陷ソ・ー繝サ・ス鬪ー蜈キ・ス・ァ繝サ・ッ
|
||||
# Explorer 鬯ョ・ッ繝サ・キ鬮ォ・ィ繝サ・ャ郢晢スサ繝サ・ヲ鬯ョ・ョ郢晢スサ繝サ・ス繝サ・コ髣費スィ郢晢スサ・つ郢晢スサ繝サ・カ驛「譎「・ス・サ郢晢スサ繝サ・ョ鬯ョ・ッ雋翫・譚溽ケ晢スサ繝サ・「鬯ョ・ョ繝サ・」郢晢スサ繝サ・ス郢晢スサ繝サ・」郢晢スサ邵コ・、・つ鬯ョ・ョ髮懶ス」繝サ・ス繝サ・ャ鬮ッ・キ髣鯉スィ繝サ・ス繝サ・キ驛「譎「・ス・サ郢晢スサ繝サ・シ鬩幢ス「隴趣ス「繝サ・ス繝サ・サShellWindows COM鬩幢ス「隴趣ス「繝サ・ス繝サ・サ鬮」雋サ・ス・ィ髯樊サゑスス・イ郢晢スサ繝サ・ス郢晢スサ繝サ・シ鬮ッ讖ク・ス・「郢晢スサ繝サ・サ驛「譎「・ス・サ郢晢スサ繝サ・サ鬩幢ス「隴趣ス「繝サ・ス繝サ・サ驛「譎「・ス・サ郢晢スサ繝サ・シ鬮ッ・キ髢ァ・エ繝サ・コ陋滂ス・郢晢スサ鬯ッ・ィ繝サ・セ髯具スケ郢晢スサ繝サ・ス繝サ・ス郢晢スサ繝サ・ィ鬯ョ・ッ陷茨スキ繝サ・ス繝サ・サ驛「譎「・ス・サ郢晢スサ繝サ・ー鬯ッ・ィ繝サ・セ郢晢スサ繝サ・ァ鬩幢ス「隴趣ス「繝サ・ス繝サ・サfeature鬩幢ス「隴趣ス「繝サ・ス繝サ・サ鬮ォ・エ繝サ・エ郢晢スサ繝サ・ァ鬯ョ・「繝サ・ー髴托スケ陞「・シ繝サ・エ髴域鱒繝サ郢晢スサ繝サ・カ鬯ッ・ゥ陝カ蟷「・ス・ク陝カ蜷カ繝サ驛「譎「・ス・サ郢晢スサ繝サ・ッ鬮ッ・キ繝サ・ソ郢晢スサ繝サ・ー驛「譎「・ス・サ郢晢スサ繝サ・ス鬯ッ・ェ繝サ・ー髯キ闌ィ・ス・キ郢晢スサ繝サ・ス郢晢スサ繝サ・ァ驛「譎「・ス・サ郢晢スサ繝サ・ッ
|
||||
windows = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
@@ -82,14 +82,14 @@ windows = { version = "0.52", features = [
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2"
|
||||
|
||||
# ===== 编译优化 =====
|
||||
# dev 构建启用增量编译 + 行号级调试(提速本地迭代),仅影响 `tauri dev`
|
||||
# ===== 驛帛・・ッ蜿ー・シ莨懷密 =====
|
||||
# dev 隴ォ繝サ・サ・コ陷キ・ッ騾包スィ陟・ィ」纃シ驛帛・・ッ繝サ+ 髯ヲ謔滓差驛、・ァ髫ケ繝サ・ッ蛹・スシ蝓溽スイ鬨セ貊捺た陜ィ・ー髴托スュ闔会ス」繝サ莨夲スシ蠕。・サ繝サ・ス・ア陷ゥ繝サ`tauri dev`
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
debug = "line-tables-only"
|
||||
|
||||
# release 譫・サコ逖ヲ霄ォ・壼悉隨ヲ蜿キ + LTO・悟㍼蟆丞ョ芽」・桁菴鍋ァッ
|
||||
# release 髫エ・ォ郢晢スサ繝サ・サ繝サ・コ鬨セ蜴・スス・ヲ鬮エ繝サ・ス・ォ郢晢スサ陞「・シ隰碑崟蝨キ繝サ・ヲ髯キ・ソ繝サ・キ + LTO郢晢スサ隰疲コ倩ゥ宣劑繝サ・ク讖ク・ス・ョ髣・スス繝サ・」郢晢スサ隴ッ竏ャ謚・ェー蜈キ・ス・ァ繝サ・ッ
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -44,6 +44,8 @@ pub mod events {
|
||||
// OSD 窗口
|
||||
pub const OSD_SYSTEM_UI_ACTIVE: &str = "osd-system-ui-active";
|
||||
pub const OSD_SYSTEM_UI_INACTIVE: &str = "osd-system-ui-inactive";
|
||||
pub const OSD_GAME_ACTIVE: &str = "osd-game-active";
|
||||
pub const OSD_GAME_INACTIVE: &str = "osd-game-inactive";
|
||||
pub const OSD_START_DRAG: &str = "osd-start-drag";
|
||||
pub const OSD_END_DRAG: &str = "osd-end-drag";
|
||||
// 截图
|
||||
|
||||
@@ -663,6 +663,67 @@ impl DownloadEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// BT 任务重新入会:句柄缺失(元数据未解析完/句柄被移除)时,重新加入会话以取回句柄。
|
||||
/// librqbit 对已加入的种子返回 AlreadyManaged(复用现有句柄,已下载片段保留),
|
||||
/// 因此对暂停→继续、错误→继续等场景都幂等、安全。
|
||||
async fn reenter_bt(&self, id: &str, url: &str, dir: &str) -> Result<(), String> {
|
||||
let added = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(crate::download_engine::torrent::INSPECT_TIMEOUT_SECS),
|
||||
self.inner.torrent.add_async(url, dir),
|
||||
).await;
|
||||
let (_, handle) = match added {
|
||||
Ok(Ok(v)) => v,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => return Err("解析磁力元数据超时:未能从 DHT/Tracker 获取种子信息".to_string()),
|
||||
};
|
||||
|
||||
// 存回句柄(句柄存在则更新,缺失则新建)
|
||||
{
|
||||
let mut handles = self.inner.handles.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match handles.get_mut(id) {
|
||||
Some(h) => h.bt = Some(handle.clone()),
|
||||
None => {
|
||||
handles.insert(
|
||||
id.to_string(),
|
||||
TaskHandle {
|
||||
gen: self.inner.next_gen.fetch_add(1, Ordering::SeqCst) + 1,
|
||||
cancel: Arc::new(AtomicBool::new(false)),
|
||||
progress: Vec::new(),
|
||||
join: Mutex::new(None),
|
||||
bt: Some(handle.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待元数据就绪(本地 .torrent 秒回;磁力依赖网络,带超时)
|
||||
if let Err(e) = self.inner.torrent.wait_initialized(&handle).await {
|
||||
return Err(e);
|
||||
}
|
||||
// 刷新结构化任务元数据(filename / infohash / 文件列表 / 大小)
|
||||
match self.inner.torrent.inspect(url).await {
|
||||
Ok(info) => {
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(t) = tasks.get_mut(id) {
|
||||
t.filename = info.name.clone();
|
||||
t.info_hash = Some(info.info_hash.clone());
|
||||
t.bt_files = info.files.clone();
|
||||
t.total_size = info.total_size;
|
||||
t.bt_metadata_ready = true;
|
||||
t.error = None;
|
||||
// 复位为 Queued:start_download 只在 Queued 下继续推进(首调时已置 Active)
|
||||
if matches!(t.status, TaskStatus::Active | TaskStatus::Error) {
|
||||
t.status = TaskStatus::Queued;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
self.persist_now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// BT 元数据解析失败:任务置为 Error 并通知前端刷新
|
||||
fn fail_bt_resolve(engine: &DownloadEngine, id: &str, error: String) {
|
||||
{
|
||||
@@ -1245,15 +1306,28 @@ impl DownloadEngine {
|
||||
// ===== BitTorrent 下载分支 =====
|
||||
if is_bt {
|
||||
let Some(bt) = existing_bt else {
|
||||
// 无 BT 句柄(异常):标记 Error
|
||||
{
|
||||
let mut tasks = self.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(task) = tasks.get_mut(&id) {
|
||||
task.status = TaskStatus::Error;
|
||||
task.error = Some("BitTorrent 句柄缺失".to_string());
|
||||
// 无 BT 句柄:常见于元数据尚未解析完就点了"暂停/继续",或句柄已被移除。
|
||||
// 不能直接报"句柄缺失"(否则此类任务点"继续"会立即失败),改为后台重新加入
|
||||
// 会话(如已加入则复用已有句柄,已下载片段保留),元数据就绪后再进入下载。
|
||||
let engine2 = self.clone();
|
||||
let id2 = id.clone();
|
||||
let url2 = task.url.clone();
|
||||
let dir2 = task.dir.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = engine2.reenter_bt(&id2, &url2, &dir2).await {
|
||||
crate::logger::log_error("download", &format!("BT 任务重入会话失败: {}", e));
|
||||
let mut tasks = engine2.inner.tasks.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(t) = tasks.get_mut(&id2) {
|
||||
if t.status == TaskStatus::Active || t.status == TaskStatus::Queued {
|
||||
t.status = TaskStatus::Error;
|
||||
t.error = Some(e);
|
||||
}
|
||||
}
|
||||
engine2.persist_now();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.persist_now();
|
||||
engine2.start_download(id2);
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -414,13 +414,21 @@ impl TorrentDownloader {
|
||||
.map_err(|e| format!("暂停种子失败: {}", e))
|
||||
}
|
||||
|
||||
/// 继续种子
|
||||
/// 继续种子(幂等):恢复已暂停的种子。
|
||||
/// 若种子本已处于 Live(例如 select_bt_files 的 set_only_files 内部已恢复过、
|
||||
/// 或暂停期间被其他逻辑恢复),直接视为成功,避免 librqbit 报 "torrent is already live"
|
||||
/// 而导致 pause→resume 链路被误判为失败。
|
||||
pub async fn unpause(&self, handle: &Arc<ManagedTorrent>) -> Result<(), String> {
|
||||
let session = self.get_session().await?;
|
||||
session
|
||||
.unpause(handle)
|
||||
.await
|
||||
.map_err(|e| format!("继续种子失败: {}", e))
|
||||
match handle.stats().state {
|
||||
librqbit::TorrentStatsState::Live => Ok(()),
|
||||
librqbit::TorrentStatsState::Error => Err("种子处于错误状态,无法继续".to_string()),
|
||||
librqbit::TorrentStatsState::Paused
|
||||
| librqbit::TorrentStatsState::Initializing { .. } => session
|
||||
.unpause(handle)
|
||||
.await
|
||||
.map_err(|e| format!("继续种子失败: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除种子(info_hash 为 hex 字符串;delete_files 是否同时删除已下载文件)
|
||||
|
||||
+14
-5
@@ -43,7 +43,8 @@ use monitor_kernel::{
|
||||
use network_monitor::network_status;
|
||||
use osd_window::{
|
||||
osd_apply_overlay_style, osd_begin_drag, osd_set_bounds, osd_set_click_through,
|
||||
osd_set_topmost, osd_start_drag_watch, osd_start_topmost_watch, osd_stop_watch,
|
||||
osd_set_topmost, osd_start_drag_watch, osd_start_game_watch, osd_start_topmost_watch,
|
||||
osd_stop_watch,
|
||||
};
|
||||
use process_manager::{
|
||||
process_all_status, process_start, process_status,
|
||||
@@ -84,7 +85,10 @@ use quickpanel::{
|
||||
quickpanel_show_window, quickpanel_unregister_shortcut, quickpanel_focus_main_window,
|
||||
};
|
||||
use tray_menu::{tray_menu_action, tray_menu_hide, tray_menu_ready};
|
||||
use updater::{app_version, update_check, update_install, update_thinghk};
|
||||
use updater::{
|
||||
app_version, update_check, update_install, update_thinghk_apply, update_thinghk_cancel,
|
||||
update_thinghk_confirm, ThinghkUpdateState,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
fn quit_app(app: tauri::AppHandle) {
|
||||
@@ -109,8 +113,9 @@ fn export_bindings() {
|
||||
// 生成命令失败时直接 throw,与原生 invoke 一致,前端无需解包 helper
|
||||
.error_handling(ErrorHandlingMode::Throw)
|
||||
.commands(collect_commands![
|
||||
// 应用更新(4)
|
||||
app_version, update_check, update_install, update_thinghk,
|
||||
// 应用更新(6)
|
||||
app_version, update_check, update_install, update_thinghk_apply,
|
||||
update_thinghk_confirm, update_thinghk_cancel,
|
||||
// proxy(20)
|
||||
proxy_activate_profile, proxy_apply_kernel_update, proxy_cancel_kernel_install, proxy_check_kernel_update, proxy_clear_system_proxy,
|
||||
proxy_close_connection, proxy_confirm_install, proxy_delete_profile, proxy_get_settings,
|
||||
@@ -175,12 +180,15 @@ pub fn run() {
|
||||
.build()
|
||||
)
|
||||
.manage(ProcessManager::new())
|
||||
.manage(ThinghkUpdateState::new())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
quit_app,
|
||||
app_version,
|
||||
update_check,
|
||||
update_install,
|
||||
update_thinghk,
|
||||
update_thinghk_apply,
|
||||
update_thinghk_confirm,
|
||||
update_thinghk_cancel,
|
||||
process_start,
|
||||
process_stop,
|
||||
process_status,
|
||||
@@ -238,6 +246,7 @@ pub fn run() {
|
||||
osd_set_topmost,
|
||||
osd_start_drag_watch,
|
||||
osd_start_topmost_watch,
|
||||
osd_start_game_watch,
|
||||
osd_stop_watch,
|
||||
downloader_get_tasks,
|
||||
downloader_add_task,
|
||||
|
||||
@@ -228,6 +228,8 @@ pub fn check_and_relaunch_if_needed(app_data_dir: &std::path::Path) -> bool {
|
||||
pub struct KernelStatus {
|
||||
pub ready: bool,
|
||||
pub is_admin: bool,
|
||||
/// PawnIO 驱动是否已安装;旧版内核无此字段,Option 兼容
|
||||
pub pawn_io_installed: Option<bool>,
|
||||
pub uptime_ms: f64,
|
||||
pub group_count: u32,
|
||||
pub sensor_count: u32,
|
||||
@@ -370,7 +372,9 @@ impl MonitorKernel {
|
||||
self.root.join("hardware-config.json")
|
||||
}
|
||||
|
||||
/// 确保内核就位:若 cores/ 无内核或版本过期(源文件较新),从资源目录复制
|
||||
/// 确保内核就位:若 cores/ 无内核或版本过期(源文件较新),从资源目录复制。
|
||||
/// 同时把 PawnIO_setup.exe(可选资源)复制过去——内核提权启动时会静默安装它,
|
||||
/// 作为 WinRing0 被系统/杀软拦截时读取温度/频率的替代驱动。
|
||||
pub fn prepare_kernel(&self, app: &AppHandle) -> Result<MonitorKernelInfo, String> {
|
||||
let kernel = self.kernel_path();
|
||||
if let Ok(src) = app.path().resolve("binaries/ThingHK.exe", BaseDirectory::Resource) {
|
||||
@@ -385,6 +389,24 @@ impl MonitorKernel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PawnIO 安装器:可选资源,缺失时仅影响自动安装能力(不影响内核运行)
|
||||
if let Ok(setup_src) = app.path().resolve("binaries/PawnIO_setup.exe", BaseDirectory::Resource) {
|
||||
if setup_src.exists() {
|
||||
let setup_dest = self.cores_dir().join("PawnIO_setup.exe");
|
||||
let need_copy = !setup_dest.exists()
|
||||
|| fs::metadata(&setup_src)
|
||||
.and_then(|s| fs::metadata(&setup_dest).map(|d| s.len() != d.len()))
|
||||
.unwrap_or(true);
|
||||
if need_copy {
|
||||
fs::create_dir_all(self.cores_dir()).ok();
|
||||
if let Err(e) = fs::copy(&setup_src, &setup_dest) {
|
||||
crate::logger::log_warn("monitor", &format!("复制 PawnIO_setup.exe 失败: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MonitorKernelInfo {
|
||||
path: kernel.to_string_lossy().to_string(),
|
||||
exists: kernel.exists(),
|
||||
@@ -489,12 +511,20 @@ impl MonitorKernel {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<KernelStatus>().await {
|
||||
Ok(s) if s.ready => {
|
||||
// PawnIO 诊断:已提权但驱动缺失时,温度/频率等 ring0 传感器大概率无法读取
|
||||
if s.is_admin && s.pawn_io_installed == Some(false) {
|
||||
crate::logger::log_warn(
|
||||
"monitor",
|
||||
"Kernel 已提权但 PawnIO 驱动未安装,CPU 温度/频率可能无法读取(检查 cores/PawnIO_setup.exe 是否随包部署)",
|
||||
);
|
||||
}
|
||||
let _ = app.emit(
|
||||
crate::constants::events::MONITOR_READY,
|
||||
serde_json::json!({
|
||||
"isAdmin": s.is_admin,
|
||||
"sensorCount": s.sensor_count,
|
||||
"providers": s.providers,
|
||||
"pawnIoInstalled": s.pawn_io_installed,
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
|
||||
+150
-4
@@ -15,9 +15,12 @@ use tauri::{AppHandle, Emitter};
|
||||
static DRAG_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||
/// 任务栏覆盖监视线程停止标志
|
||||
static TOPMOST_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||
/// 游戏全屏监视线程停止标志
|
||||
static GAME_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||
/// 监视线程句柄(用于停止时 join,避免 sleep 猜测式等待 + 线程泄漏)
|
||||
static DRAG_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
|
||||
static TOPMOST_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
|
||||
static GAME_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
|
||||
|
||||
fn drag_stop() -> &'static Arc<AtomicBool> {
|
||||
DRAG_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
|
||||
@@ -27,6 +30,10 @@ fn topmost_stop() -> &'static Arc<AtomicBool> {
|
||||
TOPMOST_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
|
||||
}
|
||||
|
||||
fn game_stop() -> &'static Arc<AtomicBool> {
|
||||
GAME_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
|
||||
}
|
||||
|
||||
/// 停止右键拖动监视线程并等待其退出(标志置位后线程最迟一个轮询周期退出)
|
||||
fn stop_drag_thread() {
|
||||
drag_stop().store(true, Ordering::SeqCst);
|
||||
@@ -51,18 +58,35 @@ fn stop_topmost_thread() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止游戏全屏监视线程并等待其退出
|
||||
fn stop_game_thread() {
|
||||
game_stop().store(true, Ordering::SeqCst);
|
||||
if let Some(h) = GAME_HANDLE
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take()
|
||||
{
|
||||
let _ = h.join();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win_api {
|
||||
use tauri::{AppHandle, Manager};
|
||||
use windows_sys::Win32::Foundation::{POINT, RECT};
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST,
|
||||
};
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_RBUTTON};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
GetClassNameW, GetCursorPos, GetForegroundWindow, GetWindowLongPtrW,
|
||||
GetWindowRect, SendMessageW, SetWindowLongPtrW, SetWindowPos,
|
||||
GWL_EXSTYLE, HTCAPTION, HWND_NOTOPMOST, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE,
|
||||
SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN, WS_EX_NOACTIVATE,
|
||||
GetClassNameW, GetCursorPos, GetForegroundWindow, GetWindowLongPtrW, GetWindowLongW,
|
||||
GetWindowRect, GetWindowThreadProcessId, SendMessageW, SetWindowLongPtrW, SetWindowPos,
|
||||
GWL_EXSTYLE, GWL_STYLE, HTCAPTION, HWND_NOTOPMOST, HWND_TOPMOST, SWP_NOACTIVATE,
|
||||
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN, WS_EX_NOACTIVATE,
|
||||
WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT,
|
||||
};
|
||||
/// 供模块外全屏判定使用的窗口样式常量(pub re-export)
|
||||
pub use windows_sys::Win32::UI::WindowsAndMessaging::WS_CAPTION;
|
||||
|
||||
/// windows-sys 的 HWND 类型别名(isize)
|
||||
pub type Hwnd = isize;
|
||||
@@ -225,6 +249,41 @@ mod win_api {
|
||||
| "Windows.UI.Shell.ShellFlyoutWindow" // Win11 Shell 弹出
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取窗口样式(GWL_STYLE)
|
||||
pub fn get_window_style(hwnd: Hwnd) -> isize {
|
||||
unsafe { GetWindowLongW(hwnd, GWL_STYLE) as isize }
|
||||
}
|
||||
|
||||
/// 判断窗口是否属于本进程(Thing 自身窗口不参与全屏判定)
|
||||
pub fn is_own_process(hwnd: Hwnd) -> bool {
|
||||
let mut pid: u32 = 0;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
}
|
||||
pid != 0 && pid == std::process::id()
|
||||
}
|
||||
|
||||
/// 获取窗口所在显示器(最近匹配)的矩形(物理像素)
|
||||
pub fn get_monitor_rect(hwnd: Hwnd) -> Option<RECT> {
|
||||
let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) };
|
||||
if monitor == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut info = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
rcMonitor: RECT { left: 0, top: 0, right: 0, bottom: 0 },
|
||||
rcWork: RECT { left: 0, top: 0, right: 0, bottom: 0 },
|
||||
dwFlags: 0,
|
||||
};
|
||||
unsafe {
|
||||
if GetMonitorInfoW(monitor, &mut info) != 0 {
|
||||
Some(info.rcMonitor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用 OSD 悬浮窗的原生样式(NoActivate + ToolWindow)
|
||||
@@ -369,11 +428,98 @@ pub fn osd_start_topmost_watch(app: AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 判断窗口是否为全屏应用(无边框/独占全屏游戏)
|
||||
///
|
||||
/// 判定条件(全部满足):
|
||||
/// 1. 无 WS_CAPTION 样式 —— 排除普通应用的"最大化"(即使系统任务栏设为自动隐藏,
|
||||
/// 最大化窗口覆盖率也接近 100%,但它们带标题栏,靠样式即可区分)
|
||||
/// 2. 非本进程窗口(Thing 主窗口/悬浮窗自身)
|
||||
/// 3. 窗口矩形与所在显示器矩形的交集覆盖率 ≥ 95%(兼容缩放/1px 误差)
|
||||
#[cfg(windows)]
|
||||
fn is_fullscreen_game_window(hwnd: isize) -> bool {
|
||||
if win_api::get_window_style(hwnd) & (win_api::WS_CAPTION as isize) != 0 {
|
||||
return false;
|
||||
}
|
||||
if win_api::is_own_process(hwnd) {
|
||||
return false;
|
||||
}
|
||||
let (Some(win_rect), Some(mon_rect)) = (
|
||||
win_api::get_window_rect(hwnd),
|
||||
win_api::get_monitor_rect(hwnd),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let iw = (win_rect.right.min(mon_rect.right) - win_rect.left.max(mon_rect.left)).max(0) as i64;
|
||||
let ih = (win_rect.bottom.min(mon_rect.bottom) - win_rect.top.max(mon_rect.top)).max(0) as i64;
|
||||
let mw = (mon_rect.right - mon_rect.left).max(0) as i64;
|
||||
let mh = (mon_rect.bottom - mon_rect.top).max(0) as i64;
|
||||
let mon_area = (mw * mh).max(1);
|
||||
iw * ih * 100 >= mon_area * 95
|
||||
}
|
||||
|
||||
/// 启动游戏全屏监视
|
||||
///
|
||||
/// 轮询检测前景窗口是否为全屏应用(无边框/独占全屏游戏),
|
||||
/// 状态变化时发出 `osd-game-active` / `osd-game-inactive` 事件。
|
||||
/// 前端据此隐藏/恢复 OSD:透明置顶 WebView 悬浮窗会占用 DWM 合成路径,
|
||||
/// 禁用游戏的独立翻转(MPO),是游戏中掉帧的根源;隐藏悬浮窗即可排除影响。
|
||||
#[tauri::command]
|
||||
pub fn osd_start_game_watch(app: AppHandle) -> Result<(), String> {
|
||||
// 停止旧线程,等待其退出后再启动新线程
|
||||
stop_game_thread();
|
||||
let stop_flag = game_stop().clone();
|
||||
stop_flag.store(false, Ordering::SeqCst);
|
||||
|
||||
let app_handle = app.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let mut fullscreen_active = false;
|
||||
|
||||
loop {
|
||||
if stop_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let fg = win_api::get_foreground_window();
|
||||
let fullscreen = fg != 0
|
||||
&& win_api::get_class_name(fg)
|
||||
.map_or(false, |c| !win_api::is_system_ui_class(&c))
|
||||
&& is_fullscreen_game_window(fg);
|
||||
|
||||
if fullscreen != fullscreen_active {
|
||||
fullscreen_active = fullscreen;
|
||||
let event = if fullscreen {
|
||||
crate::constants::events::OSD_GAME_ACTIVE
|
||||
} else {
|
||||
crate::constants::events::OSD_GAME_INACTIVE
|
||||
};
|
||||
let _ = app_handle.emit(event, ());
|
||||
crate::logger::log_info(
|
||||
"osd",
|
||||
&format!("全屏应用前台: {}", if fullscreen { "是 → 隐藏 OSD" } else { "否 → 恢复 OSD" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(mut guard) = GAME_HANDLE.lock() {
|
||||
*guard = Some(handle);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止所有 OSD 监视线程
|
||||
#[tauri::command]
|
||||
pub fn osd_stop_watch() {
|
||||
stop_drag_thread();
|
||||
stop_topmost_thread();
|
||||
stop_game_thread();
|
||||
}
|
||||
|
||||
/// 设置点击穿透(Rust 侧原生 WS_EX_TRANSPARENT,比 JS setIgnoreCursorEvents 更可靠)
|
||||
|
||||
@@ -11,6 +11,11 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
// CREATE_NO_WINDOW = 0x08000000,阻止子进程创建新的控制台窗口
|
||||
#[cfg(windows)]
|
||||
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
// CREATE_NEW_CONSOLE = 0x00000010,强制为控制台类子进程新开一个可见控制台窗口。
|
||||
// 从 GUI 宿主(无控制台)直接 spawn cmd/powershell 等控制台程序时若不设置,
|
||||
// 子进程会挂到隐藏控制台/不显示窗口,表现为"点击没反应"。
|
||||
#[cfg(windows)]
|
||||
pub const CREATE_NEW_CONSOLE: u32 = 0x00000010;
|
||||
|
||||
// Windows Job Object 相关常量,用于异常退出时自动清理子进程
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -505,11 +505,35 @@ pub fn quickpanel_run_custom_command(command: String, args: Vec<String>) -> Resu
|
||||
|
||||
/// 运行系统命令(不设置 CREATE_NO_WINDOW,使 cmd/powershell/regedit 等显示自身窗口)
|
||||
/// 适用于内置系统工具:regedit、shutdown、cmd、powershell、taskmgr 等。
|
||||
/// - 控制台类交互程序(cmd/powershell/pwsh)额外设置 CREATE_NEW_CONSOLE,
|
||||
/// 否则从 GUI 宿主启动时无可见控制台窗口(表现为"点击没反应")。
|
||||
/// - .msc 控制台文件(如 devmgmt.msc)不可被 CreateProcess 直接执行,
|
||||
/// 改由 mmc 打开(路径解析到 System32,不受当前工作目录影响)。
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn quickpanel_run_system_command(command: String, args: Vec<String>) -> Result<(), String> {
|
||||
let lower = command.to_lowercase();
|
||||
if lower.ends_with(".msc") {
|
||||
// 控制台文件:通过 mmc 打开(GUI 程序,无需新控制台)
|
||||
let system_root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
let path = format!("{}\\System32\\{}", system_root, command);
|
||||
let mut c = std::process::Command::new("mmc");
|
||||
c.arg(&path);
|
||||
return c
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("运行系统命令失败: {}", e));
|
||||
}
|
||||
let mut cmd = std::process::Command::new(&command);
|
||||
cmd.args(&args);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
let c = lower;
|
||||
if c == "cmd" || c == "powershell" || c == "pwsh" {
|
||||
cmd.creation_flags(crate::process_manager::CREATE_NEW_CONSOLE);
|
||||
}
|
||||
}
|
||||
cmd.spawn().map_err(|e| format!("运行系统命令失败: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+128
-101
@@ -2,18 +2,65 @@
|
||||
//! 更新源为自建 Gitea:`https://gitea.atie.fun/LFeng/Thing` 的 release 资产。
|
||||
//! - 便携版(无 unins000.exe 且不在 Program Files):下载新 thing.exe → update.bat 覆盖重启
|
||||
//! - 安装版(NSIS):下载新 setup.exe → 提权静默安装 /S
|
||||
//! - ThingHK 内核:下载 thing-hk_{v}.zip → 停止监控内核 → 覆盖 {app_data}/monitor/cores/ThingHK.exe
|
||||
//! mihomo 内核更新继续复用代理模块已有的 GitHub 下载机制,不在此模块处理。
|
||||
use futures_util::StreamExt;
|
||||
//! - ThingHK 内核:下载由前端下载模块完成 → apply 命令 need_stop 等待确认 → 解压覆盖
|
||||
//! {app_data}/monitor/cores/ThingHK.exe(与代理模块 mihomo 内核更新同模式)
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{atomic::{AtomicBool, Ordering}, Mutex};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::{oneshot, watch};
|
||||
|
||||
use crate::constants::events::UPDATE_PROGRESS;
|
||||
|
||||
/// 用户主动取消 ThingHK 更新的标记错误信息(前端据此静默处理,不弹错误 toast)
|
||||
const THINGHK_UPDATE_CANCELLED: &str = "更新已取消";
|
||||
|
||||
/// ThingHK 内核更新的跨命令状态:apply 过程中 need_stop 阶段等待前端确认。
|
||||
/// 与 MihomoManager 的 install_confirm/kernel_cancel 同构。
|
||||
pub struct ThinghkUpdateState {
|
||||
/// need_stop 等待阶段的确认通道(前端调 confirm 命令时唤醒 apply 继续)
|
||||
confirm_tx: Mutex<Option<oneshot::Sender<()>>>,
|
||||
/// 取消标志 + 唤醒通道(前端调 cancel 命令时置位,apply 等待循环立即返回)
|
||||
cancel_flag: AtomicBool,
|
||||
cancel_tx: watch::Sender<bool>,
|
||||
cancel_rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
impl ThinghkUpdateState {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = watch::channel(false);
|
||||
Self {
|
||||
confirm_tx: Mutex::new(None),
|
||||
cancel_flag: AtomicBool::new(false),
|
||||
cancel_tx: tx,
|
||||
cancel_rx: rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端已停止监控内核,唤醒 apply 继续解压替换
|
||||
fn confirm(&self) {
|
||||
if let Some(tx) = self.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消更新:置位取消标志并唤醒 apply 等待循环
|
||||
fn cancel(&self) {
|
||||
self.cancel_flag.store(true, Ordering::SeqCst);
|
||||
let _ = self.cancel_tx.send(true);
|
||||
}
|
||||
|
||||
/// 进入新的 apply 流程前复位取消标志
|
||||
fn reset(&self) {
|
||||
self.cancel_flag.store(false, Ordering::SeqCst);
|
||||
let _ = self.cancel_tx.send(false);
|
||||
*self.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// 发布仓库(Gitea)
|
||||
const GITEA_REPO: &str = "LFeng/Thing";
|
||||
const GITEA_BASE: &str = "https://gitea.atie.fun";
|
||||
@@ -125,72 +172,8 @@ async fn fetch_latest_release() -> Result<LatestRelease, String> {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 下载 / 解压 ----------
|
||||
|
||||
/// 下载文件到 dest,期间通过 UPDATE_PROGRESS 事件上报进度
|
||||
async fn download_with_progress(app: &AppHandle, url: &str, dest: &Path) -> Result<(), String> {
|
||||
// 注意:reqwest 的 timeout 是"从连接到响应体读完"的总超时。大文件(如
|
||||
// thing.exe 10+MB)在慢速网络下 30s 内读不完会被掐断流导致下载到一半失败
|
||||
// (此前 bug:更新包总在 ~50% 处中断)。这里只限制连接建立 15s,
|
||||
// 总超时放宽到 10 分钟兜底防止永久悬挂。
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(std::time::Duration::from_secs(15))
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.build()
|
||||
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
|
||||
let resp = client
|
||||
.get(url)
|
||||
.header("User-Agent", "thing-app")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("下载请求失败: {}", e))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("下载失败: HTTP {}", resp.status()));
|
||||
}
|
||||
let total = resp.content_length();
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut file = fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?;
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut last_percent: u8 = 0;
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|e| format!("读取下载流失败: {}", e))?;
|
||||
file.write_all(&chunk).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||
downloaded += chunk.len() as u64;
|
||||
let percent = match total {
|
||||
Some(t) if t > 0 => ((downloaded as f64 / t as f64) * 100.0) as u8,
|
||||
_ => 0,
|
||||
};
|
||||
if percent >= last_percent + 1 {
|
||||
last_percent = percent;
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
stage: "downloading".into(),
|
||||
percent,
|
||||
downloaded_bytes: downloaded,
|
||||
total_bytes: total,
|
||||
message: format!(
|
||||
"已下载 {:.2} MB / {:.2} MB",
|
||||
downloaded as f64 / 1024.0 / 1024.0,
|
||||
total.unwrap_or(0) as f64 / 1024.0 / 1024.0
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
file.flush().map_err(|e| format!("flush 失败: {}", e))?;
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
stage: "downloaded".into(),
|
||||
percent: 100,
|
||||
downloaded_bytes: downloaded,
|
||||
total_bytes: total,
|
||||
message: "下载完成".into(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// ---------- 解压 ----------
|
||||
// ThingHK 内核更新包由前端下载模块负责下载(同 mihomo),此处仅解压替换。
|
||||
|
||||
/// 用 zip crate 解压(纯 Rust,避免 PowerShell 执行策略问题)
|
||||
fn extract_zip(zip_path: &Path, dest: &Path) -> Result<(), String> {
|
||||
@@ -377,47 +360,72 @@ pub async fn update_install(app: AppHandle, downloaded_path: String) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新 ThingHK 内核:下载 thing-hk_{v}.zip → 停止监控内核 → 覆盖内核文件
|
||||
/// 应用 ThingHK 内核更新:下载阶段已由下载模块完成,本命令仅做
|
||||
/// need_stop(等待前端停止监控内核并确认)→ 解压 → 替换。
|
||||
/// 进度通过 UPDATE_PROGRESS 事件上报,前端据 need_stop 弹出确认对话框。
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_thinghk(app: AppHandle) -> Result<(), String> {
|
||||
let result = update_thinghk_inner(&app).await;
|
||||
pub async fn update_thinghk_apply(
|
||||
app: AppHandle,
|
||||
state: tauri::State<'_, ThinghkUpdateState>,
|
||||
zip_path: String,
|
||||
) -> Result<(), String> {
|
||||
state.reset();
|
||||
let result = update_thinghk_apply_inner(&app, &state, PathBuf::from(&zip_path)).await;
|
||||
if let Err(ref e) = result {
|
||||
// 失败时 emit error 阶段,避免前端进度卡在最后状态无提示
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
stage: "error".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: e.clone(),
|
||||
},
|
||||
);
|
||||
// 取消是用户主动行为,静默返回即可;其余失败 emit error 阶段避免前端进度卡死
|
||||
if e != THINGHK_UPDATE_CANCELLED {
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
stage: "error".into(),
|
||||
percent: 0,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: e.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
|
||||
let latest = fetch_latest_release().await?;
|
||||
let asset = latest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.starts_with("thing-hk_") && a.name.ends_with(".zip"))
|
||||
.ok_or("未在 release 中找到 ThingHK 内核包".to_string())?;
|
||||
// 停止监控内核(含提权模式的 /shutdown 兜底由前端先停模块),避免 exe 被占用
|
||||
if let Some(monitor) = app.try_state::<crate::monitor_kernel::MonitorKernel>() {
|
||||
monitor.stop_subscription(app).await;
|
||||
async fn update_thinghk_apply_inner(
|
||||
app: &AppHandle,
|
||||
state: &ThinghkUpdateState,
|
||||
zip_path: PathBuf,
|
||||
) -> Result<(), String> {
|
||||
if !zip_path.exists() {
|
||||
return Err(format!("下载文件不存在: {}", zip_path.display()));
|
||||
}
|
||||
if let Some(pm) = app.try_state::<crate::process_manager::ProcessManager>() {
|
||||
let _ = pm.stop("monitor");
|
||||
|
||||
// need_stop:等待前端停止监控内核并确认(exe 被占用会导致覆盖失败)。
|
||||
// 确认/取消由 confirm/cancel 命令跨命令唤醒(与 mihomo need_stop 同构)。
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
stage: "need_stop".into(),
|
||||
percent: 90,
|
||||
downloaded_bytes: 0,
|
||||
total_bytes: None,
|
||||
message: "需要停止监控内核才能继续安装".into(),
|
||||
},
|
||||
);
|
||||
let (tx, mut rx) = oneshot::channel::<()>();
|
||||
*state.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = Some(tx);
|
||||
let mut cancel_rx = state.cancel_rx.clone();
|
||||
loop {
|
||||
if state.cancel_flag.load(Ordering::SeqCst) {
|
||||
*state.confirm_tx.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
return Err(THINGHK_UPDATE_CANCELLED.to_string());
|
||||
}
|
||||
tokio::select! {
|
||||
_ = &mut rx => break,
|
||||
_ = cancel_rx.changed() => {}
|
||||
}
|
||||
}
|
||||
// 等待进程退出释放文件句柄
|
||||
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
|
||||
let temp_dir = std::env::temp_dir().join("thing-update");
|
||||
fs::create_dir_all(&temp_dir).map_err(|e| format!("创建临时目录失败: {}", e))?;
|
||||
let zip_path = temp_dir.join(&asset.name);
|
||||
download_with_progress(app, &asset.browser_download_url, &zip_path).await?;
|
||||
|
||||
// 解压阶段
|
||||
let _ = app.emit(
|
||||
UPDATE_PROGRESS,
|
||||
UpdateProgress {
|
||||
@@ -428,9 +436,11 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
|
||||
message: "正在解压内核...".into(),
|
||||
},
|
||||
);
|
||||
let temp_dir = std::env::temp_dir().join("thing-update");
|
||||
let extract_dir = temp_dir.join("thinghk_extract");
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
extract_zip(&zip_path, &extract_dir)?;
|
||||
|
||||
// 在解压目录中查找 ThingHK.exe
|
||||
let exe_path = find_thinghk_exe(&extract_dir).ok_or("内核包中未找到 ThingHK.exe".to_string())?;
|
||||
let app_data = app
|
||||
@@ -441,6 +451,7 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
|
||||
fs::create_dir_all(&cores_dir).map_err(|e| format!("创建内核目录失败: {}", e))?;
|
||||
fs::copy(&exe_path, cores_dir.join("ThingHK.exe"))
|
||||
.map_err(|e| format!("覆盖内核文件失败(请确认监控模块已停止): {}", e))?;
|
||||
|
||||
// 清理临时文件
|
||||
let _ = fs::remove_file(&zip_path);
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
@@ -457,6 +468,22 @@ async fn update_thinghk_inner(app: &AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 前端已停止监控内核,确认继续解压替换(唤醒 need_stop 等待)
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_thinghk_confirm(state: tauri::State<'_, ThinghkUpdateState>) -> Result<(), String> {
|
||||
state.confirm();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消 ThingHK 内核更新(need_stop 等待阶段有效:唤醒 apply 以「已取消」返回,zip 保留便于重试)
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn update_thinghk_cancel(state: tauri::State<'_, ThinghkUpdateState>) -> Result<(), String> {
|
||||
state.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_thinghk_exe(dir: &Path) -> Option<PathBuf> {
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"identifier": "thing.lfeng.me",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"devUrl": "http://localhost:14210",
|
||||
"beforeBuildCommand": "bun run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user