Index: resources/sql/autopatches/20140104.harbormastercmd.sql =================================================================== --- /dev/null +++ resources/sql/autopatches/20140104.harbormastercmd.sql @@ -0,0 +1,18 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildcommand ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + targetPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + command VARCHAR(128) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_target` (targetPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_build + DROP cancelRequested; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildtarget + ADD targetStatus VARCHAR(64) NOT NULL COLLATE utf8_bin; + +UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildtarget + SET targetStatus = 'target/pending' WHERE targetStatus = ''; Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -704,9 +704,10 @@ 'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php', 'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', + 'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php', 'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php', 'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php', - 'HarbormasterBuildCancelController' => 'applications/harbormaster/controller/HarbormasterBuildCancelController.php', + 'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php', 'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php', 'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php', 'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php', @@ -3155,13 +3156,14 @@ 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), + 'HarbormasterBuildActionController' => 'HarbormasterController', 'HarbormasterBuildArtifact' => array( 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildCancelController' => 'HarbormasterController', + 'HarbormasterBuildCommand' => 'HarbormasterDAO', 'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildItem' => 'HarbormasterDAO', 'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -3226,7 +3228,7 @@ 'HarbormasterPHIDTypeBuildStep' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildTarget' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildable' => 'PhabricatorPHIDType', - 'HarbormasterPlanController' => 'PhabricatorController', + 'HarbormasterPlanController' => 'HarbormasterController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', 'HarbormasterPlanListController' => Index: src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php =================================================================== --- src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php +++ src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php @@ -55,7 +55,8 @@ ), 'build/' => array( '(?:(?P\d+)/)?' => 'HarbormasterBuildViewController', - 'cancel/(?:(?P\d+)/)?' => 'HarbormasterBuildCancelController', + '(?Pstop|resume|restart)/(?:(?P\d+)/)?' + => 'HarbormasterBuildActionController', ), 'plan/' => array( '(?:query/(?P[^/]+)/)?' Index: src/applications/harbormaster/controller/HarbormasterBuildActionController.php =================================================================== --- /dev/null +++ src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -0,0 +1,146 @@ +id = $data['id']; + $this->action = $data['action']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $command = $this->action; + + $build = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$build) { + return new Aphront404Response(); + } + + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + $can_issue = $build->canRestartBuild(); + break; + case HarbormasterBuildCommand::COMMAND_STOP: + $can_issue = $build->canStopBuild(); + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + $can_issue = $build->canResumeBuild(); + break; + default: + return new Aphront400Response(); + } + + $build_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); + + if ($request->isDialogFormPost() && $can_issue) { + + // Issue the new build command. + id(new HarbormasterBuildCommand()) + ->setAuthorPHID($viewer->getPHID()) + ->setTargetPHID($build->getPHID()) + ->setCommand($command) + ->save(); + + // Schedule a build update. We may already have stuff in queue (in which + // case this will just no-op), but we might also be dealing with a + // stopped build, which won't restart unless we deal with this. + PhabricatorWorker::scheduleTask( + 'HarbormasterBuildWorker', + array( + 'buildID' => $build->getID() + )); + + return id(new AphrontRedirectResponse())->setURI($build_uri); + } + + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + if ($can_issue) { + $title = pht('Really restart build?'); + $body = pht( + 'Progress on this build will be discarded and the build will '. + 'restart. Side effects of the build will occur again. Really '. + 'restart build?'); + $submit = pht('Restart Build'); + } else { + $title = pht('Unable to Restart Build'); + if ($build->isRestarting()) { + $body = pht( + 'This build is already restarting. You can not reissue a '. + 'restart command to a restarting build.'); + } else { + $body = pht( + 'You can not restart this build.'); + } + } + break; + case HarbormasterBuildCommand::COMMAND_STOP: + if ($can_issue) { + $title = pht('Really stop build?'); + $body = pht( + 'If you stop this build, work will halt once the current steps '. + 'complete. You can resume the build later.'); + $submit = pht('Stop Build'); + } else { + $title = pht('Unable to Stop Build'); + if ($build->isComplete()) { + $body = pht( + 'This build is already complete. You can not stop a completed '. + 'build.'); + } else if ($build->isStopped()) { + $body = pht( + 'This build is already stopped. You can not stop a build which '. + 'has already been stopped.'); + } else if ($build->isStopping()) { + $body = pht( + 'This build is already stopping. You can not reissue a stop '. + 'command to a stopping build.'); + } else { + $body = pht( + 'This build can not be stopped.'); + } + } + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + if ($can_issue) { + $title = pht('Really resume build?'); + $body = pht( + 'Work will continue on the build. Really resume?'); + $submit = pht('Resume Build'); + } else { + $title = pht('Unable to Resume Build'); + if ($build->isResuming()) { + $body = pht( + 'This build is already resuming. You can not reissue a resume '. + 'command to a resuming build.'); + } else if (!$build->isStopped()) { + $body = pht( + 'This build is not stopped. You can only resume a stopped '. + 'build.'); + } + } + break; + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild($body) + ->addCancelButton($build_uri); + + if ($can_issue) { + $dialog->addSubmitButton($submit); + } + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} Index: src/applications/harbormaster/controller/HarbormasterBuildCancelController.php =================================================================== --- src/applications/harbormaster/controller/HarbormasterBuildCancelController.php +++ /dev/null @@ -1,49 +0,0 @@ -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(1); - $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); - } - -} Index: src/applications/harbormaster/controller/HarbormasterBuildViewController.php =================================================================== --- src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -214,25 +214,33 @@ ->setObject($build) ->setObjectURI("/build/{$id}"); - $action = + $can_restart = $build->canRestartBuild(); + $can_stop = $build->canStopBuild(); + $can_resume = $build->canResumeBuild(); + + $list->addAction( 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); + ->setName(pht('Restart Build')) + ->setIcon('backward') + ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) + ->setDisabled(!$can_restart) + ->setWorkflow(true)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Stop Build')) + ->setIcon('stop') + ->setHref($this->getApplicationURI('/build/stop/'.$id.'/')) + ->setDisabled(!$can_stop) + ->setWorkflow(true)); + + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Resume Build')) + ->setIcon('play') + ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) + ->setDisabled(!$can_resume) + ->setWorkflow(true)); return $list; } @@ -272,8 +280,8 @@ } private function getStatus(HarbormasterBuild $build) { - if ($build->getCancelRequested()) { - return pht('Cancelling'); + if ($build->isStopping()) { + return pht('Stopping'); } switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_INACTIVE: @@ -290,8 +298,8 @@ return pht('Failed'); case HarbormasterBuild::STATUS_ERROR: return pht('Unexpected Error'); - case HarbormasterBuild::STATUS_CANCELLED: - return pht('Cancelled'); + case HarbormasterBuild::STATUS_STOPPED: + return pht('Stopped'); default: return pht('Unknown'); } Index: src/applications/harbormaster/controller/HarbormasterBuildableListController.php =================================================================== --- src/applications/harbormaster/controller/HarbormasterBuildableListController.php +++ src/applications/harbormaster/controller/HarbormasterBuildableListController.php @@ -88,10 +88,6 @@ ->setViewer($user) ->addNavigationItems($nav->getMenu()); - if ($for_app) { - $nav->addFilter('new/', pht('New Build Plan')); - } - $nav->addLabel(pht('Build Plans')); $nav->addFilter('plan/', pht('Manage Build Plans')); Index: src/applications/harbormaster/controller/HarbormasterBuildableViewController.php =================================================================== --- src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -38,9 +38,9 @@ ->setObjectName(pht('Build %d', $build->getID())) ->setHeader($build->getName()) ->setHref($view_uri); - if ($build->getCancelRequested()) { + if ($build->isStopping()) { $item->setBarColor('black'); - $item->addAttribute(pht('Cancelling')); + $item->addAttribute(pht('Stopping')); } else { switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_INACTIVE: @@ -71,9 +71,9 @@ $item->setBarColor('red'); $item->addAttribute(pht('Unexpected Error')); break; - case HarbormasterBuild::STATUS_CANCELLED: + case HarbormasterBuild::STATUS_STOPPED: $item->setBarColor('black'); - $item->addAttribute(pht('Cancelled')); + $item->addAttribute(pht('Stopped')); break; } } Index: src/applications/harbormaster/controller/HarbormasterPlanController.php =================================================================== --- src/applications/harbormaster/controller/HarbormasterPlanController.php +++ src/applications/harbormaster/controller/HarbormasterPlanController.php @@ -1,6 +1,6 @@ setBaseURI(new PhutilURI($this->getApplicationURI())); + if ($for_app) { + $nav->addFilter('new/', pht('New Build Plan')); + } + id(new HarbormasterBuildPlanSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); Index: src/applications/harbormaster/engine/HarbormasterBuildEngine.php =================================================================== --- src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -72,16 +72,52 @@ } private function updateBuild(HarbormasterBuild $build) { - // TODO: Handle cancellation and restarts. - if ($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) { + $should_stop = false; + $should_resume = false; + $should_restart = false; + foreach ($build->getUnprocessedCommands() as $command) { + switch ($command->getCommand()) { + case HarbormasterBuildCommand::COMMAND_STOP: + $should_stop = true; + $should_resume = false; + break; + case HarbormasterBuildCommand::COMMAND_RESUME: + $should_resume = true; + $should_stop = false; + break; + case HarbormasterBuildCommand::COMMAND_RESTART: + $should_restart = true; + $should_resume = true; + $should_stop = false; + break; + } + } + + if (($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) || + ($should_restart)) { $this->destroyBuildTargets($build); $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); } + if ($should_resume) { + $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); + $build->save(); + } + + if ($should_stop && !$build->isComplete()) { + $build->setBuildStatus(HarbormasterBuild::STATUS_STOPPED); + $build->save(); + } + + foreach ($build->getUnprocessedCommands() as $command) { + $command->delete(); + } + $build->attachUnprocessedCommands(array()); + if ($build->getBuildStatus() == HarbormasterBuild::STATUS_BUILDING) { - return $this->updateBuildSteps($build); + $this->updateBuildSteps($build); } } @@ -118,8 +154,7 @@ if ($step_targets) { $is_complete = true; foreach ($step_targets as $target) { - // TODO: Move this to a top-level "status" field on BuildTarget. - if (!$target->getDetail('__done__')) { + if (!$target->isComplete()) { $is_complete = false; break; } @@ -127,8 +162,7 @@ $is_failed = false; foreach ($step_targets as $target) { - // TODO: Move this to a top-level "status" field on BuildTarget. - if ($target->getDetail('__failed__')) { + if ($target->isFailed()) { $is_failed = true; break; } @@ -212,7 +246,7 @@ foreach ($runnable as $runnable_step) { $target = HarbormasterBuildTarget::initializeNewBuildTarget( $build, - $step, + $runnable_step, $build->retrieveVariablesFromBuild()); $target->save(); Index: src/applications/harbormaster/event/HarbormasterUIEventListener.php =================================================================== --- src/applications/harbormaster/event/HarbormasterUIEventListener.php +++ src/applications/harbormaster/event/HarbormasterUIEventListener.php @@ -94,8 +94,8 @@ case HarbormasterBuild::STATUS_ERROR: $item->setIcon('minus-red', pht('Unexpected Error')); break; - case HarbormasterBuild::STATUS_CANCELLED: - $item->setIcon('minus-dark', pht('Cancelled')); + case HarbormasterBuild::STATUS_STOPPED: + $item->setIcon('minus-dark', pht('Stopped')); break; default: $item->setIcon('question', pht('Unknown')); Index: src/applications/harbormaster/query/HarbormasterBuildQuery.php =================================================================== --- src/applications/harbormaster/query/HarbormasterBuildQuery.php +++ src/applications/harbormaster/query/HarbormasterBuildQuery.php @@ -92,6 +92,16 @@ $build->attachBuildPlan(idx($plans, $plan_phid)); } + $build_phids = mpull($page, 'getPHID'); + $commands = id(new HarbormasterBuildCommand())->loadAllWhere( + 'targetPHID IN (%Ls) ORDER BY id ASC', + $build_phids); + $commands = mgroup($commands, 'getTargetPHID'); + foreach ($page as $build) { + $unprocessed_commands = idx($commands, $build->getPHID(), array()); + $build->attachUnprocessedCommands($unprocessed_commands); + } + return $page; } Index: src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php =================================================================== --- src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php +++ src/applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php @@ -113,7 +113,7 @@ ->execute(); foreach ($builds as $build) { - if ($build->isBuilding()) { + if (!$build->isComplete()) { $blockers[] = pht('Build %d', $build->getID()); } } Index: src/applications/harbormaster/storage/HarbormasterBuildCommand.php =================================================================== --- /dev/null +++ src/applications/harbormaster/storage/HarbormasterBuildCommand.php @@ -0,0 +1,13 @@ +setBuildStatus(self::STATUS_INACTIVE) - ->setCancelRequested(0); + ->setBuildStatus(self::STATUS_INACTIVE); } public function getConfiguration() { @@ -97,8 +96,7 @@ public function isBuilding() { return $this->getBuildStatus() === self::STATUS_PENDING || $this->getBuildStatus() === self::STATUS_WAITING || - $this->getBuildStatus() === self::STATUS_BUILDING || - $this->getCancelRequested(); + $this->getBuildStatus() === self::STATUS_BUILDING; } public function createLog( @@ -106,10 +104,11 @@ $log_source, $log_type) { - $log = HarbormasterBuildLog::initializeNewBuildLog($build_target); - $log->setLogSource($log_source); - $log->setLogType($log_type); - $log->save(); + $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) + ->setLogSource($log_source) + ->setLogType($log_type) + ->save(); + return $log; } @@ -139,25 +138,6 @@ return $artifact; } - /** - * 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(0); - $this->save(); - return true; - } - return false; - } - public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, @@ -212,6 +192,71 @@ 'build.id' => pht('The ID of the current build.')); } + public function isComplete() { + switch ($this->getBuildStatus()) { + case self::STATUS_PASSED: + case self::STATUS_FAILED: + case self::STATUS_ERROR: + case self::STATUS_STOPPED: + return true; + } + + return false; + } + + public function isStopped() { + return ($this->getBuildStatus() == self::STATUS_STOPPED); + } + + +/* -( Build Commands )----------------------------------------------------- */ + + + public function getUnprocessedCommands() { + return $this->assertAttached($this->unprocessedCommands); + } + + public function attachUnprocessedCommands(array $commands) { + $this->unprocessedCommands = $commands; + return $this; + } + + public function hasWaitingCommand($command_name) { + foreach ($this->getUnprocessedCommands() as $command_object) { + if ($command_object->getCommand() == $command_name) { + return true; + } + } + return false; + } + + public function canRestartBuild() { + return !$this->isRestarting(); + } + + public function canStopBuild() { + return !$this->isComplete() && + !$this->isStopped() && + !$this->isStopping(); + } + + public function canResumeBuild() { + return $this->isStopped() && + !$this->isResuming(); + } + + public function isStopping() { + return $this->hasWaitingCommand(HarbormasterBuildCommand::COMMAND_STOP); + } + + public function isResuming() { + return $this->hasWaitingCommand(HarbormasterBuildCommand::COMMAND_RESUME); + } + + public function isRestarting() { + return $this->hasWaitingCommand(HarbormasterBuildCommand::COMMAND_RESTART); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ Index: src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php =================================================================== --- src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -8,6 +8,11 @@ protected $className; protected $details; protected $variables; + protected $targetStatus; + + const STATUS_PENDING = 'target/pending'; + const STATUS_PASSED = 'target/passed'; + const STATUS_FAILED = 'target/failed'; private $build = self::ATTACHABLE; private $buildStep = self::ATTACHABLE; @@ -21,6 +26,7 @@ ->setBuildStepPHID($build_step->getPHID()) ->setClassName($build_step->getClassName()) ->setDetails($build_step->getDetails()) + ->setTargetStatus(self::STATUS_PENDING) ->setVariables($variables); } @@ -96,6 +102,30 @@ } +/* -( Status )------------------------------------------------------------- */ + + + public function isComplete() { + switch ($this->getTargetStatus()) { + case self::STATUS_PASSED: + case self::STATUS_FAILED: + return true; + } + + return false; + } + + + public function isFailed() { + switch ($this->getTargetStatus()) { + case self::STATUS_FAILED: + return true; + } + + return false; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ Index: src/applications/harbormaster/worker/HarbormasterTargetWorker.php =================================================================== --- src/applications/harbormaster/worker/HarbormasterTargetWorker.php +++ src/applications/harbormaster/worker/HarbormasterTargetWorker.php @@ -38,15 +38,15 @@ try { $implementation = $target->getImplementation(); if (!$implementation->validateSettings()) { - $target->setDetail('__failed__', true); + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); $target->save(); } else { $implementation->execute($build, $target); - $target->setDetail('__done__', true); + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_PASSED); $target->save(); } } catch (Exception $ex) { - $target->setDetail('__failed__', true); + $target->setTargetStatus(HarbormasterBuildTarget::STATUS_FAILED); $target->save(); }