import cockpit from "cockpit";
*
* The "service" module lets you monitor and manage a
* system service on localhost in a simple way.
*
* It mainly exists because talking to the systemd D-Bus API is
* not trivial enough to do it directly.
*
* - proxy = service.proxy(name)
*
* Create a proxy that represents the service named NAME.
*
* The proxy has properties and methods (described below) that
* allow you to monitor the state of the service, and perform
* simple actions on it.
*
* Initially, any of the properties can be "null" until their
* actual values have been retrieved in the background.
*
* - proxy.addEventListener('changed', event => { ... })
*
* The 'changed' event is emitted whenever one of the properties
* of the proxy changes.
*
* - proxy.exists
*
* A boolean that tells whether the service is known or not. A
* proxy with 'exists == false' will have 'state == undefined' and
* 'enabled == undefined'.
*
* - proxy.state
*
* Either 'undefined' when the state can't be retrieved, or a
* string that has one of the values "starting", "running",
* "stopping", "stopped", or "failed".
*
* - proxy.enabled
*
* Either 'undefined' when the value can't be retrieved, or a
* boolean that tells whether the service is started 'enabled'.
* What it means exactly for a service to be enabled depends on
* the service, but an enabled service is usually started on boot,
* no matter whether other services need it or not. A disabled
* service is usually only started when it is needed by some other
* service.
*
* - proxy.unit
* - proxy.details
*
* The raw org.freedesktop.systemd1.Unit and type-specific D-Bus
* interface proxies for the service.
*
* - proxy.service
*
* The deprecated name for proxy.details
*
* - promise = proxy.start()
*
* Start the service. The return value is a standard jQuery
* promise as returned from DBusClient.call.
*
* - promise = proxy.restart()
*
* Restart the service.
*
* - promise = proxy.tryRestart()
*
* Try to restart the service if it's running or starting
*
* - promise = proxy.stop()
*
* Stop the service.
*
* - promise = proxy.enable()
*
* Enable the service.
*
* - promise = proxy.disable()
*
* Disable the service.
*
* - journal = proxy.getRunJournal(options)
*
* Return the journal of the current (if running) or recent (if failed/stopped) service run,
* similar to `systemctl status`. `options` is an optional array that gets appended to the `journalctl` call.
*/
let systemd_client;
let systemd_manager;
function wait_valid(proxy, callback) {
proxy.wait(() => {
if (proxy.valid)
callback();
});
}
function with_systemd_manager(done) {
if (!systemd_manager) {
systemd_client = cockpit.dbus("org.freedesktop.systemd1");
systemd_manager = systemd_client.proxy("org.freedesktop.systemd1.Manager",
"/org/freedesktop/systemd1");
wait_valid(systemd_manager, () => {
systemd_manager.Subscribe()
.catch(error => {
if (error.name !== "org.freedesktop.systemd1.AlreadySubscribed" &&
error.name !== "org.freedesktop.DBus.Error.FileExists")
console.warn("Subscribing to systemd signals failed", error);
});
});
}
wait_valid(systemd_manager, done);
}
export function proxy(name, kind) {
const self = {
exists: null,
state: null,
enabled: null,
wait,
start,
stop,
restart,
tryRestart,
enable,
disable,
getRunJournal,
};
cockpit.event_target(self);
let unit;
let details;
let wait_promise_resolve;
const wait_promise = new Promise(resolve => { wait_promise_resolve = resolve });
if (name.indexOf(".") === -1)
name = name + ".service";
if (kind === undefined)
kind = "Service";
function update_from_unit() {
self.exists = (unit.LoadState !== "not-found" || unit.ActiveState !== "inactive");
if (unit.ActiveState === "activating")
self.state = "starting";
else if (unit.ActiveState === "deactivating")
self.state = "stopping";
else if (unit.ActiveState === "active" || unit.ActiveState === "reloading")
self.state = "running";
else if (unit.ActiveState === "failed")
self.state = "failed";
else if (unit.ActiveState === "inactive" && self.exists)
self.state = "stopped";
else
self.state = undefined;
if (unit.UnitFileState === "enabled" || unit.UnitFileState === "linked")
self.enabled = true;
else if (unit.UnitFileState === "disabled" || unit.UnitFileState === "masked")
self.enabled = false;
else
self.enabled = undefined;
self.unit = unit;
self.dispatchEvent("changed");
wait_promise_resolve();
}
function update_from_details() {
self.details = details;
self.service = details;
self.dispatchEvent("changed");
}
with_systemd_manager(function () {
systemd_manager.LoadUnit(name)
.then(path => {
unit = systemd_client.proxy('org.freedesktop.systemd1.Unit', path);
unit.addEventListener('changed', update_from_unit);
wait_valid(unit, update_from_unit);
details = systemd_client.proxy('org.freedesktop.systemd1.' + kind, path);
details.addEventListener('changed', update_from_details);
wait_valid(details, update_from_details);
})
.catch(() => {
self.exists = false;
self.dispatchEvent('changed');
});
});
function refresh() {
if (!unit || !details)
return Promise.resolve();
function refresh_interface(path, iface) {
return systemd_client.call(path, "org.freedesktop.DBus.Properties", "GetAll", [iface])
.then(([result]) => {
const props = { };
for (const p in result)
props[p] = result[p].v;
systemd_client.notify({ [unit.path]: { [iface]: props } });
})
.catch(error => console.log(error));
}
return Promise.allSettled([
refresh_interface(unit.path, "org.freedesktop.systemd1.Unit"),
refresh_interface(details.path, "org.freedesktop.systemd1." + kind),
]);
}
function on_job_new_removed_refresh(event, number, path, unit_id, result) {
if (unit_id === name)
refresh();
}
*
* We need to explicitly get new property values when getting
* a UnitNew signal since UnitNew doesn't carry them.
* However, reacting to UnitNew with GetAll could lead to an
* infinite loop since systemd emits a UnitNew in reaction to
* GetAll for units that it doesn't want to keep loaded, such
* as units without unit files.
*
* So we ignore UnitNew and instead assume that the unit state
* only changes in interesting ways when there is a job for it
* or when the daemon is reloaded (or when we get a property
* change notification, of course).
*/
systemd_manager.addEventListener("Reloading", (event, reloading) => {
if (!reloading)
refresh();
});
systemd_manager.addEventListener("JobNew", on_job_new_removed_refresh);
systemd_manager.addEventListener("JobRemoved", on_job_new_removed_refresh);
function wait(callback) {
wait_promise.then(callback);
}
*
* We don't call methods on the persistent systemd_client, as that does not have superuser
*/
function call_manager(dbus, method, args) {
return dbus.call("/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
method, args);
}
function call_manager_with_job(method, args) {
return new Promise((resolve, reject) => {
const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
let pending_job_path;
const subscription = dbus.subscribe(
{ interface: "org.freedesktop.systemd1.Manager", member: "JobRemoved" },
(_path, _iface, _signal, [_number, path, _unit_id, result]) => {
if (path === pending_job_path) {
subscription.remove();
dbus.close();
refresh().then(() => {
if (result === "done")
resolve();
else
reject(new Error(`systemd job ${method} ${JSON.stringify(args)} failed with result ${result}`));
});
}
});
call_manager(dbus, method, args)
.then(([path]) => { pending_job_path = path })
.catch(ex => {
dbus.close();
reject(ex);
});
});
}
function call_manager_with_reload(method, args) {
const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
return call_manager(dbus, method, args)
.then(() => call_manager(dbus, "Reload", []))
.then(refresh)
.finally(dbus.close);
}
function start() {
return call_manager_with_job("StartUnit", [name, "replace"]);
}
function stop() {
return call_manager_with_job("StopUnit", [name, "replace"]);
}
function restart() {
return call_manager_with_job("RestartUnit", [name, "replace"]);
}
function tryRestart() {
return call_manager_with_job("TryRestartUnit", [name, "replace"]);
}
function enable() {
return call_manager_with_reload("EnableUnitFiles", [[name], false, false]);
}
function disable() {
return call_manager_with_reload("DisableUnitFiles", [[name], false]);
}
function getRunJournal(options) {
if (!details || !details.ExecMainStartTimestamp)
return Promise.reject(new Error("getRunJournal(): unit is not known"));
const startTime = Math.floor(details.ExecMainStartTimestamp / 1000000);
return cockpit.spawn(
["journalctl", "--unit", name, "--since=@" + startTime.toString()].concat(options || []),
{ superuser: "try", error: "message" });
}
return self;
}