Monday, April 29, 2024
HomeRuby On RailsViewers of One · The Ruby Dispatch

Viewers of One · The Ruby Dispatch


Picture by


Annie Ruygt

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 the enter subject(s),
    the submit button, and the output 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,
    and output.
  • 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 through https,
    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!

Fly.io is a good way to run your Rails HotWired apps. It’s very easy to get began. You may be operating in minutes.


Deploy a Rails app at present!



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments