Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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