Solid

Solid Hardware Without Device File Archaeology

I left an import dialog open, pulled the card reader, and clicked Import. The dialog still had a pointer to a Solid::StorageAccess interface from the device it had displayed. The hardware was gone; my object graph had not heard the news.

Hotplug makes lifetime the main problem, not discovery. A device can disappear between filling a menu and handling its action. A mount can vanish while a job is queued. Keeping /dev/sdb1 does not fix that, and keeping a frontend interface pointer around is worse because it looks usable right up to the point that it is not.

The dialog now stores the device’s unique device identifier, not a borrowed interface object. When the user starts an import, it asks Solid for the device again, checks that it is still valid, obtains the storage interface for that operation, and then checks the current accessibility state. No lookup, no import.

I also listen for deviceRemoved(QString). If the identifier matches the open dialog, the UI disables Import and says the device was removed. Any queued work receives the same cancellation instead of continuing with a path that happened to exist a moment ago.

There is a small race even with the fresh lookup: unplugging is allowed to win after the check. The operation still needs to handle mount or I/O failure. The lookup keeps stale application objects out of the way; it cannot promise that a human will leave the cable alone.

deviceAdded gets less special treatment now. It gives me an identifier to inspect, and I query capabilities when I need them. The removal signal is the one that tears down assumptions.

This fixed a second oddity too: unplugging and reinserting the same reader could produce a new frontend object while the dialog held the old one. Keying the UI by identifier and reacquiring the device made that boring, which is exactly what hotplug code should be.

Network State Is Not a Boolean

My test applet announced “offline” while NetworkManager was still associating with an access point. A second later it announced “online.” Technically energetic, socially useless.

I had reduced state to a boolean. The mechanism has transitions: unavailable, disconnected, preparing, configuring, connected, and failure are meaningfully different to a user and to an application deciding whether to retry.

The applet now maps backend states into three presentation states:

enum Connectivity {
    Offline,
    Connecting,
    Online
};

It preserves the detailed state in the adapter for diagnostics, but the paint code does not understand every NetworkManager value. Solid provides the abstraction boundary; the widget receives only what it can honestly display.

I prefer reacting to state-change notifications rather than polling every second. Polling wastes work and can still miss a short transition. The caveat is startup: the client must query initial state before relying on later notifications, or it may wait forever for a change that already happened.

“Online” also means only that the connection mechanism considers a network active. It does not guarantee a working name server, reachable destination, or paid hotel login. The applet should report connection state, not certify the Internet.

Three states are less satisfyingly binary. They are also less wrong, an increasingly fashionable property in software.

Solid Lines Around Hardware

My network widget broke today because a property changed underneath it. More precisely, the widget knew enough about the hardware backend to parse a backend-specific property, and not enough to notice when that property was absent. This is the sort of knowledge that makes software feel powerful right up to the first different machine.

Solid is the KDE 4 attempt to draw a stable line between applications and hardware discovery mechanisms. An application should ask for devices and capabilities in KDE terms. A backend can obtain those facts from HAL, NetworkManager, or another platform facility. The abstraction is not meant to make all hardware identical; it is meant to stop every application from inventing a private translation layer.

The useful mental model is a device plus interfaces. A device has an identity and general properties. Interfaces describe capabilities such as storage access, battery status, or networking. Code can first ask whether a capability exists, then use the corresponding interface:

Solid::Device device(udi);

if (device.isDeviceInterface(Solid::DeviceInterface::StorageVolume)) {
    Solid::StorageVolume *volume =
        device.as<Solid::StorageVolume>();
    qDebug() << volume->label() << volume->size();
}

The pointer still needs checking because devices disappear and backends can fail. A USB disk is not contractually obliged to remain attached while my slot admires it.

Identity is harder than it first appears. A display name is for humans and can change. A device path may reflect discovery order. A HAL UDI is useful within that backend but should not leak into an application’s persistent file format if the application claims backend independence. For a short-lived object map, the current unique identifier is fine. For durable preferences, I need a stable property appropriate to the device class, and sometimes no truly stable identifier exists.

Hotplug events introduce ordering questions. My first implementation reacted to a “device added” signal by immediately requesting every interface. Some properties were not ready yet, and parent devices could appear after children. The safer path is to tolerate incomplete devices, query only required capabilities, and update when relevant properties change. Applications must not infer a complete topology from the first signal they see.

Networking demonstrates why policy should remain separate. NetworkManager can report devices, available networks, activation state, and changes. Solid can present that information consistently to KDE code. It should not mean that a text editor decides which wireless network to activate. A desktop networking component may own that policy; most applications only need to know whether connectivity is currently available, perhaps with enough uncertainty to avoid promising too much.

Storage has a similar distinction. Discovering a volume is not the same as mounting it, and mounting it is not the same as opening a file manager. Those are three mechanisms with three possible policy owners. When a camera appears, a photo application may offer an import action. Automatically performing one because a hardware signal arrived would be efficient in the same way as a trapdoor.

For the widget, I replaced direct HAL property access with a tiny internal state derived from Solid interfaces:

struct NetworkState
{
    QString name;
    bool available;
    bool active;
};

The widget receives this state and paints it. It no longer knows whether the information came from NetworkManager or a test backend. The adapter listens for device and property changes, rebuilds the affected state, and emits one application-level update. This also keeps backend callbacks away from paint code.

I prefer capability queries over a deep inheritance tree of device types. Real hardware combines roles inconveniently, and capability composition lets one object expose several useful interfaces. The caveat is discoverability: callers must know which combinations make sense, and an abstraction can become a bag of optional pointers if its contracts are weak.

I also prefer asynchronous operations for actions such as mounting or network activation. Hardware can wait for media, authentication, or a helper process. Blocking the GUI thread while reality decides what to do is poor manners. Completion needs an error path that preserves enough backend detail for diagnosis without requiring the application to parse backend internals.

None of this makes Solid finished. We still need to learn which properties are genuinely portable, how device lifetimes should be represented, and how much backend-specific information may escape without defeating the purpose. HAL itself is evolving, and NetworkManager behaviour differs across the machines I can test. A clean interface can expose uncertainty; it cannot abolish it.

My practical rule after today’s failure is simple: application code depends on capabilities, UI code depends on application state, and raw backend properties stop at the adapter boundary. Exceptions may be necessary for specialised tools, but they should look like exceptions rather than quietly becoming architecture.

The widget now survives the missing property. It also reports my wired interface as “network cable,” which is accurate, stable, and not likely to win a naming award.

HAL Is a Source, Not a Policy

I plugged in a USB disk this afternoon and my test application offered to open it twice. Unplugging it removed only one entry. This is an efficient way to make hardware discovery look supernatural.

The bug was mundane: startup enumeration and hotplug notification both created an object, while neither path checked whether the same device identifier was already present. HAL supplies facts about devices and changes, but it does not decide how my application should represent them.

I now funnel both paths through one function:

void DeviceList::addDevice(const QString &udi)
{
    if (devices.contains(udi))
        return;

    Device device = backend->device(udi);
    if (!device.isValid())
        return;

    devices.insert(udi, device);
    emit deviceAdded(udi);
}

Removal uses the same identifier and emits only after the internal state is consistent. That ordering lets slots query the list without observing a ghost.

The larger KDE 4 question is where HAL ends and a hardware abstraction such as Solid begins. I do not want every application parsing HAL properties or knowing which backend discovered a battery. Applications should ask stable questions: is this a storage volume, can it be mounted, is this network interface active? Solid can translate those questions to the platform mechanism.

I prefer keeping the backend thin and putting application policy above Solid. A media player may automatically inspect an audio disc; a file manager may offer actions; a backup tool may ignore both and look only for a labelled volume. The same hardware event should not imply the same decision.

For testing, the backend now accepts a recorded sequence of add and remove events. Replaying “add, add, remove” verifies the duplicate guard without requiring me to develop exceptional timing with a USB plug. It also makes event ordering assumptions visible in a small test rather than in a user’s device list.

The caveat is that abstraction can hide information we have not yet learned to model. During early porting I am logging the raw UDI and selected properties beside the abstract device type. When a device is classified incorrectly, that evidence is valuable. I would remove such noise from a normal release, assuming we eventually have one that does not greet a single disk as twins.