C#: Многопоточный (задачный) командлет PowerShell
По теме, я пытаюсь собрать многопоточный командлет PowerShell и борюсь. Я могу заставить код успешно работать как консольное приложение, и я могу заставить работать однопоточный командлет, но пока не могу объединить их.
Кроме того, пожалуйста, поймите, что я новичок в C# и очень мало знаю о том, что я делаю, поэтому заранее извиняюсь за зверство моего кода. Перед публикацией постарался максимально упростить.
Вот что у меня есть:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management.Automation;
using System.DirectoryServices;
using System.Diagnostics;
using System.Threading;
namespace Sample
{
[Cmdlet(VerbsCommon.Get,"LDAPObject")]
[OutputType(typeof(List<SearchResult>))]
public class GetLDAPObjectCmdlet: Cmdlet
{
[Parameter(ValueFromPipeline = true)]
public string searchRoot { get; set; }
[Parameter]
public TimeSpan maxWaitTime { get; set; }
private List<Task<searchResults>> taskList = new List<Task<searchResults>>();
protected override void BeginProcessing()
{
base.BeginProcessing();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;
Task<searchResults> searchTask = Task<searchResults>.Factory.StartNew(() => Search(ct, searchRoot));
taskList.Add(searchTask);
Stopwatch sw = Stopwatch.StartNew();
bool tasksCompleted = new bool();
try
{
tasksCompleted = Task.WaitAll(taskList.ToArray(), maxWaitTime);
}
catch (OperationCanceledException ex)
{
WriteWarning("Task cancelled.");
}
int resultCount = 0;
foreach (Task<searchResults> taskItem in taskList)
{
if ((taskItem.Status == TaskStatus.RanToCompletion) && (taskItem.Result.src != null))
{
resultCount += taskItem.Result.src.Count;
}
foreach (string message in taskItem.Result.warningMessages)
{
WriteWarning(message);
}
}
sw.Stop();
if (tasksCompleted)
{
WriteVerbose("The search returned `'" + resultCount.ToString() + "' results in '" + (sw.Elapsed.TotalSeconds).ToString() + "' seconds.");
}
else
{
WriteVerbose("The search did not complete before the deadline.");
}
}
protected override void EndProcessing()
{
base.EndProcessing();
for (int i = 0; i < taskList.Count; i++)
{
taskList[i].Dispose();
}
}
protected override void StopProcessing()
{
base.StopProcessing();
}
private static searchResults Search(CancellationToken ct, string searchRootDn)
{
if (ct.IsCancellationRequested)
{
ct.ThrowIfCancellationRequested();
}
searchResults results = new searchResults();
results.warningMessages = new List<string>();
results.verboseMessages = new List<string>();
results.warningMessages.Add("Search root: '" + searchRootDn + "'.");
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://" + searchRootDn);
try
{
Guid guid = searchRoot.Guid;
}
catch (Exception ex)
{
throw new Exception("Invalid search root");
}
string searchFilter = "some LDAP filter here";
results.warningMessages.Add("Search filter: '" + searchFilter + "'.");
DirectorySearcher directorySearcher = new DirectorySearcher();
directorySearcher.SearchRoot = searchRoot;
directorySearcher.SearchScope = SearchScope.Subtree;
directorySearcher.PageSize = 1000;
directorySearcher.Filter = searchFilter;
results.src = directorySearcher.FindAll();
directorySearcher.Dispose();
return results;
}
}
class searchResults
{
public SearchResultCollection src { get; set; }
public List<string> warningMessages { get; set; }
public List<string> verboseMessages { get; set; }
}
}
Это дает мне:
System.InvalidCastException HResult = 0x80004002 Message = Невозможно преобразовать COM-объект типа «System.__ComObject» в тип интерфейса «IDirectorySearch». Эта операция завершилась неудачно, так как вызов QueryInterface для COM-компонента для интерфейса с IID "{109BA8EC-92F0-11D0-A790-00C04FD8D5A8}" завершился сбоем из-за следующей ошибки: такой интерфейс не поддерживается (исключение из HRESULT: 0x80004002 (E_NOINTERFACE)) . Source=System.DirectoryServicesStackTrace: в System.DirectoryServices.SearchResultCollection.get_SearchObject() в System.DirectoryServices.SearchResultCollection.ResultsEnumerator.MoveNext() в System.DirectoryServices.SearchResultCollection.get_InnerList() в System.DirectoryServices.SearchResultCollection.get_Count() в MultiThreadedSearch.GetLDAPObjectCmdlet.ProcessRecord() в C:\Users\obfuscated\GETLDAPObjectCmdlet.
на resultCount += taskItem.Result.src.Count;линия.
Обратите внимание, что на самом деле мне не нужен класс searchResult ; Я только что представил его из-за отсутствия средства, с помощью которого функция поиска могла бы вносить контент в WriteWarning, WriteVerbose и т. д.
Мысли ценятся выше всего!
1 ответ
Похоже, я сам решил свою проблему. Неожиданно оказалось, что проблема связана с тем, как реализован интерфейс DirectorySearcher. В частности, результаты поиска LDAP не кэшируются до первого вызова для перечисления SearchResultsCollection, в частности, когда я пытался получить доступ к свойству Count . Это хорошо описано здесь:
Обычно мы используем оператор foreach для перечисления результатов. В отличие от некоторых других классов коллекций в .NET Framework, SearchResultCollection внутренне реализует собственный частный объект IEnumerator, который извлекает результаты из каталога. Любой доступ к свойствам SearchResultCollection, например проверка свойства Count, приведет к тому, что весь набор результатов будет извлечен из каталога, а затем пронумерован. Это может иметь большое влияние, особенно при возврате большого количества результатов, по нескольким причинам. Во-первых, использование цикла foreach позволило бы при необходимости прервать поиск, не перебирая полностью все результаты. Это более эффективно, так как мы прерываем поиск раньше и не заставляем сервер сначала возвращать все результаты. Второй, мы можем обрабатывать каждый результат, поступающий с сервера, используя цикл foreach, не дожидаясь завершения всего поиска. Использование цикла for в сочетании со свойством Count предотвращает оба этих сценария. Таким образом, мы обычно рекомендуем использовать цикл foreach при перечислении наших результатов.