diff --git a/scripts/phutil_symbols.php b/scripts/phutil_symbols.php index c0b5bd1..b27e0fd 100755 --- a/scripts/phutil_symbols.php +++ b/scripts/phutil_symbols.php @@ -1,555 +1,564 @@ #!/usr/bin/env php setTagline(pht('identify symbols in a PHP source file')); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'all', 'help' => pht( 'Report all symbols, including built-ins and declared externals.'), ), array( 'name' => 'ugly', 'help' => pht('Do not prettify JSON output.'), ), array( 'name' => 'path', 'wildcard' => true, 'help' => pht('PHP Source file to analyze.'), ), )); $paths = $args->getArg('path'); if (count($paths) !== 1) { throw new Exception(pht('Specify exactly one path!')); } $path = Filesystem::resolvePath(head($paths)); $show_all = $args->getArg('all'); $source_code = Filesystem::readFile($path); try { $tree = XHPASTTree::newFromData($source_code); } catch (XHPASTSyntaxErrorException $ex) { $result = array( 'error' => $ex->getMessage(), 'line' => $ex->getErrorLine(), 'file' => $path, ); $json = new PhutilJSON(); echo $json->encodeFormatted($result); exit(0); } $root = $tree->getRootNode(); $root->buildSelectCache(); // -( Unsupported Constructs )------------------------------------------------ $namespaces = $root->selectDescendantsOfType('n_NAMESPACE'); foreach ($namespaces as $namespace) { phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces')); } $uses = $root->selectDescendantsOfType('n_USE'); foreach ($namespaces as $namespace) { phutil_fail_on_unsupported_feature( $namespace, $path, pht('namespace `%s` statements', 'use')); } $possible_traits = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($possible_traits as $possible_trait) { $attributes = $possible_trait->getChildByIndex(0); // Can't use getChildByIndex here because not all classes have attributes foreach ($attributes->getChildren() as $attribute) { if (strtolower($attribute->getConcreteString()) === 'trait') { phutil_fail_on_unsupported_feature($possible_trait, $path, pht('traits')); } } } // -( Marked Externals )------------------------------------------------------ // Identify symbols marked with "@phutil-external-symbol", so we exclude them // from the dependency list. $externals = array(); $doc_parser = new PhutilDocblockParser(); foreach ($root->getTokens() as $token) { if ($token->getTypeName() === 'T_DOC_COMMENT') { list($block, $special) = $doc_parser->parse($token->getValue()); $ext_list = idx($special, 'phutil-external-symbol'); $ext_list = (array)$ext_list; $ext_list = array_filter($ext_list); foreach ($ext_list as $ext_ref) { $matches = null; if (preg_match('/^\s*(\S+)\s+(\S+)/', $ext_ref, $matches)) { $externals[$matches[1]][$matches[2]] = true; } } } } // -( Declarations and Dependencies )----------------------------------------- // The first stage of analysis is to find all the symbols we declare in the // file (like functions and classes) and all the symbols we use in the file // (like calling functions and invoking classes). Later, we filter this list // to exclude builtins. $have = array(); // For symbols we declare. $need = array(); // For symbols we use. $xmap = array(); // For extended classes and implemented interfaces. // -( Functions )------------------------------------------------------------- // Find functions declared in this file. // This is "function f() { ... }". $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); if ($name->getTypeName() === 'n_EMPTY') { // This is an anonymous function; don't record it into the symbol // index. continue; } $have[] = array( 'type' => 'function', 'symbol' => $name, ); } // Find functions used by this file. Uses: // // - Explicit Call // - String literal passed to call_user_func() or call_user_func_array() // - String literal in array literal in call_user_func()/call_user_func_array() // // TODO: Possibly support these: // // - String literal in ReflectionFunction(). // This is "f();". $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $name = $call->getChildByIndex(0); if ($name->getTypeName() === 'n_VARIABLE' || $name->getTypeName() === 'n_VARIABLE_VARIABLE') { // Ignore these, we can't analyze them. continue; } if ($name->getTypeName() === 'n_CLASS_STATIC_ACCESS') { // These are "C::f()", we'll pick this up later on. continue; } $call_name = $name->getConcreteString(); if ($call_name === 'call_user_func' || $call_name === 'call_user_func_array') { $params = $call->getChildByIndex(1)->getChildren(); if (!count($params)) { // This is a bare call_user_func() with no arguments; just ignore it. continue; } $symbol = array_shift($params); $type = 'function'; $symbol_value = $symbol->getStringLiteralValue(); $pos = strpos($symbol_value, '::'); if ($pos) { $type = 'class'; $symbol_value = substr($symbol_value, 0, $pos); } else if ($symbol->getTypeName() === 'n_ARRAY_LITERAL') { try { $type = 'class'; $symbol_value = idx($symbol->evalStatic(), 0); } catch (Exception $ex) {} } if ($symbol_value && strpos($symbol_value, '$') === false) { $need[] = array( 'type' => $type, 'name' => $symbol_value, 'symbol' => $symbol, ); } } else { $need[] = array( 'type' => 'function', 'symbol' => $name, ); } } // -( Classes )--------------------------------------------------------------- // Find classes declared by this file. // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); $have[] = array( 'type' => 'class', 'symbol' => $class_name, ); } // Find classes used by this file. We identify these: // // - class ... extends X // - new X // - Static method call // - Static property access // - Use of class constant // - typehints // - catch // - instanceof // - newv() // // TODO: Possibly support these: // // - String literal in ReflectionClass(). // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1)->getConcreteString(); $extends = $class->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $need[] = array( 'type' => 'class', 'symbol' => $parent, ); // Track all 'extends' in the extension map. $xmap[$class_name][] = $parent->getConcreteString(); } } // This is "new X()". $uses_of_new = $root->selectDescendantsOfType('n_NEW'); foreach ($uses_of_new as $new_operator) { $name = $new_operator->getChildByIndex(0); if ($name->getTypeName() === 'n_VARIABLE' || $name->getTypeName() === 'n_VARIABLE_VARIABLE') { continue; } $need[] = array( 'type' => 'class', 'symbol' => $name, ); } // This covers all of "X::$y", "X::y()" and "X::CONST". $static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($static_uses as $static_use) { $name = $static_use->getChildByIndex(0); if ($name->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class', 'symbol' => $name, ); } // This is "function (X $x)". $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER'); foreach ($parameters as $parameter) { $hint = $parameter->getChildByIndex(0); if ($hint->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class/interface', 'symbol' => $hint, ); } // This is "catch (Exception $ex)". $catches = $root->selectDescendantsOfType('n_CATCH'); foreach ($catches as $catch) { $need[] = array( 'type' => 'class/interface', 'symbol' => $catch->getChildOfType(0, 'n_CLASS_NAME'), ); } // This is "$x instanceof X". $instanceofs = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); foreach ($instanceofs as $instanceof) { $operator = $instanceof->getChildOfType(1, 'n_OPERATOR'); if ($operator->getConcreteString() !== 'instanceof') { continue; } $class = $instanceof->getChildByIndex(2); if ($class->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class/interface', 'symbol' => $class, ); } // This is "newv('X')". $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $call_name = $call->getChildByIndex(0)->getConcreteString(); if ($call_name !== 'newv') { continue; } $params = $call->getChildByIndex(1)->getChildren(); if (!count($params)) { continue; } $symbol = reset($params); $symbol_value = $symbol->getStringLiteralValue(); if ($symbol_value && strpos($symbol_value, '$') === false) { $need[] = array( 'type' => 'class', 'name' => $symbol_value, 'symbol' => $symbol, ); } } // -( Interfaces )------------------------------------------------------------ // Find interfaces declared in this file. // This is "interface X .. { ... }". $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1); $have[] = array( 'type' => 'interface', 'symbol' => $interface_name, ); } // Find interfaces used by this file. We identify these: // // - class ... implements X // - interface ... extends X // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1)->getConcreteString(); $implements = $class->getChildByIndex(3); $interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); foreach ($interfaces as $interface) { $need[] = array( 'type' => 'interface', 'symbol' => $interface, ); // Track 'class ... implements' in the extension map. $xmap[$class_name][] = $interface->getConcreteString(); } } // This is "interface X ... { ... }". $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1)->getConcreteString(); $extends = $interface->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $need[] = array( 'type' => 'interface', 'symbol' => $parent, ); // Track 'interface ... extends' in the extension map. $xmap[$interface_name][] = $parent->getConcreteString(); } } // -( Analysis )-------------------------------------------------------------- $declared_symbols = array(); foreach ($have as $key => $spec) { $name = $spec['symbol']->getConcreteString(); $declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset(); } $required_symbols = array(); foreach ($need as $key => $spec) { $name = idx($spec, 'name'); if (!$name) { $name = $spec['symbol']->getConcreteString(); } $type = $spec['type']; foreach (explode('/', $type) as $libtype) { if (!$show_all) { if (!empty($externals[$libtype][$name])) { // Ignore symbols declared as externals. continue 2; } if (!empty($builtins[$libtype][$name])) { // Ignore symbols declared as builtins. continue 2; } } if (!empty($declared_symbols[$libtype][$name])) { // We declare this symbol, so don't treat it as a requirement. continue 2; } } if (!empty($required_symbols[$type][$name])) { // Report only the first use of a symbol, since reporting all of them // isn't terribly informative. continue; } $required_symbols[$type][$name] = $spec['symbol']->getOffset(); } $result = array( 'have' => $declared_symbols, 'need' => $required_symbols, 'xmap' => $xmap, ); // -( Output )---------------------------------------------------------------- if ($args->getArg('ugly')) { echo json_encode($result); } else { $json = new PhutilJSON(); echo $json->encodeFormatted($result); } // -( Library )--------------------------------------------------------------- function phutil_fail_on_unsupported_feature(XHPASTNode $node, $file, $what) { $line = $node->getLineNumber(); $message = phutil_console_wrap( pht( '`%s` has limited support for features introduced after PHP 5.2.3. '. 'This library uses an unsupported feature (%s) on line %d of %s.', 'arc liberate', $what, $line, Filesystem::readablePath($file))); $result = array( 'error' => $message, 'line' => $line, 'file' => $file, ); $json = new PhutilJSON(); echo $json->encodeFormatted($result); exit(0); } function phutil_symbols_get_builtins() { $builtin = array(); $builtin['classes'] = get_declared_classes(); $builtin['interfaces'] = get_declared_interfaces(); $funcs = get_defined_functions(); $builtin['functions'] = $funcs['internal']; $compat = json_decode( file_get_contents( dirname(__FILE__).'/../resources/php_compat_info.json'), true); foreach (array('functions', 'classes', 'interfaces') as $type) { // Developers may not have every extension that a library potentially uses // installed. We supplement the list of declared functions and classes with // a list of known extension functions to avoid raising false positives just // because you don't have pcntl, etc. $extensions = array_keys($compat[$type]); $builtin[$type] = array_merge($builtin[$type], $extensions); } return array( 'class' => array_fill_keys($builtin['classes'], true) + array( 'static' => true, 'parent' => true, 'self' => true, 'PhutilBootloader' => true, + + // PHP7 defines these new parent classes of "Exception", but they do not + // exist prior to PHP7. It's possible to use them safely in PHP5, in + // some cases, to write code which is compatible with either PHP5 or + // PHP7, but it s hard for us tell if a particular use is safe or not. + // For now, assume users know what they're doing and that uses are safe. + // For discussion, see T12855. + 'Throwable' => true, + 'Error' => true, ), 'function' => array_filter( array( 'empty' => true, 'isset' => true, 'die' => true, // These are provided by libphutil but not visible in the map. 'phutil_is_windows' => true, 'phutil_load_library' => true, 'phutil_is_hiphop_runtime' => true, // HPHP/i defines these functions as 'internal', but they are NOT // builtins and do not exist in vanilla PHP. Make sure we don't mark // them as builtin since we need to add dependencies for them. 'idx' => false, 'id' => false, ) + array_fill_keys($builtin['functions'], true)), 'interface' => array_fill_keys($builtin['interfaces'], true), ); } diff --git a/src/console/PhutilInteractiveEditor.php b/src/console/PhutilInteractiveEditor.php index c3b9516..f23f157 100644 --- a/src/console/PhutilInteractiveEditor.php +++ b/src/console/PhutilInteractiveEditor.php @@ -1,284 +1,282 @@ setName('shopping_list') * ->setLineOffset(15) * ->editInteractively(); * * This will launch the user's $EDITOR to edit the specified '$document', and * return their changes into '$result'. * * @task create Creating a New Editor * @task edit Editing Interactively * @task config Configuring Options */ final class PhutilInteractiveEditor extends Phobject { private $name = ''; private $content = ''; private $offset = 0; private $preferred; private $fallback; /* -( Creating a New Editor )---------------------------------------------- */ /** * Constructs an interactive editor, using the text of a document. * * @param string Document text. * @return $this * * @task create */ public function __construct($content) { $this->setContent($content); } /* -( Editing Interactively )----------------------------------------------- */ /** * Launch an editor and edit the content. The edited content will be * returned. * * @return string Edited content. * @throws Exception The editor exited abnormally or something untoward * occurred. * * @task edit */ public function editInteractively() { $name = $this->getName(); $content = $this->getContent(); if (phutil_is_windows()) { $content = str_replace("\n", "\r\n", $content); } $tmp = Filesystem::createTemporaryDirectory('edit.'); $path = $tmp.DIRECTORY_SEPARATOR.$name; try { Filesystem::writeFile($path, $content); } catch (Exception $ex) { Filesystem::remove($tmp); throw $ex; } $editor = $this->getEditor(); $offset = $this->getLineOffset(); $err = $this->invokeEditor($editor, $path, $offset); if ($err) { Filesystem::remove($tmp); throw new Exception(pht('Editor exited with an error code (#%d).', $err)); } try { $result = Filesystem::readFile($path); Filesystem::remove($tmp); } catch (Exception $ex) { Filesystem::remove($tmp); throw $ex; } if (phutil_is_windows()) { $result = str_replace("\r\n", "\n", $result); } $this->setContent($result); return $this->getContent(); } private function invokeEditor($editor, $path, $offset) { // NOTE: Popular Windows editors like Notepad++ and GitPad do not support // line offsets, so just ignore the offset feature on Windows. We rarely // use it anyway. $offset_flag = ''; if ($offset && !phutil_is_windows()) { $offset = (int)$offset; if (preg_match('/^mate/', $editor)) { $offset_flag = csprintf('-l %d', $offset); } else { $offset_flag = csprintf('+%d', $offset); } } $cmd = csprintf( '%C %C %s', $editor, $offset_flag, $path); return phutil_passthru('%C', $cmd); } /* -( Configuring Options )------------------------------------------------- */ /** * Set the line offset where the cursor should be positioned when the editor * opens. By default, the cursor will be positioned at the start of the * content. * * @param int Line number where the cursor should be positioned. * @return $this * * @task config */ public function setLineOffset($offset) { $this->offset = (int)$offset; return $this; } /** * Get the current line offset. See setLineOffset(). * * @return int Current line offset. * * @task config */ public function getLineOffset() { return $this->offset; } /** * Set the document name. Depending on the editor, this may be exposed to * the user and can give them a sense of what they're editing. * * @param string Document name. * @return $this * * @task config */ public function setName($name) { $name = preg_replace('/[^A-Z0-9._-]+/i', '', $name); $this->name = $name; return $this; } /** * Get the current document name. See @{method:setName} for details. * * @return string Current document name. * * @task config */ public function getName() { if (!strlen($this->name)) { return 'untitled'; } return $this->name; } /** * Set the text content to be edited. * * @param string New content. * @return $this * * @task config */ public function setContent($content) { $this->content = $content; return $this; } /** * Retrieve the current content. * * @return string * * @task config */ public function getContent() { return $this->content; } /** * Set the fallback editor program to be used if the env variable $EDITOR * is not available and there is no `editor` binary in PATH. * * @param string Command-line editing program (e.g. 'emacs', 'vi') * @return $this * * @task config */ public function setFallbackEditor($editor) { $this->fallback = $editor; return $this; } /** * Set the preferred editor program. If set, this will override all other * sources of editor configuration, like $EDITOR. * * @param string Command-line editing program (e.g. 'emacs', 'vi') * @return $this * * @task config */ public function setPreferredEditor($editor) { $this->preferred = $editor; return $this; } /** * Get the name of the editor program to use. The value of the environmental * variable $EDITOR will be used if available; otherwise, the `editor` binary * if present; otherwise the best editor will be selected. * * @return string Command-line editing program. * * @task config */ public function getEditor() { if ($this->preferred) { return $this->preferred; } $editor = getenv('EDITOR'); if ($editor) { return $editor; } - // Look for `editor` in PATH, some systems provide an editor which is - // linked to something sensible. - if (Filesystem::binaryExists('editor')) { - return 'editor'; - } - if ($this->fallback) { return $this->fallback; } - if (Filesystem::binaryExists('nano')) { - return 'nano'; + $candidates = array('editor', 'nano', 'sensible-editor', 'vi'); + + foreach ($candidates as $cmd) { + if (Filesystem::binaryExists($cmd)) { + return $cmd; + } } throw new Exception( pht( 'Unable to launch an interactive text editor. Set the %s '. 'environment variable to an appropriate editor.', 'EDITOR')); } } diff --git a/src/daemon/PhutilDaemonOverseer.php b/src/daemon/PhutilDaemonOverseer.php index bf1f864..693df16 100644 --- a/src/daemon/PhutilDaemonOverseer.php +++ b/src/daemon/PhutilDaemonOverseer.php @@ -1,520 +1,535 @@ enableDiscardMode(); $args = new PhutilArgumentParser($argv); $args->setTagline(pht('daemon overseer')); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'trace-memory', 'help' => pht('Enable debug memory tracing.'), ), array( 'name' => 'verbose', 'help' => pht('Enable verbose activity logging.'), ), array( 'name' => 'label', 'short' => 'l', 'param' => 'label', 'help' => pht( 'Optional process label. Makes "%s" nicer, no behavioral effects.', 'ps'), ), )); $argv = array(); if ($args->getArg('trace')) { $this->traceMode = true; $argv[] = '--trace'; } if ($args->getArg('trace-memory')) { $this->traceMode = true; $this->traceMemory = true; $argv[] = '--trace-memory'; } $verbose = $args->getArg('verbose'); if ($verbose) { $this->verbose = true; $argv[] = '--verbose'; } $label = $args->getArg('label'); if ($label) { $argv[] = '-l'; $argv[] = $label; } $this->argv = $argv; if (function_exists('posix_isatty') && posix_isatty(STDIN)) { fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n"); } $config = @file_get_contents('php://stdin'); $config = id(new PhutilJSONParser())->parse($config); $this->libraries = idx($config, 'load'); $this->log = idx($config, 'log'); $this->daemonize = idx($config, 'daemonize'); $this->piddir = idx($config, 'piddir'); $this->config = $config; if (self::$instance) { throw new Exception( pht('You may not instantiate more than one Overseer per process.')); } self::$instance = $this; $this->startEpoch = time(); // Check this before we daemonize, since if it's an issue the child will // exit immediately. if ($this->piddir) { $dir = $this->piddir; try { Filesystem::assertWritable($dir); } catch (Exception $ex) { throw new Exception( pht( "Specified daemon PID directory ('%s') does not exist or is ". "not writable by the daemon user!", $dir)); } } if (!idx($config, 'daemons')) { throw new PhutilArgumentUsageException( pht('You must specify at least one daemon to start!')); } if ($this->log) { // NOTE: Now that we're committed to daemonizing, redirect the error // log if we have a `--log` parameter. Do this at the last moment // so as many setup issues as possible are surfaced. ini_set('error_log', $this->log); } if ($this->daemonize) { // We need to get rid of these or the daemon will hang when we TERM it // waiting for something to read the buffers. TODO: Learn how unix works. fclose(STDOUT); fclose(STDERR); ob_start(); $pid = pcntl_fork(); if ($pid === -1) { throw new Exception(pht('Unable to fork!')); } else if ($pid) { exit(0); } $sid = posix_setsid(); if ($sid <= 0) { throw new Exception(pht('Failed to create new process session!')); } } $this->logMessage( 'OVER', pht( 'Started new daemon overseer (with PID "%s").', getmypid())); $this->modules = PhutilDaemonOverseerModule::getAllModules(); $this->installSignalHandlers(); } public function addLibrary($library) { $this->libraries[] = $library; return $this; } public function run() { $this->createDaemonPools(); while (true) { if ($this->shouldReloadDaemons()) { $this->didReceiveSignal(SIGHUP); } $futures = array(); $running_pools = false; foreach ($this->getDaemonPools() as $pool) { $pool->updatePool(); if (!$this->shouldShutdown()) { if ($pool->isHibernating()) { if ($this->shouldWakePool($pool)) { $pool->wakeFromHibernation(); } } } foreach ($pool->getFutures() as $future) { $futures[] = $future; } if ($pool->getDaemons()) { $running_pools = true; } } $this->updatePidfile(); $this->updateMemory(); $this->waitForDaemonFutures($futures); if (!$futures && !$running_pools) { if ($this->shouldShutdown()) { break; } } } exit($this->err); } private function waitForDaemonFutures(array $futures) { assert_instances_of($futures, 'ExecFuture'); if ($futures) { // TODO: This only wakes if any daemons actually exit. It would be a bit // cleaner to wait on any I/O with Channels. $iter = id(new FutureIterator($futures)) ->setUpdateInterval(1); foreach ($iter as $future) { break; } } else { if (!$this->shouldShutdown()) { sleep(1); } } } private function createDaemonPools() { $configs = $this->config['daemons']; $forced_options = array( 'load' => $this->libraries, 'log' => $this->log, ); foreach ($configs as $config) { $config = $forced_options + $config; $pool = PhutilDaemonPool::newFromConfig($config) ->setOverseer($this) ->setCommandLineArguments($this->argv); $this->pools[] = $pool; } } private function getDaemonPools() { return $this->pools; } /** * Identify running daemons by examining the process table. This isn't * completely reliable, but can be used as a fallback if the pid files fail * or we end up with stray daemons by other means. * * Example output (array keys are process IDs): * * array( * 12345 => array( * 'type' => 'overseer', * 'command' => 'php launch_daemon.php --daemonize ...', * 'pid' => 12345, * ), * 12346 => array( * 'type' => 'daemon', * 'command' => 'php exec_daemon.php ...', * 'pid' => 12346, * ), * ); * * @return dict Map of PIDs to process information, identifying running * daemon processes. */ public static function findRunningDaemons() { $results = array(); list($err, $processes) = exec_manual('ps -o pid,command -a -x -w -w -w'); if ($err) { return $results; } $processes = array_filter(explode("\n", trim($processes))); foreach ($processes as $process) { list($pid, $command) = preg_split('/\s+/', trim($process), 2); $pattern = '/((launch|exec)_daemon.php|phd-daemon)/'; $matches = null; if (!preg_match($pattern, $command, $matches)) { continue; } switch ($matches[1]) { case 'exec_daemon.php': $type = 'daemon'; break; case 'launch_daemon.php': case 'phd-daemon': default: $type = 'overseer'; break; } $results[(int)$pid] = array( 'type' => $type, 'command' => $command, 'pid' => (int)$pid, ); } return $results; } private function updatePidfile() { if (!$this->piddir) { return; } $pidfile = $this->toDictionary(); if ($pidfile !== $this->lastPidfile) { $this->lastPidfile = $pidfile; $pidfile_path = $this->piddir.'/daemon.'.getmypid(); - Filesystem::writeFile($pidfile_path, phutil_json_encode($pidfile)); + try { + Filesystem::writeFile( + $pidfile_path, + phutil_json_encode($pidfile)); + } catch (Exception $ex) { + // This write can fail if the disk is full. We already tested the + // directory for writability on startup, so just ignore this and + // move on rather than crashing. If the disk is full this error may + // not make it to a log file, but at least we tried. + $this->logMessage( + 'PIDF', + pht( + 'Unable to update PID file: %s.', + $ex->getMessage())); + } } } public function toDictionary() { $daemons = array(); foreach ($this->getDaemonPools() as $pool) { foreach ($pool->getDaemons() as $daemon) { if (!$daemon->isRunning()) { continue; } $daemons[] = $daemon->toDictionary(); } } return array( 'pid' => getmypid(), 'start' => $this->startEpoch, 'config' => $this->config, 'daemons' => $daemons, ); } private function updateMemory() { if (!$this->traceMemory) { return; } $this->logMessage( 'RAMS', pht( 'Overseer Memory Usage: %s KB', new PhutilNumber(memory_get_usage() / 1024, 1))); } public function logMessage($type, $message, $context = null) { $always_log = false; switch ($type) { case 'OVER': case 'SGNL': + case 'PIDF': $always_log = true; break; } if ($always_log || $this->traceMode || $this->verbose) { error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message); } } /* -( Signal Handling )---------------------------------------------------- */ /** * @task signals */ private function installSignalHandlers() { $signals = array( SIGUSR2, SIGHUP, SIGINT, SIGTERM, ); foreach ($signals as $signal) { pcntl_signal($signal, array($this, 'didReceiveSignal')); } } /** * @task signals */ public function didReceiveSignal($signo) { $this->logMessage( 'SGNL', pht( 'Overseer ("%d") received signal %d ("%s").', getmypid(), $signo, phutil_get_signal_name($signo))); switch ($signo) { case SIGUSR2: $signal_type = self::SIGNAL_NOTIFY; break; case SIGHUP: $signal_type = self::SIGNAL_RELOAD; break; case SIGINT: // If we receive SIGINT more than once, interpret it like SIGTERM. if ($this->inGracefulShutdown) { return $this->didReceiveSignal(SIGTERM); } $this->inGracefulShutdown = true; $signal_type = self::SIGNAL_GRACEFUL; break; case SIGTERM: // If we receive SIGTERM more than once, terminate abruptly. $this->err = 128 + $signo; if ($this->inAbruptShutdown) { exit($this->err); } $this->inAbruptShutdown = true; $signal_type = self::SIGNAL_TERMINATE; break; default: throw new Exception( pht( 'Signal handler called with unknown signal type ("%d")!', $signo)); } foreach ($this->getDaemonPools() as $pool) { $pool->didReceiveSignal($signal_type, $signo); } } /* -( Daemon Modules )----------------------------------------------------- */ private function getModules() { return $this->modules; } private function shouldReloadDaemons() { $modules = $this->getModules(); $should_reload = false; foreach ($modules as $module) { try { // NOTE: Even if one module tells us to reload, we call the method on // each module anyway to make calls a little more predictable. if ($module->shouldReloadDaemons()) { $this->logMessage( 'RELO', pht( 'Reloading daemons (triggered by overseer module "%s").', get_class($module))); $should_reload = true; } } catch (Exception $ex) { phlog($ex); } } return $should_reload; } private function shouldWakePool(PhutilDaemonPool $pool) { $modules = $this->getModules(); $should_wake = false; foreach ($modules as $module) { try { if ($module->shouldWakePool($pool)) { $this->logMessage( 'WAKE', pht( 'Waking pool "%s" (triggered by overseer module "%s").', $pool->getPoolLabel(), get_class($module))); $should_wake = true; } } catch (Exception $ex) { phlog($ex); } } return $should_wake; } private function shouldShutdown() { return $this->inGracefulShutdown || $this->inAbruptShutdown; } } diff --git a/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt b/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt new file mode 100644 index 0000000..40c27ca --- /dev/null +++ b/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt @@ -0,0 +1,13 @@ +| Alpaca | +| | +| Zebra | +~~~~~~~~~~ +
+ + + +
Alpaca
Zebra
+~~~~~~~~~~ +| Alpaca | +| | +| Zebra | diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php index 474a18a..58ad3d5 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php @@ -1,73 +1,89 @@ cells + // instead of cells. + + // If it has other types of cells, it's always a content row. + + // If it has only empty cells, it's an empty row. + + if (strlen($cell)) { + if (preg_match('/^--+\z/', $cell)) { + $any_header = true; + } else { + $any_content = true; + } } + $cells[] = array('type' => 'td', 'content' => $this->applyRules($cell)); } - if (!$headings) { + $is_header = ($any_header && !$any_content); + + if (!$is_header) { $rows[] = array('type' => 'tr', 'content' => $cells); } else if ($rows) { // Mark previous row with headings. foreach ($cells as $i => $cell) { if ($cell['content']) { $rows[last_key($rows)]['content'][$i]['type'] = 'th'; } } } } if (!$rows) { return $this->applyRules($text); } return $this->renderRemarkupTable($rows); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php index f632b98..e863da9 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php @@ -1,137 +1,144 @@ getEngine(); $is_anchor = false; if (strncmp($link, '/', 1) == 0) { $base = $engine->getConfig('uri.base'); $base = rtrim($base, '/'); $link = $base.$link; } else if (strncmp($link, '#', 1) == 0) { $here = $engine->getConfig('uri.here'); $link = $here.$link; $is_anchor = true; } if ($engine->isTextMode()) { // If present, strip off "mailto:" or "tel:". $link = preg_replace('/^(?:mailto|tel):/', '', $link); if (!strlen($name)) { return $link; } return $name.' <'.$link.'>'; } if (!strlen($name)) { $name = $link; $name = preg_replace('/^(?:mailto|tel):/', '', $name); } if ($engine->getState('toc')) { return $name; } $same_window = $engine->getConfig('uri.same-window', false); if ($same_window) { $target = null; } else { $target = '_blank'; } // For anchors on the same page, always stay here. if ($is_anchor) { $target = null; } return phutil_tag( 'a', array( 'href' => $link, 'class' => 'remarkup-link', 'target' => $target, ), $name); } public function markupAlternateLink(array $matches) { $uri = trim($matches[2]); // NOTE: We apply some special rules to avoid false positives here. The // major concern is that we do not want to convert `x[0][1](y)` in a // discussion about C source code into a link. To this end, we: // // - Don't match at word boundaries; // - require the URI to contain a "/" character or "@" character; and // - reject URIs which being with a quote character. if ($uri[0] == '"' || $uri[0] == "'" || $uri[0] == '`') { return $matches[0]; } if (strpos($uri, '/') === false && strpos($uri, '@') === false && strncmp($uri, 'tel:', 4)) { return $matches[0]; } return $this->markupDocumentLink( array( $matches[0], $matches[2], $matches[1], )); } public function markupDocumentLink(array $matches) { $uri = trim($matches[1]); $name = trim(idx($matches, 2)); // If whatever is being linked to begins with "/" or "#", or has "://", // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page. $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri); if ($is_uri && strncmp('/', $uri, 1) && strncmp('#', $uri, 1)) { $protocols = $this->getEngine()->getConfig( 'uri.allowed-protocols', array()); - $protocol = id(new PhutilURI($uri))->getProtocol(); - if (!idx($protocols, $protocol)) { - // Don't treat this as a URI if it's not an allowed protocol. + + try { + $protocol = id(new PhutilURI($uri))->getProtocol(); + if (!idx($protocols, $protocol)) { + // Don't treat this as a URI if it's not an allowed protocol. + $is_uri = false; + } + } catch (Exception $ex) { + // We can end up here if we try to parse an ambiguous URI, see + // T12796. $is_uri = false; } } if (!$is_uri) { return $matches[0]; } return $this->getEngine()->storeText($this->renderHyperlink($uri, $name)); } }