Line data Source code
1 : // SPDX-License-Identifier: LGPL-2.1-or-later
2 284 : import cockpit from "cockpit";
3 :
4 : /* SERVICE MANAGEMENT API
5 : *
6 : * The "service" module lets you monitor and manage a
7 : * system service on localhost in a simple way.
8 : *
9 : * It mainly exists because talking to the systemd D-Bus API is
10 : * not trivial enough to do it directly.
11 : *
12 : * - proxy = service.proxy(name)
13 : *
14 : * Create a proxy that represents the service named NAME.
15 : *
16 : * The proxy has properties and methods (described below) that
17 : * allow you to monitor the state of the service, and perform
18 : * simple actions on it.
19 : *
20 : * Initially, any of the properties can be "null" until their
21 : * actual values have been retrieved in the background.
22 : *
23 : * - proxy.addEventListener('changed', event => { ... })
24 : *
25 : * The 'changed' event is emitted whenever one of the properties
26 : * of the proxy changes.
27 : *
28 : * - proxy.exists
29 : *
30 : * A boolean that tells whether the service is known or not. A
31 : * proxy with 'exists == false' will have 'state == undefined' and
32 : * 'enabled == undefined'.
33 : *
34 : * - proxy.state
35 : *
36 : * Either 'undefined' when the state can't be retrieved, or a
37 : * string that has one of the values "starting", "running",
38 : * "stopping", "stopped", or "failed".
39 : *
40 : * - proxy.enabled
41 : *
42 : * Either 'undefined' when the value can't be retrieved, or a
43 : * boolean that tells whether the service is started 'enabled'.
44 : * What it means exactly for a service to be enabled depends on
45 : * the service, but an enabled service is usually started on boot,
46 : * no matter whether other services need it or not. A disabled
47 : * service is usually only started when it is needed by some other
48 : * service.
49 : *
50 : * - proxy.unit
51 : * - proxy.details
52 : *
53 : * The raw org.freedesktop.systemd1.Unit and type-specific D-Bus
54 : * interface proxies for the service.
55 : *
56 : * - proxy.service
57 : *
58 : * The deprecated name for proxy.details
59 : *
60 : * - promise = proxy.start()
61 : *
62 : * Start the service. The return value is a standard jQuery
63 : * promise as returned from DBusClient.call.
64 : *
65 : * - promise = proxy.restart()
66 : *
67 : * Restart the service.
68 : *
69 : * - promise = proxy.tryRestart()
70 : *
71 : * Try to restart the service if it's running or starting
72 : *
73 : * - promise = proxy.stop()
74 : *
75 : * Stop the service.
76 : *
77 : * - promise = proxy.enable()
78 : *
79 : * Enable the service.
80 : *
81 : * - promise = proxy.disable()
82 : *
83 : * Disable the service.
84 : *
85 : * - journal = proxy.getRunJournal(options)
86 : *
87 : * Return the journal of the current (if running) or recent (if failed/stopped) service run,
88 : * similar to `systemctl status`. `options` is an optional array that gets appended to the `journalctl` call.
89 : */
90 :
91 284 : let systemd_client;
92 284 : let systemd_manager;
93 :
94 264 : function wait_valid(proxy, callback) {
95 253 : proxy.wait(() => {
96 253 : if (proxy.valid)
97 253 : callback();
98 253 : });
99 264 : }
100 :
101 264 : function with_systemd_manager(done) {
102 264 : if (!systemd_manager) {
103 : // cached forever, only used for reading/watching; no superuser
104 264 : systemd_client = cockpit.dbus("org.freedesktop.systemd1");
105 264 : systemd_manager = systemd_client.proxy("org.freedesktop.systemd1.Manager",
106 264 : "/org/freedesktop/systemd1");
107 253 : wait_valid(systemd_manager, () => {
108 253 : systemd_manager.Subscribe()
109 20 : .catch(error => {
110 20 : if (error.name != "org.freedesktop.systemd1.AlreadySubscribed" &&
111 3 : error.name != "org.freedesktop.DBus.Error.FileExists")
112 3 : console.warn("Subscribing to systemd signals failed", error);
113 20 : });
114 253 : });
115 264 : }
116 264 : wait_valid(systemd_manager, done);
117 264 : }
118 :
119 264 : export function proxy(name, kind) {
120 264 : const self = {
121 264 : exists: null,
122 264 : state: null,
123 264 : enabled: null,
124 :
125 264 : wait,
126 :
127 264 : start,
128 264 : stop,
129 264 : restart,
130 264 : tryRestart,
131 :
132 264 : enable,
133 264 : disable,
134 :
135 264 : getRunJournal,
136 264 : };
137 :
138 264 : cockpit.event_target(self);
139 :
140 264 : let unit;
141 264 : let details;
142 264 : let wait_promise_resolve;
143 264 : const wait_promise = new Promise(resolve => { wait_promise_resolve = resolve });
144 :
145 264 : if (name.indexOf(".") == -1)
146 176 : name = name + ".service";
147 264 : if (kind === undefined)
148 264 : kind = "Service";
149 :
150 245 : function update_from_unit() {
151 111 : self.exists = (unit.LoadState != "not-found" || unit.ActiveState != "inactive");
152 :
153 245 : if (unit.ActiveState == "activating")
154 64 : self.state = "starting";
155 245 : else if (unit.ActiveState == "deactivating")
156 46 : self.state = "stopping";
157 217 : else if (unit.ActiveState == "active" || unit.ActiveState == "reloading")
158 110 : self.state = "running";
159 217 : else if (unit.ActiveState == "failed")
160 39 : self.state = "failed";
161 217 : else if (unit.ActiveState == "inactive" && self.exists)
162 109 : self.state = "stopped";
163 : else
164 111 : self.state = undefined;
165 :
166 119 : if (unit.UnitFileState == "enabled" || unit.UnitFileState == "linked")
167 107 : self.enabled = true;
168 111 : else if (unit.UnitFileState == "disabled" || unit.UnitFileState == "masked")
169 109 : self.enabled = false;
170 : else
171 111 : self.enabled = undefined;
172 :
173 245 : self.unit = unit;
174 :
175 245 : self.dispatchEvent("changed");
176 245 : wait_promise_resolve();
177 245 : }
178 :
179 239 : function update_from_details() {
180 239 : self.details = details;
181 239 : self.service = details;
182 239 : self.dispatchEvent("changed");
183 239 : }
184 :
185 253 : with_systemd_manager(function () {
186 253 : systemd_manager.LoadUnit(name)
187 248 : .then(path => {
188 248 : unit = systemd_client.proxy('org.freedesktop.systemd1.Unit', path);
189 248 : unit.addEventListener('changed', update_from_unit);
190 248 : wait_valid(unit, update_from_unit);
191 :
192 248 : details = systemd_client.proxy('org.freedesktop.systemd1.' + kind, path);
193 248 : details.addEventListener('changed', update_from_details);
194 248 : wait_valid(details, update_from_details);
195 248 : })
196 0 : .catch(() => {
197 0 : self.exists = false;
198 0 : self.dispatchEvent('changed');
199 0 : });
200 253 : });
201 :
202 82 : function refresh() {
203 82 : if (!unit || !details)
204 4 : return Promise.resolve();
205 :
206 82 : function refresh_interface(path, iface) {
207 82 : return systemd_client.call(path, "org.freedesktop.DBus.Properties", "GetAll", [iface])
208 81 : .then(([result]) => {
209 81 : const props = { };
210 81 : for (const p in result)
211 81 : props[p] = result[p].v;
212 81 : systemd_client.notify({ [unit.path]: { [iface]: props } });
213 81 : })
214 7 : .catch(error => console.log(error));
215 82 : }
216 :
217 82 : return Promise.allSettled([
218 82 : refresh_interface(unit.path, "org.freedesktop.systemd1.Unit"),
219 82 : refresh_interface(details.path, "org.freedesktop.systemd1." + kind),
220 82 : ]);
221 82 : }
222 :
223 134 : function on_job_new_removed_refresh(event, number, path, unit_id, result) {
224 134 : if (unit_id == name)
225 19 : refresh();
226 134 : }
227 :
228 : /* HACK - https://github.com/systemd/systemd/issues/570#issuecomment-125334529
229 : *
230 : * We need to explicitly get new property values when getting
231 : * a UnitNew signal since UnitNew doesn't carry them.
232 : * However, reacting to UnitNew with GetAll could lead to an
233 : * infinite loop since systemd emits a UnitNew in reaction to
234 : * GetAll for units that it doesn't want to keep loaded, such
235 : * as units without unit files.
236 : *
237 : * So we ignore UnitNew and instead assume that the unit state
238 : * only changes in interesting ways when there is a job for it
239 : * or when the daemon is reloaded (or when we get a property
240 : * change notification, of course).
241 : */
242 :
243 : // This is what we want to do:
244 : // systemd_manager.addEventListener("UnitNew", function (event, unit_id, path) {
245 : // if (unit_id == name)
246 : // refresh();
247 : // });
248 :
249 : // This is what we have to do:
250 81 : systemd_manager.addEventListener("Reloading", (event, reloading) => {
251 81 : if (!reloading)
252 81 : refresh();
253 81 : });
254 :
255 264 : systemd_manager.addEventListener("JobNew", on_job_new_removed_refresh);
256 264 : systemd_manager.addEventListener("JobRemoved", on_job_new_removed_refresh);
257 :
258 0 : function wait(callback) {
259 0 : wait_promise.then(callback);
260 0 : }
261 :
262 : /* Actions
263 : *
264 : * We don't call methods on the persistent systemd_client, as that does not have superuser
265 : */
266 :
267 8 : function call_manager(dbus, method, args) {
268 8 : return dbus.call("/org/freedesktop/systemd1",
269 8 : "org.freedesktop.systemd1.Manager",
270 8 : method, args);
271 8 : }
272 :
273 8 : function call_manager_with_job(method, args) {
274 8 : return new Promise((resolve, reject) => {
275 8 : const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
276 8 : let pending_job_path;
277 :
278 8 : const subscription = dbus.subscribe(
279 8 : { interface: "org.freedesktop.systemd1.Manager", member: "JobRemoved" },
280 8 : (_path, _iface, _signal, [_number, path, _unit_id, result]) => {
281 8 : if (path == pending_job_path) {
282 8 : subscription.remove();
283 8 : dbus.close();
284 8 : refresh().then(() => {
285 8 : if (result === "done")
286 0 : resolve();
287 : else
288 0 : reject(new Error(`systemd job ${method} ${JSON.stringify(args)} failed with result ${result}`));
289 8 : });
290 8 : }
291 8 : });
292 :
293 8 : call_manager(dbus, method, args)
294 8 : .then(([path]) => { pending_job_path = path })
295 0 : .catch(ex => {
296 0 : dbus.close();
297 0 : reject(ex);
298 0 : });
299 8 : });
300 8 : }
301 :
302 8 : function call_manager_with_reload(method, args) {
303 8 : const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
304 8 : return call_manager(dbus, method, args)
305 8 : .then(() => call_manager(dbus, "Reload", []))
306 8 : .then(refresh)
307 8 : .finally(dbus.close);
308 8 : }
309 :
310 8 : function start() {
311 8 : return call_manager_with_job("StartUnit", [name, "replace"]);
312 8 : }
313 :
314 5 : function stop() {
315 5 : return call_manager_with_job("StopUnit", [name, "replace"]);
316 5 : }
317 :
318 2 : function restart() {
319 2 : return call_manager_with_job("RestartUnit", [name, "replace"]);
320 2 : }
321 :
322 0 : function tryRestart() {
323 0 : return call_manager_with_job("TryRestartUnit", [name, "replace"]);
324 0 : }
325 :
326 8 : function enable() {
327 8 : return call_manager_with_reload("EnableUnitFiles", [[name], false, false]);
328 8 : }
329 :
330 6 : function disable() {
331 6 : return call_manager_with_reload("DisableUnitFiles", [[name], false]);
332 6 : }
333 :
334 0 : function getRunJournal(options) {
335 0 : if (!details || !details.ExecMainStartTimestamp)
336 0 : return Promise.reject(new Error("getRunJournal(): unit is not known"));
337 :
338 : // collect the service journal since start time; property is μs, journal wants s
339 0 : const startTime = Math.floor(details.ExecMainStartTimestamp / 1000000);
340 0 : return cockpit.spawn(
341 0 : ["journalctl", "--unit", name, "--since=@" + startTime.toString()].concat(options || []),
342 0 : { superuser: "try", error: "message" });
343 0 : }
344 :
345 264 : return self;
346 264 : }
|