文章目录
前言
随着跨平台应用开发的需求不断增加,Tauri2.0框架凭借其高性能和跨平台的特性,成为了开发者们的热门选择。然而,在开发桌面应用时,如何确保应用程序只能运行一个实例是一个常见的需求。例如,某些应用程序需要独占系统资源,或者需要避免用户误操作导致的数据冲突。今天,我们将探讨如何在Tauri2.0框架下,使用Rust语言实现单实例应用程序的功能。
本文将详细介绍在不同操作系统(Windows、macOS、Linux)下实现单实例应用的方法,并提供完整的代码示例。通过本文,你将了解到如何在Tauri2.0应用启动时检查是否已经有实例在运行,并采取相应的措施,例如提示用户或将参数传递给已有的实例。
最后再为你介绍Tauri官方为我们实现这种需求提供的一种捷径,从而不用去管理互斥体,而是简单的插件配置就能得到相同的结果,这也是为什么要写本文的原因。这就是Tauri插件 —— Single Instance
.
一、 单实例应用的意义
在开发桌面应用时,单实例应用的意义主要体现在以下几个方面:
-
资源管理:某些应用程序需要独占特定的系统资源,例如硬件设备或独特的系统服务。如果允许多个实例运行,可能会导致资源争抢或不可预测的行为。
-
数据一致性:对于需要处理共享数据的应用程序,例如数据库管理工具或配置文件编辑器,防止多个实例同时修改数据可以避免数据冲突和不一致。
-
用户体验:在某些场景下,用户可能不小心多次启动应用程序,导致多个实例运行。通过单实例机制,可以提供更友好的用户体验,例如自动将焦点切换到已有的实例。
-
安全性:对于某些需要严格控制的应用程序,例如金融类软件或敏感数据处理工具,单实例机制可以增强应用的安全性,防止恶意的多实例攻击。
二、 实现单实例应用的方法
在Tauri2.0框架下实现单实例应用,我们需要在应用启动时检查是否已经有一个实例在运行。如果有,则采取相应的措施,例如提示用户或将参数传递给已有的实例。
1 Windows下的实现
在Windows平台下,可以通过创建一个命名的Mutex(互斥量)来实现单实例检查。Mutex是Windows提供的一种同步机制,可以用于跨进程的同步和互斥控制。
1.1 创建命名Mutex
在Windows下,我们可以通过调用CreateMutexW
函数创建一个命名的Mutex。如果Mutex已经存在,则表示已经有一个实例在运行。
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use winapi::shared::minwindef::DWORD;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::synchapi::CreateMutexW;
fn create_mutex(name: &str) -> bool {
let name = OsStr::new(name).encode_wide().chain(Some(0)).collect::<Vec<u16>>();
unsafe {
CreateMutexW(name.as_ptr(), false as DWORD, None) as DWORD
} != 0
}
fn is_single_instance(name: &str) -> bool {
let result = create_mutex(name);
if result {
// 如果Mutex已经存在,则表示已经有一个实例在运行
unsafe {
if GetLastError() == 183 {
// ERROR_ALREADY_EXISTS
return false;
}
}
}
result
}
1.2 在Tauri应用中集成Mutex检查
在Tauri应用的主函数中,我们可以调用上述函数来检查是否已经有一个实例在运行。如果已经有实例运行,则可以提示用户并退出。
fn main() {
let instance_name = "MyTauriApp";
if !is_single_instance(instance_name) {
// 如果已经有一个实例在运行,则提示用户并退出
println!("An instance of {} is already running.", instance_name);
std::process::exit(1);
}
// 启动Tauri应用
tauri::run();
}
2 macOS下的实现
在macOS平台下,可以通过BUNDLE_IDENTIFIER
来实现单实例检查。macOS提供了LSOpenURLsWithRole
函数,可以用于检查是否已经有一个应用程序在运行。
2.1 获取Bundle Identifier
在macOS下,每个应用程序都有一个唯一的Bundle Identifier,可以通过Info.plist文件配置。
use std::process::Command;
fn get_bundle_identifier() -> String {
let output = Command::new("osascript")
.arg("-e")
.arg("id of app \"System Events\"")
.output()
.expect("failed to execute osascript");
String::from_utf8(output.stdout).unwrap()
}
2.2 检查是否已经有实例在运行
通过调用LSOpenURLsWithRole
函数,我们可以检查是否已经有一个实例在运行。如果有,则返回true
,否则返回false
。
use std::os::raw::c_char;
extern crate libc;
fn is_single_instance(bundle_id: &str) -> bool {
let mut psi: libc::PROCESSENTRY32 = unsafe {
std::mem::zeroed() };
let snapshot = unsafe {
libc::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
if snapshot == 0 {
return false;
}
psi.dwSize = std::mem::size_of::<libc::PROCESSENTRY32>() as DWORD;
while unsafe {
Process32Next(snapshot, &mut psi) } != 0 {
if let Some(name) = unsafe {
CStr::from_ptr(psi.szExeFile.as_ptr() as *const c_char) }.to_str() {
if name == bundle_id {
return true;
}
}
}
unsafe {
CloseHandle(snapshot) };
false
}
3 Linux下的实现
在Linux平台下,可以通过检查进程名或使用套接字来实现单实例检查。这里我们将演示如何通过检查进程名来实现单实例检查。
3.1 获取进程列表
通过调用/proc
文件系统,我们可以获取当前运行的所有进程,并检查是否有相同的进程名。
use std::fs;
use std::path::Path;
fn get_process_list() -> Vec<String> {
let mut processes = Vec::new();
for entry in fs::read_dir("/proc").unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.chars().all(char::is_digit) {
processes.push(name.to_string());
}
}
}
}
processes
}
3.2 检查是否已经有实例在运行
通过遍历所有进程,并检查是否有相同的进程名来判断是否已经有实例在运行。
fn is_single_instance(process_name: &str) -> bool {
let processes = get_process_list();
for pid in processes {
let exe_path = format!("/proc/{}/exe", pid);
let exe_link = Path::new(&exe_path);
if exe_link.exists() {
if let Some(exe_path) = exe_link.canonicalize().ok() {
if exe_path.file_name().and_then(|n| n.to_str()) == Some(process_name) {
return true;
}
}
}
}
false
}
4 在Tauri应用中集成单实例检查
在Tauri应用的主函数中,我们可以根据不同的平台调用相应的单实例检查函数。
fn main() {
#[cfg(target_os = "windows")]
{
let instance_name = "MyTauriApp";
if !is_single_instance(instance_name) {
println!("An instance of {} is already running.", instance_name);
std::process::exit(1);
}
}
#[cfg(target_os = "macos")]
{
let bundle_id = get_bundle_identifier();
if is_single_instance(&bundle_id) {
println!("An instance of {} is already running.", bundle_id);
std::process::exit(1);
}
}
#[cfg(target_os = "linux")]
{
let process_name = "my_tauri_app";
if is_single_instance(process_name) {
println!("An instance of {} is already running.", process_name);
std::process::exit(1);
}
}
tauri::run();
}
三、使用Tauri官方提供的插件实现单例程序
1. 安装准备
首先,确保你安装的Rust版本符合条件,该插件要求你的Rust版本大于1.77.2
.
然后就是看你的应用平台是否支持该插件,官方给出以下表格
可以明显看到,只有桌面系统受支持,也就是你的应用只能是在windows
,linux
,macos
上,这个插件才会有用,否则插件是用不了的。
2. 自动安装(推荐)
使用你所选择的包管理器直接安装即可,例如pnpm
安装
pnpm tauri add single-instance
3. 手动安装
首先添加依赖
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-single-instance = "2.0.0"
然后在tauri启动的时候添加插件
pub fn run() {
tauri::Builder::default()
// 就是下面这行
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
}))
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
然后运行一下项目就装好插件了
pnpm tauri dev
四、配置单例插件
如果你只是想要简单的实现单实例的话,就以上安装配置就已经能够达到这个效果了,如果你还想要在这个过程中实现其他功能,例如用户启动了另一个程序后提示程序已经启动了
,那么可以接着往下看。
1. init
函数
在配置安装插件时有一个init
函数可以注意一下,也就是
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
// 在这里写代码 ……
}))
插件的 init()
方法接收一个闭包,该闭包在新 App 实例启动时调用,但由插件关闭。 这个闭包有三个参数:
app
:应用程序的 AppHandle ,即应用的句柄,用来操作该程序。args
:用户初始化新实例时传递的参数列表,也就是新打开的程序的传入参数。cwd
:当前工作目录表示启动新应用程序实例的目录,也就是另一个程序在哪个目录打开的。
2. 新打开程序提示例子
注意,这部分逻辑你可以自己实现,这只是个官方给的例子。
use tauri::{
AppHandle, Manager};
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
let _ = show_window(app);
}))
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn show_window(app: &AppHandle) {
let windows = app.webview_windows();
windows
.values()
.next()
.expect("Sorry, no window found")
.set_focus()
.expect("Can't Bring Window to Focus");
}