修复应用多开的情况,增加更新模块的日志

master
cysamurai 2 months ago
parent fb60188b79
commit 9148bdacb5

@ -1,7 +1,7 @@
{ {
"name": "broadcast-client", "name": "broadcast-client",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

@ -127,6 +127,7 @@ name = "broadcast-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"fs2",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@ -753,6 +754,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@ -1518,6 +1529,8 @@ version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [ dependencies = [
"cfg-if",
"futures-util",
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1794,6 +1807,17 @@ dependencies = [
"objc_exception", "objc_exception",
] ]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]] [[package]]
name = "objc_exception" name = "objc_exception"
version = "0.1.2" version = "0.1.2"
@ -2328,6 +2352,30 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rfd"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea"
dependencies = [
"block",
"dispatch",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"lazy_static",
"log",
"objc",
"objc-foundation",
"objc_id",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.37.0",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -2850,6 +2898,7 @@ dependencies = [
"plist", "plist",
"rand 0.8.5", "rand 0.8.5",
"raw-window-handle", "raw-window-handle",
"rfd",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@ -3406,6 +3455,16 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.117" version = "0.2.117"
@ -3472,6 +3531,16 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "web-sys"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "webkit2gtk" name = "webkit2gtk"
version = "0.18.2" version = "0.18.2"
@ -3588,6 +3657,19 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
dependencies = [
"windows_aarch64_msvc 0.37.0",
"windows_i686_gnu 0.37.0",
"windows_i686_msvc 0.37.0",
"windows_x86_64_gnu 0.37.0",
"windows_x86_64_msvc 0.37.0",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.39.0" version = "0.39.0"
@ -3750,6 +3832,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.39.0" version = "0.39.0"
@ -3762,6 +3850,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.39.0" version = "0.39.0"
@ -3774,6 +3868,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.39.0" version = "0.39.0"
@ -3786,6 +3886,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.39.0" version = "0.39.0"
@ -3804,6 +3910,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.39.0" version = "0.39.0"

@ -1,6 +1,6 @@
[package] [package]
name = "broadcast-client" name = "broadcast-client"
version = "0.1.0" version = "0.1.1"
description = "Broadcast Ruler Client" description = "Broadcast Ruler Client"
authors = ["team"] authors = ["team"]
edition = "2021" edition = "2021"
@ -17,7 +17,8 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[dependencies] [dependencies]
tauri = { version = "1", features = ["window-all"] } tauri = { version = "1", features = ["window-all", "dialog"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
chrono = { version = "0.4", features = ["clock"] } chrono = { version = "0.4", features = ["clock"] }
fs2 = "0.4"

@ -1,5 +1,6 @@
use std::{ use std::{
fs::{create_dir_all, read_dir, remove_file, OpenOptions}, fs::{create_dir_all, read_dir, remove_file, File, OpenOptions},
io,
io::Read, io::Read,
io::Write, io::Write,
net::TcpListener, net::TcpListener,
@ -13,6 +14,7 @@ use std::{
}; };
use chrono::Local; use chrono::Local;
use fs2::FileExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::Manager; use tauri::Manager;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -26,6 +28,30 @@ const LOG_FILE_EXT: &str = ".log";
const LOG_FILE_MAX_BYTES: u64 = 5 * 1024 * 1024; const LOG_FILE_MAX_BYTES: u64 = 5 * 1024 * 1024;
const LOG_RETENTION_DAYS: u64 = 7; const LOG_RETENTION_DAYS: u64 = 7;
#[allow(dead_code)]
struct SingleInstanceLock(File);
fn acquire_single_instance_lock(app: &tauri::App) -> Result<SingleInstanceLock, String> {
let base_dir = app.path_resolver().app_data_dir().ok_or("无法获取应用数据目录")?;
create_dir_all(&base_dir).map_err(|err| format!("创建锁目录失败: {err}"))?;
let lock_path = base_dir.join("single-instance.lock");
let lock_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(lock_path)
.map_err(|err| format!("打开锁文件失败: {err}"))?;
match lock_file.try_lock_exclusive() {
Ok(()) => Ok(SingleInstanceLock(lock_file)),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
Err("duplicate instance".to_string())
}
Err(err) => Err(format!("加锁失败: {err}")),
}
}
#[derive(Default)] #[derive(Default)]
struct SocketServiceState { struct SocketServiceState {
runtime: Mutex<Option<SocketServiceRuntime>>, runtime: Mutex<Option<SocketServiceRuntime>>,
@ -98,6 +124,22 @@ struct AptUpdateCheckResult {
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| match acquire_single_instance_lock(app) {
Ok(lock) => {
app.manage(lock);
Ok(())
}
Err(reason) if reason == "duplicate instance" => {
tauri::api::dialog::blocking::message(
None::<&tauri::Window>,
"提示",
"请勿重复打开",
);
app.handle().exit(0);
Ok(())
}
Err(reason) => Err(reason.into()),
})
.manage(SocketServiceState::default()) .manage(SocketServiceState::default())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
start_socket_service, start_socket_service,
@ -245,10 +287,31 @@ fn extract_policy_value(output: &str, key: &str) -> String {
} }
#[tauri::command] #[tauri::command]
fn check_apt_update(package_name: String, current_version: String) -> Result<AptUpdateCheckResult, String> { fn check_apt_update(
app: tauri::AppHandle,
package_name: String,
current_version: String,
) -> Result<AptUpdateCheckResult, String> {
append_socket_log(
&app,
"INFO",
format!(
"检查更新开始 package={} current_version={}",
package_name, current_version
),
);
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
let _ = (&package_name, &current_version); let _ = (&package_name, &current_version);
append_socket_log(
&app,
"WARN",
format!(
"检查更新失败 package={} reason=当前系统不支持 apt 更新检测,仅支持 Linux",
package_name
),
);
return Err("当前系统不支持 apt 更新检测,仅支持 Linux。".to_string()); return Err("当前系统不支持 apt 更新检测,仅支持 Linux。".to_string());
} }
@ -258,15 +321,29 @@ fn check_apt_update(package_name: String, current_version: String) -> Result<Apt
.arg("policy") .arg("policy")
.arg(&package_name) .arg(&package_name)
.output() .output()
.map_err(|error| format!("执行 apt-cache 失败: {error}"))?; .map_err(|error| {
let message = format!("执行 apt-cache 失败: {error}");
append_socket_log(
&app,
"ERROR",
format!("检查更新失败 package={} {}", package_name, message),
);
message
})?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(if stderr.is_empty() { let message = if stderr.is_empty() {
"apt-cache policy 执行失败".to_string() "apt-cache policy 执行失败".to_string()
} else { } else {
format!("apt-cache policy 执行失败: {stderr}") format!("apt-cache policy 执行失败: {stderr}")
}); };
append_socket_log(
&app,
"ERROR",
format!("检查更新失败 package={} {}", package_name, message),
);
return Err(message);
} }
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@ -286,7 +363,7 @@ fn check_apt_update(package_name: String, current_version: String) -> Result<Apt
package_name package_name
); );
Ok(AptUpdateCheckResult { let result = AptUpdateCheckResult {
package_name, package_name,
current_version, current_version,
installed_version, installed_version,
@ -294,7 +371,22 @@ fn check_apt_update(package_name: String, current_version: String) -> Result<Apt
has_update, has_update,
source_available, source_available,
update_command, update_command,
}) };
append_socket_log(
&app,
"INFO",
format!(
"检查更新完成 package={} installed_version={} candidate_version={} has_update={} source_available={}",
result.package_name,
result.installed_version,
result.candidate_version,
result.has_update,
result.source_available
),
);
Ok(result)
} }
} }

@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "broadcast-client", "productName": "broadcast-client",
"version": "0.1.0" "version": "0.1.1"
}, },
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

@ -561,12 +561,18 @@ async function handleCheckUpdate() {
checkingUpdate.value = true; checkingUpdate.value = true;
try { try {
console.info("[update] 检查更新开始", {
packageName: "broadcast-client",
currentVersion: appVersion.value,
});
const result = await invoke<AptUpdateCheckResult>("check_apt_update", { const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "broadcast-client", packageName: "broadcast-client",
currentVersion: "0.1.0", currentVersion: appVersion.value,
}); });
console.info("[update] 检查更新结果", result);
if (!result.sourceAvailable) { if (!result.sourceAvailable) {
console.warn("[update] 未检测到可用更新源", { packageName: result.packageName });
ElMessage.warning( ElMessage.warning(
`未检测到可用更新源,请先执行:${APT_SOURCE_SETUP_COMMAND}`, `未检测到可用更新源,请先执行:${APT_SOURCE_SETUP_COMMAND}`,
); );
@ -574,14 +580,21 @@ async function handleCheckUpdate() {
} }
if (!result.hasUpdate) { if (!result.hasUpdate) {
console.info("[update] 当前已是最新版本", { installedVersion: result.installedVersion });
ElMessage.success(`当前已是最新版本(${result.installedVersion}`); ElMessage.success(`当前已是最新版本(${result.installedVersion}`);
return; return;
} }
console.info("[update] 检测到新版本", {
installedVersion: result.installedVersion,
candidateVersion: result.candidateVersion,
updateCommand: result.updateCommand,
});
ElMessage.warning( ElMessage.warning(
`发现新版本 ${result.candidateVersion},请在终端执行:${result.updateCommand}`, `发现新版本 ${result.candidateVersion},请在终端执行:${result.updateCommand}`,
); );
} catch (error) { } catch (error) {
console.error("[update] 检查更新失败", error);
ElMessage.error(`检查更新失败:${String(error)}`); ElMessage.error(`检查更新失败:${String(error)}`);
} finally { } finally {
checkingUpdate.value = false; checkingUpdate.value = false;

@ -1,7 +1,7 @@
{ {
"name": "call-client", "name": "call-client",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

@ -394,6 +394,7 @@ dependencies = [
name = "call-client" name = "call-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"fs2",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@ -1075,6 +1076,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@ -2670,7 +2681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.48.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]

@ -1,6 +1,6 @@
[package] [package]
name = "call-client" name = "call-client"
version = "0.1.0" version = "0.1.1"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@ -25,4 +25,5 @@ custom-protocol = ["tauri/custom-protocol"]
tauri = { version = "1", features = ["api-all"] } tauri = { version = "1", features = ["api-all"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
fs2 = "0.4"

@ -1,6 +1,9 @@
use serde::Serialize; use serde::Serialize;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::process::Command; use std::process::Command;
use tauri::AppHandle;
use super::logger::app_log;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -24,10 +27,29 @@ fn extract_policy_value(output: &str, key: &str) -> String {
} }
#[tauri::command] #[tauri::command]
pub fn check_apt_update(package_name: String, current_version: String) -> Result<AptUpdateCheckResult, String> { pub fn check_apt_update(
_app: AppHandle,
package_name: String,
current_version: String,
) -> Result<AptUpdateCheckResult, String> {
let _ = app_log(
"info".to_string(),
format!(
"检查更新开始: package={}, current_version={}",
package_name, current_version
),
);
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
let _ = (&package_name, &current_version); let _ = (&package_name, &current_version);
let _ = app_log(
"warn".to_string(),
format!(
"检查更新失败: package={}, reason=当前系统不支持 apt 更新检测,仅支持 Linux",
package_name
),
);
return Err("当前系统不支持 apt 更新检测,仅支持 Linux。".to_string()); return Err("当前系统不支持 apt 更新检测,仅支持 Linux。".to_string());
} }
@ -37,15 +59,27 @@ pub fn check_apt_update(package_name: String, current_version: String) -> Result
.arg("policy") .arg("policy")
.arg(&package_name) .arg(&package_name)
.output() .output()
.map_err(|error| format!("执行 apt-cache 失败: {error}"))?; .map_err(|error| {
let message = format!("执行 apt-cache 失败: {error}");
let _ = app_log(
"error".to_string(),
format!("检查更新失败: package={}, {}", package_name, message),
);
message
})?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(if stderr.is_empty() { let message = if stderr.is_empty() {
"apt-cache policy 执行失败".to_string() "apt-cache policy 执行失败".to_string()
} else { } else {
format!("apt-cache policy 执行失败: {stderr}") format!("apt-cache policy 执行失败: {stderr}")
}); };
let _ = app_log(
"error".to_string(),
format!("检查更新失败: package={}, {}", package_name, message),
);
return Err(message);
} }
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@ -65,7 +99,7 @@ pub fn check_apt_update(package_name: String, current_version: String) -> Result
package_name package_name
); );
Ok(AptUpdateCheckResult { let result = AptUpdateCheckResult {
package_name, package_name,
current_version, current_version,
installed_version, installed_version,
@ -73,6 +107,20 @@ pub fn check_apt_update(package_name: String, current_version: String) -> Result
has_update, has_update,
source_available, source_available,
update_command, update_command,
}) };
let _ = app_log(
"info".to_string(),
format!(
"检查更新完成: package={}, installed_version={}, candidate_version={}, has_update={}, source_available={}",
result.package_name,
result.installed_version,
result.candidate_version,
result.has_update,
result.source_available
),
);
Ok(result)
} }
} }

@ -1,6 +1,11 @@
mod commands; mod commands;
mod state; mod state;
use std::{
fs::{create_dir_all, File, OpenOptions},
io,
};
use commands::{ use commands::{
config::{config_get_all, config_merge}, config::{config_get_all, config_merge},
events::{emit_to_window, list_windows}, events::{emit_to_window, list_windows},
@ -12,13 +17,57 @@ use commands::{
open_ticket_window, quit_app, open_ticket_window, quit_app,
}, },
}; };
use fs2::FileExt;
use state::AppState; use state::AppState;
use tauri::Manager;
#[allow(dead_code)]
struct SingleInstanceLock(File);
fn acquire_single_instance_lock(app: &tauri::App) -> Result<SingleInstanceLock, String> {
let base_dir = app.path_resolver().app_data_dir().ok_or("无法获取应用数据目录")?;
create_dir_all(&base_dir).map_err(|err| format!("创建锁目录失败: {err}"))?;
let lock_path = base_dir.join("single-instance.lock");
let lock_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(lock_path)
.map_err(|err| format!("打开锁文件失败: {err}"))?;
match lock_file.try_lock_exclusive() {
Ok(()) => Ok(SingleInstanceLock(lock_file)),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
Err("duplicate instance".to_string())
}
Err(err) => Err(format!("加锁失败: {err}")),
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.manage(AppState::default()) .manage(AppState::default())
.setup(|app| { .setup(|app| {
match acquire_single_instance_lock(app) {
Ok(lock) => {
app.manage(lock);
}
Err(reason) if reason == "duplicate instance" => {
tauri::api::dialog::blocking::message(
None::<&tauri::Window>,
"提示",
"请勿重复打开",
);
app.handle().exit(0);
return Ok(());
}
Err(reason) => {
return Err(reason.into());
}
}
ensure_main_window(app.handle())?; ensure_main_window(app.handle())?;
Ok(()) Ok(())
}) })

@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "call-client", "productName": "call-client",
"version": "0.1.0" "version": "0.1.1"
}, },
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@ -12,7 +12,12 @@
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
"all": true "all": true,
"http": {
"all": true,
"request": true,
"scope": ["http://*/*", "https://*/*"]
}
}, },
"windows": [ "windows": [
{ {

@ -13,11 +13,11 @@ export async function minimizeWindow(): Promise<void> {
} }
/** /**
* *
*/ */
export async function closeWindow(): Promise<void> { export async function closeWindow(): Promise<void> {
try { try {
await appWindow.close(); await invoke("quit_app");
} catch (error) { } catch (error) {
throw new Error(`关闭窗口失败: ${String(error)}`); throw new Error(`关闭窗口失败: ${String(error)}`);
} }

@ -1,4 +1,10 @@
import axios, { type AxiosRequestConfig } from "axios"; import axios, {
AxiosError,
type AxiosRequestConfig,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from "axios";
import { Body, fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { showErrorNative } from "../host/dialog"; import { showErrorNative } from "../host/dialog";
import { getSession, setSession } from "../host/session"; import { getSession, setSession } from "../host/session";
import type { SessionState } from "../host/types"; import type { SessionState } from "../host/types";
@ -27,6 +33,96 @@ type RetryableAxiosRequestConfig = AxiosRequestConfig & {
let refreshTokenPromise: Promise<string | null> | null = null; let refreshTokenPromise: Promise<string | null> | null = null;
function shouldUseTauriHttpTransport(): boolean {
return !import.meta.env.DEV;
}
function buildRequestUrl(config: AxiosRequestConfig): string {
const rawUrl = config.url ?? "";
const baseURL = config.baseURL ?? "";
const isAbsoluteUrl = /^https?:\/\//i.test(rawUrl);
const finalUrl = isAbsoluteUrl
? rawUrl
: `${baseURL.replace(/\/$/, "")}/${rawUrl.replace(/^\//, "")}`;
if (!config.params) {
return finalUrl;
}
const searchParams = new URLSearchParams();
const params = config.params as Record<string, unknown>;
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((item) => searchParams.append(key, String(item)));
return;
}
searchParams.append(key, String(value));
});
const query = searchParams.toString();
if (!query) {
return finalUrl;
}
const connector = finalUrl.includes("?") ? "&" : "?";
return `${finalUrl}${connector}${query}`;
}
function buildTauriBody(config: AxiosRequestConfig): Body | undefined {
const method = (config.method ?? "GET").toUpperCase();
if (method === "GET" || method === "HEAD" || config.data === undefined) {
return undefined;
}
if (typeof config.data === "string") {
return Body.text(config.data);
}
return Body.json(config.data);
}
/**
* 使 Tauri HTTP WebView /
*/
async function tauriAxiosAdapter(
config: InternalAxiosRequestConfig,
): Promise<AxiosResponse> {
try {
const url = buildRequestUrl(config);
const method = (config.method ?? "GET").toUpperCase() as
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
| "HEAD";
const headers = (config.headers ?? {}) as Record<string, string>;
const response = await tauriFetch<unknown>(url, {
method,
timeout: config.timeout,
headers,
body: buildTauriBody(config),
responseType: ResponseType.JSON,
});
return {
data: response.data,
status: response.status,
statusText: String(response.status),
headers: response.headers as Record<string, string>,
config,
request: null,
};
} catch (error) {
throw new AxiosError(
error instanceof Error ? error.message : String(error),
"ERR_NETWORK",
config,
);
}
}
/** /**
* baseURL * baseURL
*/ */
@ -57,6 +153,10 @@ const instance = axios.create({
}, },
}); });
if (shouldUseTauriHttpTransport()) {
instance.defaults.adapter = tauriAxiosAdapter;
}
function isApiSuccessCode(code: unknown): boolean { function isApiSuccessCode(code: unknown): boolean {
return code === 200 || code === 0; return code === 200 || code === 0;
} }

@ -258,12 +258,18 @@ async function handleCheckUpdate(): Promise<void> {
checkingUpdate.value = true; checkingUpdate.value = true;
try { try {
await log("info", `检查更新开始: package=call-client, currentVersion=${appVersion.value}`);
const result = await invoke<AptUpdateCheckResult>("check_apt_update", { const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "call-client", packageName: "call-client",
currentVersion: appVersion.value, currentVersion: appVersion.value,
}); });
await log(
"info",
`检查更新结果: package=${result.packageName}, installed=${result.installedVersion}, candidate=${result.candidateVersion}, hasUpdate=${result.hasUpdate}, sourceAvailable=${result.sourceAvailable}`,
);
if (!result.sourceAvailable) { if (!result.sourceAvailable) {
await log("warn", `检查更新提示: 未检测到可用更新源, package=${result.packageName}`);
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
[ [
@ -291,10 +297,15 @@ async function handleCheckUpdate(): Promise<void> {
} }
if (!result.hasUpdate) { if (!result.hasUpdate) {
await log("info", `检查更新提示: 当前已是最新版本, version=${result.installedVersion}`);
showMessage("success", `当前已是最新版本(${result.installedVersion}`); showMessage("success", `当前已是最新版本(${result.installedVersion}`);
return; return;
} }
await log(
"info",
`检查更新提示: 检测到新版本, installed=${result.installedVersion}, candidate=${result.candidateVersion}`,
);
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
[ [

Loading…
Cancel
Save