Как передать интерактивную оболочку удаленной программы в стандартный вывод работающей программы на C++, которая запустила удаленную программу (с использованием BSUB -I)

У меня есть программа на C++ (скажем, процесс P1), которая в ходе своего выполнения порождает новый процесс P2 на удаленной машине с помощью средства запуска, такого как LSF. P2 имеет интерактивную оболочку (может быть Python). Я хочу, чтобы пользователь на P1 некоторое время использовал эту интерактивную оболочку P2, а затем вышел из P2, когда закончит. P1 продолжается отсюда и в будущем может порождать другие интерактивные оболочки. При этом P1 либо продолжал работать в фоновом режиме, либо был заблокирован (на данный момент не имеет значения). Необходимо, чтобы локальная программа, такая как P1, запускала только P2, поскольку она может запускать другие процессы в зависимости от определенных условий. Кроме того, P1 может перезапустить P2 в случае сбоя P2. Все процессы выполняются в среде Linux.
Запуск bsub -Ip P2 с помощью popen не передает оболочку P2 на стандартный вывод P1. Это просто показывает, что программа была запущена на определенной машине.
Если потоковая передача невозможна, существует ли альтернативный способ справиться с таким сценарием.

В оболочке Linux, чтобы запустить интерактивную оболочку Python на удаленном компьютере с помощью bsub, я использую следующее:

$ bsub -Ip python
Job <625381> is submitted to default queue <interq>.
<<Waiting for dispatch ...>>
<<Starting on 1i-10-144>>
Python 3.12.0 (main, Nov 26 2023, 21:52:55) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Hi")
Hi
>>> 

Я хочу получить аналогичный интерфейс из своей программы на C++. Вот простая программа, показывающая, что я хочу сделать.

//  Program for P1
#include <iostream>

int main()
{
   /* do P1 work */

   char buffer[128];
   FILE* pipe = popen("bsub -Ip python", "r");   // P2 is python here.
   if (!pipe) {
      return 0;
   }   
   std::string result = ""; 
   while (!feof(pipe)) {
      if (fgets(buffer, 128, pipe) != NULL)
         result += buffer;
   }   
   pclose(pipe);

   /* continue P1 work */ 
  return 0;
}

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

$ ./a.out 
<<Waiting for dispatch ...>>
<<Starting on 1i-38-204>>

Поскольку bsub запустил программу в интерактивном режиме на удаленной машине, выходные данные уже должны быть переданы на мою машину. Но как получить доступ к этому потоку через мою программу и перенаправить его на стандартный вывод моей программы P1?

Это базовое перенаправление ввода-вывода, которое действительно возможно во многих языках и во многих операционных системах. С чем именно у вас проблемы? Можете ли вы показать проблемный код? Конечно, сначала сведите его к минимально воспроизводимому примеру.

Ulrich Eckhardt 27.06.2024 12:29

Кстати: это немного похоже на то, что делают screen или tmux, поэтому было бы полезно изучить их работу.

Ulrich Eckhardt 27.06.2024 12:33

@UlrichEckhardt Я добавил небольшой код, чтобы показать свое намерение. Да, я бы хотел, чтобы он работал как экран, но я не могу использовать ssh из своей программы, и мне придется полагаться на программу запуска, предоставленную LSF.

bdd 27.06.2024 13:18

Можете попробовать просто system("bsub -Ip python")? Это подключит целевую программу к стандартному выводу/стандартному вводу и заблокирует вызывающую программу до завершения процесса. Вы также можете попробовать аргументы -u и/или -i для Python, чтобы принудительно обеспечить небуферизованный вывод и интерактивный режим соответственно, в случае, если Python не автоматически определяет что-то правильно.

nneonneo 27.06.2024 13:24

@nneonneo. Да, с помощью system() я получаю поток на свой стандартный вывод. Спасибо! Я тоже должен был попробовать систему. Не могли бы вы подробнее рассказать, почему система может это сделать, но не может открыть?

bdd 27.06.2024 14:24

@bdd Не используйте system(). Невозможно разделить потоки stdout и stderr, невозможно также контролировать интерпретатор, используемый для запуска программы, вы также не можете контролировать среду программы. system() — это еще и огромный кошмар безопасности. Его следует объявить устаревшим и удалить — не используйте его.

Jesper Juhl 27.06.2024 18:06

@JesperJuhl Да, но вывод system() (когда все идет хорошо) — это именно то, что я хочу. Надеюсь найти такое же простое решение, как system(), но без проблем.

bdd 27.06.2024 19:17
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
7
65
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я сделал нечто подобное несколько лет назад. Он был основан на библиотеке Linux-epoll для обработки асинхронных событий (здесь: 3 канала для stdin, stdout и stderr), но должна быть возможность выполнять ввод-вывод другим способом.

  • откройте в программе C++ P1 3 канала с pipe2() для stdin, stdout, stderr.
  • выполните fork() для создания дочернего процесса (позже использованного в exec вашей программе Python P2)
  • на вашем родителе close() один конец каналов (канал stdin является выходом на P1, поэтому закройте вход здесь, чтобы stdout и stderr закрыть выход)
  • на родителе, обращайся с этими трубками правильно, я сделал это с помощью epoll.
  • на вашем дочернем процессе, close() другие концы труб. Теперь у вас есть напрямую соединенные каналы между двумя процессами.
  • на вашем ребенке используйте dup2(), чтобы перенести каналы на дескрипторы файлов для stdin (STDIN_FILENO), stdout (STDOUT_FILENO), stderr (STDERR_FILENO) и close() исходных дескрипторов.
  • на вашем дочернем объекте используйте предпочтительный вариант exec() для вызова вашей программы Python P2, она унаследует stdin, stdout и stderr от дочернего процесса. Идентификатор дочернего процесса останется прежним, поэтому родитель может проверить и перезапустить дочерний процесс. Если вы используете удаленно запускаемое приложение (например, с помощью ssh), у вас, конечно, будет только pid процесса ssh.

Вы можете найти аналогичный ответ (без каналов, но с примером кода) здесь:

Как в C перенаправить stdin/stdout/stderr в файлы при выполнении execvp() или аналогичного вызова?

В моем случае P1 и P2 — это процессы, работающие на разных машинах. Я не могу использовать каналы для связи между ними. Я не могу отделить P2 от P1. Знаете ли вы о подобной ситуации?

bdd 27.06.2024 12:07

да, используйте ssh для запуска и перенаправления на удаленный компьютер при выполнении exec(). ssh также перенаправляет stdin, stdout и stderr, однако stderr может работать по-другому. Вместо каналов на одной машине вы можете попробовать использовать TCP-сокеты между двумя машинами, но настройка, конечно, будет другой.

S. Gleissner 27.06.2024 12:44

Однако проблема может заключаться в неинтерактивном поведении bsub.

S. Gleissner 27.06.2024 12:56
Ответ принят как подходящий

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

В простых случаях вы можете использовать вызов библиотеки system. Это вызовет целевую программу с помощью оболочки (а-ля /bin/sh -c "system argument"), передаст ее stdin, stdout и stderr на ваш терминал и будет ждать завершения процесса, возвращая его код выхода. В вашем примере это будет system("bsub -Ip python");

Обратите внимание, что system имеет некоторые оговорки:

  • Никогда не передавайте ненадежный пользовательский ввод в аргумент system (или popen), поскольку вводимые данные напрямую используются как команда оболочки, и любая неспособность тщательно экранировать вводимые данные может привести к выполнению произвольного кода и компрометации.
  • Никогда не запускайте программы в ненадежной среде; и system, и popen неявно используют $PATH для поиска программ (поскольку они передают входную строку оболочке), а ненадежная переменная PATH может привести к компрометации.

В более сложных случаях в Linux обычно отвечают fork/exec/wait. Вы вызываете fork, чтобы создать подпроцесс, затем подпроцесс выполняет любую необходимую настройку (например, перенаправление stdin/stdout/stderr, настройку сигналов и т. д.) перед вызовом одной из функций exec* (например, execve для управления как командной строкой, так и средой). Родительский процесс использует wait для ожидания завершения подпроцесса.

Преимущество этого метода заключается в том, что это наиболее гибкий способ запуска программы, поскольку, по сути, это то же, что другие функции (popen, system и т. д.) делают «под капотом», и это позволяет вам полностью контролировать все детали — например, действительно ждать подпроцесса или нет, перенаправлять ли стандартный вывод/stderr/stdin, какой должна быть среда процесса и т. д.

Вот простой пример, который просто передает среду из родительского процесса:

#include <unistd.h>
#include <stdlib.h>

int run() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    } else if (pid > 0) {
        /* parent - wait for child process to exit */
        int status;
        waitpid(pid, &status, 0);
        // can check status to see if child exited cleanly, etc.
        return status;
    } else {
        /* child */
        // can now use dup2() to redirect stdin/stdout etc.
        // note that execlp uses PATH to search for the binary;
        // for safety you probably want execl() with the full path to your program
        // first argument is program to run; subsequent arguments are argv[]
        execlp("bsub", "bsub", "-Ip", "python", NULL);
        // exec should not return; if it does, that means it could not run the program
        perror("execl");
        _exit(1);
    }
}

не могли бы вы объяснить, почему это не работает для popen? В чем разница в том, как стандартный ввод-вывод обрабатывается execve и popen для этой разницы? Или я неправильно использую popen? В интерактивном режиме чтение/запись в io (двунаправленная потоковая передача) происходит динамически. popen является однонаправленным, и это должно быть причиной? также кажется, что поток popen не связан напрямую с std io вызывающей программы, нам нужно сделать это программно

bdd 28.06.2024 07:30
popen подключает stdio подпроцесса к каналу, тогда как другие методы подключают stdio подпроцесса непосредственно к вашему терминалу (tty). Программы могут определить, подключено ли их stdio к tty или нет (например, с помощью isatty), и могут соответствующим образом изменить свое поведение. Чаще всего это касается интерактивных программ. Некоторые программы обходят эту проблему, используя псевдо-терминал (pty), который похож на канал, но может действовать как терминал; popen этого не делает.
nneonneo 28.06.2024 07:53

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