The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 1,619 other followers

.NET/C#: Getting volume free space from UNC path requires PInvoke of GetDiskFreeSpaceEx in Kernel32.dll

Posted by jpluimers on 2013/11/26

For some remote monitoring, I needed to get information on UNC paths.

Though suggested, you cannot do this using the System.IO.DriveInfo class (not through the constructor, nor through the VB.NET FileSystem way) as that is about drives, not UNC paths. The System.IO.DriveInfo constructor clearly indicates it doesn’t work with UNC paths. And if you still try, this is the error you will get:

System.ArgumentException was unhandled
HResult=-2147024809
Message=Object must be a root directory ("C:\") or a drive letter ("C").
Source=mscorlib
StackTrace:
at System.IO.DriveInfo..ctor(String driveName
)

Same for WMI: that only works when the UNC path has already been mapped to a drive letter.

You could do with adding a temporary drive letter but since there is nothing as permanent as a temporary

P/Invoke

The actual solution is based on calling Windows API functions using P/Invoke.

In fact, there are two kernel32.dll functions that can you get information from a UNC path:

In this post, I will show the  GetDiskFreeSpaceEx example and demonstrate my view on how setup P/Invoke: how to structure everything, what to make public and internal, etc.

The on-line code will also include GetVolumeInformation.

For a Windows API call, you have the parts shown below.

Part 1: the P/Invoke itself.

The API call P/Invoke import itself. I usually assemble these myself, based on the API specifiaction at the MSDN.microsoft.com site, and hints from the PInvoke.net site. Though the basic idea of that site is very nice, I hardly use the as-is source code from it, as the quality varies a lot, often in the bad sense.

For Kernel32.dll methods, they are in a big Kernel32Dll.cs file, for which the GetDiskFreeSpace part is extracted in an example below. The documentation for such a method contains a link to the MSDN API documentation, and a summary of parameter and result values.

All imports are using the kernel32_dll const, just in case I ever need to build an intermediate wrapper DLL for kernel32.

The Charset is almost always CharSet.Auto as that works far better than the default.

I set SetLastError to true, when the API documentation for the function indicates you can get the result by calling GetLastError, and GetDiskFreeSpaceEx does.

Note that the class is internal: it is not meant to be called from outside the assembly.

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace BeSharp.Win32
{
    internal class Kernel32Dll
    {
        private const string kernel32_dll = "kernel32.dll";

        ///
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937
        ///
        ///A directory on the disk.
        /// If this parameter is NULL, the function uses the root of the current disk.
        /// If this parameter is a UNC name, it must include a trailing backslash.
        ///Total number of free bytes on a disk that are available to the user who is associated with the calling thread.
        ///Total number of bytes on a disk that are available to the user who is associated with the calling thread.
        ///Total number of free bytes on a disk.
        /// Boolean indicating if the function succeeds.
        [DllImport(kernel32_dll, SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool GetDiskFreeSpaceEx(
            string lpDirectoryName,
            out ulong lpFreeBytesAvailable,
            out ulong lpTotalNumberOfBytes,
            out ulong lpTotalNumberOfFreeBytes
            );
    }
}

Part 2: abstract return parameters into one type per API call.

For Windows API calls returning more than one value, I usually create an intermediate immutable class holding the return values, and often the most important input parameters. For the parameters, I omit the prefixes.

As the class is immutable, it has a constructor that initializes the properties. The properties themselves cannot be modified.

These classes are public.

namespace BeSharp.Win32
{
    public class DiskFreeSpaceEx
    {
        public string DirectoryName { get; private set; }
        public ulong FreeBytesAvailable { get; private set; }
        public ulong TotalNumberOfBytes { get; private set; }
        public ulong TotalNumberOfFreeBytes { get; private set; }

        public DiskFreeSpaceEx(string directoryName, ulong freeBytesAvailable, ulong totalNumberOfBytes, ulong totalNumberOfFreeBytes)
        {
            DirectoryName = directoryName;
            FreeBytesAvailable = freeBytesAvailable;
            TotalNumberOfBytes = totalNumberOfBytes;
            TotalNumberOfFreeBytes = totalNumberOfFreeBytes;
        }
    }
}

Part 3: public API wrapper methods throwing Win32Error on API call failure.

The public calleable API wrappers are static methods, each on a class that indicates the domain of the API they wrap.

These methods throw Win32Error on failure.

On success, they translate the parameters into a new instance of the parameters class.

using System.Text;
using System.IO;
using System.ComponentModel;

namespace BeSharp.Win32
{
    public class DiskInfo
    {
        ///
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364937
        ///
        ///A directory on the disk.
        /// If this parameter is NULL, the function uses the root of the current disk.
        /// If this parameter is a UNC name, it must include a trailing backslash.
        /// DiskFreeSpaceEx on success, null on error.
        public static DiskFreeSpaceEx GetDiskFreeSpaceEx(string directoryName)
        {
            ulong freeBytesAvailable;
            ulong totalNumberOfBytes;
            ulong totalNumberOfFreeBytes;

            if (Kernel32Dll.GetDiskFreeSpaceEx(directoryName, out freeBytesAvailable, out totalNumberOfBytes, out totalNumberOfFreeBytes))
            {
                DiskFreeSpaceEx result = new DiskFreeSpaceEx(directoryName, freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes);
                return result;
            }
            else
                throw new Win32Exception();
        }
    }
}

Part 4: Reflector class to make demo app easier to write.

I’ve written a small console app demonstrating the use of GetDiskFreeSpaceEx that generates CSV output for the passed parameters. It uses the anonymous type cache, and below is the code I promised a while ago.

using System;
using System.Collections.Generic;
using System.Reflection;

namespace BeSharp.Generic
{
    public class Reflector
    {
        // http://abdullin.com/journal/2008/12/13/how-to-find-out-variable-or-parameter-name-in-c.html
        public static string GetName(T item)
        {
            string result = TypeCache.Name;
            return result;
        }

        public static IList GetNames(T items)
        {
            IList result =
                forEachNameAddToList(items,
                (list, name) => list.Add(name)
                );

            return result;
        }

        private static IList forEachNameAddToList(T items, Action<IList, string> adder)
        {
            IList result = new List();

            string[] names = TypeCache.Names;
            foreach (string name in names)
            {
                adder(result, name);
            }

            return result;
        }

        const BindingFlags defaultBindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;

        private static object getValue(T item, string name, BindingFlags bindingFlags = defaultBindingFlags)
        {
            MemberInfo[] memberInfos = typeof(T).GetMember(name, bindingFlags);
            int memberInfosLength = memberInfos.Length;
            if (memberInfosLength != 1)
                throw new ArgumentException(string.Format("parameter {0} with name {1} should return 1 member with that name, but returns {2} members", Reflector.GetName(new { item }), name, memberInfosLength));
            MemberInfo memberInfo = memberInfos[0];
            object value;
            PropertyInfo propertyInfo = memberInfo as PropertyInfo;
            if (null != propertyInfo)
                value = propertyInfo.GetValue(item, null);
            else
            {
                FieldInfo fieldInfo = memberInfo as FieldInfo;
                if (null != fieldInfo)
                    value = fieldInfo.GetValue(item);
                else
                    value = item;
            }
            return value;
        }

        private static string getValueString(T item, string name)
        {
            object value = getValue(item, name);
            string result = (null == value) ? string.Empty : value.ToString();
            return result;
        }

        public static IList GetValueStrings(T items)
        {
            IList result =
                forEachNameAddToList(items,
                (list, name) =>
                {
                    string valueString = getValueString(items, name);
                    list.Add(valueString);
                }
                );

            return result;
        }
    }
}

Part 5: small console demo to generate CSV out of the API calls.

With that, the CSV can be generated:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BeSharp.Generic;
using BeSharp.Win32;

namespace UNCInfo
{
    class Program
    {
        static void Main(string[] args)
        {
            if (0 == args.Length)
                Console.WriteLine("{0} UNC-path", "UNCInfo");
            else
            {
                string header = string.Empty;
                StringBuilder result = new StringBuilder();
                foreach (string arg in args)
                {
                    DiskFreeSpaceEx diskFreeSpaceEx = DiskInfo.GetDiskFreeSpaceEx(arg);
                    if (null != diskFreeSpaceEx)
                    {
                        try
                        {
                            const string comma = ",";
                            var values = new { diskFreeSpaceEx.DirectoryName, diskFreeSpaceEx.FreeBytesAvailable, diskFreeSpaceEx.TotalNumberOfBytes, diskFreeSpaceEx.TotalNumberOfFreeBytes };
                            IList valueStrings = Reflector.GetValueStrings(values);
                            string line = string.Join(comma, valueStrings);
                            result.Append(line);
                            IList names = Reflector.GetNames(values);
                            header = string.Join(comma, names);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Exception processing '{0}': {1}", arg, ex);
                        }
                    }
                    if (result.Length > 0)
                    {
                        Console.WriteLine(header);
                        Console.Write(result);
                    }
                }
            }
        }
    }
}

Have fun with it!

–jeroen

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

 
%d bloggers like this: