diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php index 50b17523ca..4b36ba83d9 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php @@ -1,668 +1,674 @@ getViewer(); $id = $request->getURIData('id'); $log = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$log) { return new Aphront404Response(); } $log_size = $this->getTotalByteLength($log); $head_lines = $request->getInt('head'); if ($head_lines === null) { $head_lines = 8; } $head_lines = min($head_lines, 100); $head_lines = max($head_lines, 0); $tail_lines = $request->getInt('tail'); if ($tail_lines === null) { $tail_lines = 16; } $tail_lines = min($tail_lines, 100); $tail_lines = max($tail_lines, 0); $head_offset = $request->getInt('headOffset'); if ($head_offset === null) { $head_offset = 0; } $tail_offset = $request->getInt('tailOffset'); if ($tail_offset === null) { $tail_offset = $log_size; } // Figure out which ranges we're actually going to read. We'll read either // one range (either just at the head, or just at the tail) or two ranges // (one at the head and one at the tail). // This gets a little bit tricky because: the ranges may overlap; we just // want to do one big read if there is only a little bit of text left // between the ranges; we may not know where the tail range ends; and we // can only read forward from line map markers, not from any arbitrary // position in the file. $bytes_per_line = 140; $body_lines = 8; $views = array(); if ($head_lines > 0) { $views[] = array( 'offset' => $head_offset, 'lines' => $head_lines, 'direction' => 1, 'limit' => $tail_offset, ); } if ($tail_lines > 0) { $views[] = array( 'offset' => $tail_offset, 'lines' => $tail_lines, 'direction' => -1, 'limit' => $head_offset, ); } $reads = $views; foreach ($reads as $key => $read) { $offset = $read['offset']; $lines = $read['lines']; $read_length = 0; $read_length += ($lines * $bytes_per_line); $read_length += ($body_lines * $bytes_per_line); $direction = $read['direction']; if ($direction < 0) { $offset -= $read_length; if ($offset < 0) { $offset = 0; $read_length = $log_size; } } $position = $log->getReadPosition($offset); list($position_offset, $position_line) = $position; $read_length += ($offset - $position_offset); $reads[$key]['fetchOffset'] = $position_offset; $reads[$key]['fetchLength'] = $read_length; $reads[$key]['fetchLine'] = $position_line; } $reads = $this->mergeOverlappingReads($reads); foreach ($reads as $key => $read) { - $data = $log->loadData($read['fetchOffset'], $read['fetchLength']); + $fetch_offset = $read['fetchOffset']; + $fetch_length = $read['fetchLength']; + if ($fetch_offset + $fetch_length > $log_size) { + $fetch_length = $log_size - $fetch_offset; + } + + $data = $log->loadData($fetch_offset, $fetch_length); $offset = $read['fetchOffset']; $line = $read['fetchLine']; $lines = $this->getLines($data); $line_data = array(); foreach ($lines as $line_text) { $length = strlen($line_text); $line_data[] = array( 'offset' => $offset, 'length' => $length, 'line' => $line, 'data' => $line_text, ); $line += 1; $offset += $length; } $reads[$key]['data'] = $data; $reads[$key]['lines'] = $line_data; } foreach ($views as $view_key => $view) { $anchor_byte = $view['offset']; if ($view['direction'] < 0) { $anchor_byte = $anchor_byte - 1; } $data_key = null; foreach ($reads as $read_key => $read) { $s = $read['fetchOffset']; $e = $s + $read['fetchLength']; if (($s <= $anchor_byte) && ($e >= $anchor_byte)) { $data_key = $read_key; break; } } if ($data_key === null) { throw new Exception( pht('Unable to find fetch!')); } $anchor_key = null; foreach ($reads[$data_key]['lines'] as $line_key => $line) { $s = $line['offset']; $e = $s + $line['length']; if (($s <= $anchor_byte) && ($e > $anchor_byte)) { $anchor_key = $line_key; break; } } if ($anchor_key === null) { throw new Exception( pht( 'Unable to find lines.')); } if ($view['direction'] > 0) { $slice_offset = $anchor_key; } else { $slice_offset = max(0, $anchor_key - ($view['lines'] - 1)); } $slice_length = $view['lines']; $views[$view_key] += array( 'sliceKey' => $data_key, 'sliceOffset' => $slice_offset, 'sliceLength' => $slice_length, ); } foreach ($views as $view_key => $view) { $slice_key = $view['sliceKey']; $lines = array_slice( $reads[$slice_key]['lines'], $view['sliceOffset'], $view['sliceLength']); $data_offset = null; $data_length = null; foreach ($lines as $line) { if ($data_offset === null) { $data_offset = $line['offset']; } $data_length += $line['length']; } // If the view cursor starts in the middle of a line, we're going to // strip part of the line. $direction = $view['direction']; if ($direction > 0) { $view_offset = $view['offset']; $view_length = $data_length; if ($data_offset < $view_offset) { $trim = ($view_offset - $data_offset); $view_length -= $trim; } $limit = $view['limit']; if ($limit < ($view_offset + $view_length)) { $view_length = ($limit - $view_offset); } } else { $view_offset = $data_offset; $view_length = $data_length; if ($data_offset + $data_length > $view['offset']) { $view_length -= (($data_offset + $data_length) - $view['offset']); } $limit = $view['limit']; if ($limit > $view_offset) { $view_length -= ($limit - $view_offset); $view_offset = $limit; } } $views[$view_key] += array( 'viewOffset' => $view_offset, 'viewLength' => $view_length, ); } $views = $this->mergeOverlappingViews($views); foreach ($views as $view_key => $view) { $slice_key = $view['sliceKey']; $lines = array_slice( $reads[$slice_key]['lines'], $view['sliceOffset'], $view['sliceLength']); $view_offset = $view['viewOffset']; foreach ($lines as $line_key => $line) { $line_offset = $line['offset']; if ($line_offset >= $view_offset) { break; } $trim = ($view_offset - $line_offset); if ($trim && ($trim >= strlen($line['data']))) { unset($lines[$line_key]); continue; } $line_data = substr($line['data'], $trim); $lines[$line_key]['data'] = $line_data; $lines[$line_key]['length'] = strlen($line_data); $lines[$line_key]['offset'] += $trim; break; } $view_end = $view['viewOffset'] + $view['viewLength']; foreach ($lines as $line_key => $line) { $line_end = $line['offset'] + $line['length']; if ($line_end <= $view_end) { continue; } $trim = ($line_end - $view_end); if ($trim && ($trim >= strlen($line['data']))) { unset($lines[$line_key]); continue; } $line_data = substr($line['data'], -$trim); $lines[$line_key]['data'] = $line_data; $lines[$line_key]['length'] = strlen($line_data); } $views[$view_key]['viewData'] = $lines; } $spacer = null; $render = array(); $head_view = head($views); if ($head_view['viewOffset'] > $head_offset) { $render[] = array( 'spacer' => true, 'head' => $head_offset, 'tail' => $head_view['viewOffset'], ); } foreach ($views as $view) { if ($spacer) { $spacer['tail'] = $view['viewOffset']; $render[] = $spacer; } $render[] = $view; $spacer = array( 'spacer' => true, 'head' => ($view['viewOffset'] + $view['viewLength']), ); } $tail_view = last($views); if ($tail_view['viewOffset'] + $tail_view['viewLength'] < $tail_offset) { $render[] = array( 'spacer' => true, 'head' => $tail_view['viewOffset'] + $tail_view['viewLength'], 'tail' => $tail_offset, ); } $uri = $log->getURI(); $highlight_range = $request->getURIData('lines'); $rows = array(); foreach ($render as $range) { if (isset($range['spacer'])) { $rows[] = $this->renderExpandRow($range); continue; } $lines = $range['viewData']; foreach ($lines as $line) { $display_line = ($line['line'] + 1); $display_text = ($line['data']); $display_line = phutil_tag( 'a', array( 'href' => $uri.'$'.$display_line, ), $display_line); $line_cell = phutil_tag('th', array(), $display_line); $text_cell = phutil_tag('td', array(), $display_text); $rows[] = phutil_tag( 'tr', array(), array( $line_cell, $text_cell, )); } } $table = phutil_tag( 'table', array( 'class' => 'harbormaster-log-table PhabricatorMonospaced', ), $rows); // When this is a normal AJAX request, return the rendered log fragment // in an AJAX payload. if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'markup' => hsprintf('%s', $table), )); } // If the page is being accessed as a standalone page, present a // readable version of the fragment for debugging. require_celerity_resource('harbormaster-css'); $header = pht('Standalone Log Fragment'); $render_view = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setHeaderText($header) ->appendChild($table); $page_view = id(new PHUITwoColumnView()) ->setFooter($render_view); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI()) ->addTextCrumb(pht('Fragment')) ->setBorder(true); return $this->newPage() ->setTitle( array( pht('Build Log %d', $log->getID()), pht('Standalone Fragment'), )) ->setCrumbs($crumbs) ->appendChild($page_view); } private function getTotalByteLength(HarbormasterBuildLog $log) { $total_bytes = $log->getByteLength(); if ($total_bytes) { return (int)$total_bytes; } // TODO: Remove this after enough time has passed for installs to run // log rebuilds or decide they don't care about older logs. // Older logs don't have this data denormalized onto the log record unless // an administrator has run `bin/harbormaster rebuild-log --all` or // similar. Try to figure it out by summing up the size of each chunk. // Note that the log may also be legitimately empty and have actual size // zero. $chunk = new HarbormasterBuildLogChunk(); $conn = $chunk->establishConnection('r'); $row = queryfx_one( $conn, 'SELECT SUM(size) total FROM %T WHERE logID = %d', $chunk->getTableName(), $log->getID()); return (int)$row['total']; } private function getLines($data) { $parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE); if (last($parts) === '') { array_pop($parts); } $lines = array(); for ($ii = 0; $ii < count($parts); $ii += 2) { $line = $parts[$ii]; if (isset($parts[$ii + 1])) { $line .= $parts[$ii + 1]; } $lines[] = $line; } return $lines; } private function mergeOverlappingReads(array $reads) { // Find planned reads which will overlap and merge them into a single // larger read. $uk = array_keys($reads); $vk = array_keys($reads); foreach ($uk as $ukey) { foreach ($vk as $vkey) { // Don't merge a range into itself, even though they do technically // overlap. if ($ukey === $vkey) { continue; } $uread = idx($reads, $ukey); if ($uread === null) { continue; } $vread = idx($reads, $vkey); if ($vread === null) { continue; } $us = $uread['fetchOffset']; $ue = $us + $uread['fetchLength']; $vs = $vread['fetchOffset']; $ve = $vs + $vread['fetchLength']; if (($vs > $ue) || ($ve < $us)) { continue; } $min = min($us, $vs); $max = max($ue, $ve); $reads[$ukey]['fetchOffset'] = $min; $reads[$ukey]['fetchLength'] = ($max - $min); $reads[$ukey]['fetchLine'] = min( $uread['fetchLine'], $vread['fetchLine']); unset($reads[$vkey]); } } return $reads; } private function mergeOverlappingViews(array $views) { $uk = array_keys($views); $vk = array_keys($views); $body_lines = 8; $body_bytes = ($body_lines * 140); foreach ($uk as $ukey) { foreach ($vk as $vkey) { if ($ukey === $vkey) { continue; } $uview = idx($views, $ukey); if ($uview === null) { continue; } $vview = idx($views, $vkey); if ($vview === null) { continue; } // If these views don't use the same line data, don't try to // merge them. if ($uview['sliceKey'] != $vview['sliceKey']) { continue; } // If these views are overlapping or separated by only a few bytes, // merge them into a single view. $us = $uview['viewOffset']; $ue = $us + $uview['viewLength']; $vs = $vview['viewOffset']; $ve = $vs + $vview['viewLength']; $uss = $uview['sliceOffset']; $use = $uss + $uview['sliceLength']; $vss = $vview['sliceOffset']; $vse = $vss + $vview['sliceLength']; if ($ue <= $vs) { if (($ue + $body_bytes) >= $vs) { if (($use + $body_lines) >= $vss) { $views[$ukey] = array( 'sliceLength' => ($vse - $uss), 'viewLength' => ($ve - $us), ) + $views[$ukey]; unset($views[$vkey]); continue; } } } } } return $views; } private function renderExpandRow($range) { $icon_up = id(new PHUIIconView()) ->setIcon('fa-chevron-up'); $icon_down = id(new PHUIIconView()) ->setIcon('fa-chevron-down'); $up_text = array( pht('Show More Above'), ' ', $icon_up, ); $expand_up = javelin_tag( 'a', array( 'sigil' => 'harbormaster-log-expand', 'meta' => array( 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], 'head' => 4, 'tail' => 0, ), ), $up_text); $mid_text = pht( 'Show More (%s Bytes)', new PhutilNumber($range['tail'] - $range['head'])); $expand_mid = javelin_tag( 'a', array( 'sigil' => 'harbormaster-log-expand', 'meta' => array( 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], 'head' => 2, 'tail' => 2, ), ), $mid_text); $down_text = array( $icon_down, ' ', pht('Show More Below'), ); $expand_down = javelin_tag( 'a', array( 'sigil' => 'harbormaster-log-expand', 'meta' => array( 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], 'head' => 0, 'tail' => 4, ), ), $down_text); $expand_cells = array( phutil_tag( 'td', array( 'class' => 'harbormaster-log-expand-up', ), $expand_up), phutil_tag( 'td', array( 'class' => 'harbormaster-log-expand-mid', ), $expand_mid), phutil_tag( 'td', array( 'class' => 'harbormaster-log-expand-down', ), $expand_down), ); $expand_row = phutil_tag('tr', array(), $expand_cells); $expand_table = phutil_tag( 'table', array( 'class' => 'harbormaster-log-expand-table', ), $expand_row); $cells = array( phutil_tag('th', array()), phutil_tag( 'td', array( 'class' => 'harbormaster-log-expand-cell', ), $expand_table), ); return phutil_tag('tr', array(), $cells); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php index 41f2aef8e5..40641795c5 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php @@ -1,44 +1,50 @@ getViewer(); $id = $request->getURIData('id'); $log = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$log) { return new Aphront404Response(); } + $target = $log->getBuildTarget(); + $build = $target->getBuild(); + $page_title = pht('Build Log %d', $log->getID()); $log_view = id(new HarbormasterBuildLogView()) ->setViewer($viewer) ->setBuildLog($log) ->setHighlightedLineRange($request->getURIData('lines')); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb(pht('Build Logs')) + ->addTextCrumb( + pht('Build %d', $build->getID()), + $build->getURI()) ->addTextCrumb($page_title) ->setBorder(true); $page_header = id(new PHUIHeaderView()) ->setHeader($page_title); $page_view = id(new PHUITwoColumnView()) ->setHeader($page_header) ->setFooter($log_view); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->appendChild($page_view); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 4507139d47..0e09b4d658 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -1,667 +1,674 @@ getRequest(); $viewer = $request->getUser(); $id = $request->getURIData('id'); $generation = $request->getInt('g'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$build) { return new Aphront404Response(); } require_celerity_resource('harbormaster-css'); $title = pht('Build %d', $id); $page_header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($build) ->setHeaderIcon('fa-cubes'); if ($build->isRestarting()) { $page_header->setStatus( 'fa-exclamation-triangle', 'red', pht('Restarting')); } else if ($build->isPausing()) { $page_header->setStatus( 'fa-exclamation-triangle', 'red', pht('Pausing')); } else if ($build->isResuming()) { $page_header->setStatus( 'fa-exclamation-triangle', 'red', pht('Resuming')); } else if ($build->isAborting()) { $page_header->setStatus( 'fa-exclamation-triangle', 'red', pht('Aborting')); } $curtain = $this->buildCurtainView($build); $properties = $this->buildPropertyList($build); $crumbs = $this->buildApplicationCrumbs(); $this->addBuildableCrumb($crumbs, $build->getBuildable()); $crumbs->addTextCrumb($title); $crumbs->setBorder(true); if ($generation === null || $generation > $build->getBuildGeneration() || $generation < 0) { $generation = $build->getBuildGeneration(); } $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->needBuildSteps(true) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($generation)) ->execute(); if ($build_targets) { $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(mpull($build_targets, 'getPHID')) ->execute(); $messages = mgroup($messages, 'getReceiverPHID'); } else { $messages = array(); } if ($build_targets) { $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) ->execute(); $artifacts = msort($artifacts, 'getArtifactKey'); $artifacts = mgroup($artifacts, 'getBuildTargetPHID'); } else { $artifacts = array(); } $targets = array(); foreach ($build_targets as $build_target) { $header = id(new PHUIHeaderView()) ->setHeader($build_target->getName()) ->setUser($viewer) ->setHeaderIcon('fa-bullseye'); $target_box = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setHeader($header); $tab_group = new PHUITabGroupView(); $target_box->addTabGroup($tab_group); $property_list = new PHUIPropertyListView(); $target_artifacts = idx($artifacts, $build_target->getPHID(), array()); $links = array(); $type_uri = HarbormasterURIArtifact::ARTIFACTCONST; foreach ($target_artifacts as $artifact) { if ($artifact->getArtifactType() == $type_uri) { $impl = $artifact->getArtifactImplementation(); if ($impl->isExternalLink()) { $links[] = $impl->renderLink(); } } } if ($links) { $links = phutil_implode_html(phutil_tag('br'), $links); $property_list->addProperty( pht('External Link'), $links); } $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); $status = $build_target->getTargetStatus(); $status_name = HarbormasterBuildTarget::getBuildTargetStatusName($status); $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); $when = array(); $started = $build_target->getDateStarted(); $now = PhabricatorTime::getNow(); if ($started) { $ended = $build_target->getDateCompleted(); if ($ended) { $when[] = pht( 'Completed at %s', phabricator_datetime($ended, $viewer)); $duration = ($ended - $started); if ($duration) { $when[] = pht( 'Built for %s', phutil_format_relative_time_detailed($duration)); } else { $when[] = pht('Built instantly'); } } else { $when[] = pht( 'Started at %s', phabricator_datetime($started, $viewer)); $duration = ($now - $started); if ($duration) { $when[] = pht( 'Running for %s', phutil_format_relative_time_detailed($duration)); } } } else { $created = $build_target->getDateCreated(); $when[] = pht( 'Queued at %s', phabricator_datetime($started, $viewer)); $duration = ($now - $created); if ($duration) { $when[] = pht( 'Waiting for %s', phutil_format_relative_time_detailed($duration)); } } $property_list->addProperty( pht('When'), phutil_implode_html(" \xC2\xB7 ", $when)); $property_list->addProperty(pht('Status'), $status_view); $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Overview')) ->setKey('overview') ->appendChild($property_list)); $step = $build_target->getBuildStep(); if ($step) { $description = $step->getDescription(); if ($description) { $description = new PHUIRemarkupView($viewer, $description); $property_list->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $property_list->addTextContent($description); } } else { $target_box->setFormErrors( array( pht( 'This build step has since been deleted on the build plan. '. 'Some information may be omitted.'), )); } $details = $build_target->getDetails(); $property_list = new PHUIPropertyListView(); foreach ($details as $key => $value) { $property_list->addProperty($key, $value); } $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Configuration')) ->setKey('configuration') ->appendChild($property_list)); $variables = $build_target->getVariables(); $variables_tab = $this->buildProperties($variables); $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Variables')) ->setKey('variables') ->appendChild($variables_tab)); $artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts); $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Artifacts')) ->setKey('artifacts') ->appendChild($artifacts_tab)); $build_messages = idx($messages, $build_target->getPHID(), array()); $messages_tab = $this->buildMessages($build_messages); $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Messages')) ->setKey('messages') ->appendChild($messages_tab)); $property_list = new PHUIPropertyListView(); $property_list->addProperty( pht('Build Target ID'), $build_target->getID()); $property_list->addProperty( pht('Build Target PHID'), $build_target->getPHID()); $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Metadata')) ->setKey('metadata') ->appendChild($property_list)); $targets[] = $target_box; $targets[] = $this->buildLog($build, $build_target); } $timeline = $this->buildTransactionTimeline( $build, new HarbormasterBuildTransactionQuery()); $timeline->setShouldTerminate(true); $view = id(new PHUITwoColumnView()) ->setHeader($page_header) ->setCurtain($curtain) ->setMainColumn(array( $properties, $targets, $timeline, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function buildArtifacts( HarbormasterBuildTarget $build_target, array $artifacts) { $viewer = $this->getViewer(); $rows = array(); foreach ($artifacts as $artifact) { $impl = $artifact->getArtifactImplementation(); if ($impl) { $summary = $impl->renderArtifactSummary($viewer); $type_name = $impl->getArtifactTypeName(); } else { $summary = pht(''); $type_name = $artifact->getType(); } $rows[] = array( $artifact->getArtifactKey(), $type_name, $summary, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This target has no associated artifacts.')) ->setHeaders( array( pht('Key'), pht('Type'), pht('Summary'), )) ->setColumnClasses( array( 'pri', '', 'wide', )); return $table; } private function buildLog( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $limit = $request->getInt('l', 25); $logs = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); $empty_logs = array(); $log_boxes = array(); foreach ($logs as $log) { $start = 1; $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); if ($limit !== 0) { $start = count($lines) - $limit; if ($start >= 1) { $lines = array_slice($lines, -$limit, $limit); } else { $start = 1; } } $id = null; $is_empty = false; if (count($lines) === 1 && trim($lines[0]) === '') { // Prevent Harbormaster from showing empty build logs. $id = celerity_generate_unique_node_id(); $empty_logs[] = $id; $is_empty = true; } $log_view = new ShellLogView(); $log_view->setLines($lines); $log_view->setStart($start); + $prototype_view = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($log->getURI()) + ->setIcon('fa-file-text-o') + ->setText(pht('New View (Prototype)')); + $header = id(new PHUIHeaderView()) ->setHeader(pht( 'Build Log %d (%s - %s)', $log->getID(), $log->getLogSource(), $log->getLogType())) + ->addActionLink($prototype_view) ->setSubheader($this->createLogHeader($build, $log)) ->setUser($viewer); $log_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($log_view); if ($is_empty) { $log_box = phutil_tag( 'div', array( 'style' => 'display: none', 'id' => $id, ), $log_box); } $log_boxes[] = $log_box; } if ($empty_logs) { $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); $expand = phutil_tag( 'div', array( 'id' => $hide_id, 'class' => 'harbormaster-empty-logs-are-hidden', ), array( pht( '%s empty logs are hidden.', phutil_count($empty_logs)), ' ', javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => $empty_logs, 'hideIDs' => array($hide_id), ), ), pht('Show all logs.')), )); array_unshift($log_boxes, $expand); } return $log_boxes; } private function createLogHeader($build, $log) { $request = $this->getRequest(); $limit = $request->getInt('l', 25); $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); $lines_100 = $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); if ($limit === 25) { $link_25 = phutil_tag('strong', array(), $link_25); } else if ($limit === 50) { $link_50 = phutil_tag('strong', array(), $link_50); } else if ($limit === 100) { $link_100 = phutil_tag('strong', array(), $link_100); } else if ($limit === 0) { $link_0 = phutil_tag('strong', array(), $link_0); } return phutil_tag( 'span', array(), array( $link_25, ' - ', $link_50, ' - ', $link_100, ' - ', $link_0, ' Lines', )); } private function buildCurtainView(HarbormasterBuild $build) { $viewer = $this->getViewer(); $id = $build->getID(); $curtain = $this->newCurtainView($build); $can_restart = $build->canRestartBuild() && $build->canIssueCommand( $viewer, HarbormasterBuildCommand::COMMAND_RESTART); $can_pause = $build->canPauseBuild() && $build->canIssueCommand( $viewer, HarbormasterBuildCommand::COMMAND_PAUSE); $can_resume = $build->canResumeBuild() && $build->canIssueCommand( $viewer, HarbormasterBuildCommand::COMMAND_RESUME); $can_abort = $build->canAbortBuild() && $build->canIssueCommand( $viewer, HarbormasterBuildCommand::COMMAND_ABORT); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Restart Build')) ->setIcon('fa-repeat') ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) ->setDisabled(!$can_restart) ->setWorkflow(true)); if ($build->canResumeBuild()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Resume Build')) ->setIcon('fa-play') ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) ->setDisabled(!$can_resume) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Pause Build')) ->setIcon('fa-pause') ->setHref($this->getApplicationURI('/build/pause/'.$id.'/')) ->setDisabled(!$can_pause) ->setWorkflow(true)); } $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Abort Build')) ->setIcon('fa-exclamation-triangle') ->setHref($this->getApplicationURI('/build/abort/'.$id.'/')) ->setDisabled(!$can_abort) ->setWorkflow(true)); return $curtain; } private function buildPropertyList(HarbormasterBuild $build) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array( $build->getBuildablePHID(), $build->getBuildPlanPHID(), )) ->execute(); $properties->addProperty( pht('Buildable'), $handles[$build->getBuildablePHID()]->renderLink()); $properties->addProperty( pht('Build Plan'), $handles[$build->getBuildPlanPHID()]->renderLink()); $properties->addProperty( pht('Restarts'), $build->getBuildGeneration()); $properties->addProperty( pht('Status'), $this->getStatus($build)); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } private function getStatus(HarbormasterBuild $build) { $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); if ($build->isPausing()) { $status_name = pht('Pausing'); $icon = PHUIStatusItemView::ICON_RIGHT; $color = 'dark'; } else { $status = $build->getBuildStatus(); $status_name = HarbormasterBuildStatus::getBuildStatusName($status); $icon = HarbormasterBuildStatus::getBuildStatusIcon($status); $color = HarbormasterBuildStatus::getBuildStatusColor($status); } $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); return $status_view; } private function buildMessages(array $messages) { $viewer = $this->getRequest()->getUser(); if ($messages) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($messages, 'getAuthorPHID')) ->execute(); } else { $handles = array(); } $rows = array(); foreach ($messages as $message) { $rows[] = array( $message->getID(), $handles[$message->getAuthorPHID()]->renderLink(), $message->getType(), $message->getIsConsumed() ? pht('Consumed') : null, phabricator_datetime($message->getDateCreated(), $viewer), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht('No messages for this build target.')); $table->setHeaders( array( pht('ID'), pht('From'), pht('Type'), pht('Consumed'), pht('Received'), )); $table->setColumnClasses( array( '', '', 'wide', '', 'date', )); return $table; } private function buildProperties(array $properties) { ksort($properties); $rows = array(); foreach ($properties as $key => $value) { $rows[] = array( $key, $value, ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Value'), )) ->setColumnClasses( array( 'pri right', 'wide', )); return $table; } } diff --git a/src/applications/harbormaster/view/HarbormasterBuildLogView.php b/src/applications/harbormaster/view/HarbormasterBuildLogView.php index 81705f3f49..e240aab510 100644 --- a/src/applications/harbormaster/view/HarbormasterBuildLogView.php +++ b/src/applications/harbormaster/view/HarbormasterBuildLogView.php @@ -1,73 +1,88 @@ log = $log; return $this; } public function getBuildLog() { return $this->log; } public function setHighlightedLineRange($range) { $this->highlightedLineRange = $range; return $this; } public function getHighlightedLineRange() { return $this->highlightedLineRange; } public function render() { $viewer = $this->getViewer(); $log = $this->getBuildLog(); $id = $log->getID(); $header = id(new PHUIHeaderView()) ->setViewer($viewer) ->setHeader(pht('Build Log %d', $id)); $download_uri = "/harbormaster/log/download/{$id}/"; $download_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($download_uri) ->setIcon('fa-download') ->setDisabled(!$log->getFilePHID()) ->setWorkflow(true) ->setText(pht('Download Log')); $header->addActionLink($download_button); - $content_id = celerity_generate_unique_node_id(); - $content_div = javelin_tag( - 'div', - array( - 'id' => $content_id, - 'class' => 'harbormaster-log-view-loading', - ), - pht('Loading...')); - - require_celerity_resource('harbormaster-css'); - - Javelin::initBehavior( - 'harbormaster-log', - array( - 'contentNodeID' => $content_id, - 'renderURI' => $log->getRenderURI($this->getHighlightedLineRange()), - )); - $box_view = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setHeader($header) - ->appendChild($content_div); + ->setHeader($header); + + $has_linemap = $log->getLineMap(); + if ($has_linemap) { + $content_id = celerity_generate_unique_node_id(); + $content_div = javelin_tag( + 'div', + array( + 'id' => $content_id, + 'class' => 'harbormaster-log-view-loading', + ), + pht('Loading...')); + + require_celerity_resource('harbormaster-css'); + + Javelin::initBehavior( + 'harbormaster-log', + array( + 'contentNodeID' => $content_id, + 'renderURI' => $log->getRenderURI($this->getHighlightedLineRange()), + )); + + $box_view->appendChild($content_div); + } else { + $box_view->setFormErrors( + array( + pht( + 'This older log is missing required rendering data. To rebuild '. + 'rendering data, run: %s', + phutil_tag( + 'tt', + array(), + '$ bin/harbormaster rebuild-log --force --id '.$log->getID())), + )); + } return $box_view; } }