questlab/www/analytics/core/CliMulti.php

280 lines
8.2 KiB
PHP

<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik;
use Piwik\CliMulti\Output;
use Piwik\CliMulti\Process;
/**
* Class CliMulti.
*/
class CliMulti {
/**
* If set to true or false it will overwrite whether async is supported or not.
*
* @var null|bool
*/
public $supportsAsync = null;
/**
* @var \Piwik\CliMulti\Process[]
*/
private $processes = array();
/**
* @var \Piwik\CliMulti\Output[]
*/
private $outputs = array();
private $acceptInvalidSSLCertificate = false;
public function __construct()
{
$this->supportsAsync = $this->supportsAsync();
}
/**
* It will request all given URLs in parallel (async) using the CLI and wait until all requests are finished.
* If multi cli is not supported (eg windows) it will initiate an HTTP request instead (not async).
*
* @param string[] $piwikUrls An array of urls, for instance:
* array('http://www.example.com/piwik?module=API...')
* @return array The response of each URL in the same order as the URLs. The array can contain null values in case
* there was a problem with a request, for instance if the process died unexpected.
*/
public function request(array $piwikUrls)
{
$this->start($piwikUrls);
do {
usleep(100000); // 100 * 1000 = 100ms
} while (!$this->hasFinished());
$results = $this->getResponse($piwikUrls);
$this->cleanup();
self::cleanupNotRemovedFiles();
return $results;
}
/**
* Ok, this sounds weird. Why should we care about ssl certificates when we are in CLI mode? It is needed for
* our simple fallback mode for Windows where we initiate HTTP requests instead of CLI.
* @param $acceptInvalidSSLCertificate
*/
public function setAcceptInvalidSSLCertificate($acceptInvalidSSLCertificate)
{
$this->acceptInvalidSSLCertificate = $acceptInvalidSSLCertificate;
}
private function start($piwikUrls)
{
foreach ($piwikUrls as $index => $url) {
$cmdId = $this->generateCommandId($url) . $index;
$output = new Output($cmdId);
if ($this->supportsAsync) {
$this->executeAsyncCli($url, $output, $cmdId);
} else {
$this->executeNotAsyncHttp($url, $output);
}
$this->outputs[] = $output;
}
}
private function buildCommand($hostname, $query, $outputFile)
{
$bin = $this->findPhpBinary();
return sprintf('%s -q %s/console climulti:request --piwik-domain=%s %s > %s 2>&1 &',
$bin, PIWIK_INCLUDE_PATH, escapeshellarg($hostname), escapeshellarg($query), $outputFile);
}
private function getResponse()
{
$response = array();
foreach ($this->outputs as $output) {
$response[] = $output->get();
}
return $response;
}
private function hasFinished()
{
foreach ($this->processes as $index => $process) {
$hasStarted = $process->hasStarted();
if (!$hasStarted && 8 <= $process->getSecondsSinceCreation()) {
// if process was created more than 8 seconds ago but still not started there must be something wrong.
// ==> declare the process as finished
$process->finishProcess();
continue;
} elseif (!$hasStarted) {
return false;
}
if ($process->isRunning()) {
return false;
}
if ($process->hasFinished()) {
// prevent from checking this process over and over again
unset($this->processes[$index]);
}
}
return true;
}
private function generateCommandId($command)
{
return substr(Common::hash($command . microtime(true) . rand(0, 99999)), 0, 100);
}
/**
* What is missing under windows? Detection whether a process is still running in Process::isProcessStillRunning
* and how to send a process into background in start()
*/
private function supportsAsync()
{
return !SettingsServer::isWindows()
&& Process::isSupported()
&& $this->findPhpBinary();
}
private function cleanup()
{
foreach ($this->processes as $pid) {
$pid->finishProcess();
}
foreach ($this->outputs as $output) {
$output->destroy();
}
$this->processes = array();
$this->outputs = array();
}
/**
* Remove files older than one week. They should be cleaned up automatically after each request but for whatever
* reason there can be always some files left.
*/
public static function cleanupNotRemovedFiles()
{
$timeOneWeekAgo = strtotime('-1 week');
$files = _glob(self::getTmpPath() . '/*');
if(empty($files)) {
return;
}
foreach ($files as $file) {
$timeLastModified = filemtime($file);
if ($timeOneWeekAgo > $timeLastModified) {
unlink($file);
}
}
}
public static function getTmpPath()
{
$dir = PIWIK_INCLUDE_PATH . '/tmp/climulti';
return SettingsPiwik::rewriteTmpPathWithHostname($dir);
}
private function findPhpBinary()
{
if (defined('PHP_BINARY') && false === strpos(PHP_BINARY, 'fpm')) {
return PHP_BINARY;
}
$bin = '';
if (!empty($_SERVER['_']) && Common::isPhpCliMode()) {
$bin = $this->getPhpCommandIfValid($_SERVER['_']);
}
if (empty($bin) && !empty($_SERVER['argv'][0]) && Common::isPhpCliMode()) {
$bin = $this->getPhpCommandIfValid($_SERVER['argv'][0]);
}
if (empty($bin)) {
$bin = shell_exec('which php');
}
if (empty($bin)) {
$bin = shell_exec('which php5');
}
if (!empty($bin)) {
return trim($bin);
}
}
private function executeAsyncCli($url, Output $output, $cmdId)
{
$this->processes[] = new Process($cmdId);
$url = $this->appendTestmodeParamToUrlIfNeeded($url);
$query = UrlHelper::getQueryFromUrl($url, array('pid' => $cmdId));
$hostname = UrlHelper::getHostFromUrl($url);
$command = $this->buildCommand($hostname, $query, $output->getPathToFile());
Log::debug($command);
shell_exec($command);
}
private function executeNotAsyncHttp($url, Output $output)
{
try {
Log::debug("Execute HTTP API request: " . $url);
$response = Http::sendHttpRequestBy('curl', $url, $timeout = 0, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = false, $this->acceptInvalidSSLCertificate);
$output->write($response);
} catch (\Exception $e) {
$message = "Got invalid response from API request: $url. ";
if (empty($response)) {
$message .= "The response was empty. This usually means a server error. This solution to this error is generally to increase the value of 'memory_limit' in your php.ini file. Please check your Web server Error Log file for more details.";
} else {
$message .= "Response was '" . $e->getMessage() . "'";
}
$output->write($message);
}
}
private function appendTestmodeParamToUrlIfNeeded($url)
{
$isTestMode = $url && false !== strpos($url, 'tests/PHPUnit/proxy');
if ($isTestMode && false === strpos($url, '?')) {
$url .= "?testmode=1";
} elseif ($isTestMode) {
$url .= "&testmode=1";
}
return $url;
}
private function getPhpCommandIfValid($path)
{
if (!empty($path) && is_executable($path)) {
if (0 === strpos($path, PHP_BINDIR) && false === strpos($path, 'phpunit')) {
return $path;
}
}
}
}