簡體   English   中英

多線程在線交換與 Powershell 內核 - C#

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

我正在編寫一個 C# 應用程序,該應用程序將涉及使用PowerShell V2 模塊從 Exchange Online 收集數據。 在客戶執行管理員同意后,我將使用在 Windows 虛擬機上運行的多線程 C# 應用程序將 PowerShell 連接到他們的環境。 我正在使用 Net 5.0 和 PowerShell 7.x。 我需要使用多個線程,因為從單個租戶收集數據可能是一個漫長的過程。

問題是,雖然應用程序運行良好,但如果我嘗試使用多個線程同時為兩個租戶運行應用程序,則會發生沖突。 該模塊似乎不是線程安全的。

我已經構建了一個通過.Net DI 作為瞬態注入的服務。 此服務創建一個 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;
    }

當需要收集租戶的 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 將毫無問題地進行。

但是,如果您檢索郵箱信息,例如,兩個線程都會為最后連接的租戶提取數據。 這表明模塊或我的實現可能存在線程問題。

我沒有第二個租戶,但我確實有第二個具有不同權限的用戶帳戶,所以我將在我的建議中使用它。 我測試了幾種處理多個 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

我不是 100% 確定,但您可以嘗試以下方法:

執行Task.Run並在那里創建對InitialSessionState的引用。

這背后的原因(推測:):InitialSessionState.CreateDefault InitialSessionState.CreateDefault()方法是 static,但不是線程 static( 來源)。 這可能意味着您只創建一個InitialSessionState並在異步Task的所有實例之間共享它(請記住,異步Task不一定是新的Thread )。 這意味着在您的任務之間共享相同的InitialSessionState 從而導致使用一個客戶端登錄,然后是下一個客戶端。 這可以解釋這種行為。

免責聲明:我讀了這篇文章,這是我想到的第一件事。 我沒有以任何方式對此進行測試。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM