summaryrefslogtreecommitdiffstats
path: root/pkg/lib/service.js
blob: 3ef1878f786be6dac50a048157f242c979036fe4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import cockpit from "cockpit";

/* SERVICE MANAGEMENT API
 *
 * 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) {
        // cached forever, only used for reading/watching; no superuser
        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, 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();
    }

    /* HACK - https://bugs.freedesktop.org/show_bug.cgi?id=69575
     *
     * 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).
     */

    // This is what we want to do:
    // systemd_manager.addEventListener("UnitNew", function (event, unit_id, path) {
    //     if (unit_id == name)
    //         refresh();
    // });

    // This is what we have to do:
    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);
    }

    /* Actions
     *
     * 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(() => {
                        dbus.close();
                        reject();
                    });
        });
    }

    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"));

        // collect the service journal since start time; property is μs, journal wants s
        const startTime = Math.floor(details.ExecMainStartTimestamp / 1000000);
        return cockpit.spawn(
            ["journalctl", "--unit", name, "--since=@" + startTime.toString()].concat(options || []),
            { superuser: "try", error: "message" });
    }

    return self;
}