Процесс, запущенный в выделенном 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 …

Вы можете прочитать больше здесь:

В случаеssh -tt …Перехват символов — это не все, что делает драйвер терминала на удаленной стороне. Он делает гораздо больше, как с данными, поступающими от клиента, так и с данными, поступающими к клиенту (напечатанными удаленной командой). Давайте исследуем некоторые возможности, препятствия и особенности.


Немного практики

Предварительные примечания:

  • Во всех примерах используетсяssh -tt. Предполагается, что у вас есть причины и-ttявляется обязательным.

  • Если вы хотите повторить приведенные ниже примеры, установитеremote='user@server'со значениямиuserиserverэто работает для вас. Цель состоит в том, чтобы позволить вам копировать и вставлять мои команды без каких-либо усилий.

  • Однако примеры не используются. Я не очень понимаю, зачем вам этот вопрос. Если окажется, что вам нужноevalчтобы примеры работали, вам придется повозиться. Извини. Примеры мне подходят.

  • Примеры показывают, что это работает без запроса пароля и работает достаточно быстро.

  • $ обозначает мою подсказку.#обозначает комментарий. Строка с командой всегда имеет вид приглашения, за которым следует команда. Затем я получил результат (если таковой имеется). Дополнительный запрос в конце последней строки указывает на завершение команды; отсутствие запроса в конце последней строки указывает на то, что команда остановлена._указывает положение локальной каретки (текстового курсора) после завершения или остановки команды.

  • Мы хотим исследовать поведение удаленного терминала. Также есть местный терминал. Он не мешает вводу (наши команды не будут использовать его в качестве ввода), но может мешать выводу. Чтобы отложить локальный терминал, мы можем локально записать в обычный файл или канал (например) . Я решил, что это только запутает примеры. Существование местного терминала не влияет на выводы, к которым мы придем.

  • Я часто использую описания нажатий клавиш (например, Enter) для обозначения отправляемых нами символов (например, ). Это не всегда будет абсолютно точным (например, вы, Enterвероятно, генерирует , а не ). Я думаю, что нажатия клавиш более читабельны, и интерпретирую их скорее как указание на то, что мы хотим, чтобы драйвер удаленного терминала делал.

Вот примеры:

  1. Давайте повторим вашу проблему:

            $ printf 'Line1\nLine2' | ssh -tt "$remote" cat
    Line1
    Line2Line1
    _
    

    Почему там дважды? Потому что удаленный терминал повторяет его ввод. Давайте что-нибудь с этим сделаем.

  2. Попробуем подавить удаленное эхо:

            $ printf 'Line1\nLine2' | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2Line1
    _
    

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

  3. Дадим немного времени:

            $ { sleep 1; printf 'Line1\nLine2'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    _
    

    Теперь мы видимLine1прошел, но, вероятно, нет.

    Примечание — это уже хак. Если сервер был занят или связь с сервером была медленной, этого может быть недостаточно.

  4. Давай "ударим" EnterпослеLine2:

            $ { sleep 1; printf 'Line1\nLine2\n'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2
    _
    
  5. Давайте отправим Ctrl+dвместо этого Enter:

            $ { sleep 1; printf 'Line1\nLine2\004'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2_
    

    Обратите внимание, что каретка находится не в том же месте, что и раньше.

  6. Пошлем 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.

  7. Давайте отправим что-нибудь еще:

            $ { sleep 1; printf 'Line1\nLine2\004\004 Line2 continues'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null
    Line1
    Line2$ _
    

    Двойной Ctrl+dдействительно заставил нас увидеть EOF, но не прочитал остальную часть ввода.

  8. Есть больше символов, которые взаимодействуют с драйвером удаленного терминала. (восьмеричное 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же причине последнее локальное приглашение находится в отдельной строке, а не сразу после точки.

  9. Мы можем отправить 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на входе действительно вызывает сигнал. Мы увидим это яснее, если нам удастся установить ловушку в удаленной оболочке до наших локальных проходов.

  10. Треппинг Ctrl+ c:

            $ { sleep 1; printf '\03'; } | ssh -tt "$remote" 'stty -echo; trap "echo trap" INT; sleep 2' 2>/dev/null
    trap
    $ _
    

    Как видите, определенные символы во входных данных вызывают определенные изменения или действия на удаленной стороне. На данный момент мы протестировали некоторые символы, которые редко встречаются в текстовых файлах. Вы можете подумать, что если вашlocal.txtсодержит то, что люди называют текстом (буквы, цифры, символы, символы новой строки), и это ASCII, тогда вы в безопасности. Ну, не строго; см. следующий пример.

  11. Окончания строк 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, отсюда и дополнительные строки.

    Мы можем отключить эту функцию.

  12. Окончания строк 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. Вы обнаружите (помимо прочего), что это не единственный режим для терминала, и многие функции можно отключить.

  13. 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(хотя из-за буферизации может случиться так, что вы увидите частичный результат).

    Приятно это знать, но, конечно, этот пример страдает от исходной проблемы: зависания и зависания.

    также отключает функции, влияющие на поток, идущий с удаленной стороны на локальную. Локальный сервер печатает этот поток на свой стандартный вывод.

  14. Вытягивание файла. Файл примера взят с удаленной стороны. Обратите внимание, что это может быть другой файл (в другой версии или что-то еще), поэтому вы можете получить разные контрольные суммы.

            $ # 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 .

Другие вопросы по тегам