mirror of
https://github.com/urbit/developers.urbit.org.git
synced 2024-10-05 17:57:51 +03:00
commit
2ab78f5521
27
content/reference/arvo/lick/_index.md
Normal file
27
content/reference/arvo/lick/_index.md
Normal file
@ -0,0 +1,27 @@
|
||||
+++
|
||||
title = "Lick"
|
||||
weight = 96
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
|
||||
## [Overview](/reference/arvo/lick/lick)
|
||||
|
||||
An overview of Lick, Urbit's timer vane.
|
||||
|
||||
## [API Reference](/reference/arvo/lick/tasks)
|
||||
|
||||
The `task`s Lick takes and the `gift`s it returns.
|
||||
|
||||
## [Scry Reference](/reference/arvo/lick/scry)
|
||||
|
||||
The scry endpoints of Lick.
|
||||
|
||||
## [Examples](/reference/arvo/lick/examples)
|
||||
|
||||
Practical examples of using Lick's `task`s.
|
||||
|
||||
|
||||
## [Guide](/reference/arvo/lick/guide)
|
||||
|
||||
A thorough walk-through of using Lick.
|
419
content/reference/arvo/lick/guide.md
Normal file
419
content/reference/arvo/lick/guide.md
Normal file
@ -0,0 +1,419 @@
|
||||
+++
|
||||
title = "Guide"
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
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`](#lickerhoon), 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`
|
||||
|
||||
```hoon {% copy=true mode="collapse" %}
|
||||
/+ default-agent
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[%pass /lick %arvo %l %spin /'licker.sock']~
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?> ?=([%noun %ping] [mark !<(@tas vase)])
|
||||
:_ this
|
||||
[%pass /spit %arvo %l %spit /'licker.sock' %noun %ping]~
|
||||
::
|
||||
++ 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-save on-save:def
|
||||
++ on-load on-load:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
```hoon
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[%pass /lick %arvo %l %spin /'licker.sock']~
|
||||
```
|
||||
|
||||
All `++on-init` does is pass Lick a
|
||||
[`%spin`](/reference/arvo/lick/tasks#spin) task to create a new
|
||||
`licker.sock` socket.
|
||||
|
||||
### `++on-poke`
|
||||
|
||||
```hoon
|
||||
++ 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`](/reference/arvo/lick/tasks#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`
|
||||
|
||||
```hoon
|
||||
++ 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`](/reference/arvo/lick/tasks#soak-1) 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 a `cord` in the
|
||||
`noun`. 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`
|
||||
|
||||
```python {% copy=true mode="collapse" %}
|
||||
from noun import *
|
||||
import socket
|
||||
|
||||
def cue_data(data):
|
||||
x = cue(int.from_bytes(data[5:], 'little'))
|
||||
mark = intbytes(x.head).decode()
|
||||
noun = x.tail
|
||||
return (mark,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)))
|
||||
|
||||
def make_output(jammed):
|
||||
length = len(jammed).to_bytes(4, 'little')
|
||||
version = (0).to_bytes(1, 'little')
|
||||
return version+length+jammed
|
||||
|
||||
sock_path = '/home/user/zod/.urb/dev/licker/licker.sock'
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(sock_path)
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
Our Python script is also quite simple. We'll walk through it piece by
|
||||
piece.
|
||||
|
||||
```python
|
||||
from noun import *
|
||||
import socket
|
||||
```
|
||||
|
||||
First, we import the `socket` library and
|
||||
[`noun.py`](https://github.com/urbit/tools).
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
``` {% copy=true %}
|
||||
mkdir -p licker/{desk,client}
|
||||
mkdir licker/desk/{app,lib,mar}
|
||||
```
|
||||
|
||||
In the Dojo of a fakezod, mount the `%base` desk:
|
||||
|
||||
``` {% copy=true %}
|
||||
|mount %base
|
||||
```
|
||||
|
||||
Copy across some dependencies (change the pier path if necessary):
|
||||
|
||||
``` {% copy=true %}
|
||||
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:
|
||||
|
||||
``` {% copy=true %}
|
||||
echo "[%zuse 412]" > 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:
|
||||
|
||||
``` {% copy=true %}
|
||||
nano licker/desk/app/licker.hoon
|
||||
```
|
||||
|
||||
Open a `licker.py` file in an editor, paste in the `licker.py` code
|
||||
above, and save it:
|
||||
|
||||
``` {% copy=true %}
|
||||
nano licker/client/licker.py
|
||||
```
|
||||
|
||||
Download the `noun.py` dependency from the
|
||||
[urbit/tools](https://github.com/urbit/tools/tree/master) repo:
|
||||
|
||||
``` {% copy=true %}
|
||||
wget -P licker/client https://raw.githubusercontent.com/urbit/tools/master/pkg/pynoun/noun.py
|
||||
```
|
||||
|
||||
Install additional python dependencies `bitstream`, `mmh3` and `numpy`:
|
||||
|
||||
{% callout %}
|
||||
|
||||
**NOTE:** At the time of writing, `bitstream` doesn't build against
|
||||
`python>3.10`. If you have `3.11` or newer, you may need to install a
|
||||
separate `python3.10` (how your distro packages it may vary).
|
||||
|
||||
{% /callout %}
|
||||
|
||||
``` {% copy=true %}
|
||||
python -m ensurepip
|
||||
pip install bitstream mmh3 numpy
|
||||
```
|
||||
|
||||
Create and mount the `%licker` desk in the Dojo:
|
||||
|
||||
``` {% copy=true %}
|
||||
|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:
|
||||
|
||||
``` {% copy=true %}
|
||||
python licker/client/licker.py
|
||||
```
|
||||
|
||||
You should see the following in the Dojo:
|
||||
|
||||
```
|
||||
socket connected
|
||||
```
|
||||
|
||||
Now, try poking the `%licker` agent with `%ping`:
|
||||
|
||||
``` {% copy=true %}
|
||||
: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
|
||||
```
|
||||
|
||||
---
|
47
content/reference/arvo/lick/lick.md
Normal file
47
content/reference/arvo/lick/lick.md
Normal file
@ -0,0 +1,47 @@
|
||||
+++
|
||||
title = "Overview"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
Urbit's inter-process communication (IPC) vane.
|
||||
|
||||
Lick manages IPC ports, and the communication between Urbit applications and
|
||||
POSIX applications via these ports. Other vanes and applications ask Lick to
|
||||
open an IPC port, notify it when something is connected or disconnected, and
|
||||
transfer data between itself and the Unix application.
|
||||
|
||||
The IPC ports Lick creates are Unix domain sockets (`AF_UNIX` address family)
|
||||
of the `SOCK_STREAM` type.
|
||||
|
||||
The format of the full message with header and data sent to and from the socket
|
||||
is as follows:
|
||||
|
||||
|1 byte |4 bytes |n bytes|
|
||||
|-------|-----------------|-------|
|
||||
|version|jam size in bytes|jamfile|
|
||||
|
||||
The version is currently `0`.
|
||||
|
||||
The [++jam](/reference/hoon/stdlib/2p#jam)file contains a pair of `mark` and
|
||||
`noun`. The process on the host OS must therefore strip the first 5 bytes,
|
||||
[`++cue`](/reference/hoon/stdlib/2p#cue) the jamfile, check the mark and (most
|
||||
likely) convert the noun into a native data structure.
|
||||
|
||||
Here are some libraries that can cue/jam:
|
||||
|
||||
- [`pynoun`](https://github.com/urbit/tools)
|
||||
- [`nockjs`](https://github.com/urbit/nockjs)
|
||||
- [Rust Noun](https://github.com/urbit/noun)
|
||||
|
||||
Lick has no novel data types in its API apart from `name`, which is just a
|
||||
`path` representing the name of a socket.
|
||||
|
||||
## Sections
|
||||
|
||||
[API Reference](/reference/arvo/lick/tasks) - The `task`s Lick takes and the `gift`s it returns.
|
||||
|
||||
[Scry Reference](/reference/arvo/lick/scry) - The scry endpoints of Lick.
|
||||
|
||||
[Examples](/reference/arvo/lick/examples) - Practical examples of using Lick's `task`s.
|
||||
|
||||
[Guide](/reference/arvo/lick/guide) - A thorough walk-through of using Lick.
|
Loading…
Reference in New Issue
Block a user