3

To get it out the way: I understand there are different ways to go about this such as Get-ChildItem.

So, I have a custom class I'm defining to get rid of some of the overhead PowerShell cmdlets have, as well as some .Net classes. The function will run just fine locally, but as soon as I try using it as scriptblock definition with Invoke-Command against a remote computer, it will just hang; even if I invoke it against my own computer. There is a process that's created for a WinRM Plugin that is shown in Task Manager but, that's it. Here are some working examples:

PS C:\Users\Abraham> Get-FolderSize -Path C:\Users\Abraham
143.98GB

PS C:\Users\Abraham> Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList C:\Users\Abraham 143.98GB

As shown above, this will work just fine and return the sum of all files. Then, when I pass a computer name to Invoke-Command for remote execution - it just hangs:

Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList C:\Users\Abraham -ComputerName $env:COMPUTERNAME

It goes without saying that a PSSession doesn't work either; this will be the primary method of running the function - passing it to an open PSSession.

My question is, what the hell is wrong? lol. Is there something going on behind the scenes that won't allow the use of P/Invoke remotely?

Here is the actual function:

Function Get-FolderSize ([parameter(Mandatory)][string]$Path) {
    Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace ProfileMethods { public class DirectorySum { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct WIN32_FIND_DATA { public uint dwFileAttributes; public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; public uint dwReserved0; public uint dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string cFileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName; }

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    private static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);

    [DllImport("kernel32.dll")]
    private static extern bool FindClose(IntPtr hFindFile);

    public long GetFolderSize(string path)
    {
        long size = 0;
        List<string> dirList = new List<string>();
        WIN32_FIND_DATA fileData;
        IntPtr hFile = FindFirstFile(path + @"\*.*", out fileData);
        if (hFile != IntPtr.Zero)
        {
            do
            {
                if (fileData.cFileName == "." || fileData.cFileName == "..")
                {
                    continue;
                }
                string fullPath = path + @"\" + fileData.cFileName;
                if ((fileData.dwFileAttributes & 0x10) == 0x10)
                {
                    dirList.Add(fullPath);
                }
                else
                {
                    size += ((long)fileData.nFileSizeHigh * (long)uint.MaxValue + (long)fileData.nFileSizeLow);
                }
            } while (FindNextFile(hFile, out fileData));
            FindClose(hFile);
            foreach (string dir in dirList)
            {
                size += GetFolderSize(dir);
            }
        }
        return size;
    }
}

} "@ $program = [ProfileMethods.DirectorySum]::new() switch ($program.GetFolderSize($Path)) { {$_ -lt 1GB} { '{0}MB' -f [math]::Round($/1MB,2); Continue } {$ -gt 1GB -and $_ -lt 1TB} { '{0}GB' -f [math]::Round($/1GB,2); Continue } {$ -gt 1TB} { '{0}TB' -f [math]::Round($_/1TB,2); Continue } } }

EDIT: Update - So, it works on subfolders, but not the root folder. Example:

$path = 'C:\Users\Abraham\Desktop' #works
Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList $path -ComputerName $env:COMPUTERNAME

...works, but the root folder C:\Users\Abraham doesn't.


Note: Passing the UNC path to the function/method will work.

Abraham Zinala
  • 205
  • 4
  • 10

1 Answers1

4

This is a guess because you haven't tried to investigate anything so there's not nearly enough information to provide an accurate answer.

but the root folder C:\Users\Abraham doesn't.

if ((fileData.dwFileAttributes & 0x10) == 0x10)

Modern Windows systems might need additional checks here – you need to pay attention to reparse points, which come in various types (symlinks, junctions, mount points, OneDrive placeholders...) and may also have the "Directory" flag in addition to the "ReparsePoint" flag (which is 0x400).

In particular, as Vista moved the old "~\Local Settings" directory to ~\AppData\Local, for compatibility it places a directory junction at "~\AppData\Local\Application Data" which points... back to the same "~\AppData\Local".

Junctions are slightly more special than symlinks and can have their own ACLs, so normally this junction will have a Deny ACE that stops you from doing FindFirstFile(@"Application Data\*.*") (i.e. it only allows direct access to known paths). But if the ACLs on it have ever been reset, any program trying to enumerate the contents of AppData will end up following an infinite loop, descending into the same junction forever (until it runs out of path length limit).

FindFirstFile(path + @"\*.*", out fileData);

Remember that file names are not required to have an . in it. FindFirstFile() deliberately makes this work (by allowing .* to match an empty string) to help out programs still stuck in the "8.3 filename.ext" era, but that won't be the case for programs implementing their own wildcard expansion.

grawity
  • 501,077