EN /HU | Belépés

Első lépések

A rubbs kézikönyvének első oldala: hogyan telepítsd a gemet, hogyan írj és indíts el egy első boardot, melyik réteget mire használd a háromból, mit csinál a BBS.configure egyes kulcsa, hogyan bánik a szerver a kapcsolatokkal (korlátok, idle timeout, logolás), és hogyan épül fel és megy ki Dockerben egy valódi board. A rubbs 1.0.0 alapján.

Telepítés

A gem neve bbs, és nincs fent a rubygems.org-on: a saját Git-szerverünkről jön. A Gemfile-ba:

RUBY
source 'https://rubygems.org'

gem 'bbs', git: 'https://git.teletypegames.org/engines/rubbs.git'

utána bundle install. Ruby 3.2 vagy újabb kell (a kapcsolat idle timeoutját az IO#timeout= állítja be, lásd Idle timeoutok). A futásidejű függőségek az artii (FIGlet bannerek) és az sqlite3 (BBS::SQLite és BBS::Store); utóbbi natív kiterjesztést fordít, ezért a gépen vagy az image-ben C-fordító kell.

Minden kiadásnak van tagje, vX.Y.Z (v1.0.0), a verzió pedig a bbs.gemspec-ben áll. A Bundler a bundle install pillanatában HEAD-en lévő commitot rögzíti a Gemfile.lock-ban, a bundle update bbs pedig a legújabbra lép. Egy kiadáson a tag: 'v1.0.0' tart meg, egy adott commitra a ref: 'COMMIT' köt. Aki 0.x-ről jön, a repó UPGRADING.md-jében minden megszűnt vagy átnevezett API-t megtalál a helyettesítőjével együtt.

Minden betöltéséhez egy require elég:

RUBY
require 'bbs'

Hello world

RUBY
# bbs.rb
require 'bbs'

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    big_banner 'My BBS'
    ask :name, prompt: 'Your name'
    say 'Welcome!', style: :success
    pause 'Hanging up...', seconds: 2
  end
end

BBS.start
TEXT
$ bundle exec ruby bbs.rb
BBS listening on port 2323 — connect with: telnet localhost 2323

Egy másik terminálban:

TEXT
$ telnet localhost 2323

      __  ___         ____  ____ _____
     /  |/  /_  __   / __ )/ __ ) ___/
    / /|_/ / / / /  / __  / __  \__ \
   / /  / / /_/ /  / /_/ / /_/ /__/ /
  /_/  /_/\__, /  /_____/_____/____/
         /____/

  Your name: Zsolt

  Welcome!

  Hanging up...

Amikor a flow lépései elfogynak, a szerver bontja a vonalat. A szerver termináljában a Ctrl-C leállítja (Server stopped.).

Bármilyen telnetkliens jó: a telnet parancs (macOS-en brew install telnet), a SyncTERM, a NetRunner, a Qodem, a PuTTY telnet módban. Az egérhez és a teljes képernyős rétegekhez xterm-escape-eket értő terminál kell (SyncTERM, vagy telnet iTerm2-ben, Terminal.app-ban, egy linuxos terminálban). Egy sima nc nem elég: nem válaszol a telnet negotiationre, így a szerver nem tudja meg az ablak méretét.

A három réteg

A rubbs háromféleképpen tud a hívóval beszélni. Ezek nem egy egész boardra szóló alternatívák: egy hívás általában többön is végigmegy, és mindig egy Flow-ban kezdődik.

Réteg Mi ez Mire való Kézikönyv
BBS::Flow Sororientált, deklaratív párbeszéd: a lépések fentről lefelé futnak (say, ask, menu, persist, call, …), a szöveg görög, mint egy régi BBS-en. A terminál-kérdés, a login, egy kérdőív, ragasztó a többi köré. Minden board ezzel indul. Flow
BBS::Application Widget framework: menubar, státuszsor, egymást átfedő window-k, űrlapok, dialógusok, egér, témák, doorok. Synchronet-stílusú főfelület több window-val és funkcióval, ANSI-terminálra. Application
BBS::Petscii::Screen és BBS::Selector Egy soralapú board képernyője: fordított címsor, bekezdések, RETURN-nel lapozott oldalak, számozott kurzoros menük, beviteli mezők és egy szerkesztő. Commodore 64-en 40×25, modern terminálon bármekkora. Egy board C64-es arca, vagy egy egész board, ami soronként dolgozik. Commodore 64

Ökölszabály: kezdj egy Flow-val. Ha a boardnak window-k, dialógusok, menubar vagy egér kell, a fő része egy Application, előtte egy Flow-val a terminál-kérdéshez vagy a loginhoz. A C64-es arc, vagy egy board, ami soronként egyszerűbb, egy call lépésből hajtott Petscii::Screen.

A három egymás mellett, egy boardban:

RUBY
require 'bbs'

NEWS = ['rubbs 1.0 is out', 'Feedia reads RSS on a C64', 'New door: Pixelia'].freeze

DESKTOP = BBS::Application.define do
  theme BBS::Theme.synchronet
  menubar do
    menu '&File' do
      item '&About' do BBS::Dialogs.message(self, 'Hello from rubbs!') end
      item 'E&xit'  do :halt end
    end
  end
  status_hint('F10', 'Quit') { :halt }
end

def read_news(session)
  screen = BBS::Petscii::Screen.new(session, cols: 80, rows: 24)
  item = screen.pick('the news', NEWS)
  screen.say(item, color: BBS::Petscii::Colors::YE) if item
  screen.pause
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    ask :name, prompt: 'Your name'
    menu 'Where to?', loop: true do
      option('News (line by line)')   { call { |_ctx, runner| read_news(runner.session) } }
      option('Desktop (Application)') { app DESKTOP }
      option('Log off')               { exit_menu }
    end
    say 'Goodbye!'
  end
end

BBS.start

A rétegek mellett vannak építőelemek, amelyeket belőlük érsz el: a BBS::Door és a BBS::Gateway átadja a vonalat egy másik programnak vagy boardnak (Doorok), a BBS::FakeOS egy szimulált gép (Doorok), a BBS::AI egy karakter, akivel beszélgetni lehet (AI), a BBS::Petscii és a BBS::Terminal a Commodore 64-es hívót szolgálja ki (Commodore), a BBS::SQLite, a BBS::Store, a BBS::HTTP, a BBS::Cache, a BBS::RateLimit és a BBS::Wikipedia pedig egy tartalmas board adat- és hálózati részei (Adat és hálózat).

Egy kapcsolat útja

MERMAID
flowchart TD
  A[BBS::Server elfogad egy TCP-kapcsolatot] --> B{"kevesebb hívó van, mint max_sessions, és erről a címről kevesebb, mint max_sessions_per_ip?"}
  B -- nem --> X["kiírja: 'Too many callers right now' vagy 'Too many calls from your address', és bont"]
  B -- igen --> C[új thread, socket timeout = idle_seconds vagy 900 s]
  C --> D[BBS::Session.new: véletlen session_id]
  D --> E[telnet negotiation: WILL SGA, WILL ECHO, DO SGA, DO NAWS]
  E --> F{config.mouse és config.mouse_on_connect?}
  F -- igen --> G[xterm egérjelentés be]
  F -- nem --> H
  G --> H[a FlowRunner futtatja a config.flow-t]
  H --> I[lépések: say, ask, menu, call, ...]
  I --> K[app: Application]
  I --> L[call: Petscii::Screen, doorok, al-flow-k]
  K --> I
  L --> I
  I --> M[elfogyott a lépés, :halt jött, vagy a hívó bontott]
  M --> N[egér ki, socket zárva, on_session_end meghívva]

Ugyanez szövegesen:

TEXT
TCPServer ─ accept ─ admit? ─ Thread ─ Session#run
                                        ├ negotiate (SGA, ECHO, NAWS)
                                        ├ enable_mouse (config.mouse && config.mouse_on_connect)
                                        └ FlowRunner(config.flow).run
                                             ├ app  ─▶ Application#run ─ vissza a következő lépésre
                                             └ call ─▶ a saját kódod (Petscii::Screen, doorok, al-flow-k, BBS::Terminal)
          ensure: disable_mouse, socket zárása, config.on_session_end.call(session)

Minden kapcsolat a saját threadjében fut, saját Session-nel és saját flow-contexttel (ctx); a definíciók (Flow.define, Application.define) közös, csak olvasott sablonok. Ami változtatható dolgot betöltéskor egy definícióba teszel (egy konstans Hash vagy Array), azon minden hívó osztozik.

BBS.configure

RUBY
BBS.configure do |c|
  c.flow = MY_FLOW
end

A BBS.configure az egyetlen, process-szintű BBS::Config objektumot adja a blokknak; a BBS.config visszaadja (első használatkor létrehozza). A BBS.start előtt konfigurálj; a szerver és a sessionök élőben olvassák az értékeket, így egy később átírt kulcs a későbbi kapcsolatokra hat.

Kulcs Alapérték Mit csinál
flow nil A BBS::Flow, amit minden kapcsolat futtat. Kötelező: nélküle minden session BBS.config.flow is not set hibával áll le (logolva, a hívót bontja).
port ENV['BBS_PORT'] vagy 2323 A BBS.start TCP-portja. A környezeti változót a config objektum létrejöttekor olvassa, vagyis az első BBS.configure vagy BBS.config hívásnál.
max_sessions ENV['BBS_MAX_SESSIONS'] vagy 100 Egyszerre hány kapcsolatot szolgál ki. Eggyel több Too many callers right now, try again later. üzenetet kap, a szerver bont, és egy BBS busy sort logol. A környezeti változót a BBS_PORT-hoz hasonlóan olvassa; a BBS.configure-ban megadott érték erősebb.
max_sessions_per_ip ENV['BBS_MAX_SESSIONS_PER_IP'] vagy 6 Egy címről egyszerre hány kapcsolatot szolgál ki. Eggyel több Too many calls from your address, try again later. üzenetet kap, és a szerver bont (a logban BBS crowded). A privát, loopback és link-local címekre nincs korlát: egy webes proxy, például a bbs-web, a Docker-hálózatból sok hívót hoz.
idle_seconds nil Az idle-korlát másodpercben. Ez minden kapcsolat socketjének olvasási/írási timeoutja (nil esetén 900 s), és egy Application kemény idle-bontása ("Idle" üzenettel). Lásd Idle timeoutok.
mouse false Használ-e a board xterm egérjelentést (SGR, gombok és húzás). Az Applicationök és a doorok csak akkor kapcsolják be-ki maguk körül, ha ez igaz.
mouse_on_connect true A mouse mellett: elinduljon-e a jelentés rögtön a negotiation után. Állítsd false-ra, ha a flow előbb kérdez valamit, pl. egy kétarcú board, ami azt kérdezi, modern vagy C64: az egér escape-szekvenciája egy Commodore-on szemét volna. Modern terminálnál ilyenkor a BBS::Terminal.flow kapcsolja be.
screens_dir nil A screen lépések ERB-fájljainak mappája azoknál a flow-knál, amelyek nem adnak meg sajátot (Flow.define(screens_dir:)). Lásd Flow: screenek.
on_session_end nil Egy callable, amit minden kapcsolat vége után meghív a BBS::Session-nel, rendes vég és hiba esetén is. A kapcsolat threadjében fut; logoláshoz, egy "ki van bent" listához vagy a sessionhöz kötött állapot takarításához jó.
theme nil Minden olyan Application témája, amelynek nincs saját theme hívása (és az alap nélküli theme do … end blokk kiindulópontja); nil esetén BBS::Theme.synchronet. A 0.19.0 óta. Lásd Rajzolás.
log_tag ENV['BBS_LOG_TAG'] vagy 'bbs' Ezzel kezdődik minden BBS.log-sor: a board technikai neve (feedia-bbs). Lásd Logolás.
ai_concurrency 2 A processzben egyszerre hány BBS::AI::Client-kérés fut; a következő legfeljebb 5 s-ot vár, aztán BBS::AI::Busy-t kap. Lásd AI.
user_agent 'rubbs/VERSION (+https://teletypegames.org)' A BBS::HTTP-kérések User-Agent-je. Lásd Adat és hálózat.

Egy teljes konfiguráció:

RUBY
require 'bbs'

WHO = {}
WHO_LOCK = Mutex.new

BBS.configure do |c|
  c.port             = 2323
  c.max_sessions     = 20
  c.log_tag          = 'harbor-bbs'
  c.idle_seconds     = 600
  c.mouse            = true
  c.mouse_on_connect = true
  c.screens_dir      = File.join(__dir__, 'screens')
  c.on_session_end   = lambda do |session|
    WHO_LOCK.synchronize { WHO.delete(session.session_id) }
    puts "caller #{session.session_id} left"
  end

  c.flow = BBS::Flow.define do
    call do |ctx, _runner|
      WHO_LOCK.synchronize { WHO[ctx[:session_id]] = Time.now }
      nil
    end
    say 'Hello!', style: :success
    pause 'See you.', seconds: 2
  end
end

BBS.start

BBS.start és BBS::Server

RUBY
BBS.start                # a port a BBS.config.port-ból
BBS.start(port: 2424)    # felülírva

A BBS.start(port: BBS.config.port) létrehoz egy BBS::Server-t, és meghívja a run-ját, ami a process teljes élete alatt blokkol. Ugyanez kézzel:

RUBY
BBS::Server.new(port: 2424).run
Cím 0.0.0.0 (minden interfész), a megadott porton. Foglalt portnál induláskor Errno::EADDRINUSE-t dob.
Threadek Kapcsolatonként egy Ruby-thread; az accept loop sosem vár egy sessionre.
Session-korlát config.max_sessions, mutex alatt számolva; a hely a session threadjének végén szabadul fel.
Címenkénti korlát config.max_sessions_per_ip a hívó címére, kivéve a privát, loopback és link-local címeket.
Accept-hibák Az accept minden SystemCallError és IOError hibáját logolja (accept: …), 0,5 s-ot vár, és megy tovább.
Session vége A hívó bontása, a broken pipe és az idle timeout session: dropped by the client (…) sorként kerül a logba; minden más kivétel session: OSZTÁLY: ÜZENET sorként, három sor backtrace-szel. Egyik sem állítja le a szervert. A hely akárhogy ér véget a session, felszabadul.
Leállás Ctrl-C (SIGINT) vagy SIGTERM (docker stop): kiírja, hogy Server stopped., és a run visszatér.

Idle timeoutok

Az idle_seconds két szinten hat:

  1. A socket. Minden elfogadott socket timeout = idle_seconds-ot kap, vagy BBS::Server::DEFAULT_TIMEOUT-ot (900 s), ha az nil. Az ennél tovább váró blokkoló olvasás vagy írás kivételt dob, és a session "dropped by the client"-ként ér véget. Ez bontja azt a hívót, aki egy ask promptnál, egy "nyomj RETURN-t"-nél vagy egy doorban ül.
  2. Egy Application egymásodperces tickkel olvassa a billentyűket, és számolja a csendes másodperceket; idle_seconds-nál lefuttatja az on_idle blokkját (ha van), és hacsak az nem :keep-et ad vissza, kiírja, hogy "Disconnected after N s idle.", és visszatér. nil esetén magától sosem bont. A session.idle_seconds = 3600 egyetlen hívónál mindkettőt átállítja (egy sysopnál, egy hosszú olvasásnál).

Logolás

A könyvtár minden logsort egyetlen metóduson, a BBS.log(tag, message)-en át ír a stderr-re, LOG_TAG: TAG: ÜZENET alakban, így sok board sorai egy logban is egyformán olvashatók. A log_tag legyen a board technikai neve, és a board maga is ezt a metódust használja:

RUBY
BBS.configure { |c| c.log_tag = 'feedia-bbs' }
BBS.log('catalog refresh', 'done in 3.2 s')   # feedia-bbs: catalog refresh: done in 3.2 s
BBS.log('catalog refresh', error)             # feedia-bbs: catalog refresh: SocketError: …

A hívóktól független munka (egy induláskor betöltött index, egy óránként frissülő katalógus) a BBS.background(name, every: nil) { … }-be kerül: egy threadbe, ami a hibát logolja, és egyre hosszabb szünet után újrapróbálja. Lásd Adat és hálózat.

Egy board mappaszerkezete

A hálózatunk boardjai (Feedia BBS, Alexandria BBS, a Teletype BBS) ugyanazt a szerkezetet követik: egy rövid bbs.rb köti össze a részeket, a board saját logikája a lib/domain-ben van (repositoryk, amik a külvilággal beszélnek, és szolgáltatások, amik eldöntik, mit lát a hívó), a board minden arca pedig a lib/ui egy-egy mappájában.

TEXT
feedia/
├── bbs.rb                 # configure + start, semmi más
├── Gemfile                # gem 'bbs', git: 'https://git.teletypegames.org/engines/rubbs.git'
├── Gemfile.lock
├── lib/
│   ├── domain/            # a board logikája, vonal nélkül tesztelhető
│   │   ├── feeds.rb
│   │   ├── inbox.rb
│   │   └── html_text.rb
│   └── ui/
│       ├── modern.rb      # UI::Modern.flow(...) → Flow + Application ANSI-terminálra
│       ├── petscii.rb     # UI::Petscii.flow(...) → Flow Commodore 64-re (PETSCII, 40 oszlop)
│       └── petscii/screen.rb
├── config/feeds.yml       # az image-dzsel szállított adat
├── data/                  # futásidejű adat (store-ok, logok), Dockerben volume
├── bin/                   # kis admin szkriptek
├── Dockerfile
├── compose.yaml
├── entrypoint.sh
└── env-example            # a környezeti változók, amiket a board olvas

A Feedia teljes bbs.rb-je:

RUBY
require 'bbs'
require_relative 'lib/domain/feeds'
require_relative 'lib/domain/inbox'
require_relative 'lib/ui/modern'
require_relative 'lib/ui/petscii'

feeds = Feeds.load(ENV.fetch('FEEDIA_FEEDS', File.join(__dir__, 'config/feeds.yml')))
inbox = Inbox.new(path: ENV.fetch('FEEDIA_INBOX', File.join(__dir__, 'data/inbox.jsonl')),
                  webhook: ENV.fetch('FEEDIA_DISCORD_WEBHOOK', ''))

BBS.configure do |config|
  config.mouse            = true
  config.mouse_on_connect = false
  config.idle_seconds     = 600

  config.flow = BBS::Terminal.flow(title: 'Feedia', modern: UI::Modern.flow(feeds, inbox),
                                   petscii: UI::Petscii.flow(feeds, inbox))
end

BBS.start

A részek:

  • A domain-objektumok egyszer jönnek létre a bbs.rb-ben, és a UI-gyárak kapják meg őket (UI::Modern.flow(feeds, inbox)); a UI-kód Application services-ként vagy a closure-ön át éri el őket. A domain használhatja a rubbs adat- és hálózati részeit (BBS::HTTP, BBS::Cache, BBS::SQLite, BBS.log), a képernyőit soha. A repository dob, a szolgáltatás elkapja, egyszer logolja, és eldönti, mit lát a hívó; a UI sosem kap el BBS::HTTP::Error-t (lásd Adat és hálózat).
  • Minden arc egy metódus, ami Flow-t ad vissza. A UI::Modern.flow felépít egy Application-t, és egy kis Flow-t ad vissza, aminek a call lépése futtatja (app.run(session: runner.session, session_id: ctx[:session_id], context: ctx)), utána elköszön. A UI::Petscii.flow egy olyan Flow-t ad vissza, aminek a call lépése egy BBS::Petscii::Screen-t hajt.
  • A BBS::Terminal.flow megkérdezi a hívót, milyen terminálja van, és a hozzá illő arcot futtatja; ezért false a mouse_on_connect. Egy egyarcú board a config.flow-ba közvetlenül a saját Flow-ját teszi. Lásd Commodore.

Az Alexandria BBS ugyanilyen, egy domain-objektummal (lib/domain/wikipedia.rb) és egy külön window-modullal (lib/ui/modern/pharos.rb). Az Impostor BBS a másik gyakori forma: egy gem, aminek a lib/impostor.rb-je egy saját screens/ mappás Flow-t definiál, így ugyanaz a Flow önálló boardként és egy másik board doorjaként is fut (lásd Flow).

Docker

A boardok Dockerben futnak, mindegyik a saját konténerében, a hivatalos Ruby Alpine image-ből. A git a Gitből jövő gemhez kell, a build-base a natív kiterjesztésekhez (sqlite3).

Dockerfile:

DOCKERFILE
FROM ruby:3.3-alpine

RUN apk add --no-cache git build-base tzdata

WORKDIR /app

ENV BUNDLE_WITHOUT=development

COPY Gemfile Gemfile.lock ./
RUN bundle install

COPY bbs.rb entrypoint.sh ./
COPY lib/ ./lib/

RUN chmod +x entrypoint.sh

EXPOSE 2323

ENTRYPOINT ["./entrypoint.sh"]

entrypoint.sh: a boardjaink minden induláskor frissítik a rubbs-t, így egy újraindítás rebuild nélkül is felveszi az új kiadást. Szerver-újraindítás után a Git-szerver egy ideig 502-t adhat, ezért legfeljebb egy percig vár, és ha kell, a telepített verzióval indul:

SH
#!/bin/sh
set -e
RUBBS_GIT=https://git.teletypegames.org/engines/rubbs.git
tries=0
until git ls-remote "$RUBBS_GIT" HEAD >/dev/null 2>&1 || [ "$tries" -ge 12 ]; do
  tries=$((tries + 1))
  sleep 5
done
bundle update bbs || echo "rubbs update failed, starting with the installed version" >&2
exec bundle exec ruby bbs.rb

Az exec számít: így a Ruby lesz a PID 1, és a docker stop jelzése eljut hozzá.

compose.yaml:

YAML
services:
  myboard:
    build: .
    container_name: myboard
    mem_limit: 128m
    ports:
      - "2323:2323"
    volumes:
      - ./data:/app/data
    environment:
      - TZ=Europe/Budapest
    restart: unless-stopped

A konténerben a board mindig a 2323-on figyel; a ports hoszt oldala adja a nyilvános portot (a Feediáé "2327:2323"). Amit a board futás közben ír (BBS::Store- és BBS::SQLite-fájlok), az a /app/data alá kerül, ami volume, így túléli a rebuildet.

TEXT
docker compose up -d --build
docker compose logs -f myboard
telnet localhost 2323

A board kimenete (puts, BBS.log) a konténer logjába megy: a figyelő sor, a bontott sessionök és a session-hibák backtrace-szel, minden sor a board log_tag-je után.

Hogyan tovább

Lásd még