使用 Powershell Core 进行多线程在线交换 - C#

Chr*_*isW 5 c# powershell exchange-server office365 powershell-core

我正在编写一个 C# 应用程序,该应用程序将涉及使用PowerShell V2 模块从 Exchange Online 收集数据。在客户获得管理员同意后,我将使用在 Windows 虚拟机上运行的多线程 C# 应用程序与他们的环境建立 PowerShell 连接。我正在使用 Net 5.0 和 PowerShell 7.x。我需要使用多个线程,因为从单个租户收集数据可能是一个漫长的过程。

问题是,虽然应用程序运行良好,但如果我尝试使用多个线程同时为两个租户运行应用程序,则会发生冲突。该模块似乎不是线程安全的。

我构建了一个通过 .Net DI 作为瞬态注入的服务。此服务创建一个 HostedRunspace 类,用于执行 PowerShell 的状态管理。

public class HostedRunspace : IDisposable
{
    private Runspace runspace;
 
    public void Initialize(string[] modulesToLoad = null)
    {

        InitialSessionState defaultSessionState = InitialSessionState.CreateDefault();
        defaultSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.RemoteSigned;
        
        if (modulesToLoad != null)
        {
            foreach (string moduleName in modulesToLoad)
            {
                defaultSessionState.ImportPSModule(moduleName);
            }
        }

        runspace = RunspaceFactory.CreateRunspace(defaultSessionState);
        
        runspace.ThreadOptions = PSThreadOptions.UseNewThread;
        runspace.ApartmentState = ApartmentState.STA;

        runspace.Open();
    }

    public async Task<List<PSObject>> RunScript(string scriptContents, Dictionary<string, object> scriptParameters = null)
    {
        if (runspace == null)
        {
            throw new ApplicationException("Runspace must be initialized before calling RunScript().");
        }

        PSDataCollection<PSObject> pipelineObjects;
        
        using (System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(runspace))
        {
            ps.AddScript(scriptContents);

            if (scriptParameters != null)
            {
                ps.AddParameters(scriptParameters);
            }

            ps.Streams.Error.DataAdded += Error_DataAdded;
            ps.Streams.Warning.DataAdded += Warning_DataAdded;
            ps.Streams.Information.DataAdded += Information_DataAdded;

            // execute the script and await the result.
            pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false);
            
            // print the resulting pipeline objects to the console.
            Console.WriteLine("----- Pipeline Output below this point -----");
            foreach (PSObject item in pipelineObjects)
            {
                Console.WriteLine(item.BaseObject.ToString());
            }
        }

        List<PSObject> psObjects = new List<PSObject>();
        foreach (PSObject pipelineObject in pipelineObjects)
        {
            psObjects.Add(pipelineObject);
        }

        return psObjects;
    }
Run Code Online (Sandbox Code Playgroud)

当需要收集租户的 PowerShell 数据时,将创建一个新线程,如下所示:

IOnlineDataTaskRunner taskRunner = serviceProvider.GetRequiredService<IOnlineDataTaskRunner>();
Thread thread = new Thread(() => taskRunner.RunAsync(dataTask));
thread.Start();
Run Code Online (Sandbox Code Playgroud)

在这里,我获得了 PowerShell 服务的临时版本,该服务本身会新建一个 HostedRunspace。我创建一个新线程,为其提供一些配置并启动该线程。

当线程运行时,我首先必须使用证书连接到 Exchange Online。

string command = $"Connect-ExchangeOnline -CertificateThumbprint \"{Thumbprint}\" -AppId \"{ClientId}\" -ShowBanner:$false -Organization {tenant}"; 
await runspace.RunScript(command);
Run Code Online (Sandbox Code Playgroud)

然后,在此之后,我使用 PowerShell 模块执行各种其他数据检索任务,包括检索邮箱信息、存储大小等。这些也是通过

await runspace.RunScript(command);
Run Code Online (Sandbox Code Playgroud)

如上所述,如果我一次运行一个线程,就没有问题。但是,如果我将线程 1 连接到租户 A,将线程 2 连接到租户 B,则初始 Connect-ExchangeOnline 将顺利进行。

但是,例如,如果您检索邮箱信息,两个线程都会提取最后连接的租户的数据。这表明模块或我的实现可能存在线程问题。

Ken*_*nny 0

我不是 100% 确定,但您可以尝试以下操作:

执行Task.Run并创建对那里的引用InitialSessionState。

这背后的原因(前面的推测!):该InitialSessionState.CreateDefault()方法是静态的,但不是线程静态的(source)。这可能意味着您只创建一个InitialSessionState并在 async 的所有实例之间共享它Task(请记住 asyncTask不一定是 new Thread)。这意味着InitialSessionState您的任务之间共享相同的内容。从而导致先登录一个客户端,然后登录下一个客户端。这可以解释这种行为。

免责声明:我读到这篇文章,这是我首先想到的。我还没有以任何方式对此进行测试。