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.
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:
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:
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
Forrás: lib/bbs/door.rb.
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(session, context = {})
Sorrendben ezt csinálja:
session.disable_mouse, hogy a program ne kapjon egérjelentéseket, amiket nem tud olvasni.ESC[0m ESC[2J ESC[H — alapszínek, üres képernyő, kurzor a bal felső sarokban.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 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.
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 |
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
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:
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:
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
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:
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:
BBS.config.flow = BBS::Flow.define do
ask :username, prompt: 'Your name'
os TT80
say 'Back on the board.'
end
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.
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:
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.command és az on_unknown blokkok argumentumokat kapnak: (args, io) és (input, io). Minden kimenethez és állapothoz az io-t használd.| 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:
prompt { |ctx| "#{ctx[:drive] || 'A'}> " }
# vagy
prompt ->(ctx) { "#{ctx[:drive] || 'A'}> " }
baseMinden 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.
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 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 boot és a ready után a konzol halt-ig vagy bontásig ismétli:
prompt szerepben.session.readline); a nil, vagyis a bontás befejezi a ciklust.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.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:
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:
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
| 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 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:
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:
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:
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.
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:
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:
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.
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.
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:
door :feedia, BBS::Gateway.new(host: 'feedia.example', port: 2327, name: 'Feedia'), label: '&Feedia'
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:
FEEDIA = BBS::Gateway.new(host: 'host.docker.internal', port: 2327, name: 'Feedia', public_only: false)
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 |
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
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):
ESC [ A…-ként;ANSI-t, a NAWS-ra a 40×25-ös méretét válaszolja, így a board 40 oszlopra formáz.BBS::Gateway.new(host: board.host, port: board.port, translate: !board.petscii?).call(session)
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 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:
IAC IAC (egy 0xFF adatbájt) duplázva marad.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.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.
| 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:
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.BBS.config.mouse, a hívó terminálja tovább küldi az egérjelentéseket.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:
# 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
# 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.”
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:
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:
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ó.)
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.
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.
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:
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:
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.
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.
WARP_ENGINE_BBS_DOWNLOAD_MAX méretű (alapból 1 MB) fájlt küld.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.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.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).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.
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:
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
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.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.on_unknown-jában vagy egy window-bancall és az os lépésreadline, readkey, read_raw_byte, linecolors 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)BBS::Address, BBS.log és a többi közös építőelem