paavohtl Claude Opus 4.6 (1M context) commited on
Commit
79ad64a
Β·
1 Parent(s): 10cb6f9

fix: isolate named semaphore for full test concurrency

Browse files

Hook CreateSemaphoreA in headless mode to rename the "Worms Armageddon"
instance-detection semaphore to "Worms Armageddon_{pid}". Without this,
concurrent WA.exe instances detect each other via GetLastError() ==
ERROR_ALREADY_EXISTS and skip initialization, causing silent failures.

This was the last concurrency hazard β€” tests now pass reliably at j=16
(10/10 runs, vs 1/17 before the session's fixes).

Also:
- Expose temp_dir_path() from file_isolation for cross-module use
- Reduce test stagger from 200ms to 50ms (just enough for I/O smoothing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-test-runner/src/main.rs CHANGED
@@ -433,7 +433,12 @@ fn run_tests_parallel(
433
  }
434
  drop(tx); // Drop our sender so rx closes when all workers finish
435
 
 
 
436
  for (idx, test) in tests.into_iter().enumerate() {
 
 
 
437
  let _ = work_tx.send((idx, test));
438
  }
439
  drop(work_tx); // Signal no more work
 
433
  }
434
  drop(tx); // Drop our sender so rx closes when all workers finish
435
 
436
+ // Small stagger between launches to reduce startup races on shared
437
+ // resources (thm.prv, custom.dat) that can't be safely redirected.
438
  for (idx, test) in tests.into_iter().enumerate() {
439
+ if idx > 0 {
440
+ thread::sleep(Duration::from_millis(50));
441
+ }
442
  let _ = work_tx.send((idx, test));
443
  }
444
  drop(work_tx); // Signal no more work
crates/openwa-wormkit/src/replacements/file_isolation.rs CHANGED
@@ -41,6 +41,11 @@ fn errorlog_redirect() -> Option<&'static str> {
41
  .as_deref()
42
  }
43
 
 
 
 
 
 
44
  /// Get (and lazily create) a per-PID temp directory under the game folder.
45
  fn temp_dir() -> Option<&'static str> {
46
  TEMP_DIR
@@ -58,10 +63,10 @@ fn temp_dir() -> Option<&'static str> {
58
  }
59
 
60
  /// Files in the game root directory that need isolation.
61
- const ROOT_FILES: &[&str] = &["writetest.txt", "mono.tmp", "custom.dat"];
62
 
63
  /// Files in the DATA subdirectory that need isolation.
64
- const DATA_FILES: &[&str] = &["land.dat", "landgen.svg", "current.thm", "playback.thm"];
65
 
66
  /// Check if a path ends with one of our target filenames (case-insensitive).
67
  /// Returns the replacement path if it matches, None otherwise.
 
41
  .as_deref()
42
  }
43
 
44
+ /// Returns the per-PID temp directory path, if file isolation is active.
45
+ pub fn temp_dir_path() -> Option<&'static str> {
46
+ TEMP_DIR.get().and_then(|o| o.as_deref())
47
+ }
48
+
49
  /// Get (and lazily create) a per-PID temp directory under the game folder.
50
  fn temp_dir() -> Option<&'static str> {
51
  TEMP_DIR
 
63
  }
64
 
65
  /// Files in the game root directory that need isolation.
66
+ const ROOT_FILES: &[&str] = &["writetest.txt", "mono.tmp"];
67
 
68
  /// Files in the DATA subdirectory that need isolation.
69
+ const DATA_FILES: &[&str] = &["land.dat", "landgen.svg"];
70
 
71
  /// Check if a path ends with one of our target filenames (case-insensitive).
72
  /// Returns the replacement path if it matches, None otherwise.
crates/openwa-wormkit/src/replacements/headless.rs CHANGED
@@ -111,5 +111,74 @@ pub fn install() -> Result<(), String> {
111
  }
112
 
113
  let _ = log_line("[Headless] All message box paths suppressed");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  Ok(())
115
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
 
113
  let _ = log_line("[Headless] All message box paths suppressed");
114
+
115
+ // Hook CreateSemaphoreA to rename the "Worms Armageddon" instance
116
+ // semaphore per-PID. Without this, concurrent WA.exe instances detect
117
+ // each other and skip initialization or show warnings.
118
+ unsafe {
119
+ let k32 = windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(
120
+ b"kernel32.dll\0".as_ptr(),
121
+ );
122
+ if !k32.is_null() {
123
+ let proc = windows_sys::Win32::System::LibraryLoader::GetProcAddress(
124
+ k32,
125
+ b"CreateSemaphoreA\0".as_ptr(),
126
+ );
127
+ if let Some(addr) = proc {
128
+ let target = addr as *mut core::ffi::c_void;
129
+ if let Ok(trampoline) = minhook::MinHook::create_hook(
130
+ target,
131
+ hook_create_semaphore_a as *mut core::ffi::c_void,
132
+ ) {
133
+ let _ = minhook::MinHook::enable_hook(target);
134
+ ORIG_CREATE_SEMAPHORE_A
135
+ .store(trampoline as u32, core::sync::atomic::Ordering::Relaxed);
136
+ let _ = log_line(&format!(
137
+ "[Headless] Hooked CreateSemaphoreA at 0x{:08X}",
138
+ addr as usize,
139
+ ));
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
  Ok(())
146
  }
147
+
148
+ // ─── CreateSemaphoreA hook ──────────────────────────────────────────────────
149
+
150
+ use core::sync::atomic::{AtomicU32, Ordering};
151
+
152
+ static ORIG_CREATE_SEMAPHORE_A: AtomicU32 = AtomicU32::new(0);
153
+
154
+ type CreateSemaphoreAFn = unsafe extern "system" fn(
155
+ lpSemaphoreAttributes: *mut core::ffi::c_void,
156
+ lInitialCount: i32,
157
+ lMaximumCount: i32,
158
+ lpName: *const u8,
159
+ ) -> *mut core::ffi::c_void;
160
+
161
+ unsafe extern "system" fn hook_create_semaphore_a(
162
+ attrs: *mut core::ffi::c_void,
163
+ initial: i32,
164
+ max: i32,
165
+ name: *const u8,
166
+ ) -> *mut core::ffi::c_void {
167
+ let orig: CreateSemaphoreAFn =
168
+ core::mem::transmute(ORIG_CREATE_SEMAPHORE_A.load(Ordering::Relaxed));
169
+
170
+ if !name.is_null() {
171
+ if let Ok(s) = std::ffi::CStr::from_ptr(name as *const i8).to_str() {
172
+ if s == "Worms Armageddon" {
173
+ let pid = std::process::id();
174
+ let new_name = format!("Worms Armageddon_{pid}\0");
175
+ let _ = log_line(&format!(
176
+ "[Headless] Renamed semaphore \"{s}\" β†’ \"Worms Armageddon_{pid}\""
177
+ ));
178
+ return orig(attrs, initial, max, new_name.as_ptr());
179
+ }
180
+ }
181
+ }
182
+
183
+ orig(attrs, initial, max, name)
184
+ }
crates/openwa-wormkit/src/replacements/mod.rs CHANGED
@@ -1,7 +1,7 @@
1
  mod config;
2
  mod ddgame_init;
3
  mod display;
4
- mod file_isolation;
5
  mod frontend;
6
  mod game_session;
7
  mod game_state_hooks;
 
1
  mod config;
2
  mod ddgame_init;
3
  mod display;
4
+ pub(crate) mod file_isolation;
5
  mod frontend;
6
  mod game_session;
7
  mod game_state_hooks;