[英]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.