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 将顺利进行。
但是,例如,如果您检索邮箱信息,两个线程都会提取最后连接的租户的数据。这表明模块或我的实现可能存在线程问题。
我不是 100% 确定,但您可以尝试以下操作:
执行Task.Run并创建对那里的引用InitialSessionState。
这背后的原因(前面的推测!):该InitialSessionState.CreateDefault()方法是静态的,但不是线程静态的(source)。这可能意味着您只创建一个InitialSessionState并在 async 的所有实例之间共享它Task(请记住 asyncTask不一定是 new Thread)。这意味着InitialSessionState您的任务之间共享相同的内容。从而导致先登录一个客户端,然后登录下一个客户端。这可以解释这种行为。
免责声明:我读到这篇文章,这是我首先想到的。我还没有以任何方式对此进行测试。