1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-09 16:32:39 +01:00

Implemented support for build logs

Summary:
Depends on D7519.

This implements support for build logs in Harbormaster.  This includes support for appending to a log from the "Run Remote Command" build step.

It also adds the ability to cancel builds.

Currently the build view page doesn't update the logs live; I'm sure this can be achieved with Javelin, but I don't have enough experience with Javelin to actually make it poll from updates to content in the background.

{F79151}

{F79153}

{F79150}

{F79152}

Test Plan:
Tested this by setting up SSH on a Windows machine and using a Remote Command configured with:

```
C:\Windows\system32\cmd.exe /C cd C:\Build && mkdir Build_${timestamp} && cd Build_${timestamp} && git clone --recursive https://github.com/hach-que/Tychaia.git && cd Tychaia && Protobuild.exe && C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe Tychaia.Windows.sln
```

and observed the output of the build stream from the Windows machine into Phabricator.

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley

CC: Korvin, epriestley, aran

Maniphest Tasks: T1049

Differential Revision: https://secure.phabricator.com/D7521
This commit is contained in:
James Rhodes 2013-11-08 18:09:03 -08:00 committed by epriestley
parent d0de4dab24
commit 0ac1be7094
19 changed files with 1031 additions and 93 deletions

View file

@ -0,0 +1,26 @@
CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
buildPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
buildStepPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
logSource VARCHAR(255) NULL COLLATE utf8_bin,
logType VARCHAR(255) NULL COLLATE utf8_bin,
duration INT UNSIGNED NULL,
live BOOLEAN NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
KEY `key_build` (buildPHID, buildStepPHID),
UNIQUE KEY `key_phid` (phid)
) ENGINE=InnoDB, COLLATE utf8_general_ci;
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_build
ADD COLUMN cancelRequested BOOLEAN NOT NULL;
CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
logID INT UNSIGNED NOT NULL COLLATE utf8_bin,
encoding VARCHAR(30) NOT NULL COLLATE utf8_bin,
size LONG NULL,
chunk LONGBLOB NOT NULL,
KEY `key_log` (logID)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -569,22 +569,22 @@ celerity_register_resource_map(array(
),
'/rsrc/image/sprite-apps-X2.png' =>
array(
'hash' => '68bbb3f409d0eb42d65dd94769813044',
'uri' => '/res/68bbb3f4/rsrc/image/sprite-apps-X2.png',
'hash' => '67e8a6bf2d7fbb0b7961f1a0dcf8592b',
'uri' => '/res/67e8a6bf/rsrc/image/sprite-apps-X2.png',
'disk' => '/rsrc/image/sprite-apps-X2.png',
'type' => 'png',
),
'/rsrc/image/sprite-apps-large-X2.png' =>
array(
'hash' => '15368afbac0e1402c20f99f3166cdb11',
'uri' => '/res/15368afb/rsrc/image/sprite-apps-large-X2.png',
'hash' => '9bed1778022e2bd25d658842be54844d',
'uri' => '/res/9bed1778/rsrc/image/sprite-apps-large-X2.png',
'disk' => '/rsrc/image/sprite-apps-large-X2.png',
'type' => 'png',
),
'/rsrc/image/sprite-apps-large.png' =>
array(
'hash' => 'b1f1de55803cf22eb3beb391fff17b04',
'uri' => '/res/b1f1de55/rsrc/image/sprite-apps-large.png',
'hash' => '518d6e5487c8d3758921ad85c1bb7d60',
'uri' => '/res/518d6e54/rsrc/image/sprite-apps-large.png',
'disk' => '/rsrc/image/sprite-apps-large.png',
'type' => 'png',
),
@ -597,8 +597,8 @@ celerity_register_resource_map(array(
),
'/rsrc/image/sprite-apps.png' =>
array(
'hash' => 'bf7feaae848d44a461e63123c28e402f',
'uri' => '/res/bf7feaae/rsrc/image/sprite-apps.png',
'hash' => '9024ab95247f936f41c0b51c75e2e228',
'uri' => '/res/9024ab95/rsrc/image/sprite-apps.png',
'disk' => '/rsrc/image/sprite-apps.png',
'type' => 'png',
),
@ -1197,7 +1197,7 @@ celerity_register_resource_map(array(
),
'herald-rule-editor' =>
array(
'uri' => '/res/a561eb19/rsrc/js/application/herald/HeraldRuleEditor.js',
'uri' => '/res/928275b4/rsrc/js/application/herald/HeraldRuleEditor.js',
'type' => 'js',
'requires' =>
array(
@ -4183,7 +4183,7 @@ celerity_register_resource_map(array(
),
'sprite-apps-css' =>
array(
'uri' => '/res/37c55e75/rsrc/css/sprite-apps.css',
'uri' => '/res/774f4bad/rsrc/css/sprite-apps.css',
'type' => 'css',
'requires' =>
array(
@ -4192,7 +4192,7 @@ celerity_register_resource_map(array(
),
'sprite-apps-large-css' =>
array(
'uri' => '/res/8ddded36/rsrc/css/sprite-apps-large.css',
'uri' => '/res/b547fab1/rsrc/css/sprite-apps-large.css',
'type' => 'css',
'requires' =>
array(
@ -4328,7 +4328,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
'f350af41' =>
'f0d63822' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -4377,7 +4377,7 @@ celerity_register_resource_map(array(
41 => 'phabricator-tag-view-css',
42 => 'phui-list-view-css',
),
'uri' => '/res/pkg/f350af41/core.pkg.css',
'uri' => '/res/pkg/f0d63822/core.pkg.css',
'type' => 'css',
),
'2c1dba03' =>
@ -4569,15 +4569,15 @@ celerity_register_resource_map(array(
),
'reverse' =>
array(
'aphront-dialog-view-css' => 'f350af41',
'aphront-error-view-css' => 'f350af41',
'aphront-list-filter-view-css' => 'f350af41',
'aphront-pager-view-css' => 'f350af41',
'aphront-panel-view-css' => 'f350af41',
'aphront-table-view-css' => 'f350af41',
'aphront-tokenizer-control-css' => 'f350af41',
'aphront-tooltip-css' => 'f350af41',
'aphront-typeahead-control-css' => 'f350af41',
'aphront-dialog-view-css' => 'f0d63822',
'aphront-error-view-css' => 'f0d63822',
'aphront-list-filter-view-css' => 'f0d63822',
'aphront-pager-view-css' => 'f0d63822',
'aphront-panel-view-css' => 'f0d63822',
'aphront-table-view-css' => 'f0d63822',
'aphront-tokenizer-control-css' => 'f0d63822',
'aphront-tooltip-css' => 'f0d63822',
'aphront-typeahead-control-css' => 'f0d63822',
'differential-changeset-view-css' => '1084b12b',
'differential-core-view-css' => '1084b12b',
'differential-inline-comment-editor' => '5e9e5c4e',
@ -4591,7 +4591,7 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => '1084b12b',
'diffusion-commit-view-css' => '7aa115b4',
'diffusion-icons-css' => '7aa115b4',
'global-drag-and-drop-css' => 'f350af41',
'global-drag-and-drop-css' => 'f0d63822',
'inline-comment-summary-css' => '1084b12b',
'javelin-aphlict' => '2c1dba03',
'javelin-behavior' => '3e3be199',
@ -4666,56 +4666,56 @@ celerity_register_resource_map(array(
'javelin-util' => '3e3be199',
'javelin-vector' => '3e3be199',
'javelin-workflow' => '3e3be199',
'lightbox-attachment-css' => 'f350af41',
'lightbox-attachment-css' => 'f0d63822',
'maniphest-task-summary-css' => '49898640',
'phabricator-action-list-view-css' => 'f350af41',
'phabricator-application-launch-view-css' => 'f350af41',
'phabricator-action-list-view-css' => 'f0d63822',
'phabricator-application-launch-view-css' => 'f0d63822',
'phabricator-busy' => '2c1dba03',
'phabricator-content-source-view-css' => '1084b12b',
'phabricator-core-css' => 'f350af41',
'phabricator-crumbs-view-css' => 'f350af41',
'phabricator-core-css' => 'f0d63822',
'phabricator-crumbs-view-css' => 'f0d63822',
'phabricator-drag-and-drop-file-upload' => '5e9e5c4e',
'phabricator-dropdown-menu' => '2c1dba03',
'phabricator-file-upload' => '2c1dba03',
'phabricator-filetree-view-css' => 'f350af41',
'phabricator-flag-css' => 'f350af41',
'phabricator-filetree-view-css' => 'f0d63822',
'phabricator-flag-css' => 'f0d63822',
'phabricator-hovercard' => '2c1dba03',
'phabricator-jump-nav' => 'f350af41',
'phabricator-jump-nav' => 'f0d63822',
'phabricator-keyboard-shortcut' => '2c1dba03',
'phabricator-keyboard-shortcut-manager' => '2c1dba03',
'phabricator-main-menu-view' => 'f350af41',
'phabricator-main-menu-view' => 'f0d63822',
'phabricator-menu-item' => '2c1dba03',
'phabricator-nav-view-css' => 'f350af41',
'phabricator-nav-view-css' => 'f0d63822',
'phabricator-notification' => '2c1dba03',
'phabricator-notification-css' => 'f350af41',
'phabricator-notification-menu-css' => 'f350af41',
'phabricator-notification-css' => 'f0d63822',
'phabricator-notification-menu-css' => 'f0d63822',
'phabricator-object-selector-css' => '1084b12b',
'phabricator-phtize' => '2c1dba03',
'phabricator-prefab' => '2c1dba03',
'phabricator-project-tag-css' => '49898640',
'phabricator-remarkup-css' => 'f350af41',
'phabricator-remarkup-css' => 'f0d63822',
'phabricator-shaped-request' => '5e9e5c4e',
'phabricator-side-menu-view-css' => 'f350af41',
'phabricator-standard-page-view' => 'f350af41',
'phabricator-tag-view-css' => 'f350af41',
'phabricator-side-menu-view-css' => 'f0d63822',
'phabricator-standard-page-view' => 'f0d63822',
'phabricator-tag-view-css' => 'f0d63822',
'phabricator-textareautils' => '2c1dba03',
'phabricator-tooltip' => '2c1dba03',
'phabricator-transaction-view-css' => 'f350af41',
'phabricator-zindex-css' => 'f350af41',
'phui-button-css' => 'f350af41',
'phui-form-css' => 'f350af41',
'phui-form-view-css' => 'f350af41',
'phui-header-view-css' => 'f350af41',
'phui-icon-view-css' => 'f350af41',
'phui-list-view-css' => 'f350af41',
'phui-object-item-list-view-css' => 'f350af41',
'phui-property-list-view-css' => 'f350af41',
'phui-spacing-css' => 'f350af41',
'sprite-apps-large-css' => 'f350af41',
'sprite-gradient-css' => 'f350af41',
'sprite-icons-css' => 'f350af41',
'sprite-menu-css' => 'f350af41',
'sprite-status-css' => 'f350af41',
'syntax-highlighting-css' => 'f350af41',
'phabricator-transaction-view-css' => 'f0d63822',
'phabricator-zindex-css' => 'f0d63822',
'phui-button-css' => 'f0d63822',
'phui-form-css' => 'f0d63822',
'phui-form-view-css' => 'f0d63822',
'phui-header-view-css' => 'f0d63822',
'phui-icon-view-css' => 'f0d63822',
'phui-list-view-css' => 'f0d63822',
'phui-object-item-list-view-css' => 'f0d63822',
'phui-property-list-view-css' => 'f0d63822',
'phui-spacing-css' => 'f0d63822',
'sprite-apps-large-css' => 'f0d63822',
'sprite-gradient-css' => 'f0d63822',
'sprite-icons-css' => 'f0d63822',
'sprite-menu-css' => 'f0d63822',
'sprite-status-css' => 'f0d63822',
'syntax-highlighting-css' => 'f0d63822',
),
));

View file

@ -652,9 +652,11 @@ phutil_register_library_map(array(
'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
'HarbormasterBuildCancelController' => 'applications/harbormaster/controller/HarbormasterBuildCancelController.php',
'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php',
'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php',
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
@ -667,6 +669,7 @@ phutil_register_library_map(array(
'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
'HarbormasterBuildableApplyController' => 'applications/harbormaster/controller/HarbormasterBuildableApplyController.php',
@ -682,6 +685,7 @@ phutil_register_library_map(array(
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterPHIDTypeBuild' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuild.php',
'HarbormasterPHIDTypeBuildItem' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildItem.php',
'HarbormasterPHIDTypeBuildLog' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php',
'HarbormasterPHIDTypeBuildPlan' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php',
'HarbormasterPHIDTypeBuildStep' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildStep.php',
'HarbormasterPHIDTypeBuildTarget' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildTarget.php',
@ -1392,6 +1396,7 @@ phutil_register_library_map(array(
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php',
'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
@ -2254,9 +2259,11 @@ phutil_register_library_map(array(
'ReleephStatusFieldSpecification' => 'applications/releeph/field/specification/ReleephStatusFieldSpecification.php',
'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
'ReleephUserView' => 'applications/releeph/view/user/ReleephUserView.php',
'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
'SleepBuildStepImplementation' => 'applications/harbormaster/step/SleepBuildStepImplementation.php',
'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
'VariableBuildStepImplementation' => 'applications/harbormaster/step/VariableBuildStepImplementation.php',
),
'function' =>
array(
@ -2920,9 +2927,15 @@ phutil_register_library_map(array(
0 => 'HarbormasterDAO',
1 => 'PhabricatorPolicyInterface',
),
'HarbormasterBuildCancelController' => 'HarbormasterController',
'HarbormasterBuildItem' => 'HarbormasterDAO',
'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLog' => 'HarbormasterDAO',
'HarbormasterBuildLog' =>
array(
0 => 'HarbormasterDAO',
1 => 'PhabricatorPolicyInterface',
),
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPlan' =>
array(
0 => 'HarbormasterDAO',
@ -2944,6 +2957,7 @@ phutil_register_library_map(array(
'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildTarget' => 'HarbormasterDAO',
'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildViewController' => 'HarbormasterController',
'HarbormasterBuildWorker' => 'PhabricatorWorker',
'HarbormasterBuildable' =>
array(
@ -2967,6 +2981,7 @@ phutil_register_library_map(array(
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterPHIDTypeBuild' => 'PhabricatorPHIDType',
'HarbormasterPHIDTypeBuildItem' => 'PhabricatorPHIDType',
'HarbormasterPHIDTypeBuildLog' => 'PhabricatorPHIDType',
'HarbormasterPHIDTypeBuildPlan' => 'PhabricatorPHIDType',
'HarbormasterPHIDTypeBuildStep' => 'PhabricatorPHIDType',
'HarbormasterPHIDTypeBuildTarget' => 'PhabricatorPHIDType',
@ -3776,6 +3791,7 @@ phutil_register_library_map(array(
'PhabricatorGlobalLock' => 'PhutilLock',
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorHashTestCase' => 'PhabricatorTestCase',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
@ -4783,8 +4799,10 @@ phutil_register_library_map(array(
'ReleephStatusFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
'ReleephUserView' => 'AphrontView',
'ShellLogView' => 'AphrontView',
'SleepBuildStepImplementation' => 'BuildStepImplementation',
'SlowvoteEmbedView' => 'AphrontView',
'SlowvoteRemarkupRule' => 'PhabricatorRemarkupRuleObject',
'VariableBuildStepImplementation' => 'BuildStepImplementation',
),
));

View file

@ -51,6 +51,10 @@ final class PhabricatorApplicationHarbormaster extends PhabricatorApplication {
'edit/(?:(?P<id>\d+)/)?' => 'HarbormasterStepEditController',
'delete/(?:(?P<id>\d+)/)?' => 'HarbormasterStepDeleteController',
),
'build/' => array(
'(?:(?P<id>\d+)/)?' => 'HarbormasterBuildViewController',
'cancel/(?:(?P<id>\d+)/)?' => 'HarbormasterBuildCancelController',
),
'plan/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'HarbormasterPlanListController',

View file

@ -0,0 +1,35 @@
<?php
final class PhabricatorHarbormasterConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Harbormaster');
}
public function getDescription() {
return pht('Configure Harbormaster build engine.');
}
public function getOptions() {
return array(
$this->newOption(
'harbormaster.temporary.hosts.whitelist',
'list<string>',
array())
->setSummary('Temporary configuration value.')
->setLocked(true)
->setDescription(
pht(
"This specifies a whitelist of remote hosts that the \"Run ".
"Remote Command\" may connect to. This is a temporary ".
"configuration option as Drydock is not yet available.".
"\n\n".
"**This configuration option will be removed in the future and ".
"your build configuration will no longer work when Drydock ".
"replaces this option. There is ABSOLUTELY NO SUPPORT for ".
"using this functionality!**"))
);
}
}

View file

@ -0,0 +1,49 @@
<?php
final class HarbormasterBuildCancelController
extends HarbormasterController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $this->id;
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if ($build === null) {
return new Aphront404Response();
}
$build_uri = $this->getApplicationURI('/build/'.$build->getID());
if ($request->isDialogFormPost()) {
$build->setCancelRequested(true);
$build->save();
return id(new AphrontRedirectResponse())->setURI($build_uri);
}
$dialog = new AphrontDialogView();
$dialog->setTitle(pht('Really cancel build?'))
->setUser($viewer)
->addSubmitButton(pht('Cancel'))
->addCancelButton($build_uri, pht('Don\'t Cancel'));
$dialog->appendChild(
phutil_tag(
'p',
array(),
pht(
'Really cancel this build?')));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -0,0 +1,218 @@
<?php
final class HarbormasterBuildViewController
extends HarbormasterController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $this->id;
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
$title = pht("Build %d", $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($build);
$box = id(new PHUIObjectBoxView())
->setHeader($header);
$actions = $this->buildActionList($build);
$this->buildPropertyLists($box, $build, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($title));
$logs = $this->buildLog($build);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$logs
),
array(
'title' => $title,
'device' => true,
));
}
private function buildLog(HarbormasterBuild $build) {
$request = $this->getRequest();
$viewer = $request->getUser();
$limit = $request->getInt('l', 25);
$logs = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withBuildPHIDs(array($build->getPHID()))
->execute();
$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;
}
}
$log_view = new ShellLogView();
$log_view->setLines($lines);
$log_view->setStart($start);
$header = id(new PHUIHeaderView())
->setHeader(pht(
'Build Log %d (%s - %s)',
$log->getID(),
$log->getLogSource(),
$log->getLogType()))
->setSubheader($this->createLogHeader($build, $log))
->setUser($viewer);
$log_boxes[] = id(new PHUIObjectBoxView())
->setHeader($header)
->setForm($log_view);
}
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 buildActionList(HarbormasterBuild $build) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $build->getID();
$list = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($build)
->setObjectURI("/build/{$id}");
$action =
id(new PhabricatorActionView())
->setName(pht('Cancel Build'))
->setIcon('delete');
switch ($build->getBuildStatus()) {
case HarbormasterBuild::STATUS_PENDING:
case HarbormasterBuild::STATUS_WAITING:
case HarbormasterBuild::STATUS_BUILDING:
$cancel_uri = $this->getApplicationURI('/build/cancel/'.$id.'/');
$action
->setHref($cancel_uri)
->setWorkflow(true);
break;
default:
$action
->setDisabled(true);
break;
}
$list->addAction($action);
return $list;
}
private function buildPropertyLists(
PHUIObjectBoxView $box,
HarbormasterBuild $build,
PhabricatorActionListView $actions) {
$request = $this->getRequest();
$viewer = $request->getUser();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($build)
->setActionList($actions);
$box->addPropertyList($properties);
$properties->addProperty(
pht('Status'),
$this->getStatus($build));
}
private function getStatus(HarbormasterBuild $build) {
if ($build->getCancelRequested()) {
return pht('Cancelling');
}
switch ($build->getBuildStatus()) {
case HarbormasterBuild::STATUS_INACTIVE:
return pht('Inactive');
case HarbormasterBuild::STATUS_PENDING:
return pht('Pending');
case HarbormasterBuild::STATUS_WAITING:
return pht('Waiting on Resource');
case HarbormasterBuild::STATUS_BUILDING:
return pht('Building');
case HarbormasterBuild::STATUS_PASSED:
return pht('Passed');
case HarbormasterBuild::STATUS_FAILED:
return pht('Failed');
case HarbormasterBuild::STATUS_ERROR:
return pht('Unexpected Error');
case HarbormasterBuild::STATUS_CANCELLED:
return pht('Cancelled');
default:
return pht('Unknown');
}
}
}

View file

@ -34,38 +34,49 @@ final class HarbormasterBuildableViewController
$build_list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($builds as $build) {
$view_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Build %d', $build->getID()))
->setHeader($build->getName());
switch ($build->getBuildStatus()) {
case HarbormasterBuild::STATUS_INACTIVE:
$item->setBarColor('grey');
$item->addAttribute(pht('Inactive'));
break;
case HarbormasterBuild::STATUS_PENDING:
$item->setBarColor('blue');
$item->addAttribute(pht('Pending'));
break;
case HarbormasterBuild::STATUS_WAITING:
$item->setBarColor('blue');
$item->addAttribute(pht('Waiting on Resource'));
break;
case HarbormasterBuild::STATUS_BUILDING:
$item->setBarColor('yellow');
$item->addAttribute(pht('Building'));
break;
case HarbormasterBuild::STATUS_PASSED:
$item->setBarColor('green');
$item->addAttribute(pht('Passed'));
break;
case HarbormasterBuild::STATUS_FAILED:
$item->setBarColor('red');
$item->addAttribute(pht('Failed'));
break;
case HarbormasterBuild::STATUS_ERROR:
$item->setBarColor('red');
$item->addAttribute(pht('Unexpected Error'));
break;
->setHeader($build->getName())
->setHref($view_uri);
if ($build->getCancelRequested()) {
$item->setBarColor('black');
$item->addAttribute(pht('Cancelling'));
} else {
switch ($build->getBuildStatus()) {
case HarbormasterBuild::STATUS_INACTIVE:
$item->setBarColor('grey');
$item->addAttribute(pht('Inactive'));
break;
case HarbormasterBuild::STATUS_PENDING:
$item->setBarColor('blue');
$item->addAttribute(pht('Pending'));
break;
case HarbormasterBuild::STATUS_WAITING:
$item->setBarColor('blue');
$item->addAttribute(pht('Waiting on Resource'));
break;
case HarbormasterBuild::STATUS_BUILDING:
$item->setBarColor('yellow');
$item->addAttribute(pht('Building'));
break;
case HarbormasterBuild::STATUS_PASSED:
$item->setBarColor('green');
$item->addAttribute(pht('Passed'));
break;
case HarbormasterBuild::STATUS_FAILED:
$item->setBarColor('red');
$item->addAttribute(pht('Failed'));
break;
case HarbormasterBuild::STATUS_ERROR:
$item->setBarColor('red');
$item->addAttribute(pht('Unexpected Error'));
break;
case HarbormasterBuild::STATUS_CANCELLED:
$item->setBarColor('black');
$item->addAttribute(pht('Cancelled'));
break;
}
}
$build_list->addItem($item);
}

View file

@ -63,6 +63,11 @@ final class HarbormasterStepEditController
$form = id(new AphrontFormView())
->setUser($viewer);
$instructions = $implementation->getSettingRemarkupInstructions();
if ($instructions !== null) {
$form->appendRemarkupInstructions($instructions);
}
// We need to render out all of the fields for the settings that
// the implementation has.
foreach ($implementation->getSettingDefinitions() as $name => $opt) {

View file

@ -0,0 +1,37 @@
<?php
final class HarbormasterPHIDTypeBuildLog extends PhabricatorPHIDType {
const TYPECONST = 'HMCL';
public function getTypeConstant() {
return self::TYPECONST;
}
public function getTypeName() {
return pht('Build Log');
}
public function newObject() {
return new HarbormasterBuildLog();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new HarbormasterBuildLogQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$build_log = $objects[$phid];
}
}
}

View file

@ -0,0 +1,98 @@
<?php
final class HarbormasterBuildLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $buildPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBuildPHIDs(array $build_phids) {
$this->buildPHIDs = $build_phids;
return $this;
}
protected function loadPage() {
$table = new HarbormasterBuildLog();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $page) {
$builds = array();
$build_phids = array_filter(mpull($page, 'getBuildPHID'));
if ($build_phids) {
$builds = id(new HarbormasterBuildQuery())
->setViewer($this->getViewer())
->withPHIDs($build_phids)
->setParentQuery($this)
->execute();
$builds = mpull($builds, null, 'getPHID');
}
foreach ($page as $key => $build_log) {
$build_phid = $build_log->getBuildPHID();
if (empty($builds[$build_phid])) {
unset($page[$key]);
continue;
}
$build_log->attachBuild($builds[$build_phid]);
}
return $page;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->buildPHIDs) {
$where[] = qsprintf(
$conn_r,
'buildPHID IN (%Ls)',
$this->buildPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorApplicationHarbormaster';
}
}

View file

@ -38,7 +38,9 @@ abstract class BuildStepImplementation {
/**
* Run the build step against the specified build.
*/
abstract public function execute(HarbormasterBuild $build);
abstract public function execute(
HarbormasterBuild $build,
HarbormasterBuildStep $build_step);
/**
* Gets the settings for this build step.
@ -85,4 +87,11 @@ abstract class BuildStepImplementation {
public function getSettingDefinitions() {
return array();
}
/**
* Return relevant setting instructions as Remarkup.
*/
public function getSettingRemarkupInstructions() {
return null;
}
}

View file

@ -16,7 +16,10 @@ final class SleepBuildStepImplementation extends BuildStepImplementation {
return pht('Sleep for %s seconds.', $settings['seconds']);
}
public function execute(HarbormasterBuild $build) {
public function execute(
HarbormasterBuild $build,
HarbormasterBuildStep $build_step) {
$settings = $this->getSettings();
sleep($settings['seconds']);

View file

@ -0,0 +1,67 @@
<?php
abstract class VariableBuildStepImplementation extends BuildStepImplementation {
public function retrieveVariablesFromBuild(HarbormasterBuild $build) {
$results = array(
'revision' => null,
'commit' => null,
'repository' => null,
'vcs' => null,
'uri' => null,
'timestamp' => null);
$buildable = $build->getBuildable();
$object = $buildable->getBuildableObject();
$repo = null;
if ($object instanceof DifferentialRevision) {
$results['revision'] = $object->getID();
$repo = $object->getRepository();
} else if ($object instanceof PhabricatorRepositoryCommit) {
$results['commit'] = $object->getCommitIdentifier();
$repo = $object->getRepository();
}
$results['repository'] = $repo->getCallsign();
$results['vcs'] = $repo->getVersionControlSystem();
$results['uri'] = $repo->getPublicRemoteURI();
$results['timestamp'] = time();
return $results;
}
public function mergeVariables(HarbormasterBuild $build, $string) {
$variables = $this->retrieveVariablesFromBuild($build);
foreach ($variables as $name => $value) {
if ($value === null) {
$value = '';
}
$string = str_replace('${'.$name.'}', $value, $string);
}
return $string;
}
public function getAvailableVariables() {
return array(
'revision' => pht('The differential revision ID, if applicable.'),
'commit' => pht('The commit identifier, if applicable.'),
'repository' => pht('The callsign of the repository in Phabricator.'),
'vcs' => pht('The version control system, either "svn", "hg" or "git".'),
'uri' => pht('The URI to clone or checkout the repository from.'),
'timestamp' => pht('The current UNIX timestamp.'));
}
public function getSettingRemarkupInstructions() {
$text = '';
$text .= pht('The following variables are available: ')."\n";
$text .= "\n";
foreach ($this->getAvailableVariables() as $name => $desc) {
$text .= ' - `'.$name.'`: '.$desc."\n";
}
$text .= "\n";
$text .= "Use `\${name}` to merge a variable into a setting.";
return $text;
}
}

View file

@ -6,6 +6,7 @@ final class HarbormasterBuild extends HarbormasterDAO
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $cancelRequested;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
@ -45,9 +46,15 @@ final class HarbormasterBuild extends HarbormasterDAO
*/
const STATUS_ERROR = 'error';
/**
* The build has been cancelled.
*/
const STATUS_CANCELLED = 'cancelled';
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(self::STATUS_INACTIVE);
->setBuildStatus(self::STATUS_INACTIVE)
->setCancelRequested(false);
}
public function getConfiguration() {
@ -87,6 +94,37 @@ final class HarbormasterBuild extends HarbormasterDAO
return $this->assertAttached($this->buildPlan);
}
public function createLog(
HarbormasterBuildStep $build_step,
$log_source,
$log_type) {
$log = HarbormasterBuildLog::initializeNewBuildLog($this, $build_step);
$log->setLogSource($log_source);
$log->setLogType($log_type);
$log->save();
return $log;
}
/**
* Checks for and handles build cancellation. If this method returns
* true, the caller should stop any current operations and return control
* as quickly as possible.
*/
public function checkForCancellation() {
// Here we load a copy of the current build and check whether
// the user requested cancellation. We can't do `reload()` here
// in case there are changes that have not yet been saved.
$copy = id(new HarbormasterBuild())->load($this->getID());
if ($copy->getCancelRequested()) {
$this->setBuildStatus(HarbormasterBuild::STATUS_CANCELLED);
$this->setCancelRequested(false);
$this->save();
return true;
}
return false;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -1,7 +1,206 @@
<?php
final class HarbormasterBuildLog extends HarbormasterDAO {
final class HarbormasterBuildLog extends HarbormasterDAO
implements PhabricatorPolicyInterface {
protected $buildPHID;
protected $buildStepPHID;
protected $logSource;
protected $logType;
protected $duration;
protected $live;
private $build = self::ATTACHABLE;
private $buildStep = self::ATTACHABLE;
const CHUNK_BYTE_LIMIT = 102400;
/**
* The log is encoded as plain text.
*/
const ENCODING_TEXT = 'text';
public static function initializeNewBuildLog(
HarbormasterBuild $build,
HarbormasterBuildStep $build_step) {
return id(new HarbormasterBuildLog())
->setBuildPHID($build->getPHID())
->setBuildStepPHID($build_step->getPHID())
->setDuration(null)
->setLive(false);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterPHIDTypeBuildLog::TYPECONST);
}
public function attachBuild(HarbormasterBuild $build) {
$this->build = $build;
return $this;
}
public function getBuild() {
return $this->assertAttached($this->build);
}
public function getName() {
return pht('Build Log');
}
public function attachBuildStep(
HarbormasterBuildStep $build_step = null) {
$this->buildStep = $build_step;
return $this;
}
public function getBuildStep() {
return $this->assertAttached($this->buildStep);
}
public function start() {
if ($this->getLive()) {
throw new Exception("Live logging has already started for this log.");
}
$this->setLive(true);
$this->save();
return time();
}
public function append($content) {
if (!$this->getLive()) {
throw new Exception("Start logging before appending data to the log.");
}
if (strlen($content) === 0) {
return;
}
// If the length of the content is greater than the chunk size limit,
// then we can never fit the content in a single record. We need to
// split our content out and call append on it for as many parts as there
// are to the content.
if (strlen($content) > self::CHUNK_BYTE_LIMIT) {
$current = $content;
while (strlen($current) > self::CHUNK_BYTE_LIMIT) {
$part = substr($current, 0, self::CHUNK_BYTE_LIMIT);
$current = substr($current, self::CHUNK_BYTE_LIMIT);
$this->append($part);
}
return;
}
// Retrieve the size of last chunk from the DB for this log. If the
// chunk is over 500K, then we need to create a new log entry.
$conn = $this->establishConnection('w');
$result = queryfx_all(
$conn,
'SELECT id, size, encoding '.
'FROM harbormaster_buildlogchunk '.
'WHERE logID = %d '.
'ORDER BY id DESC '.
'LIMIT 1',
$this->getID());
if (count($result) === 0 ||
$result[0]["size"] + strlen($content) > self::CHUNK_BYTE_LIMIT ||
$result[0]["encoding"] !== self::ENCODING_TEXT) {
// We must insert a new chunk because the data we are appending
// won't fit into the existing one, or we don't have any existing
// chunk data.
queryfx(
$conn,
'INSERT INTO harbormaster_buildlogchunk '.
'(logID, encoding, size, chunk) '.
'VALUES '.
'(%d, %s, %d, %s)',
$this->getID(),
self::ENCODING_TEXT,
strlen($content),
$content);
} else {
// We have a resulting record that we can append our content onto.
queryfx(
$conn,
'UPDATE harbormaster_buildlogchunk '.
'SET chunk = CONCAT(chunk, %s), size = LENGTH(CONCAT(chunk, %s))'.
'WHERE id = %d',
$content,
$content,
$result[0]["id"]);
}
}
public function finalize($start = 0) {
if (!$this->getLive()) {
throw new Exception("Start logging before finalizing it.");
}
// TODO: Encode the log contents in a gzipped format.
$this->reload();
if ($start > 0) {
$this->setDuration(time() - $start);
}
$this->setLive(false);
$this->save();
}
public function getLogText() {
// TODO: This won't cope very well if we're pulling like a 700MB
// log file out of the DB. We should probably implement some sort
// of optional limit parameter so that when we're rendering out only
// 25 lines in the UI, we don't wastefully read in the whole log.
// We have to read our content out of the database and stitch all of
// the log data back together.
$conn = $this->establishConnection('r');
$result = queryfx_all(
$conn,
'SELECT chunk '.
'FROM harbormaster_buildlogchunk '.
'WHERE logID = %d '.
'ORDER BY id ASC',
$this->getID());
$content = "";
foreach ($result as $row) {
$content .= $row["chunk"];
}
return $content;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getBuild()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuild()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Users must be able to see a build to view it\'s build log.');
}
protected $buildItemPHID;
}

View file

@ -0,0 +1,101 @@
<?php
final class ShellLogView extends AphrontView {
private $start = 1;
private $lines;
private $limit;
private $highlights = array();
public function setStart($start) {
$this->start = $start;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setLines(array $lines) {
$this->lines = $lines;
return $this;
}
public function setHighlights(array $highlights) {
$this->highlights = array_fuse($highlights);
return $this;
}
public function render() {
require_celerity_resource('phabricator-source-code-view-css');
require_celerity_resource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
$line_number = $this->start;
$rows = array();
foreach ($this->lines as $line) {
$hit_limit = $this->limit &&
($line_number == $this->limit) &&
(count($this->lines) != $this->limit);
if ($hit_limit) {
$content_number = '';
$content_line = phutil_tag(
'span',
array(
'class' => 'c',
),
pht('...'));
} else {
$content_number = $line_number;
$content_line = $line;
}
$row_attributes = array();
if (isset($this->highlights[$line_number])) {
$row_attributes['class'] = 'phabricator-source-highlight';
}
// TODO: Provide nice links.
$rows[] = phutil_tag(
'tr',
$row_attributes,
hsprintf(
'<th class="phabricator-source-line" '.
'style="background-color: #fff;">%s</th>'.
'<td class="phabricator-source-code">%s</td>',
$content_number,
$content_line));
if ($hit_limit) {
break;
}
$line_number++;
}
$classes = array();
$classes[] = 'phabricator-source-code-view';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
return phutil_tag(
'div',
array(
'class' => 'phabricator-source-code-container',
'style' => 'background-color: black; color: white;'
),
phutil_tag(
'table',
array(
'class' => implode(' ', $classes),
'style' => 'background-color: black'
),
phutil_implode_html('', $rows)));
}
}

View file

@ -25,6 +25,13 @@ final class HarbormasterBuildWorker extends PhabricatorWorker {
pht('Invalid build ID "%s".', $id));
}
// It's possible for the user to request cancellation before
// a worker picks up a build. We check to see if the build
// is already cancelled, and return if it is.
if ($build->checkForCancellation()) {
return;
}
try {
$build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING);
$build->save();
@ -44,12 +51,21 @@ final class HarbormasterBuildWorker extends PhabricatorWorker {
$build->setBuildStatus(HarbormasterBuild::STATUS_ERROR);
break;
}
$implementation->execute($build);
$implementation->execute($build, $step);
if ($build->getBuildStatus() !== HarbormasterBuild::STATUS_BUILDING) {
break;
}
if ($build->checkForCancellation()) {
break;
}
}
// Check to see if the user requested cancellation. If they did and
// we get to here, they might have either cancelled too late, or the
// step isn't cancellation aware. In either case we ignore the result
// and move to a cancelled state.
$build->checkForCancellation();
// If we get to here, then the build has finished. Set it to passed
// if no build step explicitly set the status.
if ($build->getBuildStatus() === HarbormasterBuild::STATUS_BUILDING) {

View file

@ -1744,6 +1744,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20131106.nuance-v0.sql'),
),
'20131107.buildlog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20131107.buildlog.sql'),
),
);
}
}