Path: blob/master/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
12256 views
<?php12final class HarbormasterBuildLogRenderController3extends HarbormasterController {45public function shouldAllowPublic() {6return true;7}89public function handleRequest(AphrontRequest $request) {10$viewer = $this->getViewer();1112$id = $request->getURIData('id');1314$log = id(new HarbormasterBuildLogQuery())15->setViewer($viewer)16->withIDs(array($id))17->executeOne();18if (!$log) {19return new Aphront404Response();20}2122$highlight_range = $request->getURILineRange('lines', 1000);2324$log_size = $this->getTotalByteLength($log);2526$head_lines = $request->getInt('head');27if ($head_lines === null) {28$head_lines = 8;29}30$head_lines = min($head_lines, 1024);31$head_lines = max($head_lines, 0);3233$tail_lines = $request->getInt('tail');34if ($tail_lines === null) {35$tail_lines = 16;36}37$tail_lines = min($tail_lines, 1024);38$tail_lines = max($tail_lines, 0);3940$head_offset = $request->getInt('headOffset');41if ($head_offset === null) {42$head_offset = 0;43}4445$tail_offset = $request->getInt('tailOffset');46if ($tail_offset === null) {47$tail_offset = $log_size;48}4950// Figure out which ranges we're actually going to read. We'll read either51// one range (either just at the head, or just at the tail) or two ranges52// (one at the head and one at the tail).5354// This gets a little bit tricky because: the ranges may overlap; we just55// want to do one big read if there is only a little bit of text left56// between the ranges; we may not know where the tail range ends; and we57// can only read forward from line map markers, not from any arbitrary58// position in the file.5960$bytes_per_line = 140;61$body_lines = 8;6263$views = array();64if ($head_lines > 0) {65$views[] = array(66'offset' => $head_offset,67'lines' => $head_lines,68'direction' => 1,69'limit' => $tail_offset,70);71}7273if ($highlight_range) {74$highlight_views = $this->getHighlightViews(75$log,76$highlight_range,77$log_size);78foreach ($highlight_views as $highlight_view) {79$views[] = $highlight_view;80}81}8283if ($tail_lines > 0) {84$views[] = array(85'offset' => $tail_offset,86'lines' => $tail_lines,87'direction' => -1,88'limit' => $head_offset,89);90}9192$reads = $views;93foreach ($reads as $key => $read) {94$offset = $read['offset'];9596$lines = $read['lines'];9798$read_length = 0;99$read_length += ($lines * $bytes_per_line);100$read_length += ($body_lines * $bytes_per_line);101102$direction = $read['direction'];103if ($direction < 0) {104if ($offset > $read_length) {105$offset -= $read_length;106} else {107$read_length = $offset;108$offset = 0;109}110}111112$position = $log->getReadPosition($offset);113list($position_offset, $position_line) = $position;114$read_length += ($offset - $position_offset);115116$reads[$key]['fetchOffset'] = $position_offset;117$reads[$key]['fetchLength'] = $read_length;118$reads[$key]['fetchLine'] = $position_line;119}120121$reads = $this->mergeOverlappingReads($reads);122123foreach ($reads as $key => $read) {124$fetch_offset = $read['fetchOffset'];125$fetch_length = $read['fetchLength'];126if ($fetch_offset + $fetch_length > $log_size) {127$fetch_length = $log_size - $fetch_offset;128}129130$data = $log->loadData($fetch_offset, $fetch_length);131132$offset = $read['fetchOffset'];133$line = $read['fetchLine'];134$lines = $this->getLines($data);135$line_data = array();136foreach ($lines as $line_text) {137$length = strlen($line_text);138$line_data[] = array(139'offset' => $offset,140'length' => $length,141'line' => $line,142'data' => $line_text,143);144$line += 1;145$offset += $length;146}147148$reads[$key]['data'] = $data;149$reads[$key]['lines'] = $line_data;150}151152foreach ($views as $view_key => $view) {153$anchor_byte = $view['offset'];154155if ($view['direction'] < 0) {156$anchor_byte = $anchor_byte - 1;157}158159$data_key = null;160foreach ($reads as $read_key => $read) {161$s = $read['fetchOffset'];162$e = $s + $read['fetchLength'];163164if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {165$data_key = $read_key;166break;167}168}169170if ($data_key === null) {171throw new Exception(172pht('Unable to find fetch!'));173}174175$anchor_key = null;176foreach ($reads[$data_key]['lines'] as $line_key => $line) {177$s = $line['offset'];178$e = $s + $line['length'];179180if (($s <= $anchor_byte) && ($e > $anchor_byte)) {181$anchor_key = $line_key;182break;183}184}185186if ($anchor_key === null) {187throw new Exception(188pht(189'Unable to find lines.'));190}191192if ($view['direction'] > 0) {193$slice_offset = $anchor_key;194} else {195$slice_offset = max(0, $anchor_key - ($view['lines'] - 1));196}197$slice_length = $view['lines'];198199$views[$view_key] += array(200'sliceKey' => $data_key,201'sliceOffset' => $slice_offset,202'sliceLength' => $slice_length,203);204}205206foreach ($views as $view_key => $view) {207$slice_key = $view['sliceKey'];208$lines = array_slice(209$reads[$slice_key]['lines'],210$view['sliceOffset'],211$view['sliceLength']);212213$data_offset = null;214$data_length = null;215foreach ($lines as $line) {216if ($data_offset === null) {217$data_offset = $line['offset'];218}219$data_length += $line['length'];220}221222// If the view cursor starts in the middle of a line, we're going to223// strip part of the line.224$direction = $view['direction'];225if ($direction > 0) {226$view_offset = $view['offset'];227$view_length = $data_length;228if ($data_offset < $view_offset) {229$trim = ($view_offset - $data_offset);230$view_length -= $trim;231}232233$limit = $view['limit'];234if ($limit !== null) {235if ($limit < ($view_offset + $view_length)) {236$view_length = ($limit - $view_offset);237}238}239} else {240$view_offset = $data_offset;241$view_length = $data_length;242if ($data_offset + $data_length > $view['offset']) {243$view_length -= (($data_offset + $data_length) - $view['offset']);244}245246$limit = $view['limit'];247if ($limit !== null) {248if ($limit > $view_offset) {249$view_length -= ($limit - $view_offset);250$view_offset = $limit;251}252}253}254255$views[$view_key] += array(256'viewOffset' => $view_offset,257'viewLength' => $view_length,258);259}260261$views = $this->mergeOverlappingViews($views);262263foreach ($views as $view_key => $view) {264$slice_key = $view['sliceKey'];265$lines = array_slice(266$reads[$slice_key]['lines'],267$view['sliceOffset'],268$view['sliceLength']);269270$view_offset = $view['viewOffset'];271foreach ($lines as $line_key => $line) {272$line_offset = $line['offset'];273274if ($line_offset >= $view_offset) {275break;276}277278$trim = ($view_offset - $line_offset);279if ($trim && ($trim >= strlen($line['data']))) {280unset($lines[$line_key]);281continue;282}283284$line_data = substr($line['data'], $trim);285$lines[$line_key]['data'] = $line_data;286$lines[$line_key]['length'] = strlen($line_data);287$lines[$line_key]['offset'] += $trim;288break;289}290291$view_end = $view['viewOffset'] + $view['viewLength'];292foreach ($lines as $line_key => $line) {293$line_end = $line['offset'] + $line['length'];294if ($line_end <= $view_end) {295continue;296}297298$trim = ($line_end - $view_end);299if ($trim && ($trim >= strlen($line['data']))) {300unset($lines[$line_key]);301continue;302}303304$line_data = substr($line['data'], -$trim);305$lines[$line_key]['data'] = $line_data;306$lines[$line_key]['length'] = strlen($line_data);307}308309$views[$view_key]['viewData'] = $lines;310}311312$spacer = null;313$render = array();314315$head_view = head($views);316if ($head_view['viewOffset'] > $head_offset) {317$render[] = array(318'spacer' => true,319'head' => $head_offset,320'tail' => $head_view['viewOffset'],321);322}323324foreach ($views as $view) {325if ($spacer) {326$spacer['tail'] = $view['viewOffset'];327$render[] = $spacer;328}329330$render[] = $view;331332$spacer = array(333'spacer' => true,334'head' => ($view['viewOffset'] + $view['viewLength']),335);336}337338$tail_view = last($views);339if ($tail_view['viewOffset'] + $tail_view['viewLength'] < $tail_offset) {340$render[] = array(341'spacer' => true,342'head' => $tail_view['viewOffset'] + $tail_view['viewLength'],343'tail' => $tail_offset,344);345}346347$uri = $log->getURI();348349$rows = array();350foreach ($render as $range) {351if (isset($range['spacer'])) {352$rows[] = $this->renderExpandRow($range);353continue;354}355356$lines = $range['viewData'];357foreach ($lines as $line) {358$display_line = ($line['line'] + 1);359$display_text = ($line['data']);360361$row_attr = array();362if ($highlight_range) {363if (($display_line >= $highlight_range[0]) &&364($display_line <= $highlight_range[1])) {365$row_attr = array(366'class' => 'phabricator-source-highlight',367);368}369}370371$display_line = phutil_tag(372'a',373array(374'href' => $uri.'$'.$display_line,375'data-n' => $display_line,376),377'');378379$line_cell = phutil_tag('th', array(), $display_line);380$text_cell = phutil_tag('td', array(), $display_text);381382$rows[] = phutil_tag(383'tr',384$row_attr,385array(386$line_cell,387$text_cell,388));389}390}391392if ($log->getLive()) {393$last_view = last($views);394$last_line = last($last_view['viewData']);395if ($last_line) {396$last_offset = $last_line['offset'];397} else {398$last_offset = 0;399}400401$last_tail = $last_view['viewOffset'] + $last_view['viewLength'];402$show_live = ($last_tail === $log_size);403if ($show_live) {404$rows[] = $this->renderLiveRow($last_offset);405}406}407408$table = javelin_tag(409'table',410array(411'class' => 'harbormaster-log-table PhabricatorMonospaced',412'sigil' => 'phabricator-source',413'meta' => array(414'uri' => $log->getURI(),415),416),417$rows);418419// When this is a normal AJAX request, return the rendered log fragment420// in an AJAX payload.421if ($request->isAjax()) {422return id(new AphrontAjaxResponse())423->setContent(424array(425'markup' => hsprintf('%s', $table),426));427}428429// If the page is being accessed as a standalone page, present a430// readable version of the fragment for debugging.431432require_celerity_resource('harbormaster-css');433434$header = pht('Standalone Log Fragment');435436$render_view = id(new PHUIObjectBoxView())437->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)438->setHeaderText($header)439->appendChild($table);440441$page_view = id(new PHUITwoColumnView())442->setFooter($render_view);443444$crumbs = $this->buildApplicationCrumbs()445->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI())446->addTextCrumb(pht('Fragment'))447->setBorder(true);448449return $this->newPage()450->setTitle(451array(452pht('Build Log %d', $log->getID()),453pht('Standalone Fragment'),454))455->setCrumbs($crumbs)456->appendChild($page_view);457}458459private function getTotalByteLength(HarbormasterBuildLog $log) {460$total_bytes = $log->getByteLength();461if ($total_bytes) {462return (int)$total_bytes;463}464465// TODO: Remove this after enough time has passed for installs to run466// log rebuilds or decide they don't care about older logs.467468// Older logs don't have this data denormalized onto the log record unless469// an administrator has run `bin/harbormaster rebuild-log --all` or470// similar. Try to figure it out by summing up the size of each chunk.471472// Note that the log may also be legitimately empty and have actual size473// zero.474$chunk = new HarbormasterBuildLogChunk();475$conn = $chunk->establishConnection('r');476477$row = queryfx_one(478$conn,479'SELECT SUM(size) total FROM %T WHERE logID = %d',480$chunk->getTableName(),481$log->getID());482483return (int)$row['total'];484}485486private function getLines($data) {487$parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE);488489if (last($parts) === '') {490array_pop($parts);491}492493$lines = array();494for ($ii = 0; $ii < count($parts); $ii += 2) {495$line = $parts[$ii];496if (isset($parts[$ii + 1])) {497$line .= $parts[$ii + 1];498}499$lines[] = $line;500}501502return $lines;503}504505506private function mergeOverlappingReads(array $reads) {507// Find planned reads which will overlap and merge them into a single508// larger read.509510$uk = array_keys($reads);511$vk = array_keys($reads);512513foreach ($uk as $ukey) {514foreach ($vk as $vkey) {515// Don't merge a range into itself, even though they do technically516// overlap.517if ($ukey === $vkey) {518continue;519}520521$uread = idx($reads, $ukey);522if ($uread === null) {523continue;524}525526$vread = idx($reads, $vkey);527if ($vread === null) {528continue;529}530531$us = $uread['fetchOffset'];532$ue = $us + $uread['fetchLength'];533534$vs = $vread['fetchOffset'];535$ve = $vs + $vread['fetchLength'];536537if (($vs > $ue) || ($ve < $us)) {538continue;539}540541$min = min($us, $vs);542$max = max($ue, $ve);543544$reads[$ukey]['fetchOffset'] = $min;545$reads[$ukey]['fetchLength'] = ($max - $min);546$reads[$ukey]['fetchLine'] = min(547$uread['fetchLine'],548$vread['fetchLine']);549550unset($reads[$vkey]);551}552}553554return $reads;555}556557private function mergeOverlappingViews(array $views) {558$uk = array_keys($views);559$vk = array_keys($views);560561$body_lines = 8;562$body_bytes = ($body_lines * 140);563564foreach ($uk as $ukey) {565foreach ($vk as $vkey) {566if ($ukey === $vkey) {567continue;568}569570$uview = idx($views, $ukey);571if ($uview === null) {572continue;573}574575$vview = idx($views, $vkey);576if ($vview === null) {577continue;578}579580// If these views don't use the same line data, don't try to581// merge them.582if ($uview['sliceKey'] != $vview['sliceKey']) {583continue;584}585586// If these views are overlapping or separated by only a few bytes,587// merge them into a single view.588$us = $uview['viewOffset'];589$ue = $us + $uview['viewLength'];590591$vs = $vview['viewOffset'];592$ve = $vs + $vview['viewLength'];593594// Don't merge if one of the slices starts at a byte offset595// significantly after the other ends.596if (($vs > $ue + $body_bytes) || ($us > $ve + $body_bytes)) {597continue;598}599600$uss = $uview['sliceOffset'];601$use = $uss + $uview['sliceLength'];602603$vss = $vview['sliceOffset'];604$vse = $vss + $vview['sliceLength'];605606// Don't merge if one of the slices starts at a line offset607// significantly after the other ends.608if ($uss > ($vse + $body_lines) || $vss > ($use + $body_lines)) {609continue;610}611612// These views are overlapping or nearly overlapping, so we merge613// them. We merge views even if they aren't exactly adjacent since614// it's silly to render an "expand more" which only expands a couple615// of lines.616617$offset = min($us, $vs);618$length = max($ue, $ve) - $offset;619620$slice_offset = min($uss, $vss);621$slice_length = max($use, $vse) - $slice_offset;622623$views[$ukey] = array(624'viewOffset' => $offset,625'viewLength' => $length,626'sliceOffset' => $slice_offset,627'sliceLength' => $slice_length,628) + $views[$ukey];629630unset($views[$vkey]);631}632}633634return $views;635}636637private function renderExpandRow($range) {638639$icon_up = id(new PHUIIconView())640->setIcon('fa-chevron-up');641642$icon_down = id(new PHUIIconView())643->setIcon('fa-chevron-down');644645$up_text = array(646pht('Show More Above'),647' ',648$icon_up,649);650651$expand_up = javelin_tag(652'a',653array(654'sigil' => 'harbormaster-log-expand',655'meta' => array(656'headOffset' => $range['head'],657'tailOffset' => $range['tail'],658'head' => 128,659'tail' => 0,660),661),662$up_text);663664$mid_text = pht(665'Show More (%s Bytes)',666new PhutilNumber($range['tail'] - $range['head']));667668$expand_mid = javelin_tag(669'a',670array(671'sigil' => 'harbormaster-log-expand',672'meta' => array(673'headOffset' => $range['head'],674'tailOffset' => $range['tail'],675'head' => 128,676'tail' => 128,677),678),679$mid_text);680681$down_text = array(682$icon_down,683' ',684pht('Show More Below'),685);686687$expand_down = javelin_tag(688'a',689array(690'sigil' => 'harbormaster-log-expand',691'meta' => array(692'headOffset' => $range['head'],693'tailOffset' => $range['tail'],694'head' => 0,695'tail' => 128,696),697),698$down_text);699700$expand_cells = array(701phutil_tag(702'td',703array(704'class' => 'harbormaster-log-expand-up',705),706$expand_up),707phutil_tag(708'td',709array(710'class' => 'harbormaster-log-expand-mid',711),712$expand_mid),713phutil_tag(714'td',715array(716'class' => 'harbormaster-log-expand-down',717),718$expand_down),719);720721return $this->renderActionTable($expand_cells);722}723724private function renderLiveRow($log_size) {725$icon_down = id(new PHUIIconView())726->setIcon('fa-angle-double-down');727728$icon_pause = id(new PHUIIconView())729->setIcon('fa-pause');730731$follow = javelin_tag(732'a',733array(734'sigil' => 'harbormaster-log-expand harbormaster-log-live',735'class' => 'harbormaster-log-follow-start',736'meta' => array(737'headOffset' => $log_size,738'head' => 0,739'tail' => 1024,740'live' => true,741),742),743array(744$icon_down,745' ',746pht('Follow Log'),747));748749$stop_following = javelin_tag(750'a',751array(752'sigil' => 'harbormaster-log-expand',753'class' => 'harbormaster-log-follow-stop',754'meta' => array(755'stop' => true,756),757),758array(759$icon_pause,760' ',761pht('Stop Following Log'),762));763764$expand_cells = array(765phutil_tag(766'td',767array(768'class' => 'harbormaster-log-follow',769),770array(771$follow,772$stop_following,773)),774);775776return $this->renderActionTable($expand_cells);777}778779private function renderActionTable(array $action_cells) {780$action_row = phutil_tag('tr', array(), $action_cells);781782$action_table = phutil_tag(783'table',784array(785'class' => 'harbormaster-log-expand-table',786),787$action_row);788789$format_cells = array(790phutil_tag('th', array()),791phutil_tag(792'td',793array(794'class' => 'harbormaster-log-expand-cell',795),796$action_table),797);798799return phutil_tag('tr', array(), $format_cells);800}801802private function getHighlightViews(803HarbormasterBuildLog $log,804array $range,805$log_size) {806// If we're highlighting a line range in the file, we first need to figure807// out the offsets for the lines we care about.808list($range_min, $range_max) = $range;809810// Read the markers to find a range we can load which includes both lines.811$read_range = $log->getLineSpanningRange($range_min, $range_max);812list($min_pos, $max_pos, $min_line) = $read_range;813814$length = ($max_pos - $min_pos);815816// Reject to do the read if it requires us to examine a huge amount of817// data. For example, the user may request lines "$1-1000" of a file where818// each line has 100MB of text.819$limit = (1024 * 1024 * 16);820if ($length > $limit) {821return array();822}823824$data = $log->loadData($min_pos, $length);825826$offset = $min_pos;827$min_offset = null;828$max_offset = null;829830$lines = $this->getLines($data);831$number = ($min_line + 1);832833foreach ($lines as $line) {834if ($min_offset === null) {835if ($number === $range_min) {836$min_offset = $offset;837}838}839840$offset += strlen($line);841842if ($max_offset === null) {843if ($number === $range_max) {844$max_offset = $offset;845break;846}847}848849$number += 1;850}851852$context_lines = 8;853854// Build views around the beginning and ends of the respective lines. We855// expect these views to overlap significantly in normal circumstances856// and be merged later.857$views = array();858859if ($min_offset !== null) {860$views[] = array(861'offset' => $min_offset,862'lines' => $context_lines + ($range_max - $range_min) - 1,863'direction' => 1,864'limit' => null,865);866if ($min_offset > 0) {867$views[] = array(868'offset' => $min_offset,869'lines' => $context_lines,870'direction' => -1,871'limit' => null,872);873}874}875876if ($max_offset !== null) {877$views[] = array(878'offset' => $max_offset,879'lines' => $context_lines + ($range_max - $range_min),880'direction' => -1,881'limit' => null,882);883if ($max_offset < $log_size) {884$views[] = array(885'offset' => $max_offset,886'lines' => $context_lines,887'direction' => 1,888'limit' => null,889);890}891}892893return $views;894}895896}897898899