Skip to content

Commit b8df80d

Browse files
SimonCroppclaude
andcommitted
Log locked files and locking processes on test cleanup failure
Uses Windows Restart Manager API to identify which processes hold file locks when TempDirectory.Dispose() fails. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 69a4591 commit b8df80d

2 files changed

Lines changed: 153 additions & 2 deletions

File tree

src/Tests/BuildTargetsTests.cs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,158 @@ public TempDirectory()
142142

143143
public void Dispose()
144144
{
145-
if (Directory.Exists(Path))
145+
if (!Directory.Exists(Path))
146+
{
147+
return;
148+
}
149+
150+
try
146151
{
147152
Directory.Delete(Path, true);
148153
}
154+
catch (IOException ex)
155+
{
156+
TestContext.Out.WriteLine($"Failed to delete temp directory: {Path}");
157+
TestContext.Out.WriteLine($"Exception: {ex.Message}");
158+
LogLockedFiles(Path);
159+
throw;
160+
}
161+
}
162+
163+
static void LogLockedFiles(string directory)
164+
{
165+
TestContext.Out.WriteLine("Scanning for locked files...");
166+
167+
foreach (var file in Directory.GetFiles(directory, "*", SearchOption.AllDirectories))
168+
{
169+
if (IsFileLocked(file))
170+
{
171+
TestContext.Out.WriteLine($"LOCKED: {file}");
172+
var processes = GetLockingProcesses(file);
173+
foreach (var proc in processes)
174+
{
175+
TestContext.Out.WriteLine($" Locked by: {proc}");
176+
}
177+
}
178+
}
179+
}
180+
181+
static bool IsFileLocked(string filePath)
182+
{
183+
try
184+
{
185+
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
186+
return false;
187+
}
188+
catch (IOException)
189+
{
190+
return true;
191+
}
192+
catch (UnauthorizedAccessException)
193+
{
194+
return true;
195+
}
196+
}
197+
198+
static List<string> GetLockingProcesses(string filePath)
199+
{
200+
var result = new List<string>();
201+
202+
var res = RmStartSession(out var sessionHandle, 0, Guid.NewGuid().ToString());
203+
if (res != 0)
204+
{
205+
result.Add($"(Failed to start Restart Manager session: error {res})");
206+
return result;
207+
}
208+
209+
try
210+
{
211+
string[] resources = [filePath];
212+
res = RmRegisterResources(sessionHandle, (uint)resources.Length, resources, 0, null, 0, null);
213+
if (res != 0)
214+
{
215+
result.Add($"(Failed to register resource: error {res})");
216+
return result;
217+
}
218+
219+
uint procInfoNeeded = 0;
220+
uint procInfo = 0;
221+
uint rebootReasons = 0;
222+
223+
res = RmGetList(sessionHandle, out procInfoNeeded, ref procInfo, null, ref rebootReasons);
224+
if (res == ERROR_MORE_DATA && procInfoNeeded > 0)
225+
{
226+
var processInfo = new RM_PROCESS_INFO[procInfoNeeded];
227+
procInfo = procInfoNeeded;
228+
229+
res = RmGetList(sessionHandle, out procInfoNeeded, ref procInfo, processInfo, ref rebootReasons);
230+
if (res == 0)
231+
{
232+
for (var i = 0; i < procInfo; i++)
233+
{
234+
try
235+
{
236+
var proc = Process.GetProcessById(processInfo[i].Process.dwProcessId);
237+
result.Add($"PID {proc.Id}: {proc.ProcessName} ({proc.MainModule?.FileName ?? "unknown path"})");
238+
}
239+
catch
240+
{
241+
result.Add($"PID {processInfo[i].Process.dwProcessId}: {processInfo[i].strAppName} (process no longer running or inaccessible)");
242+
}
243+
}
244+
}
245+
}
246+
else if (res == 0 && procInfoNeeded == 0)
247+
{
248+
result.Add("(No processes found via Restart Manager - file may be locked by system)");
249+
}
250+
}
251+
finally
252+
{
253+
RmEndSession(sessionHandle);
254+
}
255+
256+
if (result.Count == 0)
257+
{
258+
result.Add("(Unable to determine locking process)");
259+
}
260+
261+
return result;
262+
}
263+
264+
const int ERROR_MORE_DATA = 234;
265+
266+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
267+
static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey);
268+
269+
[DllImport("rstrtmgr.dll")]
270+
static extern int RmEndSession(uint pSessionHandle);
271+
272+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
273+
static extern int RmRegisterResources(uint pSessionHandle, uint nFiles, string[]? rgsFilenames, uint nApplications, RM_UNIQUE_PROCESS[]? rgApplications, uint nServices, string[]? rgsServiceNames);
274+
275+
[DllImport("rstrtmgr.dll")]
276+
static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[]? rgAffectedApps, ref uint lpdwRebootReasons);
277+
278+
[StructLayout(LayoutKind.Sequential)]
279+
struct RM_UNIQUE_PROCESS
280+
{
281+
public int dwProcessId;
282+
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
283+
}
284+
285+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
286+
struct RM_PROCESS_INFO
287+
{
288+
public RM_UNIQUE_PROCESS Process;
289+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
290+
public string strAppName;
291+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
292+
public string strServiceShortName;
293+
public int ApplicationType;
294+
public uint AppStatus;
295+
public uint TSSessionId;
296+
[MarshalAs(UnmanagedType.Bool)]
297+
public bool bRestartable;
149298
}
150299
}

src/Tests/GlobalUsings.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
global using EmptyFiles;
22
global using NUnit.Framework;
3-
global using System.Collections.Immutable;
3+
global using System.Collections.Immutable;
4+
global using System.Diagnostics;
5+
global using System.Runtime.InteropServices;

0 commit comments

Comments
 (0)