【一起学Rust | Tauri2.0框架】单实例应用程序的深入解析:零漏洞实现与优化实战


前言

随着跨平台应用开发的需求不断增加,Tauri2.0框架凭借其高性能和跨平台的特性,成为了开发者们的热门选择。然而,在开发桌面应用时,如何确保应用程序只能运行一个实例是一个常见的需求。例如,某些应用程序需要独占系统资源,或者需要避免用户误操作导致的数据冲突。今天,我们将探讨如何在Tauri2.0框架下,使用Rust语言实现单实例应用程序的功能。

本文将详细介绍在不同操作系统(Windows、macOS、Linux)下实现单实例应用的方法,并提供完整的代码示例。通过本文,你将了解到如何在Tauri2.0应用启动时检查是否已经有实例在运行,并采取相应的措施,例如提示用户或将参数传递给已有的实例。

最后再为你介绍Tauri官方为我们实现这种需求提供的一种捷径,从而不用去管理互斥体,而是简单的插件配置就能得到相同的结果,这也是为什么要写本文的原因。这就是Tauri插件 —— Single Instance.


一、 单实例应用的意义

在开发桌面应用时,单实例应用的意义主要体现在以下几个方面:

  1. 资源管理:某些应用程序需要独占特定的系统资源,例如硬件设备或独特的系统服务。如果允许多个实例运行,可能会导致资源争抢或不可预测的行为。

  2. 数据一致性:对于需要处理共享数据的应用程序,例如数据库管理工具或配置文件编辑器,防止多个实例同时修改数据可以避免数据冲突和不一致。

  3. 用户体验:在某些场景下,用户可能不小心多次启动应用程序,导致多个实例运行。通过单实例机制,可以提供更友好的用户体验,例如自动将焦点切换到已有的实例。

  4. 安全性:对于某些需要严格控制的应用程序,例如金融类软件或敏感数据处理工具,单实例机制可以增强应用的安全性,防止恶意的多实例攻击。

二、 实现单实例应用的方法

在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 实例启动时调用,但由插件关闭。 这个闭包有三个参数:

  1. app:应用程序的 AppHandle ,即应用的句柄,用来操作该程序。
  2. args:用户初始化新实例时传递的参数列表,也就是新打开的程序的传入参数。
  3. 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");
}