diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -365,6 +365,7 @@ 'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php', 'PhutilProcessQuery' => 'filesystem/PhutilProcessQuery.php', 'PhutilProcessRef' => 'filesystem/PhutilProcessRef.php', + 'PhutilProcessRefTestCase' => 'filesystem/__tests__/PhutilProcessRefTestCase.php', 'PhutilProseDiff' => 'utils/PhutilProseDiff.php', 'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php', 'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php', @@ -1031,6 +1032,7 @@ 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilProcessQuery' => 'Phobject', 'PhutilProcessRef' => 'Phobject', + 'PhutilProcessRefTestCase' => 'PhutilTestCase', 'PhutilProseDiff' => 'Phobject', 'PhutilProseDiffTestCase' => 'PhutilTestCase', 'PhutilProseDifferenceEngine' => 'Phobject', diff --git a/src/filesystem/PhutilProcessQuery.php b/src/filesystem/PhutilProcessQuery.php --- a/src/filesystem/PhutilProcessQuery.php +++ b/src/filesystem/PhutilProcessQuery.php @@ -36,13 +36,14 @@ list($pid, $command) = $parts; $ref = id(new PhutilProcessRef()) - ->setPID((int)$pid) - ->setCommand($command); + ->setPID((int)$pid); + + $argv = $this->getArgv($pid, $command); + $ref->setArgv($argv); // If this process is a "phd-daemon" process, mark it as a daemon // overseer. - $is_overseer = (bool)preg_match('/\bphd-daemon\b/', $command); - $ref->setIsOverseer($is_overseer); + $is_overseer = $ref->getIsOverseer(); // If this is an overseer and the command has a "-l" ("Label") argument, // the argument contains the "PHABRICATOR_INSTANCE" value for the daemon. @@ -80,4 +81,49 @@ return array_values($refs); } + private function getArgv($pid, $command) { + + // In the output of "ps", arguments in process titles are not escaped, so + // we can not distinguish between the processes created by running these + // commands by looking only at the output of "ps": + // + // echo 'a b' + // echo a b + // + // Both commands will have the same process title in the output of "ps". + + // This means we may split the command incorrectly in the general case, + // and this misparsing may be important if the process binary resides in + // a directory with spaces in its path and we're trying to identify which + // binary a process is running. + + // On Ubuntu, and likely most other Linux systems, we can get a raw + // command line from "/proc" with arguments delimited by "\0". + + // On macOS, there's no "/proc" and we don't currently have a robust way + // to split the process command in a way that parses spaces properly, so + // fall back to a best effort based on the output of "ps". This is almost + // always correct, since it is uncommon to put binaries under paths with + // spaces in them. + + $proc_cmdline = sprintf('/proc/%d/cmdline', $pid); + try { + $argv = Filesystem::readFile($proc_cmdline); + $argv = explode("\0", $argv); + + // The output itself is terminated with "\0", so remove the final empty + // argument. + if (last($argv) === '') { + array_pop($argv); + } + + return $argv; + } catch (Exception $ex) { + // If we fail to read "/proc", fall through to less reliable methods. + } + + // If we haven't found a better source, just split the "ps" output on + // spaces. + return preg_split('/\s+/', $command); + } } diff --git a/src/filesystem/PhutilProcessRef.php b/src/filesystem/PhutilProcessRef.php --- a/src/filesystem/PhutilProcessRef.php +++ b/src/filesystem/PhutilProcessRef.php @@ -7,6 +7,7 @@ private $command; private $isOverseer; private $instance; + private $argv; public function setPID($pid) { $this->pid = $pid; @@ -17,21 +18,23 @@ return $this->pid; } - public function setCommand($command) { - $this->command = $command; - return $this; - } - public function getCommand() { - return $this->command; - } + if (!$this->command) { + $this->command = phutil_string_cast(csprintf('%LR', $this->argv)); + } - public function setIsOverseer($is_overseer) { - $this->isOverseer = $is_overseer; - return $this; + return $this->command; } public function getIsOverseer() { + if ($this->isOverseer === null) { + $this->isOverseer = $this->getCommandMatch( + array( + array('phd-daemon'), + array('php', 'phd-daemon'), + )); + } + return $this->isOverseer; } @@ -44,4 +47,34 @@ return $this->instance; } + private function getCommandMatch(array $patterns) { + $argv = $this->getArgv(); + + foreach ($patterns as $pattern) { + $pattern = array_values($pattern); + $is_match = true; + for ($ii = 0; $ii < count($pattern); $ii++) { + if (basename($argv[$ii]) !== $pattern[$ii]) { + $is_match = false; + break; + } + } + + if ($is_match) { + return true; + } + } + + return false; + } + + public function setArgv(array $argv) { + $this->argv = $argv; + return $this; + } + + public function getArgv() { + return $this->argv; + } + } diff --git a/src/filesystem/__tests__/PhutilProcessRefTestCase.php b/src/filesystem/__tests__/PhutilProcessRefTestCase.php new file mode 100644 --- /dev/null +++ b/src/filesystem/__tests__/PhutilProcessRefTestCase.php @@ -0,0 +1,59 @@ +setArgv($argv); + + $actual = $ref->getIsOverseer(); + + $this->assertEqual( + $expect, + $actual, + pht('argv: %s', implode(' ', $argv))); + } + } + +}