Запуск дочернего процесса и чтение его вывода, linux

Категория: Решение задач | Скилл: C++ , linux | Дата: 18.10.2020
Задача: программно запускать безголовый браузер, и получать из него данные. Все просто, за исключением некоторых мелочей. Сделать это все надо в linux.

С процессами под linux столкнулся впервые ...

Для тех кому интересны детали задачи:

  • безголовый браузер нужен потому что нужно выдергивать всю страницу целиком, даже в тех случаях когда ее содержимое составляется на javascript, к тому же была необходимость обходить блокировку со стороны cloudflare
  • в качестве безголового браузера был выбрать phantomjs, который в общем справляется со своей работой, но слегка устаревший + дебаг стремный, но пока нужен именно он (тянуть всякие chrome очень не хочется)
  • безголовый браузер может не отвечать несмотря на истекший таймаут, поэтому контроль времени работы должен быть на хост программе

Дочерний процесс

Можно воспользоваться простой функцией popen, но не смог найти как в этом случае узнать состояние процесса, а ждать окончания таймаута нерационально. Кстати интернеты подсказали что popen внутри использует fork.

Функция fork создает "разветвление программы" позволяя в текущей программе параллельно запустить дочерний процесс:

int iPid = fork();
if(iPid == 0)
{
  //код для дочернего процесса
  ...
  exit(0);
}
else
{
  //код для родительского процесса, отслеживание состояния дочернего, обработка вывода и прочее
}

Чтобы в дочернем процесс запустить внешнюю программу из командной оболочки можно воспользоваться функцией system.

Вывод процесса

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

int aPipe[2];
pipe(aPipe);

Теперь этот канал можно использовать для межпроцессного общения, в моем случае родительскому процессу необходимо считать вывод дочернего, для этого:

int aPipe[2];
pipe(aPipe);

pid = fork();
if (pid == 0)
{
	close(aPipe[0]); //закрытие дескриптор чтения
	dup2(aPipe[1], 1); //направялем вывод дочернего процесса в stdout
	...
	exit(0);
}
else
{
	close(aPipe[1]); //закрываем дескриптор записи

	//считываем из дескриптора чтения
	std::string sOut;
	char ch;
	while(read(aPipe[0], &ch, 1))
		sOut += ch;

	close(aPipe[0]); //закрываем дескриптор чтения
}

Кстати, посимвольное чтение из дескриптора чтения сделал из-за того что пачки символов считывались некорректно, в чем дело не разобрался.

Свой таймаут

Как таковой таймаут не нужен, он использован лишь потому что на данный момент мной не найдена нормальная альтернатива phantomjs, который имеет свойство в очень редких случаях тихо висеть. Однако здесь используется механизм состояния процесса, что интересно :)

В родительском процессе, для контроля таймаута необходимо использовать состояние процесса, потому что бессмысленно ждать весь таймаут (процесс может закончить работу раньше). Это может сделать функция waitpid c WNOHANG:

int iStatus = 0;
int iResWait = waitpid(iPid, &iStatus, WNOHANG);

if(iResWait == iPid && WIFEXITED(iStatus))
{
  //процесс завершился
}

Итоговая функция

bool RunHeadlessBrowser(const char *sURL, uint32_t uiTimeout, std::string &sOut)
{
	const uint32_t uiMinTimeOut = 5;
	if(uiTimeout < uiMinTimeOut)
		uiTimeout = uiMinTimeOut;

	std::string sCmd = "phantomjs --ssl-protocol=any --ignore-ssl-errors=true get-page.js ";
	sCmd += sURL;
	int iPid, iStatus, iResWait;

	int aPipe[2];
	pipe(aPipe);

	iPid = fork();
	if (iPid == 0)
	{
		close(aPipe[0]); //закрытие дескриптора чтения
		dup2(aPipe[1], 1); //направялем вывод дочернего процесса в stdout
		system(sCmd.c_str());
		exit(0);
	}
	else
	{
		uint32_t uiPastTime=0;
		while (true)
		{
			++uiPastTime;
			iResWait = waitpid(iPid, &iStatus, WNOHANG);
			if(iResWait == iPid && WIFEXITED(iStatus))
				break;

			if(uiTimeout >= uiPastTime)
			{
				kill(iPid, 0);
				break;
			}

			sleep(1);
		}

		close(aPipe[1]); //закрываем дескриптор записи

		//считываем из дескриптора чтения
		char ch;
		while(read(aPipe[0], &ch, 1))
			sOut += ch;

		close(aPipe[0]); //закрываем дескриптор чтения

		return true;
	}
}
Я Виталий, ник в сети Byurrer.
Увлекаюсь программированием, веду интересные проекты, пишу здесь об интересующих меня вещах: о работе, проектах, увлечениях и проффесиональном развитии.
Мое резюме

Проекты
SkyXEngine, PHP-API, S4G
Категории
В разработке :)
Популярное
В разработке :)