Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/kill-process-locking-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
GENERATED FILE - DO NOT EDIT
This file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).
Source File: /docs/mdsource/kill-process-locking-file.source.md
To change this file edit the source file and then run MarkdownSnippets.
-->

# Kill process locking file

When a verification writes a `.received.*` file, the operation can fail if another process (for example, an editor or a previously launched diff tool) is holding a lock on that file. The exception surfaces as an `IOException`: "The process cannot access the file because it is being used by another process".

Verify can detect those processes via the Windows Restart Manager and terminate them so the write can be retried.


## Enable

Set the `Verify_KillProcessLockingFile` environment variable to `true`.

When enabled, on Windows, any `IOException` raised during a snapshot file write triggers a lookup of every process holding a handle to the target path. Each such process is killed and the write is retried once.


## Platform support

The feature only runs on Windows, since the Restart Manager API is Windows-only. On other platforms, the environment variable has no effect.


## Caveats

Killing processes is destructive. Enable this only in environments where losing the work in those processes is acceptable, such as CI machines or developer machines where the only processes that would lock these files are short-lived diff or editor processes.
1 change: 1 addition & 0 deletions docs/mdsource/doc-index.include.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
* [Verifying binary data](/docs/binary.md)
* [Exception Message Format](/docs/exception-message-format.md)
* [Build server](/docs/build-server.md)
* [Kill process locking file](/docs/kill-process-locking-file.md)
* [Comparers](/docs/comparer.md)
* [Converters](/docs/converter.md)
* [Recording](/docs/recording.md)
Expand Down
22 changes: 22 additions & 0 deletions docs/mdsource/kill-process-locking-file.source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Kill process locking file

When a verification writes a `.received.*` file, the operation can fail if another process (for example, an editor or a previously launched diff tool) is holding a lock on that file. The exception surfaces as an `IOException`: "The process cannot access the file because it is being used by another process".

Verify can detect those processes via the Windows Restart Manager and terminate them so the write can be retried.


## Enable

Set the `Verify_KillProcessLockingFile` environment variable to `true`.

When enabled, on Windows, any `IOException` raised during a snapshot file write triggers a lookup of every process holding a handle to the target path. Each such process is killed and the write is retried once.


## Platform support

The feature only runs on Windows, since the Restart Manager API is Windows-only. On other platforms, the environment variable has no effect.


## Caveats

Killing processes is destructive. Enable this only in environments where losing the work in those processes is acceptable, such as CI machines or developer machines where the only processes that would lock these files are short-lived diff or editor processes.
1 change: 1 addition & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ To change this file edit the source file and then run MarkdownSnippets.
* [Verifying binary data](/docs/binary.md)
* [Exception Message Format](/docs/exception-message-format.md)
* [Build server](/docs/build-server.md)
* [Kill process locking file](/docs/kill-process-locking-file.md)
* [Comparers](/docs/comparer.md)
* [Converters](/docs/converter.md)
* [Recording](/docs/recording.md)
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ Browser testing via
* [Verifying binary data](/docs/binary.md)
* [Exception Message Format](/docs/exception-message-format.md)
* [Build server](/docs/build-server.md)
* [Kill process locking file](/docs/kill-process-locking-file.md)
* [Comparers](/docs/comparer.md)
* [Converters](/docs/converter.md)
* [Recording](/docs/recording.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Type: Exception,
Message: Could not convert `Verify_KillProcessLockingFile` environment variable to a bool. Value: foo,
StackTrace:
at FileLockKiller.ParseEnvironmentVariable(String text)
at FileLockKillerTests.<>c.<ParseEnvironmentVariable_failure>b__1_0()
}
14 changes: 14 additions & 0 deletions src/Verify.Tests/FileLockKillerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public class FileLockKillerTests
{
[Fact]
public void ParseEnvironmentVariable()
{
Assert.False(FileLockKiller.ParseEnvironmentVariable(null));
Assert.False(FileLockKiller.ParseEnvironmentVariable("false"));
Assert.True(FileLockKiller.ParseEnvironmentVariable("true"));
}

[Fact]
public Task ParseEnvironmentVariable_failure() =>
Throws(() => FileLockKiller.ParseEnvironmentVariable("foo"));
}
2 changes: 1 addition & 1 deletion src/Verify/Compare/FileComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ static async Task<EqualityResult> InnerCompare(FilePair file, Stream receivedStr
return new(Equality.Equal, compareResult.Message, null, null);
}

File.Copy(fileStream.Name, file.ReceivedPath, true);
IoHelpers.CopyFile(fileStream.Name, file.ReceivedPath);
return new(Equality.NotEqual, compareResult.Message, null, null);
}

Expand Down
49 changes: 49 additions & 0 deletions src/Verify/FileLockKiller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
static class FileLockKiller
{
static FileLockKiller()
{
var text = Environment.GetEnvironmentVariable("Verify_KillProcessLockingFile");
Enabled = ParseEnvironmentVariable(text);
}

public static bool Enabled { get; }

public static bool ParseEnvironmentVariable(string? text)
{
if (text is null)
{
return false;
}

if (bool.TryParse(text, out var result))
{
return result;
}

throw new($"Could not convert `Verify_KillProcessLockingFile` environment variable to a bool. Value: {text}");
}

public static void KillProcessesLockingFile(string path)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}

foreach (var process in RestartManager.GetProcessesLockingFile(path))
{
try
{
process.Kill();
process.WaitForExit(5000);
}
catch
{
}
finally
{
process.Dispose();
}
}
}
}
27 changes: 25 additions & 2 deletions src/Verify/IoHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,31 @@ public static Task DisposeAsyncEx(this Stream stream)
}
#endif

static FileStream OpenWrite(string path) =>
new(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true);
static FileStream OpenWrite(string path)
{
try
{
return new(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true);
}
catch (IOException) when (FileLockKiller.Enabled)
{
FileLockKiller.KillProcessesLockingFile(path);
return new(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true);
}
}

public static void CopyFile(string source, string destination)
{
try
{
File.Copy(source, destination, true);
}
catch (IOException) when (FileLockKiller.Enabled)
{
FileLockKiller.KillProcessesLockingFile(destination);
File.Copy(source, destination, true);
}
}

public static FileStream OpenRead(string path) =>
new(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
Expand Down
134 changes: 134 additions & 0 deletions src/Verify/RestartManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
[SupportedOSPlatform("windows")]
static class RestartManager
{
const int RmRebootReasonNone = 0;
const int CchRmMaxAppName = 255;
const int CchRmMaxSvcName = 63;
const int ErrorMoreData = 234;

[StructLayout(LayoutKind.Sequential)]
struct RM_UNIQUE_PROCESS
{
public int dwProcessId;
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
}

enum RM_APP_TYPE
{
RmUnknownApp = 0,
RmMainWindow = 1,
RmOtherWindow = 2,
RmService = 3,
RmExplorer = 4,
RmConsole = 5,
RmCritical = 1000
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct RM_PROCESS_INFO
{
public RM_UNIQUE_PROCESS Process;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CchRmMaxAppName + 1)]
public string strAppName;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CchRmMaxSvcName + 1)]
public string strServiceShortName;

public RM_APP_TYPE ApplicationType;
public uint AppStatus;
public uint TSSessionId;

[MarshalAs(UnmanagedType.Bool)]
public bool bRestartable;
}

[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
static extern int RmRegisterResources(
uint pSessionHandle,
uint nFiles,
string[] rgsFilenames,
uint nApplications,
[In] RM_UNIQUE_PROCESS[]? rgApplications,
uint nServices,
string[]? rgsServiceNames);

[DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)]
static extern int RmStartSession(
out uint pSessionHandle,
int dwSessionFlags,
string strSessionKey);

[DllImport("rstrtmgr.dll")]
static extern int RmEndSession(uint pSessionHandle);

[DllImport("rstrtmgr.dll")]
static extern int RmGetList(
uint dwSessionHandle,
out uint pnProcInfoNeeded,
ref uint pnProcInfo,
[In, Out] RM_PROCESS_INFO[]? rgAffectedApps,
ref uint lpdwRebootReasons);

public static List<Process> GetProcessesLockingFile(string path)
{
var processes = new List<Process>();
var key = Guid.NewGuid().ToString();

var startResult = RmStartSession(out var handle, 0, key);
if (startResult != 0)
{
return processes;
}

try
{
string[] resources = [path];
var registerResult = RmRegisterResources(handle, (uint) resources.Length, resources, 0, null, 0, null);
if (registerResult != 0)
{
return processes;
}

uint pnProcInfo = 0;
var rebootReasons = (uint) RmRebootReasonNone;

var listResult = RmGetList(handle, out var pnProcInfoNeeded, ref pnProcInfo, null, ref rebootReasons);
if (listResult == 0)
{
return processes;
}

if (listResult != ErrorMoreData)
{
return processes;
}

var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
pnProcInfo = pnProcInfoNeeded;
listResult = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref rebootReasons);
if (listResult != 0)
{
return processes;
}

for (var i = 0; i < pnProcInfo; i++)
{
try
{
var process = Process.GetProcessById(processInfo[i].Process.dwProcessId);
processes.Add(process);
}
catch (ArgumentException)
{
}
}
}
finally
{
RmEndSession(handle);
}

return processes;
}
}
Loading