简体   繁体   English

多线程在线交换与 Powershell 内核 - C#

[英]Multi-threaded Exchange Online With Powershell Core - C#

I'm writing a C# application that will involve collecting data from Exchange Online using the PowerShell V2 module .我正在编写一个 C# 应用程序,该应用程序将涉及使用PowerShell V2 模块从 Exchange Online 收集数据。 After clients perform an admin consent, I will make PowerShell connections to their environments using a multi-threaded C# app running on a Windows virtual machine.在客户执行管理员同意后,我将使用在 Windows 虚拟机上运行的多线程 C# 应用程序将 PowerShell 连接到他们的环境。 I'm using Net 5.0 and PowerShell 7.x.我正在使用 Net 5.0 和 PowerShell 7.x。 I need to use multiple threads because collecting data from a single tenant can be a lengthy process.我需要使用多个线程,因为从单个租户收集数据可能是一个漫长的过程。

The problem is that while the app runs fine, if I try to run the application for two tenants at the same time using multiple threads, there is collision.问题是,虽然应用程序运行良好,但如果我尝试使用多个线程同时为两个租户运行应用程序,则会发生冲突。 The module does not appear to be thread-safe.该模块似乎不是线程安全的。

I've built a service that gets injected via.Net DI as a transient.我已经构建了一个通过.Net DI 作为瞬态注入的服务。 This service creates a HostedRunspace class that performs state management for PowerShell.此服务创建一个 HostedRunspace class,它为 PowerShell 执行 state 管理。

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;
    }

When it becomes time to collect a tenant's PowerShell data, a new thread is created like so:当需要收集租户的 PowerShell 数据时,将创建一个新线程,如下所示:

IOnlineDataTaskRunner taskRunner = serviceProvider.GetRequiredService<IOnlineDataTaskRunner>();
Thread thread = new Thread(() => taskRunner.RunAsync(dataTask));
thread.Start();

Here I'm getting a transient version of my PowerShell service, which will itself new up a HostedRunspace.在这里,我得到了我的 PowerShell 服务的临时版本,它本身将新建一个 HostedRunspace。 I create a new thread, provide it some configuration and start the thread.我创建一个新线程,为其提供一些配置并启动该线程。

When the thread runs, I first must connect to Exchange Online which I do using a certificate.当线程运行时,我首先必须使用证书连接到 Exchange Online。

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

Then, after this, I perform a variety of other data retrieval tasks using the PowerShell Module, including retrieving mailbox information, storage size, etc. These are also executed via然后,在此之后,我使用 PowerShell 模块执行各种其他数据检索任务,包括检索邮箱信息、存储大小等。这些也是通过

await runspace.RunScript(command);

As stated above, if I run one thread at a time, there is no issue.如上所述,如果我一次运行一个线程,则没有问题。 But if I connect thread 1 to tenant A and thread 2 to tenant B, the initial Connect-ExchangeOnline will take place with no issues.但是,如果我将线程 1 连接到租户 A 并将线程 2 连接到租户 B,则初始 Connect-ExchangeOnline 将毫无问题地进行。

But then if you retrieve mailbox information, for example, both threads will pull data for whichever tenant connected last.但是,如果您检索邮箱信息,例如,两个线程都会为最后连接的租户提取数据。 This indicates that there may be a threading issue with the module or perhaps with my implementation.这表明模块或我的实现可能存在线程问题。

I don't have a second Tenant, but I do have a second user account with different permissions so that's what I'll use in my suggestions.我没有第二个租户,但我确实有第二个具有不同权限的用户帐户,所以我将在我的建议中使用它。 I tested a couple ways of handling multiple exo connections, and here's what worked:我测试了几种处理多个 exo 连接的方法,这就是有效的方法:

Using Powershell jobs via Start-Job .通过Start-Job使用 Powershell 作业。 Jobs take place in their own sessions and have some level of isolation:作业在它们自己的会话中进行,并且具有一定程度的隔离性:

Start-Job -Name admin01 -ScriptBlock {
    Connect-ExchangeOnline -UserPrincipalName admin01@domain.com
    # Sleep so that second connection happens before this Get-Mailbox
    Start-Sleep -Seconds 10; 
    Get-Mailbox user02@domain.com  ## should succeed as admin
}
Start-Job -Name user01  -ScriptBlock {
    Connect-ExchangeOnline -UserPrincipalName user01@domain.com
    Get-Mailbox user02@domain.com  ## should error due to access as user
}

Receive-Job -Name admin01  ## returns mailbox
Receive-Job -Name user01   ## returns error

It looks like Connect-ExchangeOnline uses PSSession objects to store your connection, which you can re-import to verify you are connected to the correct tenant eg:看起来Connect-ExchangeOnline使用PSSession对象来存储您的连接,您可以重新导入以验证您是否连接到正确的租户,例如:

Get-PSSession | ft -AutoSize

Id Name                            ComputerName          ComputerType  State  ConfigurationName  Availability
-- ----                            ------------          ------------  -----  -----------------  ------------
 5 ExchangeOnlineInternalSession_1 outlook.office365.com RemoteMachine Opened Microsoft.Exchange    Available
 6 ExchangeOnlineInternalSession_2 outlook.office365.com RemoteMachine Opened Microsoft.Exchange    Available

So I was able to use Import-PSSession to load a specific connection for a command:所以我能够使用Import-PSSession为命令加载特定连接:

# First, run as admin01
Connect-ExchangeOnline -UserPrincipalName admin01@domain.com
Get-Mailbox user02@domain.com  ## Success

# Then, run as user01
Connect-ExchangeOnline -UserPrincipalName user01@domain.com
Get-Mailbox user02@domain.com  ## Error

# Then, run as admin01 again:
Import-PSSession (Get-PSSession 5) -AllowClobber
Get-Mailbox user02@domain.com  ## Success

Finally, just running two separate powershell instances works as well.最后,只运行两个单独的 powershell 实例也可以。

I'm not very familiar with .net, but my guess is you're currently either re-using the SessionState when starting a new thread, or you're sharing your runspace across threads.我对 .net 不是很熟悉,但我的猜测是您目前正在启动新线程时重新使用SessionState ,或者您正在跨线程共享runspace

I'm not a 100% sure, but you can try the following:我不是 100% 确定,但您可以尝试以下方法:

Do a Task.Run and create the reference to InitialSessionState there.执行Task.Run并在那里创建对InitialSessionState的引用。

The reason behind this (speculation ahead:): the InitialSessionState.CreateDefault() method is static, but not thread static ( source ).这背后的原因(推测:):InitialSessionState.CreateDefault InitialSessionState.CreateDefault()方法是 static,但不是线程 static( 来源)。 Which could mean that you only create one InitialSessionState and share it among all the instances of your async Task s (remember that an async Task is not necessarily a new Thread ).这可能意味着您只创建一个InitialSessionState并在异步Task的所有实例之间共享它(请记住,异步Task不一定是新的Thread )。 Which means that the same InitialSessionState is shared between your tasks.这意味着在您的任务之间共享相同的InitialSessionState Thus resulting in logging on with one client and then the next.从而导致使用一个客户端登录,然后是下一个客户端。 This could explain the behaviour.这可以解释这种行为。

Disclaimer: I read this and this is the first thing that came to mind.免责声明:我读了这篇文章,这是我想到的第一件事。 I have not tested this in any way.我没有以任何方式对此进行测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM