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.
-
Read gives a line a slot, the target first:
target 2 hfs 40960 4194304 backups source -For a disk the line is: the slot, its tier, its format, bytes used, capacity, and its label (a disk with no label ends after its capacity). An empty slot is
slot -. The file holds the slots as they were when it was first read; seek back to the start to read them afresh. - Write: each line written is an action, done at once:
format,wipe,clone,installorupdate. Any other line fails the write withEINVAL, and so does an action the station refuses (no disk, no power, it doesn’t fit). Throughio, a write is buffered: the line goes when the file is flushed or closed, and that call reports the failure. - For
sys.poll, the file is always ready to read and to write, and never hasPRI(the station has no interrupt).
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))
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