Modular Computers the manual GitHub

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:

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.

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.

See also