Утечки памяти при использовании пространства выполнения powershell в .NET - PullRequest
0 голосов
/ 23 октября 2019

Я обнаружил огромную утечку памяти в проекте, которая появляется после выполнения команды PS. Здесь я создаю пример, который представляет мою проблему:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

public class TestCase
{
    public void Test()
    {
        GetCommandsInfo(GetModules());
    }

    private void GetCommandsInfo(string[] modules)
    {
        InitialSessionState initialState = InitialSessionState.CreateDefault();
        initialState.ImportPSModule(modules);

        using (Runspace runspace = RunspaceFactory.CreateRunspace(initialState))
        {
            runspace.Open();

            using (Pipeline pipeline = runspace.CreatePipeline())
            {
                string script = "Get-Command -ListImported -CommandType Cmdlet,Alias";
                pipeline.Commands.AddScript(script);

                IEnumerable psObjects = pipeline.Invoke();
                if (psObjects == null)
                {
                    Debug.Fail("Pipeline.Invoke failed!");
                    throw new InvalidOperationException();
                }
            }

            runspace.Close();
        }
    }

    private static string[] GetModules()
    {
        string[] result;
        RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
        using (Runspace runspace = RunspaceFactory.CreateRunspace(rsConfig))
        {
            runspace.Open();

            using (Pipeline pipeline = runspace.CreatePipeline())
            {
                pipeline.Commands.AddScript("Get-Module -ListAvailable");
                IEnumerable psObjects = pipeline.Invoke();
                if (psObjects == null)
                {
                    Debug.Fail("Pipeline.Invoke failed!");
                    throw new InvalidOperationException();
                }

                result = GetModules(psObjects);
            }

            runspace.Close();
        }

        return result;
    }

    private static string[] GetModules(IEnumerable psObjects)
    {
        if (psObjects == null)
        {
            Debug.Fail("Get-Module: collection of PS Objects is null!");
            throw new ArgumentNullException("psObjects");
        }

        List<string> result = new List<string>();
        foreach (PSObject psObject in psObjects)
        {
            if (psObject == null || psObject.ImmediateBaseObject == null)
            {
                Debug.Fail("Get-Module: an item is null!");
                continue;
            }

            PSModuleInfo moduleInfo = psObject.ImmediateBaseObject as PSModuleInfo;
            if (moduleInfo == null)
            {
                Debug.Fail("Get-Module: an item is not PSModuleInfo!");
            }
            result.Add(moduleInfo.Name);
        }

        return result.ToArray();
    }
}

После исследования использования памяти я обнаружил, что большая часть памяти занята внутренними структурами из System.Management.Automation. Поэтому я начал искать способ освобождения ресурсов и обнаружил, что такое же поведение можно воспроизвести в PS:

Get-Module -ListAvailable | ForEach { Import-Module $_.Name }
Get-Command -ListImported -CommandType Cmdlet,Alias

Я уже пытался инициировать сборку мусора и удалить все новые переменные, которые появляются после выполнения скрипта,Я даже пытался запустить новый Runspace в PowerShell ISE для запуска сценария, но ресурсы все еще не высвобождались.

Во время моего исследования я обнаружил, что этот код все еще потребляет кучу памяти:

public static void TestLeak()
{
    using (Runspace runspace = RunspaceFactory.CreateRunspace())
    {
        runspace.Open();

        using (Pipeline pipeline = runspace.CreatePipeline())
        {
            StringBuilder builder = new StringBuilder();
            builder.Append(@"for ($i = 0; $i -lt 1000; $i++)
            {
                Get-Process *
            }");
            pipeline.Commands.AddScript(builder.ToString());

            Collection<PSObject> psObjects = pipeline.Invoke();
            Console.WriteLine("Test count: " + psObjects.Count);
        }

        runspace.Close();
    }
}

Но если мы изменим это:

Get-Process *

на это:

$res = Get-Process *

, тогда память не протечет. Переменная выходит из области видимости, а затем освобождается сборщиком мусора. Но в первом случае похоже, что PowerShell хранит результаты где-то во внутренних структурах. Я пытался добавить переменную в свой сценарий для хранения результатов командлета, но ничего не изменилось, так как моя переменная никогда не выходит из области видимости!

Я застрял и не могу найти решения для этой проблемы. Может быть, кто-то сталкивался с такой проблемой и может помочь мне с этим.

...