Процесс, запущенный в выделенном SSH TTY, не прерывается после использования стандартного ввода
Я могу выполнить передачу файлов через SSH * следующим образом:
ssh -T ${HOST} eval "cat > remote.txt" < local.txt
Однако, если я вместо этого выделю TTY, он зависнет, пока я не нажму Ctrl+C:
ssh -tt ${HOST} eval "cat > remote.txt" < local.txt
Вопрос: почему это? Есть ли обходной путь?
Лучшее, что я могу понять, это то, что локальный EOF не распространяется на удаленный процесс.
Сведения о платформе: OpenSSH_5.3p1, CentOS 6,7 x86_64
* В моем реальном случае я хочу использовать этот подход для передачи файлов непосредственно удаленному пользователю sudo; Я не могу использовать SCP, потому что я не могу использовать SSH в качестве пользователя sudo. Файл sudoers в моей целевой среде имеет
requiretty
установить, следовательно, необходимость в TTY.
1 ответ
Немного теории
Лучшее, что я могу понять, это то, что локальный EOF не распространяется на удаленный процесс.
В принципе да, можно так сказать. В *nix EOF — это не символ, а условие. Когда программа, читающая файл, использует и читать больше нечего, она получает 0 байт и тогда понимает, что дошла до конца файла.
Чтение с терминала несколько отличается. Вы выделяете терминал на удаленной стороне, и ваш пульт считывает данные с этого терминала. Терминал находится в режиме. В этом режиме драйвер терминала буферизует данные (обычно это позволяет осуществлять базовое редактирование, например, с помощью Backspace), он отправляет данные по адресу Enter. В вашем случае он будет интерпретировать любой символ, исходящий из Enter. Когда больше нечего читать, нет и Enter, поэтому драйвер терминала ничего не отправляет иread()
из твоих продолжает ждать. Это имеет смысл, поскольку обычно это пользователь, печатающий на терминале; предполагать EOF, когда пользователь печатает недостаточно быстро, было бы очень неправильно.
Есть способ заставить драйвер терминала в режиме отправлять 0 байт. Если он читает символ (ASCII EOT), он отправит все, что в данный момент буферизует (буферизация во время ожидания Enter). Обычно вы отправляете на терминал, набрав Ctrl+d. Ctrl+dвместо Enterзаставит драйвер терминала отправить уже набранную строку без какого-либо символа новой строки (или подобного). Ctrl+dafter Ctrl+d(или Ctrl+dafter Enter) отправит ровно 0 байт, а затем такой инструмент, как ваш, увидит условие EOF и завершит работу.
Базовый (который не означает «хороший») обходной путь — отправить как минимум два символа после содержимого вашего локального файла:
{ cat local.txt; printf '\004\004'; } | ssh -tt …
Вы можете прочитать больше здесь:
- Ctrl+D для завершения ввода строки терминала
- Почему сочетание клавиш Ctrl-D (EOF) выходит из оболочки?
В случаеssh -tt …
Перехват символов — это не все, что делает драйвер терминала на удаленной стороне. Он делает гораздо больше, как с данными, поступающими от клиента, так и с данными, поступающими к клиенту (напечатанными удаленной командой). Давайте исследуем некоторые возможности, препятствия и особенности.
Немного практики
Предварительные примечания:
Во всех примерах используется
ssh -tt
. Предполагается, что у вас есть причины и-tt
является обязательным.Если вы хотите повторить приведенные ниже примеры, установите
remote='user@server'
со значениямиuser
иserver
это работает для вас. Цель состоит в том, чтобы позволить вам копировать и вставлять мои команды без каких-либо усилий.Однако примеры не используются. Я не очень понимаю, зачем вам этот вопрос. Если окажется, что вам нужно
eval
чтобы примеры работали, вам придется повозиться. Извини. Примеры мне подходят.Примеры показывают, что это работает без запроса пароля и работает достаточно быстро.
$
обозначает мою подсказку.#
обозначает комментарий. Строка с командой всегда имеет вид приглашения, за которым следует команда. Затем я получил результат (если таковой имеется). Дополнительный запрос в конце последней строки указывает на завершение команды; отсутствие запроса в конце последней строки указывает на то, что команда остановлена._
указывает положение локальной каретки (текстового курсора) после завершения или остановки команды.Мы хотим исследовать поведение удаленного терминала. Также есть местный терминал. Он не мешает вводу (наши команды не будут использовать его в качестве ввода), но может мешать выводу. Чтобы отложить локальный терминал, мы можем локально записать в обычный файл или канал (например) . Я решил, что это только запутает примеры. Существование местного терминала не влияет на выводы, к которым мы придем.
Я часто использую описания нажатий клавиш (например, Enter) для обозначения отправляемых нами символов (например, ). Это не всегда будет абсолютно точным (например, вы, Enterвероятно, генерирует , а не ). Я думаю, что нажатия клавиш более читабельны, и интерпретирую их скорее как указание на то, что мы хотим, чтобы драйвер удаленного терминала делал.
Вот примеры:
Давайте повторим вашу проблему:
$ printf 'Line1\nLine2' | ssh -tt "$remote" cat Line1 Line2Line1 _
Почему там дважды? Потому что удаленный терминал повторяет его ввод. Давайте что-нибудь с этим сделаем.
Попробуем подавить удаленное эхо:
$ printf 'Line1\nLine2' | ssh -tt "$remote" 'stty -echo; cat' Line1 Line2Line1 _
Это не сработало. Это неочевидно, но причина в том, что удаленный терминал повторил ввод, прежде чем переконфигурировать его.
Дадим немного времени:
$ { sleep 1; printf 'Line1\nLine2'; } | ssh -tt "$remote" 'stty -echo; cat' Line1 _
Теперь мы видим
Line1
прошел, но, вероятно, нет.Примечание — это уже хак. Если сервер был занят или связь с сервером была медленной, этого может быть недостаточно.
Давай "ударим" Enterпосле
Line2
:$ { sleep 1; printf 'Line1\nLine2\n'; } | ssh -tt "$remote" 'stty -echo; cat' Line1 Line2 _
Давайте отправим Ctrl+dвместо этого Enter:
$ { sleep 1; printf 'Line1\nLine2\004'; } | ssh -tt "$remote" 'stty -echo; cat' Line1 Line2_
Обратите внимание, что каретка находится не в том же месте, что и раньше.
Пошлем Ctrl+dдважды:
$ { sleep 1; printf 'Line1\nLine2\004\004'; } | ssh -tt "$remote" 'stty -echo; cat' Line1 Line2Connection to … closed. $ _
Пульт вышел. Вышеуказанное является нашим основным обходным решением.
Connection to … closed.
(с одним завершающим символом новой строки) исходит сам по себе и выводится локально в stderr, то есть на локальный терминал. Это не имеет никакого отношения к удаленному терминалу. С этого момента мы будем подавлять его с помощью2>/dev/null
.Давайте отправим что-нибудь еще:
$ { sleep 1; printf 'Line1\nLine2\004\004 Line2 continues'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null Line1 Line2$ _
Двойной Ctrl+dдействительно заставил нас увидеть EOF, но не прочитал остальную часть ввода.
Есть больше символов, которые взаимодействуют с драйвером удаленного терминала. (восьмеричное 10, десятичное 8, ASCII BS) похоже на Backspace. Давайте не будем раздражать пульт, говоря, что мы любим собак:
$ { sleep 1; printf 'I like dogs\010\010\010\010cats.\n\004'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null I like cats. $ _
Здесь мы использовали один Ctrl+dпосле Enter. По этой Enterже причине последнее локальное приглашение находится в отдельной строке, а не сразу после точки.
Мы можем отправить Ctrl+ c(, ASCII ETX):
$ { sleep 1; printf 'I like...\ndogs\03'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null $ _
Несмотря на то, что Enterв потоке (который обычно выполняет чтение и печать)
I like...
), вывод пуст. Ctrl+ cбыл перехвачен драйвером удаленного терминала, драйвер отправил его и был завершен до того, как успел напечатать. Да,\03
на входе действительно вызывает сигнал. Мы увидим это яснее, если нам удастся установить ловушку в удаленной оболочке до наших локальных проходов.Треппинг Ctrl+ c:
$ { sleep 1; printf '\03'; } | ssh -tt "$remote" 'stty -echo; trap "echo trap" INT; sleep 2' 2>/dev/null trap $ _
Как видите, определенные символы во входных данных вызывают определенные изменения или действия на удаленной стороне. На данный момент мы протестировали некоторые символы, которые редко встречаются в текстовых файлах. Вы можете подумать, что если ваш
local.txt
содержит то, что люди называют текстом (буквы, цифры, символы, символы новой строки), и это ASCII, тогда вы в безопасности. Ну, не строго; см. следующий пример.Окончания строк Windows, т.е.
$ # local test, for comparison $ printf 'Line1\r\nLine2\r\nLine3' Line1 Line2 Line3$ _
$ # actual remote test $ { sleep 1; printf 'Line1\r\nLine2\r\nLine3'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null Line1 Line2 _
Отсутствующий
Line3
и застопорившаяся команда неудивительны; эти пустые строки могут быть. Они появляются потому, что драйвер терминала на удаленной стороне преобразует каждый в\n
. Вместо\r\n
наши видят\n\n
, отсюда и дополнительные строки.Мы можем отключить эту функцию.
Окончания строк Windows, без перевода.
$ { sleep 1; printf 'Line1\r\nLine2\r\nLine3'; } | ssh -tt "$remote" 'stty -echo -icrnl; cat' 2>/dev/null Line1 Line2 _
На этот раз каждый персонаж получился как . Также есть настройка, которая заставляет драйвер терминала игнорировать файлы . Есть много настроек; некоторые влияют на ввод, некоторые — на вывод. См. вывод (локальный)
stty -a
, читатьman 1 stty
. Вы обнаружите (помимо прочего), что это не единственный режим для терминала, и многие функции можно отключить.raw
режим.$ { sleep 1; printf 'Windows\r\nSIGINT\n\003Backspace\010\nEOF\004\004\nExtra\n'; } | ssh -tt "$remote" 'stty raw -echo; cat' 2>/dev/null Windows SIGINT Backspace EOF Extra _
отключает многие функции . В приведенном выше примере ни
\r
не был переведен ни\003
вызванныйSIGINT
, ни\010
удалил предыдущий символ. Но тоже не получилось и заглохло. В выводе локального терминала мы не видим непечатаемых символов, но все символы перешли туда и обратно как есть. Вы можете подтвердить это, перейдя кod -c
(хотя из-за буферизации может случиться так, что вы увидите частичный результат).Приятно это знать, но, конечно, этот пример страдает от исходной проблемы: зависания и зависания.
также отключает функции, влияющие на поток, идущий с удаленной стороны на локальную. Локальный сервер печатает этот поток на свой стандартный вывод.
Вытягивание файла. Файл примера взят с удаленной стороны. Обратите внимание, что это может быть другой файл (в другой версии или что-то еще), поэтому вы можете получить разные контрольные суммы.
$ # checksum on the remote, for comparison $ ssh -tt "$remote" 'md5sum </bin/bash' 2>/dev/null 4600132e6a7ae0d451566943a9e79736 - $ _
$ # pulling the file and calculating checksum locally $ ssh -tt "$remote" 'stty raw; cat /bin/bash' 2>/dev/null | md5sum 4600132e6a7ae0d451566943a9e79736 - $ _
$ # without stty raw, for comparison $ ssh -tt "$remote" 'cat /bin/bash' 2>/dev/null | md5sum 358991d6d2182cbea143297d14d98f41 - $ _
Первые две команды показывают, что копия, появившаяся на локальной стороне, была ( почти наверняка ) идентична удаленному исходному файлу. Последняя команда показывает, что она имеет решающее значение, без нее локальная копия была бы другой (т.е. драйвер удаленного терминала исказил файл).
В каждом случае ни один инструмент не считывал данные с удаленного терминала, поэтому отсутствие состояния EOF не было проблемой.
Кажется, если клиенту нужно, использование может быть хорошим методом копирования произвольного файла с SSH-сервера на клиент. Я действительно подтвердил это также с помощью капли, созданной из
/dev/urandom
, размером 1 ГиБ. Некоторые наблюдения:- В этом случае нет соответствующих данных, которые могли бы попасть на удаленный терминал до его настройки. Все соответствующие данные начинаются после того, как выполнит свою работу. Нет необходимости в местном или около того.
- Если
stty
,cat
или удаленная оболочка (или что-либо, что вы добавили в удаленную команду) напечатала в свой stderr, то то, что она напечатала, повредит копию. То же самое произойдет, если какой-либо удаленный инструмент напечатает файл . Вы можете перенаправить stderr на удаленной стороне, но запретив доступ к/dev/tty
это не так просто.
Некоторые идеи
Если тебе надоssh -t
Кажется, в некоторых обстоятельствах (без дополнительной печати на удаленном терминале) вы можете вполне надежно извлечь произвольный удаленный файл и ожидать автоматического выхода из локального файла. Вам просто нужно на удаленной стороне.
Поместить файл таким образом, чтобы обеспечить локальный выход, проблематично. Вы не можете сделать это с помощью , так как для работы вам нужен механизм Ctrl+ . dЕсли вам удастся (а я не уверен, что это в полной мере возможно) настроить драйвер удаленного терминала типаstty raw
во всех аспектах, кроме Ctrl+d, поэтому захват Ctrl+d— это единственное, что он делает с входным потоком, тогда вы сможете поместить любой файл, который не содержит\004
байт. Отложить\004\004
и это должно в конечном итоге привести к выходу.
Более,stty eof
позволяет вам выбрать байт для этой цели. Инструмент понимает однобайтовый аргумент или обозначение каретки . Последний позволяет вам просто использовать, напримерstty eof ^A
(где буквально^
и это буквальноA
) и выбери\001
как байт.
Учитывая определение текстового файла в POSIX , лучше всего было бы выбрать\0
(нулевой байт) и иметь возможность поместить любой текстовый файл. К сожалению, нельзя использовать нулевой байт в аргументах командной строки . В моих тестахstty eof ^@
не сработало.
Тем не менее, если вы найдете еще один байт, которого нет в вашем файле, вы можете добиться успеха. Однако в общем случае произвольный файл может содержать каждый возможный байт хотя бы один раз. Кодирование Base64 (или подобное), выполняемое «на лету» в источнике и декодирование в пункте назначения, должно быть надежным решением, работающим сicanon
режим драйвера удаленного терминала. Пример:
$ md5sum </bin/bash
11227b11f565de042c48654a241e9d1c -
$ { sleep 1; base64 /bin/bash; printf '\004\004'; } | ssh -tt "$remote" 'stty -echo; base64 -d | md5sum' 2>/dev/null
11227b11f565de042c48654a241e9d1c -
$ _
(Контрольная сумма отличается от4600…
рассчитано ранее, потому что мой локальный отличается от моего удаленного/bin/bash
.)
Обратите внимание, что если вы хотите сохранить файл на стороне сервера, в этом нет необходимости (что в любом случае не является идеальным решением проблемы, которую он должен решить). А на самом деле вам это и не нужно. Разрешение драйверу удаленного терминала возвращать данные обратно (частично или полностью) — это проблема только с точки зрения напрасных циклов ЦП и пропускной способности, но ничего не нарушает (локально вы можете>/dev/null
). В приведенном выше примере я использовалsleep 1
иstty -echo
только потому, что я хотел увидеть вывод с пультаmd5sum
один.
Другой подход
На самом деле я решил более общую проблему. Смотрите мой вопрос, на который я ответил сам:
ssh
с отдельными stdin, stdout, stderr И tty .