We’re Fly.io. We run apps for our customers on {hardware} we host world wide. Fly.io occurs to be an amazing place to run Rails purposes. Take a look at learn how to get began!
I’ve acquired an app for scheduling dance competitions.
I unretired to discover internet hosting it on the cloud. I’ve written about how it’s configured.
Within the first 12 months this software was used to schedule occasions in six cities. As we’re coming to a detailed of the second 12 months the present depend in twenty six cities. I should be ready in case the variety of cities quadruples once more subsequent 12 months.
Deploying a brand new occasion, location, and even area is merely a matter of updating just a few configuration information after which operating a script. This usually takes just a few minute, however do it sufficient instances and people minutes add up. My aim is to automate what I’m doing manually so it may be carried out in seconds.
I’ve began by creating varieties and having the controller replace textual content information; this effort is pretty mundane and routine. Launching a script asynchronously from a browser course of and streaming the response again dwell to the browser because it runs is much less frequent. Mark Ericksen just lately wrote an weblog article on learn how to do related issues with Phoenix. The constructing blocks out there for Rails are fairly totally different, so a special strategy is required.
This weblog put up will present you learn how to construct a Rails software that streams fly logs
output to the browser. You possibly can already discover this performance within the dashboard, however the level is that when you can stream the output of that command you may deal with any command.
Let’s get began. In case you are impatient, ignore the textual content and with just a few copy and pastes you may have a demo up and operating. After that skip all the way down to the abstract.
Step 1: Generate a brand new software
rails new console --css=tailwind
cd console
bin/rails generate channel output
bin/rails generate stimulus submit
bin/rails generate controller demo cmd
bundle add ansi-to-html
We can be utilizing Tailwindcss for styling,
Motion Cable to stream the outcomes,
Stimulus to wire the UI to the cable,
and a vanilla controller for the server aspect logic.
As a result of the output of fly logs
is colorized with ANSI escape codes, we may even use ansi-to-html to transform these colours to HTML.
Earlier than we proceed any additional, remark out the channel in app/javascript/channels/index.js
as we gained’t be utilizing it instantly:
// Import all of the channels for use by Motion Cable
// import "channels/output_channel"
The rationale we’re not utilizing the (singular) output channel instantly is that Motion Cable is designed to broadcast information to all subscribers. This gained’t do in any respect for this use case. As a substitute we can be creating separate streams for every shopper after which direct every output to a particular stream, thereby successfully broadcasting the outcomes of our instructions to an viewers of 1.
Step 2: Create an HTML kind
Subsequent exchange app/views/demo/command.html.erb
with the next:
<div class="w-full" data-controller="submit">
<h1 class="text-4xl font-extrabold text-center">Command demo</h1>
<enter data-submit-target="enter" title="app" placeholder="appname"
class="appearance-none mt-4 w-40 mx-auto block bg-gray-200
text-gray-700 border rounded py-3 px-4 mb-3
leading-tight focus:outline-none focus:bg-white">
<button data-submit-target="submit"
class="flex mx-auto bg-blue-500 hover:bg-blue-700 text-white
font-bold py-2 px-4 border-2 rounded-xl
my-4">submit</button>
<div class="border-2 border-black rounded-xl p-2 hidden">
<div data-submit-target="output" data-stream=""
class="w-full mx-auto overflow-y-auto h-auto font-mono text-sm
max-h-[25rem] min-h-[25rem]">
</div>
</div>
</div>
That is normal HTML. It doesn’t even use any Rails kind helpers.
Nor even a HTML <kind>
factor – the fields can be wired collectively
utilizing Stimulus. Notes:
data-controller
names the stimulus class (submit
)data-submit-target
recognized theenter
subject(s),
thesubmit
button, and theoutput
space.data-steam
on the output goal incorporates the one little bit of ERB,
and that comprise the token that can uniquely establish the stream.
The tailwind CSS stylings are taken from the Monitoring tab from the
fly io dashboard, minus the background colour.
The div
factor that incorporates the output is initially hidden.
Half 3: wire the shape parts to the channel
Change app/javascript/controllers/submit_controller.js
with the
following:
import { Controller } from "@hotwired/stimulus"
import shopper from '../channels/shopper'
// Connects to data-controller="submit"
export default class extends Controller {
static targets = [ "input", "submit", "output" ]
join() {
this.buttonTarget.addEventListener('click on', occasion => {
occasion.preventDefault()
const { outputTarget } = this
const params = {}
for (const enter of this.inputTargets) {
params[input.name] = enter.worth
}
shopper.subscriptions.create({
channel: "OutputChannel",
stream: outputTarget.dataset.stream
}, {
related() {
this.carry out("command", params)
outputTarget.parentNode.classList.take away("hidden")
},
obtained(information) {
let div = doc.createElement("div")
div.setAttribute("class",
"pb-2 break-all overflow-x-hidden")
div.innerHTML = information
let backside = outputTarget.scrollHeight -
outputTarget.scrollTop -
outputTarget.clientHeight
outputTarget.appendChild(div)
if (backside == 0) div.scrollIntoView()
}
})
})
}
}
This stimulus class:
- Identifies the three “targets” to match within the HTML:
enter
,submit
,
andoutput
. - Defines an motion to be executed when the submit button is clicked
- Extracts the outputTarget and the title and worth of every of the inputs.
- Creates a subscription on the
OutputChannel
, figuring out the substream
taken from the output goal factor. Two features are outlined
for the subscription:related
will request that the command be carried out, passing
the parameters constructed from the enter(s). Moreover the
enclosing factor for the output goal can be unhidden.obtained
will add a line to the output. If the output stream
was scrolled to the underside on the time a line is added the
scroll will advance to point out the brand new content material.
Half 4: wire the shape parts to the channel
Change app/controllers/demo_controller.rb
with the next:
class DemoController < ApplicationController
def cmd
@stream = OutputChannel.register do |params|
["flyctl", "logs", "--app", params["app"]]
finish
finish
finish
This will not appear to be a lot, however it’s maybe a very powerful
half. I’m not an professional on safety, however I’m fairly certain that
letting random individuals on the web present instructions to be executed
in your server is a nasty concept. This code takes various precautions:
- Streams are by invitation solely. As we are going to quickly see a random
token can be generated by the channel, and this token can be
positioned within the HTML which presumably can be served throughhttps
,
so solely the recipient can provoke a stream. - Even with a token, the one instructions that can be issued are
offered by the server, optionally augmented by parameters that
are handed by the shopper. This code can do additional validation
and even present totally different instructions primarily based on the enter offered. - The ultimate command is an array of strings permitting the shell
to be bypassed, stopping shell injection assaults.
Half 5: the channel itself
Change app/channels/output_channel.rb
with the next:
require 'open3'
class OutputChannel < ApplicationCable::Channel
def subscribed
@stream = params[:stream]
@pid = nil
stream_from @stream if @@registry[@stream]
finish
def command(information)
block = @@registry[@stream]
run(block.name(information)) if block
finish
def unsubscribed
Course of.kill("KILL", @pid) if @pid
finish
non-public
@@registry = {}
BLOCK_SIZE = 4096
def self.register(&block)
token = SecureRandom.base64(15)
@@registry[token] = block
token
finish
def logger
@logger ||= Logger.new(nil)
finish
def html(string)
Ansi::To::Html.new(string).to_html
finish
def run(command)
Open3.popen3(*command) do |stdin, stdout, stderr, wait_thr|
@pid = wait_thr.pid
information = [stdout, stderr]
stdin.close_write
half = { stdout => "", stderr => "" }
till information.all? file do
prepared = IO.choose(information)
subsequent except prepared
prepared[0].every do |f|
strains = f.read_nonblock(BLOCK_SIZE).cut up("n", -1)
subsequent if strains.empty?
strains[0] = half[f] + strains[0] except half[f].empty?
half[f] = strains.pop()
strains.every line
rescue EOFError => e
finish
finish
half.values.every do |half|
transmit html(half) except half.empty?
finish
information.every file.shut
@pid = nil
rescue Interrupt
rescue => e
places e.to_s
guarantee
information.every file.shut
@pid = nil
finish
finish
finish
There’s so much right here. Let’s begin with the general public interface:
self.register
is what generates a safe random token and
saves away the block of code that generates the command to
be executed for later use.subscribed
is known as when the stimulus controller creates a subscription.
Observe that it’s going to solely create a stream if the title of the
stream is within the registry.command
is what is known as when the stimulus controller calls
carry out
. It can run the block of code from the registry to
decide the command.unsubscribed
will kill any operating course of if the cable is closed
Now the non-public components:
logger
will disable the logger. By default Motion Cable will
log each request which may produce plenty of output. Be happy
to take away this or filter the output as desired.html
will name the ANSI to html converter.run
will launch the command and monitor each the stdout and
stderr streams, studying from them in buffered blocks as output
turns into out there, splitting that output in strains and transmitting
these strains as they develop into out there.
That completes the implementation. Give it a whirl!
Launch your Rails software utilizing bin/dev
, after which navitate to
http://localhost:3000/demo/cmd
. If you happen to don’t have an current fly
software to observe, change the command in app/controllers/demo_controller.rb
to one thing that can produce output. Maybe tail -f
on a file?
Abstract
Motion Cable does the heavy lifting on this state of affairs. As
documented it could
seem daunting and unapproachable, and there seem like valuable few
examples of this sort to be taught from, however in follow it may be very straightforward to
use.
As assembled, there are 4 items to the puzzle. The HTML and Rails
controller are distinctive to the precise request, and the Stimulus controller
and OutputChannel are designed to be reusable by different requests. In reality
a single software can have a number of scripts and all that may be wanted
is HTML and a controller motion for every.
My software may have separate scripts for creating and deleting machines,
copying information to volumes, and different administrative duties. There undoubtedly
can be just a few duties the place I might want to drop all the way down to the command line, however
more often than not I’ll have the ability to do issues that used to require my laptop computer
from my cellphone.
This instance used the output of script instructions because the supply for actual time updates, however different
sources are certainly potential: maybe a ChatGPT server or inventory quotes?
Let your creativeness run wild!