fix(launch): serialize concurrent same-profile launches (issue #11)

N parallel `open --profile <same>` (e.g. chatgpt-imagegen's web backend firing
3 image gens at once) collided on the profile-copy disk I/O and Chrome's profile
lock: every candidate burned its full ~30s launch timeout and ALL failed (0
success), because the loser instances hung without writing DevToolsActivePort.

ProfileLaunchLock takes a cross-process flock on a per-resolved-profile lock
file, held across the copy + launch until Chrome is up, so concurrent
same-profile launches queue instead of colliding — the storm becomes
all-succeed-serially instead of all-fail. The kernel releases the lock when the
holder exits, so a crash can't wedge the queue; acquisition is best-effort
(launch proceeds unlocked if it can't be taken). Uncontended single launches
are unaffected.
This commit is contained in:
leeguooooo
2026-06-12 14:40:03 +09:00
parent 2aa216dd7a
commit eb60053183
+59
View File
@@ -341,6 +341,46 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
})
}
/// Cross-process advisory lock that serializes concurrent launches of the SAME
/// Chrome profile (issue #11). Held via `flock` on a per-profile lock file; the
/// kernel releases it automatically when the holding process exits, so a crash
/// can't wedge the queue. Best-effort: if the lock can't be acquired the launch
/// proceeds unlocked rather than failing.
struct ProfileLaunchLock {
#[cfg(unix)]
_file: std::fs::File,
}
impl ProfileLaunchLock {
fn acquire(profile: &str) -> Option<Self> {
let safe: String = profile
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect();
let path = std::env::temp_dir().join(format!("chrome-use-launch-{safe}.lock"));
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path)
.ok()?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
// Blocking exclusive lock: concurrent same-profile launches queue.
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
return None;
}
Some(ProfileLaunchLock { _file: file })
}
#[cfg(not(unix))]
{
let _ = file;
Some(ProfileLaunchLock {})
}
}
}
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
let chrome_path = match &options.executable_path {
Some(p) => PathBuf::from(p),
@@ -363,6 +403,13 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
// rewrite options so the retry loop uses the copied profile.
let mut resolved_options: Option<LaunchOptions> = None;
let mut profile_temp_dir: Option<PathBuf> = None;
// Serialize concurrent launches of the SAME named profile across processes
// (issue #11). Without this, N parallel `open --profile <same>` collide on
// the profile-copy disk I/O / Chrome's profile lock, every candidate burns
// its full launch timeout, and all fail. The flock queues them instead and
// auto-releases on process exit, so a crash can't wedge the queue. Held
// until Chrome is up (function return).
let mut _launch_lock: Option<ProfileLaunchLock> = None;
if let Some(ref profile) = options.profile {
if is_chrome_profile_name(profile) {
@@ -372,6 +419,7 @@ pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
.to_string()
})?;
let resolved = resolve_chrome_profile(&user_data_dir, profile)?;
_launch_lock = ProfileLaunchLock::acquire(&resolved);
let temp_path = copy_chrome_profile(&user_data_dir, &resolved)?;
let mut opts = options.clone();
@@ -1886,6 +1934,17 @@ mod tests {
assert!(is_chrome_profile_name(""));
}
#[test]
fn test_profile_launch_lock_acquires_and_sanitizes() {
// Uncontended acquire succeeds and writes a sanitized per-profile lock
// file (issue #11: serialize concurrent same-profile launches).
let lock = ProfileLaunchLock::acquire("Profile 5/weird:name");
assert!(lock.is_some(), "uncontended lock should acquire");
let expected = std::env::temp_dir().join("chrome-use-launch-Profile_5_weird_name.lock");
assert!(expected.exists(), "lock file should exist at {expected:?}");
drop(lock);
}
#[test]
fn test_is_chrome_profile_name_paths() {
assert!(!is_chrome_profile_name("/tmp/dir"));