Ship Monitoring
The %ahoy desk by ~midden-fabler provides a number of agents to automatically monitor ship activity such as breaching and network uptime. This tutorial examines the %ahoy agent specifically with some slight simplifications to demonstrate how an Urbit-native app can be constructed. You will see how to render a front-end using Sail, employ the +abet nested core design pattern, construct CLI generators, and set wakeup timers using Behn.
%ahoy presents a web UI at /ahoy rendered using Sail and ~paldev's Rudder library alongside command-line generators to add, delete, and modify ship watches. Notifications are sent using %hark-store if a ship hasn't been contacted after a specified amount of time.
:ahoy|add-watch ~sampel ~d1
:ahoy|del-watch ~sampel
:ahoy|set-update-interval ~m30/sur Structure Files
/sur Structure FilesAs with other agents, we think about our data structures and actions before we dive into the agent code. The structure file here defines the state for the agent, records, which is a collection of ships to watch and the update interval for sending notifications.
+$ records
$: watchlist=(map ship @dr)
update-interval=@dr
==Three commands are supported: to add a ship to the watchlist at a given watch interval, to delete the ship, or to change the check interval. (Modifying a ship is the same as adding it.)
+$ command
$% [%add-watch =ship t=@dr]
[%del-watch =ship]
[%set-update-interval t=@dr]
==/sur/ahoy.hoon:
|%
+$ records
$: watchlist=(map ship @dr)
update-interval=@dr
==
+$ command
$% [%add-watch =ship t=@dr]
[%del-watch =ship]
[%set-update-interval t=@dr]
==
--No special mark files are necessary for %ahoy.
/app Agent Files
/app Agent FilesThe agent itself is simple: it maintains records as state and processes pokes from generators or the front-end and gifts from %behn in particular.
In addition, %ahoy sends notifications using %hark-store, the notification process integrated with Landscape and Grid.
Pokes
At the macro level, +on-poke recognizes three poke cages:
%nounfor pinging a ship.%ahoy-commandfor commands per/sur/ahoy.hoon.handle-http-requestfor displaying the webpage.
Most of the poke work takes place through %ahoy-command, which checks on the ship state per Ames’ scheme of %alien and %known ships, then maintains the agent state by its watchlist.
%ahoy-command
=+ !<(cmd=command vase)
?- -.cmd
%add-watch
=/ ss=(unit ship-state:ames)
(~(ship-state ahoy bowl) ship.cmd)
?~ ss
~& >> [%ahoy '%alien ship not added']
[~ this]
:- [(send-plea:hc ship.cmd)]~
this(watchlist (~(put by watchlist) ship.cmd t.cmd))
::
%del-watch
`this(watchlist (~(del by watchlist) ship.cmd))
::
%set-update-interval
`this(update-interval t.cmd)
==HTTP requests are processed into a form useful to rudder, a front-end rendering library for native Hoon webpages. rudder facilitates a Sail-based webpage being exposed through three arms:
+argueresponds toPOSTrequests.+finalis called afterPOSTrequests.+buildresponds toGETrequests, most commonly just yielding the webpage.
A number of other facilities in rudder are employed here as well:
+order:rudderis a type for handling inbound requests from Eyre.+steer:rudderis the helper constructor for producing pages.+point:rudderis a routing arm.+fours:rudderis a 404 error handler.+brief:rudderis a type union,?(~ @t).
%handle-http-request
=; out=(quip card _+.state)
[-.out this(+.state +.out)]
%. [bowl !<(order:rudder vase) +.state]
%- (steer:rudder _+.state command)
:^ pages
(point:rudder /[dap.bowl] & ~(key by pages))
(fours:rudder +.state)
|= cmd=command
^- $@ brief:rudder
[brief:rudder (list card) _+.state]
=^ cards this
(on-poke %ahoy-command !>(cmd))
['Processed succesfully.' cards +.state]Gifts
The agent expects to receive a %wake gift periodically from Behn on the wire %update-interval. It handles this by means of an arm in the agent's helper core, +on-update-interval.
[%update-interval ~]
=^ cards state
on-update-interval:hc
[cards this]This helper core arm notably employs the +abet nested core pattern for handling cards. The +abet nested core is a design pattern rather than a specific core. It is designed to accumulate cards, often using +emit and +emil, then send them all at once.
The +abet pattern itself is rather simple to construct. It enables other arms to construct a list of cards rather than having to produce complex =^-style constructions. This instance of the nested core pattern consists of three arms (omitting an +abed arm):
+emitis used to submit a card to a collection of cards in the helper core.+emilis similar but accepts a list of cards.+abetissues the list of cards back along with the state to be updated. (Note that the core must be scoped such that the Gall agent's state is visible.)
Other arms (such as +set-timer) then simply construct cards which are inserted into the +abet core's list.
For %ahoy, the main arm we need to examine is +on-update-interval. This arm resets the timer, sends checks to all of the ships, and then sends notifications to %hark-store for anything unresponsive.
++ on-update-interval
^- (quip card _state)
:: reset timer
=. this (emit (set-timer update-interval))
:: send pleas
=. this
%- emil
%+ turn ~(tap in ~(key by watchlist))
|= [who=ship]
(send-plea who)
:: send notifications
=. this
%- emil
%- zing
%+ turn ~(tap in down-status)
|= [who=ship]
(send-notification who)
abetThe +send-plea status check is interesting: it checks whether Ames is responsive on a particular ship without doing anything to the remote ship except eliciting an error. (|hi or similar would unnecessarily spam the recipient's Dojo.)
++ send-plea
|= [who=ship]
^- card
[%pass /ahoy/(scot %p who) %arvo %a %plea who %evil-vane / ~]%hark-store is the standard cross-agent notification store provided by Grid and recognized by Landscape. The notification message requires a little bit of explicit construction as $action but can be treated as boilerplate code aside from the text.
++ send-notification
|= [who=ship]
^- (list card)
?. .^(? %gu /(scot %p our.bowl)/hark-store/(scot %da now.bowl)) ~
=/ when=@dr (need (~(last-contact ahoy bowl) who))
=/ title=(list content:hark)
=- [ship+who - ~]
text+(crip " has not been contacted in {<when>}")
=/ =bin:hark [/[dap.bowl] q.byk.bowl /(scot %p who)]
=/ =action:hark [%add-note bin title ~ now.bowl / /[dap.bowl]]
=/ =cage [%hark-action !>(action)]
[%pass /hark %agent [our.bowl %hark-store] %poke cage]~The /lib/ahoy.hoon library file provides helper logic for determining ship status. In particular, scries are simplified. For instance, (~(last-contact ahoy bowl) ship) can be used instead of the scry below.
The CSS styling is included via a library core:
Rendering Sigils
Sigils are unique visual representations of @p ship identifiers. Many Urbit apps use sigils in small or large sizes as ship icons.
A sigil library is provided with ~paldev's Suite tools. We do not include the contents of /lib/sigil.hoon or /lib/sigil/symbols.hoon here due to their length.
The sigils are rendered in /app/ahoy/webui/index.hoon.
/gen Generator Files
/gen Generator FilesSome agents (notably %helm, a Dojo tool) are instrumented to work directly with generators at the command line. The %ahoy agent demonstrates this with several generator files such as /gen/add-watch.hoon, used thus:
:ahoy|add-watch ~sampel-palnet ~h2/gen/ahoy/add-watch.hoon
:: :ahoy|add-watch ~sampel ~d1
::
:- %say
|= $: ^
[who=ship t=@dr ~]
~
==
[%ahoy-command [%add-watch who t]]As you can see here, an %ahoy-command is generated which is then passed to the %ahoy agent as a poke using Dojo's | logic. (A generator called with Dojo's + logic would be located in /gen, whereas | tells Dojo to look inside the agent's folder, much like a /mar mark file.)
Such agent-specific generator files can be much cleaner than manual poke logic:
:ahoy|add-watch ~sampel-palnet ~h2is the equivalent of
:ahoy &ahoy-command [%add-watch ~zod ~h2]Exercise: Compose a Generator
Without consulting the
%ahoysource code, compose a generator/gen/ahoy/del-watch.hoonwhich removes a ship from the watchlist.
Last updated