diff --git a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php index 4574056deb..0cae8541d2 100644 --- a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php @@ -1,106 +1,105 @@ <?php /** * @group console */ final class DarkConsoleXHProfPlugin extends DarkConsolePlugin { protected $xhprofID; public function getName() { return 'XHProf'; } public function getColor() { $data = $this->getData(); if ($data['xhprofID']) { return '#ff00ff'; } return null; } public function getDescription() { return 'Provides detailed PHP profiling information through XHProf.'; } public function generateData() { return array( 'xhprofID' => $this->xhprofID, 'profileURI' => (string)$this ->getRequestURI() ->alter('__profile__', 'page'), ); } public function getXHProfRunID() { return $this->xhprofID; } public function renderPanel() { $data = $this->getData(); $run = $data['xhprofID']; $profile_uri = $data['profileURI']; if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) { $href = PhabricatorEnv::getDoclink('article/Installation_Guide.html'); $install_guide = phutil_tag( 'a', array( 'href' => $href, 'class' => 'bright-link', ), 'Installation Guide'); return '<div class="dark-console-no-content">'. 'The "xhprof" PHP extension is not available. Install xhprof '. 'to enable the XHProf console plugin. You can find instructions in '. 'the '.$install_guide.'.'. '</div>'; } $result = array(); $header = '<div class="dark-console-panel-header">'. phutil_tag( 'a', array( 'href' => $profile_uri, 'class' => $run ? 'disabled button' : 'green button', ), 'Profile Page'). '<h1>XHProf Profiler</h1>'. '</div>'; $result[] = $header; if ($run) { $result[] = '<a href="/xhprof/profile/'.$run.'/" '. 'class="bright-link" '. 'style="float: right; margin: 1em 2em 0 0;'. 'font-weight: bold;" '. 'target="_blank">Profile Permalink</a>'. '<iframe src="/xhprof/profile/'.$run.'/?frame=true"></iframe>'; } else { $result[] = '<div class="dark-console-no-content">'. 'Profiling was not enabled for this page. Use the button above '. 'to enable it.'. '</div>'; } return implode("\n", $result); } public function willShutdown() { - if (DarkConsoleXHProfPluginAPI::isProfilerRequested() && - (DarkConsoleXHProfPluginAPI::isProfilerRequested() !== 'all')) { + if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $this->xhprofID = DarkConsoleXHProfPluginAPI::stopProfiler(); } } } diff --git a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php index e2f9cad21d..afad7f1b8e 100644 --- a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php +++ b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php @@ -1,127 +1,171 @@ <?php /** * @group console * @phutil-external-symbol function xhprof_enable * @phutil-external-symbol function xhprof_disable */ final class DarkConsoleXHProfPluginAPI { private static $profilerStarted; public static function isProfilerAvailable() { return extension_loaded('xhprof'); } public static function getProfilerHeader() { return 'X-Phabricator-Profiler'; } public static function isProfilerRequested() { if (!empty($_REQUEST['__profile__'])) { return $_REQUEST['__profile__']; } $header = AphrontRequest::getHTTPHeader(self::getProfilerHeader()); if ($header) { return $header; } - static $profilerRequested = null; + return false; + } + + public static function shouldStartProfiler() { + if (self::isProfilerRequested()) { + return true; + } - if (!isset($profilerRequested)) { + static $sample_request = null; + + if ($sample_request === null) { if (PhabricatorEnv::getEnvConfig('debug.profile-rate')) { $rate = PhabricatorEnv::getEnvConfig('debug.profile-rate'); if (mt_rand(1, $rate) == 1) { - $profilerRequested = true; + $sample_request = true; } else { - $profilerRequested = false; + $sample_request = false; } } } - return $profilerRequested; + return $sample_request; + } + + public static function isProfilerStarted() { + return self::$profilerStarted; } public static function includeXHProfLib() { // TODO: this is incredibly stupid, but we may not have Phutil metamodule // stuff loaded yet so we can't just phutil_get_library_root() our way // to victory. $root = __FILE__; for ($ii = 0; $ii < 6; $ii++) { $root = dirname($root); } require_once $root.'/externals/xhprof/xhprof_lib.php'; } + public static function saveProfilerSample( + AphrontRequest $request, + $access_log) { + + if (!self::isProfilerStarted()) { + return; + } + + $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); + $profile_sample = id(new PhabricatorXHProfSample()) + ->setFilePHID($profile); + + if (self::isProfilerRequested()) { + $sample_rate = 0; + } else { + $sample_rate = PhabricatorEnv::getEnvConfig('debug.profile-rate'); + } + + $profile_sample->setSampleRate($sample_rate); + + if ($access_log) { + $profile_sample + ->setUsTotal($access_log->getData('T')) + ->setHostname($access_log->getData('h')) + ->setRequestPath($access_log->getData('U')) + ->setController($access_log->getData('C')) + ->setUserPHID($request->getUser()->getPHID()); + } + + $profile_sample->save(); + } + public static function hookProfiler() { - if (!self::isProfilerRequested()) { + if (!self::shouldStartProfiler()) { return; } if (!self::isProfilerAvailable()) { return; } if (self::$profilerStarted) { return; } self::startProfiler(); self::$profilerStarted = true; } public static function startProfiler() { self::includeXHProfLib(); xhprof_enable(); } public static function stopProfiler() { - if (self::$profilerStarted) { - $data = xhprof_disable(); - $data = serialize($data); - $file_class = 'PhabricatorFile'; - - // Since these happen on GET we can't do guarded writes. These also - // sometimes happen after we've disposed of the write guard; in this - // case we need to disable the whole mechanism. - - $use_scope = AphrontWriteGuard::isGuardActive(); - if ($use_scope) { - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - } else { - AphrontWriteGuard::allowDangerousUnguardedWrites(true); - } + if (!self::isProfilerStarted()) { + return null; + } - $caught = null; - try { - $file = call_user_func( - array($file_class, 'newFromFileData'), - $data, - array( - 'mime-type' => 'application/xhprof', - 'name' => 'profile.xhprof', - )); - } catch (Exception $ex) { - $caught = $ex; - } + $data = xhprof_disable(); + $data = serialize($data); + $file_class = 'PhabricatorFile'; - if ($use_scope) { - unset($unguarded); - } else { - AphrontWriteGuard::allowDangerousUnguardedWrites(false); - } + // Since these happen on GET we can't do guarded writes. These also + // sometimes happen after we've disposed of the write guard; in this + // case we need to disable the whole mechanism. - if ($caught) { - throw $caught; - } else { - return $file->getPHID(); - } + $use_scope = AphrontWriteGuard::isGuardActive(); + if ($use_scope) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } else { - return null; + AphrontWriteGuard::allowDangerousUnguardedWrites(true); + } + + $caught = null; + try { + $file = call_user_func( + array($file_class, 'newFromFileData'), + $data, + array( + 'mime-type' => 'application/xhprof', + 'name' => 'profile.xhprof', + )); + } catch (Exception $ex) { + $caught = $ex; + } + + if ($use_scope) { + unset($unguarded); + } else { + AphrontWriteGuard::allowDangerousUnguardedWrites(false); + } + + if ($caught) { + throw $caught; + } else { + return $file->getPHID(); } } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index ea9cc314da..80c7a37f5d 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,393 +1,393 @@ <?php /** * This is a standard Phabricator page with menus, Javelin, DarkConsole, and * basic styles. * */ final class PhabricatorStandardPageView extends PhabricatorBarePageView { private $baseURI; private $applicationName; private $glyph; private $menuContent; private $showChrome = true; private $disableConsole; private $searchDefaultScope; private $pageObjects = array(); private $applicationMenu; public function setApplicationMenu(PhabricatorMenuView $application_menu) { $this->applicationMenu = $application_menu; return $this; } public function getApplicationMenu() { return $this->applicationMenu; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowChrome($show_chrome) { $this->showChrome = $show_chrome; return $this; } public function getShowChrome() { return $this->showChrome; } public function setSearchDefaultScope($search_default_scope) { $this->searchDefaultScope = $search_default_scope; return $this; } public function getSearchDefaultScope() { return $this->searchDefaultScope; } public function appendPageObjects(array $objs) { foreach ($objs as $obj) { $this->pageObjects[] = $obj; } } public function getTitle() { $use_glyph = true; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user && $user->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_TITLES) !== 'glyph') { $use_glyph = false; } } return ($use_glyph ? $this->getGlyph() : '['.$this->getApplicationName().']'). ' '.parent::getTitle(); } protected function willRenderPage() { parent::willRenderPage(); if (!$this->getRequest()) { throw new Exception( "You must set the Request to render a PhabricatorStandardPageView."); } $console = $this->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-zindex-css'); require_celerity_resource('phabricator-core-buttons-css'); require_celerity_resource('sprite-gradient-css'); require_celerity_resource('phabricator-standard-page-view'); Javelin::initBehavior('workflow', array()); $request = $this->getRequest(); $user = null; if ($request) { $user = $request->getUser(); } if ($user) { $default_img_uri = PhabricatorEnv::getCDNURI( '/rsrc/image/icon/fatcow/document_black.png' ); $download_form = phabricator_form( $user, array( 'action' => '#', 'method' => 'POST', 'class' => 'lightbox-download-form', 'sigil' => 'download', ), phutil_tag( 'button', array(), pht('Download'))); Javelin::initBehavior( 'lightbox-attachments', array( 'defaultImageUri' => $default_img_uri, 'downloadForm' => $download_form, )); } Javelin::initBehavior('aphront-form-disable-on-submit'); Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('konami', array()); $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); } Javelin::initBehavior( 'refresh-csrf', array( 'tokenName' => AphrontRequest::getCSRFTokenName(), 'header' => AphrontRequest::getCSRFHeaderName(), 'current' => $current_token, )); Javelin::initBehavior('device'); if ($console) { require_celerity_resource('aphront-dark-console-css'); $headers = array(); - if (DarkConsoleXHProfPluginAPI::isProfilerRequested()) { + if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } Javelin::initBehavior( 'dark-console', array( 'uri' => $request ? (string)$request->getRequestURI() : '?', 'selected' => $user ? $user->getConsoleTab() : null, 'visible' => $user ? (int)$user->getConsoleVisible() : true, 'headers' => $headers, )); // Change this to initBehavior when there is some behavior to initialize require_celerity_resource('javelin-behavior-error-log'); } $menu = id(new PhabricatorMainMenuView()) ->setUser($request->getUser()) ->setDefaultSearchScope($this->getSearchDefaultScope()); if ($this->getController()) { $menu->setController($this->getController()); } if ($this->getApplicationMenu()) { $menu->setApplicationMenu($this->getApplicationMenu()); } $this->menuContent = $menu->render(); } protected function getHead() { $monospaced = PhabricatorEnv::getEnvConfig('style.monospace'); $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user) { $monospaced = nonempty( $user->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MONOSPACED), $monospaced); } } $response = CelerityAPI::getStaticResourceResponse(); $head = array( parent::getHead(), '<style type="text/css">'. '.PhabricatorMonospaced { font: '.$monospaced.'; }'. '</style>', $response->renderSingleResource('javelin-magical-init'), ); return implode("\n", $head); } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $request = $this->getRequest(); $response = parent::willSendResponse($response); $console = $request->getApplicationConfiguration()->getConsole(); if ($console) { $response = str_replace( '<darkconsole />', $console->render($request), $response); } return $response; } protected function getBody() { $console = $this->getConsole(); $user = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); } $header_chrome = null; if ($this->getShowChrome()) { $header_chrome = $this->menuContent; } $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { $developer_warning = phutil_tag( 'div', array( 'class' => 'aphront-developer-error-callout', ), pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); } // Render the "you have unresolved setup issues..." warning. $setup_warning = null; if ($user && $user->getIsAdmin()) { $open = PhabricatorSetupCheck::getOpenSetupIssueCount(); if ($open) { $setup_warning = phutil_tag( 'div', array( 'class' => 'setup-warning-callout', ), phutil_tag( 'a', array( 'href' => '/config/issue/', ), pht('You have %d unresolved setup issue(s)...', $open))); } } return phutil_render_tag( 'div', array( 'id' => 'base-page', 'class' => 'phabricator-standard-page', ), $developer_warning. $setup_warning. $header_chrome. '<div class="phabricator-standard-page-body">'. ($console ? '<darkconsole />' : null). parent::getBody(). '<div style="clear: both;"></div>'. '</div>'); } protected function getTail() { $request = $this->getRequest(); $user = $request->getUser(); $container = null; if ($user->isLoggedIn()) { $aphlict_object_id = celerity_generate_unique_node_id(); $aphlict_container_id = celerity_generate_unique_node_id(); $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); $client_uri = new PhutilURI($client_uri); if ($client_uri->getDomain() == 'localhost') { $this_host = $this->getRequest()->getHost(); $this_host = new PhutilURI('http://'.$this_host.'/'); $client_uri->setDomain($this_host->getDomain()); } $enable_debug = PhabricatorEnv::getEnvConfig('notification.debug'); Javelin::initBehavior( 'aphlict-listen', array( 'id' => $aphlict_object_id, 'containerID' => $aphlict_container_id, 'server' => $client_uri->getDomain(), 'port' => $client_uri->getPort(), 'debug' => $enable_debug, 'pageObjects' => array_fill_keys($this->pageObjects, true), )); $container = phutil_tag( 'div', array( 'id' => $aphlict_container_id, 'style' => 'position: absolute; width: 0; height: 0;', ), ''); } $response = CelerityAPI::getStaticResourceResponse(); $tail = array( parent::getTail(), $container, $response->renderHTMLFooter(), ); return implode("\n", $tail); } protected function getBodyClasses() { $classes = array(); if (!$this->getShowChrome()) { $classes[] = 'phabricator-chromeless-page'; } $agent = AphrontRequest::getHTTPHeader('User-Agent'); // Try to guess the device resolution based on UA strings to avoid a flash // of incorrectly-styled content. $device_guess = 'device-desktop'; if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) { $device_guess = 'device-phone device'; } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) { $device_guess = 'device-tablet device'; } $classes[] = $device_guess; return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } } diff --git a/webroot/index.php b/webroot/index.php index 38e776f241..c6af0494f7 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,158 +1,140 @@ <?php require_once dirname(dirname(__FILE__)).'/support/PhabricatorStartup.php'; PhabricatorStartup::didStartup(); try { PhabricatorStartup::loadCoreLibraries(); PhabricatorEnv::initializeWebEnvironment(); // This is the earliest we can get away with this, we need env config first. PhabricatorAccessLog::init(); $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { PhabricatorStartup::setGlobal('log.access', $access_log); $access_log->setData( array( 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); } DarkConsoleXHProfPluginAPI::hookProfiler(); PhutilErrorHandler::setErrorListener( array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); $sink = new AphrontPHPHTTPSink(); $response = PhabricatorSetupCheck::willProcessRequest(); if ($response) { $sink->writeResponse($response); return; } $host = AphrontRequest::getHTTPHeader('Host'); $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); // Until an administrator sets "phabricator.base-uri", assume it is the same // as the request URI. This will work fine in most cases, it just breaks down // when daemons need to do things. $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); $request_base_uri = "{$request_protocol}://{$host}/"; PhabricatorEnv::setRequestBaseURI($request_base_uri); $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); if ($access_log) { $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), 'C' => get_class($controller), )); } // If execution throws an exception and then trying to render that exception // throws another exception, we want to show the original exception, as it is // likely the root cause of the rendering exception. $original_exception = null; try { $response = $controller->willBeginExecution(); if ($access_log) { if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), )); } } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $original_exception = $ex; $response = $application->handleException($ex); } try { $response = $controller->didProcessRequest($response); $response = $application->willSendResponse($response, $controller); $response->setRequest($request); $sink->writeResponse($response); } catch (Exception $ex) { $write_guard->dispose(); if ($access_log) { $access_log->write(); } if ($original_exception) { $ex = new PhutilAggregateException( "Multiple exceptions during processing and rendering.", array( $original_exception, $ex, )); } PhabricatorStartup::didFatal('[Rendering Exception] '.$ex->getMessage()); } $write_guard->dispose(); if ($access_log) { $request_start = PhabricatorStartup::getStartTime(); $access_log->setData( array( 'c' => $response->getHTTPResponseCode(), 'T' => (int)(1000000 * (microtime(true) - $request_start)), )); $access_log->write(); } - if (DarkConsoleXHProfPluginAPI::isProfilerRequested()) { - $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); - $profile_sample = id(new PhabricatorXHProfSample()) - ->setFilePHID($profile); - if (empty($_REQUEST['__profile__'])) { - $sample_rate = PhabricatorEnv::getEnvConfig('debug.profile-rate'); - } else { - $sample_rate = 0; - } - $profile_sample->setSampleRate($sample_rate); - if ($access_log) { - $profile_sample->setUsTotal($access_log->getData('T')) - ->setHostname($access_log->getData('h')) - ->setRequestPath($access_log->getData('U')) - ->setController($access_log->getData('C')) - ->setUserPHID($request->getUser()->getPHID()); - } - $profile_sample->save(); - } + DarkConsoleXHProfPluginAPI::saveProfilerSample($request, $access_log); } catch (Exception $ex) { PhabricatorStartup::didFatal("[Exception] ".$ex->getMessage()); }