Я использую PowerShell v2.0 для выполнения заданного процесса с помощью объекта System.Diagnostics.Process, асинхронно перехватывая сообщения stdout / stderr, записывая соответствующие $ EventArgs.Data на хост (для целей отладки) и предварительно ожидая его с помощью метка времени, в конечном счете, для целей регистрации:

$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.RedirectStandardOutput = $true
$ProcessInfo.RedirectStandardError = $true
$ProcessInfo.UseShellExecute = $false
$ProcessInfo.CreateNoWindow = $true
$ProcessInfo.FileName = "ping"
$ProcessInfo.Arguments = "google.com"

$Process = New-Object System.Diagnostics.Process
$Process.StartInfo = $ProcessInfo

$CaptureStandardOutputStandardError = {
    If (![String]::IsNullOrEmpty($EventArgs.Data)) {
        Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss.fffffff") [ExecCmd]: $($EventArgs.Data)"
    }
}

Register-ObjectEvent -InputObject $Process -SourceIdentifier StdOutEvent -Action $CaptureStandardOutputStandardError -EventName 'OutputDataReceived' | Out-Null
Register-ObjectEvent -InputObject $Process -SourceIdentifier ErrOutEvent -Action $CaptureStandardOutputStandardError -EventName 'ErrorDataReceived' | Out-Null

$Process.Start() | Out-Null
$Process.BeginOutputReadLine()
$Process.BeginErrorReadLine()

If ($Process.WaitForExit(10000)) {
    # Ensure streams are flushed
    $Process.WaitForExit()

    # Do stuff
} Else {
    # Timeout
}

Unregister-Event StdOutEvent
Unregister-Event ErrOutEvent

Я ожидаю, что сообщения stdout / stderr, записанные на консоль, будут иметь метки времени с интервалом в секунду (очевидно, что ping записывает в конвейер каждую секунду), но по какой-то причине все метки времени находятся в пределах примерно 10 миллисекунд друг от друга, поэтому зарегистрированные события, очевидно, запускаются, когда процесс завершается. Итак, я жду этого:

2016-11-27 14: 53: 15.6581302 [ExecCmd]: проверка связи с google.com [172.217.17.110] с 32 байтами данных: 2016-11-27 14: 53: 15.8778445 [ExecCmd]: ответ от 172.217.17.110: байты = 32 время = 1 мс TTL = 59 2016-11-27 14: 53: 16.6796113 [ExecCmd]: ответ от 172.217.17.110: байтов = 32 время = 1 мс TTL = 59 2016-11-27 14: 53: 17.7548025 [ExecCmd] : Ответ от 172.217.17.110: байты = 32 время = 1 мс TTL = 59 и т. Д.

Но вместо этого, увидев это ...

2016-11-26 19: 00: 53.0813327 [ExecCmd]: проверка связи с google.com [172.217.17.46] с 32 байтами данных: 2016-11-26 19: 00: 53.0842700 [ExecCmd]: ответ от 172.217.17.46: байты = 32 время = 1 мс TTL = 59 2016-11-26 19: 00: 53.0860183 [ExecCmd]: ответ от 172.217.17.46: байты = 32 время = 1 мс TTL = 59 2016-11-26 19: 00: 53.0899003 [ExecCmd] : Ответ от 172.217.17.46: байты = 32 время = 1 мс TTL = 59 и т. Д.

Я воссоздал это на C #, используя .NET Framework v2.0 (поскольку я использую PSv2), и он работает, как ожидалось. Код для справки:

ProcessStartInfo processInfo = new ProcessStartInfo();
processInfo.RedirectStandardError = true;
processInfo.RedirectStandardOutput = true;
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;
processInfo.FileName = "ping";
processInfo.Arguments = "google.com";

Process process = new Process();
process.StartInfo = processInfo;

process.OutputDataReceived += (sender, e) => {
    if (!string.IsNullOrEmpty(e.Data))
    {
        Console.WriteLine("{0} [ExecCmd]: {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffffff"), e.Data);
    }
};

process.ErrorDataReceived += (sender, e) => {
    if (!string.IsNullOrEmpty(e.Data))
    {
        Console.WriteLine("{0} [ExecCmd]: {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffffff"), e.Data);
    }
};

process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();

if (process.WaitForExit(10000))
{
    // Ensure streams are flushed
    process.WaitForExit();

    // Do stuff
}
else
{
    // Timeout
}

Что мне не хватает? Я не понимаю, почему один и тот же код эффективно работает на C #, но не на PS с той же версией .NET framework. Если вы пропустите оператор $ Process.WaitForExit (1000), он вернет правильные значения временной метки, поэтому я знаю, что PS может возвращать правильные значения. Самое близкое, что я могу найти, - это заменить оператор if ($Process.WaitForExit(10000)) оператором while (!$Process.HasExited) {}. Это работает, но не является жизнеспособным решением, поскольку (по понятным причинам) убивает процессор - и, как ни странно, добавление Start-Sleep x приводит к тому, что процесс не завершается. Я играл с событием $ Process.Exited, но это приводит к тому, что процесс никогда не завершается, хотя событие срабатывает и событие $ Process.HasExited возвращает $ true. Аналогичным образом создание объекта Timer и проверка $ Process.HasExited заставляет процесс также никогда не завершаться.

Я видел много вопросов, связанных с асинхронным захватом stdout / stderr с объектами Process в PS, но не смог найти никаких конкретных вопросов, связанных с моим вариантом использования. Любая помощь / совет / руководство были бы очень признательны, так как я ломаю голову этим! Заранее спасибо!

Саймон

0
sthounsell 27 Ноя 2016 в 18:38

2 ответа

Лучший ответ

Я не мог понять, как отображать потоки stdout / err в реальном времени на консоли и записывать в файл с дополнительными значениями (отметка времени и т. Д.) При подписке на события Process.OutputDataReceived и ErrorDataReceived, поэтому в конце я изменил такт и закончил делать следующее:

Function Write-Log ($Message) {
    Write-Output $Message
    Add-Content -Path "C:\temp\test.txt" -Value "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss.fffffff") [ExecCmd]: $Message"
}

Invoke-Expression "ping google.com" | ForEach-Object {Write-Log $_}

Это работает, однако я не уверен, является ли это наилучшей практикой или нет - тем не менее, он кажется эффективным и надежным.

0
sthounsell 28 Ноя 2016 в 10:17

Вы не можете зарегистрировать текущее время с помощью Get-Date . Вы должны получить доступ к TimeGenerated для Powershell.

Примерный пример:

(get-eventlog System | where-object {$_.EventID -eq “6005”})[0].TimeGenerated

Он сообщит вам точное время мероприятия. В вашем случае вы можете использовать следующее:

$Event.TimeGenerated.ToString("AnyFormat")
0
Ranadip Dutta 28 Ноя 2016 в 06:14