【问题标题】:Multi-threaded Exchange Online With Powershell Core - C#使用 Powershell Core 进行多线程在线交换 - C#
【发布时间】:2021-07-21 05:52:28
【问题描述】:

我正在编写一个 C# 应用程序,该应用程序将涉及使用 PowerShell V2 module 从 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;
    }

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

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

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

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

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

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

await runspace.RunScript(command);

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

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

【问题讨论】:

  • 您从 C# 运行 powershell 脚本是否有原因?为什么不直接用C#实现powershell,那你就没有问题了?
  • @Neil 我不确定我是否理解您的问题。 “在 C# 中实现 PowerShell”是什么意思?我引用了 Microsoft.PowerShell.SDK nuget 并使用它来运行 PowerShell Core,我相信这是在 .net 核心应用程序中运行 PowerShell 的方式。没有其他 Exchange Online API 可供我用作替代方案。
  • 有必要使用C#吗?您可以使用运行空间以及 ThreadJob 模块进行多线程处理,并且仅使用 PowerShell 不会出现此问题
  • 我可能错了,但不是 PS 库,只是 REST 调用的包装器吗?在这种情况下,只需从 C# 进行 REST 调用
  • @Neil 没有记录 REST 调用。我希望他们是,因为我讨厌 PowerShell,很明显。

标签: c# powershell office365 exchange-server powershell-core


【解决方案1】:

我没有第二个租户,但我确实有第二个具有不同权限的用户帐户,所以我将在我的建议中使用它。我测试了几种处理多个 exo 连接的方法,效果如下:

通过Start-Job 使用 Powershell 作业。作业在它们自己的会话中进行,并且有一定程度的隔离:

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

看起来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

所以我能够使用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

最后,只运行两个单独的 powershell 实例也可以。

我对 .net 不是很熟悉,但我猜你目前要么在开始一个新线程时重新使用 SessionState,要么你正在跨线程共享你的 runspace

【讨论】:

  • 非常感谢您的回答。我希望有一个更以 C# 为中心的解决方案,但您的回答给了我一个可以用来解决我的问题的框架。我授予你接受的答案。另外,您的答案也写得很好,并附有相关示例。
【解决方案2】:

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

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

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

免责声明:我读了这篇文章,这是我想到的第一件事。我没有以任何方式对此进行过测试。

【讨论】:

    猜你喜欢
    • 2013-08-03
    • 2010-12-10
    • 2023-03-12
    • 1970-01-01
    • 2017-07-20
    • 1970-01-01
    • 2021-05-29
    • 1970-01-01
    • 2015-09-26
    相关资源
    最近更新 更多