1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 */ 8namespace Piwik\CliMulti; 9 10use Piwik\CliMulti; 11use Piwik\Common; 12use Piwik\Container\StaticContainer; 13use Piwik\Filesystem; 14use Piwik\SettingsServer; 15 16/** 17 * There are three different states 18 * - PID file exists with empty content: Process is created but not started 19 * - PID file exists with the actual process PID as content: Process is running 20 * - PID file does not exist: Process is marked as finished 21 * 22 * Class Process 23 */ 24class Process 25{ 26 const PS_COMMAND = 'ps x'; 27 const AWK_COMMAND = 'awk \'! /defunct/ {print $1}\''; 28 29 private $finished = null; 30 private $pidFile = ''; 31 private $timeCreation = null; 32 private static $isSupported = null; 33 private $pid = null; 34 private $started = null; 35 36 public function __construct($pid) 37 { 38 if (!Filesystem::isValidFilename($pid)) { 39 throw new \Exception('The given pid has an invalid format'); 40 } 41 42 $pidDir = CliMulti::getTmpPath(); 43 Filesystem::mkdir($pidDir); 44 45 $this->pidFile = $pidDir . '/' . $pid . '.pid'; 46 $this->timeCreation = time(); 47 $this->pid = $pid; 48 49 $this->markAsNotStarted(); 50 } 51 52 private static function isForcingAsyncProcessMode() 53 { 54 try { 55 return (bool) StaticContainer::get('test.vars.forceCliMultiViaCurl'); 56 } catch (\Exception $ex) { 57 return false; 58 } 59 } 60 61 public function getPid() 62 { 63 return $this->pid; 64 } 65 66 private function markAsNotStarted() 67 { 68 $content = $this->getPidFileContent(); 69 70 if ($this->doesPidFileExist($content)) { 71 return; 72 } 73 74 $this->writePidFileContent(''); 75 } 76 77 public function hasStarted($content = null) 78 { 79 if (!$this->started) { 80 $this->started = $this->checkPidIfHasStarted($content); 81 } 82 // 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. 83 // therefore we want to "cache" a successful start 84 return $this->started; 85 } 86 87 private function checkPidIfHasStarted($content = null) 88 { 89 if (is_null($content)) { 90 $content = $this->getPidFileContent(); 91 } 92 93 if (!$this->doesPidFileExist($content)) { 94 // process is finished, this means there was a start before 95 return true; 96 } 97 98 if ('' === trim($content)) { 99 // pid file is overwritten by startProcess() 100 return false; 101 } 102 103 // process is probably running or pid file was not removed 104 return true; 105 } 106 107 public function hasFinished() 108 { 109 if ($this->finished) { 110 return true; 111 } 112 113 $content = $this->getPidFileContent(); 114 115 return !$this->doesPidFileExist($content); 116 } 117 118 public function getSecondsSinceCreation() 119 { 120 return time() - $this->timeCreation; 121 } 122 123 public function startProcess() 124 { 125 $this->writePidFileContent(getmypid()); 126 } 127 128 public function isRunning() 129 { 130 $content = $this->getPidFileContent(); 131 132 if (!$this->doesPidFileExist($content)) { 133 return false; 134 } 135 136 if (!$this->pidFileSizeIsNormal($content)) { 137 $this->finishProcess(); 138 return false; 139 } 140 141 if ($this->isProcessStillRunning($content)) { 142 return true; 143 } 144 145 if ($this->hasStarted($content)) { 146 $this->finishProcess(); 147 } 148 149 return false; 150 } 151 152 private function pidFileSizeIsNormal($content) 153 { 154 $size = Common::mb_strlen($content); 155 156 return $size < 500; 157 } 158 159 public function finishProcess() 160 { 161 $this->finished = true; 162 Filesystem::deleteFileIfExists($this->pidFile); 163 } 164 165 private function doesPidFileExist($content) 166 { 167 return false !== $content; 168 } 169 170 private function isProcessStillRunning($content) 171 { 172 if (!self::isSupported()) { 173 return true; 174 } 175 176 $lockedPID = trim($content); 177 $runningPIDs = self::getRunningProcesses(); 178 179 return !empty($lockedPID) && in_array($lockedPID, $runningPIDs); 180 } 181 182 private function getPidFileContent() 183 { 184 return @file_get_contents($this->pidFile); 185 } 186 187 /** 188 * Tests only 189 * @internal 190 * @param $content 191 */ 192 public function writePidFileContent($content) 193 { 194 file_put_contents($this->pidFile, $content); 195 } 196 197 public static function isSupported() 198 { 199 if (!isset(self::$isSupported)) { 200 $reasons = self::isSupportedWithReason(); 201 self::$isSupported = empty($reasons); 202 } 203 return self::$isSupported; 204 } 205 206 public static function isSupportedWithReason() 207 { 208 $reasons = []; 209 210 if (defined('PIWIK_TEST_MODE') 211 && self::isForcingAsyncProcessMode() 212 ) { 213 $reasons[] = 'forcing multicurl use for tests'; 214 } 215 216 if (SettingsServer::isWindows()) { 217 $reasons[] = 'not supported on windows'; 218 } 219 220 if (self::isMethodDisabled('shell_exec')) { 221 $reasons[] = 'shell_exec is disabled'; 222 return $reasons; // shell_exec is used for almost every other check 223 } 224 225 $getMyPidDisabled = self::isMethodDisabled('getmypid'); 226 if ($getMyPidDisabled) { 227 $reasons[] = 'getmypid is disabled'; 228 } 229 230 if (self::isSystemNotSupported()) { 231 $reasons[] = 'system returned by `uname -a` is not supported'; 232 } 233 234 if (!self::psExistsAndRunsCorrectly()) { 235 $reasons[] = 'shell_exec(' . self::PS_COMMAND . '" 2> /dev/null") did not return a success code'; 236 } else if (!$getMyPidDisabled) { 237 $pid = @getmypid(); 238 if (empty($pid) || !in_array($pid, self::getRunningProcesses())) { 239 $reasons[] = 'could not find our pid (from getmypid()) in the output of `' . self::PS_COMMAND . '`'; 240 } 241 } 242 243 if (!self::awkExistsAndRunsCorrectly()) { 244 $reasons[] = 'awk is not available or did not run as we would expect it to'; 245 } 246 247 return $reasons; 248 } 249 250 private static function psExistsAndRunsCorrectly() 251 { 252 return self::returnsSuccessCode(self::PS_COMMAND . ' 2>/dev/null'); 253 } 254 255 private static function awkExistsAndRunsCorrectly() 256 { 257 $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'); 258 return trim($testResult) == '537'; 259 } 260 261 private static function isSystemNotSupported() 262 { 263 $uname = @shell_exec('uname -a 2> /dev/null'); 264 265 if (empty($uname)) { 266 $uname = php_uname(); 267 } 268 269 if (strpos($uname, 'synology') !== false) { 270 return true; 271 } 272 return false; 273 } 274 275 public static function isMethodDisabled($command) 276 { 277 if (!function_exists($command)) { 278 return true; 279 } 280 281 $disabled = explode(',', ini_get('disable_functions')); 282 $disabled = array_map('trim', $disabled); 283 return in_array($command, $disabled) || !function_exists($command); 284 } 285 286 private static function returnsSuccessCode($command) 287 { 288 $exec = $command . ' > /dev/null 2>&1; echo $?'; 289 $returnCode = @shell_exec($exec); 290 $returnCode = trim($returnCode); 291 return 0 == (int) $returnCode; 292 } 293 294 public static function getListOfRunningProcesses() 295 { 296 $processes = @shell_exec(self::PS_COMMAND . ' 2>/dev/null'); 297 if (empty($processes)) { 298 return array(); 299 } 300 return explode("\n", $processes); 301 } 302 303 /** 304 * @return int[] The ids of the currently running processes 305 */ 306 public static function getRunningProcesses() 307 { 308 $ids = explode("\n", trim(shell_exec(self::PS_COMMAND . ' 2>/dev/null | ' . self::AWK_COMMAND . ' 2>/dev/null'))); 309 310 $ids = array_map('intval', $ids); 311 $ids = array_filter($ids, function ($id) { 312 return $id > 0; 313 }); 314 315 return $ids; 316 } 317} 318