У меня есть программа на 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?
Кстати: это немного похоже на то, что делают screen
или tmux
, поэтому было бы полезно изучить их работу.
@UlrichEckhardt Я добавил небольшой код, чтобы показать свое намерение. Да, я бы хотел, чтобы он работал как экран, но я не могу использовать ssh из своей программы, и мне придется полагаться на программу запуска, предоставленную LSF.
Можете попробовать просто system("bsub -Ip python")
? Это подключит целевую программу к стандартному выводу/стандартному вводу и заблокирует вызывающую программу до завершения процесса. Вы также можете попробовать аргументы -u и/или -i для Python, чтобы принудительно обеспечить небуферизованный вывод и интерактивный режим соответственно, в случае, если Python не автоматически определяет что-то правильно.
@nneonneo. Да, с помощью system() я получаю поток на свой стандартный вывод. Спасибо! Я тоже должен был попробовать систему. Не могли бы вы подробнее рассказать, почему система может это сделать, но не может открыть?
@bdd Не используйте system()
. Невозможно разделить потоки stdout и stderr, невозможно также контролировать интерпретатор, используемый для запуска программы, вы также не можете контролировать среду программы. system()
— это еще и огромный кошмар безопасности. Его следует объявить устаревшим и удалить — не используйте его.
@JesperJuhl Да, но вывод system() (когда все идет хорошо) — это именно то, что я хочу. Надеюсь найти такое же простое решение, как system(), но без проблем.
Я сделал нечто подобное несколько лет назад. Он был основан на библиотеке Linux-epoll для обработки асинхронных событий (здесь: 3 канала для stdin
, stdout
и stderr
), но должна быть возможность выполнять ввод-вывод другим способом.
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. Знаете ли вы о подобной ситуации?
да, используйте ssh для запуска и перенаправления на удаленный компьютер при выполнении exec()
. ssh также перенаправляет stdin, stdout и stderr, однако stderr может работать по-другому. Вместо каналов на одной машине вы можете попробовать использовать TCP-сокеты между двумя машинами, но настройка, конечно, будет другой.
Однако проблема может заключаться в неинтерактивном поведении bsub
.
Многие программы определяют, подключен ли стандартный вывод к 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 вызывающей программы, нам нужно сделать это программно
popen
подключает stdio подпроцесса к каналу, тогда как другие методы подключают stdio подпроцесса непосредственно к вашему терминалу (tty). Программы могут определить, подключено ли их stdio к tty или нет (например, с помощью isatty
), и могут соответствующим образом изменить свое поведение. Чаще всего это касается интерактивных программ. Некоторые программы обходят эту проблему, используя псевдо-терминал (pty), который похож на канал, но может действовать как терминал; popen этого не делает.
Это базовое перенаправление ввода-вывода, которое действительно возможно во многих языках и во многих операционных системах. С чем именно у вас проблемы? Можете ли вы показать проблемный код? Конечно, сначала сведите его к минимально воспроизводимому примеру.