Search code examples
c++systemddbus

How to get the state of a service with sd-bus?


I need to query, monitor and possibly change the state of a few systemd services from a C++ application. It looks like sd-bus is the right way to do this, but I'm having a terrible time finding an example.

So, how do I:

1) Query the current status of a service via sd-bus, similar to systemctl status foo.service?

2) Monitor the status of a service such that I get a callback whenever it changes?

3) Change the status of a service, similar to systemctl start/stop/restart?

Thanks!


Solution

  • Using the sd-bus API is absolutely correct (header #include <systemd/sd-bus.h>)

    First you need get access to a bus object:

    I do this:

    Systemctl::Systemctl() :
        m_bus(nullptr)
    {
        int r = sd_bus_default_system(&m_bus);
    
        if (r < 0)
            throw exception("Could not open systemd bus");
    }
    

    If you're having problems opening the bus:

    1. Run as root/sudo
    2. Make some polkit policies to grant your user/group access to this command
    3. Run the _user bus instead of the _system bus

    Don't forget to release the bus when you are done:

    Systemctl::~Systemctl()
    {
        sd_bus_unref(m_bus);
    }
    

    Now you had 3 questions:

    1. Query the status

    For each unit, I have a class which holds the escaped name (foo_2eservice) as m_name, and a reference to the bus in m_bus. Call this method with any property. You seem to be most interested in "ActiveState" or "SubState".

    std::string Unit::GetPropertyString(const std::string& property) const
    {
        sd_bus_error err = SD_BUS_ERROR_NULL;
        char* msg = nullptr;
        int r;
    
        r = sd_bus_get_property_string(m_bus,
            "org.freedesktop.systemd1",
            ("/org/freedesktop/systemd1/unit/" + m_unit).c_str(),
            "org.freedesktop.systemd1.Unit",
            property.c_str(),
            &err,
            &msg);
    
        if (r < 0)
        {
            std::string err_msg(err.message);
            sd_bus_error_free(&err);
    
            std::string err_str("Failed to get " + property + " for service "
                                + m_name + ". Error: " + err_msg);
    
            throw exception(err_str);
        }
    
        sd_bus_error_free(&err);
    
        // Free memory (avoid leaking)
        std::string ret(msg);
        free (msg);
    
        return ret;
    }
    
    1. Monitor the status of a service:

    The first step is to set up a file-descriptor to subscribe to changes. In this case you are interested in subscribing to the "PropertiesChanged" signal. Note that you'll get a signal for any property changing, not just the state. In the sd_bus_add_match() call, there is room for a callback, though I haven't experimented with it.

    void Systemctl::SubscribeToUnitChanges(const std::string& escaped_name)
    {
        /* This function is an easier helper, but it as only introduced in systemd 237
         * Stretch is on 232 while buster is on 241 .  Need re replace this as long as
         * we still support stretch
        sd_bus_match_signal(
            m_bus,
            nullptr, // slot
            nullptr, // sender
            std::string("/org/freedesktop/systemd1/unit/" + escaped_name).c_str(), // path
            "org.freedesktop.DBus.Properties", // interface
            "PropertiesChanged", // member
            nullptr, // callback
            nullptr // userdata
        );
        */
        std::string match =  "type='signal'";
            match += ",path='/org/freedesktop/systemd1/unit/" + escaped_name + "'" ;
            match += ",interface='org.freedesktop.DBus.Properties'";
            match += ",member='PropertiesChanged'";
    
        sd_bus_add_match(
            m_bus,
            nullptr, // slot
            match.c_str(),
            nullptr, // callback
            nullptr // userdata
        );
    }
    

    Instead what I do is periodically poll the bus for the subscribed changes and update each unit:

    bool Systemctl::ProcessBusChanges()
    {
        bool changed = false;
        sd_bus_message* msg = nullptr;
    
        // for each new message
        std::list<std::string> escaped_names;
        while( sd_bus_process(m_bus, &msg) )
        {
            // Note:  Once sd_bus_process returns 0, We are supposed to call
            // sd_bus_wait, or check for changes on sd_bus_get_fd before calling
            // this function again.  We're breaking that rule.  I don't really know
            // the consequences.
            if (msg)
            {
                std::string path = strna( sd_bus_message_get_path(msg) );
                sd_bus_message_unref(msg);
    
                std::string escaped_name = path.erase(0, path.find_last_of('/')+1 );
                escaped_names.push_back(escaped_name);
    
                changed = true;
            }
        }
    
        escaped_names.sort();
        escaped_names.unique();
        for (auto unit : escaped_names)
        {
            auto it = m_units.find(unit);
            if (it != m_units.end())
                it->second.RefreshDynamicProperties();
        }
    
        return changed;
    }
    

    If it tells us that the bus has changed, then I go ahead and read all of my monitored units on that bus.

    1. Change the status

    This one is easy. I use the following, where method is one of "StartUnit", "StopUnit", or "RestartUnit".

    static void CallMethodSS(sd_bus* bus,
                             const std::string& name,
                             const std::string& method)
    {
        sd_bus_error err = SD_BUS_ERROR_NULL;
        sd_bus_message* msg = nullptr;
        int r;
    
        r = sd_bus_call_method(bus,
            "org.freedesktop.systemd1",         /* <service>   */
            "/org/freedesktop/systemd1",        /* <path>      */
            "org.freedesktop.systemd1.Manager", /* <interface> */
            method.c_str(),                     /* <method>    */
            &err,                               /* object to return error in */
            &msg,                               /* return message on success */
            "ss",                               /* <input_signature (string-string)> */
            name.c_str(),  "replace" );         /* <arguments...> */
    
        if (r < 0)
        {
            std::string err_str("Could not send " + method +
                                " command to systemd for service: " + name +
                                ". Error: " + err.message );
    
            sd_bus_error_free(&err);
            sd_bus_message_unref(msg);
            throw exception(err_str);
        }
    
        // Extra stuff that might be useful:  display the response...
        char* response;
        r = sd_bus_message_read(msg, "o", &response);
        if (r < 0)
        {
          LogError("Failed to parse response message: %s\n", strerror(-r) );
        }
    
        sd_bus_error_free(&err);
        sd_bus_message_unref(msg);
    }