From 2a53e87d3cd5d14966c9ac5e6538ffadd9fe0d47 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:33:38 +0000 Subject: [PATCH] =?UTF-8?q?fix(windows):=20=EC=9E=91=EC=97=85=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EC=A4=84=20=EC=95=B1=20=EC=9D=B4=EB=A6=84=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20=EC=A4=91=EB=B3=B5=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=20=EC=8B=A4=ED=96=89=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows 전역 Named Mutex를 사용하여 단일 인스턴스를 보장합니다. 이미 실행 중인 경우 기존 창을 활성화하고 새 프로세스를 종료합니다. - acquire_single_instance_lock(): Global\DmNote_SingleInstance 뮤텍스 생성 - focus_existing_window_and_exit(): FindWindowW로 기존 창 탐색 후 포그라운드 전환 windows 0.61.3 API 호환성에 맞게 구현 (bool, PCWSTR, HWND.0.is_null() 패턴) Fixes #41 Co-authored-by: 이시훈 --- src-tauri/src/main.rs | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8bfaa88..026abe4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -37,6 +37,12 @@ use store::AppStore; fn main() { #[cfg(target_os = "windows")] { + // 단일 인스턴스 보장: 이미 실행 중인 경우 기존 창을 활성화하고 종료 + if !acquire_single_instance_lock() { + focus_existing_window_and_exit(); + return; + } + // WebView2 투명 오버레이(레이어드/알파) 이슈가 특정 런타임 버전에서 발생할 수 있어, // 고정(Fixed) 런타임을 번들/지정한 경우 우선 사용하도록 합니다. apply_embedded_webview2_fixed_runtime_override(); @@ -710,6 +716,70 @@ fn extract_zip_bytes_to_dir(zip_bytes: &[u8], dest_dir: &std::path::Path) -> Res Ok(()) } +/// Windows 전역 Named Mutex로 단일 인스턴스 잠금을 획득합니다. +/// 이미 다른 인스턴스가 실행 중이면 false를 반환합니다. +#[cfg(target_os = "windows")] +fn acquire_single_instance_lock() -> bool { + use std::ffi::OsStr; + use std::iter::once; + use std::os::windows::ffi::OsStrExt; + use windows::Win32::{ + Foundation::{GetLastError, ERROR_ALREADY_EXISTS}, + System::Threading::CreateMutexW, + }; + + let name = OsStr::new("Global\\DmNote_SingleInstance") + .encode_wide() + .chain(once(0u16)) + .collect::>(); + + unsafe { + let result = CreateMutexW( + None, + false, + windows::core::PCWSTR(name.as_ptr()), + ); + match result { + Ok(_handle) => { + // 뮤텍스 핸들을 의도적으로 유지 (프로세스 종료 시 자동 해제) + // _handle을 drop하지 않기 위해 forget + std::mem::forget(_handle); + let last_err = GetLastError(); + last_err != ERROR_ALREADY_EXISTS + } + Err(_) => false, + } + } +} + +/// 기존 실행 중인 창을 포그라운드로 가져오고 현재 프로세스를 종료합니다. +#[cfg(target_os = "windows")] +fn focus_existing_window_and_exit() { + use std::ffi::OsStr; + use std::iter::once; + use std::os::windows::ffi::OsStrExt; + use windows::Win32::UI::WindowsAndMessaging::{ + FindWindowW, IsIconic, SetForegroundWindow, ShowWindow, SW_RESTORE, + }; + + let title: Vec = OsStr::new("DM Note - Settings") + .encode_wide() + .chain(once(0u16)) + .collect(); + + unsafe { + let hwnd = FindWindowW(None, windows::core::PCWSTR(title.as_ptr())); + if !hwnd.0.is_null() { + if IsIconic(hwnd).as_bool() { + let _ = ShowWindow(hwnd, SW_RESTORE); + } + let _ = SetForegroundWindow(hwnd); + } + } + + std::process::exit(0); +} + /// macOS 접근성(Accessibility) 권한을 확인하고, /// 없으면 시스템 권한 요청 다이얼로그를 자동으로 표시합니다. /// `AXIsProcessTrustedWithOptions`에 `kAXTrustedCheckOptionPrompt: true`를 전달하면