1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\CliMulti;
use Piwik\CliMulti;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Filesystem;
use Piwik\SettingsServer;
/**
* There are three different states
* - PID file exists with empty content: Process is created but not started
* - PID file exists with the actual process PID as content: Process is running
* - PID file does not exist: Process is marked as finished
*
* Class Process
*/
class Process
{
public const PS_COMMAND = 'ps wwx';
public const AWK_COMMAND = 'awk \'! /defunct/ {print $1}\'';
private $finished = null;
private $pidFile = '';
private $timeCreation = null;
private static $isSupported = null;
private $pid = null;
private $started = null;
public function __construct($pid)
{
if (!Filesystem::isValidFilename($pid)) {
throw new \Exception('The given pid has an invalid format');
}
$pidDir = CliMulti::getTmpPath();
Filesystem::mkdir($pidDir);
$this->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(Common::getProcessId());
}
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';
} elseif (!$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;
}
}
|