Lick Guide
In this guide we'll write a pair of simple apps to demonstrate how Lick works. One will be a Gall agent called licker.hoon
, and the other a Python script called licker.py
.
The Gall agent will create a socket through Lick and the Python script will connect to it. When the Gall agent is poked with a message of %ping
, it'll send it through the socket to the Python script. The Python script will print ping!
, then send a %pong
message back through the socket to the Gall agent, which will print pong!
to the Dojo.
First, we'll look at these two files.
licker.hoon
licker.hoon
Our Gall agent is extremely simple and has no state. It only uses three agent arms: ++on-init
, ++on-poke
and ++on-arvo
.
++on-init
++on-init
++ on-init
^- (quip card _this)
:_ this
[%pass /lick %arvo %l %spin /'licker.sock']~
All ++on-init
does is pass Lick a %spin
task to create a new licker.sock
socket.
++on-poke
++on-poke
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> ?=([%noun %ping] [mark !<(@tas vase)])
:_ this
[%pass /spit %arvo %l %spit /'licker.sock' %noun %ping]~
When ++on-poke
receives a poke with a mark
of %noun
and data of %ping
, it passes Lick a %spit
task with the same data. Lick will send it on through to our licker.sock
socket for our Python script. This lets us poke our agent from the Dojo like:
> :licker %ping
++on-arvo
++on-arvo
++ on-arvo
|= [=wire sign=sign-arvo]
^- (quip card _this)
?. ?=([%lick %soak *] sign) (on-arvo:def +<)
?+ [mark noun]:sign (on-arvo:def +<)
[%connect ~] ((slog 'socket connected' ~) `this)
[%disconnect ~] ((slog 'socket disconnected' ~) `this)
[%error *] ((slog leaf+"socket {(trip ;;(@t noun.sign))}" ~) `this)
[%noun %pong] ((slog 'pong!' ~) `this)
==
++on-arvo
expects a %soak
gift from Lick. A %soak
is primarily a message coming in from the socket, though connection status is also communicated in %soak
s. The four cases we handle are:
%connect
: An external process has connected to the socket.%disconnect
: An external process has disconnected from the socket.%error
: An error has occurred. The error message is acord
in thenoun
. The only time you'll get this is if you tried to%spit
a message to the socket but there was nothing connected to it. In that case, the error message will be'not connected'
.[%noun %pong]
: This is the successful response we expect from the Python script.
In all cases we just ++slog
a message to the terminal.
licker.py
licker.py
Our Python script is also quite simple. We'll walk through it piece by piece.
from noun import *
import socket
First, we import the socket
library and noun.py
.
def cue_data(data):
x = cue(int.from_bytes(data[5:], 'little'))
mark = intbytes(x.head).decode()
noun = x.tail
return (mark,noun)
This function takes some data from the socket, decodes it, and returns a pair of the mark
and noun
. The data initially has the following format:
[1B: version][4B: size of jam in bytes][nB: jammed data]
The version is always 0
(though this may change in the future). The cue_data
function just strips off the the version and size headers, but you may wish to verify these.
After that, cue_data
converts the jam to an integer and passes it to the cue
function in noun.py
to decode. It converts the mark
to a string, then returns it along with the raw noun.
def jam_result(mark, msg):
mark = int.from_bytes(mark.encode(), 'little')
noun = int.from_bytes(msg.encode(), 'little')
return intbytes(jam(Cell(mark, noun)))
This function takes a mark
string and msg
string, converts them to integers, forms a cell and jams them with the jam
function in noun.py
. It's used to produce the jam when sending something back to the socket.
def make_output(jammed):
length = len(jammed).to_bytes(4, 'little')
version = (0).to_bytes(1, 'little')
return version+length+jammed
Once jam_result
has been run, make_output
calculates the length of the jam, sets the version number, and puts it all together so it can be sent off to the socket.
sock_path = '/home/user/piers/zod/.urb/dev/licker/licker.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sock_path)
Here we specify the path to the socket and open the connection. Lick sockets live in:
<pier>/.urb/dev/<agent>/<socket name>
You'll need to change sock_path
to your pier location.
while True:
try:
data = sock.recv(1024)
mark, noun = cue_data(data)
except TimeoutError:
pass
if (mark != 'noun'):
pass
msg = intbytes(noun).decode()
if (msg != 'ping'):
pass
print('ping!')
jammed = jam_result('noun', 'pong')
output = make_output(jammed)
sock.send(output)
This is the main loop of our script. It listens for a message from the socket, calls cue_data
to decode it, checks it's an expected ping
, prints it, produces a pong
in response and sends it back to the socket.
Setup
Create the folders for the project:
mkdir -p licker/{desk,client}
mkdir licker/desk/{app,lib,mar}
In the Dojo of a fakezod, mount the %base
desk:
|mount %base
Copy across some dependencies (change the pier path if necessary):
cp -r zod/base/mar/{bill*,hoon*,kelvin*,mime*,noun*,txt*} licker/desk/mar/
cp -r zod/base/lib/{default-agent*,skeleton*} licker/desk/lib/
Add a desk.bill
sys.kelvin
files:
echo "[%zuse 410]" > licker/desk/sys.kelvin
echo "~[%licker]" > licker/desk/desk.bill
Open a licker.hoon
app in an editor, paste in the licker.hoon
code above, and save it:
nano licker/desk/app/licker.hoon
Open a licker.py
file in an editor, paste in the licker.py
code above, and save it:
nano licker/client/licker.py
Download the noun.py
dependency from the urbit/tools repo:
wget -P licker/client https://raw.githubusercontent.com/urbit/build-on-urbit/tools/master/pkg/pynoun/noun.py
Install additional python dependencies bitstream
, mmh3
and numpy
:
python -m ensurepip
pip install bitstream mmh3 numpy
Create and mount the %licker
desk in the Dojo:
|new-desk %licker
|mount %licker
Delete the existing files and copy in the new ones:
rm -r zod/licker/*
cp -r licker/desk/* zod/licker/
In the Dojo, commit the files and install the desk:
|commit %licker
|install our %licker
Try it out
First, run the Python script:
python licker/client/licker.py
You should see the following in the Dojo:
socket connected
Now, try poking the %licker
agent with %ping
:
:licker %ping
In the terminal running the Python script, you should see:
ping!
And in the Dojo, you should see the response:
pong!
Now try closing the Python script. You should see the following in the Dojo:
socket disconnected
If you try :licker %ping
again, you'll see this error message:
socket not connected
Last updated