Modular Computers the manual GitHub

Disk Station

A Disk Station is a dock for disks. It formats, wipes, clones and installs mcos on a disk without crafting it alone. Each action is a method, a line written to its file, and a switch in its screen.

Facts

| | | |—|—| | Type | disk_station | | In /dev | diskstation0, diskstation1… in bus order | | Address | Cabled: it is known by its id: name | | Interrupt | None | | Power | 1 W at rest; each action costs 2000 units (twenty ticks of a running CPU), paid from the bus’s power while a running computer supplies it, else from the station’s battery | | Info | none beyond the common ones |

For the block and its slots, see Disk Station.

Target and source

The station works on its target disk. Its source disk is optional and only read, as a forensic dock’s write blocker is: every write to it fails with read-only disk. The source is used by clone.

Both disks, and the station itself, are on the bus of the computers wired to it, beside each other. A disk behind the station is on the bus only while the station is. See Bus. Once on the bus, each disk is a normal hard disk, with its own /dev/storage file.

In mcos

/dev/diskstation0

The station’s file is text, as a sysfs attribute is.

cat /dev/diskstation0
echo install > /dev/diskstation0

Its ioctls are its methods

The file’s ioctls are the methods below, by name, in any open mode. They say why an action was refused, as a write cannot: the ioctl gives nil, the station’s message (no power, doesn't fit: ...) and EINVAL. An action that worked gives true.

local ok, why = sys.ioctl(sys.fileno(io.open("/dev/diskstation0")), "install")

Methods

Every action fails with no disk when the target slot is empty, and with no power when neither the bus nor the battery can pay for it.

format() → nothing

Puts an empty hfs filesystem on the target. Everything on it is gone.

wipe() → nothing

Makes the target raw, with no filesystem. Everything on it is gone.

clone() → nothing

Makes the target a copy of the source: its format and its contents, with the files’ times kept. Fails with no source disk when the source slot is empty, and with doesn't fit when what is on the source is larger than the target: an hfs source’s used bytes, or a raw source’s whole capacity (a raw source needs a target as big). Nothing changes when it is refused.

install() → nothing

Formats the target as hfs, then copies mcos onto it (the mod’s latest).

update() → nothing

Writes the mod’s latest mcos files over the target’s, /etc too. Everything else on the disk is left, and /home is never written over. The target must be hfs (a raw one fails with raw disk: no filesystem).

get() → {target = disk, source = disk}

The state of the slots. A disk is {tier, mode, used, capacity, label}; label is left out when the disk has none, and an empty slot is left out of the table.

set({action = name}) → nothing

Does an action by its name: format, wipe, clone, install or update. Any other name fails with no such action.

Events

None. Changes to the disks are reported by the disks themselves (Hard disk), so a program that watches a disk sees the work.

Examples

Do one action, checking first

Through the station’s file: reads the slots, checks the action can be done, writes it, and reads the result.

-- Do one Disk Station action through its /dev file, checking first that the disks it needs are in.
--
-- usage: lua disk-station-do.lua action [station]
--   action   format, wipe, clone, install or update
--   station  the station's name in /dev (default diskstation0)
--
-- Reading the station's file gives a line a slot: "target 2 hfs 40960 4194304 backups" (the slot,
-- then its disk's tier, format, bytes used, capacity and label), or "source -" for an empty slot. A
-- line written is an action, done at once; a write that fails (the action refused) fails with
-- EINVAL. (The station's methods, as ioctls, say why: see disk-station-clone.lua.)
local ACTIONS = {format = true, wipe = true, clone = true, install = true, update = true}

local action, station = ...
station = "/dev/" .. (station or "diskstation0")
if not ACTIONS[action] then
  io.stderr:write("usage: disk-station-do.lua format|wipe|clone|install|update [station]\n")
  os.exit(2)
end

local function die(...)
  io.stderr:write("disk-station-do: ", ...)
  io.stderr:write("\n")
  os.exit(1)
end

-- The slots, as the file says them: {tier, mode, used, capacity, label} for a disk, false for none.
local function slots()
  local file = assert(io.open(station))
  local found = {}
  for line in file:lines() do
    local slot, rest = line:match("^(%S+) (.*)$")
    if rest == "-" then
      found[slot] = false
    else
      local tier, mode, used, capacity, label = rest:match("^(%d+) (%S+) (%d+) (%d+) ?(.*)$")
      found[slot] = {tier = tonumber(tier), mode = mode, used = tonumber(used),
        capacity = tonumber(capacity), label = label ~= "" and label or nil}
    end
  end
  file:close()
  return found
end

local before = slots()
if not before.target then
  die("no disk in the target slot")
elseif action == "clone" then
  local source = before.source
  if not source then
    die("clone: no disk in the source slot")
  end
  local needed = source.mode == "raw" and source.capacity or source.used
  if needed > before.target.capacity then
    die("clone: the source needs ", needed, " bytes; the target holds ", before.target.capacity)
  end
elseif action == "update" and before.target.mode ~= "hfs" then
  die("update: the target is ", before.target.mode, ", not hfs: install instead")
end

local file = assert(io.open(station, "w"))
file:write(action, "\n")
local ok, why = file:close() -- (the line is written at the latest when the file closes)
if not ok then
  die(action, ": ", why)
end

local after = slots().target
print(string.format("%s: done; the target is now %s, %d of %d bytes used", action, after.mode, after.used, after.capacity))

Download disk-station-do.lua

lua disk-station-do.lua install

Clone a disk

Through the station’s methods, which say why when they refuse.

-- Clone the source disk onto the target, through the station's methods (its file's ioctls), and
-- report what was copied.
--
-- usage: lua disk-station-clone.lua [station]     (default diskstation0)
--
-- "get" gives the slots as a table ({target = disk, source = disk}, each {tier, mode, used,
-- capacity, label}, an empty slot left out); "clone" makes the target a copy of the source, its
-- format and its files (their times kept), or refuses with nothing changed. The source is only read:
-- the station is its write blocker.
local sys = require("sys")

local station = "/dev/" .. (... or "diskstation0")
local file = assert(io.open(station))
local fd = sys.fileno(file)

local function describe(disk)
  return string.format("tier %d %s, %d of %d bytes used%s", disk.tier, disk.mode, disk.used, disk.capacity,
    disk.label and (", labeled " .. disk.label) or "")
end

local slots = assert(sys.ioctl(fd, "get"))
if not (slots.source and slots.target) then
  io.stderr:write("disk-station-clone: put a disk in both slots (", slots.source and "target" or "source", " is empty)\n")
  os.exit(1)
end
print("source: " .. describe(slots.source))
print("target: " .. describe(slots.target) .. " (everything on it will be gone)")

local ok, why = sys.ioctl(fd, "clone")
if not ok then
  io.stderr:write("disk-station-clone: ", why, "\n") -- (doesn't fit, no power...)
  os.exit(1)
end
slots = assert(sys.ioctl(fd, "get"))
print("cloned: the target is " .. describe(slots.target))
file:close()

Download disk-station-clone.lua

A duplicator

Left running, it makes each disk put in the target slot a copy of the source, or a fresh mcos install when there is no source, and logs each one.

-- A disk duplicator: each disk put in the station's target slot is made ready on its own, a copy of
-- the source disk when there is one, else a fresh mcos install, and the work is logged.
--
-- usage: lua disk-station-duplicator.lua [station [log]] &
--   station  the station's name in /dev (default diskstation0)
--   log      where to log each disk done (default /home/duplicator.log)
--
-- The station raises no interrupt when a disk goes in, so the program looks at its slots every POLL
-- seconds ("get", its method), sleeping in between (sys.sleep blocks: it uses no time). A disk is
-- done once, when it appears in the target slot; take it out and put in the next.
local sys = require("sys")

local POLL = 1 -- seconds between looks at the slots

local station, logname = ...
station = "/dev/" .. (station or "diskstation0")
logname = logname or "/home/duplicator.log"

local file = assert(io.open(station))
local fd = sys.fileno(file)

local function log(line)
  local out = assert(io.open(logname, "a"))
  out:write(os.date("%T "), line, "\n")
  out:close()
end

local waiting = true -- the target slot was empty at the last look (a disk in at the start is done)
while true do
  local slots = sys.ioctl(fd, "get")
  if type(slots) == "table" and slots.target and waiting then
    local action = slots.source and "clone" or "install"
    local ok, why = sys.ioctl(fd, action)
    if ok then
      local disk = assert(sys.ioctl(fd, "get")).target
      log(string.format("%s: done, tier %d %s, %d bytes used", action, disk.tier, disk.mode, disk.used))
    else
      log(action .. ": failed: " .. tostring(why))
    end
  end
  waiting = not (type(slots) == "table" and slots.target)
  sys.sleep(POLL)
end

Download disk-station-duplicator.lua