Skip to content

Commit bf0c942

Browse files
committed
Add option for enforcing single running instance
1 parent 9f455c4 commit bf0c942

File tree

9 files changed

+229
-50
lines changed

9 files changed

+229
-50
lines changed

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ Options:
6969
Show or attach to a console window (auto, always, never, attach) [default: auto]
7070
-w, --current-dir <CURRENT_DIR>
7171
Working directory of the command (inherit, unpack, runner, command) [default: inherit]
72+
-o, --once
73+
Allow only one running instance
7274
-z, --build-dictionary
7375
Build compression dictionary
7476
-l, --list-runners
@@ -125,6 +127,7 @@ This option specifies the versioning strategy. Accepted values are:
125127
* `none`: Packed files are always unpacked and already unpacked files will be overwritten.
126128

127129
It defaults to `sidebyside`. The version is determined by a unique identifier generated during the packing process or specified with the [`version-string`](#version-string) option.
130+
Using `replace` or `none` might cause unpacking to fail if another instance of the packed executable is already running unless the [`once`](#once) option is set.
128131

129132
#### verification
130133

@@ -172,6 +175,12 @@ This option changes the working directory of the packed executable. Accepted val
172175

173176
It defaults to `inherit`.
174177

178+
#### once
179+
180+
This option prevents multiple instances of the packed executable from running at the same time. When set, the runner will check for running processes on the system and will exit immediately if a running instance of the executable is found during startup.
181+
182+
This option currently only affects Windows and Linux runners. On Windows, if the packed executable is a GUI application, the runner will bring its window into the foreground and activate it.
183+
175184
#### build-dictionary
176185

177186
This option builds a zstandard compression dictionary from the input files and stores it in the output executable. This can improve the compression ratio when many small and similar files are packed.
@@ -182,7 +191,7 @@ Building a dictionary can increase the packing time and can in some cases negati
182191

183192
## Performance
184193

185-
Wrappe is optimized for compression ratio and decompression speed, generally matching or outperforming other packers in terms of both. Packed files are decompressed from the memory-mapped executable directly to disk in parallel, while extraction is skipped when the files are already unpacked. This enables fast startup of packed executables with minimal overhead.
194+
Wrappe is optimized for compression ratio and decompression speed, generally matching or outperforming other packers in terms of both. It uses a custom metadata format designed for parallel iteration and decompression and compact storage of file information. Packed files are concurrently decompressed from the memory-mapped executable directly to disk, while extraction is skipped when the files are already unpacked to enable fast startup of packed executables with minimal overhead.
186195

187196
> As an example, a 400 MB PyInstaller one-directory output with 1500 files packed with wrappe at maximum compression level results in a 100 MB executable that unpacks and starts in around 500 milliseconds on a modern system on the first run and instantly on subsequent runs. This is around 50% faster and only 5% larger than the same project packed by PyInstaller in one-file mode with UPX compression, which unpacks and loads into memory on every run.
188197

src/main.rs

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub struct Args {
5555
/// Working directory of the command (inherit, unpack, runner, command)
5656
#[arg(short = 'w', long, default_value = "inherit")]
5757
current_dir: String,
58+
/// Only allow one instance of the application to run
59+
#[arg(short = 'o', long, default_value = "false")]
60+
once: bool,
5861
/// Build compression dictionary
5962
#[arg(short = 'z', long, default_value = "false")]
6063
build_dictionary: bool,
@@ -331,6 +334,7 @@ fn main() {
331334
unpack_target,
332335
versioning,
333336
unpack_directory,
337+
once: if args.once { 1 } else { 0 },
334338
command,
335339
arguments,
336340
wrappe_format: WRAPPE_FORMAT,

src/types.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub use zerocopy::AsBytes;
22

3-
pub const WRAPPE_FORMAT: u8 = 202;
3+
pub const WRAPPE_FORMAT: u8 = 203;
44
pub const WRAPPE_SIGNATURE: [u8; 8] = [0x50, 0x45, 0x33, 0x44, 0x41, 0x54, 0x41, 0x00];
55
pub const NAME_SIZE: usize = 128;
66
pub const ARGS_SIZE: usize = 512;
@@ -16,6 +16,7 @@ pub struct StarterInfo {
1616
pub uid: [u8; 16],
1717
pub unpack_target: u8,
1818
pub versioning: u8,
19+
pub once: u8,
1920
pub wrappe_format: u8,
2021
pub unpack_directory: [u8; NAME_SIZE],
2122
pub command: [u8; NAME_SIZE],

startpe/Cargo.toml

+17-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ path = "src/main.rs"
1616

1717
[features]
1818

19-
default = ["prefetch"]
19+
default = ["prefetch", "once"]
2020
prefetch = []
21+
once = ["dep:procfs"]
2122

2223
[profile.release]
2324

@@ -34,7 +35,7 @@ strip = "symbols"
3435

3536
dirs = "5.0.1"
3637
filetime = "0.2.23"
37-
fslock = "0.2.1"
38+
fslock-guard = "0.1.3"
3839
memchr = "2.7.2"
3940
memmap2 = "0.9.4"
4041
rayon = "1.10.0"
@@ -44,4 +45,17 @@ zstd = { version = "0.13.1", default-features = false, features = [] }
4445

4546
[target.'cfg(windows)'.dependencies]
4647

47-
winapi = { version = "0.3.9", features = ["wincon", "libloaderapi"] }
48+
windows-sys = { version = "0.52.0", features = [
49+
"Win32_Foundation",
50+
"Win32_System_Console",
51+
"Win32_System_LibraryLoader",
52+
"Win32_System_Threading",
53+
"Win32_System_Diagnostics",
54+
"Win32_System_Diagnostics_ToolHelp",
55+
"Win32_System_ProcessStatus",
56+
"Win32_UI_WindowsAndMessaging",
57+
] }
58+
59+
[target.'cfg(target_os = "linux")'.dependencies]
60+
61+
procfs = { version = "0.16.0", default-features = false, optional = true }

startpe/src/decompress.rs

-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ use std::{
1212
use std::os::windows::fs::OpenOptionsExt;
1313

1414
use filetime::{set_file_times, set_symlink_file_times, FileTime};
15-
use fslock::LockFile;
1615
use rayon::prelude::*;
1716
use twox_hash::XxHash64;
1817
use zerocopy::Ref;
@@ -197,12 +196,6 @@ pub fn decompress(
197196
);
198197
}
199198

200-
create_dir_all(unpack_dir)
201-
.unwrap_or_else(|e| panic!("couldn't create directory {}: {}", unpack_dir.display(), e));
202-
203-
let mut lockfile = LockFile::open(&unpack_dir.join(LOCK_FILE)).unwrap();
204-
lockfile.lock().unwrap();
205-
206199
// verify files
207200
if verification > 0 && !should_extract && file_sections > 0 {
208201
if show_information >= 2 {

startpe/src/main.rs

+46-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
env::{current_exe, var_os},
3-
fs::{read_link, File},
3+
fs::{create_dir_all, read_link, File},
44
io::Write,
55
mem::size_of,
66
panic::set_hook,
@@ -17,8 +17,9 @@ use std::os::unix::process::CommandExt;
1717
use std::process::Stdio;
1818

1919
#[cfg(windows)]
20-
use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS};
20+
use windows_sys::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS};
2121

22+
use fslock_guard::LockFileGuard;
2223
use memchr::memmem;
2324
use memmap2::MmapOptions;
2425
use zerocopy::Ref;
@@ -38,6 +39,9 @@ use versioning::*;
3839
#[cfg(feature = "prefetch")]
3940
mod prefetch;
4041

42+
#[cfg(feature = "once")]
43+
mod once;
44+
4145
fn main() {
4246
set_hook(Box::<_>::new(move |panic| {
4347
if let Some(message) = panic.payload().downcast_ref::<&str>() {
@@ -194,20 +198,48 @@ fn main() {
194198
println!("target directory: {}", unpack_dir.display());
195199
}
196200

197-
let run_path = &unpack_dir.join(
198-
std::str::from_utf8(
199-
&info.command[0..(info
200-
.command
201-
.iter()
202-
.position(|&c| c == b'\0')
203-
.unwrap_or(info.command.len()))],
204-
)
205-
.unwrap(),
206-
);
201+
let command_name = std::str::from_utf8(
202+
&info.command[0..(info
203+
.command
204+
.iter()
205+
.position(|&c| c == b'\0')
206+
.unwrap_or(info.command.len()))],
207+
)
208+
.unwrap();
209+
let run_path = &unpack_dir.join(command_name);
207210
if show_information >= 2 {
208211
println!("runpath: {}", run_path.display());
209212
}
210213

214+
create_dir_all(&unpack_dir)
215+
.unwrap_or_else(|e| panic!("couldn't create directory {}: {}", unpack_dir.display(), e));
216+
217+
let lockfile = if info.once == 1 {
218+
let lockfile = LockFileGuard::try_lock(unpack_dir.join(LOCK_FILE))
219+
.unwrap_or_else(|e| panic!("couldn't lock file: {}", e));
220+
if lockfile.is_none() {
221+
println!("another instance is already unpacking, exiting...");
222+
return;
223+
}
224+
lockfile.unwrap()
225+
} else {
226+
LockFileGuard::lock(unpack_dir.join(LOCK_FILE)).unwrap_or_else(|e| {
227+
panic!("couldn't lock file: {}", e);
228+
})
229+
};
230+
231+
#[cfg(feature = "once")]
232+
if info.once == 1 {
233+
if show_information >= 2 {
234+
println!("checking for running processes...");
235+
}
236+
let running = once::check_instance(run_path).unwrap();
237+
if running {
238+
println!("another instance is already running, exiting...");
239+
return;
240+
}
241+
}
242+
211243
let should_extract = match info.versioning {
212244
0 => get_version(&unpack_dir) != version,
213245
1 => get_version(&unpack_dir) != version,
@@ -245,6 +277,8 @@ fn main() {
245277
}
246278
}
247279

280+
drop(lockfile);
281+
248282
let baked_arguments = std::str::from_utf8(
249283
&info.arguments[0..(info
250284
.arguments

startpe/src/once.rs

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::path::Path;
2+
3+
#[cfg(windows)]
4+
pub fn check_instance(run_path: &Path) -> Result<bool, std::io::Error> {
5+
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
6+
use windows_sys::Win32::{
7+
System::{
8+
Diagnostics::ToolHelp::{
9+
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
10+
TH32CS_SNAPPROCESS,
11+
},
12+
Threading::{
13+
OpenProcess, QueryFullProcessImageNameW, PROCESS_QUERY_LIMITED_INFORMATION,
14+
},
15+
},
16+
UI::WindowsAndMessaging::EnumWindows,
17+
};
18+
19+
unsafe extern "system" fn enum_windows_proc(hwnd: isize, lparam: isize) -> i32 {
20+
use windows_sys::Win32::UI::WindowsAndMessaging::{
21+
GetWindowThreadProcessId, SetForegroundWindow, ShowWindow, SW_SHOW,
22+
};
23+
let mut process_id = 0;
24+
unsafe {
25+
GetWindowThreadProcessId(hwnd, &mut process_id);
26+
}
27+
if process_id == lparam as u32 {
28+
unsafe { ShowWindow(hwnd, SW_SHOW) };
29+
let result = unsafe { SetForegroundWindow(hwnd) };
30+
if result == 0 {
31+
return 1;
32+
}
33+
0
34+
} else {
35+
1
36+
}
37+
}
38+
39+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
40+
if snapshot == 0 {
41+
return Err(std::io::Error::last_os_error());
42+
}
43+
let mut entry = unsafe { std::mem::zeroed::<PROCESSENTRY32W>() };
44+
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
45+
if unsafe { Process32FirstW(snapshot, &mut entry) } != 0 {
46+
let command_name = run_path.file_name().unwrap().to_os_string();
47+
let mut path = [0u16; 1024];
48+
loop {
49+
let process_name: &[u16] = unsafe {
50+
std::slice::from_raw_parts(
51+
entry.szExeFile.as_ptr().cast::<u16>(),
52+
entry
53+
.szExeFile
54+
.iter()
55+
.take(entry.szExeFile.len())
56+
.position(|&c| c == 0)
57+
.unwrap_or(entry.szExeFile.len()),
58+
)
59+
};
60+
let process_name = OsString::from_wide(process_name);
61+
if process_name == command_name {
62+
let process = unsafe {
63+
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, entry.th32ProcessID)
64+
};
65+
let mut len = path.len() as u32;
66+
let result =
67+
unsafe { QueryFullProcessImageNameW(process, 0, path.as_mut_ptr(), &mut len) };
68+
if result == 0 {
69+
return Err(std::io::Error::last_os_error());
70+
}
71+
let path = OsString::from_wide(&path[..len as usize]);
72+
if path == run_path.as_os_str() {
73+
let result =
74+
unsafe { EnumWindows(Some(enum_windows_proc), entry.th32ProcessID as _) };
75+
if result == 0 {
76+
let err = std::io::Error::last_os_error();
77+
if err.raw_os_error() != Some(0) {
78+
return Err(err);
79+
}
80+
}
81+
return Ok(true);
82+
}
83+
}
84+
if unsafe { Process32NextW(snapshot, &mut entry) } == 0 {
85+
break;
86+
}
87+
}
88+
}
89+
90+
Ok(false)
91+
}
92+
93+
#[cfg(target_os = "linux")]
94+
pub fn check_instance(run_path: &Path) -> Result<bool, std::io::Error> {
95+
for proc in procfs::process::all_processes().unwrap() {
96+
match proc {
97+
Ok(p) => match p.exe() {
98+
Ok(exe) => {
99+
if exe == run_path {
100+
return Ok(true);
101+
}
102+
}
103+
Err(_e) => {
104+
#[cfg(debug_assertions)]
105+
eprintln!("error: {}", _e);
106+
continue;
107+
}
108+
},
109+
Err(_e) => {
110+
#[cfg(debug_assertions)]
111+
eprintln!("error: {}", _e);
112+
continue;
113+
}
114+
}
115+
}
116+
Ok(false)
117+
}
118+
119+
#[cfg(not(any(windows, target_os = "linux")))]
120+
#[inline(always)]
121+
pub fn check_instance(_: &Path) -> Result<bool, std::io::Error> { Ok(false) }

0 commit comments

Comments
 (0)