Skip to content
Draft
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
4 changes: 3 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<Project>

<PropertyGroup>
<ProjFSManagedVersion>2.0.0</ProjFSManagedVersion>
<ProjFSManagedVersion>2.1.0</ProjFSManagedVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>

</Project>
156 changes: 156 additions & 0 deletions ProjectedFSLib.Managed.Test/DisposeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using Microsoft.Windows.ProjFS;
using NUnit.Framework;
using System;

namespace ProjectedFSLib.Managed.Test
{
/// <summary>
/// Tests for VirtualizationInstance IDisposable implementation.
/// These tests verify the dispose pattern mechanics without requiring
/// the ProjFS optional feature to be enabled on the machine.
/// </summary>
public class DisposeTests
{
[Test]
public void VirtualizationInstance_ImplementsIDisposable()
{
// VirtualizationInstance must implement IDisposable to prevent zombie processes.
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

Assert.That(instance, Is.InstanceOf<IDisposable>());
}

[Test]
public void IVirtualizationInstance_ExtendsIDisposable()
{
// The interface itself must extend IDisposable so all implementations are required
// to support disposal.
Assert.That(typeof(IDisposable).IsAssignableFrom(typeof(IVirtualizationInstance)));
}

[Test]
public void Dispose_CanBeCalledMultipleTimes()
{
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

// Should not throw on any call.
instance.Dispose();
instance.Dispose();
instance.Dispose();
}

[Test]
public void StopVirtualizing_CanBeCalledMultipleTimes()
{
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

// Should not throw on any call.
instance.StopVirtualizing();
instance.StopVirtualizing();
}

[Test]
public void StopVirtualizing_ThenDispose_DoesNotThrow()
{
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

instance.StopVirtualizing();
instance.Dispose();
}

[Test]
public void AfterDispose_MethodsThrowObjectDisposedException()
{
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

instance.Dispose();

Assert.Throws<ObjectDisposedException>(() =>
instance.ClearNegativePathCache(out _));

Assert.Throws<ObjectDisposedException>(() =>
instance.DeleteFile("test.txt", UpdateType.AllowDirtyMetadata, out _));

Assert.Throws<ObjectDisposedException>(() =>
instance.WritePlaceholderInfo(
"test.txt", DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now,
System.IO.FileAttributes.Normal, 0, false, new byte[128], new byte[128]));

Assert.Throws<ObjectDisposedException>(() =>
instance.CreateWriteBuffer(4096));

Assert.Throws<ObjectDisposedException>(() =>
instance.CompleteCommand(0));

Assert.Throws<ObjectDisposedException>(() =>
instance.StartVirtualizing(null!));
}

[Test]
public void AfterStopVirtualizing_MethodsThrowObjectDisposedException()
{
var instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>());

instance.StopVirtualizing();

// StopVirtualizing should have the same effect as Dispose.
Assert.Throws<ObjectDisposedException>(() =>
instance.ClearNegativePathCache(out _));

Assert.Throws<ObjectDisposedException>(() =>
instance.CreateWriteBuffer(4096));
}

[Test]
public void UsingStatement_DisposesAutomatically()
{
VirtualizationInstance instance;
using (instance = new VirtualizationInstance(
"C:\\nonexistent",
poolThreadCount: 0,
concurrentThreadCount: 0,
enableNegativePathCache: false,
notificationMappings: new System.Collections.Generic.List<NotificationMapping>()))
{
// Instance is alive here.
}

// After using block, instance should be disposed.
Assert.Throws<ObjectDisposedException>(() =>
instance.ClearNegativePathCache(out _));
}
}
}
6 changes: 4 additions & 2 deletions ProjectedFSLib.Managed/ProjFSLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public string NotificationRoot

private static void ValidateNotificationRoot(string root)
{
if (root == "." || (root != null && root.StartsWith(".\\")))
if (root == "." || (root != null && root.StartsWith(".\\", StringComparison.Ordinal)))
{
throw new ArgumentException(
"notificationRoot cannot be \".\" or begin with \".\\\"");
Expand Down Expand Up @@ -216,7 +216,9 @@ public delegate bool NotifyPreCreateHardlinkCallback(
// Interfaces
public interface IWriteBuffer : IDisposable
{
#pragma warning disable CA1720 // Identifier contains type name — established public API, cannot rename
IntPtr Pointer { get; }
#pragma warning restore CA1720
UnmanagedMemoryStream Stream { get; }
long Length { get; }
}
Expand Down Expand Up @@ -289,7 +291,7 @@ HResult GetFileDataCallback(
string triggeringProcessImageFileName);
}

public interface IVirtualizationInstance
public interface IVirtualizationInstance : IDisposable
{
/// <summary>Returns the virtualization instance GUID.</summary>
Guid VirtualizationInstanceId { get; }
Expand Down
29 changes: 16 additions & 13 deletions ProjectedFSLib.Managed/ProjFSNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ internal static extern int PrjStartVirtualizing(
ref PRJ_CALLBACKS callbacks,
IntPtr instanceContext,
ref PRJ_STARTVIRTUALIZING_OPTIONS options,
out IntPtr namespaceVirtualizationContext);
out SafeProjFsHandle namespaceVirtualizationContext);

// PrjStopVirtualizing takes raw IntPtr (not SafeProjFsHandle) because it is
// called from SafeProjFsHandle.ReleaseHandle(), where the SafeHandle is already
// closed and cannot be marshaled. All other ProjFS APIs use SafeProjFsHandle.
#if NET7_0_OR_GREATER
[LibraryImport(ProjFSLib)]
internal static partial void PrjStopVirtualizing(IntPtr namespaceVirtualizationContext);
Expand All @@ -51,7 +54,7 @@ internal static partial int PrjWritePlaceholderInfo(
[DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int PrjWritePlaceholderInfo(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
string destinationFileName,
ref PRJ_PLACEHOLDER_INFO placeholderInfo,
uint length);
Expand All @@ -63,7 +66,7 @@ internal static partial int PrjWritePlaceholderInfo2(
[DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int PrjWritePlaceholderInfo2(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
string destinationFileName,
ref PRJ_PLACEHOLDER_INFO placeholderInfo,
uint placeholderInfoSize,
Expand All @@ -76,7 +79,7 @@ internal static partial int PrjWritePlaceholderInfo2Raw(
[DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "PrjWritePlaceholderInfo2")]
internal static extern int PrjWritePlaceholderInfo2Raw(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
IntPtr destinationFileName,
IntPtr placeholderInfo,
uint placeholderInfoSize,
Expand All @@ -89,7 +92,7 @@ internal static partial int PrjUpdateFileIfNeeded(
[DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int PrjUpdateFileIfNeeded(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
string destinationFileName,
ref PRJ_PLACEHOLDER_INFO placeholderInfo,
uint length,
Expand All @@ -103,7 +106,7 @@ internal static partial int PrjDeleteFile(
[DllImport(ProjFSLib, CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int PrjDeleteFile(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
string destinationFileName,
uint updateFlags,
out uint failureReason);
Expand Down Expand Up @@ -146,7 +149,7 @@ internal static partial int PrjWriteFileData(
[DllImport(ProjFSLib, ExactSpelling = true)]
internal static extern int PrjWriteFileData(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
ref Guid dataStreamId,
IntPtr buffer,
ulong byteOffset,
Expand All @@ -159,7 +162,7 @@ internal static partial IntPtr PrjAllocateAlignedBuffer(
[DllImport(ProjFSLib, ExactSpelling = true)]
internal static extern IntPtr PrjAllocateAlignedBuffer(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
UIntPtr size);

#if NET7_0_OR_GREATER
Expand All @@ -181,7 +184,7 @@ internal static partial int PrjCompleteCommand(
[DllImport(ProjFSLib, ExactSpelling = true)]
internal static extern int PrjCompleteCommand(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
int commandId,
int completionResult,
IntPtr extendedParameters);
Expand All @@ -193,7 +196,7 @@ internal static partial int PrjCompleteCommandWithNotification(
[DllImport(ProjFSLib, ExactSpelling = true, EntryPoint = "PrjCompleteCommand")]
internal static extern int PrjCompleteCommandWithNotification(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
int commandId,
int completionResult,
ref PRJ_COMPLETE_COMMAND_EXTENDED_PARAMETERS extendedParameters);
Expand All @@ -209,7 +212,7 @@ internal static partial int PrjClearNegativePathCache(
[DllImport(ProjFSLib, ExactSpelling = true)]
internal static extern int PrjClearNegativePathCache(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
out uint totalEntryNumber);

// ============================
Expand Down Expand Up @@ -306,7 +309,7 @@ internal static partial int PrjGetVirtualizationInstanceInfo(
[DllImport(ProjFSLib, ExactSpelling = true)]
internal static extern int PrjGetVirtualizationInstanceInfo(
#endif
IntPtr namespaceVirtualizationContext,
SafeProjFsHandle namespaceVirtualizationContext,
ref PRJ_VIRTUALIZATION_INSTANCE_INFO virtualizationInstanceInfo);

// ============================
Expand All @@ -318,7 +321,7 @@ internal struct PRJ_CALLBACK_DATA
{
public uint Size;
public uint Flags;
public IntPtr NamespaceVirtualizationContext;
public IntPtr namespaceVirtualizationContext; // PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT (native handle, NOT SafeHandle)
public int CommandId;
public Guid FileId;
public Guid DataStreamId;
Expand Down
37 changes: 37 additions & 0 deletions ProjectedFSLib.Managed/SafeProjFsHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Microsoft.Windows.ProjFS
{
/// <summary>
/// SafeHandle wrapper for the PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT returned
/// by PrjStartVirtualizing. Guarantees PrjStopVirtualizing is called even
/// during rude app domain unloads, Environment.Exit, or finalizer-only cleanup.
/// </summary>
/// <remarks>
/// <para>
/// SafeHandle is a CriticalFinalizerObject — the CLR guarantees its
/// ReleaseHandle runs after all normal finalizers and during constrained
/// execution regions. This provides the strongest possible guarantee that
/// the ProjFS virtualization root is released, preventing zombie processes.
/// </para>
/// </remarks>
internal class SafeProjFsHandle : SafeHandleZeroOrMinusOneIsInvalid
{
/// <summary>
/// Parameterless constructor required by P/Invoke marshaler for out-parameter usage.
/// </summary>
public SafeProjFsHandle() : base(ownsHandle: true) { }

protected override bool ReleaseHandle()
{
// Must use the raw 'handle' field (IntPtr) here, not 'this'.
// Inside ReleaseHandle, the SafeHandle is already marked as closed —
// passing 'this' to a P/Invoke taking SafeProjFsHandle would fail
// because the marshaler refuses to marshal a closed SafeHandle.
ProjFSNative.PrjStopVirtualizing(handle);
return true;
}
}
}
Loading