Haiku BVolumeRoster: Volume Enumeration, Mount Events, and Safe Reconciliation
Build reliable Haiku volume inventories with BVolumeRoster, mount notifications, error-aware metadata reads, and race-safe reconciliation after media changes.
Haiku’s Storage Kit exposes mounted file systems through BVolumeRoster and BVolume. The roster is useful for a file manager, backup utility, media catalog, or status panel that needs to discover currently available volumes and react when one mounts or unmounts. It is not a disk-partition manager, a mount controller, or a durable identity service. That distinction matters: a notification tells an application that the mounted-volume set changed, but it does not make a previously cached path, directory handle, or operation safe to reuse.
The robust model is deliberately simple: enumerate to construct a current view, watch for change notifications, and reconcile from a fresh enumeration when a notification arrives. Treat events as invalidation signals, not as a complete transactional history of every device operation. The official Haiku documentation describes BVolumeRoster as a wrapper around next_dev() enumeration and node watching; the public API and current implementation are the authority for the details below.
What the roster represents
BVolumeRoster provides GetNextVolume(), Rewind(), GetBootVolume(), and mount-change watching. GetNextVolume() fills a caller-provided BVolume object with the next available mounted volume. BVolume is a small wrapper around a device identifier and volume metadata supplied by the file-system layer. It can report a name, root directory, capacity, free bytes, and characteristics such as read-only, removable, persistent, or shared where supported.
Do not confuse a volume with a physical disk. A single physical disk can contain multiple volumes; a removable device can be absent; a network file system may be mounted without local removable media; and a mounted file system can outlive a particular user-interface selection or path string. Nor does the roster tell you how to partition, format, mount, or eject media. Use the relevant system facilities for those actions and treat their result as a separate operation.
The dev_t returned by BVolume::Device() is useful to refer to a volume in the current system context. The API documentation does not define it as a globally unique, permanent identifier suitable for databases or synchronization across boots. Persisted records should use an application-specific matching strategy and be prepared for ambiguity or disappearance. A display name is also not a stable key: labels can be changed and different volumes may use the same human-readable name.
Enumerate the current set and check every result
The roster constructor is ready for use. Rewind before beginning a new pass, then call GetNextVolume() until enumeration reports that there are no more entries. A preallocated BVolume object is required. For each successful item, inspect the volume only while its operations remain valid and check each returned status or numeric value rather than assuming that every file system implements every capability.
BVolumeRoster roster;
BVolume volume;
roster.Rewind();
while (roster.GetNextVolume(&volume) == B_OK) {
const dev_t device = volume.Device();
char name[B_FILE_NAME_LENGTH];
status_t nameStatus = volume.GetName(name);
status_t rootStatus = volume.InitCheck();
off_t capacity = volume.Capacity();
off_t freeBytes = volume.FreeBytes();
// Publish only fields whose calls succeeded. Keep device and name
// as current-session inventory data, not as a permanent identity.
if (nameStatus == B_OK && rootStatus == B_OK) {
UpdateVolumeRow(device, name, capacity, freeBytes);
}
}
This sketch intentionally does not interpret every non-B_OK result as an empty inventory. B_BAD_VALUE is documented when the last volume was already returned, but callers should log and distinguish unexpected errors if the target Haiku revision exposes them. More importantly, Capacity() and FreeBytes() return signed off_t values and can report an error as a negative value. Do not cast a negative result to an unsigned type; doing so can turn a failed query into an enormous apparent capacity. For production code, encapsulate these queries in helpers that return both status and value, and retain “unknown” separately from zero.
The roster’s cursor is an enumeration mechanism, not a subscription or guaranteed immutable snapshot. A device can change state while an application is enumerating. If the UI needs an eventually current list, make reconciliation idempotent: build or update entries from the latest pass, and remove records that are no longer present only after a completed pass that your code considers valid. Do not let a partial pass caused by an error erase the entire previously displayed inventory.
Watch mount changes, then reconcile
StartWatching(BMessenger) requests mount and unmount notifications for a target. The message format follows watch_node() notifications. The relevant node-monitor opcodes are B_DEVICE_MOUNTED and B_DEVICE_UNMOUNTED, carried in the message’s opcode field when the message is a B_NODE_MONITOR notification. BVolumeRoster stops a previous watch if StartWatching() is called again with a different target, and StopWatching() or destruction ends the watch.
void VolumeHandler::MessageReceived(BMessage* message)
{
int32 opcode;
if (message->what == B_NODE_MONITOR
&& message->FindInt32("opcode", &opcode) == B_OK
&& (opcode == B_DEVICE_MOUNTED || opcode == B_DEVICE_UNMOUNTED)) {
ScheduleVolumeReconciliation();
return;
}
BHandler::MessageReceived(message);
}
The handler schedules reconciliation instead of doing a potentially expensive full inventory scan inline. If several notifications arrive quickly, coalesce them into one queued refresh. Ensure the BMessenger target is attached to a live looper before enabling the watch, and stop watching during teardown. A failed StartWatching() is not a harmless warning: without a live watch, the inventory can become stale indefinitely unless you have another refresh policy.
Notifications are change signals, not a substitute for querying the current state. Avoid building correctness around undocumented assumptions that every event contains a stable volume key or that mount and unmount messages are delivered as an exactly-once, gap-free transaction log. The safest response is to enumerate again and reconcile by current properties. If the application also watches individual files with BNodeMonitor, keep that separate: volume mount notifications and node-level file changes answer different questions.
There is a startup race to handle. If you enumerate first and only then start watching, a mount can occur in the gap and the first inventory misses it. If you watch first and then enumerate, a change can race with that initial pass and cause redundant refreshes. Redundant reconciliation is inexpensive compared with a stale view. A practical sequence is to start watching, perform a full inventory, and schedule one more pass if a notification was observed during that initial scan. Protect the “refresh pending” and “scan in progress” state with the application’s looper or an explicit lock; do not mutate UI state from arbitrary worker threads.
Treat each mounted root as a short-lived capability
BVolume::GetRootDirectory() can initialize a BDirectory at a volume’s root. That directory is useful for browsing, but it is not proof that the medium will remain mounted until the next operation. A user can remove a USB device, a network mount can disappear, or the file system can become unavailable. Check operation results and handle failures as ordinary lifecycle events.
Do not cache a BDirectory or a child path indefinitely and assume that it remains usable because the volume was once present. When a traversal fails, report the failure in context, invalidate stale UI state, and reconcile the roster. Use appropriate node references or entry references for the operation at hand, but still handle B_ENTRY_NOT_FOUND, I/O errors, and other status returns. A path is a name in the current namespace, not a lease on media.
The volume name has similar limits. BVolume::SetName() can rename a volume, and the implementation has special handling for the root directory’s mounted path when it corresponds to the old name. Therefore, do not infer a filesystem’s mount path from its label or use a label as a security boundary. Display names should be escaped and treated as data, especially if included in logs or generated filenames.
Build a reconciliation routine, not a notification parser
A useful reconciliation routine has four properties:
- It runs on one serialized execution context or protects shared state explicitly.
- It enumerates into a temporary snapshot, checking status and querying only the metadata the UI actually needs.
- It merges successful observations with existing records, marking unavailable fields as unknown and removing absent devices only after a complete pass.
- It replaces the visible model atomically, so a user never sees a half-updated list while devices are changing.
For a backup application, the same rules prevent a dangerous failure mode: silently skipping a volume that disappeared during traversal and then claiming the backup set is complete. Record which volumes were observed, which were processed, and which operations failed. If the roster changes mid-run, decide whether to retry, mark that device incomplete, or require operator confirmation. Do not represent a partial scan as a successful full backup.
Test the lifecycle, not just the happy path
On a disposable Haiku system, test startup with zero removable volumes, multiple simultaneous mounts, a read-only file system, repeated unmount/remount of the same device, and a mount change while the initial enumeration is in progress. Exercise an invalid messenger and a target that is destroyed, verifying the application stops watching before teardown. Disconnect media while a file operation is active and verify that the error propagates without a crash, stale pointer use, or false success state.
Log useful identifiers and results, not just “volume changed.” Include the notification opcode, whether a reconciliation was queued or coalesced, enumeration status, current display name when readable, and the operation that failed. Avoid logging user file contents or sensitive names unnecessarily. For acceptance, verify that a newly mounted volume appears after refresh, an unmounted volume disappears after a successful reconciliation, partial failures do not clear valid records, and capacity errors never render as huge unsigned values.
BVolumeRoster is a compact API, but reliable volume-aware software depends on lifecycle discipline around it. Start a watch, treat events as prompts to refresh, model failed queries explicitly, and make every filesystem operation resilient to the volume vanishing between discovery and use.
Related:
- Haiku Node Monitoring: Receiving Live File-System Changes Through the Storage Kit
- BFS: How Haiku’s File System Doubles as a Database
Sources: