Cleanup
This commit is contained in:
parent
25b3f095b6
commit
91cc3a62e4
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
## Talk Timer
|
## Talk Timer
|
||||||
|
|
||||||
**TODO**: Add a buttload of emojis and absolutely hilarious GIFs to this document. Engineers (especially those of the Divvy persuasion) really, really, _really_ love emojis and GIFs.
|
**TODO**: Add a buttload of emojis and absolutely hilarious GIFs to this
|
||||||
|
document. Engineers (especially those of the Divvy persuasion) really, really,
|
||||||
|
_really_ love emojis and GIFs.
|
||||||
|
|
||||||
**TODO**: How can we get interactive and collaborative displays showing ports and connections opening to fully leverage Livebook-ness?
|
**TODO**: How can we get interactive and collaborative displays showing ports
|
||||||
|
and connections opening to fully leverage Livebook-ness?
|
||||||
|
|
||||||
This talk is supposed to be 5-15 minutes long, so let's make sure we keep it that way with a timer and a super annoying alert!
|
This talk is supposed to be 5-15 minutes long, so let's make sure we keep it
|
||||||
|
that way with a timer and a super annoying alert!
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
alias RanchTalk.TalkTimer
|
alias RanchTalk.TalkTimer
|
||||||
|
@ -22,7 +26,8 @@ alias RanchTalk.TalkTimer
|
||||||
|
|
||||||
## Ranch Introduction
|
## Ranch Introduction
|
||||||
|
|
||||||
> Special thanks to Cody Poll for the excuse to waste a ton of time playing around with Livebook!
|
> Special thanks to Cody Poll for the excuse to waste a ton of time playing
|
||||||
|
> around with Livebook!
|
||||||
|
|
||||||
From https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/:
|
From https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/:
|
||||||
|
|
||||||
|
@ -32,26 +37,36 @@ Ok, neat. What in the world does this mean?
|
||||||
|
|
||||||
### What is a Socket?
|
### What is a Socket?
|
||||||
|
|
||||||
I'm not going to go too deep into sockets for the purposes of this talk, so we will operate under this very basic definition of a socket:
|
I'm not going to go too deep into sockets for the purposes of this talk, so we
|
||||||
|
will operate under this very basic definition of a socket:
|
||||||
|
|
||||||
1. An interface provided by the operating system (OS, Linux in most cases)
|
1. An interface provided by the operating system (OS, Linux in most cases)
|
||||||
2. We can open and close sockets to indicate to the OS that we want it to receive packets or not, usually on a given network address and host port
|
2. We can open and close sockets to indicate to the OS that we want it to
|
||||||
3. If a socket is opened, it may or may not have packets for us to receive at any given time
|
receive packets or not, usually on a given network address and host port
|
||||||
|
3. If a socket is opened, it may or may not have packets for us to receive at
|
||||||
|
any given time
|
||||||
|
|
||||||
If you have a single-threaded process, you usually have an event loop that looks something like this:
|
If you have a single-threaded process, you usually have an event loop that
|
||||||
|
looks something like this:
|
||||||
|
|
||||||
1. Open a socket
|
1. Open a socket
|
||||||
2. Check for messages (or packets) in the socket
|
2. Check for messages (or packets) in the socket
|
||||||
3. If there are any messages available from the socket, handle them
|
3. If there are any messages available from the socket, handle them
|
||||||
4. Go to 2
|
4. Go to 2
|
||||||
|
|
||||||
This means that while you are processing messages, you cannot receive any more messages, so if it takes a long time to do step 3, your app ain't gonna scale.
|
This means that while you are processing messages, you cannot receive any more
|
||||||
|
messages, so if it takes a long time to do step 3, your app ain't gonna scale.
|
||||||
|
|
||||||
Now, Ranch is specifically for TCP sockets, which are slightly more complicated.
|
Now, Ranch is specifically for TCP sockets, which are slightly more
|
||||||
|
complicated.
|
||||||
|
|
||||||
#### TCP Sockets
|
#### TCP Sockets
|
||||||
|
|
||||||
The TCP protocol requires that we "establish" the connection first, due to its bi-directional nature. Contrast this with the UDP protocol, where you can just blast packets unidirectionally to hosts/ports and they'll get received if a socket is listening there or just dropped otherwise. This makes our event loop look _very loosely_ like this now:
|
The TCP protocol requires that we "establish" the connection first, due to its
|
||||||
|
bi-directional nature. Contrast this with the UDP protocol, where you can just
|
||||||
|
blast packets unidirectionally to hosts/ports and they'll get received if
|
||||||
|
a socket is listening there or just dropped otherwise. This makes our event
|
||||||
|
loop look _very loosely_ like this now:
|
||||||
|
|
||||||
1. Open a socket
|
1. Open a socket
|
||||||
2. Check if any pending connections exist on the socket
|
2. Check if any pending connections exist on the socket
|
||||||
|
@ -63,109 +78,168 @@ The TCP protocol requires that we "establish" the connection first, due to its b
|
||||||
7. If it's not, remove the connection
|
7. If it's not, remove the connection
|
||||||
8. Go to 2
|
8. Go to 2
|
||||||
|
|
||||||
You can quickly see that in a "classical" single-threaded program, this could get pretty overwhelming depending on what "handle them" might entail! You could pretty easily have messages piling up in your socket if your program is synchronously reaching out to a cache, then querying a database, or calling another service. It would be stuck waiting for all of these operations while new messages pour into your socket!
|
You can quickly see that in a "classical" single-threaded program, this could
|
||||||
|
get pretty overwhelming depending on what "handle them" might entail! You could
|
||||||
|
pretty easily have messages piling up in your socket if your program is
|
||||||
|
synchronously reaching out to a cache, then querying a database, or calling
|
||||||
|
another service. It would be stuck waiting for all of these operations while
|
||||||
|
new messages pour into your socket!
|
||||||
|
|
||||||
### How does Ranch solve this?
|
### How does Ranch solve this?
|
||||||
|
|
||||||
Well, let's imagine _you_ are the poor, single-threaded program taking care of all this stuff. You're running around like mad from the OS socket, to the connection list, shuffling messages all over the place, **and** you're responsible for reading every single one, processing it, and responding.
|
Well, let's imagine _you_ are the poor, single-threaded program taking care of
|
||||||
|
all this stuff. You're running around like mad from the OS socket, to the
|
||||||
|
connection list, shuffling messages all over the place, **and** you're
|
||||||
|
responsible for reading every single one, processing it, and responding.
|
||||||
|
|
||||||
Obviously, so modern web framework or socket library works this way for obvious reasons. You (or your machine) would be completely overwhelmed!
|
Obviously, so modern web framework or socket library works this way for obvious
|
||||||
|
reasons. You (or your machine) would be completely overwhelmed!
|
||||||
|
|
||||||
But this is where Ranch (and Erlang/OTP and Elixir) really shine.
|
But this is where Ranch (and Erlang/OTP and Elixir) really shine.
|
||||||
|
|
||||||
#### How would we ideally _want_ to solve this?
|
#### How would we ideally _want_ to solve this?
|
||||||
|
|
||||||
Let's imagine how we would _want_ this to play out. Just like a real-ish mailroom, instead of a single individual running around shuffling all the messages, we would want something like this:
|
Let's imagine how we would _want_ this to play out. Just like a real-ish
|
||||||
|
mailroom, instead of a single individual running around shuffling all the
|
||||||
|
messages, we would want something like this:
|
||||||
|
|
||||||
**Spoilers below!**
|
**Spoilers below!**
|
||||||
|
|
||||||
A bunch of people would constantly be checking the socket for new connections (an "acceptor pool", if you will). Then, when they get one, they take it to a connection manager, who then sets up a pool of listeners to handle messages as they come in. Those listeners take each message and hand it off to a dedicated handler, just for that message. Yep, each message would get _their own_ handler person (process) just for them!
|
A bunch of people would constantly be checking the socket for new connections
|
||||||
|
(an "acceptor pool", if you will). Then, when they get one, they take it to
|
||||||
|
a connection manager, who then sets up a pool of listeners to handle messages
|
||||||
|
as they come in. Those listeners take each message and hand it off to
|
||||||
|
a dedicated handler, just for that message. Yep, each message would get _their
|
||||||
|
own_ handler person (process) just for them!
|
||||||
|
|
||||||
This would be amazing! Now things are more asynchronous. Oh wait, maybe they're _too_ asynchronous. TCP is an _ordered_ protocol, after all, so we might want a single connection listener per connection, instead of a bunch of listeners.
|
This would be amazing! Now things are more asynchronous. Oh wait, maybe they're
|
||||||
|
_too_ asynchronous. TCP is an _ordered_ protocol, after all, so we might want
|
||||||
|
a single connection listener per connection, instead of a bunch of listeners.
|
||||||
|
|
||||||
And this is basically what ranch does! Other languages might have a single thread for this task of receiving connections or handling packets, but we're in Elixir-land, yo! We can have a process for everything!
|
And this is basically what ranch does! Other languages might have a single
|
||||||
|
thread for this task of receiving connections or handling packets, but we're in
|
||||||
|
Elixir-land, yo! We can have a process for everything!
|
||||||
|
|
||||||
So enough talk, let's see it in action!
|
So enough talk, let's see it in action!
|
||||||
|
|
||||||
## Investigating a Ranch
|
## Investigating a Ranch
|
||||||
|
|
||||||
For starters, we're inside a Livebook, which is a Phoenix LiveView application. Phoenix uses Cowboy as its HTTP(S) server. Cowboy uses Ranch for accepting incoming TCP connections _and_ handling packets from those connections. This means we're _already_ running Ranch and that we've already got at least one listener and connection active -- _you_!
|
For starters, we're inside a Livebook, which is a Phoenix LiveView application.
|
||||||
|
Phoenix uses Cowboy as its HTTP(S) server. Cowboy uses Ranch for accepting
|
||||||
|
incoming TCP connections _and_ handling packets from those connections. This
|
||||||
|
means we're _already_ running Ranch and that we've already got at least one
|
||||||
|
listener and connection active -- _you_!
|
||||||
|
|
||||||
Let's see if we can find ourselves. Erlang/OTP has a ton of awesome tools for looking at the primitives (processes, ports, and sockets), so lets look into some ways to see what we've already got happening, what's going on under the hood, and then let's build our own TCP acceptor pool to dive into.
|
Let's see if we can find ourselves. Erlang/OTP has a ton of awesome tools for
|
||||||
|
looking at the primitives (processes, ports, and sockets), so lets look into
|
||||||
|
some ways to see what we've already got happening, what's going on under the
|
||||||
|
hood, and then let's build our own TCP acceptor pool to dive into.
|
||||||
|
|
||||||
But before we just start looking for stuff blindly, let's investigate Ranch's documentation to see how it works so we know better what to look for. Don't worry, I'm not really going to make you read documentation yourself during a talk, so I've summarized the important stuff we'll look at below:
|
But before we just start looking for stuff blindly, let's investigate Ranch's
|
||||||
|
documentation to see how it works so we know better what to look for. Don't
|
||||||
|
worry, I'm not really going to make you read documentation yourself during
|
||||||
|
a talk, so I've summarized the important stuff we'll look at below:
|
||||||
|
|
||||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
|
* https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
|
||||||
* Just the stuff we've already talked about (minus all the boring socket detail stuff)
|
* Just the stuff we've already talked about (minus all the boring socket
|
||||||
|
detail stuff)
|
||||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
|
* https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
|
||||||
* We start Ranch by adding the dependency and running [`:application.ensure_all_started(:ranch)`](https://ninenines.eu/docs/en/ranch/2.1/manual/)
|
* We start Ranch by adding the dependency and running
|
||||||
* We can start a listener with [`:ranch.start_listener/5`](https://ninenines.eu/docs/en/ranch/2.1/manual/ranch.start_listener/)
|
[`:application.ensure_all_started(:ranch)`](https://ninenines.eu/docs/en/ranch/2.1/manual/)
|
||||||
|
* We can start a listener with
|
||||||
|
[`:ranch.start_listener/5`](https://ninenines.eu/docs/en/ranch/2.1/manual/ranch.start_listener/)
|
||||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
|
* https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
|
||||||
* Ranch is an OTP `Application` (named `:ranch`)
|
* Ranch is an OTP `Application` (named `:ranch`)
|
||||||
* It has a "top `Supervisor`" which supervises the `:ranch_server` process _and_ any listeners
|
* It has a "top `Supervisor`" which supervises the `:ranch_server` process
|
||||||
|
_and_ any listeners
|
||||||
* Ranch uses a "custom `Supervisor`" for managing connections
|
* Ranch uses a "custom `Supervisor`" for managing connections
|
||||||
* Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
|
* Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
|
||||||
* Listeners consist of three kinds of processes:
|
* Listeners consist of three kinds of processes:
|
||||||
* The listener `GenServer`
|
* The listener `GenServer`
|
||||||
* A `Supervisor` that watches the acceptor processes
|
* A `Supervisor` that watches the acceptor processes
|
||||||
* The second argument to `:ranch/start_listener/5` indicates the number of processes that will be accepting new connections and we should be careful choosing this number
|
* The second argument to `:ranch/start_listener/5` indicates the number
|
||||||
|
of processes that will be accepting new connections and we should be
|
||||||
|
careful choosing this number
|
||||||
* It defaults to `100`
|
* It defaults to `100`
|
||||||
* A `Supervisor` that watches the connection processes
|
* A `Supervisor` that watches the connection processes
|
||||||
* Each listener is registered with the `:ranch_server` `GenServer`
|
* Each listener is registered with the `:ranch_server` `GenServer`
|
||||||
* All socket operations go through "transport handlers"
|
* All socket operations go through "transport handlers"
|
||||||
* These are simple callback modules (`@behaviour`s) for performing operations on sockets
|
* These are simple callback modules (`@behaviour`s) for performing
|
||||||
* Accepted connections are given to "the protocol handler" (just TCP for our use case)
|
operations on sockets
|
||||||
|
* Accepted connections are given to "the protocol handler" (just TCP for our
|
||||||
|
use case)
|
||||||
|
|
||||||
Sweet! Armed with this knowledge, we should be able to find evidence of these facts in our system _right now_. Let's do it!
|
Sweet! Armed with this knowledge, we should be able to find evidence of these
|
||||||
|
facts in our system _right now_. Let's do it!
|
||||||
|
|
||||||
The first and most simple way to look at this stuff is using Livebook's built-in LiveDashboard. You can get to it [here](/dashboard).
|
The first and most simple way to look at this stuff is using Livebook's
|
||||||
|
built-in LiveDashboard. You can get to it [here](/dashboard).
|
||||||
|
|
||||||
**NOTE**: You can select either node from the top-right dropdown. Since Livebook attaches itself as a clustered node to my Mix project I had you clone and both of them are running `:ranch`, either will work!
|
**NOTE**: You can select either node from the top-right dropdown. Since
|
||||||
|
Livebook attaches itself as a clustered node to my Mix project I had you clone
|
||||||
|
and both of them are running `:ranch`, either will work!
|
||||||
|
|
||||||
**Everybody [opens the dashboard](/dashboard), obviously**
|
**Everybody [opens the dashboard](/dashboard), obviously**
|
||||||
|
|
||||||
Ok, looks nice and all, but what are we looking at now? By default, it drops us into an overview page with nothing relevant to this talk.
|
Ok, looks nice and all, but what are we looking at now? By default, it drops us
|
||||||
|
into an overview page with nothing relevant to this talk.
|
||||||
|
|
||||||
Let's go see if we can find the `:ranch` `Application` on [the Applications page](/dashboard/applications).
|
Let's go see if we can find the `:ranch` `Application` on [the Applications
|
||||||
|
page](/dashboard/applications).
|
||||||
|
|
||||||
`Ctrl-F "ranch"` - Easy enough! We can click on it and see the `:ranch_sup`, which is the "top supervisor" mentioned previously, and the `:ranch_server` `GenServer` also mentioned! Cool, they weren't lying to us... at least not completely.
|
`Ctrl-F "ranch"` - Easy enough! We can click on it and see the `:ranch_sup`,
|
||||||
|
which is the "top supervisor" mentioned previously, and the `:ranch_server`
|
||||||
|
`GenServer` also mentioned! Cool, they weren't lying to us... at least not
|
||||||
|
completely.
|
||||||
|
|
||||||
We can click on `:ranch_server` to see more information.
|
We can click on `:ranch_server` to see more information.
|
||||||
|
|
||||||
Now, if you selected the Livebook node and NOT the empty shell-of-a-node that is the attached Mix project (you should switch to the correct one now!), you will see that `:ranch_server` monitors a couple of `Supervisor`s:
|
Now, if you selected the Livebook node and NOT the empty shell-of-a-node that
|
||||||
|
is the attached Mix project (you should switch to the correct one now!), you
|
||||||
|
will see that `:ranch_server` monitors a couple of `Supervisor`s:
|
||||||
|
|
||||||
* `:ranch_conns_sup`
|
* `:ranch_conns_sup`
|
||||||
* `:ranch_listener_sup`
|
* `:ranch_listener_sup`
|
||||||
* `:ranch_conns_sup`
|
* `:ranch_conns_sup`
|
||||||
* `:ranch_listener_sup`
|
* `:ranch_listener_sup`
|
||||||
|
|
||||||
Awesome! We can see the connection `Supervisor` and listener `Supervisor` for port `5588` and likewise for port `5589`. The former for serving the page you're looking at _right now_ and the latter for iFrames or something. Who cares!
|
Awesome! We can see the connection `Supervisor` and listener `Supervisor` for
|
||||||
|
port `5588` and likewise for port `5589`. The former for serving the page
|
||||||
|
you're looking at _right now_ and the latter for iFrames or something. Who
|
||||||
|
cares!
|
||||||
|
|
||||||
We can see exactly what the docs are telling us. Very cool.
|
We can see exactly what the docs are telling us. Very cool.
|
||||||
|
|
||||||
But if we click on one of the `conns` `Supervisor`s, I don't see a hundred processes under `Monitors` hanging out waiting for connections. What gives?
|
But if we click on one of the `conns` `Supervisor`s, I don't see a hundred
|
||||||
|
processes under `Monitors` hanging out waiting for connections. What gives?
|
||||||
|
|
||||||
Yeah, I dunno. Maybe somebody in the audience knows why they aren't monitored (or at least why they don't show up here).
|
Yeah, I dunno. Maybe somebody in the audience knows why they aren't monitored
|
||||||
|
(or at least why they don't show up here).
|
||||||
|
|
||||||
But if you go to [the Processes page](/dashboard/processes) and `Ctrl-F ":ranch_acceptor.loop"` you will see exactly 200 results.
|
But if you go to [the Processes page](/dashboard/processes) and `Ctrl-F
|
||||||
|
":ranch_acceptor.loop"` you will see exactly 200 results.
|
||||||
|
|
||||||
Ok, this is cool and all, and if we had time, we could look at this in the Observer from pretty much any `iex` session like so:
|
Ok, this is cool and all, and if we had time, we could look at this in the
|
||||||
|
Observer from pretty much any `iex` session like so:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
:observer.start()
|
:observer.start()
|
||||||
```
|
```
|
||||||
|
|
||||||
But we're all getting impatient to build our own Ranch. It won't have horses on it, but it'll have something even better. TCP sockets!
|
But we're all getting impatient to build our own Ranch. It won't have horses on
|
||||||
|
it, but it'll have something even better. TCP sockets!
|
||||||
|
|
||||||
## Building Your Own Ranch in 30 Seconds
|
## Building Your Own Ranch in 30 Seconds
|
||||||
|
|
||||||
My apologies to all the folks that built real ranches over much longer periods of time and with far fewer TCP sockets to show for it.
|
My apologies to all the folks that built real ranches over much longer periods
|
||||||
|
of time and with far fewer TCP sockets to show for it.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
Application.ensure_all_started(:ranch)
|
Application.ensure_all_started(:ranch)
|
||||||
```
|
```
|
||||||
|
|
||||||
Man, being able to take advantage of Open Source contributors' work is really hard work. That was so easy! Now let's start accepting some TCP connections!
|
Man, being able to take advantage of Open Source contributors' work is really
|
||||||
|
hard work. That was so easy! Now let's start accepting some TCP connections!
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule EchoHandler do
|
defmodule EchoHandler do
|
||||||
|
@ -197,21 +271,28 @@ end
|
||||||
# Ranch Complete
|
# Ranch Complete
|
||||||
```
|
```
|
||||||
|
|
||||||
Ooh, _now_ if we look in our dashboard (at the non-Livebook node) we can see the supervised processes all linked up properly! But I still don't see a hundred monitored processes, so I'm obviously missing _something_. Oh well.
|
Ooh, _now_ if we look in our dashboard (at the non-Livebook node) we can see
|
||||||
|
the supervised processes all linked up properly! But I still don't see
|
||||||
|
a hundred monitored processes, so I'm obviously missing _something_. Oh well.
|
||||||
|
|
||||||
Either way, the ranch is done. Yeah, it really was _that easy_. Let's connect to it and see if it really does echo back to us! You can use `nc` (netcat), `telnet`, or we can use Erlang's `:gen_tcp` like so:
|
Either way, the ranch is done. Yeah, it really was _that easy_. Let's connect
|
||||||
|
to it and see if it really does echo back to us! You can use `nc` (netcat),
|
||||||
|
`telnet`, or we can use Erlang's `:gen_tcp` like so:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary])
|
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary])
|
||||||
```
|
```
|
||||||
|
|
||||||
See how `:gen_tcp` returns `{:ok, #Port<...>}`? A `Port` is a special Erlang/OTP-ism we can learn about another time. For now, should have got us a connection! Let's send something.
|
See how `:gen_tcp` returns `{:ok, #Port<...>}`? A `Port` is a special
|
||||||
|
Erlang/OTP-ism we can learn about another time. For now, should have got us
|
||||||
|
a connection! Let's send something.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
:gen_tcp.send(socket, "Hello, socket! " <> to_string(DateTime.utc_now()))
|
:gen_tcp.send(socket, "Hello, socket! " <> to_string(DateTime.utc_now()))
|
||||||
```
|
```
|
||||||
|
|
||||||
And if we got `:ok`, this `Process` should have a message in its [mailbox](https://elixir-lang.org/getting-started/processes.html).
|
And if we got `:ok`, this `Process` should have a message in its
|
||||||
|
[mailbox](https://elixir-lang.org/getting-started/processes.html).
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
messages = :erlang.process_info(self(), :messages)
|
messages = :erlang.process_info(self(), :messages)
|
||||||
|
@ -228,10 +309,10 @@ So it works. You get it now.
|
||||||
Application.stop(:ranch)
|
Application.stop(:ranch)
|
||||||
```
|
```
|
||||||
|
|
||||||
<div style="text-align: center; font-family: IosevkaLyte">
|
<div style="text-align: center; font-family: IosevkaLyte; font-size: 24px;">
|
||||||
Thanks for coming!
|
Thanks for coming!
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; font-size: 60px; font-family: IosevkaLyte">
|
<div style="text-align: center; font-size: 60px; font-family: IosevkaLyte;">
|
||||||
Fin
|
Fin
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,8 +13,10 @@ Install and run a local Livebook in `attached` mode and automatically grab my
|
||||||
code:
|
code:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
asdf install
|
||||||
mix escript.install github livebook-dev/livebook
|
mix escript.install github livebook-dev/livebook
|
||||||
git clone https://git.lyte.dev/lytedev/ranch-talk.git
|
git clone https://git.lyte.dev/lytedev/ranch-talk.git
|
||||||
|
cd ranch-talk
|
||||||
mix do deps.get, compile
|
mix do deps.get, compile
|
||||||
env LIVEBOOK_PORT=5588 LIVEBOOK_IFRAME_PORT=5589 \
|
env LIVEBOOK_PORT=5588 LIVEBOOK_IFRAME_PORT=5589 \
|
||||||
livebook server --default-runtime mix \
|
livebook server --default-runtime mix \
|
||||||
|
|
Loading…
Reference in a new issue