net-vips: Unstable and floating exception with the access of the file after reading it in Image.NewFromFile

Hi @kleisauke!

I got an exception and think that it relates to the implementation of NetVips (or LibVips) System.IO.IOException: The process cannot access the file ‘test-X.png’ because it is being used by another process

Explanation

So, we got an unstable and floating exception with the access of the file that was created via System.Diagnostics.Process in async operation and which was read in Image.NewFromFile after that.

So for the reproducing, needs 3 things:

  1. creates files in the async method via System.Diagnostics.Process in some temp directory
  2. read files via Image.NewFromFile
  3. try to delete the temp directory

The exception occurs on deleting the temp directory on the 3 step.

Why I think the issue relates to the NetVips (libvips).

  1. The first reason, if I do not read files via Image.NewFromFile (2 step), then deletion temp directory works stable and well.

  2. The second reason… If I do: Image.NewFromBuffer(File.ReadAllBytes(filePath), access: Enums.Access.Sequential) instead of Image.NewFromFile(filePath, access: Enums.Access.Sequential) the code works stable and well also (no exception).

Resources for reproducing

Bellow, I listed the full test code where you can reproduce the issue. Also, here the project with test code for easy reproducing: TestNetVipsAsync.zip This is the test picture (test.png) needed for this test: test

Important! For reproducing this issue, the test picture must be quite small (as I created).

Full test example

(Please, create the directory C:\Temp\TestNetVips and put there the picture test.png)

static async Task Main(string[] args)
{
	Console.WriteLine("Hello NetVips!");

	string basePath = "C:\\Temp\\TestNetVips\\";
	Directory.SetCurrentDirectory(basePath);

	var testPngFile = "test.png";
	var testDirectory = "TestDirectory";
	Directory.CreateDirectory(testDirectory);

	// Prepare 50 test files (clone 50 from test.png)
	for (int i = 0; i < 50; i++)
		File.Copy(testPngFile, Path.Combine(testDirectory, $"test-{i}.png"), true);

	async Task DoAll(string tempDirectory)
	{
		try
		{
			// prepare directory for converting
			Directory.CreateDirectory(tempDirectory);

			// Create test files for converting via external process
			// System.Diagnostics.Process
			using (var proc = Process.Start(new ProcessStartInfo
			{
				FileName = "xcopy",
				Arguments = $"/S /E /Q /Y {basePath}\\{testDirectory}\\*.* {Path.Combine(basePath, tempDirectory) }\\*.*\r\n",
				WorkingDirectory = "C:\\Temp",
				UseShellExecute = false,
				CreateNoWindow = true
			}))
			{
				proc?.WaitForExit();
			}

			// Do any await operation to make method really async and run it in different thread
			await File.ReadAllBytesAsync("test.png");
			// Ensure we are in different threads
			Debug.WriteLine($"Work with {tempDirectory} in {Thread.CurrentThread.ManagedThreadId} thread");

			// Open files for converting
			foreach (var filePath in Directory.GetFiles(tempDirectory, "test-*.png", SearchOption.TopDirectoryOnly))
			{
				using (Image image = Image.NewFromBuffer(File.ReadAllBytes(filePath), access: Enums.Access.Sequential))
				{
					// Do nothing, just read, this is already enough for issue
					//image.Jpegsave(".....", 75);
				}
			}

			// Delete temp directory !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
			// Here sometimes unexpectedly we got (this is an issue):
			// "System.IO.IOException: The process cannot access the file 'test-X.png' because it is being used by another process"
			Directory.Delete(tempDirectory, true);
		}
		catch (Exception e)
		{
			Debug.WriteLine(e);
			throw;
		}
	}


	try
	{
		// 10 iteration because the exception not stable
		for (int i = 0; i < 10; i++)
		{
			// 20 parallel tasks for reading files via NetVips
			var tasks = new List<Task>();
			for (int j = 0; j < 20; j++)
				tasks.Add(DoAll($"TempDirectory-{i}-{j}"));

			await Task.WhenAll(tasks);
		}
	}
	catch (Exception e)
	{
		Console.WriteLine(e);
		throw;
	}
}

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20 (13 by maintainers)

Most upvoted comments

NetVips.Native v8.12.1 is now available.

PR https://github.com/libvips/libvips/pull/2497 should fix this. The pre-compiled libvips Windows binaries build from that PR can be downloaded here (for testing purposes): https://libvips-packaging.s3.amazonaws.com/vips-dev-w64-web-8.12.0-29fb557-static.zip

Ah, I didn’t realize that WaitForExitAsync is only for .NET Core 5.0+. If you want, I can retest this with .NET Core 3.1, and see if there is way to fix this with this requirement.

The Image.NewFromFile -> Image.NewFromBuffer change is more like a workaround than a solution. I think you could also workaround this with the Image.NewFromStream function to avoid loading the entire file into memory, for example: https://github.com/kleisauke/net-vips/blob/087f9f7245b425285d548847d2f6f2c9abef28e3/tests/NetVips.Tests/ForeignTests.cs#L1055-L1060

I was able to reproduce it, and after some testing, I was able to resolve it with this patch:

--- a/issue-139.cs
+++ b/issue-139.cs
@@ -2,6 +2,8 @@ static async Task Main(string[] args)
 {
 	Console.WriteLine("Hello NetVips!");
 
+	Cache.MaxFiles = 0;
+
 	string basePath = "C:\\Temp\\TestNetVips\\";
 	Directory.SetCurrentDirectory(basePath);
 
@@ -31,7 +31,7 @@ static async Task Main(string[] args)
 				CreateNoWindow = true
 			}))
 			{
-				proc?.WaitForExit();
+				await proc?.WaitForExitAsync();
 			}
 
 			// Do any await operation to make method really async and run it in different thread

(tested 20 times without problems)

So, it looks like a race condition between the xcopy command and whenever libvips opens a file. This comment might also be relevant here: https://github.com/dotnet/runtime/blob/4ac2aaa87db417e57715abe63078bb6a2d8a18a4/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs#L1424-L1458

There might be still a change you’ll get a System.UnauthorizedAccessException exception on Windows due to Thumbs.db, search indexers and/or anti-virus software. In this case, it might be better to delete the image immediately after you have used it. For example:

--- a/issue-139.cs
+++ b/issue-139.cs
@@ -47,12 +49,13 @@ static async Task Main(string[] args)
 					// Do nothing, just read, this is already enough for issue
 					//image.Jpegsave(".....", 75);
 				}
+				File.Delete(filePath);
 			}
 
 			// Delete temp directory !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 			// Here sometimes unexpectedly we got (this is an issue):
 			// "System.IO.IOException: The process cannot access the file 'test-X.png' because it is being used by another process"
-			Directory.Delete(tempDirectory, true);
+			Directory.Delete(tempDirectory, false);
 		}
 		catch (Exception e)
 		{