EN /HU | Belépés

Doorok

Minden, amivel a rubbs a hívó telnetvonalát átadja valami másnak, majd visszaveszi: a doorok (BBS::Door), a szimulált gépek, amikbe a hívó gépelhet (BBS::FakeOS), a telnet-gatewayek más boardokhoz (BBS::Gateway), és a ZMODEM-letöltés a hívó termináljára (BBS::Zmodem). Az oldal minden példáját a rubbs 0.17.0-n futtattuk, a szöveg a 0.17.1-hez igazítva.

Mi az a door

A régi boardokon a door egy külön program volt, amibe a hívó átlépett: a BBS átadta a vonalat, a program közvetlenül beszélt a hívóval, és amikor kilépett, a vonal visszakerült a boardhoz. A rubbsban a door egy BBS::Door, ami háromféle programot tud becsomagolni:

Program Mi ez Hogyan futtatja a door Hogyan lép ki a hívó
BBS::FakeOS-definíció szimulált gép: bootszkript és parancsértelmező FakeOS::Runner.new(session, definition, context: context).run egy parancs, ami io.halt-ot hív (EXIT, BYE, …)
BBS::Flow soronkénti párbeszéd FlowRunner a context másolatán, utána „Press ENTER to return to the BBS...” a flow véget ér, aztán ENTER
bármi, aminek van call(session, context) metódusa lambda, metódusobjektum, saját osztály, BBS::Gateway program.call(session, context) a metódus visszatér

Egy door oda-vissza útja egy applicationből:

MERMAID
sequenceDiagram
  participant C as Hívó
  participant A as Application
  participant B as Builder#run
  participant D as Door#run
  participant P as Program
  C->>A: Doors > TT-80
  A->>A: open_door(:tt80) beállítja a context[:door]-t, stop!
  A-->>B: véget ér az eseményciklus
  B->>B: on_door blokk (door, context)
  B->>D: door.run(session, context)
  D->>C: egér ki, színek alaphelyzetbe, képernyőtörlés
  D->>P: elindítja a programot
  P->>C: közvetlenül beszél a hívóval
  C->>P: EXIT
  P-->>D: visszatér
  D->>C: egér vissza (ha BBS.config.mouse)
  D-->>B: visszatér
  B->>A: újraépíti az applicationt (ugyanazzal a contexttel)
  A->>C: az asztal, újrarajzolva

A legkisebb door egy lambda, egy flow-ból indítva:

RUBY
require 'bbs'

DICE = BBS::Door.new(:dice, lambda { |session, ctx|
  session.write("Rolling for #{ctx[:username]}... #{rand(1..6)}\r\nPress a key.")
  session.readkey
}, label: '&Dice')

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    ask :username, prompt: 'Your name'
    call do |ctx, runner|
      DICE.run(runner.session, ctx)
      runner.write "\r\nBack on the board.\r\n"
      nil
    end
  end
end

BBS.start

BBS::Door

Forrás: lib/bbs/door.rb.

Door.new

RUBY
BBS::Door.new(name, program, label: nil)
Argumentum Alapérték Jelentés
name kötelező a door neve; Symbolként tárolja (door.name)
program kötelező egy BBS::FakeOS::Definition, egy BBS::Flow, vagy bármilyen objektum, aminek van call metódusa
label: name.to_s.upcase a menüpont szövege; az & jelöli a gyorsbillentyűt, mint a '&SERMO'-ban

Minden más ArgumentError-t dob („not a FakeOS definition or a callable”). Olvasható: name, label, program.

Door#run

RUBY
door.run(session, context = {})

Sorrendben ezt csinálja:

  1. session.disable_mouse, hogy a program ne kapjon egérjelentéseket, amiket nem tud olvasni.
  2. Kiírja: ESC[0m ESC[2J ESC[H — alapszínek, üres képernyő, kurzor a bal felső sarokban.
  3. Elindítja a programot a fajtája szerint (lásd a fenti táblázatot).
  4. Utána, bármi történt: session.enable_mouse, ha a BBS.config.mouse be van kapcsolva.

Ha a program elhasal, nil-t ad vissza (lásd lent); különben azt, amit a program.

A context

A context hash a hívó állapota az egész hívás alatt. A FakeOS- és a callable door ugyanazt a hasht kapja: amit a door beleír, az visszatérés után is ott marad, így egy door a hívás végéig megtarthatja az állapotát (AI-chat előzményeit, egy meghajtóbetűt, egy pontszámot), és a board utána ki is olvashatja.

A BBS::Flow door ehelyett context.dup-ot kap, hogy az ask lépései ne írhassák felül a board saját kulcsait (:username, :authenticated, …). A flow-futtatónak kell a context[:session_id] is; applicationön belül ez már ott van, önálló használatnál add át. Ha a flow saját ERB-screenekkel jön, adj neki screens_dir:-t (lásd Flow). A flow-nak nincs saját kilépő parancsa, ezért amikor véget ér, a door kiírja: „Press ENTER to return to the BBS...”, és vár egy sorra.

Hibák és bontás

Egy door hibája nem bonthatja a hívást:

Mi történik a programban Eredmény
bármilyen StandardError (bug, sikertelen lekérdezés) BBS.log-gal naplózza door NÉV címkével (osztály, üzenet, a backtrace három sora); a run nil-t ad, a hívó visszakerül a boardra
IOError vagy SystemCallError (leszakadt a vonal) továbbdobja: a hívás véget ér, ahogy kell
egy FakeOS vagy Flow program alatt szakad le a vonal a futtatója lenyeli az IOError-t, EPIPE-t és ECONNRESET-et, és visszatér; a board a következő olvasásnál veszi észre a bontást
RUBY
require 'bbs'

broken = BBS::Door.new(:broken, ->(_session, _ctx) { raise 'boom' })
broken.run(session, ctx)   # naplózza: "my-bbs: door broken: RuntimeError: boom", nil-t ad

dropped = BBS::Door.new(:dropped, ->(_session, _ctx) { raise IOError, 'line dropped' })
dropped.run(session, ctx)  # IOError-t dob: a hívás véget ér

Doorok egy applicationben

Az Application DSL-je deklarálja a doorokat, menübe teszi őket, és bármelyik handlerből megnyitja. Forrás: lib/bbs/application.rb.

DSL / metódus Hol Mit csinál
door(name, program, label: nil) Application.define deklarál egy doort: Door.new(name, program, label: label), deklarációs sorrendben
doors menu(...) { } belsejében minden deklarált doorhoz egy menüpont door.label felirattal, mindegyik open_door(door.name)-et hív
open_door(name) a futó applicationön (menüpontok, státuszsor-hintek, billentyűhandlerek, window-gombok, lista-callbackek, helperek) a doort a context[:door]-ba teszi, a most kezelt esemény után leállítja az eseményciklust, és :halt-ot ad; ismeretlen névre ArgumentError
on_door { \|door, context\| } Application.define közvetlenül azelőtt fut, hogy a vonal a doorhoz kerül: bejegyzés a node-listába, egy naplósor
Builder#run(session:, session_id:, context: {}) a definiált application felépíti és futtatja az applicationt; ha egy door miatt ért véget, futtatja a doort, és újraépíti az applicationt; akkor tér vissza, ha a hívó door nélkül lép ki, vagy megszűnik a vonal (ilyenkor a függő doort nem nyitja meg)
Application#doors a futó application a deklarált doorok név szerinti Hashben

Az open_door a Builder#run-on át működik, és (0.17.1 óta) az app BOARD flow-lépés is ezt használja. A sima BOARD.build(...).run egyszer futtatja az eseményciklust, és megáll: ott az open_door csak befejezi az applicationt. Doorokkal rendelkező applicationt egy flow app lépéséből indíts, vagy call lépésből, ha kód is kell köré, mint itt:

RUBY
require 'bbs'
require_relative 'tt80' # a TT-80 FakeOS a lenti teljes példából

QUIZ = BBS::Flow.define do
  big_banner 'Quiz', font: 'small'
  ask :answer, prompt: 'What is 6 x 7'
  call do |ctx, runner|
    runner.write(ctx[:answer] == '42' ? "  Right!\r\n" : "  No, it is 42.\r\n")
    nil
  end
end

DICE = lambda do |session, ctx|
  session.write("Rolling for #{ctx[:username]}... #{rand(1..6)}\r\nPress a key.")
  session.readkey
end

BOARD = BBS::Application.define do
  door :tt80, TT80, label: '&TT-80'
  door :quiz, QUIZ, label: '&Quiz'
  door :dice, DICE, label: '&Dice'

  menubar do
    menu('&Doors') { doors }
  end

  status_hint('F5', 'Dice') { open_door(:dice) }
  status_hint('Alt-X', 'Bye') { :halt }

  on_door do |door, ctx|
    warn "#{ctx[:username]} enters #{door.name}"
  end
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    ask :username, prompt: 'Your name'
    call do |ctx, runner|
      BOARD.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
      nil
    end
  end
end

BBS.start

Az F5 dob a kockával, az Alt-D megnyitja a Doors menüt a TT-80, Quiz és Dice pontokkal. A kvíz :answer kulcsa a kvíz contextmásolatában marad; a TT-80 meghajtóbetűje a board contextjében.

A Teletype BBS (bbs/teletype-bbs) adatként tartja a doorjait (TeletypeBBS#doors a lib/domain/teletype_bbs.rb-ben: név, program, felirat, a legkeskenyebb szélesség, amin elfér, és opcionálisan egy C64-es változat); a modern arc ciklusban deklarálja őket, az on_door pedig „Door: SERMO”-t ír a hívó node-jához a Who is Online-ban:

RUBY
bbs.doors.each { |d| door d.name, d.program, label: d.label }

on_door do |door, ctx|
  bbs.online.activity(ctx[:session_id], "Door: #{door.label.delete('&')}")
end

Door application nélkül

A Door#run-nak csak egy session és egy context kell. Egy flow call lépése, egy PETSCII-képernyő vagy saját kód közvetlenül is megnyithatja, mint az oldal első példájában. A Teletype BBS PETSCII-arcának (lib/ui/petscii/board.rb) nincs applicationje, és pontosan így csinálja; ha a doornak van 40 oszlopos változata, azt választja:

RUBY
BBS::Door.new(door.name, door.petscii || door.program, label: door.label).run(@screen.session, @ctx)

Door nélkül egy flow helyben is futtathat FakeOS-t az os lépéssel. Ez a flow contextjén osztozik, de nincs képernyőtörlés, egérkezelés és hibavédelem:

RUBY
BBS.config.flow = BBS::Flow.define do
  ask :username, prompt: 'Your name'
  os TT80
  say 'Back on the board.'
end

BBS::FakeOS

Forrás: lib/bbs/fake_os.rb. Teletype-stílusú szimulált gép: egy szkriptelt bootfolyamat, aztán egy parancsértelmező. Minden szimulált OS csak egy újabb definíció; a motor általános, így egy új gép konfiguráció, nem új motorkód.

Egy definíció váza

RUBY
require 'bbs'

DOS = BBS::FakeOS.define do
  name   'MS-DOS'
  prompt 'C:\\>'
  colors text: "\e[0;37m", accent: "\e[1;32m"

  boot do
    print 'Award Modular BIOS v4.51PG', delay: 0.4
    memtest 16_384
    pause 0.5
    cls
    big_banner 'MS-DOS'
  end

  ready { puts 'Type VER, CLS or EXIT.' }

  command('ver')          { |_args, io| io.puts 'MS-DOS Version 6.22' }
  command('cls')          { |_args, io| io.cls }
  command('exit', 'quit') { |_args, io| io.halt }

  on_unknown do |_input, io|
    io.puts 'Bad command or file name'
  end
end

A BBS::FakeOS.define { ... } a blokkot egy új FakeOS::Definition-ön értékeli ki, és azt adja vissza. Egy definícióban kétféle blokk van, és mást látnak:

  • A boot és a ready a konzolon fut (instance_eval): a print, puts, cls, ctx, session és a többi fogadó nélkül működik. Vigyázat: itt a puts és a print a hívónak ír, nem a szerver stdoutjára; naplózni warn-nal lehet.
  • A command és az on_unknown blokkok argumentumokat kapnak: (args, io) és (input, io). Minden kimenethez és állapothoz az io-t használd.

A definíciós DSL

Metódus Alapérték Mit csinál
name(value = nil) 'OS' beállítja a nevet; argumentum nélkül visszaadja. Csak tájékoztató: a shell nem írja ki
prompt(value = nil, &block) '> ' String, vagy egy blokk / lambda, ami a contextet kapja és a promptot adja, minden sor előtt kiértékelve; argumentum nélkül a mostanit adja vissza. A '' is megengedett (a Commodore-doorok maguk írják ki a READY.-t)
colors(overrides = nil) DEFAULT_COLORS SGR-sztringeket fésül a színszerepekbe; argumentum nélkül a szerepek Hashét adja
boot { } nincs a bekapcsolási szkript, egyszer fut a konzolon
ready { } nincs egyszer fut a boot után, az első prompt előtt
command(*names, help: nil) { \|args, io\| } nincs parancsot regisztrál egy vagy több néven; az első a kanonikus
on_unknown { \|input, io\| } „Bad command or file name”-et ír mi legyen azzal a bemenettel, ami egyik parancsra sem illik

A boot_block, ready_block, commands, order és unknown olvasók mutatják, mi lett deklarálva. A commands minden kisbetűs nevet (az aliasokat is) { name:, help:, block: }-ra képez; az order a kanonikus neveket sorolja deklarációs sorrendben. Egy HELP parancs ezekből rakja össze a listáját (lásd a teljes példát).

Egy prompt, ami követi a gép állapotát:

RUBY
prompt { |ctx| "#{ctx[:drive] || 'A'}> " }
# vagy
prompt ->(ctx) { "#{ctx[:drive] || 'A'}> " }

Színszerepek és a base

Minden kimeneti primitív kap egy style: szerepet. Ismeretlen szerep esetén a :text színe érvényes.

Szerep Alapérték Ki használja
text "\e[0;37m" (világosszürke) puts, print, type, memtest
bright "\e[1;37m" a saját kiemeléseid
accent "\e[1;32m" banner, big_banner
error "\e[1;31m" „Bad command or file name”, „Error: …”
dim "\e[1;30m" a halk szövegeid (AI-válaszok, tippek)
prompt "\e[0;37m" a prompt
base '' ide tér vissza minden reset

Minden kimenet után a konzol ESC[0m-et ír, majd a base-t, és a cls is ezt írja a képernyőtörlés előtt. Ha tehát a base hátteret állít, az a háttér túléli a promptot, a hívó visszhangját és az új sorokat, a cls pedig az egész képernyőt kitölti vele. A többi szerepnek is add meg ugyanazt a hátteret: a 0;-val kezdődő SGR törli.

RUBY
colors base:   "\e[44m",          # kék képernyő
       text:   "\e[0;37;44m",
       bright: "\e[0;97;44m",
       error:  "\e[0;91;44m",
       prompt: "\e[0;97;44m"

A konzol

A FakeOS::Console-é a session és a kimenet. A boot-ban és a ready-ben ez a self, a parancsokban az io argumentum.

Metódus Mit csinál
puts(text = '', style: :text) egy sor, CRLF-fel a végén
print(text = '', delay: 0, style: :text, newline: 1) szöveg, utána newline darab CRLF, aztán delay másodperc szünet; newline: 0 a sorban hagyja a kurzort
type(text, style: :text, cps: 900, newline: 1) írógép: karakterenként, másodpercenként cps karakterrel (cps: 0 egyben írja ki)
newline(count = 1) count darab CRLF
cls alaphelyzetbe (a base-re) állítja a színeket, és törli a képernyőt
pause(seconds) alszik
banner(text, style: :accent) keretes egysoros banner (BBS::Banner.render)
big_banner(text, style: :accent, font: 'standard') figlet-banner az artii-n át ('standard', 'small', 'slant', …)
memtest(kilobytes, step: 2048, delay: 0.03, label: 'Memory Test : ') a számláló memóriateszt, helyben egy soron, a végén „64K OK”
cols, rows a hívó ablakmérete (telnet NAWS), alapból 80 és 24
halt az aktuális parancs után befejezi a shellt
ctx a context hash (a door contextje)
session a BBS::Session a readline-hoz, readkey-hez, write-hoz
definition a futó definíció

A shell ciklusa

A boot és a ready után a konzol halt-ig vagy bontásig ismétli:

  1. Kiírja a promptot a prompt szerepben.
  2. Beolvas egy sort (session.readline); a nil, vagyis a bontás befejezi a ciklust.
  3. Levágja a szóközöket; üres sorra csak újra promptol.
  4. Leválasztja az első szót, kisbetűsíti, és megkeresi a commands-ban, így a parancsok kis- és nagybetűtől függetlenek. A blokk Stringként kapja a sor maradékát ('', ha nincs), és a konzolt.
  5. Ha nincs találat: az on_unknown megkapja az egész levágott sort; on_unknown nélkül „Bad command or file name” az error szerepben.

Egy parancsban vagy az on_unknown-ban keletkező StandardError-t naplózza (my-bbs: fake os command: …), a hívónak „Error: ÜZENET”-ként mutatja az error szerepben, és a shell megy tovább. Az IOError és a SystemCallError továbbmegy. A boot-ban vagy a ready-ben keletkező kivételt a konzol nem kapja el: doorban ez visszaviszi a hívót a boardra.

Nincs beépített EXIT vagy HELP. Regisztráld magad; az EXIT io.halt-ot hív:

RUBY
command 'exit', 'quit', 'bye', help: 'back to the BBS' do |_args, io|
  io.puts 'Goodbye.'
  io.halt
end

Egy parancs a sessionön át saját kis párbeszédet is folytathat:

RUBY
command 'name', help: 'asks for your name' do |_args, io|
  io.print 'NAME? ', newline: 0
  name = io.session.readline.to_s.strip
  io.puts "HELLO, #{name.upcase}."
end

Egy FakeOS futtatása

Hogyan Context Plusz
BBS::Door.new(:name, DEFINITION).run(session, ctx) ugyanaz a hash képernyőtörlés, egér ki és be, hibavédelem
door :name, DEFINITION egy applicationben az application contextje mind a fenti, plusz a menü és az on_door
os DEFINITION lépés egy flow-ban a flow contextje nincs
BBS::FakeOS::Runner.new(session, DEFINITION, context: {}).run a megadott hash nincs; bontáskor nil-t ad

Egy teljes gép: a TT-80

Egy kitalált 1980-as mikroszámítógép kék képernyővel, bootfolyamattal, az aktuális meghajtót mutató prompttal, generált HELP-pel, egy argumentumos paranccsal, egy saját kérdést feltevővel, egy szándékosan elhasalóval, és EXIT-tel. Mentsd tt80.rb néven:

RUBY
require 'bbs'

TT80 = BBS::FakeOS.define do
  name   'TT-80'
  prompt ->(ctx) { "#{ctx[:drive] || 'A'}> " }

  blue = "\e[44m"
  colors base:   blue,
         text:   "\e[0;37;44m",
         bright: "\e[0;97;44m",
         accent: "\e[0;93;44m",
         error:  "\e[0;91;44m",
         dim:    "\e[0;36;44m",
         prompt: "\e[0;97;44m"

  boot do
    cls
    print 'TT-80 ROM MONITOR V1.3', delay: 0.2
    memtest 64, step: 16, delay: 0.02, label: 'RAM CHECK '
    pause 0.3
    cls
    banner 'TT-80 DISK OPERATING SYSTEM'
    type 'Insert a disk, or just type.', style: :dim, cps: 120
    ctx[:drive] ||= 'A'
  end

  ready do
    puts "#{session.term_cols}x#{session.term_rows} screen. HELP lists the commands.", style: :dim
    newline
  end

  command 'help', '?', help: 'this list' do |_args, io|
    io.definition.order.each do |name|
      entry = io.definition.commands[name]
      io.puts format('  %-6s %s', name.upcase, entry[:help]), style: :bright
    end
  end

  command 'dir', help: 'list the disk' do |_args, io|
    io.puts "  #{io.ctx[:drive]}: GAMES    BAS   #{io.ctx[:drive]}: README   TXT"
  end

  command 'drive', help: 'DRIVE B switches the drive' do |args, io|
    letter = args.strip.upcase
    if letter.match?(/\A[AB]\z/)
      io.ctx[:drive] = letter
    else
      io.puts 'DRIVE A or DRIVE B', style: :error
    end
  end

  command 'name', help: 'asks for your name' do |_args, io|
    io.print 'NAME? ', newline: 0
    name = io.session.readline.to_s.strip
    io.puts "HELLO, #{name.upcase}."
  end

  command 'size', help: 'the screen size' do |_args, io|
    io.puts "#{io.cols} COLUMNS, #{io.rows} ROWS"
  end

  command 'crash', help: 'a broken command' do |_args, _io|
    raise 'DISK NOT READY'
  end

  command 'exit', 'bye', help: 'back to the BBS' do |_args, io|
    io.puts 'SYSTEM HALTED.'
    io.halt
  end

  on_unknown do |input, io|
    io.puts "#{input.split.first.upcase}? SYNTAX ERROR", style: :error
  end
end

És egy board, ami doorként nyitja meg, bbs.rb:

RUBY
require 'bbs'
require_relative 'tt80'

TT80_DOOR = BBS::Door.new(:tt80, TT80, label: '&TT-80')

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    ask :username, prompt: 'Your name'
    call do |ctx, runner|
      TT80_DOOR.run(runner.session, ctx)
      runner.write "\r\nBack on the board, #{ctx[:username]}. Drive was #{ctx[:drive]}.\r\n"
      nil
    end
  end
end

BBS.start

Egy hívás 100×30-as terminállal:

TEXT
TT-80 ROM MONITOR V1.3
RAM CHECK 64K OK

  ╔═══════════════════════════════╗
  ║  TT-80 DISK OPERATING SYSTEM  ║
  ╚═══════════════════════════════╝

Insert a disk, or just type.
100x30 screen. HELP lists the commands.

A> help
  HELP   this list
  DIR    list the disk
  DRIVE  DRIVE B switches the drive
  NAME   asks for your name
  SIZE   the screen size
  CRASH  a broken command
  EXIT   back to the BBS
A> drive b
B> crash
Error: DISK NOT READY
B> foo bar
FOO? SYNTAX ERROR
B> exit
SYSTEM HALTED.

Back on the board, Zsolt. Drive was B.

FakeOS élesben

SERMO (bbs/sermo-bbs, sermo gem, lib/sermo/sermo_os.rb): zöld foszforos gép a Neumatronic-univerzumból. A bootfolyamat egy oldalnyi print ..., delay: sor és egy saját label:-ű memtest; tizenegy szó szerinti parancs (VER, DIR, MEM, …) a „digitális oldal”, minden mást az on_unknown egy AI-karakternek küld a BBS::AI::Typewriter-en át (lásd AI-karakterek). A gem csak a definíció, a Sermo::OS; a gazdaboard futtatja:

RUBY
require 'sermo'

BBS::FakeOS::Runner.new(session, Sermo::OS, context: ctx).run
# vagy egy applicationben:
door :sermo, Sermo::OS, label: '&SERMO'

Commodore 64 és Plus/4 (bbs/teletype-bbs, lib/doors/commodore.rb): egyetlen Commodore.os(machine, games:, sizes:) épít FakeOS-t bármelyik géphez. Maga a definíció apró; a gép egy Shell objektumban él, amit a context tart:

RUBY
BBS::FakeOS.define do
  name   machine.platform
  prompt ''                      # a BASIC maga írja ki a READY.-t
  colors machine.colors          # base: "\e[94;44m" a C64 kék képernyőjéhez

  boot do
    ctx[:commodore] = Shell.new(self, machine, games: games, files: files)
    ctx[:commodore].power_on
  end

  on_unknown { |input, io| io.ctx[:commodore].enter(input) }
end

Egyetlen command sincs benne: minden sor a shellhez megy, ami értelmezi a LOAD"$",8, LIST, RUN parancsokat és a BASIC-sorokat, maga tördeli a kimenetet 40 oszlopra, és EXIT-re io.halt-ot hív. Egy futó BASIC-program io.session.readkey(timeout: 0)-val figyel, így bármelyik billentyű RUN/STOP-ként működik.

Chat of Ra (lib/doors/ra.rb): napisten-karakter, a játéklogika parancsokban (RIDDLE, HINT, SKIP, GAME), a szabad beszélgetés az on_unknown-on át a modellhez megy.

BBS::Gateway

Forrás: lib/bbs/gateway.rb és lib/bbs/gateway/, 0.17.0 óta; a jelentés és a connect_timeout: a 0.31.0-ban jött, a translate: a 0.34.0-ban, a public_only: az 1.0.0-ban. Door egy másik telnetes boardhoz, mint a régi BBS-ek telnet-gatewayei: a hívó vonala változatlanul továbbmegy a másik boardhoz, és akkor jön vissza, amikor az bont. A másik board a saját kódját futtatja a saját szerverén; a gateway csak a bájtokat viszi.

Gateway.new

RUBY
BBS::Gateway.new(host:, port:, name: nil, terminal: 'XTERM-256COLOR', connect_timeout: 5, idle: nil,
                 translate: false, public_only: true)
Opció Alapérték Jelentés
host: kötelező a másik board hosztneve vagy címe
port: kötelező a telnetportja (to_i-vel alakítva)
name: "HOST:PORT" a név a „Connecting to …”, a „… hung up” üzenetekben és a naplóban
terminal: 'XTERM-256COLOR' a jelentett termináltípus, ha a másik board rákérdez (telnet TTYPE)
connect_timeout: 5 ennyi másodpercig vár, hogy a másik board fogadja a kapcsolatot
idle: nil ennyi másodperc kétirányú csend után a hívás úgy ér véget, mintha a hívó bontott volna; nil esetén BBS.config.idle_seconds (ha az sincs, 900 s)
translate: false egy PETSCII-hívó egy AsciiBridge-en át kapja a másik boardot (lásd Egy C64 hív egy ASCII-boardot); a modern hívó vonalát sosem fordítja
public_only: true csak a nyilvános interneten lévő címet hív (BBS::Address.public?); megbízható szomszédhoz, például a host.docker.internal-hoz false
Konstans Érték Jelentés
Gateway::CONNECT_TIMEOUT 5 a connect_timeout: alapértéke
Gateway::BACK_WAIT 30 ennyi másodpercig vár a hívó billentyűjére, mielőtt mindenképp visszaveszi a vonalat

Olvasható: host, port, name. A gateway callable — call(session, context = {}) —, így bármelyik programhoz hasonlóan door lehet belőle:

RUBY
door :feedia, BBS::Gateway.new(host: 'feedia.example', port: 2327, name: 'Feedia'), label: '&Feedia'

Csak nyilvános címek

Az 1.0.0 óta a gateway egyszer oldja fel a hostot (IPv4-címre, ha van ilyen), a BBS::Address.public?-kal ellenőrzi a címet, és erre az ellenőrzött címre kapcsolódik. A loopback, privát, link-local, CGNAT és ULA címeket elutasítja: egy listázás, amely 127.0.0.1-et vagy egy konténert ad meg, nem teheti a boardot a saját hálózatába vezető átjáróvá. A hívó ezt olvassa: „NAME may not be called from here.”, a jelentés error-ja pedig BBS::Gateway::Refused.

Az a board, amely szándékosan a saját szomszédait tárcsázza, ezt ki is mondja:

RUBY
FEEDIA = BBS::Gateway.new(host: 'host.docker.internal', port: 2327, name: 'Feedia', public_only: false)

Jelentés minden hívásról

Egy sok boardot tárcsázó telefonkönyv tudni akarja, hogyan sikerült egy-egy hívás. Blokkal a call a hívás végén átad egy BBS::Gateway::Report-ot (akkor is, ha a hívó közben bontott):

Mező Jelentés
connected fogadta-e a másik board a kapcsolatot
error ami miatt nem jött létre: SocketError (a név nem oldható fel), BBS::Gateway::Refused, Errno::ECONNREFUSED, időtúllépés; nil, ha létrejött
address a hívott IP-cím
connect_seconds mennyi ideig tartott a kapcsolódás
received a másik board által küldött bájtok, a telnet-parancsok nélkül
hung_up ki zárta le az átkelést: :board vagy :caller
RUBY
BBS::Gateway.new(host: board.host, port: board.port, name: board.name, connect_timeout: 10).call(session) do |report|
  record(board, report.connected ? 'ok' : report.error.class.name)
end

Egy C64 hív egy ASCII-boardot

A gateway érintetlenül viszi a bájtokat, így segítség nélkül egy Commodore 64 csak PETSCII-ül beszélő boardot hívhat. translate: true-val a PETSCII-hívó vonala egy BBS::Gateway::AsciiBridge-en megy át (0.34.0 óta):

  • a board szövege (UTF-8, vagy CP437, ha kiderül, hogy nem UTF-8) a PETSCII-kódolón át ér a C64-hez: a betűk a C64 kis-nagybetűjével, az ANSI-színek C64-színekként, a vonalrajz a legközelebbi jellel;
  • a C64 billentyűi úgy jutnak a boardhoz, ahogy egy ANSI-terminál küldené őket: a RETURN CR-ként, a DEL backspace-ként, a kurzorbillentyűk ESC [ A…-ként;
  • a híd mögött a C64 a TTYPE-ra ANSI-t, a NAWS-ra a 40×25-ös méretét válaszolja, így a board 40 oszlopra formáz.
RUBY
BBS::Gateway.new(host: board.host, port: board.port, translate: !board.petscii?).call(session)

Egy átkelés

MERMAID
sequenceDiagram
  participant C as Hívó
  participant G as Gateway (ez a board)
  participant R as Másik board
  C->>G: kiválasztja a doort
  G->>C: Connecting to NAME…
  G->>R: TCP-kapcsolat (5 s)
  R->>G: IAC DO NAWS, IAC DO TTYPE, IAC WILL ECHO …
  G->>R: IAC WILL NAWS + a hívó mérete, IAC WILL TTYPE, IAC DO ECHO …
  R->>G: IAC SB TTYPE SEND
  G->>R: IAC SB TTYPE IS XTERM-256COLOR
  loop amíg valamelyik oldal nem bont
    R->>G: képernyőbájtok
    G->>C: változatlanul továbbadva
    C->>G: billentyűk, egérjelentések
    G->>R: változatlanul továbbadva
    C->>G: IAC SB NAWS (átméretezett ablak)
    G->>R: IAC SB NAWS (az új méret)
  end
  R--xG: bont
  G->>C: NAME hung up. Press a key to return.
  C->>G: egy billentyű
  G-->>C: vissza ezen a boardon

A telnetkliens-oldal

A másik board felé a gateway a telnetkliens. Minden (parancs, opció) párra egyszer válaszol, így egy újra kérdező board sem okoz körbeforgást:

A másik board küldi A gateway válasza
WILL ECHO, WILL SGA DO
WILL bármi más DONT
DO NAWS WILL NAWS, utána a hívó ablakmérete; PETSCII-vonalon WONT NAWS
DO SGA WILL SGA
DO TTYPE (24-es opció) WILL TTYPE; PETSCII-vonalon WONT TTYPE
WILL BINARY, DO BINARY elfogadja, és ugyanezt kéri a hívó termináljától is, így egy BBS::Xmodem-átvitel a gatewayen át is működik
DO bármi más WONT
WONT, DONT semmi
SB TTYPE SEND SB TTYPE IS és a terminal: sztring; PETSCII-vonalon semmi

A másik irányban:

  • A közönséges bájtok mindkét irányban érintetlenül mennek át: ANSI, UTF-8, egérjelentések, PETSCII. A duplázott IAC IAC (egy 0xFF adatbájt) duplázva marad.
  • A hívó ablakméret-jelentései (SB NAWS) ennek a boardnak a sessionjét is frissítik (Session#resize!), így az átkelés után a board már az új méretben rajzol; friss NAWS-jelentésként pedig akkor adja tovább őket, ha a másik board kérte a NAWS-t.
  • A hívó többi telnetparancsát nem adja tovább: ez a board már megegyezett a hívó termináljával, a másik board helyette a gateway válaszait kapja.

Egy PETSCII-vonal (a session.petscii! után, lásd Commodore 64) sem ablakméretet, sem termináltípust nem jelent, így a túloldali kétarcú board (BBS::Terminal) Commodore 64-nek veszi a hívót, és PETSCII-ül beszél. Az általa küldött bájtok érintetlenül mennek a vonalra, nem ennek a boardnak a PETSCII-kódolóján át; a gateway csak translate:-tel lép közbe (lásd Egy C64 hív egy ASCII-boardot).

A session.line a hívó nyers socketje. A gateway közvetlenül ezt olvassa és írja; egy saját door, ami változatlanul adja tovább a vonalat, ugyanígy teheti.

Bontás és hibák

Mi történik Mit lát a hívó Mit csinál a call
a másik board bont „NAME hung up. Press a key to return.” legfeljebb 30 s-ig vár egy billentyűre, aztán visszatér: vissza ezen a boardon
a kapcsolatot elutasítják, a hoszt ismeretlen, vagy letelik a connect_timeout „NAME does not answer. Try again later. Press a key.” naplózza a hibát (gateway: NAME (HOST:PORT): …), legfeljebb 30 s-ig vár egy billentyűre, nil-t ad
a cím nem nyilvános, és a public_only be van kapcsolva „NAME may not be called from here. Press a key.” ugyanez, BBS::Gateway::Refused-dal
a vonal idle másodpercig mindkét irányban néma semmit úgy zárja a hívást, mintha a hívó bontott volna
a hívó bont semmit lezárja a másik board socketjét, és IOError-t dob: a hívás véget ér (a Door#run továbbdobja)

Két következmény, amit érdemes tudni:

  • Az átkelés akkor ér véget, ha a vonal idle: másodpercig mindkét irányban néma (BBS.config.idle_seconds, beállítás nélkül 900 s); aki csak olvas, miközben a másik board küld, az bent marad.
  • A másik board terminálmódjai úgy maradnak, ahogy hagyta őket. Ha bekapcsolta az egérjelentést, és kikapcsolás nélkül bontott, ennek a boardnak pedig ki van kapcsolva a BBS.config.mouse, a hívó terminálja tovább küldi az egérjelentéseket.

Példa: egy board egy másik doorjaként

A B board a 2473-as porton a fenti TT-80-at futtatja; az A board a 2474-esen gatewayként kínálja, egy nem létező boarddal együtt:

RUBY
# board_b.rb
require 'bbs'
require_relative 'tt80'

BBS.configure do |c|
  c.port = 2473
  c.flow = BBS::Flow.define do
    call do |ctx, runner|
      runner.session.settle(timeout: 1.5)   # előbb jöjjön meg az ablakméret
      BBS::FakeOS::Runner.new(runner.session, TT80, context: ctx).run
      nil
    end
  end
end

BBS.start
RUBY
# board_a.rb
require 'bbs'

TT80_BBS = BBS::Gateway.new(host: '127.0.0.1', port: 2473, name: 'TT-80 BBS', public_only: false)
NOWHERE  = BBS::Gateway.new(host: '127.0.0.1', port: 2479, name: 'Nowhere BBS', public_only: false)

BBS.configure do |c|
  c.port = 2474
  c.flow = BBS::Flow.define do
    menu 'Where to?', loop: true do
      option 'TT-80 BBS' do
        call { |ctx, runner| BBS::Door.new(:tt80, TT80_BBS).run(runner.session, ctx); nil }
      end
      option 'Nowhere BBS' do
        call { |ctx, runner| BBS::Door.new(:nowhere, NOWHERE).run(runner.session, ctx); nil }
      end
      option 'Hang up' do
        exit_menu
      end
    end
  end
end

BBS.start

telnet localhost 2474 120×40-es ablakkal, 1-es opció: a B board kiírja: „120x40 screen”; méretezd át az ablakot, és a SIZE parancsa az új méretet mutatja; a B boardon az EXIT után jön: „TT-80 BBS hung up. Press a key to return.” A 2-es opcióra: „Nowhere BBS does not answer. Try again later. Press a key.”

Élesben: TTG Gate BBS

A TTG Gate BBS (bbs/ttg-gate-bbs, 2399-es port) a hálózatunk minden boardját gatewayen át éri el: a tárcsázómenüje a config/boards.yml-ből választ boardot, a lib/ui/dial.rb pedig továbbadja a vonalat. A saját boardjaink szomszédok a hoszton, ezért ez a gateway public_only: false-t ad meg; a Global Discovery telefonkönyvének boardjait az alapbeállítással, a listázás ellenőrzött nyilvános címén hívja, translate:-tel, ha egy C64 ASCII-terminálra készült boardot hív, és minden hívás jelentését rögzíti:

RUBY
BBS::Gateway.new(host: board.host, port: board.port, name: board.name, public_only: false).call(session)

Minden board a hoszton publikálja a telnetportját, így a gate konténere a hoszton át éri el őket (TTG_GATE_BBS_GATEWAY_HOST, host.docker.internal). Linuxos Dockerben alapból nincs host.docker.internal; a compose.yaml a hoszt gateway-címére képezi:

YAML
services:
  ttg-gate-bbs:
    extra_hosts:
      - "host.docker.internal:host-gateway"

A C64-es hívó a gatewayen át sem jelent ablakméretet, így a túloldali board a PETSCII-arcát választja. (2026-11-01-ig a Teletype BBS-nek saját gateway-doorjai voltak az Alexandriához, a Feediához és a többiekhez; azóta a gate az egyetlen átjáró.)

BBS::Zmodem

Forrás: lib/bbs/zmodem.rb, 0.9.0 óta. Chuck Forsberg ZMODEM-jének küldő oldala, annyi, amennyi kell ahhoz, hogy a hívó terminálja fájlokat mentsen a boardról.

send_files

RUBY
BBS::Zmodem.send_files(session, files, wait: 30, telnet: true)
Argumentum Alapérték Jelentés
session kötelező a hívó sessionje; az átvitel read_raw_byte-tal olvassa és write-tal írja
files kötelező [name, data] vagy [name, data, mtime] elemek tömbje; a name-ből csak a fájlnév marad, a data String (bináris is lehet), az mtime Time (alapból Time.now)
wait: 30 ennyi másodpercig vár, hogy egy ZMODEM-fogadó válaszoljon
telnet: true minden 0xFF-et telnetes IAC-ként dupláz; sima bájtcsőhöz false
Eredmény Mikor
:ok a terminál átvette a fájlokat, vagy néhányat kihagyott (ZSKIP)
:cancelled a hívó Esc-et vagy Ctrl-C-t nyomott, amíg a board a terminálra várt, a terminál öt CAN-t küldött, vagy ZCAN-nal, ZABORT-tal, ZFERR-rel válaszolt
:timeout a wait: alatt nem válaszolt fogadó, vagy egy fájlt ötször felkínált válasz nélkül
:hangup leszakadt a vonal

:cancelled és :timeout esetén a board elküldi a megszakító sorozatot (nyolc CAN, nyolc backspace), így a félúton járó terminál is leáll.

Application#with_line

Az átvitelhez nyers vonal és tiszta képernyő kell. A with_line törölt képernyőn, egérjelentés nélkül adja a sessiont a blokkjának, utána visszakapcsolja az egeret (ha BBS.config.mouse), kikényszeríti az application teljes újrarajzolását, és azt adja vissza, amit a blokk:

RUBY
require 'bbs'

README = "Welcome to the files area.\r\n"

FILES = BBS::Application.define do
  helpers do
    def download_readme
      result = with_line do |session|
        session.write("  Sending README.TXT by ZMODEM; Esc gives up.\r\n")
        BBS::Zmodem.send_files(session, [['README.TXT', README, Time.now]], wait: 20)
      end
      message = {
        ok:        'README.TXT is on your disk.',
        cancelled: 'The download was cancelled.',
        timeout:   'Your terminal did not start a ZMODEM download.'
      }.fetch(result, 'The line dropped.')
      BBS::Dialogs.message(self, message, title: ' Download ')
    end
  end

  status_hint('F2', 'Download') { download_readme }
  status_hint('Alt-X', 'Bye') { :halt }
end

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    call do |ctx, runner|
      FILES.run(session: runner.session, session_id: ctx[:session_id], context: ctx)
      nil
    end
  end
end

BBS.start

Applicationön kívül közvetlenül a sessionnel hívd a send_files-t, például egy flow-ból:

RUBY
require 'bbs'

BBS.configure do |c|
  c.flow = BBS::Flow.define do
    call do |_ctx, runner|
      session = runner.session
      data    = (0..255).map(&:chr).join.b * 64   # 16 KB, benne minden bájtérték
      session.write("Sending BYTES.BIN by ZMODEM; Esc gives up.\r\n")
      result = BBS::Zmodem.send_files(session, [['BYTES.BIN', data, Time.at(0)], ['HELLO.TXT', "Hello\r\n"]])
      session.write("\r\nResult: #{result}\r\n")
      nil
    end
  end
end

BBS.start

Mindkét példát az lrzsz rz-jével ellenőriztük egy telnethíd mögött: a fájlok bájtra pontosan megérkeztek, a 0xFF-ek is, az eredmény :ok volt.

Mely terminálokkal működik

A SyncTERM, a NetRunner és a Qodem magától elindítja a letöltést, amikor meglátja a ZMODEM-kérést. Sima telnet mögött az lrzsz rz-je fogadja. Webes terminálban a zmodem.js (a bbsweb böngészős letöltésként menti a fájlt). A board először rz-t és egy CR-t küld, ami egy shellszerű kliensben elindítja az rz-t, aztán a ZRQINIT fejlécet, legfeljebb 10 másodpercenként ismételve, amíg a fogadó nem válaszol.

Hogyan küld

  • Minden vezérlőkaraktert (0x00–0x1F és 0x80–0x9F) ZDLE-vel escape-elve küld, így a folyamban nincs csupasz CR, és nem kell telnet BINARY mód.
  • Az adat CRC-32-vel megy, ha a fogadó felajánlja (CANFC32 a ZRINIT-jében), különben CRC-16-tal, 1 KB-os alcsomagokban.
  • Ha a fogadó pufferméretet jelent, a board ennyi bájt után megáll egy ZACK-ra.
  • A ZRPOS attól a pozíciótól indítja újra az adatot; a ZCRC-kérésre a fájl CRC-32-jével válaszol.
  • Az utolsó fájl után: ZFIN, aztán „OO”.
  • Az egész fájl egy Ruby String a memóriában: tegyél elé méretkorlátot. A WarpEngine BBS legfeljebb WARP_ENGINE_BBS_DOWNLOAD_MAX méretű (alapból 1 MB) fájlt küld.

Buktatók

  • A 0xFF duplázott telnetes IAC, nem ZRUB1. A ZMODEM a 0xFF-et ZDLE plusz ZRUB1 formában is escape-elhetné, de a zmodem.js nem ismeri ezt: a ZDLE-dekódolása egyszerűen „mínusz 64”. Ezért a 0xFF nyersen megy, telnetvonalon IAC IAC-ként duplázva, amit minden fogadó telnetrétege visszaalakít egyetlen 0xFF-re. Ha nincs telnet a két fél között (egy socket egyenesen az rz-be, egy cső), add meg: telnet: false.
  • Az rz ismétli magát. Az lrzsz rz-je induláskor több ZRINIT-et küld, és ismétli a ZRPOS-t; a küldő figyelmen kívül hagyja a ZFILE-ra érkező ZRINIT-et és azt a ZRPOS-t, ami arra a pozícióra mutat, ahonnan épp elindult, különben kétszer küldené az adatot.
  • PETSCII-vonalon nem. C64-es vonalon a session.write a PETSCII-kódolón megy át, és összetörné a bináris folyamot; a C64 XMODEM-mel kapja meg a fájlját (lásd lejjebb).
  • A with_line-on belül. Applicationben mindig a with_line-on belül futtasd az átvitelt: különben az eseményciklus egérjelentései és képernyő-diffelése belezavar a bináris folyamba, és utána nem rajzolódik újra a képernyő.

Élesben a WarpEngine BBS (bbs/warp-engine-bbs, lib/ui/modern.rb és lib/ui/transfer.rb) pontosan a with_line-os példát csinálja: lehozza a fájlt a katalógusból, elküldi send_files(session, [[title.file, data]])-val (vagy XMODEM-mel), és az eredményből dialógust csinál.

BBS::Xmodem

Forrás: lib/bbs/xmodem.rb, 0.26.0 óta. Az XMODEM, az XMODEM-CRC és az XMODEM-1K küldő oldala: ezt az átvitelt ismerik a C64 terminálprogramjai (CCGMS, Novaterm, UltimateTerm), és a SyncTERM, a Qodem vagy az rx is. Az XMODEM nem küld fájlnevet, ezért szólj a hívónak, hogy nevezze el a fájlt, és indítsa el a letöltést a termináljában:

RUBY
result = app.with_line do |session|
  session.write("  Start an XMODEM download of ARIADNE.PRG now; Esc gives up.\r\n")
  BBS::Xmodem.send_file(session, bytes, one_k: true)
end
# => :ok, :cancelled, :timeout (no receiver started) or :hangup
  • Az ellenőrzést a fogadó választja: egy C CRC-16-ot kap, egy NAK ellenőrzőösszeget. one_k: true-val a blokkok 1024 bájtosak, ha a fogadó CRC-t kért, különben mindig 128-asak.
  • Az utolsó blokkot SUB (0x1a) bájtokkal tölti ki, így a mentett fájl legfeljebb egy blokknyival hosszabb lehet; egy C64-es program ugyanúgy betöltődik.
  • PETSCII-vonalon is működik: a bájtok a session.write_raw-val mennek ki, a 0xFF duplázott telnet-IAC-ként, és a vonalat az átvitel idejére telnet BINARY módba kéri, utána vissza (sima csőhöz telnet: false).
  • wait: másodpercig (60) vár a fogadóra; közben az Esc vagy a Ctrl-C (RUN/STOP) feladja, és a terminál két CAN-ja bármikor megszakítja. Egy blokk NAK-ra vagy 20 s csend után újra megy, legfeljebb tízszer.

Lásd még

  • rubbs — az áttekintés és a tartalomjegyzék
  • AI-karakterek — AI-karakter egy FakeOS on_unknown-jában vagy egy window-ban
  • Application — az asztal, amit a doorok elhagynak, és ahová visszatérnek
  • Flow — flow-k doorként, a call és az os lépés
  • Commodore 64 — PETSCII-vonalak, és hogy a gateway miért nem jelent rajtuk ablakméretet
  • Telnet és a store — a session: readline, readkey, read_raw_byte, line
  • Változásnapló — Door (0.6.0), flow-doorok és colors base: (0.7.0), ZMODEM (0.9.0), Gateway (0.17.0), XMODEM (0.26.0), a gateway jelentése (0.31.0), translate: (0.34.0), public_only: (1.0.0)
  • Adat és hálózat — BBS::Address, BBS.log és a többi közös építőelem