Саша это я, да
В первой части речь пойдет о самом незатейливом способе реализации автоматического управления устройствами. Несмотря на свою простоту и множество ограничений этот метод, тем не менее, позволит нам вводить команды на устройстве также и собственноручно.

Немного о процессах

.NET Framework предоставляет возможность перенаправлять стандартные потоки (вывода, ввода, ошибок) запускаемых процессов. Однако не все приложения используют их. Например, такой фокус не сработает с telnet и openssh. Таким образом, количество консольных приложений, с которыми мы сможем работать, ограничено. К счастью plink, консольный вариант Putty, подходит для наших нужд как нельзя лучше. Помимо использования стандартных потоков, plink предоставляет нам функции клиентов telnet, ssh и rlogin. Существует несколько неприятных моментов, которые, опять-таки к счастью, можно решить с помощью подручных инструментов, но об этом чуть позже.

Простое решение

Существует достаточно простой способ автоматически выполнять команды на удаленном устройстве. Для этого нужно перед запуском plink перенаправить его поток ввода с помощью метода RedirectStandardInput. Для этого нам даже не понадобится C#, т.к. PowerShell имеет доступ к объектам .NET Framework. Рассмотрим код PowerShell:

  1. $ps = New-Object -TypeName System.Diagnostics.Process
  2. $ps.StartInfo.UseShellExecute = $false
  3. $ps.StartInfo.RedirectStandardInput = $true
  4. $ps.StartInfo.FileName = "plink"
  5. $ps.StartInfo.Arguments = "-telnet 172.30.20.5"

  6. [void]$ps.Start()

  7. $myStreamWriter = $ps.StandardInput

  8. Start-Sleep -m 500
  9. $myStreamWriter.Write("root`r")
  10. Start-Sleep -m 500
  11. $myStreamWriter.Write("password`r")
  12. Start-Sleep -m 500
  13. $myStreamWriter.Write("enable`r")
  14. Start-Sleep -m 500
  15. $myStreamWriter.Write("exit`r")

  16. $myStreamWriter.Close();

  17. if (!$ps.HasExited) { $ps.Kill() }


Разберем код по строкам:
1:создаем объект .NET Framework класса System.Diagnostics.Process.
2-5: свойство StartInfo содержит параметры процесса, которые нужно указать перед его запуском.
2: чтобы появилась возможность перенаправлять потоки, свойству UseShellExecute необходимо присвоить значение false.
4-5: здесь, я полагаю, всё очевидно. Указываем, что запускать и с какими аргументами. Если путь к plink.exe не прописан в $env:path, необходимо указать полный путь.
7: запускаем процесс. [void] используется для того, чтобы PowerShell не выводил на экран возвращаемое методом Start() значение (True). В качестве альтернативы можно использовать $ps.Start() | Out-Null.
9: после старта процесса мы можем получить доступ к стандартному потоку ввода с помощью свойства StandardInput нашего объекта Process, которое ссылается на объект класса StreamWriter.
11-18: Отправка команд в поток ввода plink.
11: Перед отправкой каждой команды будем делать небольшую паузу (полсекунды) для того, чтобы устройство успело подготовиться и отправить нам приглашение (в данном случае - запрос имени пользователя).
12: Используем метод Write для записи в поток. При этом посылаемая последовательность должна заканчиваться символом возврата каретки. В случае PowerShell это `r. Также можно использовать метод WriteLine() - тогда управляющие символы нам не потребуются.
20: Отправка команд завершена, закрываем поток ввода.
22: Если последняя отправленная команда не инициировала корректного окончания сессии, то после завершения работы скрипта сам процесс останется в фоне. В данной строке мы с помощью свойства HasExited проверяем состояние процесса, и в случае если он все еще незавершен, убиваем его методом Kill().

Двигаемся дальше. Что будет, если мы случайно присвоим свойству FileName некорректное значение (например, "plik";)? Правильно, будет вызвано исключение. Мы с вами люди приличные и было бы неплохо, если бы такие моменты были нами учтены. Поэтому добавим следующую строку в наш скрипт:

trap{ Write-Host ("Error: Не найден путь к файлу " + $ps.StartInfo.FileName + "."); exit 1 }

Она выведет сообщение о некорректных данных и завершит скрипт.

А что если мы хотим, скажем, только автоматически залогониться на устройстве, а затем взять всё управление в свои руки? Для этого нам послужит следующий код:

  1. do { $myStreamWriter.Write([Console]::ReadKey($true).KeyChar) }
  2. while (!$ps.HasExited)


Цикл будет существовать вплоть до завершения процесса. На каждой итерации с помощью метода [Console]::ReadKey($true) считывается каждый отдельный символ. Единственный аргумент, установленный в true, отключает локальный вывод на экран вводимых символов (устройство и так будет отсылать их обратно, так что они будут выведены экран). ReadKey возвращает объект System.ConsoleKeyInfo, свойство KeyChar которого представляет собой введенный символ. Его-то мы в конечном итоге и отпраляем устройству с помощью $myStreamWriter.Write().

В итоге скрипт примет следующий вид:

  1. $ps = New-Object -TypeName System.Diagnostics.Process
  2. $ps.StartInfo.UseShellExecute = $false
  3. $ps.StartInfo.RedirectStandardInput = $true
  4. $ps.StartInfo.FileName = "plink"
  5. $ps.StartInfo.Arguments = "-telnet 172.30.20.5"

  6. trap{ Write-Host ("Error: Не найден путь к файлу " + $ps.StartInfo.FileName + "."); exit 1 }

  7. [void]$ps.Start()

  8. $myStreamWriter = $ps.StandardInput

  9. Start-Sleep -m 500
  10. $myStreamWriter.Write("root`r")
  11. Start-Sleep -m 500
  12. $myStreamWriter.Write("password`r")
  13. Start-Sleep -m 500
  14. $myStreamWriter.Write("enable`r")

  15. #Перед началом ввода очистим экран
  16. Clear-Host

  17. do { $myStreamWriter.Write([Console]::ReadKey($true).KeyChar) }
  18. while (!$ps.HasExited)

  19. $myStreamWriter.Close();

  20. if (!$ps.HasExited) { $ps.Kill() }

Возможно в процессе тестирования вы отметили прескорбный факт - нет реакции на нажатие клавиш со стрелками. В действительности это вполне решаемая проблема: если ReadKey считывает нажатие одной из этих клавиш, то мы должны отправить на вход процесса определенную специальную последовательность:

  1. do {

  2. $key = Console.ReadKey($true)

  3. if ( $key.Key -eq [System.ConsoleKey]::UpArrow ) { $myStreamWriter.Write("\x1b[A") }
  4. elseif ( $key.Key -eq [System.ConsoleKey]::DownArrow ) { $myStreamWriter.Write("\x1b[B") }
  5. elseif ( $key.Key -eq [System.ConsoleKey]::LeftArrow ) { $myStreamWriter.Write("\x1b[D") }
  6. elseif ( $key.Key -eq [System.ConsoleKey]::RightArrow ) { $myStreamWriter.Write("\x1b[C") }
  7. else { $myStreamWriter.Write($key.KeyChar) }

  8. } while (!$ps.HasExited)





NoteЕсли с помощью вашего скрипта вы пробовали подключаться к шеллу Линукса и выполняли ping к какому-нибудь узлу, то могли заметить, что стандартное прерывание выполнения команды по нажатию CTRL-C работает не так, как вы того ожидали. В plink эта комбинация используется для завершения работы. Я не знаю, как можно обойти эту проблему, если вы привыкли использовать стандартный эмулятор терминала и не готовы от него отказываться. Могу лишь сказать, что в ConEmu для этой цели отлично срабатывает комбинация CTRL-SHIFT-C.


Окончательный вариант кода

Сказ про Plink, Expect и PowerShell. Предисловие
Сказ про Plink, Expect и PowerShell. Часть II: Нам нужен multithreading (Начало)
Сказ про Plink, Expect и PowerShell. Часть II: Нам нужен multithreading (Продолжение)
Сказ про Plink, Expect и PowerShell. Часть III: PowerShell!

@темы: RedirectStandardInput, Putty, PowerShell, Plink, .NET Framework