mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +01:00
Sort of make Harbormaster build logs page properly
Summary: Depends on D19139. Ref T13088. This doesn't actually work, but is close enough that a skilled attacker might be able to briefly deceive a small child. Test Plan: - Viewed some very small logs under very controlled conditions, saw content. - Larger logs vaguely do something resembling working correctly. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13088 Differential Revision: https://secure.phabricator.com/D19141
This commit is contained in:
parent
6dc341be87
commit
11d1dc484b
9 changed files with 721 additions and 7 deletions
|
@ -78,7 +78,7 @@ return array(
|
||||||
'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
|
'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
|
||||||
'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
|
'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
|
||||||
'rsrc/css/application/flag/flag.css' => 'bba8f811',
|
'rsrc/css/application/flag/flag.css' => 'bba8f811',
|
||||||
'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4',
|
'rsrc/css/application/harbormaster/harbormaster.css' => 'fecac64f',
|
||||||
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
|
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
|
||||||
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
|
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
|
||||||
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
|
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
|
||||||
|
@ -416,6 +416,7 @@ return array(
|
||||||
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
|
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
|
||||||
'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
|
'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
|
||||||
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
|
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
|
||||||
|
'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '0844f3c1',
|
||||||
'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
|
'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
|
||||||
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
|
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
|
||||||
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
|
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
|
||||||
|
@ -578,7 +579,7 @@ return array(
|
||||||
'font-fontawesome' => 'e838e088',
|
'font-fontawesome' => 'e838e088',
|
||||||
'font-lato' => 'c7ccd872',
|
'font-lato' => 'c7ccd872',
|
||||||
'global-drag-and-drop-css' => 'b556a948',
|
'global-drag-and-drop-css' => 'b556a948',
|
||||||
'harbormaster-css' => 'f491c9f4',
|
'harbormaster-css' => 'fecac64f',
|
||||||
'herald-css' => 'cd8d0134',
|
'herald-css' => 'cd8d0134',
|
||||||
'herald-rule-editor' => 'dca75c0e',
|
'herald-rule-editor' => 'dca75c0e',
|
||||||
'herald-test-css' => 'a52e323e',
|
'herald-test-css' => 'a52e323e',
|
||||||
|
@ -635,6 +636,7 @@ return array(
|
||||||
'javelin-behavior-event-all-day' => 'b41537c9',
|
'javelin-behavior-event-all-day' => 'b41537c9',
|
||||||
'javelin-behavior-fancy-datepicker' => 'ecf4e799',
|
'javelin-behavior-fancy-datepicker' => 'ecf4e799',
|
||||||
'javelin-behavior-global-drag-and-drop' => '960f6a39',
|
'javelin-behavior-global-drag-and-drop' => '960f6a39',
|
||||||
|
'javelin-behavior-harbormaster-log' => '0844f3c1',
|
||||||
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
|
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
|
||||||
'javelin-behavior-high-security-warning' => 'a464fe03',
|
'javelin-behavior-high-security-warning' => 'a464fe03',
|
||||||
'javelin-behavior-history-install' => '7ee2b591',
|
'javelin-behavior-history-install' => '7ee2b591',
|
||||||
|
@ -960,6 +962,9 @@ return array(
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
'javelin-workflow',
|
'javelin-workflow',
|
||||||
),
|
),
|
||||||
|
'0844f3c1' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
),
|
||||||
'08f4ccc3' => array(
|
'08f4ccc3' => array(
|
||||||
'phui-oi-list-view-css',
|
'phui-oi-list-view-css',
|
||||||
),
|
),
|
||||||
|
|
|
@ -1230,6 +1230,7 @@ phutil_register_library_map(array(
|
||||||
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
|
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
|
||||||
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
|
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
|
||||||
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
|
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
|
||||||
|
'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
|
||||||
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
|
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
|
||||||
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
|
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
|
||||||
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
|
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
|
||||||
|
@ -6519,6 +6520,7 @@ phutil_register_library_map(array(
|
||||||
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
|
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
|
||||||
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
|
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
|
||||||
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
|
||||||
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
|
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
|
||||||
'HarbormasterBuildLogView' => 'AphrontView',
|
'HarbormasterBuildLogView' => 'AphrontView',
|
||||||
'HarbormasterBuildLogViewController' => 'HarbormasterController',
|
'HarbormasterBuildLogViewController' => 'HarbormasterController',
|
||||||
|
|
|
@ -97,7 +97,10 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
|
||||||
'buildkite/' => 'HarbormasterBuildkiteHookController',
|
'buildkite/' => 'HarbormasterBuildkiteHookController',
|
||||||
),
|
),
|
||||||
'log/' => array(
|
'log/' => array(
|
||||||
'view/(?P<id>\d+)/' => 'HarbormasterBuildLogViewController',
|
'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
|
||||||
|
=> 'HarbormasterBuildLogViewController',
|
||||||
|
'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
|
||||||
|
=> 'HarbormasterBuildLogRenderController',
|
||||||
'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
|
'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,562 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HarbormasterBuildLogRenderController
|
||||||
|
extends HarbormasterController {
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
$viewer = $this->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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tail_lines > 0) {
|
||||||
|
$views[] = array(
|
||||||
|
'offset' => $tail_offset,
|
||||||
|
'lines' => $tail_lines,
|
||||||
|
'direction' => -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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']);
|
||||||
|
|
||||||
|
$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'];
|
||||||
|
|
||||||
|
$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 ($direction > 0) {
|
||||||
|
$slice_offset = $line_key;
|
||||||
|
} else {
|
||||||
|
$slice_offset = max(0, $line_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;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$view_offset = $data_offset;
|
||||||
|
$view_length = $data_length;
|
||||||
|
if ($data_offset + $data_length > $view['offset']) {
|
||||||
|
$view_length -= (($data_offset + $data_length) - $view['offset']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
$line_data = substr($line['data'], $trim);
|
||||||
|
if (!strlen($line_data)) {
|
||||||
|
unset($lines[$line_key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trim = ($line_end - $view_end);
|
||||||
|
$line_data = substr($line['data'], -$trim);
|
||||||
|
if (!strlen($line_data)) {
|
||||||
|
unset($lines[$line_key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[$line_key]['data'] = $line_data;
|
||||||
|
$lines[$line_key]['length'] = strlen($line_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$views[$view_key]['viewData'] = $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
$spacer = null;
|
||||||
|
$render = array();
|
||||||
|
foreach ($views as $view) {
|
||||||
|
if ($spacer) {
|
||||||
|
$spacer['tail'] = $view['viewOffset'];
|
||||||
|
$render[] = $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
$render[] = $view;
|
||||||
|
|
||||||
|
$spacer = array(
|
||||||
|
'spacer' => true,
|
||||||
|
'head' => ($view['viewOffset'] + $view['viewLength']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = $log->getURI();
|
||||||
|
$highlight_range = $request->getURIData('lines');
|
||||||
|
|
||||||
|
$rows = array();
|
||||||
|
foreach ($render as $range) {
|
||||||
|
if (isset($range['spacer'])) {
|
||||||
|
$rows[] = phutil_tag(
|
||||||
|
'tr',
|
||||||
|
array(),
|
||||||
|
array(
|
||||||
|
phutil_tag(
|
||||||
|
'th',
|
||||||
|
array(),
|
||||||
|
null),
|
||||||
|
phutil_tag(
|
||||||
|
'td',
|
||||||
|
array(),
|
||||||
|
array(
|
||||||
|
javelin_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'sigil' => 'harbormaster-log-expand',
|
||||||
|
'meta' => array(
|
||||||
|
'headOffset' => $range['head'],
|
||||||
|
'tailOffset' => $range['tail'],
|
||||||
|
'head' => 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'Show Up ^^^^'),
|
||||||
|
'... '.($range['tail'] - $range['head']).' bytes ...',
|
||||||
|
javelin_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'sigil' => 'harbormaster-log-expand',
|
||||||
|
'meta' => array(
|
||||||
|
'headOffset' => $range['head'],
|
||||||
|
'tailOffset' => $range['tail'],
|
||||||
|
'tail' => 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'Show Down VVVV'),
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,8 +4,7 @@ final class HarbormasterBuildLogViewController
|
||||||
extends HarbormasterController {
|
extends HarbormasterController {
|
||||||
|
|
||||||
public function handleRequest(AphrontRequest $request) {
|
public function handleRequest(AphrontRequest $request) {
|
||||||
$request = $this->getRequest();
|
$viewer = $this->getViewer();
|
||||||
$viewer = $request->getUser();
|
|
||||||
|
|
||||||
$id = $request->getURIData('id');
|
$id = $request->getURIData('id');
|
||||||
|
|
||||||
|
@ -21,7 +20,8 @@ final class HarbormasterBuildLogViewController
|
||||||
|
|
||||||
$log_view = id(new HarbormasterBuildLogView())
|
$log_view = id(new HarbormasterBuildLogView())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->setBuildLog($log);
|
->setBuildLog($log)
|
||||||
|
->setHighlightedLineRange($request->getURIData('lines'));
|
||||||
|
|
||||||
$crumbs = $this->buildApplicationCrumbs()
|
$crumbs = $this->buildApplicationCrumbs()
|
||||||
->addTextCrumb(pht('Build Logs'))
|
->addTextCrumb(pht('Build Logs'))
|
||||||
|
|
|
@ -129,6 +129,30 @@ final class HarbormasterBuildLog
|
||||||
$this->getID());
|
$this->getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadData($offset, $length) {
|
||||||
|
return substr($this->getLogText(), $offset, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReadPosition($read_offset) {
|
||||||
|
$position = array(0, 0);
|
||||||
|
|
||||||
|
$map = $this->getLineMap();
|
||||||
|
if (!$map) {
|
||||||
|
throw new Exception(pht('No line map.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
list($map) = $map;
|
||||||
|
foreach ($map as $marker) {
|
||||||
|
list($offset, $count) = $marker;
|
||||||
|
if ($offset > $read_offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$position = $marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $position;
|
||||||
|
}
|
||||||
|
|
||||||
public function getLogText() {
|
public function getLogText() {
|
||||||
// TODO: Remove this method since it won't scale for big logs.
|
// TODO: Remove this method since it won't scale for big logs.
|
||||||
|
|
||||||
|
@ -148,6 +172,15 @@ final class HarbormasterBuildLog
|
||||||
return "/harbormaster/log/view/{$id}/";
|
return "/harbormaster/log/view/{$id}/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRenderURI($lines) {
|
||||||
|
if (strlen($lines)) {
|
||||||
|
$lines = '$'.$lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->getID();
|
||||||
|
return "/harbormaster/log/render/{$id}/{$lines}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Chunks )------------------------------------------------------------- */
|
/* -( Chunks )------------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
final class HarbormasterBuildLogView extends AphrontView {
|
final class HarbormasterBuildLogView extends AphrontView {
|
||||||
|
|
||||||
private $log;
|
private $log;
|
||||||
|
private $highlightedLineRange;
|
||||||
|
|
||||||
public function setBuildLog(HarbormasterBuildLog $log) {
|
public function setBuildLog(HarbormasterBuildLog $log) {
|
||||||
$this->log = $log;
|
$this->log = $log;
|
||||||
|
@ -13,6 +14,15 @@ final class HarbormasterBuildLogView extends AphrontView {
|
||||||
return $this->log;
|
return $this->log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setHighlightedLineRange($range) {
|
||||||
|
$this->highlightedLineRange = $range;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHighlightedLineRange() {
|
||||||
|
return $this->highlightedLineRange;
|
||||||
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
$viewer = $this->getViewer();
|
$viewer = $this->getViewer();
|
||||||
$log = $this->getBuildLog();
|
$log = $this->getBuildLog();
|
||||||
|
@ -34,10 +44,28 @@ final class HarbormasterBuildLogView extends AphrontView {
|
||||||
|
|
||||||
$header->addActionLink($download_button);
|
$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())
|
$box_view = id(new PHUIObjectBoxView())
|
||||||
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||||
->setHeader($header)
|
->setHeader($header)
|
||||||
->appendChild('...');
|
->appendChild($content_div);
|
||||||
|
|
||||||
return $box_view;
|
return $box_view;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,3 +30,41 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
color: {$lightgreytext};
|
color: {$lightgreytext};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-view-loading {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: {$lightgreytext};
|
||||||
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-table th {
|
||||||
|
background-color: {$paste.highlight};
|
||||||
|
border-right: 1px solid {$paste.border};
|
||||||
|
|
||||||
|
-moz-user-select: -moz-none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-table th a {
|
||||||
|
display: block;
|
||||||
|
color: {$darkbluetext};
|
||||||
|
text-align: right;
|
||||||
|
padding: 2px 6px 1px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-table th a:hover {
|
||||||
|
background: {$paste.border};
|
||||||
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-table td {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding: 2px 8px 1px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.harbormaster-log-table tr.harbormaster-log-highlighted td {
|
||||||
|
background: {$paste.highlight};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @provides javelin-behavior-harbormaster-log
|
||||||
|
* @requires javelin-behavior
|
||||||
|
*/
|
||||||
|
|
||||||
|
JX.behavior('harbormaster-log', function(config) {
|
||||||
|
var contentNode = JX.$(config.contentNodeID);
|
||||||
|
|
||||||
|
JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) {
|
||||||
|
if (!e.isNormalClick()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.kill();
|
||||||
|
|
||||||
|
var row = e.getNode('tag:tr');
|
||||||
|
var data = e.getNodeData('harbormaster-log-expand');
|
||||||
|
|
||||||
|
var uri = new JX.URI(config.renderURI)
|
||||||
|
.addQueryParams(data);
|
||||||
|
|
||||||
|
var request = new JX.Request(uri, function(r) {
|
||||||
|
var result = JX.$H(r.markup).getNode();
|
||||||
|
var rows = JX.DOM.scry(result, 'tr');
|
||||||
|
|
||||||
|
JX.DOM.replace(row, rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
function onresponse(r) {
|
||||||
|
JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false);
|
||||||
|
|
||||||
|
JX.DOM.setContent(contentNode, JX.$H(r.markup));
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = new JX.URI(config.renderURI);
|
||||||
|
|
||||||
|
new JX.Request(uri, onresponse)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in a new issue