Lua, ubus ir ubox…

Posted: 2015-06-11 in Darbeliai
Žymos:, , ,

Lua | Programavimas | Darau, blėSenokai apie Lua berašiau. Apie metalentukes bei atseit objektinį programavimą taip ir neparašiau. Bet tik dėl to, kad objektizmo su Lua aš beveik nenaudoju. Pats sau esu parašęs vos tris klases per visą Lua karjerą, o skaityti kitų „objektinį“ kodą išmokau kažkaip natūraliai. Na, ne visai natūraliai, teko pasiskaityti ir suprasti, kas per biesas tos metalentukės yr, bet visgi — apie jas kitą kartą. O šį kartą apie visai normaliems žmonėms nepanaudojamą ir nelabai reikalingą dalyką. Sakykim, kad čia užrašėlis sau ir noras išsilieti ant skaitytojų galvų — tų, kurie visgi ryšis šitą brėdį perskaityti.

Ai, neseniai sužinojau tokią spalvą — „biežinė“ arba „biežava“. Nebuvau susidūręs. Bet čia šiaip, ne į temą.

Neseniai prireikė sukurti porą programulkių OpenWRT aplinkai ir dar padaryt taip, kad jos tarpusavyje bendrautų. Moksliškai tas vadinasi interprocess communication, t.y. duomenukų apsikeitimas tarp procesų (programų). Praktiškai visi procesų apsikaitaliojimai duomenukais yra serverio-kliento tipo išskyrus šiek tiek egzotiškesnius variantus. Man serveris-klientas tinka, nes praktiškai toks ir tikslas yra.

Taigi OpenWRT, maršrutizatorius ir Lua. Kaip padaryti tarpusavyje bendraujančius procesus? Pirmas ateinantis į galvą — TCP. T.y. per tinklą. Nesvarbu, kad tas tinklas būtų tik ant localhost. Taip galima ir taip kartais daroma. Bet man to visai nesinorėjo, bo potencialiai tai yra saugumo skylė. Kitas variantas — Linux socket’ai. Socketai yr beveik tas pats, kas TCP, tik kad komunikavimas vyksta ne per tinklą, o per virtualius failus, t.y. tam tikrą Linux branduolio fyčią. Trečias variantas — named pipes. Šis gan greitai buvo atmestas, nes su paipais reikia elgtis ganėtinai atsargiai, be to, nepatiko man, kaip tas reikalas veikė su Lua. Linux socket’ai irgi neveikė taip puikiai, kaip norėjosi, be to, iškilo problemikė, ką daryti su keliais klientais. Lua mano aplinkoje yra vienos gijos, daugiau pasidaryti negaliu. Taigi lieka semaforai ir Lua coroutines — ganėtinai šlykštus reikalas. Pavyko, tiesa, rasti Copas biblioteką, kuri nuima galvos skausmą. Bet vis tiek — kodas gargariškas ir komplikuotas, nepatiko.

Tada atsirado protingas kolega su patarimais ir ketvirtas variantas — ubusUbus išoriškai — tai toks Linux’o dbus perskerstas variantas. Supaprastintai, tai ir yra labai lengvas būdas tarpusavyje bendrauti procesams. Serverio tipo programa pas ubus prisiregistruoja, kaip tarnyba, o klientas pas ubus gali paklausti, ar tokia tai tarnyba yra. O jei yra, tai, aišku, ja pasinaudoti.

Kadangi ubus yra OpenWRT platformos dalis, tai ja gerai pasirūpinta. Yra ubus demonukas, kuris viskuo rūpinasi, yra bibliotekos (ir netgi Lua biblioteka įsiūta) beigi komandinės eilutės įrankiai bei įnagiai. Taip sakant, imk, naudokis, programuok ir džiaukis lengvai pasiektais rezultatais. Sakote, taip nebūna? Būna būna, tik… su keliom išlygom 😀 OpenWRT platforma tikrai nėra labai gerai dokumentuota, daug kas dokumentuota nuotrupomis, iš kurių tenka nujausti visą likusią dalį. Na, ubus dar ganėtinai gražiai aprašyta, bet jau patyriau, kad funkcionalumo ten nupasakota koks penktadalis. O man tai reikia daugiau.

Nuspręsta, tarprocesiniam mėtymuisi bitukais bus naudojamas ubus. Toliau. Kadangi, kaip minėjau, Lua mano atveju yra vienos gijos, reikia susikrapštyti kokį tai freimworkėlį, kuris reaguotų į ubus klientų užklausas, periodiškai atlikinėtų darbelius beigi paliktų vietos plėstis ir į TCP, nes žinau, jog ateity mano serveriui reikės ir tinklo sąsajos. Naudoti Copas? Bet Copas skirtas tik socketiniam darbui, be to, pats savyje neturi laikmatukų, tektų juos pasiimti iš kitur. Aišku, Copas turi vieną privalumą: galima jį sukombinuoti su kitom korutinom, bet tai vėlgi duoda papildomo darbo.

Netyčiukais, bandydamas GitHube išsikapstyti daugiau ubus naudojimo pavyzdžių, ypač serverių kūrimo — nes klientų tai pilna, užšokau ant tokio įdomaus pavyzdėlio su kažkokia uloop biblioteka. Kodas iš tų magiškų, kur kelios eilutės kažką inicializuoja, o paskutinė paleidžia programos vykdymo ciklą… ir viskas. Tipo, tame cikle viskas automagiškai pasidaro.

Kai dabar jumi rašau, tai galiu pasakyti: viskas veikia puikiai, ubus ir ubox (kurio dalis yra ką tik minėtas uloop) daro tai, ką aš noriu ir taip, kaip aš noriu, kodas švarus ir neperteklinis. Bet: vis dar nesuprantu, kaip uloop biblioteka veikia, nors nagrinėjausi ir originalų C kodą. Jeigu aš vieną eilutę įdedu keliom eilutėm žemiau, nieks neveikia. Suprantat, aš specialiai ubus serviso su uloop nesurišu. Niekaip. O kažkaip veikia, jei vieną uloop eilutę įrašau anksčiau. Taigi išvada yra tokia, kad uloop glaudžiai susijusi su ubus tarnybų bibliotekomis ir jų panaudojimus automagiškai pamato ir užsiregistruoja. Kito paaiškinimo neturiu, bet žinau, kad taip būna. Iš kitos pusės, ne visada privalu suprasti viską, dažniausiai užtenka išmokti naudotis.

Dar trumpai apie uloop. Ši biblioteka vykdo kelias funkcijas, kurios mano projektui gyvybiškai svarbios, būtent:

  • Automagiškai palaiko reagavimą į ubus užklausas
  • Turi integruotą laikmačių palaikymą
  • Moka dorotis su keliais socket’ų klientais
  • Gali paleisti išorinius procesus ir iškelti įvykį, kai procesai baigia darbą

Pasisekė, kaip aklai vištai šūdas. Oj, atsiprašau, t.y. grūdas. Nes čia iš tikro geras grūdas, tikras perliukas.

Ką gi, jei dar neatsibodo įžanga, keli kodo gabaliukai šitos smaguvos pailiustravimui.

Ubus tarnyba

Sakykim, jei maršrutizatoriaus konsolėje parašysite ubus list, tai pamatysite ten besivoliojančią krūvelę jau priregistruotų tarnybų, kurios teikia informaciją ir leidžia valdyti įvairius tinklų parametrus:

root@cyanspot:/tmp# ubus list
hostapd.wlan0
hostapd.wlan1
log
network
network.device
network.interface
network.interface.guest
network.interface.hotspot
network.interface.lan
network.interface.loopback
network.interface.wan
network.wireless
service
system

Šioms tarnyboms galima iškviesti jų metodukus. Sakykim, norim sužinoti WAN būseną, tai rašom:

root@cyanspot:/tmp# ubus call network.interface.wan status '{}'
{
	"up": true,
	"pending": false,
	"available": true,
	"autostart": true,
	"uptime": 197480,
	"l3_device": "eth0.1",
	"proto": "dhcp",
	"device": "eth0.1",
	"metric": 0,
	"delegation": true,
	"ipv4-address": [
		{
			"address": "192.168.41.196",
			"mask": 24
		}
	],
	"ipv6-address": [
		
	],
	"ipv6-prefix": [
		
	],
	"ipv6-prefix-assignment": [
		
	],
	"route": [
		{
			"target": "0.0.0.0",
			"mask": 0,
			"nexthop": "192.168.41.1",
			"source": "0.0.0.0\/0"
		}
	],
	"dns-server": [
		"192.168.50.1"
	],
	"dns-search": [
		"lan"
	],
	"inactive": {
		"ipv4-address": [
			
		],
		"ipv6-address": [
			
		],
		"route": [
			
		],
		"dns-server": [
			
		],
		"dns-search": [
			
		]
	},
	"data": {
		
	}
}

Mūsų užduotėlė — sukurti va tokią kažkokią tarnybą, kuri atsakinėtų į tam tikras užklausas. Sakykim, sukursim tarnybą pavadinimu „organas“, metoduką „raibas“, o parametrą kokį nors, pavyzdžiui, „spalva“. Ir gal „spalva2“. Ir padarysim, kad tarnybėlė atsakinėtų, kokio raibumo organas gaunasi. Naudosime ubus tarnybos registravimui ir atsakinėjimui, na, o uloop — juodam tarnybos darbui atlikti, t.y. laukti užklausų iš (potencialiai) daugybės klientų ir operatyviai atsakinėti. Kaip pastebėsite, kode uloop bus ne kažkas.

Taigi štai organo tarnybėlės išeities kodas:

require "ubus"
require "uloop"

-- Šitą būtina iškviesti prieš prisijungimą prie ubus,
-- kitaip tarnyba neveiks. Automagija.
uloop.init()

-- Jungiamės prie ubus tarnybos.
local conn = ubus.connect()

-- Susikuriam funkcijukę, kuri raibins organą.
local function raibas(req, msg)
	if not msg.spalva or not msg.spalva2 then
		-- Patikra, ar atsiųstos abi spalvos.
		conn:reply(req, { error = "Nenurodyta nei viena spalva!" })
		return
	end
	
	conn:reply(req, { raibumas = string.format("Organas %si-%si raibas", msg.spalva, msg.spalva2) })
end

-- O čia dabar komplikuotas reikalas toks.
-- Registruojam tarnybą „organas“ su metodu „raibas“.
-- Metodą „raibas“ sudaro nuoroda į aukščiau aprašytą
-- funkciją beigi kintamųjų registravimas, kuriuos reikia
-- šitam metodui perduoti. Tai — metainformacija tiems,
-- Kas norėtų pasidomėti, ko reikia šio metodo iškvietimui.
conn:add {
	organas = {
		raibas = {
			raibas,
			{ spalva = ubus.STRING, spalva2 = ubus.STRING }
		}
	}
}

-- Viskas, leidžiam uloop.run(). Kaip matote, nėra tiesioginio
-- surišimo su ubus tarnyba, tačiau šis ciklas sukasi ir vykdo
-- tarnybos „klausymą“. Automagija.
uloop.run()

Išsaugom šitą reikalą maršrutizatoriuje, faile „organas.lua“, ir paleidžiam lua organas.lua. Ekrane nieko nebus matyt, tačiau jei kitame konsolės lange paleisim komandą ubus list, pamatysim naują tarnybą „organas“:

root@cyanspot:~# ubus list
hostapd.wlan0
hostapd.wlan1
log
network
network.device
network.interface
network.interface.guest
network.interface.hotspot
network.interface.lan
network.interface.loopback
network.interface.wan
network.wireless
organas
service
system

Ką gi, štai gi, vagi na. Dabar kviečiam tokią komandžikę (kreipiamės į „organo“ tarnybą) beigi žiūrom, ką gausim:

root@cyanspot:~# ubus call organas raibas '{ "spalva": "raudona", "spalva2": "juoda" }'
{
	"raibumas": "Organas raudonai-juodai raibas"
}

Valio! Tarnybėlė veikia. O jei nepaduosim kurios nors spalvos?

root@cyanspot:~# ubus call organas raibas '{ "spalva": "geltona" }'
{
	"error": "Nenurodyta nei viena spalva!"
}

Šiaip aš iš pradžių galvojau, kad tarnybos metodo priregistravimas ir reikalingų kintamųjų aprašymas galbūt jau bus kaip nors apdorojamas pačiame ubus demone. Bet ne, reikalingų kintamųjų aprašymas yra tik inforamcinis. Na, tiems, kurie nežinodami tarnybų jas nori panaršyti, pažiūrėti, kokius metodus jos turi, kokių parametrų jiems reikia. Bet parametrų validavimas yra jau tarnybos bėda, o ne ubus automagija.

Kaip supratote, viena programa gali priregistruoti ir kelias tarnybas beigi, aišku, kiekvienoje tarnyboje gali būti ir bile kiek metodų. Bet štai šitokiu būdu bendravimas tarp procesų yra gan paprastas. Va toks Lua skriptukas darys lygiai tą patį, ką daro ir komandinė eilutė: užklaus kažko iš tarnybos.

require "ubus"

local conn = ubus.connect()

local resp = conn:call("organas", "raibas", { spalva = "geltona", spalva2 = "balta" })

for i, v in pairs(resp) do
	print(i, v)
end

Išsaugojus šį tekstuką faile ir paleidus gausis va kas:

root@cyanspot:~# lua ask-organas.lua 
raibumas	Organas geltonai-baltai raibas

Tai va, tiek tų tarnybų. Dabar tik daryt tikrus darbus 🙂

Uloop laikmačiai

Žiauriai naudinga uloop savybė yra laikmačiai. Jie skirti kažkokias funkcijas įvykdyti po nurodyto laiko tarpo. Šiek tiek pasikrapščius galima padaryti, kad tos funkcijos laikmačius atnaujintų ir suktųsi periodiškai, sakykim, kas 3 sekundes (ar kas valandą, kaip norisi ar reikia).

Štai paprasčiausias pavyzdukas, kur programa išspausdina Unix laiką, tada paleidžia laikmatį, o tas irgi išspausdina Unix laiką. Čia laikmatis paleidžiamas tik vieną kartą:

require "uloop"

uloop.init()

-- Sukuriam uloop laikmati ir jam sukuriam
-- funkciją, kurią jis turės įvykdyti.
local t = uloop.timer(
	function()
		print(os.time())
	end
)

-- Nustatom laikmatį pasileisti po 3 sekundžių.
t:set(3000)

-- Išspausdinam dabartinį laiką.
print(os.time())

uloop.run()

Išsaugom faile l1.lua ir paleidžiam:

root@cyanspot:/tmp# lua l1.lua 
1433943067
1433943070

Nagi va ir viskas. Bet čia vienkartinis laikmatukas. Jam paduodama anoniminė funkcija (deklaruojama vietoje). O jeigu mumi reikia periodinio kažkokios funkcijos vykdymo? Viskas gan paprasta, truputuką perrašom kodą:

require "uloop"

uloop.init()
-- Sukuriam laikmačio kintamąjį iš anksto
local t

local function periodic()
	print(os.time())
	-- Pačioje funkcijoje nurodom laikmačio pasileidimą
	-- kitukart po trijų sekundžių
	t:set(3000)
end

-- Sukuriam „realų“ laikmatį su aukščiau aprašyta funkcija
t = uloop.timer(periodic)

-- Nustatom laikmatį pasileisti po 3 sekundžių.
t:set(3000)

-- Išspausdinam dabartinį laiką.
print(os.time())

uloop.run()

Išsaugom į l2.lua ir paleidžiam, va ir rezultatas, laiko išvedimas kartojamas kas tris sekundes:

root@cyanspot:/tmp# lua l2.lua 
1433943374
1433943377
1433943380
1433943383
1433943386
1433943389
1433943392
1433943395

Su laikmačiais viskas yzy, škia.

TCP serveris

O dabar jau bjauresnis reikalas. Darom kiek primityvoką TCP serverį, bet su uloop. Kad nereikėtų patiems korutinų rašyt keleto klientų apdorojimui.

require "uloop"
require "socket"

uloop.init()

local tcp = socket.tcp()
-- Labai svarbu  nulinis timeout'as. Tai reiškia,
-- kad tinklo prievado skaitymas nebus blokuojantis!
tcp:settimeout(0)
tcp:bind("*", "1822")
tcp:listen()

-- Pirmiausia užregistruojam TCP susijungimo dorojimo įvykį.
tcp_event = uloop.fd_add(tcp,
	function(tfd, events)
		-- Duodam trumpą timeoutą, per kurį gal koks klientas
		-- prisijungs.
		tfd:settimeout(3)
		local client = assert(tfd:accept())
		if client then
			-- Jei klientas prisijungia, registruojam naują įvykį:
			-- skaitymą iš kliento.
			uloop.fd_add(client, function(client_sock, events)
					local req = client_sock:receive()
					if req then
						-- Jei pavyko kažką gauti iš kliento, atspausdijam.
						print("Gavom per tinklą: "..tostring(req))
					else
						-- O jei nepavyko, tai konkretaus kliento socket'ą
						-- uždarom ir užmirštam.
						client_sock:close()
					end
				end,
				uloop.ULOOP_READ
			)
		end
	end,
	uloop.ULOOP_READ
)

uloop.run()

Nu va. Dabar vienoje konsolėje paleidžiam serverį, o kitoj — telnetinamės į atidarytą 1822 prievadą bei rašinėjam nesąmones:

root@cyanspot:~# telnet localhost 1822
blahaha
bimbambam
mušam bimbalą??

Serverio konsolėje matosi, kas gaunama:

root@cyanspot:/tmp# lua tcpserv.lua 
Gavom per tinklą: blahaha
Gavom per tinklą: bimbambam
Gavom per tinklą: mušam bimbalą??

Nu va matot, kaip viskas yzy su tuo uloop 🙂 Šast, pyst ir yr.

Uloop procesų valdymas

O dabar — paskutinė užrašėlio dalis. Apie tai, kaip paleisti kažkokį procesą fone ir gauti žinią, kad jis baigė vykdytis. Paprastai, sakykim, paleidus procesą su os.execute(…) programa sustos ir lauks, kol paleistas procesas baigs vykdytis. Nebent gale parašysite ampersando ženkliuką ir procesas nukeliaus į foną. Bet tada „nepagausite“, kada jis baigė vykdymą. Kartais to užtenka, kartais — ne. Aktyvus serveris, kuris turi operatyviai aptarnauti krūvelę klientų, negali sau leisti tokios prabangos, kaip proceso užbaigties laukimas. Ką gi, uloop eilinį kartą gelbsti ir čia.

Pasirašiau paprastutį, penkias sekundes uždelsiantį, shell skriptuką:

#!/bin/sh

echo "Procesas su PID=$$, arg1=$1, arg2=$2"
sleep 5
echo "Procesas $$ nusibaig."

O štai čia — Lua skriptas su uloop proceso paleidimo ir jo pabaigos įvykio apdorojimu. Pirmiausia eina kviečiamos programos pavadinimas, toliau eina argumentai jai. Po to “PROCESS=1” (ar koks kitas skaičiukas, stringas ar daugiau tokių eilučių) nueina į proceso aplinką (environment). Taip galima perduoti reikalingus aplinkos kintamuosius. Paskutinis argumentas — proceso pabaigos apdorojimo funkcijas (handler) Va maždaug taip atrodo:

require "uloop"

uloop.init()

function process_end(r)
	print "Procesas nusibaigė"
	print(r)
	print(os.time())
end

print(os.time())
uloop.process("/tmp/sleep.sh", { "bla", "blue"}, { "PROCESS=1" }, process_end)

uloop.run()

O štai paleidžiam ir va toks rezultatas gaunasi (išspausdinami laikai prieš paleidžiant procesą ir apdorojant jo pabaigos įvykį):

root@cyanspot:/tmp# lua proc.lua 
1433947110
Procesas su PID=13750, arg1=bla, arg2=blue
Procesas 13750 nusibaig.
Procesas nusibaigė
0
1433947115

Kaip matot, print(r) išspausdina nuliuką. Tai — paprasčiausias exit kodas.

Na tai tiek. Atminkite, kad ubus ir uloop yra OpenWRT reikalas, normaliame gyvenime nelabai panaudosit. Ale va, jei sugalvosit savo maršrutizatorių patobulinti, tai šitos priemonės baisiai geros 🙂

Advertisements

Parašykite komentarą

Įveskite savo duomenis žemiau arba prisijunkite per socialinį tinklą:

WordPress.com Logo

Jūs komentuojate naudodamiesi savo WordPress.com paskyra. Atsijungti / Keisti )

Twitter picture

Jūs komentuojate naudodamiesi savo Twitter paskyra. Atsijungti / Keisti )

Facebook photo

Jūs komentuojate naudodamiesi savo Facebook paskyra. Atsijungti / Keisti )

Google+ photo

Jūs komentuojate naudodamiesi savo Google+ paskyra. Atsijungti / Keisti )

Connecting to %s