Home > database >  Find running processes targeting .NET Core 3.1
Find running processes targeting .NET Core 3.1

Time:12-07

With the upcoming deprecation of .NET Core 3.1, we wanted to see if there was a way we could locate any "missed" products that may be running .NET Core 3.1 in our environments.

The environments these would be on would be Windows Servers running PS 5 or later, but not 7. The machines would all have 3.1 and 6.0 installed, as well as Framework 4.8.

We would not necessarily have Sysinternals already on these computers, but I could use a component from that if it can make this work.

Some of the computers may be running IIS related code with a NoManagedCode app pool, so would not be exes.

In all cases, I can definitively restrict the search paths for where these programs would be installed to. Anything outside of those specific paths should throw an error if netcore31 is removed, so that is acceptable fallout. Consider C:\path1, C:\Deep\path2, C:\Place\Where\Things\Are. Assume I have administrative privileges, of course.

CodePudding user response:

The following may work well enough:

It relies on helper function Get-DotNetTargetFramework (source code below), which tries to extract target-framework information via reflection from files that are .NET assemblies, building on this .NET Framework answer.

Caveats:

  • It requires PowerShell (Core) to run.

  • On occasion, loading assembly files fails - I'm unsure what that depends on; .LoadFile is used to load the assemblies.

Sample call:

$workingFolder = 'C:\ProgramData\chocolatey\lib\'

Get-ChildItem -LiteralPath $workingFolder -Recurse -Include *.exe, *.dll |
  Get-DotNetTargetFramework |
  Where-Object { $_.FrameworkType -eq 'Core' -and $_.FrameworkVersion -le [version] '3.1' }

Note:

  • Matching files that aren't .NET assemblies generate warnings, which you can suppress with 3>$null

  • Matching files that are .NET assemblies, but can't be loaded, result in non-terminating errors, which you can suppress with 2>$null and / or collect for later processing in, say, variable $errs with -ErrorVariable errs

  • .NET Framework assemblies prior to v4.7.1 aren't guaranteed to have a System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription attribute, which the code relies upon. All .NET (Core)-targeted assemblies should.


Get-DotNetTargetFramework source code:

function Get-DotNetTargetFramework {
  <#
.SYNOPSIS
  Outputs target framework / runtime version information for .NET assemblies.
.DESCRIPTION
  Note: Runs in PowerShell *Core* only.

  The target .NET assemblies must be specifiy by their file paths, and 
  only literal file paths are accepted, but you can pipe Get-ChildItem output
  to this command.

  Input files that aren't .NET assemblies result in a warning.
  
  Assemblies that have no target-framework information in their manifests
  were typically compiled for .NET Framework versions before v4.7.1.
  Since then, and in all .NET (Core) versions, that information should be present.
  
.EXAMPLE
  Get-ChildItem -Recurse -Include *.exe, *.dll | Get-DotNetTargetFramework
  
  Outputs target framework information for all *.exe and *.dll files in the current
  directory subtree.
#>
  [CmdletBinding(PositionalBinding = $false)]
  param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [Alias('PSPath')]
    [string] $LiteralPath
  )
  begin {
    # !! We require running in PS Core, because NET Framework can't (easily) load .NET Core assemblies.
    if (-not $IsCoreCLR) { throw "This command runs in PowerShell (Core) only." }
  }
  process {
    $fullName = Convert-Path -LiteralPath $LiteralPath
    if (-not $?) { return }
    try {
      $assembly = [System.Reflection.Assembly]::LoadFile($fullName)
      $targetFramework = $assembly.GetCustomAttributesData().Where({ $_.AttributeType -eq [System.Runtime.Versioning.TargetFrameworkAttribute] }).ConstructorArguments.Value
      $versionString = if ($targetFramework -match '\d (?:\.\d ){1,3}') { $Matches[0] }
      [pscustomobject] @{
        AssemblyFilePath     = $fullName
        FrameworkType        = ('Framework', 'Core')[$targetFramework -and $targetFramework -match '^\.NETCoreApp']
        FrameworkVersion     = if ($versionString) { $versionString -as [version] }
        FrameworkDescription = if ($targetFramework) { $targetFramework } else { '.NETFramework,Version=vPRE_4.7.1' }
      }
    }
    catch {
      if ($_.Exception.InnerException -is [System.BadImageFormatException]) {
        Write-Warning "Not a .NET assembly: '$fullName'"
      }
      elseif ($_.Exception.InnerException -is [System.IO.FileLoadException]) {
        # !! Unclear, when that happens. $_.Exception.InnerException provides NO further clues.
        Write-Error " .NET assembly that cannot be loaded: '$fullName'`n$_"
      }
      else {
        Write-Error "Unexpected error with '$fullName': $_"
      }
    }
  }
}

CodePudding user response:

After discussing with my team and a little experimentation, this is the direction we are currently leaning for solution, but I am 100% open to other suggestions on how to determine this:

$workingFolder = 'C:\ProgramData\chocolatey\lib\'

Get-ChildItem -Directory -Path $workingFolder | % {
    $path = $_.FullName
    $children = Get-ChildItem -Path $path -Recurse -Filter 'lib\runtimes\win\lib\' -Directory -ErrorAction Ignore
    if ($children.Count -gt 0 -and -not ($children.Name -contains 'net6.0')) { 
        $problemFolder = Join-Path -Path $path -ChildPath 'lib\runtimes\win\lib\'
        return (Get-ChildItem -Path $problemFolder -Directory) 
    }
}

In this case, every folder that has the subpath lib\runtimes\win\lib (netcore|6 apps) but that does not have a subpath under that for net6.0 should drop some entries like this:

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         4/7/2022   3:29 PM                netcoreapp2.0
d-----         4/7/2022   3:29 PM                netcoreapp2.1
d-----         4/7/2022   3:29 PM                netcoreapp3.0
d-----         4/7/2022   3:29 PM                netstandard2.0

Which we can see do not have a net6.0 but clearly have older runtime releated folders. The parent folders that either have no lib\runtimes\win\lib structure, or that do have the lib\runtimes\win\lib\net6.0 subfolder tree, will not return on the above result set. Which is the behavior I'm seeing.

The faulty assumption that I know I am making is that it is possible to have a net6.0 application that does not contain this runtimes folder, but I feel like the knowledge and desire necessary to evoke that situation is suspect, as most of my devs, while highly competent, have no desire to change these semantics and make life harder for themselves. Rogue actors may exist in the ecosystem, but I hope not. Third parties likewise may have a problem, but again, simplest path is where most folks live.

While I am quite open to other answers (begging for them), I think this is what we are going to run with for now for our team (for the purposes of others coming here also wanting a solution to this problem)

  • Related