Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / harbormaster / conduit / HarbormasterSendMessageConduitAPIMethod.php
blobef03c87c2143a86c22413f4ab7b484b723c7fadb
1 <?php
3 final class HarbormasterSendMessageConduitAPIMethod
4 extends HarbormasterConduitAPIMethod {
6 public function getAPIMethodName() {
7 return 'harbormaster.sendmessage';
10 public function getMethodSummary() {
11 return pht(
12 'Modify running builds, and report build results.');
15 public function getMethodDescription() {
16 return pht(<<<EOREMARKUP
17 Pause, abort, restart, and report results for builds.
18 EOREMARKUP
22 protected function newDocumentationPages(PhabricatorUser $viewer) {
23 $pages = array();
25 $pages[] = $this->newSendingDocumentationBoxPage($viewer);
26 $pages[] = $this->newBuildsDocumentationBoxPage($viewer);
27 $pages[] = $this->newCommandsDocumentationBoxPage($viewer);
28 $pages[] = $this->newTargetsDocumentationBoxPage($viewer);
29 $pages[] = $this->newUnitDocumentationBoxPage($viewer);
30 $pages[] = $this->newLintDocumentationBoxPage($viewer);
32 return $pages;
35 private function newSendingDocumentationBoxPage(PhabricatorUser $viewer) {
36 $title = pht('Sending Messages');
37 $content = pht(<<<EOREMARKUP
38 Harbormaster build objects work somewhat differently from objects in many other
39 applications. Most application objects can be edited directly using synchronous
40 APIs (like `maniphest.edit`, `differential.revision.edit`, and so on).
42 However, builds require long-running background processing and Habormaster
43 objects have a more complex lifecycle than most other application objects and
44 may spend significant periods of time locked by daemon processes during build
45 execition. A synchronous edit might need to wait an arbitrarily long amount of
46 time for this lock to become available so the edit could be applied.
48 Additionally, some edits may also require an arbitrarily long amount of time to
49 //complete//. For example, aborting a build may execute cleanup steps which
50 take minutes (or even hours) to complete.
52 Since a synchronous API could not guarantee it could return results to the
53 caller in a reasonable amount of time, the edit API for Harbormaster build
54 objects is asynchronous: to update a Harbormaster build or build target, use
55 this API (`harbormaster.sendmessage`) to send it a message describing an edit
56 you would like to effect or additional information you want to provide.
57 The message will be processed by the daemons once the build or target reaches
58 a suitable state to receive messages.
60 Select an object to send a message to using the `receiver` parameter. This
61 API method can send messages to multiple types of objects:
63 <table>
64 <tr>
65 <th>Object Type</th>
66 <th>PHID Example</th>
67 <th>Description</th>
68 </tr>
69 <tr>
70 <td>Harbormaster Buildable</td>
71 <td>`PHID-HMBB-...`</td>
72 <td>%s</td>
73 </tr>
74 <tr>
75 <td>Harbormaster Build</td>
76 <td>`PHID-HMBD-...`</td>
77 <td>%s</td>
78 </tr>
79 <tr>
80 <td>Harbormaster Build Target</td>
81 <td>`PHID-HMBT-...`</td>
82 <td>%s</td>
83 </tr>
84 </table>
86 See below for specifics on sending messages to different object types.
87 EOREMARKUP
89 pht(
90 'Buildables may receive control commands like "abort" and "restart". '.
91 'Sending a control command to a Buildable is the same as sending it '.
92 'to each Build for the Buildable.'),
93 pht(
94 'Builds may receive control commands like "pause", "resume", "abort", '.
95 'and "restart".'),
96 pht(
97 'Build Targets may receive build status and result messages, like '.
98 '"pass" or "fail".'));
100 $content = $this->newRemarkupDocumentationView($content);
102 return $this->newDocumentationBoxPage($viewer, $title, $content)
103 ->setAnchor('sending')
104 ->setIconIcon('fa-envelope-o');
107 private function newBuildsDocumentationBoxPage(PhabricatorUser $viewer) {
108 $title = pht('Updating Builds');
110 $content = pht(<<<EOREMARKUP
111 You can use this method (`harbormaster.sendmessage`) to send control commands
112 to Buildables and Builds.
114 Specify the Build or Buildable to receive the control command by providing its
115 PHID in the `receiver` parameter.
117 Sending a control command to a Buildable has the same effect as sending it to
118 each Build for the Buildable. For example, sending a "Pause" message to a
119 Buildable will pause all builds for the Buildable (or at least attempt to).
121 When sending control commands, the `unit` and `lint` parameters of this API
122 method must be omitted. You can not report lint or unit results directly to
123 a Build or Buildable, and can not report them alongside a control command.
125 More broadly, you can not report build results directly to a Build or
126 Buildable. Instead, report results to a Build Target.
128 See below for a list of control commands.
130 EOREMARKUP
133 $content = $this->newRemarkupDocumentationView($content);
135 return $this->newDocumentationBoxPage($viewer, $title, $content)
136 ->setAnchor('builds')
137 ->setIconIcon('fa-cubes');
140 private function newCommandsDocumentationBoxPage(PhabricatorUser $viewer) {
141 $messages = HarbormasterBuildMessageTransaction::getAllMessages();
143 $rows = array();
145 $rows[] = '<tr>';
146 $rows[] = '<th>'.pht('Message Type').'</th>';
147 $rows[] = '<th>'.pht('Description').'</th>';
148 $rows[] = '</tr>';
150 foreach ($messages as $message) {
151 $row = array();
153 $row[] = sprintf(
154 '<td>`%s`</td>',
155 $message->getHarbormasterBuildMessageType());
157 $row[] = sprintf(
158 '<td>%s</td>',
159 $message->getHarbormasterBuildMessageDescription());
161 $rows[] = sprintf(
162 '<tr>%s</tr>',
163 implode("\n", $row));
166 $message_table = sprintf(
167 '<table>%s</table>',
168 implode("\n", $rows));
170 $title = pht('Control Commands');
172 $content = pht(<<<EOREMARKUP
173 You can use this method to send control commands to Buildables and Builds.
175 This table summarizes which object types may receive control commands:
177 <table>
178 <tr>
179 <th>Object Type</th>
180 <th>PHID Example</th>
181 <th />
182 <th>Description</th>
183 </tr>
184 <tr>
185 <td>Harbormaster Buildable</td>
186 <td>`PHID-HMBB-...`</td>
187 <td>{icon check color=green}</td>
188 <td>Buildables may receive control commands.</td>
189 </tr>
190 <tr>
191 <td>Harbormaster Build</td>
192 <td>`PHID-HMBD-...`</td>
193 <td>{icon check color=green}</td>
194 <td>Builds may receive control commands.</td>
195 </tr>
196 <tr>
197 <td>Harbormaster Build Target</td>
198 <td>`PHID-HMBT-...`</td>
199 <td>{icon times color=red}</td>
200 <td>You may **NOT** send control commands to build targets.</td>
201 </tr>
202 </table>
204 You can send these commands:
208 To send a command message, specify the PHID of the object you would like to
209 receive the message using the `receiver` parameter, and specify the message
210 type using the `type` parameter.
212 EOREMARKUP
214 $message_table);
216 $content = $this->newRemarkupDocumentationView($content);
218 return $this->newDocumentationBoxPage($viewer, $title, $content)
219 ->setAnchor('commands')
220 ->setIconIcon('fa-exclamation-triangle');
223 private function newTargetsDocumentationBoxPage(PhabricatorUser $viewer) {
224 $messages = HarbormasterMessageType::getAllMessages();
226 $head_type = pht('Type');
227 $head_desc = pht('Description');
229 $rows = array();
230 $rows[] = "| {$head_type} | {$head_desc} |";
231 $rows[] = '|--------------|--------------|';
232 foreach ($messages as $message) {
233 $description = HarbormasterMessageType::getMessageDescription($message);
234 $rows[] = "| `{$message}` | {$description} |";
236 $message_table = implode("\n", $rows);
238 $content = pht(<<<EOREMARKUP
239 If you run external builds, you can use this method to publish build results
240 back into Harbormaster after the external system finishes work (or as it makes
241 progress).
243 To report build status or results, you must send a message to the appropriate
244 Build Target. This table summarizes which object types may receive build status
245 and result messages:
247 <table>
248 <tr>
249 <th>Object Type</th>
250 <th>PHID Example</th>
251 <th />
252 <th>Description</th>
253 </tr>
254 <tr>
255 <td>Harbormaster Buildable</td>
256 <td>`PHID-HMBB-...`</td>
257 <td>{icon times color=red}</td>
258 <td>Buildables may **NOT** receive status or result messages.</td>
259 </tr>
260 <tr>
261 <td>Harbormaster Build</td>
262 <td>`PHID-HMBD-...`</td>
263 <td>{icon times color=red}</td>
264 <td>Builds may **NOT** receive status or result messages.</td>
265 </tr>
266 <tr>
267 <td>Harbormaster Build Target</td>
268 <td>`PHID-HMBT-...`</td>
269 <td>{icon check color=green}</td>
270 <td>Report build status and results to Build Targets.</td>
271 </tr>
272 </table>
274 The simplest way to use this method to report build results is to call it once
275 after the build finishes with a `pass` or `fail` message. This will record the
276 build result, and continue the next step in the build if the build was waiting
277 for a result.
279 When you send a status message about a build target, you can optionally include
280 detailed `lint` or `unit` results alongside the message. See below for details.
282 If you want to report intermediate results but a build hasn't completed yet,
283 you can use the `work` message. This message doesn't have any direct effects,
284 but allows you to send additional data to update the progress of the build
285 target. The target will continue waiting for a completion message, but the UI
286 will update to show the progress which has been made.
288 When sending a message to a build target to report the status or results of
289 a build, your message must include a `type` which describes the overall state
290 of the build. For example, use `pass` to tell Harbormaster that a build target
291 completed successfully.
293 Supported message types are:
297 EOREMARKUP
299 $message_table);
301 $title = pht('Updating Build Targets');
303 $content = $this->newRemarkupDocumentationView($content);
305 return $this->newDocumentationBoxPage($viewer, $title, $content)
306 ->setAnchor('targets')
307 ->setIconIcon('fa-bullseye');
310 private function newUnitDocumentationBoxPage(PhabricatorUser $viewer) {
311 $head_key = pht('Key');
312 $head_desc = pht('Description');
313 $head_name = pht('Name');
314 $head_type = pht('Type');
316 $rows = array();
317 $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
318 $rows[] = '|-------------|--------------|--------------|';
319 $unit_spec = HarbormasterBuildUnitMessage::getParameterSpec();
320 foreach ($unit_spec as $key => $parameter) {
321 $type = idx($parameter, 'type');
322 $type = str_replace('|', ' '.pht('or').' ', $type);
323 $description = idx($parameter, 'description');
324 $rows[] = "| `{$key}` | //{$type}// | {$description} |";
326 $unit_table = implode("\n", $rows);
328 $rows = array();
329 $rows[] = "| {$head_key} | {$head_name} | {$head_desc} |";
330 $rows[] = '|-------------|--------------|--------------|';
331 $results = ArcanistUnitTestResult::getAllResultCodes();
332 foreach ($results as $result_code) {
333 $name = ArcanistUnitTestResult::getResultCodeName($result_code);
334 $description = ArcanistUnitTestResult::getResultCodeDescription(
335 $result_code);
336 $rows[] = "| `{$result_code}` | **{$name}** | {$description} |";
338 $result_table = implode("\n", $rows);
340 $valid_unit = array(
341 array(
342 'name' => 'PassingTest',
343 'result' => ArcanistUnitTestResult::RESULT_PASS,
345 array(
346 'name' => 'FailingTest',
347 'result' => ArcanistUnitTestResult::RESULT_FAIL,
351 $json = new PhutilJSON();
352 $valid_unit = $json->encodeAsList($valid_unit);
355 $title = pht('Reporting Unit Results');
357 $content = pht(<<<EOREMARKUP
358 You can report test results when updating the state of a build target. The
359 simplest way to do this is to report all the results alongside a `pass` or
360 `fail` message, but you can also send a `work` message to report intermediate
361 results.
364 To provide unit test results, pass a list of results in the `unit`
365 parameter. Each result should be a dictionary with these keys:
369 The `result` parameter recognizes these test results:
373 This is a simple, valid value for the `unit` parameter. It reports one passing
374 test and one failing test:
376 ```lang=json
379 EOREMARKUP
381 $unit_table,
382 $result_table,
383 $valid_unit);
385 $content = $this->newRemarkupDocumentationView($content);
387 return $this->newDocumentationBoxPage($viewer, $title, $content)
388 ->setAnchor('unit');
391 private function newLintDocumentationBoxPage(PhabricatorUser $viewer) {
393 $head_key = pht('Key');
394 $head_desc = pht('Description');
395 $head_name = pht('Name');
396 $head_type = pht('Type');
398 $rows = array();
399 $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |";
400 $rows[] = '|-------------|--------------|--------------|';
401 $lint_spec = HarbormasterBuildLintMessage::getParameterSpec();
402 foreach ($lint_spec as $key => $parameter) {
403 $type = idx($parameter, 'type');
404 $type = str_replace('|', ' '.pht('or').' ', $type);
405 $description = idx($parameter, 'description');
406 $rows[] = "| `{$key}` | //{$type}// | {$description} |";
408 $lint_table = implode("\n", $rows);
410 $rows = array();
411 $rows[] = "| {$head_key} | {$head_name} |";
412 $rows[] = '|-------------|--------------|';
413 $severities = ArcanistLintSeverity::getLintSeverities();
414 foreach ($severities as $key => $name) {
415 $rows[] = "| `{$key}` | **{$name}** |";
417 $severity_table = implode("\n", $rows);
419 $valid_lint = array(
420 array(
421 'name' => pht('Syntax Error'),
422 'code' => 'EXAMPLE1',
423 'severity' => ArcanistLintSeverity::SEVERITY_ERROR,
424 'path' => 'path/to/example.c',
425 'line' => 17,
426 'char' => 3,
428 array(
429 'name' => pht('Not A Haiku'),
430 'code' => 'EXAMPLE2',
431 'severity' => ArcanistLintSeverity::SEVERITY_ERROR,
432 'path' => 'path/to/source.cpp',
433 'line' => 23,
434 'char' => 1,
435 'description' => pht(
436 'This function definition is not a haiku.'),
440 $json = new PhutilJSON();
441 $valid_lint = $json->encodeAsList($valid_lint);
443 $title = pht('Reporting Lint Results');
444 $content = pht(<<<EOREMARKUP
445 Like unit test results, you can report lint results when updating the state
446 of a build target. The `lint` parameter should contain results as a list of
447 dictionaries with these keys:
451 The `severity` parameter recognizes these severity levels:
455 This is a simple, valid value for the `lint` parameter. It reports one error
456 and one warning:
458 ```lang=json
462 EOREMARKUP
464 $lint_table,
465 $severity_table,
466 $valid_lint);
468 $content = $this->newRemarkupDocumentationView($content);
470 return $this->newDocumentationBoxPage($viewer, $title, $content)
471 ->setAnchor('lint');
474 protected function defineParamTypes() {
475 $messages = HarbormasterMessageType::getAllMessages();
477 $more_messages = HarbormasterBuildMessageTransaction::getAllMessages();
478 $more_messages = mpull($more_messages, 'getHarbormasterBuildMessageType');
480 $messages = array_merge($messages, $more_messages);
481 $messages = array_unique($messages);
483 sort($messages);
485 $type_const = $this->formatStringConstants($messages);
487 return array(
488 'receiver' => 'required string|phid',
489 'type' => 'required '.$type_const,
490 'unit' => 'optional list<wild>',
491 'lint' => 'optional list<wild>',
492 'buildTargetPHID' => 'deprecated optional phid',
496 protected function defineReturnType() {
497 return 'void';
500 protected function execute(ConduitAPIRequest $request) {
501 $viewer = $request->getViewer();
503 $receiver_name = $request->getValue('receiver');
505 $build_target_phid = $request->getValue('buildTargetPHID');
506 if ($build_target_phid !== null) {
507 if ($receiver_name === null) {
508 $receiver_name = $build_target_phid;
509 } else {
510 throw new Exception(
511 pht(
512 'Call specifies both "receiver" and "buildTargetPHID". '.
513 'When using the modern "receiver" parameter, omit the '.
514 'deprecated "buildTargetPHID" parameter.'));
518 if ($receiver_name === null || !strlen($receiver_name)) {
519 throw new Exception(
520 pht(
521 'Call omits required "receiver" parameter. Specify the PHID '.
522 'of the object you want to send a message to.'));
525 $message_type = $request->getValue('type');
526 if ($message_type === null || !strlen($message_type)) {
527 throw new Exception(
528 pht(
529 'Call omits required "type" parameter. Specify the type of '.
530 'message you want to send.'));
533 $receiver_object = id(new PhabricatorObjectQuery())
534 ->setViewer($viewer)
535 ->withNames(array($receiver_name))
536 ->executeOne();
537 if (!$receiver_object) {
538 throw new Exception(
539 pht(
540 'Unable to load object "%s" to receive message.',
541 $receiver_name));
544 $is_target = ($receiver_object instanceof HarbormasterBuildTarget);
545 if ($is_target) {
546 return $this->sendToTarget($request, $message_type, $receiver_object);
549 if ($request->getValue('unit') !== null) {
550 throw new Exception(
551 pht(
552 'Call includes "unit" parameter. This parameter must be omitted '.
553 'when the receiver is not a Build Target.'));
556 if ($request->getValue('lint') !== null) {
557 throw new Exception(
558 pht(
559 'Call includes "lint" parameter. This parameter must be omitted '.
560 'when the receiver is not a Build Target.'));
563 $is_build = ($receiver_object instanceof HarbormasterBuild);
564 if ($is_build) {
565 return $this->sendToBuild($request, $message_type, $receiver_object);
568 $is_buildable = ($receiver_object instanceof HarbormasterBuildable);
569 if ($is_buildable) {
570 return $this->sendToBuildable($request, $message_type, $receiver_object);
573 throw new Exception(
574 pht(
575 'Receiver object (of class "%s") is not a valid receiver.',
576 get_class($receiver_object)));
579 private function sendToTarget(
580 ConduitAPIRequest $request,
581 $message_type,
582 HarbormasterBuildTarget $build_target) {
583 $viewer = $request->getViewer();
585 $save = array();
587 $lint_messages = $request->getValue('lint', array());
588 foreach ($lint_messages as $lint) {
589 $save[] = HarbormasterBuildLintMessage::newFromDictionary(
590 $build_target,
591 $lint);
594 $unit_messages = $request->getValue('unit', array());
595 foreach ($unit_messages as $unit) {
596 $save[] = HarbormasterBuildUnitMessage::newFromDictionary(
597 $build_target,
598 $unit);
601 $save[] = HarbormasterBuildMessage::initializeNewMessage($viewer)
602 ->setReceiverPHID($build_target->getPHID())
603 ->setType($message_type);
605 $build_target->openTransaction();
606 foreach ($save as $object) {
607 $object->save();
609 $build_target->saveTransaction();
611 // If the build has completely paused because all steps are blocked on
612 // waiting targets, this will resume it.
613 $build = $build_target->getBuild();
615 PhabricatorWorker::scheduleTask(
616 'HarbormasterBuildWorker',
617 array(
618 'buildID' => $build->getID(),
620 array(
621 'objectPHID' => $build->getPHID(),
624 return null;
627 private function sendToBuild(
628 ConduitAPIRequest $request,
629 $message_type,
630 HarbormasterBuild $build) {
631 $viewer = $request->getViewer();
633 $xaction =
634 HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
635 $message_type);
636 if (!$xaction) {
637 throw new Exception(
638 pht(
639 'Message type "%s" is not supported.',
640 $message_type));
643 // NOTE: This is a slightly weaker check than we perform in the web UI.
644 // We allow API callers to send a "pause" message to a pausing build,
645 // for example, even though the message will have no effect.
646 $xaction->assertCanApplyMessage($viewer, $build);
648 $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());
651 private function sendToBuildable(
652 ConduitAPIRequest $request,
653 $message_type,
654 HarbormasterBuildable $buildable) {
655 $viewer = $request->getViewer();
657 $xaction =
658 HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
659 $message_type);
660 if (!$xaction) {
661 throw new Exception(
662 pht(
663 'Message type "%s" is not supported.',
664 $message_type));
667 // Reload the Buildable to load Builds.
668 $buildable = id(new HarbormasterBuildableQuery())
669 ->setViewer($viewer)
670 ->withIDs(array($buildable->getID()))
671 ->needBuilds(true)
672 ->executeOne();
674 $can_send = array();
675 foreach ($buildable->getBuilds() as $build) {
676 if ($xaction->canApplyMessage($viewer, $build)) {
677 $can_send[] = $build;
681 // NOTE: This doesn't actually apply a transaction to the Buildable,
682 // but that transaction is purely informational and should probably be
683 // implemented as a Message.
685 foreach ($can_send as $build) {
686 $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());