Your own service
A service is a program that runs in the background, started by hand or at every boot, while you
use the computer for other things. mcos runs services with rc. Here is a whole
one, a visitor log: every creature a motion sensor sees, with the world’s
date and time. All of its files come first; the rest of the page explains them.
You need a computer with mcos and a motion sensor card (ls /dev lists
motion0).
The whole thing
1. The program, /home/visitors.lua:
-- visitors: a service that keeps a log of every creature the motion sensor sees, with the world's
-- date and time, a line each, in /home/visitors.log. It runs in the background as a service (rc
-- visitors start), and ends cleanly when stopped (rc sends SIGTERM).
local sys = require("sys")
local LOG = "/home/visitors.log"
local sensor = assert(io.open("/dev/motion0"))
-- rc's stop: say so in the log, and end.
sys.signal("SIGTERM", function()
local log = io.open(LOG, "a")
log:write(os.date("%c"), " stopped\n")
log:close()
os.exit(0)
end)
-- Each event, a line: "minecraft:pig 1.5 0.0 -3.0" (what, and where from the sensor).
for line in sensor:lines() do
local mob = line:match("^(%S+)")
-- Opened and closed each time: a file on a disk shows what's written only once it's closed.
local log = assert(io.open(LOG, "a"))
log:write(os.date("%c"), " ", mob, "\n")
log:close()
end
2. The service, /etc/rc.d/visitors (named for the service: visitors):
-- /etc/rc.d/visitors: the visitors service's definition, for rc. Its settings are all it needs:
-- rc starts the command in the background, keeps its pid in /run/visitors.pid, and stops it.
description = "logs what the motion sensor sees"
command = "/bin/lua"
command_args = {"/home/visitors.lua"}
output_log = "/tmp/visitors.log"
3. Start it at every boot, in /etc/rc.conf (a line a service; rc visitors enable writes it):
# /etc/rc.conf: the services started at boot, a line each.
visitors_enable=YES
4. Use it:
$ rc visitors start
$ rc visitors status
visitors is running (pid 12)
$ cat /home/visitors.log
Day 3 14:02:11 minecraft:pig
Day 3 14:02:40 minecraft:player
$ rc visitors restart
$ rc visitors stop
$ rc visitors status
visitors is stopped
$ rc # every service: enabled or not, running or not
That’s all a service needs. The settings in /etc/rc.d/visitors are enough for rc to start, stop,
restart and check it.
Every action written out
When the definition sets only command, rc does each action itself. A definition can do any of
them its own way instead, by defining a function with the action’s name, and can add actions of its
own: any other function is one (rc visitors count). This is the same service with all of them
written out, doing what rc does by default, plus a count. Copy it, and change what you need:
-- /etc/rc.d/visitors, every action written out: what rc does for a service when its definition
-- only sets command (start, stop, restart, status), done here by the service's own functions, and
-- an action of its own (rc visitors count). rc runs the one named; a function this file doesn't
-- define, rc does itself. 'name' is the service's name (visitors), set by rc.
local sys = require("sys")
description = "logs what the motion sensor sees"
command = "/bin/lua"
command_args = {"/home/visitors.lua"}
output_log = "/tmp/visitors.out"
local LOG = "/home/visitors.log" -- what the program writes
local PIDFILE = "/run/" .. name .. ".pid"
local STOP_SECONDS = 5 -- how long it has to end after SIGTERM, before SIGKILL
local STOP_POLL = 0.1
local NOT_RUNNING = 3 -- status's answer when it isn't running (LSB's)
-- Its process id while it runs, from its pid file; else nil.
local function running()
local file = io.open(PIDFILE)
if not file then
return nil
end
local pid = math.tointeger(tonumber(file:read("l") or ""))
file:close()
for _, process in ipairs(sys.processes()) do
if process.pid == pid and process.state ~= "zombie" then
return pid
end
end
end
-- rc visitors start: runs it as a daemon (a session of its own, so no terminal's Ctrl+C reaches it;
-- its input /dev/null, its output and errors to output_log), its pid in /run/visitors.pid.
function start()
if running() then
print(name .. " is already running")
return 0
end
local null = assert(sys.open("/dev/null"))
local out = assert(sys.open(output_log, "a"))
local pid = assert(sys.spawn(command, command_args, {setsid = true, fds = {[0] = null, [1] = out, [2] = out}}))
sys.close(null)
sys.close(out)
local file = assert(io.open(PIDFILE, "w"))
file:write(pid, "\n")
file:close()
return 0
end
-- rc visitors stop: SIGTERM, then SIGKILL if it hasn't ended in STOP_SECONDS.
function stop()
local pid = running()
if not pid then
print(name .. " is not running")
return 0
end
sys.kill(pid, "SIGTERM")
local waited = 0
while running() and waited < STOP_SECONDS do
sys.sleep(STOP_POLL)
waited = waited + STOP_POLL
end
if running() then
sys.kill(pid, "SIGKILL")
end
sys.unlink(PIDFILE)
return 0
end
-- rc visitors restart.
function restart()
stop()
return start()
end
-- rc visitors status: says whether it runs; 0 when it does, 3 when not.
function status()
local pid = running()
print(name .. (pid and " is running (pid " .. pid .. ")" or " is stopped"))
return pid and 0 or NOT_RUNNING
end
-- rc visitors count: an action of its own: how many visitors its log has.
function count()
local seen = 0
local file = io.open(LOG)
if file then
for line in file:lines() do
if not line:find(" stopped$") then
seen = seen + 1
end
end
file:close()
end
print(seen .. " visitors")
return 0
end
$ rc visitors start
$ rc visitors count
2 visitors
$ rc visitors stop
A function returns the action’s exit status (0: it went well). A function the file doesn’t define,
rc does itself, so you can write just the one you want to change (a stop that saves something
first, say) and leave the rest out.
A daemon that loops forever
The visitor log waits on a sensor, so a read is its loop. Most daemons you write will instead do
something, sleep, and do it again, forever, until they’re stopped. Here is one whole: a ticker that
writes the world’s time to a log every few seconds, and ends cleanly on rc ticker stop.
1. The program, /home/ticker.lua:
-- ticker: a service that wakes every few seconds, forever, and writes the world's time to
-- /home/ticker.log; when rc stops it (SIGTERM), it finishes the line it's on, says so, and ends.
-- Its argument is how many seconds it sleeps between lines (5 when not given).
local sys = require("sys")
local LOG = "/home/ticker.log"
local DEFAULT_SECONDS = 5 -- how long it sleeps between lines, unless its argument says
local seconds = tonumber(arg[1]) or DEFAULT_SECONDS
-- Appends a line to the log. Opened and closed each time: a file on a disk shows what's written
-- only once it's closed, so you can read the log while it runs.
local function log(text)
local file = assert(io.open(LOG, "a"))
file:write(os.date("%c"), " ", text, "\n")
file:close()
end
-- rc's stop sends SIGTERM. The handler only notes it; the loop sees the note and ends. A sleep
-- the signal came in during returns early (EINTR), so it ends at once, not a sleep later.
local stopping = false
sys.signal("SIGTERM", function()
stopping = true
end)
log("started, a line every " .. seconds .. " seconds")
local beats = 0
while not stopping do -- forever, until stopped
beats = beats + 1
log("tick " .. beats)
sys.sleep(seconds) -- waits without using the CPU: the computer draws its waiting power
end
log("stopped after " .. beats .. " ticks")
2. The service, /etc/rc.d/ticker:
-- /etc/rc.d/ticker: the ticker service, a line every 2 seconds, its output and errors in /tmp.
description = "writes the time to /home/ticker.log"
command = "/bin/lua"
command_args = {"/home/ticker.lua", "2"}
output_log = "/tmp/ticker.out"
3. Use it:
$ rc ticker start
$ tail -f /home/ticker.log # Ctrl+C to stop watching (the service goes on)
Day 3 14:02:11 started, a line every 2 seconds
Day 3 14:02:11 tick 1
Day 3 14:02:13 tick 2
^C
$ rc ticker stop
$ tail -1 /home/ticker.log
Day 3 14:02:16 stopped after 3 ticks
Can it loop forever?
Yes. A service is meant to: while not stopping do ... end (or while true do ... end) is the
daemon’s whole life. mcos runs every process in turns, so a loop never freezes the computer or your
shell, and rc stop reaches it wherever it is.
Can it sleep?
Yes, and it should. sys.sleep(seconds) waits that long (the world’s seconds: 20 ticks each) without
using the CPU, so the computer draws its waiting power, not its running power (see
Power). Any call that waits does the same: reading a device’s file (as the
visitor log does), sys.poll, reading a socket. A loop that never waits (while true do end)
still works and still stops, but keeps the CPU busy and the computer at its running power the whole
time, for nothing.
How does rc stop end the loop?
rc ticker stop sends the process SIGTERM, then waits up to five seconds for it to end, then sends
SIGKILL, which nothing can catch. What SIGTERM does is up to the program:
- No handler: it ends the process at once, wherever it is: mid-sleep, mid-read, mid-loop. Fine when there’s nothing to finish.
- A handler (
sys.signal("SIGTERM", function() ... end)): the handler runs instead, in your program, and a call that was waiting (thesys.sleep) returns early, withnilandEINTR. So the ticker’s handler only setsstopping = true; the sleep ends, the loop checksstopping, and the program writes its last line and returns. Keep a handler this small, and do the cleanup after the loop, where you know what state you’re in. - Exit from the handler, as the visitor log does (
os.exit(0)after its last line), when the program has no loop of its own to finish.
A handler that doesn’t end the program (one that ignores the signal) gets SIGKILL after five
seconds anyway. rc ticker status then says it’s stopped, its status 3.
What each part does
The program
A service is an ordinary program that doesn’t end on its own.
- It waits, not spins. Reading the sensor’s file waits for the next event, so it uses no time while nothing moves.
- It ends cleanly when stopped.
rc stopsendsSIGTERM; the program catches it withsys.signal, writes a last line, and exits. Without a handler,SIGTERMsimply ends it. - It closes what it writes. A file on a disk shows what’s written only once it’s closed, so the
log is opened and closed for each line, and you can read it while the service runs
(
tail -f /home/visitors.log).
Try it by hand first: lua /home/visitors.lua, walk past the computer, press Ctrl+C, then
cat /home/visitors.log.
The definition
/etc/rc.d/<name> is a Lua script, run by rc with name set to the service’s name. Its settings:
| Setting | What it is |
|---|---|
command |
The program to run (/bin/lua for a Lua script) |
command_args |
Its arguments: a list, or a string split on spaces |
description |
What it is (rc lists it) |
output_log |
Where what it prints, and its errors, go; nowhere when it isn’t set |
rc start runs the command in a session of its own (so Ctrl+C at your shell never reaches it), its
input from /dev/null, and keeps its process id in /run/<name>.pid (/run is made fresh at every
boot). rc stop sends SIGTERM, then SIGKILL if it hasn’t ended in five seconds. rc status is 0
while it runs, 3 when it doesn’t (LSB’s “not running”).
Starting at boot
At boot, /etc/rc runs rc boot, which starts every service with name_enable=YES in
/etc/rc.conf, in the file’s order. rc visitors enable and rc visitors disable edit that line for
you. Type reboot, and once the computer’s up, rc visitors status says it’s running again.