Skip to content

Commit

Permalink
feat: single instance check on windows (#26)
Browse files Browse the repository at this point in the history
* deps: update

* feat: single instance on windows

* docs: add info about restarts in the readme
  • Loading branch information
Nerixyz authored Mar 23, 2022
1 parent 6e6f34f commit cb0b284
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 31 deletions.
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ default setting). Search for `current-song2.exe` in the _Background Processes_ a

**Alternatively**: In the _Task Manager_, go to _Details_ and search for `current-song2.exe`.

**If you only want to restart the app** then you can simply reopen the app, and it will ask you to stop the old instance.

### Autostart

To remove the application from autostart, run `current-song2.exe --remove-autostart` from a terminal.
Expand All @@ -50,7 +52,7 @@ Alternatively you can **disable** the autostart entry in the Task Manager (start
# Configuration

⚠ The config is loaded at the start of CurrentSong. So in order to apply the configuration, you need to **restart** the
application.
application. On Windows you should only need to double-click the `current-song2.exe` again, and it will ask you to stop the old process.

The configuration uses the [toml](https://toml.io) format.

Expand Down
2 changes: 1 addition & 1 deletion lib/win-gsmtc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ path = "src/lib.rs"

[dependencies]
tokio = { version = "1.15", features = ["sync", "macros", "rt"] }
windows = { version = "0.32", features = ["Media_Control", "Foundation", "Foundation_Collections", "Storage_Streams"] }
windows = { version = "0.34", features = ["Media_Control", "Foundation", "Foundation_Collections", "Storage_Streams"] }
tracing = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion lib/win-wrapper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"

[dependencies]
windows = { version = "0.32", features = ["Win32_Foundation", "alloc", "Win32_UI_WindowsAndMessaging", "Win32_System_Registry", "Win32_Security", "Win32_UI_Shell", "Win32_System_Threading"] }
windows = { version = "0.34", features = ["Win32_Foundation", "alloc", "Win32_UI_WindowsAndMessaging", "Win32_System_Registry", "Win32_Security", "Win32_UI_Shell", "Win32_System_Threading", "Win32_System_ProcessStatus"] }
31 changes: 18 additions & 13 deletions lib/win-wrapper/src/elevate.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
use crate::pwstr::ManagedPwstr;
use std::env;
use windows::Win32::{
Foundation::{CloseHandle, GetLastError, PWSTR, WIN32_ERROR},
System::Threading::WaitForSingleObject,
UI::{
Shell::{ShellExecuteExW, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW},
WindowsAndMessaging::SW_NORMAL,
use windows::{
core::PCWSTR,
Win32::{
Foundation::{CloseHandle, GetLastError, WIN32_ERROR},
System::Threading::WaitForSingleObject,
UI::{
Shell::{
ShellExecuteExW, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW,
},
WindowsAndMessaging::SW_NORMAL,
},
},
};

pub fn elevate_self() -> Result<(), WIN32_ERROR> {
unsafe {
let mut verb: ManagedPwstr = "runas".into();
let mut file: ManagedPwstr = env::current_exe().unwrap().as_os_str().into();
let mut parameters: ManagedPwstr = "--elevated".into();
let verb: ManagedPwstr = "runas".into();
let file: ManagedPwstr = env::current_exe().unwrap().as_os_str().into();
let parameters: ManagedPwstr = "--elevated".into();
let mut info = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
lpVerb: verb.get_pwstr(),
lpFile: file.get_pwstr(),
lpParameters: parameters.get_pwstr(),
lpDirectory: PWSTR(std::ptr::null_mut()),
lpVerb: verb.get_pcwstr(),
lpFile: file.get_pcwstr(),
lpParameters: parameters.get_pcwstr(),
lpDirectory: PCWSTR(std::ptr::null()),
nShow: SW_NORMAL.0 as _,
fMask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC,
..Default::default()
Expand Down
1 change: 1 addition & 0 deletions lib/win-wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod elevate;
pub mod message_box;
pub mod path;
mod pwstr;
pub mod single_instance;
30 changes: 27 additions & 3 deletions lib/win-wrapper/src/pwstr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,37 @@ use std::{
iter,
os::windows::ffi::OsStrExt,
};
use windows::Win32::Foundation::PWSTR;
use windows::core::PCWSTR;

pub struct ManagedPwstr(Vec<u16>);

impl ManagedPwstr {
pub unsafe fn get_pwstr(&mut self) -> PWSTR {
PWSTR(self.0.as_mut_ptr())
pub unsafe fn get_pcwstr(&self) -> PCWSTR {
PCWSTR(self.0.as_ptr())
}

pub fn alloc(len: usize) -> Self {
Self(vec![0; len])
}

pub fn as_mut_slice(&mut self) -> &mut [u16] {
&mut self.0
}
}

impl PartialEq for ManagedPwstr {
fn eq(&self, other: &Self) -> bool {
for (a, b) in self.0.iter().zip(other.0.iter()) {
if *a != *b {
return false;
} else if *a == 0 {
// both values are equal, we hit a null terminator
// strings must be equal
return true;
}
}
// we didn't see a null terminator yet
return false;
}
}

Expand Down
99 changes: 99 additions & 0 deletions lib/win-wrapper/src/single_instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::pwstr::ManagedPwstr;
use std::{env, mem, ptr};
use windows::{
core::{Error, Result, HRESULT},
Win32::{
Foundation::{
CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, ERROR_NOT_FOUND, ERROR_SUCCESS,
HANDLE, MAX_PATH,
},
System::{
ProcessStatus::{K32EnumProcesses, K32GetModuleFileNameExW},
Threading::{
CreateMutexW, OpenProcess, TerminateProcess, PROCESS_QUERY_INFORMATION,
PROCESS_TERMINATE,
},
},
},
};

/// Tries to create and lock on a system wide mutex
///
/// If the mutex is already locked, then another application locked the mutex and another instance is already running.
///
/// Returns `false` if another instance is already running, and `true` if we are the only instance running.
pub fn try_create_new_instance(unique_instance_id: &str) -> bool {
// make sure this is a handle and not a result so we get a compiler error when the metadata changes
let _: HANDLE = unsafe { CreateMutexW(ptr::null(), true, unique_instance_id) };
unsafe {
match GetLastError() {
ERROR_SUCCESS => true,
ERROR_ALREADY_EXISTS => false,
x => {
eprintln!("Unexpected error - {:?}", x);
false
}
}
}
}

pub fn kill_other_instances_of_this_application() -> Result<()> {
let mut path_buf = ManagedPwstr::alloc(MAX_PATH as usize + 1);
let this_path = ManagedPwstr::from(env::current_exe().unwrap().into_os_string());

let (processes, n_processes) = get_all_processes()?;
let pid = match processes
.into_iter()
.take(n_processes as usize)
.find(|pid| cmp_path(*pid, &this_path, &mut path_buf))
{
Some(pid) => pid,
None => return Err(Error::from(HRESULT::from(ERROR_NOT_FOUND))),
};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, None, pid);
if handle.is_invalid() {
return Err(GetLastError().into());
}

if !TerminateProcess(handle, u32::MAX).as_bool() {
return Err(GetLastError().into());
}
CloseHandle(handle);
}

Ok(())
}

fn get_all_processes() -> Result<(Vec<u32>, u32)> {
let mut buf = vec![0u32; 1024];
let mut returned_bytes = 0;
unsafe {
if !K32EnumProcesses(
buf.as_mut_ptr(),
(mem::size_of::<u32>() * buf.len()) as u32,
&mut returned_bytes,
)
.as_bool()
{
return Err(GetLastError().into());
}
}
Ok((buf, returned_bytes / (mem::size_of::<u32>() as u32)))
}

fn cmp_path(pid: u32, path: &ManagedPwstr, path_buf: &mut ManagedPwstr) -> bool {
unsafe {
let proc = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
if proc.is_invalid() {
return false;
}
let chars = K32GetModuleFileNameExW(proc, None, path_buf.as_mut_slice());
CloseHandle(proc);
if chars == 0 {
false
} else {
path == path_buf
}
}
}
34 changes: 34 additions & 0 deletions src/win_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use win_wrapper::{
autostart::{add_self_to_autostart, check_autostart, remove_autostart, ERROR_ACCESS_DENIED},
elevate::elevate_self,
message_box::{MessageBox, Okay, YesNo},
single_instance,
};

#[cfg(debug_assertions)]
Expand All @@ -32,6 +33,8 @@ pub fn win_main() {
std::process::exit(0);
}

handle_multiple_instances();

if CONFIG.no_autostart || check_autostart(APPLICATION_NAME) {
return;
}
Expand Down Expand Up @@ -80,6 +83,37 @@ pub fn win_main() {
};
}

fn handle_multiple_instances() {
// consider using something random?
// not dependant on the version!
if !single_instance::try_create_new_instance(&format!(
"current-song2::main-executable::{}",
CONFIG.server.port
)) {
if MessageBox::<YesNo>::information(
"Another instance is already running. Kill the other instance?",
)
.with_title(APPLICATION_NAME)
.show()
.unwrap_or(YesNo::No)
== YesNo::Yes
{
match single_instance::kill_other_instances_of_this_application() {
Ok(_) => (),
Err(e) => {
MessageBox::<Okay>::error(&format!(
"Could not kill the other instance: {:?}",
e
))
.with_title(APPLICATION_NAME)
.show()
.ok();
}
}
}
}
}

fn elevated_main() -> ! {
if let Err(e) = add_self_to_autostart(APPLICATION_NAME) {
MessageBox::<Okay>::error(&format!(
Expand Down

0 comments on commit cb0b284

Please sign in to comment.