pidFile = $pidDir . '/' . $pid . '.pid'; $this->timeCreation = time(); $this->pid = $pid; $this->markAsNotStarted(); } private static function isForcingAsyncProcessMode() { try { return (bool) StaticContainer::get('test.vars.forceCliMultiViaCurl'); } catch (\Exception $ex) { return false; } } public function getPid() { return $this->pid; } private function markAsNotStarted() { $content = $this->getPidFileContent(); if ($this->doesPidFileExist($content)) { return; } $this->writePidFileContent(''); } public function hasStarted($content = null) { if (!$this->started) { $this->started = $this->checkPidIfHasStarted($content); } // PID will be deleted when process has finished so we want to remember this process started at some point. Otherwise we might return false here once the process finished. // therefore we want to "cache" a successful start return $this->started; } private function checkPidIfHasStarted($content = null) { if (is_null($content)) { $content = $this->getPidFileContent(); } if (!$this->doesPidFileExist($content)) { // process is finished, this means there was a start before return true; } if ('' === trim($content)) { // pid file is overwritten by startProcess() return false; } // process is probably running or pid file was not removed return true; } public function hasFinished() { if ($this->finished) { return true; } $content = $this->getPidFileContent(); return !$this->doesPidFileExist($content); } public function getSecondsSinceCreation() { return time() - $this->timeCreation; } public function startProcess() { $this->writePidFileContent(getmypid()); } public function isRunning() { $content = $this->getPidFileContent(); if (!$this->doesPidFileExist($content)) { return false; } if (!$this->pidFileSizeIsNormal($content)) { $this->finishProcess(); return false; } if ($this->isProcessStillRunning($content)) { return true; } if ($this->hasStarted($content)) { $this->finishProcess(); } return false; } private function pidFileSizeIsNormal($content) { $size = Common::mb_strlen($content); return $size < 500; } public function finishProcess() { $this->finished = true; Filesystem::deleteFileIfExists($this->pidFile); } private function doesPidFileExist($content) { return false !== $content; } private function isProcessStillRunning($content) { if (!self::isSupported()) { return true; } $lockedPID = trim($content); $runningPIDs = self::getRunningProcesses(); return !empty($lockedPID) && in_array($lockedPID, $runningPIDs); } private function getPidFileContent() { return @file_get_contents($this->pidFile); } /** * Tests only * @internal * @param $content */ public function writePidFileContent($content) { file_put_contents($this->pidFile, $content); } public static function isSupported() { if (!isset(self::$isSupported)) { $reasons = self::isSupportedWithReason(); self::$isSupported = empty($reasons); } return self::$isSupported; } public static function isSupportedWithReason() { $reasons = []; if (defined('PIWIK_TEST_MODE') && self::isForcingAsyncProcessMode() ) { $reasons[] = 'forcing multicurl use for tests'; } if (SettingsServer::isWindows()) { $reasons[] = 'not supported on windows'; return $reasons; } if (self::isMethodDisabled('shell_exec')) { $reasons[] = 'shell_exec is disabled'; return $reasons; // shell_exec is used for almost every other check } $getMyPidDisabled = self::isMethodDisabled('getmypid'); if ($getMyPidDisabled) { $reasons[] = 'getmypid is disabled'; } if (self::isSystemNotSupported()) { $reasons[] = 'system returned by `uname -a` is not supported'; } if (!self::psExistsAndRunsCorrectly()) { $reasons[] = 'shell_exec(' . self::PS_COMMAND . '" 2> /dev/null") did not return a success code'; } else if (!$getMyPidDisabled) { $pid = @getmypid(); if (empty($pid) || !in_array($pid, self::getRunningProcesses())) { $reasons[] = 'could not find our pid (from getmypid()) in the output of `' . self::PS_COMMAND . '`'; } } if (!self::awkExistsAndRunsCorrectly()) { $reasons[] = 'awk is not available or did not run as we would expect it to'; } return $reasons; } private static function psExistsAndRunsCorrectly() { return self::returnsSuccessCode(self::PS_COMMAND . ' 2>/dev/null'); } private static function awkExistsAndRunsCorrectly() { $testResult = @shell_exec('echo " 537 s000 Ss 0:00.05 login -pfl theuser /bin/bash -c exec -la bash /bin/bash" | ' . self::AWK_COMMAND . ' 2>/dev/null'); return trim($testResult) == '537'; } private static function isSystemNotSupported() { $uname = @shell_exec('uname -a 2> /dev/null'); if (empty($uname)) { $uname = php_uname(); } if (strpos($uname, 'synology') !== false) { return true; } return false; } public static function isMethodDisabled($command) { if (!function_exists($command)) { return true; } $disabled = explode(',', ini_get('disable_functions')); $disabled = array_map('trim', $disabled); return in_array($command, $disabled) || !function_exists($command); } private static function returnsSuccessCode($command) { $exec = $command . ' > /dev/null 2>&1; echo $?'; $returnCode = @shell_exec($exec); if (false === $returnCode || null === $returnCode) { return false; } $returnCode = trim($returnCode); return 0 == (int) $returnCode; } public static function getListOfRunningProcesses() { $processes = @shell_exec(self::PS_COMMAND . ' 2>/dev/null'); if (empty($processes)) { return array(); } return explode("\n", $processes); } /** * @return int[] The ids of the currently running processes */ public static function getRunningProcesses() { $ids = explode("\n", trim(shell_exec(self::PS_COMMAND . ' 2>/dev/null | ' . self::AWK_COMMAND . ' 2>/dev/null'))); $ids = array_map('intval', $ids); $ids = array_filter($ids, function ($id) { return $id > 0; }); return $ids; } }