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

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

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

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

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

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

Можно воспользоваться простой функцией 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;
	}
}

Вариант проще

После публикации поступил комментарий:

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

Здесь сказано что такая ситуация невозможна, так как дочерний процесс станет "зомби" и ядро все еще будет хранить о нем минимальную информацию пока не будет вызывана wait функция:

A child that terminates, but has not been waited for becomes a "zombie". The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child.

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

В начале команды для терминала введем (установка таймаута на 5 секунд): timeout 5s

Из описания timeout видно что по истечению таймаута процесс будет завершен с кодом 124, однако system вернет 1, поэтому дочерний процесс должен учесть этот момент:

int iRet = system(sCmd.c_str());
exit((iRet == 0 ? 0 : 1));

А родительский процесс должен отреагировать на ошибочное завершение дочернего процесса (в данном случае все просто: если дочерний процесс завершился с ошибкой то вернуть false):

iResWait = waitpid(iPid, &iStatus, 0);
if(WIFEXITED(iStatus) != 0 && WEXITSTATUS(iStatus) != 0)
	return false;
Теперь можно выкинуть самописный таумаут :)

Но так как теперь нам в общем-то не надо контролировать время выполнения процесса, то все можно уложить в такую простую функцию:

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

	std::string sCmd = "/crawler/phantomjs --ssl-protocol=any --ignore-ssl-errors=true /crawler/get-page.js ";
	std::stringstream ssCmd;
	ssCmd << "timeout " << uiTimeout << "s " << "/crawler/phantomjs --ssl-protocol=any --ignore-ssl-errors=true /crawler/get-page.js " << sURL;
	sCmd = ssCmd.str();

	FILE *pP = popen(sCmd.c_str(), "r");
	char ch;
	while(fread(&ch, 1, 1, pP))
		sOut += ch;

	return (pclose(pP) == 0);
}

Стоило изначально так сделать, но по ходу выяснения был приобретен бесценный опыт :)